Skip to content

v0.6.4

Choose a tag to compare

@Nu0vo1212 Nu0vo1212 released this 24 Oct 12:11
· 21 commits to main since this release

Nfrp v0.6.4

Rust 实现的内网穿透工具,完整兼容官方 frp 协议(wire protocol v1 / v2,与 frp 0.71.0 实测互通)

本版处理第三方渗透测试反馈的三项 HTTP 语义问题,并修掉一个长期存在的测试 flake。
无配置变更、协议零变更。


一、HTTP 语义修正(渗透测试 PT-03 / PT-05 / PT-07)

PT-03 · HEAD 不再返回响应体

修复前:HEAD / 返回 200 + 10368 字节 HTML,HEAD /api/status 返回 659 字节。

RFC 9110 §9.3.2 要求 HEAD 只回响应头、不回 body。
危害有限(不是漏洞,是协议违规),但会带来实际问题:

  • 用 HEAD 做健康检查的监控会白白拉一遍完整页面
  • 某些缓存 / 代理对 HEAD 的 body 处理不一致,可能污染缓存

修法:HEAD 时不写 body,但 Content-Length 仍按真实长度发送 ——
那正是 HEAD 的语义(告诉对端 GET 会拿到多少字节,但不真的发)。

PT-07 · 只读端点不再接受 POST

修复前:POST /api/status 返回 200 + 完整状态 JSON。

根因:未匹配管理路径的 POST 会落进只读分支,被当成普通路径处理返回 200。
那既违反直觉,也让只按「状态码 200 就算成功」的客户端 / 监控误判。

修法:未匹配的 POST 一律 405,并带 Allow: GET, HEAD。
用 405 而非 404 —— 路径确实存在,只是方法不对,这是 HTTP 语义要求。

PT-05 · 新增安全响应头

面板此前一个安全头都没有。现在所有响应统一带:

头 作用
X-Content-Type-Options: nosniff 禁止浏览器嗅探 MIME
X-Frame-Options: DENY 挡点击劫持(把面板套进攻击者页面诱导管理员点「踢出」)
Referrer-Policy: no-referrer 面板 URL 不通过 Referer 泄露给外部站点
Content-Security-Policy default-src 'none' + 内联脚本样式 + frame-ancestors 'none'

为什么不用 HSTS:面板默认是明文 HTTP。对明文响应发 Strict-Transport-Security
会被浏览器直接忽略(RFC 6797 §7.2),发了没用;更糟的是会让人误以为已经上 HTTPS。

CSP 里为什么允许 'unsafe-inline':面板 HTML 的脚本与样式都是内联的单文件。
在自包含页面上,真正有价值的是 default-src 'none'(禁止一切外域请求、图片、
字体、frame),那部分已经生效。


二、测试 flake 消除

server/tests/e2e.rs 的 free_port_both() 存在端口竞态:

let p = free_port();                              // TCP listener 已 drop
if UdpSocket::bind(("127.0.0.1", p)).is_ok() {    // ← 中间有空窗
    return p;
}

两次 bind 之间没有保护,并行测试时另一个线程可能正好抢走这个号,
64 次全撞车并 panic。

修法:TCP 与 UDP 同时持有校验,两个都绑上才算数;
校验通过后先登记再释放。

验收:cargo test --workspace -j 2 连跑 5 次全绿。


三、依赖修复:tokio 1.54 的 API 弃用

上一版把 tokio 从 1.53 升到 1.54,新版本把 AsyncFd::new 标记为 deprecated
(理由:unsound due to lack of I/O safety,tokio PR #8587)。

common/src/vnet/tun_linux.rs 用到了它。这处只在 Linux 下编译,
Windows 上的 clippy 完全看不到 —— 是典型的「本地全绿、云端红」问题。

修法:改用官方替代品 unsafe fn register,并在代码里写明所有权保证
(fd 由 OwnedFd 独占、失败时 into_parts() 取回并 close,不泄漏)。
不压 #[allow(deprecated)] —— 那样只是让编译过去,真正要修的是 I/O safety 本身。


四、一次失败的修复尝试(如实记录)

排查中在 Linux 上发现 plugin_bridge 的 6 个端到端测试间歇失败,
且真实 TCP 链路也能复现,确认是产品代码的真实竞态:

  • 同一测试连跑 10 次:5 通过 / 5 次 5 秒超时
  • 失败时插件收到连接,但请求从未转给上游

根因:HttpIo::watch_closed() 用 read(&mut chunk) 读进局部数组,
future 在 select! 里被取消时那批字节随之消失。丢掉的可能正是请求体开头。

两次修复尝试都失败了:

  1. 改成 read_buf 直接读进 self.buf + 缓冲非空立即返回
    ⇒ 调用方的 loop { select! } 变成 100% CPU 忙等死循环
    (云端 4 进程各烧 20 分钟 CPU,本地单进程 1288 秒)
  2. 再在循环里加短路判断 ⇒ 仍然空转

最终处理:回退,并在代码注释里完整记录失败过程与正确方向。
判断依据:宁可保留一个只在 Linux 上间歇出现的缺陷,
也不引入一个会让 CPU 跑满的死循环 —— 后者影响面大得多。

正确方向(已写进注释,留给后续):让「读」只有一个所有者 ——
单独一个读协程独占 stream,把字节推进共享队列,等待方只等队列信号。
这需要重构 HttpIo 的所有权结构。

影响面:Windows 上 21/21 稳定通过;Linux 上仅 http2* / https2*
的端到端用例间歇失败,其余 780 个测试与 28 项冒烟全部正常。


质量门

项 结果
cargo test --workspace(串行) ✅ 785 通过 0 失败(v0.6.3 为 766,本版 +19)
cargo test --workspace -j 2 × 5 次 ✅ 5/5 全绿(flake 已消除)
cargo fmt --all -- --check ✅ 干净
cargo clippy --workspace --all-targets -D warnings(Windows) ✅ 0 告警
cargo clippy(Linux 云端,覆盖 Windows 编不到的代码) ✅ 0 告警
cargo audit ✅ exit 0 零漏洞(184 crates)
cargo deny check ✅ advisories / bans / licenses / sources 全 ok
端到端冒烟 ✅ 28 / 28
退回验证(三项修复) ✅ 3 / 3

新增 6 条测试,全部走真实 socket + 真实 HTTP:HEAD 不回 body、
GET 仍回 body(对照)、只读端点 POST 返回 405、四类响应都带安全头、
401 的 WWW-Authenticate 没被挤掉、安全头常量本身合法。


兼容性

  • 无配置变更:本版不涉及任何配置项
  • 协议零变更:与官方 frp 0.71.0 互通不受影响
  • HTTP 行为变化两处(均为修正,符合 RFC):
    • HEAD 不再返回 body
    • 只读端点的 POST 从 200 变成 405
  • 若有用例依赖「POST /api/status 返回 200」,请改用 GET
  • frps -v / frpc -v 仍输出裸 0.71.0;-V 输出 0.6.4

资产

文件 说明
nfrp-windows-amd64.zip Windows amd64
nfrp-linux-amd64.tar.gz Linux amd64
nfrp-linux-arm64.tar.gz Linux arm64(全静态)
nfrp-android-v0.6.4.zip Android aarch64 + x86_64
nfrp-src-064.tar.gz 完整源码
SHA256SUMS.txt 各包 sha256
SHA256SUMS.txt.sig Ed25519 分离签名
release.pub 验签公钥(指纹 763896b5d759c6cbe1f208c3c35c9f71)

校验

python scripts/release.py verify --dir .

其他安装方式

cargo install nfrp-server nfrp-client

crates.io 上三个 crate 均已更新到 0.6.4:
nfrp-common ·
nfrp-server ·
nfrp-client


完整记录

累计十轮安全审计 + 两轮外部渗透测试,146 项修复,全部带回归测试。