// Package api 提供 REST API 路由与处理器 package api import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "net" "net/http" "os" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "vpn-platform/backend/internal/core" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/service/custom" "vpn-platform/backend/internal/service/ipsec" "vpn-platform/backend/internal/service/openvpn" "vpn-platform/backend/internal/service/wireguard" "vpn-platform/backend/internal/store" ) // Server API 服务器 type Server struct { mgr *core.Manager } func NewServer(mgr *core.Manager) *Server { return &Server{mgr: mgr} } // ---- 简易 Token 会话 ---- var ( sessions = map[string]sessionInfo{} sessionsMu sync.RWMutex ) type sessionInfo struct { username string userID uint role string expireAt time.Time } func issueToken(user *model.User) string { raw := make([]byte, 32) _, _ = rand.Read(raw) token := base64.RawURLEncoding.EncodeToString(raw) sessionsMu.Lock() sessions[token] = sessionInfo{username: user.Username, userID: user.ID, role: user.Role, expireAt: time.Now().Add(24 * time.Hour)} sessionsMu.Unlock() return token } func authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") token = strings.TrimPrefix(token, "Bearer ") sessionsMu.RLock() sess, ok := sessions[token] sessionsMu.RUnlock() if !ok || time.Now().After(sess.expireAt) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录或会话已过期"}) return } c.Set("username", sess.username) c.Set("user_id", sess.userID) c.Set("role", sess.role) c.Next() } } // RegisterRoutes 注册全部路由 func (s *Server) RegisterRoutes(r *gin.Engine) { api := r.Group("/api") api.POST("/login", s.login) api.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) }) auth := api.Group("") auth.Use(authMiddleware()) { auth.GET("/overview", s.overview) auth.GET("/protocols", s.listProtocols) auth.POST("/password", s.changePassword) auth.GET("/mfa/status", s.mfaStatus) auth.POST("/mfa/setup", s.mfaSetup) auth.POST("/mfa/enable", s.mfaEnable) auth.POST("/mfa/disable", s.mfaDisable) admin := auth.Group("") admin.Use(requireRole("admin")) admin.GET("/users", s.listUsers) admin.POST("/users", s.createUser) admin.PUT("/users/:id", s.updateUser) admin.DELETE("/users/:id", s.deleteUser) admin.GET("/ldap", s.getLDAPConfig) admin.PUT("/ldap", s.saveLDAPConfig) admin.POST("/ldap/test", s.testLDAP) admin.GET("/vpn-users", s.listVPNUsers) admin.POST("/vpn-users", s.createVPNUser) admin.PUT("/vpn-users/:id", s.updateVPNUser) admin.DELETE("/vpn-users/:id", s.deleteVPNUser) admin.POST("/tunnels/:id/openvpn/pki", s.initializeOpenVPNPKI) admin.GET("/vpn-users/:id/config", s.downloadVPNUserConfig) admin.GET("/tunnels/:id/wireguard/peers/:peer_id/config", s.downloadWireGuardPeerConfig) auth.GET("/tunnels", s.listTunnels) auth.POST("/tunnels", s.createTunnel) auth.GET("/tunnels/:id", s.getTunnel) auth.PUT("/tunnels/:id", s.updateTunnel) auth.DELETE("/tunnels/:id", s.deleteTunnel) auth.POST("/tunnels/:id/start", s.startTunnel) auth.POST("/tunnels/:id/stop", s.stopTunnel) auth.GET("/tunnels/:id/config", s.downloadConfig) auth.GET("/tunnels/:id/status", s.tunnelStatus) auth.POST("/tunnels/:id/test", s.testConnectivity) auth.GET("/ipsec/templates", s.ipsecTemplates) auth.POST("/ipsec/compare", s.ipsecCompare) auth.GET("/nodes", s.listNodes) auth.POST("/nodes", s.createNode) auth.DELETE("/nodes/:id", s.deleteNode) auth.GET("/plugins", s.listPlugins) auth.POST("/plugins", s.createPlugin) auth.PUT("/plugins/:id", s.updatePlugin) auth.DELETE("/plugins/:id", s.deletePlugin) auth.GET("/audit", s.listAudit) } } func (s *Server) initializeOpenVPNPKI(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } if t.Protocol != "openvpn" { c.JSON(400, gin.H{"error": "仅 OpenVPN 隧道支持 PKI 初始化"}) return } if err := openvpn.New().InitializePKI(t); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "initialize_openvpn_pki", t.Name, "") c.JSON(200, gin.H{"ok": true}) } func (s *Server) downloadVPNUserConfig(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(400, gin.H{"error": "非法 ID"}) return } var user model.VPNUser if err := store.DB.First(&user, id).Error; err != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } t, err := store.GetTunnel(user.TunnelID) if err != nil { c.JSON(404, gin.H{"error": "OpenVPN 隧道不存在"}) return } if t.Protocol != "openvpn" { c.JSON(400, gin.H{"error": "拨号用户不属于 OpenVPN 隧道"}) return } conf, err := openvpn.New().GenerateUserConfig(t, &user) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=openvpn-%s.ovpn", user.Username)) c.Data(200, "application/x-openvpn-profile", []byte(conf)) } func (s *Server) downloadWireGuardPeerConfig(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } if t.Protocol != "wireguard" { c.JSON(400, gin.H{"error": "仅 WireGuard 隧道支持 Peer 配置"}) return } conf, err := wireguard.New().GeneratePeerClientConfig(t, c.Param("peer_id")) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=wireguard-%s-%s.conf", t.Name, c.Param("peer_id"))) c.Data(200, "text/plain; charset=utf-8", []byte(conf)) } // ---- 认证 ---- func (s *Server) login(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` MFACode string `json:"mfa_code"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } user, err := s.authenticateUser(req.Username, req.Password) if err != nil { c.JSON(401, gin.H{"error": "用户名或密码错误"}) return } if !user.Enabled { c.JSON(403, gin.H{"error": "用户已禁用"}) return } if err := verifyMFA(user, req.MFACode); err != nil { c.JSON(401, gin.H{"error": err.Error(), "mfa_required": true}) return } c.JSON(200, gin.H{"token": issueToken(user), "username": user.Username, "role": user.Role}) } func (s *Server) changePassword(c *gin.Context) { var req struct { Old string `json:"old_password"` New string `json:"new_password"` } if err := c.ShouldBindJSON(&req); err != nil || len(req.New) < 6 { c.JSON(400, gin.H{"error": "新密码至少 6 位"}) return } username := c.GetString("username") var user model.User if err := store.DB.Where("username = ?", username).First(&user).Error; err != nil { c.JSON(404, gin.H{"error": "用户不存在"}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Old)); err != nil { c.JSON(400, gin.H{"error": "原密码错误"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.New), bcrypt.DefaultCost) store.DB.Model(&user).Update("password", string(hash)) store.Audit(username, "change_password", username, "修改登录密码") c.JSON(200, gin.H{"ok": true}) } // ---- 总览 ---- func (s *Server) overview(c *gin.Context) { var total, running int64 store.DB.Model(&model.Tunnel{}).Count(&total) store.DB.Model(&model.Tunnel{}).Where("state = ?", "running").Count(&running) var nodeCount, pluginCount int64 store.DB.Model(&model.Node{}).Count(&nodeCount) store.DB.Model(&model.Plugin{}).Where("enabled = ?", true).Count(&pluginCount) // 各协议数量分布 type protoCount struct { Protocol string Count int64 } var dist []protoCount store.DB.Model(&model.Tunnel{}).Select("protocol, count(*) as count").Group("protocol").Scan(&dist) // 总流量 var sum struct { Rx int64 Tx int64 } store.DB.Model(&model.Tunnel{}).Select("COALESCE(SUM(rx_bytes),0) as rx, COALESCE(SUM(tx_bytes),0) as tx").Scan(&sum) // 即将到期(7 天内) var expiring []model.Tunnel week := time.Now().Add(7 * 24 * time.Hour) store.DB.Where("expire_at IS NOT NULL AND expire_at < ?", week).Order("expire_at asc").Limit(5).Find(&expiring) c.JSON(200, gin.H{ "total": total, "running": running, "nodes": nodeCount, "plugins": pluginCount, "rx_bytes": sum.Rx, "tx_bytes": sum.Tx, "protocol_distribution": dist, "expiring": expiring, }) } func (s *Server) listProtocols(c *gin.Context) { type item struct { Name string `json:"name"` DisplayName string `json:"display_name"` Available bool `json:"available"` Dependency string `json:"dependency"` Custom bool `json:"custom"` } var out []item for _, p := range s.mgr.List() { ok, dep := p.Available() out = append(out, item{p.Name(), p.DisplayName(), ok, dep, strings.HasPrefix(p.Name(), "custom:")}) } c.JSON(200, out) } // ---- 专线管理 ---- func (s *Server) listTunnels(c *gin.Context) { var tunnels []model.Tunnel q := store.DB.Preload("Node").Order("id desc") if p := c.Query("protocol"); p != "" { q = q.Where("protocol = ?", p) } if kw := c.Query("keyword"); kw != "" { q = q.Where("name LIKE ?", "%"+kw+"%") } q.Find(&tunnels) // 附带实时状态 type resp struct { model.Tunnel Runtime *core.Status `json:"runtime,omitempty"` } out := make([]resp, 0, len(tunnels)) for _, t := range tunnels { r := resp{Tunnel: t} if st, ok := s.mgr.GetStatus(t.ID); ok { r.Runtime = &st } out = append(out, r) } c.JSON(200, out) } type tunnelReq struct { Name string `json:"name"` Protocol string `json:"protocol"` Remark string `json:"remark"` Config map[string]interface{} `json:"config"` AutoStart bool `json:"auto_start"` ExpireAt *time.Time `json:"expire_at"` NodeID *uint `json:"node_id"` } func (s *Server) createTunnel(c *gin.Context) { var req tunnelReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误: " + err.Error()}) return } if req.Name == "" || req.Protocol == "" { c.JSON(400, gin.H{"error": "名称与协议类型必填"}) return } p, err := s.mgr.Get(req.Protocol) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } cfg, err := p.Validate(req.Config) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } t := model.Tunnel{ Name: req.Name, Protocol: req.Protocol, Remark: req.Remark, State: "stopped", AutoStart: req.AutoStart, ExpireAt: req.ExpireAt, NodeID: req.NodeID, } // 先创建不含配置的记录以取得 ID;敏感字段只在内存中交给协议层, // 由 SaveTunnelConfig 拆分并加密后才会写入数据库。 t.Config = "{}" if err := store.DB.Create(&t).Error; err != nil { c.JSON(500, gin.H{"error": "保存失败: " + err.Error()}) return } raw, _ := json.Marshal(cfg) t.Config = string(raw) if err := p.Create(&t); err != nil { store.DB.Delete(&t) _ = service.RemoveInstance(&t) c.JSON(500, gin.H{"error": "生成配置失败: " + err.Error()}) return } username := c.GetString("username") store.Audit(username, "create_tunnel", fmt.Sprintf("%s(%s)", t.Name, t.Protocol), "") if t.AutoStart { if err := p.Start(&t); err != nil { t.LastError = err.Error() t.State = "error" } else { t.State = "running" } store.DB.Save(&t) } c.JSON(200, t) } func (s *Server) getTunnel(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } // 客户端配置预览 var clientConf string if p, err := s.mgr.Get(t.Protocol); err == nil { clientConf, _ = p.GenerateClientConfig(t) } c.JSON(200, gin.H{"tunnel": t, "client_config": clientConf}) } func (s *Server) updateTunnel(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } var req tunnelReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } var ( p core.Protocol newRaw string hasCfg = req.Config != nil wasRunning = t.State == "running" ) if hasCfg { var err error p, err = s.mgr.Get(t.Protocol) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } cfg, err := p.Validate(req.Config) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } raw, _ := json.Marshal(cfg) newRaw = string(raw) if wasRunning { if err := p.Stop(t); err != nil { c.JSON(500, gin.H{"error": "停止旧配置失败: " + err.Error()}) return } } } if req.Name != "" { t.Name = req.Name } t.Remark = req.Remark t.AutoStart = req.AutoStart t.ExpireAt = req.ExpireAt // 不通过 Save(t) 写入 Config,避免更新请求中的密钥明文落库。 if err := store.DB.Model(t).Updates(map[string]interface{}{ "name": t.Name, "remark": t.Remark, "auto_start": t.AutoStart, "expire_at": t.ExpireAt, }).Error; err != nil { c.JSON(500, gin.H{"error": "保存失败: " + err.Error()}) return } if hasCfg { t.Config = newRaw if err := p.Create(t); err != nil { c.JSON(500, gin.H{"error": "生成配置失败: " + err.Error()}) return } if wasRunning { if err := p.Start(t); err != nil { t.State = "error" t.LastError = err.Error() } else { t.State = "running" t.LastError = "" } store.DB.Model(t).Updates(map[string]interface{}{"state": t.State, "last_error": t.LastError}) } } store.Audit(c.GetString("username"), "update_tunnel", t.Name, "") c.JSON(200, t) } func (s *Server) deleteTunnel(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } if p, err := s.mgr.Get(t.Protocol); err == nil { _ = p.Delete(t) } if err := service.RemoveInstance(t); err != nil && !os.IsNotExist(err) { c.JSON(500, gin.H{"error": "清理实例文件失败: " + err.Error()}) return } s.mgr.RemoveStatus(t.ID) store.DB.Delete(t) store.Audit(c.GetString("username"), "delete_tunnel", t.Name, "") c.JSON(200, gin.H{"ok": true}) } func (s *Server) startTunnel(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } if t.ExpireAt != nil && time.Now().After(*t.ExpireAt) { c.JSON(400, gin.H{"error": "专线已到期,请先调整到期时间"}) return } p, err := s.mgr.Get(t.Protocol) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if t.State == "running" { c.JSON(200, gin.H{"ok": true, "state": "running", "message": "专线已在运行"}) return } if err := p.Start(t); err != nil { t.State = "error" t.LastError = err.Error() store.DB.Save(t) c.JSON(500, gin.H{"error": err.Error()}) return } t.State = "running" t.LastError = "" store.DB.Save(t) store.Audit(c.GetString("username"), "start_tunnel", t.Name, "") c.JSON(200, gin.H{"ok": true, "state": "running"}) } func (s *Server) stopTunnel(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } p, err := s.mgr.Get(t.Protocol) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if err := p.Stop(t); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } t.State = "stopped" store.DB.Save(t) store.Audit(c.GetString("username"), "stop_tunnel", t.Name, "") c.JSON(200, gin.H{"ok": true, "state": "stopped"}) } func (s *Server) downloadConfig(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } p, err := s.mgr.Get(t.Protocol) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } conf, err := p.GenerateClientConfig(t) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } ext := "conf" switch t.Protocol { case "openvpn": ext = "ovpn" case "frp": ext = "toml" case "ipsec", "l2tp": ext = "txt" } filename := fmt.Sprintf("%s-%s.%s", t.Protocol, strings.ReplaceAll(t.Name, " ", "_"), ext) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) c.Data(200, "text/plain; charset=utf-8", []byte(conf)) } func (s *Server) tunnelStatus(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } p, err := s.mgr.Get(t.Protocol) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } st, err := p.Status(t) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, st) } // testConnectivity 一键测试与对端网关的连通性 func (s *Server) testConnectivity(c *gin.Context) { t, err := s.mustTunnel(c) if err != nil { return } cfg, err := store.TunnelConfig(t) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } target := service.Str(cfg, "remote_gateway") if target == "" { target = service.Str(cfg, "server_addr") } if target == "" { target = service.Str(cfg, "public_endpoint") } if host, _, err := net.SplitHostPort(target); err == nil { target = host } if target == "" { c.JSON(400, gin.H{"error": "该专线未配置对端地址,无法测试"}) return } result := map[string]interface{}{"target": target} start := time.Now() // ICMP 不可用时退化为 TCP 常见端口探测 conn, err := net.DialTimeout("tcp", net.JoinHostPort(target, "500"), 3*time.Second) if err == nil { conn.Close() result["ike_udp500_note"] = "TCP 探测通过(仅供参考,IPSec 实际走 UDP 500/4500)" result["reachable"] = true } else { conn2, err2 := net.DialTimeout("tcp", net.JoinHostPort(target, "443"), 3*time.Second) if err2 == nil { conn2.Close() result["reachable"] = true result["note"] = "对端可达(TCP 443)" } else { result["reachable"] = false result["note"] = "TCP 探测未通过,可能防火墙仅放行 UDP,建议结合 IPSec SA 状态判断" } } result["latency_ms"] = time.Since(start).Milliseconds() store.Audit(c.GetString("username"), "test_connectivity", t.Name, fmt.Sprintf("target=%s", target)) c.JSON(200, result) } // ---- IPSec 专用 ---- func (s *Server) ipsecTemplates(c *gin.Context) { c.JSON(200, ipsec.Templates()) } // ipsecCompare 双方参数一致性对比检查 func (s *Server) ipsecCompare(c *gin.Context) { var req struct { Local map[string]interface{} `json:"local"` Peer map[string]interface{} `json:"peer"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } diffs := ipsec.ComparePeerParams(req.Local, req.Peer) c.JSON(200, gin.H{"consistent": len(diffs) == 0, "differences": diffs}) } // ---- 节点管理 ---- func (s *Server) listNodes(c *gin.Context) { var nodes []model.Node store.DB.Find(&nodes) c.JSON(200, nodes) } func (s *Server) createNode(c *gin.Context) { var n model.Node if err := c.ShouldBindJSON(&n); err != nil || n.Name == "" { c.JSON(400, gin.H{"error": "节点名称必填"}) return } n.ID = 0 store.DB.Create(&n) store.Audit(c.GetString("username"), "create_node", n.Name, "") c.JSON(200, n) } func (s *Server) deleteNode(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) store.DB.Delete(&model.Node{}, id) c.JSON(200, gin.H{"ok": true}) } // ---- 自定义协议插件 ---- func (s *Server) listPlugins(c *gin.Context) { var plugins []model.Plugin store.DB.Find(&plugins) c.JSON(200, plugins) } func (s *Server) createPlugin(c *gin.Context) { var p model.Plugin if err := c.ShouldBindJSON(&p); err != nil || p.Name == "" || p.StartHook == "" { c.JSON(400, gin.H{"error": "插件标识与 start 钩子必填"}) return } if strings.ContainsAny(p.Name, " /;|&") { c.JSON(400, gin.H{"error": "插件标识不能包含特殊字符"}) return } p.ID = 0 if p.DisplayName == "" { p.DisplayName = p.Name } if err := store.DB.Create(&p).Error; err != nil { c.JSON(400, gin.H{"error": "插件标识已存在"}) return } if p.Enabled { s.mgr.Register(custom.New(&p)) } store.Audit(c.GetString("username"), "create_plugin", p.Name, "") c.JSON(200, p) } func (s *Server) updatePlugin(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var p model.Plugin if err := store.DB.First(&p, id).Error; err != nil { c.JSON(404, gin.H{"error": "插件不存在"}) return } var req model.Plugin if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } p.DisplayName = req.DisplayName p.Description = req.Description p.CreateHook, p.StartHook, p.StopHook = req.CreateHook, req.StartHook, req.StopHook p.StatusHook, p.ConfigHook = req.StatusHook, req.ConfigHook p.Enabled = req.Enabled store.DB.Save(&p) if p.Enabled { s.mgr.Register(custom.New(&p)) } c.JSON(200, p) } func (s *Server) deletePlugin(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) store.DB.Delete(&model.Plugin{}, id) c.JSON(200, gin.H{"ok": true}) } // ---- 审计 ---- func (s *Server) listAudit(c *gin.Context) { var logs []model.AuditLog store.DB.Order("id desc").Limit(100).Find(&logs) c.JSON(200, logs) } // ---- 工具 ---- func (s *Server) mustTunnel(c *gin.Context) (*model.Tunnel, error) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(400, gin.H{"error": "非法 ID"}) return nil, err } t, err := store.GetTunnel(uint(id)) if err != nil { c.JSON(404, gin.H{"error": "专线不存在"}) return nil, err } return t, nil }