Files
opsx-vpn/backend/main.go
T

178 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VPN 管理平台 —— 单二进制交付(前端静态资源内嵌) // Logan.Li
package main
import (
"context"
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"vpn-platform/backend/internal/api"
"vpn-platform/backend/internal/core"
"vpn-platform/backend/internal/identityprovider"
"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/gre"
"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"
"vpn-platform/backend/internal/tlsservice"
)
//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
}
identityprovider.StartDirectoryScheduler(context.Background())
// 注册内置协议
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(gre.New())
mgr.Register(openvpn.New())
// 加载数据库中的自定义协议插件
custom.LoadAll(mgr)
r := gin.New()
// HTTP 访问日志(带轮转,默认 20MB x 3 文件)
r.Use(accessLogger())
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)
})
// 启动时自动清理过期日志(默认保留 90 天,可通过 VPN_LOG_RETENTION_DAYS 覆盖)
retentionDays := 90
if v := os.Getenv("VPN_LOG_RETENTION_DAYS"); v != "" {
if d, err := strconv.Atoi(v); err == nil && d > 0 {
retentionDays = d
}
}
api.AutoCleanupLogs(retentionDays)
// 状态采集 + 自启恢复
monitor.Start(mgr, 15*time.Second)
monitor.AutoStartAll(mgr)
if err := tlsservice.Start(r, dataDir); err != nil {
log.Fatalf("HTTPS/ACME 启动失败: %v", err)
}
if err := srv.RefreshManagedGatewayOnStart(); err != nil {
log.Printf("Nginx 路由白名单同步失败,仍可通过本机入口检查: %v", err)
}
addr := os.Getenv("VPN_LISTEN_ADDR")
if addr == "" {
addr = ":18080"
}
log.Printf("VPN 管理平台已启动: http://localhost%s", 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)
}
}
// accessLogger 返回自定义的 HTTP 请求日志中间件(带日志轮转)
// 格式: [2006-01-02 15:04:05] "GET /api/tunnels 200 12.5ms 192.168.1.1"
func accessLogger() gin.HandlerFunc {
// 构造日志文件路径
dataDir := service.BaseDir()
logPath := fmt.Sprintf("%s/access.log", dataDir)
// 轮转配置:默认 20MB x 3 文件,可通过环境变量覆盖
cfg := service.DefaultRotateConfig("VPN_ACCESS_LOG_MAX_SIZE", 20*1024*1024, 3)
writer, err := service.NewRotatingWriter(logPath, cfg)
if err != nil {
// 降级到 stderr
log.Printf("警告: 无法创建访问日志文件 %s: %v,降级到标准输出", logPath, err)
return gin.Logger()
}
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
line := fmt.Sprintf("[%s] %s %s %d %s %s\n",
time.Now().Format("2006-01-02 15:04:05"),
c.Request.Method,
path,
status,
latency.Round(time.Millisecond),
c.ClientIP(),
)
_, _ = writer.Write([]byte(line))
}
}