今天聊一个几乎每个做网络、搞联机应用、或者在家搭过服务器的人都绕不开的话题:NAT 穿透。
先说个真实场景。某个周末我在外面临时要连家里 NAS 拿一份资料,手机 APP 死活连不上,浏览器开管理页面也超时,家里一切正常,路由器看起来也并没有坏。折腾半天最后发现,问题根本不在 NAS 也不在路由器,而是我压根就没有公网 IP,整个内网被运营商级 NAT 罩在里面,外面的请求根本进不来。这个场景放在今天实在太常见了,我身边做开发的、玩自托管的、搞游戏联机的,几乎都踩过同一个坑。
这篇文章我想把 NAT 穿透这件事从头到尾讲透:NAT 为什么会挡住入站连接,它有哪些类型,穿透的几种主流方案各自原理是什么,真实打洞时有哪些工程细节,以及当穿透失败时怎么定位问题。适合刚接触内网穿透的新手,也适合已经在用各种工具但没搞明白底层逻辑的人。文章里不会出现具体商业产品名称,我会用通用的技术概念和方案类型来描述,原理都是一样的。
1. NAT 不是防火墙,但它把所有入站请求挡在了门外
1.1 公网地址不够用,才有了 NAT 这套“复用”机制
要理解 NAT 穿透,先得搞清楚 NAT 到底做了什么。IPv4 地址总数大约 43 亿个,但今天全球联网设备远远超过这个数。运营商和设备厂商不可能给每一台设备都分配一个公网 IP,于是就有了“局域网私有地址 + 出口统一转换”的方案。你家路由器拿一个公网 IP(或者更惨,跟几百户共享一个公网 IP),内网设备用 192.168.x.x、10.x.x.x 这类私有地址通信。数据包出家门的时候,路由器会把“内网 IP + 端口”改写成“公网 IP + 一个新的端口”,然后在自己的映射表里记一笔;响应数据回来时,路由器再根据映射表把包送回对应的内网设备。
这套机制的精髓在于那张映射表。我打个比方:NAT 路由器就像公司前台,内网每一台设备是公司员工。员工打外线电话时,前台记下“谁用了哪条外线打到哪去了”;外线电话打进来找某位员工时,前台得翻记录,如果这个员工从没往外打过电话,前台根本不知道该把电话转给谁。你的手机在外面访问家里 NAS 时,NAS 从没有主动向外发起过连接,路由器映射表里自然没有对应的记录,外来的数据包到了路由器这一层就被丢弃了。这就是“进不来”的根本原因。
很多教程喜欢说“NAT 是一种安全机制”,我建议别这么理解。它确实客观上挡住了一堆扫描和入站攻击,但它本来是为了解决地址短缺而生的,只是顺带产生了一个“屏蔽入站”的效果。你要是真把它当成安全边界来用,等到需要做 P2P 联机、远程访问、自建服务时,就会发现自己给自己挖了个坑。
1.2 映射表里藏着的三个关键信息
NAT 映射表每一条记录通常包含三样东西:内网地址端口、公网地址端口、外部目标地址端口。这里有个容易被忽略的点:不同的 NAT 实现,映射表建立的时机和更新策略不一样。大多数家用路由器的映射表是“外出时建立,超时后删除”;少量企业级设备支持“静态映射”或“端口转发规则”,那是管理员手工配置的,不在此列。
你还要知道,NAT 对 TCP 和 UDP 的处理逻辑是有差别的。TCP 有连接状态,连接关闭后映射条目通常很快回收;UDP 无连接,映射条目只能靠超时老化,这个超时时间各个厂商实现差别很大,短的 30 秒,长的能到几分钟。这个差异直接影响了后面讲“打洞保活”时的心跳间隔设多少。我见过有人把心跳设成 60 秒,遇到一个 UDP 超时只有 30 秒的路由器,通道每 2 分钟断一次,排查半天才找到原因。
2. 穿透前必须先认清 NAT 的四张面孔:从 Full Cone 到 Symmetric
2.1 STUN 检测:先知道自己在哪种 NAT 后面
NAT 穿透领域把 NAT 行为分成了几类,分类标准就是“同一个内网端口对外通信时,映射规则怎么变”。要判断当前网络属于哪一类,最常用的手段是 STUN(Session Traversal Utilities for NAT)。原理很简单:你的设备向公网上的 STUN 服务器发一个请求,服务器在响应里告诉你“我这边看到的你的公网地址和端口是什么”,你拿这个和本机网卡地址对比,再连续向不同的服务器端口发请求,观察映射端口是否变化,就能推断出 NAT 类型。
这条信息是后续所有方案选择的地基。我强烈建议你在做任何穿透尝试之前,先花几分钟做一次 NAT 类型检测。很多人在网上照着教程折腾半天还是连不上,最后发现原因就是自己处在最严格的对称型 NAT 后面,而教程方案根本就没考虑这种场景。
2.2 四类 NAT 的行为差异与判断方法
这四类 NAT 的官方定义和通俗解释如下:
Full Cone NAT(全圆锥型):一旦内网某端口建立了一条映射,任何外网主机都能通过这个映射地址访问内网设备,不管内网设备是否曾经主动联系过那个外网主机。这是最“开放”的 NAT,穿起来最容易。
Address Restricted Cone(地址受限圆锥型):同样是一个映射端口对外,但外网主机必须满足一个条件——内网设备曾经主动访问过它的 IP 地址,它的数据包才能进来。端口不限。
Port Restricted Cone(端口受限圆锥型):限制得更紧,不但要看外网 IP,还要看外网端口。内网设备曾经访问过的“IP + 端口”组合才能回包。如果某个外网 IP 的 8000 端口收到过你的主动访问,那么它从 8001 端口发来的包照样被丢弃。
Symmetric NAT(对称型):最严格的一种。每一次向不同的外部目标发起连接,NAT 都会分配一个新的公网端口,而且对入站数据同样实行“只响应曾主动访问过的目标”策略。换句话说,你永远无法复用同一个映射端口和不同主机通信。
判断方法上,关键看两个维度:第一,向不同的外部目标发请求时,映射端口是否保持稳定;第二,重复从同一个内网端口向同一个目标发请求,映射端口是否保持不变。用表格总结一下:
| NAT 类型 | 同一内网端口映射不同目标时的端口行为 | 外网主机能否主动访问 |
|---|---|---|
| Full Cone | 端口不变 | 任意主机均可 |
| Address Restricted | 端口不变 | 只有曾访问过的 IP |
| Port Restricted | 端口不变 | 只有曾访问过的 IP + 端口 |
| Symmetric | 每次都变 | 只有曾访问过的 IP + 端口 |
看到这里你可能反应过来了:NAT 越“开放”越好穿,而对称型 NAT 基本宣告了传统 UDP 打洞方案失效。下面讲的每一种穿透方案,本质上都是在和这张映射表博弈。
3. 穿透的技术路线:人工映射、UPnP、打洞、中继,各自解决什么问题
3.1 最朴素的路:端口映射和 DDNS
如果你的宽带还能拿到公网 IP(哪怕动态的),最简单可靠的方案就是手工做端口映射。登录路由器管理页面,把“外部端口 xxxx”转发到内网某台机器的“内部端口 xxxx”,再用 DDNS 解决公网 IP 变化的问题。这个方案思路清晰,前提条件也硬性:你必须有一个真正的公网 IP。
现实是,越来越多的宽带用户根本拿不到公网 IP。你登录路由器看到 WAN 口 IP 是 100.64.x.x 或 10.x.x.x 这类地址时,基本可以确定你处在运营商级 NAT(也叫 CGNAT)后面——运营商在光猫之上还套了一层 NAT,你自己再怎么做端口映射也是徒劳。我遇到过不少人折腾一晚上端口映射,最后发现 WAN 口地址本来就是私有的,方向从一开始就错了。
判断方法也简单:登录路由器看 WAN 口 IP,再在公网环境下用一个查询服务看自己的出口 IP,两者如果不一致,说明中间至少隔了一层运营商 NAT。这种情况下就别指望端口映射了,直接看打洞或中继方案。
3.2 UPnP:让应用自己去请求端口映射
UPnP 里的 IGD(Internet Gateway Device)协议允许内网设备自动向路由器申请端口映射。游戏主机、部分下载软件、一些 P2P 应用就是靠它自动开端口的。好处是配置基本为零,坏处是很多路由器的 UPnP 实现有安全隐患,也经常被运营商或安全策略关闭。
我个人的态度是:UPnP 适合临时场景,不适合作为生产方案。一是因为它的映射行为不受你完全控制,映射表里可能会攒下一堆陈旧的条目;二是因为某些固件的 UPnP 实现逻辑特别诡异,端口映射成功之后还会被其他规则覆盖。如果你真要依赖 UPnP,请定期检查路由器上的映射列表,确认实际生效的规则符合预期。
3.3 UDP 打洞:穿透的核心机制
打洞(hole punching)是 NAT 穿透最核心、也最优雅的机制。它的思路是:既然 NAT 只会放行“内网曾经主动访问过的目标”的回包,那就让双方先主动向对方“打一枪”,在各自 NAT 的映射表里建立一条准予通过的规则。
以两个客户端 A 和 B 都在不同 NAT 后面为例,中间有个公网信令服务器 S 帮它们交换地址。完整过程大致这样:
- A 向 S 发消息,S 记录下 A 的 NAT 公网地址和端口 a'。
- B 向 S 发消息,S 记录下 B 的 NAT 公网地址和端口 b'。
- S 把 b' 告诉 A,把 a' 告诉 B。
- A 向 b' 发一个 UDP 包,B 向 a' 发一个 UDP 包。这两枪可能有一部分会“打到空处”——因为对方 NAT 还没建立反向规则,但没关系,关键是让自己这边的 NAT 知道“我在和这个外部目标通信”。
- 接下来 A 和 B 继续互相发包。由于 NAT 已经记录了“内网主机曾向对方地址发起过通信”,双方的入站包都会被放行,一条直连通道就此打通。
关键在于“打洞包”要交错进行。A 打过去的包即便被 B 侧 NAT 丢弃,A 侧 NAT 也已经记住了 B 的地址;等 B 打回来的包到了 A 侧 NAT,A 侧 NAT 发现“这个地址我曾经访问过”,直接放行。B 那边同理。所以打洞不是一次就能成功,通常要连续发上几轮。
这个机制和我在 1.2 里提到的“UDP 映射超时”直接相关。如果打洞失败或通道建立后长时间没数据,映射条目会失效,通道就断了。所以 P2P 应用都有心跳保活机制,而且心跳间隔要针对“最坏情况下的 NAT 超时”来设置,不能拍脑袋定一个 120 秒就完事。
3.4 TCP 打洞和中继兜底
很多人会问:TCP 能不能打洞?能,但麻烦得多。TCP 协议有连接状态机,需要在两个端点之间完成三次握手,而 NAT 设备对 TCP 的处理比 UDP 严格得多。TCP 打洞通常利用“同时打开”(simultaneous open)技巧——双方在同一时间向对方的映射地址发起 SYN,报文路径上的 NAT 设备会认为这是自己内网主机发出的连接的响应,从而放行。但实际成功率远低于 UDP,而且在严格的对称型 NAT 后面基本无解。
所以绝大多数产品在打洞失败时会退化到中继模式。中继(relay)就是所有数据都经过公网服务器转发,客户端之间不再直连。标准方案是 TURN 协议,它的优点是突破性最强——只要客户端能连上公网服务器,数据就一定能通;缺点是带宽和延迟受限于中继服务器的性能,而且服务器流量成本很高。
到这里你应该明白一个核心结论了:穿透方案不是单选题,而是“先打洞,打不穿就中继”的组合拳。打洞是免费的午餐,但可遇不可求;中继是安全垫,保证功能可用性。至于具体怎么选,看下一节。
4. 打通内网的五种落地手段:从改路由器到组网产品
4.1 方案全景:每种手段的适用场景
我把常见的落地手段归成五类,按“人工介入程度”从低到高排:
手动端口映射 + DDNS:适合自己完全掌控网络和路由器、且确实有公网 IP 的场景。远程桌面、SSH、HTTP 服务这类单一端口服务用它最干净。
UPnP 自动映射:适合游戏主机这种不方便手动配置、且服务端口经常变化的应用。前提是当前路由器开启 UPnP 且工作正常。
信令服务器 + UDP 打洞:适合两个客户端都在 NAT 后面、希望建立直连 P2P 通道的场景。典型应用包括 VoIP 通话、视频会议、部分游戏联机。打洞通道不占用服务器带宽,服务器只负责握手。
中继转发服务器:适合打洞失败后的兜底,或者对延迟不敏感、但必须保证连通性的场景。企业内部远程访问多用这类方案,代价是服务器带宽成本和延迟。
虚拟组网产品:这一类我会单独展开说,因为它实际上是把“打洞 + 中继 + 虚拟网卡 + 自动路由”打包封装后提供给你的服务。设备安装客户端后会被分配一个虚拟内网 IP,彼此之间像在同一个局域网里一样互访,底层是打洞还是中继对用户透明。
4.2 虚拟组网方案为什么省心
虚拟组网产品的核心价值在于“屏蔽复杂度”。它会在每台设备上创建一张虚拟网卡,拦截对虚拟网段的访问,然后通过客户端进程在互联网上找路。能打洞时走直连,延迟低、带宽大;打洞失败时自动切换中继链路,保证设备仍然可见、可访问,只是速度会差一些。
这类方案的适用场景极其明确:你想把家里那台机器当成“云主机”用,但又不想申请公网 IP、不想配置路由器、不想维护一个中继服务器的时候。我这些年给朋友推荐远程访问家里 NAS 的方案,基本都指向这类产品。它们也解决了一个端口映射类方案解决不了的痛点:多设备、多端口、动态网络环境。你不需要管自己在哪个 WiFi 下面、路由器是哪家固件,只要客户端能上网,就能把设备组进同一个虚拟局域网。
代价当然也有:一是所有流量都经过厂商的协调服务器和中继池,数据链路多了一层;二是免费额度通常有限,高频大流量场景可能要付费或在自建服务器上部署。不过对大多数家庭远程访问需求来说,这种“无感”体验是值得的。
4.3 自建隧道类工具:另一种中继思路
还有一类工具,它的工作模式是“内网设备主动连接到公网服务器,保持一条长连接;外部请求先到达公网服务器,再通过这条长连接转发到内网设备”。它的本质是一个通用反向代理隧道,适合暴露单个或少数几个内网服务,比如本地开发的 Webhook 调试、临时把内网页面分享给外部协作者。
这种思路能穿透所有 NAT,因为连接是从内网往外的,映射表里天然就有记录。它和虚拟组网的区别在于:虚拟组网偏向“设备互联”,隧道工具偏向“服务发布”。前者给你一块虚拟私网,后者给你一个公网入口。如果你的核心需求只是“让外部访问我内网上的这一个服务”,隧道类工具配置起来更快;如果需求是“让我的多台设备随时互相访问”,那就用虚拟组网方案。
到底选哪种,我给一个很实用的判断标准:数一下你需要暴露的端口和服务数量。只有一两个、且是普通的 HTTP/SSH 服务,隧道工具够用了。超过三个端口、要访问 SMB 共享、要跑数据库客户端、要在手机和电脑之间互访文件,直接上虚拟组网方案,省心程度高一个量级。
5. 真正打洞时的工程细节:端口预测、平行发包与重试策略
5.1 “好运端口”:NAT 端口分配的预测规律
接下来这部分是实战中才会遇到的细节,也是教程很少讲的内容。
很多家用路由器的 NAT 实现,在同一个内网端口连续向不同外部目标发起连接时,会为每个新目标分配一个递增的外部端口。这个增量经常是 2,而不是 1。具体原因是某些 NAT 实现在端口分配时会保留一部分低位或做奇偶对齐,导致相邻映射端口之间差 2。早期国外文献把这种可预测的端口称之为“lucky port”,翻译过来就是“好运端口”。
打洞时如果能预测出对方 NAT 下一个可能使用的端口,你就能提前向多个候选端口同时发 UDP 包,命中概率大增。不过需要特别提醒,这个规律不是标准,不同厂商、不同固件的表现完全不同。有的路由器是线性递增,有的按 8 的倍数跳,有的加一点随机扰动,还有的根本没法预测。所以端口预测只能作为“提高成功率的手段”,不能当“保证成功的依据”。
我实际做打洞模块时,会把端口预测和下面要说的平行发包结合起来,而不是死等某一个端口的回包。你向十个端口同时打总比只打一个端口成功率高,代价只是多付出几个 UDP 包,几乎可以忽略。
5.2 平行发包与多轮重试
平行发包(parallel hole punching)的本质是:客户端从同一个本地端口向目标 NAT 可能出现的多个外部端口同时发送 UDP 包。每个包都会让本地 NAT 记一条映射,其中只要有一个端口正好落在对方 NAT 的映射范围内,通道就打开了一半。
重试策略同样关键。打洞很少一轮就成功,常见做法是第一轮发包间隔 100 毫秒、第二轮 200 毫秒、第三轮 500 毫秒,逐步退避,总共打 6-10 轮。为什么需要多轮?因为两个 NAT 的映射建立时序不同步,对方可能过了 200 毫秒才开始打洞,你第一轮全部落空,等到第二轮时它的映射刚建立,你的包正好在窗口期内到达。同步也是打洞成功的关键变量,所以信令服务器在握手后要尽量让双方同时启动打洞流程。
我遇到过一个工程上的小坑:某个在内网跑着的服务,本地 socket 绑定的是“两个不同的源端口”,外层 NAT 映射出来的外部端口就完全不同。我在打洞时一开始只从一个本地端口发包,成功率很低;后来改成“每个本地端口都参与打洞”,成功率高了不少。这就是 5.1 说的映射表条目和本地端口绑定的问题。
5.3 打洞成功后的保活和协议选择
洞打穿之后,真正的考验才开始。UDP 映射条目有超时时间,短的话几十秒就没了。你必须周期性地发送心跳包来维持映射条目,一般建议心跳间隔取“NAT 超时时间的一半”,比如你的目标网络最坏超时 60 秒,那心跳就设 30 秒。问题是你未必知道对方 NAT 的精确超时时间,所以很多实现采用“自适应心跳”——先是 15 秒一次,连续几次成功后把间隔逐步拉长到 90 秒,中间如果发现链路断了再缩回去。
另一个容易被忽略的点:UDP 打洞成功后,你就只能用 UDP 承载业务流量吗?不一定。如果业务本身就是 UDP(比如游戏、VoIP),当然没问题;但如果你最终想要的是一个 TCP 通道,那就需要在 UDP 之上自己实现可靠传输,或者打洞成功后重新协商端口做 TCP 直连。后一种思路时间窗口很难卡准,因为 TCP 打洞的成功条件和 UDP 不一样。实际上现在很多开源库的典型做法是:优先尝试 UDP 打洞,成功后直接在 UDP 上跑一层自己的重传和流控逻辑,而不是死磕 TCP 穿透。
6. 排查与验证:为什么我的穿透失败了
6.1 一个完整的排查链路
穿透失败的时候,不要一上来就换方案或重启路由器。我建议按下面这套链路走一遍,多数问题几分钟就能定位。
第一,确认有没有公网 IP。登录路由器看 WAN 口 IP,如果是 100.64.x.x、10.x.x.x、192.168.x.x 这类地址,基本就是运营商级 NAT,端口映射方案直接废掉。第二,用 STUN 检测 NAT 类型,同时确认出网链路是否稳定。第三,检查目标设备本身的防火墙或安全软件,确认对应端口没有被本地防火墙拦截。第四,检查路由器的高级设置,确认有没有开启什么“安全防护”“ALG 加速”等会干扰 NAT 转发或端口映射的功能项。第五,看数据包是否真的到达了目标设备。这一步需要监听端口,比如在机器上起一个 netcat 监听,再从外部尝试连接,用抓包工具看有没有包到达。
我在实践中的体会是,很多时候穿透失败的根子既不是 NAT 类型,也不是公网 IP 问题,而是内网防火墙和路由器安全策略叠加导致的。NAT 穿透方案只解决了“路由器把包转进来”这一层,转进来之后包还得过主机防火墙和软件防护这一关。做排查时从内到外逐层清理,比无头苍蝇一样换方案要有用得多。
6.2 常见坑位台账
为了节省大家的时间,我把这些年遇到的穿透失败原因整理成了一张台账:
| 症状 | 常见根因 | 处理方向 |
|---|---|---|
| 端口映射配置了但外部连不上 | WAN 口是私有 IP,实际处于运营商级 NAT 后面 | 放弃映射,改用打洞/中继方案 |
| 打洞偶尔成功、经常断 | 心跳保活间隔超过 NAT 映射超时时间 | 缩短心跳间隔,改自适应心跳 |
| UDP 能通、TCP 死活不通 | 对称型 NAT 或高性能防火墙对 TCP 状态追踪太严格 | 在 UDP 之上跑可靠传输,或干脆中继 |
| 内网能访问、外网不行 | 路由器开启了“SPI 防火墙”或“ALG”干扰规则 | 对照路由器文档关闭相关功能并重测 |
| 某运营商宽带下成功率特别低 | 运营商对 UDP 限速或在出口做特殊 NAT | 换一种探测策略或直接走中继 |
| 同一台机器有时通有时不通 | 本地 DHCP 地址变了,端口映射绑定的是旧 IP | 给目标设备设置静态 DHCP 保留 |
6.3 给新手的几条实操建议
如果你刚开始接触穿透,我的建议是先做减法,别一上来就搞一整套自研穿透方案。
先拿现成的组网产品跑通远程访问需求,确认业务链路没问题;等你有余力了,再用 STUN 检测工具摸清自家网络属于哪类 NAT。如果只是服务我一个远程桌面、访问 NAS 这层需求,组网产品一天就能解决问题,没必要从零开始写打洞代码。如果你是学习目的想深入了解穿透原理,那就自己搭一个公网信令服务器,写一对客户端,在可控网络环境里把 UDP 打洞完整跑一遍,体会不同 NAT 类型下的行为差异。
无论哪条路,都要记得先回答一个基础问题:我能不能拿到公网 IP。能拿到,优先端口映射;拿不到,立刻转打洞或中继方案。方向对了,后面才不会浪费大把时间。
最后分享一个让我印象最深的教训:有一次我以为自己精心设计的打洞模块出了问题,代码审查了整整两天,后来发现只是测试环境的机房网络本身就在一层严格的对称型 NAT 后面,而这个环境之前是用来验证中继功能的,压根没做过打洞测试。从那以后,我做的第一件事永远是先在目标网络环境下检测 NAT 类型,再决定要不要继续调试打洞逻辑。穿透方案没有银弹,一切从认识你的网络开始。