frp 版本发布说明解读:UDP 二进制编解码、pool_count 拒绝服务修复与配置校验加固
Release.md 记录了 frp 当前版本的三项变更:UDP 数据面在 wire 协议 v2 下新增二进制编解码(Feature),以及针对工作连接池的负数 DoS 修复、frpc verify 忽略 featureGates 的修复、子域主机域名校验绕过修复(Fixes)。读完本文,你将理解 UDP 二进制报文的协商机制与线格式设计、pool_count 校验缺陷的根因与修复位置,以及 HTTP 代理域名校验的规范化和测试依据,便于在升级 frp 时评估兼容性并自行复现验证。
一、核心特性:v2 协议下 UDP 报文的二进制编解码
发布说明中的 Feature 项指出:当 frpc 与 frps 在 wire 协议 v2 下成功协商该能力后,普通 UDP 代理与 SUDP 代理的报文载荷将使用专用二进制编解码,获得更紧凑的线表示;wire 协议 v1 保持 JSON 不变;v2 下若对端未声明或未协商成功,则回退到 JSON 的 UDPPacket。
1.1 协商机制:能力声明而非默认开启
该特性采用"客户端声明、服务端选择"的协商模式。从源码看,能力标识定义为 wire 包 中的常量 UDPPacketCodecBinary = "binary-v1",与消息编解码标识 MessageCodecJSON = "json" 并列。协商过程发生在 v2 握手的 ClientHello/ServerHello 阶段:
- 客户端在 Hello 中声明支持的
UDPPacketCodecs(见 wire.go 中UDPPacketCodecs: []string{UDPPacketCodecBinary}); - 服务端在 crypto.go 中通过
Supports(codecs, UDPPacketCodecBinary)判断客户端是否声明该能力,声明了才在 ServerHello 中选回UDPPacketCodecBinary; - 握手测试 wire_test.go 覆盖了三类断言:协商成功、协商失败回退、以及对"服务端单方面声称但客户端未声明"这类非法选择的拒绝(
unadvertised.Selected.Message.UDPPacketCodec = UDPPacketCodecBinary用例)。
这意味着:旧版本 frpc 连接新版本 frps(或反之)时,该特性自动不生效,静默回退 JSON,不产生兼容性问题。
1.2 二进制线格式:1 字节 flags + 紧凑地址 + 长度前缀载荷
编解码实现位于 pkg/msg/udp_binary.go。报文体(body)结构为:
+--------+---------------+--------------+--------+----------------+
| flags | localAddr* | remoteAddr | len(u16) | content |
| 1 byte | 21/39 bytes | 21/39 bytes | 2 bytes | <= 65507 B |
+--------+---------------+--------------+--------+----------------+
flags位定义:udpPacketFlagLocalAddr = 1<<0、udpPacketFlagRemoteAddr = 1<<1。RemoteAddr 在 UDP 转发路径中是必需的,编码时强制置位、解码时缺失即报错;LocalAddr 可选,缺省时节省整个地址段;- 地址段采用紧凑布局:1 字节地址族(4 表示 IPv4,6 表示 IPv6)+ 4/16 字节 IP + 2 字节端口 + 1 字节 zone 长度 + zone 字节串。IPv4 地址强制 zone 为空,IPv6 zone 限制在 255 字节内且必须是合法 UTF-8——这与 Go
net.UDPAddr.Zone(链接本地地址的作用域)语义对应; - 载荷上限
MaxUDPPayloadSize = 65507,对应 UDP 单报文 65507 字节最大载荷;整体还受 wire 层DefaultMaxFramePayloadSize = 64*1024帧长上限约束; - 解码侧防御性很强:拒绝保留位被置位、拒绝截断的地址/长度/载荷、拒绝尾部多余字节,且返回的
Content是独立拷贝("does not alias the input frame buffer"),避免帧缓冲复用时数据被覆盖。
1.3 读写字器的选择与严格性
NewUDPPacketReadWriter(udp_binary.go)是编解码的入口,按 wire 协议版本与协商结果分派:
| wire 协议 | 协商结果 | 使用的 ReadWriter |
|---|---|---|
| v1(或空) | 不允许指定编解码 | NewV1ReadWriter(JSON) |
| v2 | 未协商 | NewV2ReadWriter(v2 帧 + JSON 体) |
| v2 | binary-v1 |
NewV2BinaryUDPPacketReadWriter |
值得注意的一个设计细节:V2BinaryUDPPacketReadWriter 只替换 UDP 报文的编解码,其余控制消息仍走 wire_v2.go 中"2 字节类型号 + JSON"的通用编解码(消息类型表见 v2MsgTypeMap,二进制 UDP 报文的新类型号是 V2TypeUDPPacketBinary = 19,区别于 JSON 的 V2TypeUDPPacket = 13)。一旦协商成功,若对端发来 JSON 类型的 UDP 报文,读路径会直接报错 "received JSON UDP packet after binary codec negotiation",即协商成功后不允许混用两种 UDP 编解码,保证数据面格式的一致性。
针对该编解码的专项测试在 udp_binary_test.go 与 udp_benchmark_test.go(基准测试可量化二进制与 JSON 两种表示的长度和编解码开销差异)。
二、修复一:负数 pool_count 导致的服务器 panic 与拒绝服务
2.1 缺陷根因
发布说明指出:客户端发送负数 pool_count 曾导致服务器 panic 并可被用于远程拒绝服务;修复方式是在分配工作连接池资源之前拒绝负值。
pool_count 是 frpc 登录时告知服务端的工作连接池大小,对应登录消息字段 PoolCount(客户端侧赋值见 control_session.go)。服务端在接受控制会话时用它构建工作连接池。缺陷在于:旧的池容量计算把 poolCount 直接参与 make(chan ...) 等通道容量运算,负数会与容量偏移量相加后越过零,触发 panic;恶意客户端反复登录即可耗尽服务端资源。
2.2 修复实现与边界
修复位于 server/control.go 的 newControl 路径,校验顺序是:
PoolCount < 0→ 直接返回invalid pool count %d, must be non-negative,不分配任何资源;- 服务端配置的
MaxPoolCount < 0→ 同样拒绝; - 取
min(PoolCount, MaxPoolCount)得到有效池大小,并防止与workConnPoolCapacityOffset相加后溢出math.MaxInt(cannot safely add分支)。
配套的表驱动测试 server/control_test.go 的 TestNewControlPoolCountBoundaries 覆盖了关键边界:
- 负数池(
-11、-10、-1)一律报invalid pool count,其中-10恰好等于"负到能抵消容量偏移"的临界点,正是旧实现的 panic 触发区; - 池大小被服务端
MaxPoolCount截断(10 → 5); math.MaxInt/math.MaxInt64组合下验证溢出防护分支。
客户端侧的配置默认值也值得对照:pkg/config/v1/client.go 中 PoolCount 缺省为 1;服务端的 MaxPoolCount 缺省为 5(pkg/config/v1/server.go),且服务端配置校验已拒绝负数(validation/server.go)。因此该修复补齐的是来自网络对端的不可信输入这一环。
三、修复二:frpc verify 现在尊重 featureGates
3.1 问题场景
frpc verify 用于在不启动代理的情况下校验客户端配置文件,命令入口在 cmd/frpc/sub/verify.go:读取配置 → validation.ValidateAllClientConfig → 打印 the configuration file ... syntax is ok 或报错退出。
问题在于:verify 原先忽略了配置中的 featureGates,导致像 VirtualNet 这类由特性开关控制的功能,即使用户已在配置中显式开启,verify 仍会按"特性未开启"的默认策略拒绝相应配置,产生误报。
3.2 修复后的验证路径
修复后,verifyClientConfig 会构造 security.NewUnsafeFeatures(allowUnsafe) 并连同加载出的配置一起交给校验器,特性开关(定义见 pkg/policy/featuregate/feature_gate.go)参与最终判定。测试 verify_test.go 中的 TestVerifyClientConfigFeatureGates 验证了三种情况:
featureGates = { VirtualNet = true }→ 含 VirtualNet 配置通过;featureGates = { VirtualNet = false }→ 被拒绝;featureGates = { UnknownFeature = true }→ 未知特性名本身被拒绝。
实操建议:升级后,如果此前 frpc verify 对开启 VirtualNet 的配置误报,可直接重跑验证:
frpc verify -c frpc.toml
四、修复三:subDomainHost 域名校验的大小写绕过
4.1 安全背景
HTTP/HTTPS 代理中,subdomain 模式会把流量路由到 subDomain.<subDomainHost>;为避免客户端用 customDomains 注册与子域主机冲突的域名,服务端侧校验要求 custom domain 不能落在 subDomainHost 之下。
绕过的成因:旧校验做字符串后缀比较时未统一大小写。攻击者只需提交混合大小写的域名(如 A.fRp.ExaMpLe.CoM),就能通过本应命中的 HasSuffix 判断,把 subDomainHost 子域下的自定义域名注册进来,破坏子域路由的隔离预期。
4.2 修复实现与测试
修复在 pkg/config/v1/validation/proxy.go 的 validateDomainConfigForServer:比较前对 subDomainHost 和每个 custom domain 都执行 strings.ToLower 归一化,再做"标签数更长且以 .subDomainHost 结尾"的后缀判断,命中即报:
custom domain [xxx] should not belong to subdomain host [yyy]
测试 proxy_test.go 用 FRP.Example.Com(混合大小写)作为 subDomainHost 的用例确认了大小写不再影响判定。该修复对使用者的影响是:此前能"侥幸"通过校验的混合大小写冲突域名,升级后会被显式拒绝,属于行为收紧,升级时如遇新报错,应按提示调整 customDomains。
五、升级与自验证建议
- 兼容性:本次变更全部向后兼容——UDP 二进制编码仅在双方都支持 v2 且协商成功时启用,v1 用户零影响;两项校验修复属于"拒绝非法输入",正常配置不受影响,仅混合大小写冲突域名会被新规则拦截;
- 验证配置:对启用了
featureGates的配置,用frpc verify -c <配置文件>做一次基线校验; - 复现测试:仓库内的 control_test.go、verify_test.go、proxy_test.go 与 udp_binary_test.go 覆盖了三处修复和编解码的边界行为,可运行
go test ./server/... ./cmd/... ./pkg/msg/...自行确认。
本次发布的主线可以概括为"数据面更紧凑、控制面更严格":UDP 通道在高版本之间获得更省带宽的二进制表示,同时登录协商与域名校验两个入口堵上了由不可信输入引发的滥用面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00