178 lines
5.0 KiB
Go
178 lines
5.0 KiB
Go
// 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))
|
||
}
|
||
}
|