feat: 每用户最大连接数改为可配置,默认 30
将 maxConnsPerUser 从硬编码常量(3)改为数据库动态配置项,管理员可在 /admin/vpn 隧道设置中调整,保存后对新连接立即生效。 - model/vpn.go: VpnSetting 新增 MaxConnsPerUser 字段,gorm default:30 - db/db.go: 种子数据设默认 30;旧库回填 0 值为 30 - vpn/tunnel.go: 删除 maxConnsPerUser 常量,改读 VPN.Settings(),兜底 30 - handler/vpn.go: API 响应/请求结构体新增字段,校验范围 1-1000 - VpnView.vue: 隧道设置表单新增"每用户最大连接数"输入框 - zh.ts/en.ts: 新增 maxConnsPerUser 文案,更新首页多设备描述 - docs/client-development.md: 更新常量表为可配置项
This commit is contained in:
@@ -780,7 +780,7 @@ ip6tables -t nat -A POSTROUTING -s <VPN_V6_SUBNET> -o <物理网卡> -j MASQUERA
|
|||||||
| `readyTimeout` | 30s | 等待 ready 超时 | `internal/vpn/tunnel.go:19` |
|
| `readyTimeout` | 30s | 等待 ready 超时 | `internal/vpn/tunnel.go:19` |
|
||||||
| `pingPeriod` | 30s | Ping 周期 | `internal/vpn/tunnel.go:20` |
|
| `pingPeriod` | 30s | Ping 周期 | `internal/vpn/tunnel.go:20` |
|
||||||
| `maxMessageSize` | 1 MB | 单消息上限 | `internal/vpn/tunnel.go:21` |
|
| `maxMessageSize` | 1 MB | 单消息上限 | `internal/vpn/tunnel.go:21` |
|
||||||
| `maxConnsPerUser` | 3 | 单用户并发连接上限 | `internal/vpn/tunnel.go:22` |
|
| `maxConnsPerUser` | 30(可配置) | 单用户并发连接上限 | `internal/model/vpn.go` (`VpnSetting.MaxConnsPerUser`) |
|
||||||
| `tokenExpire` | 24h | JWT 有效期 | `internal/middleware/auth.go:15` |
|
| `tokenExpire` | 24h | JWT 有效期 | `internal/middleware/auth.go:15` |
|
||||||
| 登录限流 | 5/min·IP | `/api/login` 限流 | `internal/middleware/ratelimit.go:76` |
|
| 登录限流 | 5/min·IP | `/api/login` 限流 | `internal/middleware/ratelimit.go:76` |
|
||||||
| 密码认证限流 | 5/min·(IP+用户名) | WebSocket 密码认证限流 | `internal/vpn/auth.go:15,41` |
|
| 密码认证限流 | 5/min·(IP+用户名) | WebSocket 密码认证限流 | `internal/vpn/auth.go:15,41` |
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export default {
|
|||||||
serverConfigTunIp: 'Server Configures TUN IP',
|
serverConfigTunIp: 'Server Configures TUN IP',
|
||||||
autoConfig: 'Auto',
|
autoConfig: 'Auto',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
|
maxConnsPerUser: 'Max Connections Per User',
|
||||||
saveSettings: 'Save Settings',
|
saveSettings: 'Save Settings',
|
||||||
saveSuccess: 'Saved successfully',
|
saveSuccess: 'Saved successfully',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
@@ -168,7 +169,7 @@ export default {
|
|||||||
'Admin and user roles with full user CRUD, enable/disable, and password changes, plus self-protection rules.',
|
'Admin and user roles with full user CRUD, enable/disable, and password changes, plus self-protection rules.',
|
||||||
multiDevice: 'Multi-Device Concurrent',
|
multiDevice: 'Multi-Device Concurrent',
|
||||||
multiDeviceDesc:
|
multiDeviceDesc:
|
||||||
'Up to 3 concurrent connections per user with automatic tunnel IP assignment.',
|
'Supports multi-device concurrent connections per user, configurable in admin panel.',
|
||||||
dualStack: 'IPv4/IPv6 Dual-Stack',
|
dualStack: 'IPv4/IPv6 Dual-Stack',
|
||||||
dualStackDesc:
|
dualStackDesc:
|
||||||
'Supports both IPv4 and IPv6 subnets with NAT dual-stack forwarding and anti-spoofing.',
|
'Supports both IPv4 and IPv6 subnets with NAT dual-stack forwarding and anti-spoofing.',
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
serverConfigTunIp: '服务端配置 TUN IP',
|
serverConfigTunIp: '服务端配置 TUN IP',
|
||||||
autoConfig: '自动配置',
|
autoConfig: '自动配置',
|
||||||
manual: '手动',
|
manual: '手动',
|
||||||
|
maxConnsPerUser: '每用户最大连接数',
|
||||||
saveSettings: '保存设置',
|
saveSettings: '保存设置',
|
||||||
saveSuccess: '保存成功',
|
saveSuccess: '保存成功',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
@@ -165,7 +166,7 @@ export default {
|
|||||||
userManageDesc:
|
userManageDesc:
|
||||||
'管理员与普通角色,支持用户增删改、启用禁用与改密,内置自保护规则。',
|
'管理员与普通角色,支持用户增删改、启用禁用与改密,内置自保护规则。',
|
||||||
multiDevice: '多设备并发',
|
multiDevice: '多设备并发',
|
||||||
multiDeviceDesc: '每用户最多 3 个并发连接,自动分配隧道 IP 地址。',
|
multiDeviceDesc: '每用户支持多设备并发连接,可在管理后台配置上限。',
|
||||||
dualStack: 'IPv4/IPv6 双栈',
|
dualStack: 'IPv4/IPv6 双栈',
|
||||||
dualStackDesc: '同时支持 IPv4 与 IPv6 子网,NAT 双栈转发与源地址反欺骗。',
|
dualStackDesc: '同时支持 IPv4 与 IPv6 子网,NAT 双栈转发与源地址反欺骗。',
|
||||||
reservation: '静态 IP 预留',
|
reservation: '静态 IP 预留',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface Settings {
|
|||||||
allow_client_to_client: boolean
|
allow_client_to_client: boolean
|
||||||
do_local_ip_config: boolean
|
do_local_ip_config: boolean
|
||||||
do_remote_ip_config: boolean
|
do_remote_ip_config: boolean
|
||||||
|
max_conns_per_user: number
|
||||||
}
|
}
|
||||||
interface ClientInfo {
|
interface ClientInfo {
|
||||||
user_id: number
|
user_id: number
|
||||||
@@ -83,6 +84,7 @@ const form = ref<Settings>({
|
|||||||
allow_client_to_client: false,
|
allow_client_to_client: false,
|
||||||
do_local_ip_config: true,
|
do_local_ip_config: true,
|
||||||
do_remote_ip_config: true,
|
do_remote_ip_config: true,
|
||||||
|
max_conns_per_user: 30,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchSettings() {
|
async function fetchSettings() {
|
||||||
@@ -403,6 +405,10 @@ onMounted(() => {
|
|||||||
<option :value="false">{{ t('vpn.manual') }}</option>
|
<option :value="false">{{ t('vpn.manual') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ t('vpn.maxConnsPerUser') }}</label>
|
||||||
|
<input v-model.number="form.max_conns_per_user" type="number" min="1" max="1000" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 mt-6">
|
<div class="flex items-center gap-4 mt-6">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -105,8 +105,16 @@ func seedDefaultAdmin(cfg *config.DatabaseConfig) error {
|
|||||||
func seedDefaultVpnSettings() error {
|
func seedDefaultVpnSettings() error {
|
||||||
var s model.VpnSetting
|
var s model.VpnSetting
|
||||||
if err := DB.First(&s, model.VpnSettingSingletonID).Error; err == nil {
|
if err := DB.First(&s, model.VpnSettingSingletonID).Error; err == nil {
|
||||||
|
needSave := false
|
||||||
if s.Subnet6 == "" {
|
if s.Subnet6 == "" {
|
||||||
s.Subnet6 = "fd00:dead:beef::/112"
|
s.Subnet6 = "fd00:dead:beef::/112"
|
||||||
|
needSave = true
|
||||||
|
}
|
||||||
|
if s.MaxConnsPerUser == 0 {
|
||||||
|
s.MaxConnsPerUser = 30
|
||||||
|
needSave = true
|
||||||
|
}
|
||||||
|
if needSave {
|
||||||
DB.Save(&s)
|
DB.Save(&s)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -120,6 +128,7 @@ func seedDefaultVpnSettings() error {
|
|||||||
InterfaceName: "",
|
InterfaceName: "",
|
||||||
DoLocalIPConfig: true,
|
DoLocalIPConfig: true,
|
||||||
DoRemoteIPConfig: true,
|
DoRemoteIPConfig: true,
|
||||||
|
MaxConnsPerUser: 30,
|
||||||
}
|
}
|
||||||
return DB.Create(&s).Error
|
return DB.Create(&s).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type vpnSettingsResponse struct {
|
|||||||
AllowClientToClient bool `json:"allow_client_to_client"`
|
AllowClientToClient bool `json:"allow_client_to_client"`
|
||||||
DoLocalIPConfig bool `json:"do_local_ip_config"`
|
DoLocalIPConfig bool `json:"do_local_ip_config"`
|
||||||
DoRemoteIPConfig bool `json:"do_remote_ip_config"`
|
DoRemoteIPConfig bool `json:"do_remote_ip_config"`
|
||||||
|
MaxConnsPerUser int `json:"max_conns_per_user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateVpnSettingsRequest struct {
|
type updateVpnSettingsRequest struct {
|
||||||
@@ -33,6 +34,7 @@ type updateVpnSettingsRequest struct {
|
|||||||
AllowClientToClient *bool `json:"allow_client_to_client"`
|
AllowClientToClient *bool `json:"allow_client_to_client"`
|
||||||
DoLocalIPConfig *bool `json:"do_local_ip_config"`
|
DoLocalIPConfig *bool `json:"do_local_ip_config"`
|
||||||
DoRemoteIPConfig *bool `json:"do_remote_ip_config"`
|
DoRemoteIPConfig *bool `json:"do_remote_ip_config"`
|
||||||
|
MaxConnsPerUser *int `json:"max_conns_per_user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVpnSettings() (model.VpnSetting, error) {
|
func loadVpnSettings() (model.VpnSetting, error) {
|
||||||
@@ -131,6 +133,7 @@ func GetVpnSettings(c *gin.Context) {
|
|||||||
AllowClientToClient: s.AllowClientToClient,
|
AllowClientToClient: s.AllowClientToClient,
|
||||||
DoLocalIPConfig: s.DoLocalIPConfig,
|
DoLocalIPConfig: s.DoLocalIPConfig,
|
||||||
DoRemoteIPConfig: s.DoRemoteIPConfig,
|
DoRemoteIPConfig: s.DoRemoteIPConfig,
|
||||||
|
MaxConnsPerUser: s.MaxConnsPerUser,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +188,13 @@ func UpdateVpnSettings(c *gin.Context) {
|
|||||||
if req.DoRemoteIPConfig != nil {
|
if req.DoRemoteIPConfig != nil {
|
||||||
s.DoRemoteIPConfig = *req.DoRemoteIPConfig
|
s.DoRemoteIPConfig = *req.DoRemoteIPConfig
|
||||||
}
|
}
|
||||||
|
if req.MaxConnsPerUser != nil {
|
||||||
|
if *req.MaxConnsPerUser < 1 || *req.MaxConnsPerUser > 1000 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "每用户最大连接数范围 1-1000"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.MaxConnsPerUser = *req.MaxConnsPerUser
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.DB.Save(&s).Error; err != nil {
|
if err := db.DB.Save(&s).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存设置失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存设置失败"})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type VpnSetting struct {
|
|||||||
AllowClientToClient bool `gorm:"default:false"`
|
AllowClientToClient bool `gorm:"default:false"`
|
||||||
DoLocalIPConfig bool `gorm:"default:true"`
|
DoLocalIPConfig bool `gorm:"default:true"`
|
||||||
DoRemoteIPConfig bool `gorm:"default:true"`
|
DoRemoteIPConfig bool `gorm:"default:true"`
|
||||||
|
MaxConnsPerUser int `gorm:"default:30"`
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const (
|
|||||||
readyTimeout = 30 * time.Second
|
readyTimeout = 30 * time.Second
|
||||||
pingPeriod = 30 * time.Second
|
pingPeriod = 30 * time.Second
|
||||||
maxMessageSize = 1 << 20
|
maxMessageSize = 1 << 20
|
||||||
maxConnsPerUser = 3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -99,7 +98,11 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeConnsMu.Lock()
|
activeConnsMu.Lock()
|
||||||
if activeConns[user.ID] >= maxConnsPerUser {
|
maxConns := VPN.Settings().MaxConnsPerUser
|
||||||
|
if maxConns <= 0 {
|
||||||
|
maxConns = 30
|
||||||
|
}
|
||||||
|
if activeConns[user.ID] >= maxConns {
|
||||||
activeConnsMu.Unlock()
|
activeConnsMu.Unlock()
|
||||||
_ = sendJSON(conn, controlMessage{Type: "error", Message: "连接数已达上限"})
|
_ = sendJSON(conn, controlMessage{Type: "error", Message: "连接数已达上限"})
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user