CGNAT 续集:两个月后,我在 miniupnp 的上游 PR 里审自己踩过的坑
七月中旬我写过一篇《在路由器上手搓 MiniUPnPd:CGNAT 穿透实录》,讲我怎么在路由器上从源码编译 miniupnpd,绕过运营商的 CGNAT 把 UPnP 救活。写完我以为这事就翻篇了。
九月初开始,邮箱里连续几天躺同一个 GitHub 通知:miniupnp 上游有个 PR,编号 764。
点进去看完第一个 commit 标题,我愣了几秒——这个 PR 在修的东西,和我家路由器上那条链路,是同一条。
这个 PR 在干什么
PR 是 9 月 9 日开的,作者 Self-Hosting-Group,7 个 commit,动了 19 个文件,base 是 master。到我写这篇的时候它还是 open 状态,没合并。
几个 commit 标题单看就很眼熟:
- miniupnpd: Improve start banner and add security warnings —— 启动横幅里加安全警告,配置不安全的话开机就告诉你
- miniupnpd: Less excessive logging by default —— 默认日志降噪
- miniupnpd: Log IPv4 mapping disabled/enabled messages by default —— 映射被禁用/启用的时候记一条日志
- miniupnpd: Fix IPv4 mapping re-enable after network down/up on CGNAT —— 外网口 down/up 之后,CGNAT 模式下的 IPv4 映射要重新 enable
第一条戳我的地方在于:我七月份为了搞明白 miniupnpd 在我这个配置下到底会怎么行为,是去翻源码、翻 config.h、一个个特性开关试出来的。那些「这样配是不安全的」「那样配会静默失败」的结论,当时全在我自己的脑子里。而这个 PR 想做的事,是把这类结论直接印在启动横幅上,让下一个人开机就能看见。
最后一条是真正让我坐直的那条。它修的是 CGNAT + STUN 模式下的一种失败:外网接口一旦 down/up,STUN 探到的外部地址要重算,而旧逻辑不会把已有的 IPv4 映射重新 enable 一遍,于是映射就悄悄死了,表现就是「之前好好的,突然不通了」,重启服务又好。我旧文里梳理过整条链路——LAN 客户端、UPnP 映射、主动出站打洞、CGNAT 的 conntrack——所以一看这个 commit 标题,我立刻知道它在修链路上的哪一环。这种失败模式我家没碰到过(我外网口不常抖),但看得懂它为什么重要,和亲身被它坑过,其实只差一点运气。
我做了什么
我把 branch 拉下来,逐个 commit 看了一遍。重点看两件事。
一是 STUN 开和不开两条路径是不是都对。CGNAT 场景下外部地址来自 STUN 探测,非 CGNAT 场景下来自 WAN 口本身;接口抖动时这两条路径的重算逻辑必须分开验证,不然修好一个弄坏另一个,而且坏的那个恰好是大多数人在用的那条。
二是日志。miniupnpd 啰嗦起来是很要命的,我旧文里被它刷过 syslog。所以「默认降噪」和「映射 disable/enable 记一条」必须放在一起看:该安静的时候安静,该留证据的时候留得住。只降噪不留痕,下次映射悄悄死掉还是查不到;只留痕不降噪,日志又回到刷爆的老路。
看完没发现问题,点了 approve。这个 PR 现在有三个 reviewer 批准,我是其中之一。
一点感受
开源的回路有时候短得让人意外。
七月,我在自己家被一个行为折腾了一晚上,最后靠绕路解决,顺便写成了文章。九月,一个不认识的人在上游动手修这一片,改法和我绕的路不一样,但诊断落在同一条链路上。而我恰好是那个「因为被折腾过、所以看得懂它为什么重要」的人,于是我能做的贡献,就是认真把它审完,确认它没有修好一个弄坏另一个。
这种感觉挺难描述的。有点像你在山里自己趟出一条路,两个月后发现有人把这条路修成了步道,而修路的人递给你一把验收用的尺子。
PR 还没合并。等它进了某个 release,我打算在路由器上真跑一遍,把外网口 down/up 之后映射自动恢复的过程完整记下来,再写一篇实测。到时候如果它和我预期的一样工作,这篇续集就算真正写完了。
最后把旧文结尾那句话再贴一遍,因为这次依然成立:先确认你的 NAT 类型是 full cone,不然 CGNAT 穿透是不可能的。对称 NAT 下每次出站的端口偏移不固定,没法提前告知对端,打洞就是白搭。我家是 full cone,才能走通这条路。你那边要先查清楚。