205 lines
7.5 KiB
Go
205 lines
7.5 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"syscall"
|
||
"time"
|
||
)
|
||
|
||
// ---------------------------------------------------------------------
|
||
// main + общие хелперы
|
||
// ---------------------------------------------------------------------
|
||
|
||
// EN: Application entrypoint and process bootstrap.
|
||
// EN: This file wires CLI modes, registers all HTTP routes, and starts background
|
||
// EN: watchers plus the localhost API server.
|
||
// RU: Точка входа приложения и bootstrap процесса.
|
||
// RU: Этот файл связывает CLI-режимы, регистрирует все HTTP-маршруты и запускает
|
||
// RU: фоновые вотчеры вместе с локальным API-сервером.
|
||
func Run() {
|
||
// ---------------------------------------------------------------------
|
||
// CLI modes
|
||
// ---------------------------------------------------------------------
|
||
|
||
// CLI mode: routes-update
|
||
if len(os.Args) > 1 && (os.Args[1] == "routes-update" || os.Args[1] == "-routes-update") {
|
||
fs := flag.NewFlagSet("routes-update", flag.ExitOnError)
|
||
iface := fs.String("iface", "", "VPN interface (empty/auto = detect active)")
|
||
_ = fs.Parse(os.Args[2:])
|
||
lock, err := os.OpenFile(lockFile, os.O_CREATE|os.O_RDWR, 0o644)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "lock open error: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
||
fmt.Println("routes update already running")
|
||
lock.Close()
|
||
return
|
||
}
|
||
res := routesUpdate(*iface)
|
||
_ = syscall.Flock(int(lock.Fd()), syscall.LOCK_UN)
|
||
_ = lock.Close()
|
||
if res.OK {
|
||
fmt.Println(res.Message)
|
||
return
|
||
}
|
||
fmt.Fprintln(os.Stderr, res.Message)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// CLI mode: routes-clear
|
||
if len(os.Args) > 1 && os.Args[1] == "routes-clear" {
|
||
res := routesClear()
|
||
if res.OK {
|
||
fmt.Println(res.Message)
|
||
return
|
||
}
|
||
fmt.Fprintln(os.Stderr, res.Message)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// CLI mode: autoloop
|
||
if len(os.Args) > 1 && os.Args[1] == "autoloop" {
|
||
fs := flag.NewFlagSet("autoloop", flag.ExitOnError)
|
||
iface := fs.String("iface", "", "VPN interface (empty/auto = detect active)")
|
||
table := fs.String("table", "agvpn", "routing table name")
|
||
mtu := fs.Int("mtu", 1380, "MTU for default route")
|
||
stateDirFlag := fs.String("state-dir", stateDir, "state directory")
|
||
defaultLoc := fs.String("default-location", "Austria", "default location")
|
||
_ = fs.Parse(os.Args[2:])
|
||
resolvedIface := normalizePreferredIface(*iface)
|
||
if resolvedIface == "" {
|
||
resolvedIface, _ = resolveTrafficIface(loadTrafficModeState().PreferredIface)
|
||
}
|
||
if resolvedIface == "" {
|
||
fmt.Fprintln(os.Stderr, "autoloop: cannot resolve VPN interface (set --iface or preferred iface)")
|
||
os.Exit(1)
|
||
}
|
||
runAutoloop(resolvedIface, *table, *mtu, *stateDirFlag, *defaultLoc)
|
||
return
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// API server bootstrap
|
||
// ---------------------------------------------------------------------
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
|
||
ensureSeeds()
|
||
|
||
mux := http.NewServeMux()
|
||
|
||
// ---------------------------------------------------------------------
|
||
// route registration
|
||
// ---------------------------------------------------------------------
|
||
|
||
// health
|
||
mux.HandleFunc("/healthz", handleHealthz)
|
||
// event stream (SSE)
|
||
mux.HandleFunc("/api/v1/events/stream", handleEventsStream)
|
||
|
||
// статус selective-routes
|
||
mux.HandleFunc("/api/v1/status", handleGetStatus)
|
||
mux.HandleFunc("/api/v1/routes/status", handleGetStatus)
|
||
|
||
// login state
|
||
mux.HandleFunc("/api/v1/vpn/login-state", handleVPNLoginState)
|
||
|
||
// systemd state
|
||
mux.HandleFunc("/api/v1/systemd/state", handleSystemdState)
|
||
|
||
// сервис selective-routes
|
||
mux.HandleFunc("/api/v1/routes/service/start",
|
||
makeRoutesServiceActionHandler("start"))
|
||
mux.HandleFunc("/api/v1/routes/service/stop",
|
||
makeRoutesServiceActionHandler("stop"))
|
||
mux.HandleFunc("/api/v1/routes/service/restart",
|
||
makeRoutesServiceActionHandler("restart"))
|
||
// универсальный: {"action":"start|stop|restart"}
|
||
mux.HandleFunc("/api/v1/routes/service", handleRoutesService)
|
||
// ручной апдейт маршрутов (Go-реализация вместо bash)
|
||
mux.HandleFunc("/api/v1/routes/update", handleRoutesUpdate)
|
||
|
||
// таймер маршрутов (новый API)
|
||
mux.HandleFunc("/api/v1/routes/timer", handleRoutesTimer)
|
||
// старый toggle для совместимости
|
||
mux.HandleFunc("/api/v1/routes/timer/toggle", handleRoutesTimerToggle)
|
||
|
||
// rollback / clear (Go implementation)
|
||
mux.HandleFunc("/api/v1/routes/rollback", handleRoutesClear)
|
||
// alias: /routes/clear
|
||
mux.HandleFunc("/api/v1/routes/clear", handleRoutesClear)
|
||
// fast restore from clear-cache
|
||
mux.HandleFunc("/api/v1/routes/cache/restore", handleRoutesCacheRestore)
|
||
|
||
// фиксим policy route
|
||
mux.HandleFunc("/api/v1/routes/fix-policy-route", handleFixPolicyRoute)
|
||
mux.HandleFunc("/api/v1/routes/fix-policy", handleFixPolicyRoute)
|
||
mux.HandleFunc("/api/v1/traffic/mode", handleTrafficMode)
|
||
mux.HandleFunc("/api/v1/traffic/mode/test", handleTrafficModeTest)
|
||
mux.HandleFunc("/api/v1/traffic/interfaces", handleTrafficInterfaces)
|
||
mux.HandleFunc("/api/v1/traffic/candidates", handleTrafficCandidates)
|
||
|
||
// trace: хвост + JSON + append для GUI
|
||
mux.HandleFunc("/api/v1/trace", handleTraceTailPlain)
|
||
mux.HandleFunc("/api/v1/trace-json", handleTraceJSON)
|
||
mux.HandleFunc("/api/v1/trace/append", handleTraceAppend)
|
||
|
||
// DNS upstreams
|
||
mux.HandleFunc("/api/v1/dns-upstreams", handleDNSUpstreams)
|
||
mux.HandleFunc("/api/v1/dns/status", handleDNSStatus)
|
||
mux.HandleFunc("/api/v1/dns/mode", handleDNSModeSet)
|
||
mux.HandleFunc("/api/v1/dns/smartdns-service", handleDNSSmartdnsService)
|
||
|
||
// SmartDNS service
|
||
mux.HandleFunc("/api/v1/smartdns/service", handleSmartdnsService)
|
||
mux.HandleFunc("/api/v1/smartdns/runtime", handleSmartdnsRuntime)
|
||
mux.HandleFunc("/api/v1/smartdns/prewarm", handleSmartdnsPrewarm)
|
||
|
||
// domains editor
|
||
mux.HandleFunc("/api/v1/domains/table", handleDomainsTable)
|
||
mux.HandleFunc("/api/v1/domains/file", handleDomainsFile)
|
||
|
||
// SmartDNS wildcards
|
||
mux.HandleFunc("/api/v1/smartdns/wildcards", handleSmartdnsWildcards)
|
||
|
||
// AdGuard VPN: status + autoloop + autoconnect + locations
|
||
mux.HandleFunc("/api/v1/vpn/autoloop-status", handleVPNAutoloopStatus)
|
||
mux.HandleFunc("/api/v1/vpn/status", handleVPNStatus)
|
||
mux.HandleFunc("/api/v1/vpn/autoconnect", handleVPNAutoconnect)
|
||
mux.HandleFunc("/api/v1/vpn/locations", handleVPNListLocations)
|
||
mux.HandleFunc("/api/v1/vpn/location", handleVPNSetLocation)
|
||
|
||
// AdGuard VPN: interactive login session (PTY)
|
||
mux.HandleFunc("/api/v1/vpn/login/session/start", handleVPNLoginSessionStart)
|
||
mux.HandleFunc("/api/v1/vpn/login/session/state", handleVPNLoginSessionState)
|
||
mux.HandleFunc("/api/v1/vpn/login/session/action", handleVPNLoginSessionAction)
|
||
mux.HandleFunc("/api/v1/vpn/login/session/stop", handleVPNLoginSessionStop)
|
||
// logout
|
||
mux.HandleFunc("/api/v1/vpn/logout", handleVPNLogout)
|
||
|
||
// ---------------------------------------------------------------------
|
||
// HTTP server
|
||
// ---------------------------------------------------------------------
|
||
|
||
srv := &http.Server{
|
||
Addr: "127.0.0.1:8080",
|
||
Handler: logRequests(mux),
|
||
ReadHeaderTimeout: 5 * time.Second,
|
||
}
|
||
|
||
go startWatchers(ctx)
|
||
|
||
log.Printf("selective-vpn API listening on %s", srv.Addr)
|
||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||
log.Fatalf("server error: %v", err)
|
||
}
|
||
}
|