Files
opsx-vpn/backend/internal/trafficmgr/trafficmgr.go
T
Logan.Li 1e2f8ef24f 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参数)
2026-07-23 01:42:36 +08:00

206 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}