Skip to content

v0.6.5

Choose a tag to compare

@Nu0vo1212 Nu0vo1212 released this 24 Oct 15:15
· 20 commits to main since this release

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 个变体,含服务端→客户端方向的那些
    (客户端发它们就是方向违例,正是要测的输入)

断言三件事:

  1. 不崩 —— 之后用新连接仍能正常登录并注册代理;
  2. 不挂住 —— 控制连接必须"要么还活、要么已被关闭",
    不能既不响应也不断(那种会话会永久占住服务端槽位);
  3. 不泄漏 —— 断开后该端口上的后端计数归零。

四、四个配置字段真正生效

之前它们只在"未实现字段"清单里告警,现在都真的能用。

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 不受影响)
  • 两处行为变更(都向官方对齐,已写进示例配置):
    1. /metrics 默认关闭,要采集需配 enablePrometheus = true
    2. 配了 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-client

crates.io 三个 crate 均已更新到 0.6.5。


完整记录

本轮详细报告:SECURITY-FIXES-ROUND15.md