先中转、后打洞:libnpf 的 NAT 穿透与对称型 NAT 实战
NAS、家里的电脑、路由器后的摄像头都藏在 NAT 后面,彼此"看不见"。本文介绍开源 C 库 libnpf 怎样探测 NAT、怎样打通对称型 NAT,以及怎样做到"打开就能用":先走 UDP 中转,后台慢慢打洞,打通后无缝切到直连,UDP 实在不通才用 WebSocket 中转。它体积小,又特别适合视频的点对点传输。
一、libnpf 是什么
libnpf 是一个 C99 写的 P2P 网络库:让 NAT 后面的设备打洞直连,打不通自动走中转,建立端到端加密的会话,承载端口转发、应用数据和音视频。
- 仓库:github.com/wanghengwen...(MIT)
- 底层传输:AnLiu(暗流),基于 UDP 的可靠传输,支持加密、多流、BBRv2、SACK/RACK、FEC
- 平台:Linux、macOS、iOS、Android、Windows

三条路径的优先级是 P2P 直连 > UDP 中转 > WS 中转。
体积:libnpf 按嵌入式设备的标准做了小型化。

x86_64 上用 -Os 编译,libnpf.so 的代码段共 733 KB,其中 TLS(mbedTLS)就占 231 KB。只被连接的设备可以关掉主动连接功能(NPF_PEERS=OFF),用 musl 静态链接后,整个程序(含 C 库、TLS 和网页控制台)代码段只有 607 KB,路由器、摄像头、NAS 都放得下。
二、NAT 要按三个维度看
"全锥 / 限制锥 / 对称"四分类太粗。libnpf 按 RFC 4787 分三个独立维度:
| 维度 | 取值 |
|---|---|
| 映射 | EIM(与目标无关)、ADM(与目标 IP 有关)、APDM(与 IP:端口 有关,即"对称型") |
| 过滤 | EIF(谁都能进)、ADF(发过包的 IP)、APDF(发过包的 IP:端口) |
| 端口分配 | 保持、顺序、端口块(运营商 CGNAT 常见)、随机 |
映射为 EIM 的一侧叫易侧 ,ADM/APDM 的叫难侧。
三、NAT 探测

两台不同 IP 的服务器凑出 E1、E2、E3 三个端点。设备用同一个 socket 依次请求,比较三个映射地址:全相同是 EIM,只有 E3 不同是 ADM,E1、E2 就不同是 APDM。再开 8 个新 socket 看端口序列,判断顺序、端口块还是随机。
过滤行为的测法:留一个只向 E1 发过包的 socket F,请 E2、E3 从自己的地址给 F 发响应。F 收到 E3 的是 EIF,只收到 E2 的是 ADF,都收不到是 APDF。
报文借鉴 STUN 的机制,但不用 STUN 的格式。STUN 有明文头和固定的 magic cookie,一条规则就能屏蔽。libnpf 的报文整包加密(ChaCha20 + SipHash 的 SIV 构造),长度随机,服务器对未认证的请求不作任何回应。
四、对称型 NAT 怎么打
先按双方画像选策略:
| 本端 \ 对端 | EIM | 难侧·顺序 | 难侧·端口块 | 难侧·随机 |
|---|---|---|---|---|
| EIM | S1 同时打洞 | S2 端口预测 | S3(缩小范围) | S3 生日悖论 |
| 难侧 | --- | S4,否则 S2B | S4,否则 S5 | S4,否则不打 |
S1 最简单:平台下发一个服务器时间的开始时刻,双方同时向对方发包,实测 0.23 秒打通。
S3:生日悖论暴力打洞

一侧 EIM、一侧随机端口的对称型 NAT 时,难侧开 N 个 socket(默认 256 个)都发往易侧,在自己的 NAT 上留下 N 个"允许易侧进来"的映射。易侧向难侧公网 IP 的随机端口喷 M 个包,打中任意一个就双向打通。命中率是生日问题:
scss
P ≈ 1 − exp(−N·M / 64512)

N = 256、M = 1024 时约 98%。难侧是端口块时只喷块内,成功率更高。实测易侧在家宽、难侧在云厂商 NAT 网关后,喷 142 个包命中,0.9 秒打通。
两边都是难侧
- S4:任一方过滤是 EIF/ADF(只看 IP 不看端口),这一方开 N 个 socket 接收,另一方喷包。命中率同 S3,与端口分配无关,优先用;
- S2B:双方都是顺序分配。每轮现测当前端口,主叫升序、被叫降序对喷,各 32 个一遍,期间被占端口合计少于约 30 个就能一轮打通;
- S5:双方都是小端口块。每秒 4 个包慢慢对喷 5 分钟,512×512 的块平均约 4.5 分钟打通。会话这时已在中转上,不急;
- 不打:双随机 + 双 APDF 时,两边各发 1000 个包成功率约 0.5%,平台直接让会话留在中转。
每轮打洞还会上报"对方朝向本方的真实端口"。有不请自来的包就说明本方其实是 ADF,下一轮改用 S4;端口集中在一段就只喷这一段。
别打爆运营商的端口配额:某手机卡每用户约 500 个端口,两个难侧打洞同时开了 512 个 socket,新的 UDP 流全部不通。所以同一主叫只允许一个难侧打洞,两次间隔 15 秒,打通后立即关闭多余 socket。
五、先 UDP 中转,后台慢慢打洞
打洞快的不到 1 秒,慢的几分钟。如果"打通才能用",用户就得干等。libnpf 把"能用"和"直连"拆开:

- 呼叫时平台为双方分配 UDP 中转。E2E 握手一完成,AnLiu 连接就跑在中转上,用户马上能用;
- 打洞在后台进行,失败按 10 秒、30 秒、1、2、3 分钟退避,最多 10 轮;
- 打通后切到直连,中转热备 30 秒,往返检查通过后才释放。
中转服务器只转发 AnLiu 的端到端加密数据报,看不到内容。
无缝切换靠 AnLiu
中转和直连上跑的是同一个 AnLiu 连接(conv 不变,MTU 统一为 1200)。AnLiu 不碰 socket,通过 output 回调交出数据报,"走哪条路"完全由回调决定:
c
static int anl_out(char *buf, int len, anl_t *w, void *user)
{
npf_call *c = user;
if (c->p2p) /* 打通后走直连 */
udp_send(c, c->p2p_fd, &c->p2p_addr, (const uint8_t *)buf, (size_t)len);
/* 没有直连时走中转;直连可疑时两边都发,AnLiu 去重 */
if ((!c->p2p || c->p2p_suspect) && c->relay.active)
relay_send_data(c, (const uint8_t *)buf, (size_t)len);
return 0;
}
切换瞬间的问题都交给 AnLiu:
- 重复:中转上的在途包、故意双发的包,AnLiu 按序号去重;
- 丢包:SACK/RACK 检测并重传;
- 乱序:可靠流按序交付;
- 路径变化:BBRv2 重新估计带宽和 RTT。
所以切换逻辑可以很简单:拿不准时两条路都发。直连上 3 秒(或 2 倍 RTO)收不到对端报文就进入"疑似失效",双发 10 秒,同时每 500 ms 做一次往返检查。检查通过就恢复直连,否则留在中转并重新打洞。直连只是优化,打不通或中途断掉,连接都不会断。
六、为什么特别适合视频点对点
视频要的是"新",不是"全":一帧迟到了,比丢掉还糟。libnpf 的每一路音视频就是一条 AnLiu 的半可靠流,帧原样发送:
- 过期整帧丢弃 :帧在队列里超过
max_age_ms(默认 500 ms)就整帧丢掉,已发出但超时的不再重传,通知接收端跳过; - 关键帧感知 :开了
drop_until_key,丢帧后直接跳到下一个关键帧,不花屏;回调里的lost_before可以直接驱动解码器; - 按流 FEC:Reed-Solomon 前向纠错,固定或自适应冗余,丢包不等重传就能恢复;
- 码率建议 :BBRv2 测瓶颈带宽和最小 RTT,不塞满队列;
npf_media_stats_get给出扣除包头、FEC 和重传后的target_rate,编码器据此调码率,排队时立即下调; - 多路并发、优先级:每个会话每方最多 30 路,与端口转发共用一个连接,音视频优先于文件传输。
c
npf_media_opt mo;
npf_media_opt_default(&mo);
mo.max_age_ms = 300; /* 300 ms 发不出就丢 */
mo.drop_until_key = 1; /* 丢帧后等关键帧 */
uint32_t m = npf_media_open(n, session, &mo, &err);
npf_media_send(n, m, key ? NPF_FRAME_KEY : 0, frame, len);
再加上前面的路径设计:走中转时画面立刻就有,打通后切到直连延迟下降,切换过程中画面不中断。
七、兜底:WebSocket 中转
公司防火墙只放 443、代理吞掉 UDP 时,NAT 探测收不到任何回应,或者 UDP 中转绑不上,才改用 WS 中转:
- 独立的 WSS 连接,走 443 端口,看起来和普通 HTTPS 一样;
- 先做 Noise_XK 握手,数据用端到端的 ChaCha20-Poly1305 加密;
- TCP 本身可靠,所以不跑 AnLiu,帧格式与 AnLiu 路径相同,上层不区分底下是哪条路。
为免误判,打洞占着端口导致中转绑不上时,先等最多 15 秒;探测无回应后定期再测,UDP 恢复后会话重建回 UDP(这一步目前会中断现有连接,所以 WS 只作兜底)。
八、小结
- 按映射、过滤、端口分配三个维度测 NAT;
- 对称型 NAT 分情况打:预测、生日悖论、宽松过滤、慢速对喷,数学上不划算的就不打;
- 控制 socket 数,别耗尽运营商端口;
- 会话先跑在 UDP 中转上,同一个 AnLiu 连接只换出口,重复和丢包交给 AnLiu;UDP 不通才用 WebSocket 兜底;
- 代码段几百 KB,音视频走半可靠流,适合嵌入式设备和视频点对点。
这些对应用都是透明的:SDK 只有一个头文件 npf.h。设计文档见仓库的 docs/ 目录。