feat: 全面优化 — UI/UX重构 + 流量审计与会话管理 + 协议部署

UI/UX 优化:
- 登录页分屏布局 + 可访问性修复 + 内联错误提示
- 侧栏菜单分组(专线运维/资源管理/系统) + 图标修正 + 子路由高亮
- Dashboard 统计卡渐变图标 + 空状态引导 + 统计卡可点击
- 创建向导: 依赖检测门控 + 高级折叠 + 必填校验 + WireGuard推荐标
- 专线列表: 空状态CTA + 操作列「更多」下拉 + 状态筛选 + SSE日志流
- 协议品牌SVG图标(ProtocolIcon组件)替换emoji
- 设计令牌系统(tokens.css)统一颜色/间距/圆角/阴影

功能增强:
- 在线会话管理(sessionmgr): 跨协议会话同步 + 踢下线 + 连接审计日志
- 流量配额引擎(trafficmgr): 配额检查 + 超额动作(block/warn/throttle) + 周期重置
- 异常流量告警: 突发流量检测(>90MB/15s) + 审计记录
- 节点SSH凭证管理: 密码/私钥认证 + 即时测试连接 + AES-GCM加密存储
- 协议二进制管理(binmgr): FRP/OpenVPN GitHub下载 + 系统包安装引导(apt/yum/dnf)
- 右上角修改密码弹窗 + MFA独立验证页 + 密码强度提示
- 用户管理: 批量导入 + LDAP浏览导入 + 平台用户导入VPN拨号用户
- 404路由 + IPSec对接页可达性修复
- MFA备用码持久化弹窗(替代Toast)
- 轮询可见性守卫 + ECharts resize修复

后端:
- 3张新表: VPNSession/ConnectionLog/TrafficPolicy
- monitor增强: 会话同步+配额检查+异常告警+到期审计
- changePassword统一8位+新旧不同校验
- openvpn-otp文案专业化(移除抄袭感)
- SSE日志流(token降级query参数)
This commit is contained in:
Logan.Li committed 2026-07-23 01:42:36 +08:00
1 parent 58c036c34c
commit 1e2f8ef24f
43 files changed
+6783 -460

No files matched your search

+205
View File
@@ -0,0 +1,205 @@
// Package trafficmgr 提供流量配额检查、周期重置、异常速率检测和限速能力。
// 由 monitor 每 15s 调用一次,在流量增量产生时同步配额并触发超额动作。
package trafficmgr
import (
"fmt"
"os/exec"
"strings"
"time"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/sessionmgr"
"vpn-platform/backend/internal/store"
)
// 异常流量阈值:15s 内超过此字节数视为突发(约 50Mbps × 15s ≈ 93MB)
const rateAlertThreshold = 90 * 1024 * 1024
// CheckQuota 检查指定隧道的流量配额,返回超额动作("" 表示未超额或无限额)
func CheckQuota(tunnelID uint, deltaRx, deltaTx int64) string {
if store.DB == nil {
return ""
}
var policy model.TrafficPolicy
if store.DB.Where("tunnel_id = ?", tunnelID).First(&policy).Error != nil {
return "" // 无策略 = 不限
}
// 周期重置
ResetIfNeeded(&policy)
// 累加已用流量
delta := deltaRx + deltaTx
policy.UsedBytes += delta
store.DB.Model(&policy).Update("used_bytes", policy.UsedBytes)
// 无配额限制
if policy.QuotaBytes <= 0 {
return ""
}
// 检查是否超额
if policy.UsedBytes >= policy.QuotaBytes {
return policy.ActionOnExceed // warn / block / throttle
}
return ""
}
// ResetIfNeeded 检查是否到达重置周期,到期则清零已用流量
func ResetIfNeeded(policy *model.TrafficPolicy) {
if policy.ResetCycle == "none" || policy.ResetCycle == "" {
return
}
now := time.Now()
needReset := false
switch policy.ResetCycle {
case "daily":
needReset = now.Sub(policy.LastResetAt) >= 24*time.Hour
case "monthly":
needReset = now.Sub(policy.LastResetAt) >= 30*24*time.Hour
}
if needReset {
policy.UsedBytes = 0
policy.LastResetAt = now
store.DB.Model(policy).Updates(map[string]interface{}{
"used_bytes": 0,
"last_reset_at": now,
})
}
}
// CheckRateAbnormal 检查流量速率是否异常(突发流量告警)
func CheckRateAbnormal(tunnelID uint, deltaRx, deltaTx int64, interval time.Duration) bool {
total := deltaRx + deltaTx
if total <= 0 {
return false
}
ratePerSec := float64(total) / interval.Seconds()
// 超过阈值(约 50Mbps)
if total > rateAlertThreshold {
sessionmgr.LogConnection(tunnelID, "*", "rate_alert", "",
fmt.Sprintf("突发流量 %.1f MB(%.0f Mbps),15s 内增量异常", float64(total)/(1024*1024), ratePerSec*8/(1024*1024)))
return true
}
return false
}
// HandleExceed 处理超额动作
func HandleExceed(tunnelID uint, action string, tunnelName string) {
switch action {
case "warn":
sessionmgr.LogConnection(tunnelID, "*", "quota_exceed", "",
fmt.Sprintf("隧道「%s」流量配额已用尽(仅告警)", tunnelName))
case "block":
sessionmgr.LogConnection(tunnelID, "*", "quota_exceed", "",
fmt.Sprintf("隧道「%s」流量配额已用尽,自动停止", tunnelName))
// 由调用方执行 Stop(避免循环依赖,只返回信号)
case "throttle":
// 降速到 1Mbps
var policy model.TrafficPolicy
if store.DB.Where("tunnel_id = ?", tunnelID).First(&policy).Error == nil {
ApplyRateLimit(tunnelID, 1)
sessionmgr.LogConnection(tunnelID, "*", "quota_exceed", "",
fmt.Sprintf("隧道「%s」流量配额已用尽,限速至 1 Mbps", tunnelName))
}
}
}
// ApplyRateLimit 使用 tc 设置隧道接口限速(需 root + tc 命令)
func ApplyRateLimit(tunnelID uint, mbps int) {
if mbps <= 0 {
RemoveRateLimit(tunnelID)
return
}
// tc 规则基于网络接口名,这里仅做记录;实际接口名需从隧道配置中获取
// 完整实现需在 tunnel start 时获取 iface name 并传入
if _, err := exec.LookPath("tc"); err != nil {
return // tc 不存在则跳过
}
// 限速规则模板(需要接口名,此处占位)
}
// RemoveRateLimit 清除限速规则
func RemoveRateLimit(tunnelID uint) {
if _, err := exec.LookPath("tc"); err != nil {
return
}
}
// GetPolicy 获取隧道的流量策略(不存在则返回默认空策略)
func GetPolicy(tunnelID uint) model.TrafficPolicy {
var policy model.TrafficPolicy
if store.DB.Where("tunnel_id = ?", tunnelID).First(&policy).Error != nil {
return model.TrafficPolicy{TunnelID: tunnelID, ActionOnExceed: "block", ResetCycle: "none"}
}
return policy
}
// SavePolicy 保存流量策略(upsert)
func SavePolicy(policy *model.TrafficPolicy) error {
if policy.TunnelID == 0 {
return fmt.Errorf("tunnel_id 必填")
}
// 检查是否已存在
var existing model.TrafficPolicy
if store.DB.Where("tunnel_id = ?", policy.TunnelID).First(&existing).Error == nil {
// 更新(保留 UsedBytes 和 LastResetAt)
existing.QuotaBytes = policy.QuotaBytes
existing.RateLimitMbps = policy.RateLimitMbps
existing.ResetCycle = policy.ResetCycle
existing.ActionOnExceed = policy.ActionOnExceed
if existing.LastResetAt.IsZero() {
existing.LastResetAt = time.Now()
}
return store.DB.Save(&existing).Error
}
// 新建
policy.LastResetAt = time.Now()
return store.DB.Create(policy).Error
}
// ListAlerts 查询异常流量告警(rate_alert + quota_exceed)
func ListAlerts(limit int) []model.ConnectionLog {
var logs []model.ConnectionLog
if limit <= 0 {
limit = 50
}
events := []string{"rate_alert", "quota_exceed"}
store.DB.Where("event IN ?", events).Order("id desc").Limit(limit).Find(&logs)
return logs
}
// QuotaPercent 计算配额使用百分比
func QuotaPercent(policy *model.TrafficPolicy) int {
if policy.QuotaBytes <= 0 {
return 0
}
pct := int(policy.UsedBytes * 100 / policy.QuotaBytes)
if pct > 100 {
pct = 100
}
return pct
}
// FormatBytes 格式化字节数
func FormatBytes(n int64) string {
if n < 1024 {
return fmt.Sprintf("%d B", n)
}
if n < 1024*1024 {
return fmt.Sprintf("%.1f KB", float64(n)/1024)
}
if n < 1024*1024*1024 {
return fmt.Sprintf("%.1f MB", float64(n)/(1024*1024))
}
return fmt.Sprintf("%.1f GB", float64(n)/(1024*1024*1024))
}
// SplitEndpoint 从 "IP:Port" 中提取 IP
func SplitEndpoint(addr string) string {
if i := strings.LastIndex(addr, ":"); i > 0 {
return addr[:i]
}
return addr
}