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:
1 parent
c7f59f2cbf
commit
ef7eb378d2
28 files changed
+1363
-439
No files matched your search
+120
-68
@@ -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 临时连接测试请求(不落库)
|
||||
|
||||
@@ -419,16 +419,17 @@ func (s *Server) listVPNUsers(c *gin.Context) {
|
||||
}
|
||||
func (s *Server) createVPNUser(c *gin.Context) {
|
||||
var req struct {
|
||||
TunnelID uint `json:"tunnel_id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Source string `json:"source"`
|
||||
LDAPDN string `json:"ldap_dn"`
|
||||
Password string `json:"password"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
TunnelID uint `json:"tunnel_id"`
|
||||
PlatformUserID uint `json:"platform_user_id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Source string `json:"source"`
|
||||
LDAPDN string `json:"ldap_dn"`
|
||||
Password string `json:"password"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil || req.TunnelID == 0 || req.Username == "" {
|
||||
if c.ShouldBindJSON(&req) != nil || req.TunnelID == 0 || (req.Username == "" && req.PlatformUserID == 0) {
|
||||
c.JSON(400, gin.H{"error": "隧道和用户名必填"})
|
||||
return
|
||||
}
|
||||
@@ -439,6 +440,12 @@ func (s *Server) createVPNUser(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "来源必须为 local 或 ldap"})
|
||||
return
|
||||
}
|
||||
// 手工创建仅支持独立本地拨号账号。LDAP 身份必须从用户管理批量导入,
|
||||
// 这样可以复用平台身份源,并由服务端查找、固定对应的 LDAP DN。
|
||||
if req.PlatformUserID == 0 && req.Source == "ldap" {
|
||||
c.JSON(400, gin.H{"error": "LDAP 用户请通过“批量导入”从用户管理导入"})
|
||||
return
|
||||
}
|
||||
t, err := store.GetTunnel(req.TunnelID)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "隧道不存在"})
|
||||
@@ -450,20 +457,55 @@ func (s *Server) createVPNUser(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "该协议不支持拨号用户(支持 OpenVPN / L2TP / IKEv2)"})
|
||||
return
|
||||
}
|
||||
if req.PlatformUserID != 0 {
|
||||
if t.Protocol != "openvpn" {
|
||||
c.JSON(400, gin.H{"error": "平台用户导入仅支持 OpenVPN;L2TP/IKEv2 需要独立拨号密码"})
|
||||
return
|
||||
}
|
||||
var account model.User
|
||||
if err := store.DB.First(&account, req.PlatformUserID).Error; err != nil {
|
||||
c.JSON(404, gin.H{"error": "平台用户不存在"})
|
||||
return
|
||||
}
|
||||
if !account.Enabled {
|
||||
c.JSON(400, gin.H{"error": "平台用户已禁用,无法导入"})
|
||||
return
|
||||
}
|
||||
req.Username, req.Email, req.Source = account.Username, account.Email, account.Source
|
||||
if req.DisplayName == "" {
|
||||
req.DisplayName = account.Username
|
||||
}
|
||||
if req.Source == "ldap" {
|
||||
dn, err := findLDAPUserDN(account.Username)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "LDAP 平台用户导入失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
req.LDAPDN = dn
|
||||
}
|
||||
}
|
||||
user := model.VPNUser{TunnelID: req.TunnelID, Username: req.Username, DisplayName: req.DisplayName, Email: req.Email, Source: req.Source, LDAPDN: req.LDAPDN, Enabled: true}
|
||||
if req.Enabled != nil {
|
||||
user.Enabled = *req.Enabled
|
||||
}
|
||||
if user.Source == "local" {
|
||||
if len(req.Password) < 8 {
|
||||
if req.PlatformUserID != 0 {
|
||||
var account model.User
|
||||
if err := store.DB.First(&account, req.PlatformUserID).Error; err != nil || account.Password == "" {
|
||||
c.JSON(400, gin.H{"error": "平台本地用户密码不可用"})
|
||||
return
|
||||
}
|
||||
user.Password = account.Password
|
||||
} else if len(req.Password) < 8 {
|
||||
c.JSON(400, gin.H{"error": "本地拨号用户密码至少 8 位"})
|
||||
return
|
||||
}
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
user.Password = string(hash)
|
||||
// L2TP/IKEv2 需要可逆拨号密钥;OpenVPN 仅用 bcrypt,同时存一份便于统一导出说明。
|
||||
if enc, err := store.EncryptSecret(req.Password); err == nil {
|
||||
user.DialSecret = enc
|
||||
} else {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
user.Password = string(hash)
|
||||
// L2TP/IKEv2 需要可逆拨号密钥;OpenVPN 仅用 bcrypt,同时存一份便于统一导出说明。
|
||||
if enc, err := store.EncryptSecret(req.Password); err == nil {
|
||||
user.DialSecret = enc
|
||||
}
|
||||
}
|
||||
} else if user.LDAPDN == "" {
|
||||
c.JSON(400, gin.H{"error": "LDAP 拨号用户必须填写 LDAP DN"})
|
||||
@@ -476,6 +518,114 @@ func (s *Server) createVPNUser(c *gin.Context) {
|
||||
_ = refreshDialAuth(t)
|
||||
c.JSON(200, user)
|
||||
}
|
||||
|
||||
// importPlatformVPNUsers imports selected platform local/LDAP accounts into an
|
||||
// OpenVPN tunnel. L2TP/IKEv2 use CHAP/EAP and require a dedicated dial-in
|
||||
// secret, so they intentionally remain manual-user only.
|
||||
func (s *Server) importPlatformVPNUsers(c *gin.Context) {
|
||||
var req struct {
|
||||
TunnelID uint `json:"tunnel_id"`
|
||||
PlatformUserIDs []uint `json:"platform_user_ids"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil || req.TunnelID == 0 || len(req.PlatformUserIDs) == 0 {
|
||||
c.JSON(400, gin.H{"error": "请选择目标 OpenVPN 隧道和至少一个平台用户"})
|
||||
return
|
||||
}
|
||||
t, err := store.GetTunnel(req.TunnelID)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "隧道不存在"})
|
||||
return
|
||||
}
|
||||
if t.Protocol != "openvpn" {
|
||||
c.JSON(400, gin.H{"error": "平台用户批量导入仅支持 OpenVPN;L2TP/IKEv2 请创建独立拨号账号"})
|
||||
return
|
||||
}
|
||||
var accounts []model.User
|
||||
if err := store.DB.Where("id IN ? AND enabled = ?", req.PlatformUserIDs, true).Find(&accounts).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "读取平台用户失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
created, skipped := 0, []string{}
|
||||
for _, account := range accounts {
|
||||
var exists int64
|
||||
store.DB.Model(&model.VPNUser{}).Where("tunnel_id = ? AND username = ?", t.ID, account.Username).Count(&exists)
|
||||
if exists > 0 {
|
||||
skipped = append(skipped, account.Username+"(已存在)")
|
||||
continue
|
||||
}
|
||||
user := model.VPNUser{TunnelID: t.ID, Username: account.Username, DisplayName: account.Username, Email: account.Email, Source: account.Source, Enabled: true}
|
||||
if account.Source == "ldap" {
|
||||
dn, err := findLDAPUserDN(account.Username)
|
||||
if err != nil {
|
||||
skipped = append(skipped, account.Username+"(LDAP DN 获取失败)")
|
||||
continue
|
||||
}
|
||||
user.LDAPDN = dn
|
||||
} else {
|
||||
user.Password = account.Password
|
||||
}
|
||||
if err := store.DB.Create(&user).Error; err != nil {
|
||||
skipped = append(skipped, account.Username+"(创建失败)")
|
||||
continue
|
||||
}
|
||||
created++
|
||||
}
|
||||
store.Audit(c.GetString("username"), "import_vpn_users", t.Name, fmt.Sprintf("成功 %d,跳过 %d", created, len(skipped)))
|
||||
c.JSON(200, gin.H{"created": created, "skipped": skipped})
|
||||
}
|
||||
|
||||
func (s *Server) batchDeleteVPNUsers(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil || len(req.IDs) == 0 {
|
||||
c.JSON(400, gin.H{"error": "请选择要删除的拨号用户"})
|
||||
return
|
||||
}
|
||||
var users []model.VPNUser
|
||||
store.DB.Where("id IN ?", req.IDs).Find(&users)
|
||||
tunnels := map[uint]bool{}
|
||||
for _, user := range users {
|
||||
revokeOpenVPNCert(&user)
|
||||
tunnels[user.TunnelID] = true
|
||||
}
|
||||
if err := store.DB.Delete(&model.VPNUser{}, req.IDs).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "批量删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
for tunnelID := range tunnels {
|
||||
if t, err := store.GetTunnel(tunnelID); err == nil {
|
||||
_ = refreshDialAuth(t)
|
||||
}
|
||||
}
|
||||
store.Audit(c.GetString("username"), "batch_delete_vpn_users", "VPN 拨号用户", fmt.Sprintf("删除 %d 个", len(users)))
|
||||
c.JSON(200, gin.H{"deleted": len(users)})
|
||||
}
|
||||
|
||||
// findLDAPUserDN resolves a platform LDAP account to the DN required by the
|
||||
// OpenVPN authentication helper. The password is never read or stored here.
|
||||
func findLDAPUserDN(username string) (string, error) {
|
||||
cfg, err := ldapSettings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conn, err := ldapConnect(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
filter := cfg.UserFilter
|
||||
if strings.Contains(filter, "%s") {
|
||||
filter = fmt.Sprintf(filter, ldap.EscapeFilter(username))
|
||||
} else {
|
||||
filter = fmt.Sprintf("(&%s(%s=%s))", filter, cfg.UsernameAttribute, ldap.EscapeFilter(username))
|
||||
}
|
||||
result, err := conn.Search(ldap.NewSearchRequest(cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 5, false, filter, []string{"dn"}, nil))
|
||||
if err != nil || len(result.Entries) != 1 {
|
||||
return "", fmt.Errorf("未找到 LDAP 用户 %q", username)
|
||||
}
|
||||
return result.Entries[0].DN, nil
|
||||
}
|
||||
func (s *Server) updateVPNUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.VPNUser
|
||||
|
||||
Reference in new issue
Block a user