Repository navigation
v0.6.4
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! 里被取消时那批字节随之消失。丢掉的可能正是请求体开头。
两次修复尝试都失败了:
- 改成
read_buf直接读进self.buf+ 缓冲非空立即返回
⇒ 调用方的loop { select! }变成 100% CPU 忙等死循环
(云端 4 进程各烧 20 分钟 CPU,本地单进程 1288 秒) - 再在循环里加短路判断 ⇒ 仍然空转
最终处理:回退,并在代码注释里完整记录失败过程与正确方向。
判断依据:宁可保留一个只在 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-clientcrates.io 上三个 crate 均已更新到 0.6.4:
nfrp-common ·
nfrp-server ·
nfrp-client
完整记录
SECURITY-FIXES-ROUND14.md—— 本轮SECURITY-FIXES-ROUND4..13.md—— 前几轮SECURITY-FIXES.md—— 第一~三轮汇总
累计十轮安全审计 + 两轮外部渗透测试,146 项修复,全部带回归测试。