feat(client): add authorized WireGuard device enrollment and desktop connection
This commit is contained in:
1 parent
d2f1f5040e
commit
6dd3c7dd45
25 files changed
+3274
-131
No files matched your search
@@ -452,9 +452,9 @@ func (s *Server) createVPNUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
switch t.Protocol {
|
||||
case "openvpn", "l2tp", "ikev2":
|
||||
case "openvpn", "l2tp", "ikev2", "wireguard":
|
||||
default:
|
||||
c.JSON(400, gin.H{"error": "该协议不支持拨号用户(支持 OpenVPN / L2TP / IKEv2)"})
|
||||
c.JSON(400, gin.H{"error": "该协议不支持拨号用户(支持 OpenVPN / L2TP / IKEv2 / WireGuard)"})
|
||||
return
|
||||
}
|
||||
if req.PlatformUserID != 0 {
|
||||
@@ -586,6 +586,14 @@ func (s *Server) batchDeleteVPNUsers(c *gin.Context) {
|
||||
store.DB.Where("id IN ?", req.IDs).Find(&users)
|
||||
tunnels := map[uint]bool{}
|
||||
for _, user := range users {
|
||||
if err := s.revokeClientDevicesForUser(&user, c.GetString("username")); err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销客户端设备失败,请重试:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := revokeClientSessionsForUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"error": "注销客户端会话失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
revokeOpenVPNCert(&user)
|
||||
tunnels[user.TunnelID] = true
|
||||
}
|
||||
@@ -642,6 +650,16 @@ func (s *Server) updateVPNUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user.DisplayName, user.Email = req.DisplayName, req.Email
|
||||
if (req.Enabled != nil && !*req.Enabled && user.Enabled) || req.Password != "" {
|
||||
if err := s.revokeClientDevicesForUser(&user, c.GetString("username")); err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销客户端设备失败,账号未修改:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := revokeClientSessionsForUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"error": "注销客户端会话失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
if !*req.Enabled && user.Enabled {
|
||||
revokeOpenVPNCert(&user)
|
||||
@@ -665,6 +683,14 @@ func (s *Server) deleteVPNUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.VPNUser
|
||||
if store.DB.First(&user, id).Error == nil {
|
||||
if err := s.revokeClientDevicesForUser(&user, c.GetString("username")); err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销客户端设备失败,用户未删除:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := revokeClientSessionsForUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"error": "注销客户端会话失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
revokeOpenVPNCert(&user)
|
||||
tunnelID := user.TunnelID
|
||||
store.DB.Delete(&model.VPNUser{}, id)
|
||||
@@ -691,7 +717,21 @@ func revokeOpenVPNCert(user *model.VPNUser) {
|
||||
}
|
||||
}
|
||||
|
||||
// vpnUserMFASetup 为 OpenVPN 拨号用户生成 TOTP 密钥(待确认)。
|
||||
// WireGuard Peer 长期有效,用户凭据或授权变化前必须先从网关撤销对应设备。
|
||||
func (s *Server) revokeClientDevicesForUser(user *model.VPNUser, operator string) error {
|
||||
var devices []model.ClientDevice
|
||||
if err := store.DB.Where("vpn_user_id = ? AND revoked = ?", user.ID, false).Find(&devices).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range devices {
|
||||
if err := s.revokeDevice(&devices[i], operator); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// vpnUserMFASetup 为 OpenVPN/WireGuard 拨号用户生成 TOTP 密钥(待确认)。
|
||||
func (s *Server) vpnUserMFASetup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.VPNUser
|
||||
@@ -700,13 +740,17 @@ func (s *Server) vpnUserMFASetup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
t, err := store.GetTunnel(user.TunnelID)
|
||||
if err != nil || t.Protocol != "openvpn" {
|
||||
c.JSON(400, gin.H{"error": "仅 OpenVPN 拨号用户支持 MFA"})
|
||||
if err != nil || (t.Protocol != "openvpn" && t.Protocol != "wireguard") {
|
||||
c.JSON(400, gin.H{"error": "仅 OpenVPN / WireGuard 拨号用户支持 MFA"})
|
||||
return
|
||||
}
|
||||
issuer := "OPSX-OpenVPN"
|
||||
if t.Protocol == "wireguard" {
|
||||
issuer = "OPSX VPN"
|
||||
}
|
||||
// TOTP 标准参数(RFC 6238):SHA1、30s、6 位、base32,兼容主流 Authenticator
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "OPSX-OpenVPN",
|
||||
Issuer: issuer,
|
||||
AccountName: fmt.Sprintf("%s@%s", user.Username, t.Name),
|
||||
Period: 30,
|
||||
Digits: otp.DigitsSix,
|
||||
@@ -775,6 +819,14 @@ func (s *Server) vpnUserMFAEnable(c *gin.Context) {
|
||||
user.MFASecret = secret
|
||||
user.MFABackupCodes = backups
|
||||
user.MFAEnabled = true
|
||||
if err := s.revokeClientDevicesForUser(&user, c.GetString("username")); err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销已有客户端设备失败,MFA 未启用:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := revokeClientSessionsForUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"error": "注销客户端会话失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := store.DB.Save(&user).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -790,6 +842,14 @@ func (s *Server) vpnUserMFADisable(c *gin.Context) {
|
||||
c.JSON(404, gin.H{"error": "拨号用户不存在"})
|
||||
return
|
||||
}
|
||||
if err := s.revokeClientDevicesForUser(&user, c.GetString("username")); err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销客户端设备失败,MFA 未关闭:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := revokeClientSessionsForUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"error": "注销客户端会话失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
store.DB.Model(&user).Updates(map[string]interface{}{
|
||||
"mfa_enabled": false, "mfa_secret": "", "mfa_backup_codes": "",
|
||||
"mfa_pin": "", "mfa_has_pin": false, "mfa_fail_count": 0, "mfa_locked_until": nil,
|
||||
@@ -838,6 +898,18 @@ func (s *Server) vpnUserMFAPin(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
if req.Pin != "" && (len(req.Pin) < 4 || len(req.Pin) > 32 || strings.ContainsAny(req.Pin, " \t\r\n")) {
|
||||
c.JSON(400, gin.H{"error": "PIN 长度需 4-32 位且不能包含空白字符"})
|
||||
return
|
||||
}
|
||||
if err := s.revokeClientDevicesForUser(&user, c.GetString("username")); err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销客户端设备失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := revokeClientSessionsForUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"error": "注销客户端会话失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := openvpn.SetUserMFAPin(&user, req.Pin); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user