baseline: api+gui traffic mode + candidates picker

Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
This commit is contained in:
beckline
2026-02-14 15:32:25 +03:00
parent 50e2999cad
commit 10a10f44a8
55 changed files with 16488 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
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)
}
}