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(构建备份二进制)
This commit is contained in:
opsx committed 2026-10-20 20:58:58 +08:00
1 parent c7f59f2cbf
commit ef7eb378d2
28 files changed
+1363 -439

No files matched your search

+120 -68
View File
@@ -31,14 +31,14 @@ import (
"vpn-platform/backend/internal/sessionmgr"
"vpn-platform/backend/internal/sshutil"
"vpn-platform/backend/internal/store"
"vpn-platform/backend/internal/trafficmgr"
"vpn-platform/backend/internal/taskmgr"
"vpn-platform/backend/internal/trafficmgr"
)
// Server API 服务器
type Server struct {
mgr *core.Manager
taskMgr *taskmgr.Manager
mgr *core.Manager
taskMgr *taskmgr.Manager
}
func NewServer(mgr *core.Manager) *Server { return &Server{mgr: mgr, taskMgr: taskmgr.NewManager()} }
@@ -227,6 +227,7 @@ func (s *Server) RegisterRoutes(r *gin.Engine) {
// 只读
auth.GET("/overview", s.overview)
auth.GET("/readiness", s.readiness)
auth.GET("/protocols", s.listProtocols)
auth.GET("/tunnels", s.listTunnels)
auth.GET("/tunnels/:id", s.getTunnel)
@@ -292,6 +293,8 @@ func (s *Server) RegisterRoutes(r *gin.Engine) {
admin.POST("/sessions/:id/disconnect", s.disconnectSession)
admin.GET("/vpn-users", s.listVPNUsers)
admin.POST("/vpn-users", s.createVPNUser)
admin.POST("/vpn-users/import-platform", s.importPlatformVPNUsers)
admin.POST("/vpn-users/batch-delete", s.batchDeleteVPNUsers)
admin.PUT("/vpn-users/:id", s.updateVPNUser)
admin.DELETE("/vpn-users/:id", s.deleteVPNUser)
admin.POST("/vpn-users/:id/mfa/setup", s.vpnUserMFASetup)
@@ -600,11 +603,26 @@ func (s *Server) overview(c *gin.Context) {
c.JSON(200, gin.H{
"total": total, "running": running, "nodes": nodeCount, "plugins": pluginCount,
"online_sessions": sessionmgr.CountActive(),
"rx_bytes": sum.Rx, "tx_bytes": sum.Tx,
"rx_bytes": sum.Rx, "tx_bytes": sum.Tx,
"protocol_distribution": dist, "expiring": expiring,
})
}
// readiness reports host prerequisites that directly affect VPN forwarding.
func (s *Server) readiness(c *gin.Context) {
forwarding := false
if raw, err := os.ReadFile("/proc/sys/net/ipv4/ip_forward"); err == nil {
forwarding = strings.TrimSpace(string(raw)) == "1"
}
iface, ifaceErr := service.DefaultRouteInterface()
c.JSON(200, gin.H{
"root": os.Geteuid() == 0,
"ipv4_forwarding": forwarding,
"default_interface": iface,
"default_route_ready": ifaceErr == nil,
})
}
func (s *Server) listProtocols(c *gin.Context) {
type item struct {
Name string `json:"name"`
@@ -836,11 +854,14 @@ func (s *Server) getTunnel(c *gin.Context) {
return
}
// 客户端配置预览
var clientConf string
var clientConf, clientConfError string
if p, err := s.mgr.Get(t.Protocol); err == nil {
clientConf, _ = p.GenerateClientConfig(t)
clientConf, err = p.GenerateClientConfig(t)
if err != nil {
clientConfError = err.Error()
}
}
c.JSON(200, gin.H{"tunnel": t, "client_config": clientConf})
c.JSON(200, gin.H{"tunnel": t, "client_config": clientConf, "client_config_error": clientConfError})
}
func (s *Server) updateTunnel(c *gin.Context) {
@@ -855,63 +876,94 @@ func (s *Server) updateTunnel(c *gin.Context) {
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()})
hasCfg := req.Config != nil
if !hasCfg {
if req.Name != "" {
t.Name = req.Name
}
t.Remark, t.AutoStart, t.ExpireAt = req.Remark, req.AutoStart, req.ExpireAt
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
}
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()})
store.Audit(c.GetString("username"), "update_tunnel", t.Name, "")
c.JSON(200, t)
return
}
if hasCfg {
t.Config = newRaw
if err := p.Create(t); err != nil {
c.JSON(500, gin.H{"error": "生成配置失败: " + err.Error()})
return
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)
original := *t
candidate := original
candidate.Config = string(raw)
if req.Name != "" {
candidate.Name = req.Name
}
candidate.Remark, candidate.AutoStart, candidate.ExpireAt = req.Remark, req.AutoStart, req.ExpireAt
wasRunning := original.State == "running"
backup, err := service.SnapshotInstance(&original)
if err != nil {
c.JSON(500, gin.H{"error": "无法备份旧配置,已取消更新: " + err.Error()})
return
}
defer service.CleanupInstanceSnapshot(backup)
rollback := func(cause error) error {
_ = service.RestoreInstanceSnapshot(&original, backup)
if err := store.DB.Save(&original).Error; err != nil {
return fmt.Errorf("恢复数据库配置失败: %w", err)
}
if wasRunning {
if err := p.Start(t); err != nil {
t.State = "error"
t.LastError = err.Error()
} else {
t.State = "running"
t.LastError = ""
if err := p.Start(&original); err != nil {
return fmt.Errorf("恢复旧服务失败: %w", err)
}
store.DB.Model(t).Updates(map[string]interface{}{"state": t.State, "last_error": t.LastError})
}
*t = original
return cause
}
if wasRunning {
if err := p.Stop(&original); err != nil {
c.JSON(500, gin.H{"error": "停止旧配置失败: " + err.Error()})
return
}
}
if err := p.Create(&candidate); err != nil {
if restoreErr := rollback(err); restoreErr != err {
c.JSON(500, gin.H{"error": "生成新配置失败,且回滚异常: " + restoreErr.Error()})
return
}
c.JSON(500, gin.H{"error": "生成新配置失败,已恢复旧配置: " + err.Error()})
return
}
if wasRunning {
if err := p.Start(&candidate); err != nil {
if restoreErr := rollback(err); restoreErr != err {
c.JSON(500, gin.H{"error": "启动新配置失败,且回滚异常: " + restoreErr.Error()})
return
}
c.JSON(500, gin.H{"error": "启动新配置失败,已恢复旧服务: " + err.Error()})
return
}
candidate.State, candidate.LastError = "running", ""
if err := store.DB.Model(&candidate).Updates(map[string]interface{}{"state": candidate.State, "last_error": ""}).Error; err != nil {
_ = rollback(err)
c.JSON(500, gin.H{"error": "保存运行状态失败,已恢复旧服务: " + err.Error()})
return
}
}
*t = candidate
store.Audit(c.GetString("username"), "update_tunnel", t.Name, "")
c.JSON(200, t)
}
@@ -1063,11 +1115,11 @@ func (s *Server) tunnelTrafficHistory(c *gin.Context) {
since := time.Now().Add(-time.Duration(hours) * time.Hour)
type row struct {
Time time.Time `json:"time"`
Rx int64 `json:"rx"`
Tx int64 `json:"tx"`
DeltaRx int64 `json:"delta_rx"`
DeltaTx int64 `json:"delta_tx"`
Time time.Time `json:"time"`
Rx int64 `json:"rx"`
Tx int64 `json:"tx"`
DeltaRx int64 `json:"delta_rx"`
DeltaTx int64 `json:"delta_tx"`
}
var results []row
@@ -1338,15 +1390,15 @@ func (s *Server) ipsecCompare(c *gin.Context) {
// nodeReq 节点创建/更新请求(含明文凭证,仅传输用,不入库明文)
type nodeReq struct {
Name string `json:"name"`
Address string `json:"address"`
Port int `json:"port"`
Kind string `json:"kind"`
Username string `json:"username"`
AuthType string `json:"auth_type"`
Password string `json:"password"`
Name string `json:"name"`
Address string `json:"address"`
Port int `json:"port"`
Kind string `json:"kind"`
Username string `json:"username"`
AuthType string `json:"auth_type"`
Password string `json:"password"`
PrivateKey string `json:"private_key"`
Remark string `json:"remark"`
Remark string `json:"remark"`
}
// nodeTestReq 临时连接测试请求(不落库)