// VPN 管理平台 —— 单二进制交付(前端静态资源内嵌) // Logan.Li package main import ( "embed" "io/fs" "log" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" "vpn-platform/backend/internal/api" "vpn-platform/backend/internal/core" "vpn-platform/backend/internal/monitor" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/service/custom" "vpn-platform/backend/internal/service/frp" "vpn-platform/backend/internal/service/ikev2" "vpn-platform/backend/internal/service/ipsec" "vpn-platform/backend/internal/service/l2tp" "vpn-platform/backend/internal/service/openvpn" "vpn-platform/backend/internal/service/wireguard" "vpn-platform/backend/internal/store" ) //go:embed all:dist var webFS embed.FS func main() { gin.SetMode(gin.ReleaseMode) // 数据目录:默认 ./data,可用 VPN_DATA_DIR 覆盖 dataDir := service.BaseDir() if err := store.Init(dataDir); err != nil { log.Fatalf("初始化存储失败: %v", err) } // OpenVPN invokes this same, already-installed binary from its // auth-user-pass-verify hook. Keep it before HTTP startup so the hook has a // short-lived process and a meaningful exit status. if len(os.Args) > 1 && os.Args[1] == "openvpn-auth" { if err := openvpn.RunAuthHelper(os.Args[2:]); err != nil { log.Printf("OpenVPN 用户认证失败: %v", err) os.Exit(1) } return } // 注册内置协议 mgr := core.NewManager() mgr.Register(wireguard.New()) mgr.Register(ipsec.New()) mgr.Register(ikev2.New()) mgr.Register(l2tp.New()) mgr.Register(frp.New()) mgr.Register(openvpn.New()) // 加载数据库中的自定义协议插件 custom.LoadAll(mgr) r := gin.New() r.Use(gin.Recovery()) srv := api.NewServer(mgr) srv.RegisterRoutes(r) // 内嵌前端(SPA 回退到 index.html) dist, err := fs.Sub(webFS, "dist") if err != nil { log.Fatalf("内嵌资源损坏: %v", err) } fileServer := http.FileServer(http.FS(dist)) r.NoRoute(func(c *gin.Context) { path := strings.TrimPrefix(c.Request.URL.Path, "/") if path == "" { path = "index.html" } if f, err := dist.Open(path); err == nil { f.Close() // 带 hash 的静态资源(assets/*)可长期缓存;index.html 必须每次重新验证 if strings.HasPrefix(path, "assets/") { c.Header("Cache-Control", "public, max-age=31536000, immutable") } else { c.Header("Cache-Control", "no-cache") } fileServer.ServeHTTP(c.Writer, c.Request) return } // SPA fallback — 必须 no-cache,否则浏览器缓存旧 index.html 会白屏 c.Header("Cache-Control", "no-cache") c.Request.URL.Path = "/" fileServer.ServeHTTP(c.Writer, c.Request) }) // 状态采集 + 自启恢复 monitor.Start(mgr, 15*time.Second) monitor.AutoStartAll(mgr) addr := os.Getenv("VPN_LISTEN_ADDR") if addr == "" { addr = ":8080" } log.Printf("VPN 管理平台已启动: http://localhost%s (默认账号 admin / admin123)", addr) log.Printf("协议依赖检测:") for _, p := range mgr.List() { ok, dep := p.Available() mark := "✓" if !ok { mark = "✗" } log.Printf(" [%s] %-10s %s", mark, p.Name(), dep) } if err := r.Run(addr); err != nil { log.Fatal(err) } }