Repository navigation
v0.6.5
Nfrp v0.6.5
Rust 实现的内网穿透工具,完整兼容官方 frp 协议(wire protocol v1 / v2,与 frp 0.71.0 实测互通)
本版有一个必须升级的理由:修掉了 OIDC 的 ES256 验签缺陷 —— 在那之前,
用 ES256 的 IdP(Keycloak / Auth0 / Azure AD 的默认算法)签发的 token 全部验不过。
另外补齐了 4 个配置字段,并把协议状态机测试从"降级探针"升级为完整枚举。
一、最重要的修复:ES256 验签用了错误的签名格式
问题
pick_algorithm 里 ES256/ES384 用的是 *_ASN1(期望 DER 编码),
但 JWS 的椭圆曲线签名不是 DER。RFC 7518 §3.4:
Concatenate the two octet sequences in the order R and then S.
The resulting 64-octet sequence is the JWS Signature value.
验证侧同样强制:
The JWS Signature value MUST be a 64-octet sequence.
影响
任何真实 IdP 签发的 ES256 token 都会验签失败 —— OIDC 认证在多数生产环境不可用。
(RS256 不受影响,所以用 RS256 的部署一直正常。)
为什么之前没发现
有一条测试向量本身是错的:VEC_EC_SIG 以 0x30 0x44 0x02 0x20 开头,
是 DER 编码 —— 当初用 openssl 生成,而 openssl 默认就输出 DER。
于是「错误实现 + 错误向量」一起错,测试全绿、线上全挂。
教训:自造测试向量只能证明"实现和向量一致",证明不了"和标准一致"。
修复
改用 ECDSA_P256_SHA256_FIXED / ECDSA_P384_SHA384_FIXED(r‖s),
并把测试向量换成 RFC 7515 附录 A.3 的官方权威向量,另加一条反向门
(DER 编码的签名必须验不过)。
二、真实 IdP 端到端测试(新增 9 条)
之前的 27 条 OIDC 测试全是纯函数级(DER 编码、base64url、JWT 分段),
从来没有用真签名跑过一遍 —— 上面的 bug 正是这么漏掉的。
现在在本机起一个最小 HTTP server 扮演 IdP,提供 discovery + JWKS,
用真实 ECDSA P-256 密钥签发 JWT,整条链路真跑:
| 覆盖 | 说明 |
|---|---|
| 主干链路 | discovery → JWKS → ring 验签 |
| 密钥轮换 | IdP 换密钥后 refresh() 能拉到新的 |
| 未知 kid | 找不到 key 必须拒绝,不能放行 |
| 签名篡改 | 改一个字符就拒 |
| 空 JWKS | {"keys":[]} 必须报错,不能当"于是全放行" |
| 畸形 JSON | 非 JSON 要报错 |
| 垃圾坐标 | x/y 非法 base64 时不 panic |
| 缺 jwks_uri | 必须报错 |
| 明文 jwks_uri | 默认拒绝(公钥走明文 = 认证可被绕过) |
三、完整的协议状态机枚举
上一轮只有"降级探针"(喂 2 条跨状态消息看服务端不崩)。现在做真正的枚举:
3 个协议阶段 × 20 条消息 = 60 个组合,每条都喂给真实服务端:
- 阶段:
PreLogin(未登录)/PostLogin(已登录未注册)/Registered(已注册) - 消息:覆盖
FrpMessage全部 20 个变体,含服务端→客户端方向的那些
(客户端发它们就是方向违例,正是要测的输入)
断言三件事:
- 不崩 —— 之后用新连接仍能正常登录并注册代理;
- 不挂住 —— 控制连接必须"要么还活、要么已被关闭",
不能既不响应也不断(那种会话会永久占住服务端槽位); - 不泄漏 —— 断开后该端口上的后端计数归零。
四、四个配置字段真正生效
之前它们只在"未实现字段"清单里告警,现在都真的能用。
quicBindPort(服务端)
配了就开独立 UDP 端口,与 bindPort 无关:
quicBindPort = 7101配了之后不再顺带占用 bindPort/udp(那是旧行为的隐式副作用)。
只依赖 transport_protocol = "quic" 的部署不受影响。
includes(客户端)
includes = ["conf.d/*.toml"]按官方语义:每项是路径或 glob;命中文件只取 proxies 与 visitors 追加,
其它字段(serverAddr/token…)一律忽略 —— 它是"批量追加代理定义",
不是配置继承/覆盖。
细节:glob 语义对齐 Go filepath.Match;相对路径以主配置所在目录为基准;
目录内容排序后加载;合并后重名明确报错。
tokenSource(客户端 + 服务端)
[auth.tokenSource]
type = "file"
[auth.tokenSource.file]
path = "/etc/nfrp/token"或 type = "exec" 跑子进程取 stdout。读到的值去首尾空白
(文件末尾的换行不 trim 就对不上)。
优先级:命令行 -t > tokenSource > token。
exec 的安全处理:不经 shell(command 与 args 分开传)、
10 秒超时后强杀(否则卡住的子进程会让客户端永远起不来)、
启动日志打出令牌来源、取到的值绝不进日志。
enablePrometheus(服务端)
enablePrometheus = true默认关闭(与官方一致)。此前 /metrics 是无条件开放的 ——
指标含业务信息(在线客户端数、代理名、流量字节),"只想开个面板"的部署
不该默认多暴露一份数据面。
/api/healthz 不受影响 —— 健康探针永远可用。
质量门
| 项 | 结果 |
|---|---|
cargo test --workspace(串行) |
✅ 811 通过 / 0 失败(上轮 785,+26) |
cargo fmt --all -- --check |
✅ 干净 |
cargo clippy --workspace --all-targets -D warnings |
✅ 0 告警 |
cargo audit |
✅ exit 0 零漏洞(184 crates) |
| 退回验证(ES256) | ✅ 退回变红 / 还原变绿 / 文件干净 |
兼容性
- 协议零变更:与官方 frp 0.71.0 互通不受影响
- OIDC 修复是纯增益:以前 ES256 完全不能用,现在能用(RS256 不受影响)
- 两处行为变更(都向官方对齐,已写进示例配置):
/metrics默认关闭,要采集需配enablePrometheus = true- 配了
quicBindPort后不再占用bindPort/udp
- 其余新配置项都是"配了才生效",不配时行为与之前完全一致
-v仍输出裸0.71.0;-V输出0.6.5
已知未解决
plugin_bridge在 Linux 上的竞态(HttpIo::watch_closed取消时丢字节):
已确认是真问题(10 次跑 5 次失败,真实 TCP 链路可复现),
但两次修复尝试都引入 100% CPU 忙等死循环,已回退并在代码里记录正确方向
(让"读"只有一个所有者,需重构所有权)。Windows 上稳定。- Windows VirtualNet 仍未实现(需 Wintun 驱动 + 管理员权限 + 真机验证)
- TSan / Miri 本轮未跑完(云端 nightly 下载未完成)
- 其余配置字段:
useEncryption/useCompression(数据面协议级改动)、
udpPacketSize、dnsServer、proxyURL、pprofEnable、assetsDir、
sshTunnelGateway、httpPlugins、natholeAnalysisDataReserveHours
资产
| 文件 | 说明 |
|---|---|
nfrp-windows-amd64.zip |
Windows amd64 |
nfrp-linux-amd64.tar.gz |
Linux amd64 |
nfrp-linux-arm64.tar.gz |
Linux arm64(全静态) |
nfrp-android-v0.6.5.zip |
Android aarch64 + x86_64 |
nfrp-src-065.tar.gz |
完整源码 |
SHA256SUMS.txt |
各包 sha256 |
SHA256SUMS.txt.sig |
Ed25519 分离签名 |
release.pub |
验签公钥(指纹 763896b5d759c6cbe1f208c3c35c9f71) |
cargo install nfrp-server nfrp-clientcrates.io 三个 crate 均已更新到 0.6.5。
完整记录
本轮详细报告:SECURITY-FIXES-ROUND15.md