11 KiB
规则每 IP 连接数与限速设计
日期: 2026-04-27 状态: 待审核 作者: AI Assistant
概述
在转发规则的高级设置中新增两类每客户端 IP 限制:每 IP 最大连接数、每 IP 带宽限速。保留现有总量限制语义不变,新增字段只在用户显式配置时生效。
实现优先复用 GOST 已有能力:climiters 的 $$ N 表示每个客户端 IP 独立最大连接数;limiters 支持 IP/CIDR 级带宽桶,可用 0.0.0.0/0 和 ::/0 实现默认覆盖所有 IPv4/IPv6 客户端的每 IP 带宽限速。
背景
当前 FLVX 已经支持规则级最大连接数和规则级限速,但这两个限制都是规则总量:
maxConn下发为 GOSTclimiters的$ N,限制整条规则的总并发连接数。speedId下发为 GOSTlimiters的$ in out,限制整条规则的总带宽。
用户需要的是按客户端 IP 隔离的限制,例如每个 IP 最多 5 个连接、每个 IP 最多 10 Mbps,而不是所有客户端共享同一个总量。
GOST 能力确认
连接数限制
go-gost/x/limiter/conn/conn.go 已内置以下语义:
| Key | 含义 |
|---|---|
$ |
全局连接数限制,所有客户端共享一个 limiter |
$$ |
每个客户端 IP 独立连接数限制,每个 IP 创建自己的 limiter |
IP / CIDR |
指定 IP 或 CIDR 的连接数限制 |
因此每 IP 连接数无需新增 agent 限制器,只需后端下发 $$ N。
带宽限制
go-gost/x/limiter/traffic/traffic.go 已内置以下语义:
| Key | 含义 |
|---|---|
$ |
服务级总带宽限制 |
$$ |
连接级带宽限制 |
IP / CIDR |
客户端 IP 或 CIDR 级带宽限制 |
CIDR 级限制使用 generator,为命中的客户端 IP 创建独立 limiter。使用 0.0.0.0/0 和 ::/0 可以覆盖所有 IPv4/IPv6 客户端,实现每 IP 带宽限速。
现有缺口
TCP listener 已在 Accept 后用客户端地址包装连接级 traffic limiter,路径可用于每 IP 带宽。UDP listener 当前只在 PacketConn 上应用服务级 limiter,没有在 Accept() 后按客户端 UDP pseudo-connection 包装 limiter,也没有挂接 connection limiter。因此要让 UDP 与 TCP 语义一致,需要补齐 UDP listener 的 per-client wrapper。
目标
- 保留现有
maxConn和speedId的总量语义。 - 在规则上新增每 IP 最大连接数。
- 在规则上新增每 IP 带宽限速。
- 同一规则允许同时配置总量限制和每 IP 限制。
- 普通用户不能设置或修改限速规则字段,保持现有权限模型。
- TCP 和 UDP 入口都尽量遵循相同限制语义。
非目标
- 不新增按用户组、节点组、国家地区、ASN 的限制。
- 不新增请求频率限制;本次“每个 IP 限速”指带宽限速,不是新建连接频率。
- 不改变已有 speed limit 规则表的单位和含义。
- 不把用户级默认最大连接数改成每 IP 语义;用户级
maxConn继续作为默认总连接数。
数据模型
在 forward 表新增两个字段:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
ip_max_conn |
int | 0 |
每 IP 最大连接数,0 表示不启用 |
ip_speed_id |
nullable int64 | NULL |
每 IP 带宽限速规则 ID,NULL 表示不启用 |
Go 模型新增:
IPMaxConn int `gorm:"column:ip_max_conn;not null;default:0"`
IPSpeedID sql.NullInt64 `gorm:"column:ip_speed_id"`
字段会通过现有 auto-migrate 机制创建,保持 SQLite/PostgreSQL 兼容,不使用 SQLite 不兼容的 GORM tags。
API 行为
创建规则
/forward/create 新增入参:
{
"ipMaxConn": 5,
"ipSpeedId": 123
}
规则:
ipMaxConn缺省或小于等于0时按0存储,不启用每 IP 连接数限制。ipSpeedId缺省或不存在时存为NULL,不启用每 IP 带宽限速。ipSpeedId指向不存在的限速规则时按NULL处理,沿用现有speedId的容错策略。- 普通用户提交非空
ipSpeedId时返回错误,保持与speedId一致的权限边界。
更新规则
/forward/update 新增入参:
{
"ipMaxConn": 5,
"ipSpeedId": 123
}
规则:
- 未提交
ipMaxConn时保留原值;提交空值或0时清除每 IP 连接数限制。 - 未提交
ipSpeedId时保留原值;提交null时清除每 IP 带宽限速。 - 普通用户不能把
ipSpeedId改成不同的非空值。 - 更新后重新同步运行时服务和 limiter。
列表返回
/forward/list 返回项新增:
{
"ipMaxConn": 5,
"ipSpeedId": 123,
"ipSpeedLimitName": "每IP 10Mbps"
}
ipSpeedLimitName 可选,但建议返回,便于前端显示缺失或已删除的限速规则。
后端运行时同步
连接数限制器
将现有连接限制器构建从单一总量扩展为组合规则。
当前行为:
{
"name": "rule_conn_limit_42",
"limits": ["$ 100"]
}
新增行为:
{
"name": "rule_conn_limit_42",
"limits": ["$ 100", "$$ 5"]
}
规则:
maxConn > 0时追加$ maxConn。ipMaxConn > 0时追加$$ ipMaxConn。- 如果规则未配置
maxConn且用户有MaxConn > 0,继续继承用户级总连接数,追加$ user.MaxConn。 - 如果两者都没有,则不下发
climiter,服务不引用climiter。 - limiter 名称继续优先使用
rule_conn_limit_<forwardID>;只有用户级默认总连接数且规则没有任何连接限制时可继续使用user_conn_limit_<userID>,避免不必要的 per-rule limiter。
带宽限制器
将现有规则限速从单一 speedId 扩展为组合 limiter。
当前行为:
{
"name": "123",
"limits": ["$ 1.3MB 1.3MB"]
}
新增每 IP 行为:
{
"name": "rule_traffic_limit_42",
"limits": [
"$ 1.3MB 1.3MB",
"0.0.0.0/0 1.3MB 1.3MB",
"::/0 1.3MB 1.3MB"
]
}
规则:
- 只有总量
speedId时,保持现有名称和下发路径,服务继续引用speedId字符串。 - 只有每 IP
ipSpeedId时,创建rule_traffic_limit_<forwardID>,只包含 IPv4/IPv6 CIDR 行。 - 总量和每 IP 同时存在时,创建
rule_traffic_limit_<forwardID>,同时包含$和 CIDR 行。 - 如果规则没有
speedId,则总量仍可继承 user tunnel 的speedId,保持现有 fallback 语义;当继承的总量限速与ipSpeedId同时存在时,也使用rule_traffic_limit_<forwardID>组合 limiter。 - 每 IP 限速不从 user tunnel 继承,只由规则字段控制。
AddLimiters失败且提示已存在时,使用UpdateLimiters更新。
服务配置
buildForwardServiceConfigs 需要从当前 limiterID *int64 / cLimiterName string 扩展为更明确的运行时限制描述,例如:
type forwardRuntimeLimiters struct {
TrafficLimiter string
ConnLimiter string
}
服务配置只关心最终引用的 limiter 名称:
service["limiter"] = runtimeLimiters.TrafficLimiterservice["climiter"] = runtimeLimiters.ConnLimiter
这样可以把“如何构建 limiter payload”的逻辑和“如何构建 service JSON”的逻辑分开。
Agent/GOST 调整
WebSocket 命令
当前 agent WebSocket 已支持:
AddLimiters/UpdateLimiters/DeleteLimitersAddCLimiters/UpdateCLimiters/DeleteCLimiters
本设计无需新增命令类型。
UDP listener
补齐 go-gost/x/listener/udp/listener.go 的 Accept() 包装逻辑,使 UDP pseudo-connection 与 TCP listener 一致:
- 对
l.options.ConnLimiter按客户端地址应用连接数限制。 - 对
l.options.TrafficLimiter按conn.RemoteAddr().String()应用连接级 traffic wrapper。
需要注意 UDP pseudo-connection 的生命周期由内部 UDP listener 的 TTL/keepalive 控制;connection limiter 必须在 pseudo-connection 关闭时释放计数。
前端设计
在 vite-frontend/src/pages/forward.tsx 的规则高级设置中新增两个控件:
每 IP 最大连接数
- 类型:number input。
- 文案:
每个客户端 IP 可同时建立的最大连接数;0 或空表示不限制。 - 字段:
ipMaxConn。
每 IP 限速
- 类型:Select,复用现有限速规则列表。
- 文案:
每个客户端 IP 独享该带宽限制;不选择表示不限制。 - 字段:
ipSpeedId。 - 只对管理员显示,保持与
规则限速一致。
前端类型需要同步更新:
ForwardApiItemForwardMutationPayloadForwardForm或页面内等价类型
错误处理与兼容性
- 旧数据默认
ip_max_conn=0、ip_speed_id=NULL,行为与当前版本一致。 - 现有 agent 已支持 limiter 命令和 GOST limiter 语法;发布时需要包含 UDP 修复,才能让 TCP/UDP 都获得完整语义。
- 节点离线时沿用现有 warning 行为,规则仍可保存,在线节点跳过下发。
- 如果每 IP speed limit ID 被删除,更新时按
NULL处理,列表页可提示或自动清除,和现有speedId行为一致。 - 如果 IPv6 CIDR 在某些监听路径未命中,IPv4 行仍正常生效;测试应覆盖 IPv4,IPv6 通过 payload 合同保证下发。
测试计划
后端 contract 测试
新增或扩展 go-backend/tests/contract/max_conn_limit_contract_test.go:
- 创建规则时设置
ipMaxConn=5,断言AddCLimiterspayload 包含$$ 5。 - 同时设置
maxConn=100和ipMaxConn=5,断言 payload 包含$ 100和$$ 5。 - 用户级
MaxConn存在且规则ipMaxConn=5时,断言 payload 包含$ userMaxConn和$$ 5。
新增每 IP 限速 contract 测试:
- 创建规则时设置
ipSpeedId,断言AddLimiterspayload 包含0.0.0.0/0 ...和::/0 ...。 - 同时设置
speedId和ipSpeedId,断言组合 limiter 包含$ ...与两个 CIDR 行,服务引用rule_traffic_limit_<forwardID>。 - 普通用户提交
ipSpeedId返回错误。
Repository/API 测试
CreateForwardTx、UpdateForward、列表查询读写ip_max_conn和ip_speed_id。/forward/list返回ipMaxConn、ipSpeedId。
GOST/x 测试
go-gost/x/limiter/conn:验证$$ N为不同 IP 创建独立 limiter。go-gost/x/limiter/traffic:验证0.0.0.0/0为不同 IPv4 创建独立 limiter。- UDP listener:验证 Accept 返回的 UDP pseudo-connection 关闭后释放 connection limiter。
验证命令
(cd go-backend && go test ./...)
(cd go-gost/x && go test ./limiter/... ./listener/udp/...)
(cd vite-frontend && pnpm run build)
推荐实施顺序
- 后端模型、repo DTO、API 字段读写。
- 后端 limiter payload 构建与服务引用重构。
- Contract 测试覆盖连接数和带宽 payload。
- GOST UDP listener per-client wrapper 与相关测试。
- 前端高级设置表单和类型更新。
- 运行后端测试、GOST/x 相关测试、前端构建。
风险
- UDP pseudo-connection 生命周期和 TCP 连接不同,连接数释放必须依赖 Close 包装正确执行。
- 总带宽和每 IP 带宽组合时 limiter 名称从纯 speed ID 变为 rule-level 名称,需要确保更新已有规则时不会留下错误引用。
- 旧节点如果没有 UDP wrapper 修复,TCP 生效但 UDP 每 IP 语义可能不完整;发布时应要求 agent 同步升级。
- 每 IP 带宽是每个入口节点本地独立限制,不是跨节点全局聚合限制。