Files
opsx-vpn/backend/main.go
T
opsx ef7eb378d2 feat: 新增 GRE 隧道协议,管理端口调整为 18080
- backend: 新增 GRE 隧道实现(ip tunnel 点对点互联),binmgr 支持 iproute2
- backend: api/identity 调整,各协议 service(gre/ikev2/ipsec/l2tp/openvpn)统一改动
- backend: OpenVPN 认证辅助与用户口令同步逻辑完善
- frontend: 协议图标/向导/编辑器支持 GRE,VPN 用户页重构(234 行)
- frontend: 新增 RouteListInput 路由列表输入组件,Dashboard/Layout 优化
- i18n: 中英文文案同步更新
- test: 新增 binmgr/common/gre 单测,openvpn 测试补充
- docs: README 补充 GRE 说明,默认监听端口 8080 → 18080
- chore: .gitignore 增加 opsx-vpn.previous(构建备份二进制)
2026-10-20 20:58:58 +08:00

168 lines
4.6 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 (
"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/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"
)
//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(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)
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))
}
}