先中转、后打洞:libnpf 的 NAT 穿透与对称型 NAT 实战

先中转、后打洞:libnpf 的 NAT 穿透与对称型 NAT 实战

NAS、家里的电脑、路由器后的摄像头都藏在 NAT 后面,彼此"看不见"。本文介绍开源 C 库 libnpf 怎样探测 NAT、怎样打通对称型 NAT,以及怎样做到"打开就能用":先走 UDP 中转,后台慢慢打洞,打通后无缝切到直连,UDP 实在不通才用 WebSocket 中转。它体积小,又特别适合视频的点对点传输。

一、libnpf 是什么

libnpf 是一个 C99 写的 P2P 网络库:让 NAT 后面的设备打洞直连,打不通自动走中转,建立端到端加密的会话,承载端口转发、应用数据和音视频。

三条路径的优先级是 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 把"能用"和"直连"拆开:

  1. 呼叫时平台为双方分配 UDP 中转。E2E 握手一完成,AnLiu 连接就跑在中转上,用户马上能用;
  2. 打洞在后台进行,失败按 10 秒、30 秒、1、2、3 分钟退避,最多 10 轮;
  3. 打通后切到直连,中转热备 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 只作兜底)。

八、小结

  1. 按映射、过滤、端口分配三个维度测 NAT;
  2. 对称型 NAT 分情况打:预测、生日悖论、宽松过滤、慢速对喷,数学上不划算的就不打;
  3. 控制 socket 数,别耗尽运营商端口;
  4. 会话先跑在 UDP 中转上,同一个 AnLiu 连接只换出口,重复和丢包交给 AnLiu;UDP 不通才用 WebSocket 兜底;
  5. 代码段几百 KB,音视频走半可靠流,适合嵌入式设备和视频点对点。

这些对应用都是透明的:SDK 只有一个头文件 npf.h。设计文档见仓库的 docs/ 目录。

相关推荐
优信电子1 小时前
HC05蓝牙模块主机与从机进行连接通信
单片机·物联网·嵌入式
K成长日志1 小时前
BLE Host层L2CAP -- 配置参数选项
物联网·网络协议·嵌入式·蓝牙·iot·ble·bluetooth
en.en..2 天前
第一篇:位域打包:看懂 I2C 地址左移的底层逻辑
stm32·嵌入式·位运算·i2c·底层驱动
youcans_2 天前
【嵌入式软件AI编程】21. 整理通用提示词模板
stm32·单片机·嵌入式·ai编程
番茄灭世神2 天前
PN学堂GD32课程第15篇——HardFault异常
c语言·单片机·嵌入式
LCG米3 天前
STM32串口接收卡死根因排查:ORE溢出标志陷阱、错误标志清除顺序与中断恢复实测
stm32·单片机·嵌入式硬件·串口·嵌入式
阿源-3 天前
从 INF、DSC、DEC 到 FDF:理解 EDK II 的 UEFI 编译与固件生成流程
嵌入式·uefi·edk2·固件
2035去旅行3 天前
SOC 里的 CPU 处理器 和 L1/L2 Cache
嵌入式·arm·soc
优信电子3 天前
STM32 驱动 SHT35 温湿度传感器:从 I²C 通信到 CRC 校验的完整实战
单片机·嵌入式