NAT穿越的工作原理

2020-08-22 05:57:41

我们在关于TailscaleWorks的帖子中涵盖了很多内容。但是,我们掩饰了如何通过NAT(网络地址转换器)并将您的设备直接相互连接,而不管它们之间有什么障碍。现在我们来谈谈这件事吧!

让我们从一个简单的问题开始:在两台机器之间建立点对点连接。在Tailscale的例子中,我们希望设置一个WireGuard®隧道,但这并不重要。我们使用的技术是广泛适用的,是许多人几十年来的工作成果。例如,WebRTC使用这一套技巧在Web浏览器之间发送点对点的音频、视频和数据。VoIP电话和一些视频游戏使用类似的技术,尽管并不总是成功。

我们将一般性地讨论这些技术,并在适当的情况下使用Tailscale等作为示例。假设您正在制定自己的协议,并且希望穿越NAT。你需要两样东西。

首先,协议应该基于UDP。您可以使用TCP进行NAT穿越,但是它给已经很复杂的问题增加了另一层复杂性,甚至可能需要根据您想要穿越的深度进行内核定制。在本文的其余部分,我们将重点介绍UDP。

如果您使用TCP是因为您希望在NAT穿越完成时使用面向流的连接,请考虑改用QUI。它建立在UDP之上,所以我们可以将重点放在UDP上进行NAT遍历,并且在最后仍然有一个很好的流协议。

其次,您需要直接控制发送和接收网络数据包的网络套接字。通常,您不能使用现有的网络库并使其穿越NAT,因为您必须发送和接收额外的数据包,而这些数据包不是您试图使用的“主”协议的一部分。一些协议将NAT遍历与其他协议紧密集成在一起(例如WebRTC)。但是,如果您正在构建自己的网络,将NAT穿越视为与您的主协议共享一个套接字的单独实体会很有帮助。两者并行运行,其中一个启用另一个。

直接套接字访问可能很困难,这取决于您的情况。一种解决方法是运行本地代理。您的协议与此代理进行通信,并且代理将您的数据包进行NAT穿越并将其中继到对等点。这一层的间接层让您在不更改原始程序的情况下从NAT遍历中获益。

排除了前提条件后,让我们从基本原则开始进行NAT穿越。我们的目标是让udp数据包在两台设备之间双向流动,这样我们的另一种协议(有线保护、Quic、WebRTC、…)。可以做些很酷的事。要让它正常工作有两个障碍:状态防火墙和NAT设备。

状态防火墙是我们两个问题中比较简单的一个。事实上,大多数NAT设备都包括状态防火墙,因此我们需要先解决这个子集,然后才能处理NAT。

要考虑的化身有很多种。一些你可能认识的是Windows Defender防火墙、Ubuntu的UFW(使用iptables/nftables)、BSD的PF(也被MacOS使用)和AWS的安全组。它们都是可配置的,但最常见的配置允许所有“出站”连接并阻止所有“入站”连接。可能会有一些精心挑选的例外,例如允许入站SSH。

但是连接和“方向”是协议设计者想象的虚构。在网络上,每个连接最终都是双向的;它们都是单独的数据包来回飞来飞去。防火墙如何知道什么是入站的,什么是出站的?

这就是有状态部分的用武之地。状态防火墙记住它们过去看到的数据包,并可以在决定如何处理出现的新数据包时使用该知识。

对于UDP,规则非常简单:如果防火墙之前看到匹配的出站数据包,则允许入站UDP数据包。例如,如果我们的笔记本电脑防火墙发现UDP数据包从2.2.2.2:1234到7.7.7.7:5678离开笔记本电脑,它会注意到从7.7.7.7:5678到2.2.2.2:1234的传入数据包也可以。世界上生锈的那一边显然打算用7.7.7.7:5678进行通信,所以我们应该让他们回话。

(顺便说一句,一旦2.2.2.2:1234与任何人进行了通信,一些非常宽松的防火墙可能会允许来自2.2.2.2:1234的流量返回到2.2.2.2:1234。这样的防火墙让我们的穿越工作变得更容易,但也越来越少见。)。

UDP流量的这一规则对我们来说只是一个小问题,只要路径上的所有防火墙都“面向”相同的方向。通常情况下,当你与互联网上的服务器通信时就会出现这种情况。我们唯一的限制是防火墙后面的机器必须是启动所有连接的机器。除非它先开口,否则什么都不能说。

这很好,但不是很有趣:我们已经重新发明了客户端/服务器通信,其中服务器使其自身可以很容易地被客户端访问。在VPN世界中,这导致了一种中心辐条拓扑结构:中心没有防火墙阻止其访问,有防火墙的发言人连接到中心。

当我们的两个“客户”想要直接对话时,问题就开始了。现在,防火墙是面对面的。根据我们上面制定的规则,这意味着双方都必须先走,但也不能先走,因为对方必须先走!

我们怎么才能绕过这一关呢?一种方法是要求用户重新配置一个或两个防火墙,以“打开一个端口”并允许另一台机器的流量。这对用户不太友好。它也不能扩展到像Tailscale这样的网状网络,在这种网络中,我们希望对等点在互联网上有规律地移动。当然,在许多情况下,您无法控制防火墙:您不能在您最喜欢的咖啡馆或机场重新配置路由器。(至少,希望不会!)。

诀窍在于仔细阅读我们为状态防火墙建立的规则。对于UDP,规则是:数据包必须先流出,然后才能回流。

但是,除了正确排列的IP和端口之外,没有任何内容说明数据包必须相互关联。只要某些数据包以正确的来源和目的地流出,任何看起来像是响应的数据包都会被允许返回,即使对方从未收到您的数据包!

因此,要遍历这些多个状态防火墙,我们需要共享一些信息才能开始:对等点必须事先知道其对等点正在使用的IP:端口。一种方法是手动静态配置每个对等点,但是这种方法的伸缩性不是很大。为了超越这一点,我们构建了一个协调服务器,以灵活、安全的方式保持ip:port信息的同步。

然后,对等项开始相互发送UDP数据包。他们必须预料到这些数据包中的一些会丢失,所以除非您准备重新传输,否则它们无法携带任何宝贵的信息。这通常适用于UDP,但在这里尤其适用。在这个过程中我们要丢失一些包裹。

我们的笔记本电脑和工作站现在都在监听固定端口,因此它们都确切知道要与哪个IP:端口通信。让我们来看看发生了什么。

笔记本电脑的第一个数据包,从2.2.2.2:1234到7.7.7.7:5678,通过Windows Defender防火墙进入互联网。另一端的公司防火墙会阻止该数据包,因为它没有7.7.7.7:5678的记录,也没有与2.2.2.2:1234通话的记录。但是,Windows Defender现在记住,它应该预期并允许从7.7.7.7:5678到2.2.2.2:1234的响应。

接下来,工作站的第一个数据包(从7.7.7.7:5678到2.2.2.2:1234)穿过公司防火墙并穿过互联网。当它到达笔记本电脑时,Windows Defender认为“啊,这是对我看到的出站请求的响应”,然后让包通过!此外,公司防火墙现在记住,它应该期望2.2.2.2:1234到7.7.7.7:5678之间的响应,并且这些数据包也是正常的。

在收到来自工作站的数据包的鼓励下,笔记本电脑会发回另一个数据包。它通过Windows DefenderFirewall,穿过公司防火墙(因为它是对之前发送的数据包的“响应”),然后到达工作站。

成功!我们已经通过一对防火墙建立了双向通信,乍一看,这是可以阻止的。

这并不总是那么容易。我们依赖于对第三方系统的一些间接影响,这需要谨慎处理。在管理穿越防火墙的连接时,我们需要记住什么?

两个端点必须几乎同时尝试通信,这样所有中间防火墙都会打开,而两个对等点仍然有效。一种方法是让对等点不断重试,但这是浪费的。如果两个对等点都知道同时开始测试建立连接,不是更好吗?

这听起来可能有点递归:要沟通,首先你需要能够沟通。然而,这个预先存在的“侧通道”并不需要非常花哨:它可以有几秒钟的延迟,总共只需要传递几千字节,所以一个小小的VM可以很容易地为数千台机器牵线搭桥。

在很久以前,我使用XMPP聊天消息作为辅助通道,效果很好。再举一个例子,WebRTC要求您想出自己的“信令通道”(这个名称揭示了WebRTC的IPTelephony祖先),并将其插入WebRTC API中。在Tailscale中,我们的协调服务器和DERP(迂回加密路由协议)服务器群充当我们的辅助通道。

状态防火墙的内存有限,这意味着我们需要定期通信来保持连接活动。如果在一段时间内看不到数据包(UDP的常见值是30秒),防火墙就会忘记会话,我们必须重新开始。为了避免这种情况,我们使用atimer,并且必须定期发送数据包以重置计时器,或者使用某种带外方式按需重新启动连接。

从好的方面来说,有一件事我们不需要担心,那就是我们的两个同行之间到底有多少防火墙。只要它们是有状态的,并且允许出站连接,同步传输技术就可以通过任意数量的层。这真的很好,因为这意味着我们只需实现一次逻辑,它就可以随时随地工作。

嗯,不完全是。要实现这一点,我们的对等点需要提前知道为他们的对等点使用什么IP:端口。这就是NAT的用武之地,毁了我们的乐趣。

我们可以将NAT(网络地址转换)设备看作具有另一个非常恼人的功能的状态防火墙:除了所有的状态防火墙内容之外,它们还会在数据包通过时更改数据包。

从最广泛的意义上讲,NAT设备是执行任何类型的网络地址转换的任何设备,即更改源或目的IP地址或端口。然而,当谈到连通性问题和NAT穿越时,所有问题都来自源NAT(简称SNAT)。如您所料,还有DNAT(目标NAT),它非常有用,但与NAT穿越无关。

SNAT最常见的用途是使用比设备数量更少的IP地址将许多设备连接到互联网。在消费级路由器中,我们将所有设备映射到一个面向公共的IP地址。这是可取的,因为事实证明,世界上想要访问互联网的设备比提供给它们的IP地址要多得多(至少在IPv4中-我们稍后将介绍IPv6)。NAT让我们有许多设备共享一个IP地址,所以尽管全球IPv4地址短缺,我们仍可以利用手头的地址进一步扩展互联网。

让我们来看看当你的笔记本电脑连接到家里的WiFi并与互联网上的服务器通话时会发生什么。

您的笔记本电脑发送从192.168.0.20:1234到7.7.7.7:5678的UDP数据包。这与笔记本电脑拥有公有IP完全相同。但这在互联网上行不通:192.168.0.20是一个私人IP地址,出现在许多不同人的私人网络中。互联网不知道如何回复我们。

输入家庭路由器。笔记本电脑的数据包在通往互联网的途中流经家庭路由器,路由器发现这是一个前所未有的新会话。

它知道192.168.0.20不会在互联网上运行,但它可以解决这个问题:它在自己的公共IP地址上挑选一些未使用的UDP端口-我们将使用2.2.2.2:4242-并创建一个NAT映射,建立一个等价的:192.168.0.20:1234在LAN端与在Internet端的2.2.2.2:4242相同。

从现在开始,每当它看到与该映射匹配的数据包时,它都会相应地重写数据包中的IP和端口。

继续我们的数据包的传输:家庭路由器应用它刚刚创建的NAT映射,并将数据包转发到互联网。只是现在,数据包来自2.2.2.2:4242,而不是192.168.0.20:1234。它会继续传到服务器,而服务器并不明智。它与2.2.2.2:4242进行通信,就像我们在前面的示例中没有NAT一样。

正如您预期的那样,来自服务器的响应以另一种方式传回,家庭路由器将2.2.2.2:4242重写回192.168.0.20:1234。笔记本电脑也没有变得更聪明,从它的角度来看,互联网神奇地想出了如何处理它的专用IP地址。

我们这里的例子是家用路由器,但同样的原理也适用于公司网络。通常的区别在于,NAT层由多台机器组成(出于高可用性或容量的原因),并且它们可以拥有多个公共IP地址,因此它们有更多的公共IP端口组合可供选择,并且可以同时支持更多的活动客户端。

我们现在遇到了一个问题,看起来与我们之前使用状态防火墙时的情况类似,但是使用NAT设备时出现了问题:

我们的问题是我们的两个对等点不知道他们的对等点的IP:端口是什么。更糟糕的是,严格地说,在另一个对等体发送数据包之前没有IP:端口,因为NAT映射只在去往互联网的出站流量需要它时才会创建。我们又回到了有状态的防火墙问题上,只会更糟:双方都必须先对话,但双方都不知道要和谁对话,只有另一方先对话才能知道。

我们怎样才能打破僵局呢?这就是昏迷的用武之地。STUN是一组对NAT设备的详细行为进行研究的协议,也是一种帮助穿越NAT的协议。目前我们主要关心的是网络协议。

STUN依赖于一个简单的观察:当您从NAT客户端与Internet上的服务器交谈时,服务器看到的是您的NAT设备为您创建的公共IP:端口,而不是您的LAN IP:端口。因此,服务器可以告诉您它看到的IP:端口。这样,您就可以知道来自您的局域网IP:端口的流量在Internet上是什么样子,您可以告诉您的对等点有关该映射的信息,现在他们知道要将数据包发送到哪里了!我们又回到了穿越防火墙的“简单”案例。

这基本上就是STUN协议的全部内容:您的机器发送“从您的角度看,我的端点是什么?”向STUNserver发出请求,服务器会回复“这是我看到您的UDP数据包来自的IP:端口。”

(STUN协议中有更多的东西--有一种在响应中混淆ip:port的方法,以防止真正损坏的NAT破坏数据包的有效负载,以及一个仅由TURN和ICE使用的完整身份验证机制,这两个兄弟协议用于STUN,我们稍后会讨论这一点。对于地址发现,我们可以忽略所有这些内容。)。

顺便说一句,这就是为什么我们在引言中说,如果您想自己实现这一点,NAT穿越逻辑和您的主协议必须共享一个网络套接字。每个套接字在NAT设备上得到不同的映射,因此为了发现您的公共IP:端口,您必须发送和接收来自您打算用于通信的套接字的STUN数据包,否则您将得到无用的答案。

考虑到击晕作为一种工具,我们似乎已经接近尾声了。每台机器都可以执行STUN来发现其本地套接字的面向公众的IP:端口,告诉它的对等机那是什么,每个人都做防火墙穿越的事情,然后我们都设置好了…。对吗?

嗯,这是一个喜忧参半的问题。这在某些情况下会奏效,但在其他情况下不会。一般来说,这适用于大多数家庭路由器,而对于一些企业NAT网关则会失败。NAT设备的手册中提到它是安全设备越多,失败的可能性就越大。(NAT不会以任何有意义的方式增强安全性,但这是另一次的大肆宣扬。)。

问题是我们早些时候做的一个假设:当STUN服务器告诉我们从它的角度看我们是2.2.2.2:4242时,我们认为这意味着从整个互联网的角度看我们是2.2.2.2:4242,因此任何人都可以通过与2.2.2.2:4242交谈联系到我们。

事实证明,这并不总是正确的。一些NAT设备的行为完全符合我们的假设。他们的有状态防火墙组件仍然希望看到数据包以正确的顺序流动,但我们可以可靠地找出正确的IP:端口给我们的对等点,并做我们的同步传输技巧来通过。这些NAT很棒,我们的STUN和同时发送数据包的组合将与这些配合得很好。

(从理论上讲,还有一些NAT设备是超级宽松的,并且根本不附带状态防火墙之类的东西。在这些情况下,你甚至不需要同时传输,STUN请求给了你一个互联网IP:端口,任何人都可以连接到,而不需要进一步的仪式。如果这样的设备仍然存在,它们就会变得越来越罕见。)。

其他NAT设备的难度更大,并且会为您与之通话的每个不同目的地创建完全不同的NAT映射。在这样的设备上,如果我们使用相同的套接字发送到5.5.5.5:1234和7.7.7.7:2345,我们将在2.2.2.2上得到两个不同的端口,每个目的地一个。如果你用错误的端口顶嘴,你就打不通。

既然我们已经发现并不是所有的NAT设备都以相同的方式工作,我们应该谈谈术语。如果您以前做过任何与NAT遍历相关的操作,您可能听说过“完全锥体”、“受限锥体”、“端口限制锥体”和“对称”NAT。这些术语来自对NAT穿越的早期研究。

老实说,这个术语相当令人困惑。我总是看一看受限的锥形纳特应该是什么样子的。从经验上讲,我并不是唯一这样做的人,因为大多数互联网都把“简单”的NAT称为“全圆锥”,而现在它们更有可能是端口受限的圆锥。

最近的研究和RFC提出了一个更好的分类法。首先,他们认识到,与早期研究中单一的“锥体”维度相比,行为有更多不同的维度,所以关注NAT的锥度并不一定有帮助。其次,他们想出了更清楚地传达NAT正在做什么的词语。

上面的“简单”和“硬”NAT在一个维度上是不同的:它们的NAT映射是否取决于目的地是什么。RFC4787将简单变体称为“端点独立映射”(Endpoint-Independent Mapping,简称EIM),将硬变体称为“端点依赖映射”(Endpoint-Dependent Mapping,简称EDM)。EDM有一个子类别,它指定映射是仅在目标IP上变化,还是在目标IP和端口上都变化。对于NAT穿越,区别并不重要。这两种EDM NAT对我们来说都是坏消息。

在命名困难的伟大传统中,与端点无关的NAT仍然依赖于端点:每个源IP:port都会得到不同的映射,因为否则您的数据包将与其他人的数据包混合在一起,这就是Hao。严格地说,我们应该说“Destination EndpointIndependent Mapping”(Deim?),但这太冗长了,而且由于“SourceEndpoint Independent Mapping”是“破坏”的另一种说法,我们没有具体说明。Endpoint始终表示“目标Endpoint”。

您可能想知道2种端点依赖如何映射到4种锥度。

.