782 lines
21 KiB
Go
782 lines
21 KiB
Go
// 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
|
||
}
|