How NAT traversal works(https://tailscale.com/blog/how-nat-traversal-works/)
。之前有读者问过关于 NAT 穿越问题,刚好今天找到一篇非常好的文章分享出来,希望对你有帮助!到达客户端侧时,Windows Defender 认为这是刚才出向包的应答包,因此就放行它进入了!此外,右侧的防火墙此时也记录了:2.2.2.2:1234 -> 7.7.7.7:5678 的包应该放行。
笔记本收到服务器发来的包之后,发送一个包作为应答。这个包穿过 Windows Defender 防火墙 和服务端防火墙(因为这是对服务端发送的包的应答包),达到服务端。
The STUN protocol has a bunch more stuff in it — there’s a way of obfuscating the ip:port in the response to stop really broken NATs from mangling the packet’s payload, and a whole authentication mechanism that only really gets used by TURN and ICE, sibling protocols to STUN that we’ll talk about in a bit. We can ignore all of that stuff for address discovery.
in theory, there are also NAT devices that are super relaxed, and don’t ship with stateful firewall stuff at all. In those, you don’t even need simultaneous transmission, the STUN request gives you an internet ip:port that anyone can connect to with no further ceremony. If such devices do still exist, they’re increasingly rare.
有多种中继实现方式。
TURN (Traversal Using Relays around NAT):经典方式,核心理念是用户(人)先去公网上的 TURN 服务器认证,成功后后者会告诉你:“我已经为你分配了 ip:port,接下来将为你中继流量”,然后将这个 ip:port 地址告诉对方,让它去连接这个地址,接下去就是非常简单的客户端/服务器通信模型了。Tailscale 并不使用 TURN。这种协议用起来并不是很好,而且与 STUN 不同, 它没有真正的交互性,因为互联网上并没有公开的 TURN 服务器。
DERP (Detoured Encrypted Routing Protocol)
这是我们创建的一个协议,DERP,
它是一个通用目的包中继协议,运行在 HTTP 之上,而大部分网络都是允许 HTTP 通信的。
它根据目的公钥(destination’s public key)来中继加密的流量(encrypted payloads)。
前面也简单提到过,DERP 既是我们在 NAT 穿透失败时的保底通信方式(此时的角色 与 TURN 类似),也是在其他一些场景下帮助我们完成 NAT 穿透的旁路信道。换句话说,它既是我们的保底方式,也是有更好的穿透链路时,帮助我们进行连接升 级(upgrade to a peer-to-peer connection)的基础设施。
有了“中继”这种保底方式之后,我们穿透的成功率大大增加了。如果此时不再阅读本文接下来的内容,而是把上面介绍的穿透方式都实现了,我预计:
90% 的情况下,你都能实现直连穿透;
剩下的 10% 里,用中继方式能穿透一些(some);这已经算是一个“足够好”的穿透实现了。
如果你并不满足于“足够好”,那我们可以做的事情还有很多!
本节将介绍一些五花八门的 tricks,在某些特殊场景下会帮到我们。单独使用这项技术都 无法解决 NAT 穿透问题,但将它们巧妙地组合起来,我们能更加接近 100% 的穿透成功率。
回忆 hard NAT 中遇到的问题,如下图所示,关键问题是:easy NAT 不知道该往 hard NAT 方的哪个 ip:port 发包。
但必须要往正确的 ip:port 发包,才能穿透防火墙,实现双向互通。怎么办呢?
首先,我们能知道 hard NAT 的一些 ip:port,因为我们有 STUN 服务器。
这里先假设我们获得的这些 IP 地址都是正确的(这一点并不总是成立,但这里先这么假 设。而实际上,大部分情况下这一点都是成立的,如果对此有兴趣,可以参考 REQ-2 in RFC 4787)。
IP 地址确定了,剩下的就是端口了。总共有 65535 中可能,我们能遍历这个端口范围吗?
如果发包速度是 100 packets/s,那最坏情况下,需要 10 分钟来找到正确的端口。还是那句话,这虽然不是最优的,但总比连不上好。
这很像是端口扫描(事实上,确实是),实际中可能会触发对方的网络入侵检测软件。
利用 birthday paradox 算法, 我们能对端口扫描进行改进。
上一节的基本前提是:hard side 只打开一个端口,然后 easy side 暴力扫描 65535 个端口来寻找这个端口;
这里的改进是:在 hard size 开多个端口,例如 256 个(即同时打开 256 个 socket,目的地址都是 easy side 的 ip:port), 然后 easy side 随机探测这边的端口。
这里省去算法的数学模型,如果你对实现干兴趣,可以看看我写的 python calculator。计算过程是“经典”生日悖论的一个小变种。下面是随着 easy side random probe 次数(假设 hard size 256 个端口)的变化,两边打开的端口有重合(即通信成功)的概率:
根据以上结果,如果还是假设 100 ports/s 这样相当温和的探测速率,那 2 秒钟就有约 50% 的成功概率。即使非常不走运,我们仍然能在 20s 时几乎 100% 穿透成功,而此时只探测了总端口空间的 4%。
非常好!虽然这种 hard NAT 给我们带来了严重的穿透延迟,但最终结果仍然是成功的。那么,如果是两个 hard NAT,我们还能处理吗?
这种情况下仍然可以用前面的 多端口+随机探测 方式,但成功概率要低很多了:
每次通过一台 hard NAT 去探测对方的端口(目的端口)时,我们自己同时也生成了一个随机源端口,
这意味着我们的搜索空间变成了二维 {src port, dst port} 对,而不再是之前的一维 dst port 空间。
这里我们也不就具体计算展开,只告诉结果:仍然假设目的端打开 256 个端口,从源端发起 2048 次(20 秒), 成功的概率是:0.01%。
如果你之前学过生日悖论,就并不会对这个结果感到惊讶。理论上来说,
要达到 99.9% 的成功率,我们需要两边各进行170,000 次探测 —— 如果还是以 100 packets/sec 的速度,就需要 28 分钟。
要达到 50% 的成功率,“只”需要 54,000 packets,也就是 9 分钟。
如果不使用生日悖论方式,而且暴力穷举,需要 1.2 年时间!对于某些应用来说,28 分钟可能仍然是一个可接受的时间。用半个小时暴力穿透 NAT 之后, 这个连接就可以一直用着 —— 除非 NAT 设备重启,那样就需要再次花半个小时穿透建个新连接。但对于 交互式应用来说,这样显然是不可接受的。
更糟糕的是,如果去看常见的办公网路由器,你会震惊于它的 active session low limit 有多么低。例如,一台 Juniper SRX 300 最多支持 64,000 active sessions。也就是说,
如果我们想创建一个成功的穿透连接,就会把它的整张 session 表打爆 (因为我们要暴力探测 65535 个端口,每次探测都是一条新连接记录)!这显然要求这台路由器能从容优雅地处理过载的情况。
这只是创建一条连接带来的影响!如果 20 台机器同时对这台路由器发起穿透呢?绝对的灾难!至此,我们通过这种方式穿透了比之前更难一些的网络拓扑。这是一个很大的成就,因为 家用路由器一般都是 easy NAT,hard NAT 一般都是办公网路由器或云 NAT 网关。这意味着这种方式能帮我们解决
home-to-office(家->办公室)
home-to-cloud (家->云) 的场景,以及一部分
office-to-cloud (办公室->云)
cloud-to-cloud (云->办公室) 场景。
如果我们能让 NAT 设备的行为简单点,不要把事情搞这么复杂,那建 立连接(穿透)就会简单很多。真有这样的好事吗?还真有,有专门的一种协议叫 端口映射协议(port mapping protocols)。通过这种协议禁用掉前面 遇到的那些乱七八糟的东西之后,我们将得到一个非常简单的“请求-响应”。
下面是三个具体的端口映射协议:
UPnP IGD (Universal Plug’n’Play Internet Gateway Device)
最老的端口控制协议, 诞生于 1990s 晚期,因此使用了很多上世纪 90 年代的技术 (XML、SOAP、multicast HTTP over UDP —— 对,HTTP over UDP ),而且很难准确和安全地实现这个协议。但以前很多路由器都内置了 UPnP 协议, 现在仍然很多。
请求和响应:
“你好,请将我的 lan-ip:port 转发到公网(WAN)”, “好的,我已经为你分配了一个公网映射 wan-ip:port ”。
NAT-PMP
UPnP IGD 出来几年之后,Apple 推出了一个功能类似的协议,名为 NAT-PMP (NAT Port Mapping Protocol)。
但与 UPnP 不同,这个协议只做端口转发,不管是在客户端还是服务端,实现起来都非常简单。
PCP
稍后一点,又出现了 NAT-PMP v2 版,并起了个新名字PCP (Port Control Protocol)。
因此要更好地实现穿透,可以
先判断本地的默认网关上是否启用了 UPnP IGD, NAT-PMP and PCP, 如果探测发现其中任何一种协议有响应,我们就申请一个公网端口映射,
可以将这理解为一个加强版 STUN:我们不仅能发现自己的公网 ip:port,而且能指示我们的 NAT 设备对我们的通信对端友好一些 —— 但并不是为这个端口修改或添加防火墙规则。
接下来,任何到达我们 NAT 设备的、地址是我们申请的端口的包,都会被设备转发到我们。
但我们不能假设这个协议一定可用:
本地 NAT 设备可能不支持这个协议;
设备支持但默认禁用了,或者没人知道还有这么个功能,因此从来没开过;
安全策略要求关闭这个特性。
这一点非常常见,因为 UPnP 协议曾曝出一些高危漏洞(后面都修复了,因此如果是较新的设备,可以安全地使用 UPnP —— 如果实现没问题)。不幸的是,某些设备的配置中,UPnP, NAT-PMP,PCP 是放在一个开关里的(可能 统称为 “UPnP” 功能),一开全开,一关全关。因此如果有人担心 UPnP 的安全性,他连另 外两个也用不了。
最后,终归来说,只要这种协议可用,就能有效地减少一次 NAT,大大方便建连过程。但接下来看一些不常见的场景。
目前为止,我们看到的客户端和服务端都各只有一个 NAT 设备。如果有多个 NAT 设备会 怎么样?例如下面这种拓扑:
这个例子比较简单,不会给穿透带来太大问题。包从客户端 A 经过多次 NAT 到达公网的过程,与前面分析的穿过多层有状态防火墙是一样的:
额外的这层(NAT 设备)对客户端和服务端来说都不可见,我们的穿 透技术也不关心中间到底经过了多少层设备。
真正有影响的其实只是最后一层设备,因为对端需要在这一层设备上 找到入口让包进来。
具体来说,真正有影响的是端口转发协议。
客户端使用这种协议分配端口时,为我们分配端口的是最靠近客户端的这层 NAT 设备;而我们期望的是让最离客户端最远的那层 NAT 来分配,否则我们得到的就是一个网络中间层分配的 ip:port,对端是用不了的;不幸的是,这几种协议都不能递归地告诉我们下一层 NAT 设备是多少 —— 虽然可以用 traceroute 之类的工具来探测网络路径,再加上 猜路上的设备是不是 NAT 设备(尝试发送 NAT 请求) —— 但这个就看运气了。这就是为什么互联网上充斥着大量的文章说 double-NAT 有多糟糕,以 及警告用户为保持后向兼容不要使用 double-NAT。但实际上,double-NAT 对于绝大部分 互联网应用来说都是不可见的(透明的),因为大部分应用并不需要主动地做这种 NAT 穿 透。
但我也绝不是在建议你在自己的网络中设置 double-NAT。
破坏了端口映射协议之后,某些视频游戏的多人(multiplayer)模式就会无法使用,
也可能会使你的 IPv6 网络无法派上用场,后者是不用 NAT 就能双向直连的一个好方案。
但如果 double-NAT 并不是你能控制的,那除了不能用到这种端口映射协议之外,其他大部分东西都是不受影响的。
double-NAT 的故事到这里就结束了吗?—— 并没有,而且更大型的 double-NAT 场景将展现在我们面前。
即使用 NAT 来解决 IPv4 地址不够的问题,地址仍然是不够用的,ISP(互联网服务提供商) 显然 无法为每个家庭都分配一个公网 IP 地址。那怎么解决这个问题呢?ISP 的做法是不够了就再嵌套一层 NAT:
家用路由器将你的客户端 SNAT 到一个 “intermediate” IP 然后发送到运营商网络,
ISP’s network 中的 NAT 设备再将这些 intermediate IPs 映射到少量的公网 IP。后面这种 NAT 就称为“运营商级 NAT”(carrier-grade NAT,或称电信级 NAT),缩写 CGNAT。如下图所示:
在此之前,办公网用户要快速实现 NAT 穿透,只需在他们的路由器上手动设置端口映射就行了。但有了 CGNAT 之后就不管用了,因为你无法控制运营商的 CGNAT!好消息是:这其实是 double-NAT 的一个小变种,因此前面介绍的解决方式大部分还仍然是适用的。某些东西可能会无法按预期工作,但只要肯给 ISP 交钱,这些也都能解决。除了 port mapping protocols,其他我们已经介绍的所有东西在 CGNAT 里都是适用的。
但我们确实遇到了一个新挑战:如何直连两个在同一 CGNAT 但不同家用路由器中的对端呢?如下图所示:
在这种情况下,STUN 就无法正常工作了:STUN 看到的是客户端在公网(CGNAT 后面)看到的地址, 而我们想获得的是在 “middle network” 中的 ip:port,这才是对端真正需要的地址,
怎么办呢?
如果你想到了端口映射协议,那恭喜,答对了!如果 peer 中任何一个 NAT 支持端口映射协议, 对我们就能实现穿透,因为它分配的 ip:port 正是对端所需要的信息。
这里讽刺的是:double-NAT(指 CGNAT)破坏了端口映射协议,但在这里又救了我们!当然,我们假设这些协议一定可用,因为 CGNAT ISP 倾向于在它们的家用路由器侧关闭 这些功能,已避免软件得到“错误的”结果,产生混淆。
如果不走运,NAT 上没有端口映射功能怎么办?
让我们回到基于 STUN 的技术,看会发生什么。两端在 CGNAT 的同一侧,假设 STUN 告诉我们 A 的地址是 2.2.2.2:1234,B 的地址是 2.2.2.2:5678。
那么接下来的问题是:如果 A 向 2.2.2.2:5678 发包会怎么样?期望的 CGNAT 行为是:
执行 A 的 NAT 映射规则,即对 2.2.2.2:1234 -> 2.2.2.2:5678 进行 SNAT。
注意到目的地址 2.2.2.2:5678 匹配到的是 B 的入向 NAT 映射,因此接着对这个包执行 DNAT,将目的 IP 改成 B 的私有地址。
通过 CGNAT 的 internal 接口(而不是 public 接口,对应公网)将包发给 B。这种 NAT 行为有个专门的术语,叫 hairpinning(直译为发卡,意思 是像发卡一样,沿着一边上去,然后从另一边绕回来),
大家应该猜到的一个事实是:不是所以 NAT 都支持 hairpin 模式。实际上,大量 well-behaved NAT 设备都不支持 hairpin 模式,
因为它们都有 “只有 src_ip 是私有地址且 dst_ip 是公网地址的包才会经过我” 之类的假设。
因此对于这种目的地址不是公网、需要让路由器把包再转回内网的包,它们会直接丢弃。
这些逻辑甚至是直接实现在路由芯片中的,因此除非升级硬件,否则单靠软件编程无法改变这种行为。Hairpin 是所有 NAT 设备的特性(支持或不支持),并不是 CGNAT 独有的。
在大部分情况下,这个特性对我们的 NAT 穿透目的来说都是无所谓的,因为我们期望中 两个 LAN NAT 设备会直接通信,不会再向上绕到它们的默认网关 CGNAT 来解决这个问题。
Hairpin 特性可有可无这件事有点遗憾,这可能也是为什么 hairpin 功能经常 broken 的原因。
一旦必须涉及到 CGNAT,那 hairpinning 对连接性来说就至关重要了。
Hairpinning 使内网连接的行为与公网连接的行为完成一致,因此我们无需关心目的 地址类型,也不用知晓自己是否在一台 CGNAT 后面。
如果 hairpinning 和 port mapping protocols 都不可用,那只能降级到中继模式了。
行文至此,一些读者可能已经对着屏幕咆哮:不要再用 IPv4 了!花这么多时间精力解决这些没意义的东西,还不如直接换成 IPv6!
的确,之所以有这些乱七八糟的东西,就是因为 IPv4 地址不够了,我们一直在用越来越复杂的 NAT 来给 IPv4 续命。
如果 IP 地址够用,无需 NAT 就能让世界上的每个设备都有一个自己的公网 IP 地址,这些问题不就解决了吗?
简单来说,是的,这也正是 IPv6 能做的事情。但是,也只说对了一半:在理想的全 IPv6 世界中,所有这些东西会变得更加简单,但我们面临的问题并不会完全消失 —— 因为有状态防火墙仍然还是存在的。
办公室中的电脑可能有一个公网 IPv6 地址,但你们公司肯定会架设一个防火墙,只允许 你的电脑主动访问公网,而不允许反向主动建连。
其他设备上的防火墙也仍然存在,应用类似的规则。
因此,我们仍然会用到
本文最开始介绍的防火墙穿透技术,以及帮助我们获取自己的公网 ip:port 信息的旁路信道
仍然需要在某些场景下 fallback 到中继模式,例如 fallback 到最通用的 HTTP 中继 协议,以绕过某些网络禁止 outbound UDP 的问题。
但我们现在可以抛弃 STUN、生日悖论、端口映射协议、hairpin 等等东西了。这是一个好消息!
另一个更加严峻的现实问题是:当前并不是一个全 IPv6 世界。目前世界上
大部分还是 IPv4,
大约 33% 是 IPv6,而且分布极度不均匀,因此某些 通信对所在的可能是 100% IPv6,也可能是 0%,或二者之间。
不幸的是,这意味着,IPv6 还无法作为我们的解决方案。就目前来说,它只是我们的工具箱中的一个备选。对于某些 peer 来说,它简直是完美工 具,但对其他 peer 来说,它是用不了的。如果目标是“任何情况下都能穿透(连接) 成功”,那我们就仍然需要 IPv4+NAT 那些东西。
IPv4/IPv6 共存也引出了一个新的场景:NAT64 设备。
前面介绍的都是 NAT44 设备:它们将一个 IPv4 地址转换成另一 IPv4 地址。NAT64 从名字可以看出,是将一个内侧 IPv6 地址转换成一个外侧 IPv4 地址。利用 DNS64 设备,我们能将 IPv4 DNS 应答给 IPv6 网络,这样对终端来说,它看到的就是一个 全 IPv6 网络,而仍然能访问 IPv4 公网。
Incidentally, you can extend this naming scheme indefinitely. There have been some experiments with NAT46; you could deploy NAT66 if you enjoy chaos; and some RFCs use NAT444 for carrier-grade NAT.
如果需要处理 DNS 问题,那这种方式工作良好。例如,如果连接到 google.com,将这个域名解析成 IP 地址的过程会涉及到 DNS64 设备,它又会进一步 involve NAT64 设备,但后一步对用户来说是无感知的。
但对于 NAT 和防火墙穿透来说,我们会关心每个具体的 IP 地址和端口。
如果设备支持 CLAT (Customer-side translator — from Customer XLAT),那我们就很幸运:
CLAT 假装操作系统有直接 IPv4 连接,而背后使用的是 NAT64,以对应用程序无感知。在有 CLAT 的设备上,我们无需做任何特殊的事情。
CLAT 在移动设备上非常常见,但在桌面电脑、笔记本和服务器上非常少见, 因此在后者上,必须自己做 CLAT 做的事情:检测 NAT64+DNS64 的存在,然后正确地使用它们。
首先检测是否存在 NAT64+DNS64。
方法很简单:向 ipv4only.arpa. 发送一个 DNS 请求。这个域名会解析 到一个已知的、固定的 IPv4 地址,而且是纯 IPv4 地址。如果得到的 是一个 IPv6 地址,就可以判断有 DNS64 服务器做了转换,而它必然会用到 NAT64。这样 就能判断出 NAT64 的前缀是多少。
此后,要向 IPv4 地址发包时,发送格式为{NAT64 prefix + IPv4 address} 的 IPv6 包。类似地,收到来源格式为 {NAT64 prefix + IPv4 address} 的包时,就是 IPv4 流量。
接下来,通过 NAT64 网络与 STUN 通信来获取自己在 NAT64 上的公网 ip:port,接 下来就回到经典的 NAT 穿透问题了 —— 除了需要多做一点点事情。
幸运的是,如今的大部分 v6-only 网络都是移动运营商网络,而几乎所有手机都支持 CLAT。运营 v6-only 网络的 ISPs 会在他们给你的路由器上部署 CLAT,因此最后你其实不需要做什么事情。但如果想实现 100% 穿透,就需要解决这种边边角角的问题,即必须显式支持从 v6-only 网络连接 v4-only 对端。
针对具体场景,该选择哪种穿透方式?至此,我们的 NAT 穿透之旅终于快结束了。我们已经覆盖了有状态防火墙、简单和高级 NAT、IPv4 和 IPv6。只要将以上解决方式都实现了,NAT 穿透的目的就达到了!
但是,
对于给定的 peer,如何判断改用哪种方式呢?
如何判断这是一个简单有状态防火墙的场景,还是该用到生日悖论算法,还是需要手动处理 NAT64 呢?
还是通信双方在一个 WiFi 网络下,连防火墙都没有,因此不需要任何操作呢?
早期 NAT 穿透比较简单,能让我们精确判断出 peer 之间的路径特点,然后针对性地采用相应的解决方式。但后面,网络工程师和 NAT 设备开发工程师引入了一些新理念,给路径判断造成很大困难。因此 我们需要简化客户端侧的思考(判断逻辑)。
这就要提到 Interactive Connectivity Establishment (ICE,交换式连接建立) 协议了。与 STUN/TURN 类似,ICE 来自电信领域,因此其 RFC 充满了 SIP、SDP、信令会话、拨号等等电话术语。但如果忽略这些领域术语,我们会看到它描述了一个极其优雅的判断最佳连接路径的算法。
真的?这个算法是:每种方法都试一遍,然后选择最佳的那个方法。就是这个算法,惊喜吗?
来更深入地看一下这个算法。
这里的讨论不会严格遵循 ICE spec,因此如果是在自己实现一个可互操作的 ICE 客户端,应该通读RFC 8445, 根据它的描述来实现。这里忽略所有电信术语,只关注核心的算法逻辑, 并提供几个在 ICE 规范允许范围的灵活建议。
为实现和某个 peer 的通信,首先需要确定我们自己用的(客户端侧)这个 socket 的地址, 这是一个列表,至少应该包括:
我们自己的 IPv6 ip:ports
我们自己的 IPv4 LAN ip:ports(局域网地址)
通过 STUN 服务器获取到的我们自己的 IPv4 WAN ip:ports(公网地址,可能会经过 NAT64 转换)
通过端口映射协议获取到的我们自己的 IPv4 WAN ip:port(NAT 设备的端口映射协议分配的公网地址)
运营商提供给我们的 endpoints(例如,静态配置的端口转发)
通过旁路信道与 peer 互换这个列表。两边都拿到对方的列表后,就开始互相探测对方提供的地址。列表中地址没有优先级,也就是说,如果对方给的了 15 个地址,那我们应该把这 15 个地址都探测一遍。
这些探测包有两个目的:
打开防火墙,穿透 NAT,也就是本文一直在介绍的内容;
健康检测。我们在不断交换(最好是已认证的)“ping/pong” 包,来检测某个特定的路径是不是端到端通的。
最后,一小会儿之后,从可用的备选地址中(根据某些条件)选择“最佳”的那个,任务完成!
这个算法的优美之处在于:只要选择最佳线路(地址)的算法是正确的,那就总能获得最佳路径。
ICE 会预先对这些备选地址进行排序(通常:LAN > WAN > WAN+NAT),但用户也可以自己指定这个排序行为。
从 v0.100.0 开始,Tailscale 从原来的 hardcode 优先级切换成了根据 round-trip latency 的方式,它大部分情况下排序的结果和 LAN > WAN > WAN+NAT 是一致的。但相比于静态排序,我们是动态计算每条路径应该属于哪个类别。
ICE spec 将协议组织为两个阶段:
探测阶段
通信阶段
但不一定要严格遵循这两个步骤的顺序。在 Tailscale,
我们发现更优的路径之后就会自动切换过去, 所有的连接都是先选择 DERP 模式(中继模式)。这意味着连接立即就能建立(优先级最低但 100% 能成功的模式),用户不用任何等待, 然后并行进行路径发现。通常几秒钟之后,我们就能发现一条更优路径,然后将现有连接透明升级(upgrade)过去。但有一点需要关心:非对称路径。ICE 花了一些精力来保证通信双方选择的是相同的网络 路径,这样才能保证这条路径上有双向流量,能保持防火墙和 NAT 设备的连接一直处于 open 状态。自己实现的话,其实并不需要花同样大的精力来实现这个保证,但需要确保你所有使用的所有路径上,都有双向流量。这个目标就很简单了,只需要定期在所有已使用的路径上发 ping/pong 就行了。
要实现健壮性,还需要检测当前已选择的路径是否已经失败了(例如,NAT 设备维护清掉了所有状态), 如果失败了就要降级(downgrade)到其他路径。这里有两种方式:
持续探测所有路径,维护一个降级时会用的备用地址列表;
直接降级到保底的中继模式,然后再通过路径探测升级到更好的路径。
考虑到发生降级的概率是非常小的,因此这种方式可能是更经济的。
最后需要提到安全。
本文的所有内容都假设:我们使用的上层协议已经有了自己的安全机制( 例如 QUIC 协议有 TLS 证书,WireGuard 协议有自己的公钥)。如果还没有安全机制,那显然是要立即补上的。一旦动态切换路径,基于 IP 的安全机制就是无用的了 (IP 协议最开始就没怎么考虑安全性),至少要有端到端的认证。
严格来说,如果上层协议有安全机制,那即使收到是欺骗性的 ping/pong 流量,问题都不大, 最坏的情况也就是攻击者诱导两端通过他们的系统来中继流量。而有了端到端安全机制,这并不是一个大问题(取决于你的威胁模型)。
但出于谨慎考虑,最好还是对路径发现的包也做认证和加密。具体如何做可以咨询你们的应用安全工程师。
我们终于完成了 NAT 穿透的目标!
如果实现了以上提到的所有技术,你将得到一个业内领先的 NAT 穿透软件,能在绝大多数场景下实现端到端直连。如果直连不了,还可以降级到保底的中继模式(对于长尾来说只能靠中继了)。
但这些工作相当复杂!其中一些问题研究起来很有意思,但很难做到完全正确,尤其是那些 非常边边角角的场景,真正出现的概率极小,但解决它们所需花费的经历又极大。不过,这种工作只需要做一次,一旦解决了,你就具备了某种超级能力:探索令人激动的、相对还比较崭新的端到端应用(peer-to-peer applications)世界。
去中心化软件领域中的许多有趣想法,简化之后其实都变成了 跨过公网(互联网)实现端到端直连 这一问题,开始时可能觉得很简单,但真正做才 发现比想象中难多了。现在知道如何解决这个问题了,动手开做吧!
实现健壮的 NAT 穿透需要下列基础:
一种基于 UDP 的协议;
能在程序内直接访问 socket;
有一个与 peer 通信的旁路信道;
若干 STUN 服务器;
一个保底用的中继网络(可选,但强烈推荐)
然后需要:
遍历所有的 ip:port;
查询 STUN 服务器来获取自己的公网 ip:port 信息,以及判断自己这一侧的 NAT 的“难度”(difficulty);
使用 port mapping 协议来获取更多的公网 ip:ports;
检查 NAT64,通过它获取自己的公网 ip:port;
将自己的所有公网 ip:ports 信息通过旁路信道与 peer 交换,以及某些加密秘钥来保证通信安全;
通过保底的中继方式与对方开始通信(可选,这样连接能快速建立)
如果有必要/想这么做,探测对方的提供的所有 ip:port,以及执行生日攻击(birthday attacks)来穿透 harder NAT;
发现更优路径之后,透明升级到该路径;
如果当前路径断了,降级到其他可用的路径;
确保所有东西都是加密的,并且有端到端认证。
转自:网络技术平台
侵权请私聊公众号删文
热文推荐