NAT的本质:一个聪明又讨厌的状态机
你在家里打开电脑,联网,一切看似顺理成章。但背后有一台设备在偷换数据包的地址,它就是NAT。别把它想成简单的翻译,这玩意儿是一个复杂的状态机。核心是那张连接跟踪表(conntrack),每一条记录包含五元组:源IP、源端口、目的IP、目的端口、协议,外加状态和超时时间戳。 比如你访问百度,路由器收到内网机器发来的SYN包,立即在表中插入一条:192.168.1.2:12345 <-> 120.240.0.1:80, tcp, SYN_SENT,然后把源地址改成公网IP并分配一个新端口,比如45678。等百度回应SYN-ACK,路由器查表,改回内网地址,转发。看起来很流水线?其实坑在后面。
性能损耗:硬件加速是救命稻草
很多人以为NAT只是改个包头,性能损耗可忽略。但实际不是。每个数据包都要经过连接跟踪查找、NAT规则匹配、地址翻译、校验和重算。在没有硬件卸载的纯CPU软NAT下,性能急剧下降。我做过实测:一台x86工控机跑OpenWrt,开启软NAT,千兆带宽BT下载时CPU占用几乎100%,吞吐量只有300Mbps左右。换用硬件加速(启用Shortcut-FE或类似的CTF),同样场景吞吐量可以跑到940Mbps,CPU占用仅15%。看这组数据更刺激:64字节小包测试,软NAT转发速率8万pps,延迟1ms;启用硬件NAT后,转发速率升至200万pps,延迟降到50μs以内。 这种差距在游戏、VoIP等小包高频场景下是天地之别。不过硬件NAT也有代价,一旦启用,流量绕过了Netfilter框架,你的QoS、流量统计、连接数限制全部失效。鱼和熊掌不可兼得。有些高端路由器采用混合模式,让有加速需求的数据流走硬件,控制类走CPU,这才是工程之美。
落地前的三个深坑及填坑指南
坑一:连接跟踪的超时陷阱 TCP连接有优雅关闭,但NAT可不管。很多NAT设备对已建立但空闲的TCP连接默认超时时间短得离谱,比如15分钟;对UDP更狠,有时30秒就清了。你的长连接应用可能突然死掉,双方都懵了。我曾经在维护一个物联网平台的MQTT服务时就栽在这上面:设备TCP连接一切正常,没数据时就在那挂着,可过十几分钟设备再也推不上来,日志显示连接重置,后来查到是中间NAT盒子把记录清了。教训惨痛。解决方法:必须心跳。TCP场景下开启SO_KEEPALIVE,调节tcp_keepalive_time为600秒,keepalive_intvl为60秒,probes为3次。 这样即使空闲,也会定期探测,维持NAT表项。UDP更激进,每25秒发个无意义的心跳包,同时监听响应。实践中发现,对于移动端,考虑省电,心跳间隔略大于NAT超时一半就行,比如NAT超时60秒,心跳30秒较稳。 坑二:多层NAT下的端口映射彻底失效 运营商大规模推CGN(Carrier-Grade NAT),你要是分配到100.64.x.x或者10.x.x.x,那恭喜你,你后面还有一层大NAT。此时你做了端口映射,外网通过你路由器公网IP根本连不进来,因为你路由器公网IP本身也是内网地址。这就是NAT444死局。什么BT下载没速度,远程桌面连不上,都是常态。个别运营商甚至会拦截入向连接。我的解决方案排序:第一,打电话投诉要公网IP,很多运营商能给(至少电信还能)。第二,换IPv6,只要两端都有,直接端到端。第三,中继转发(TURN),但需要服务器,成本高。 还有个野路子:如果支持PCP(Port Control Protocol),可以向CGN请求映射,但运营商几乎不开。这坑无解,只能盼IPv6普及。 坑三:对称型NAT导致P2P打洞成功率惨淡 做过P2P视频通话的都知道,对称型NAT是噩梦。我们统计过超过2万次打洞尝试:在两端家庭网络都是对称型的情况下,UDP打洞成功率不足5%;一端对称另一端端口受限锥形,成功率约22%;两端都是端口受限锥形,成功率能到85%。 对称型的端口分配毫无规律,无法预测。我们被迫采用中继,但全中继成本太高。后来改进策略:同时进行UDP打洞和TCP打洞,并优先尝试已知端口范围(比如有些对称NAT分配端口有递增规律)。TCP同时打开机制可以利用,但需要精确时序。我们的实践是:先发起UDP打洞,如果0.5秒内未收到对方包,立即启动TCP同时打开,并同时向TURN服务器请求分配中继资源。 在第一个数据包成功到达并建立连接后,断开其他备用通道。这套组合拳把连接成功率提升到了近90%,非常有效。顺便吐槽一句:就算有UPnP,很多路由器实现也有bug,别太指望它。
作者|大讲堂
排版|大讲堂
审核|阿辰
大讲堂