layout | title | date | categories | tags | comments | mathjax | copyrights |
---|---|---|---|---|---|---|---|
post |
NAT穿透——原理与实践 |
2023-12-10 00:00:00 +0800 |
网络 |
nat |
true |
true |
原创 |
今年暑假的时候,学校为了护网,禁掉了所有的内网穿透工具。尽管这种拔网线的方式会给远程科研带来巨大的麻烦,但其无疑是极为有效的安全措施。那究竟什么是内网穿透?它又有哪些安全风险呢?
我们先简单回顾一下什么是 NAT。
随着可用 IPv4 的数量不断减少,人们开始探索如何压榨出单个 IPv4 地址的最大价值,让一个地址可以被多台电脑使用,这就是 NAT (Network address translation) 技术。NAT 技术的原理是,让一些电脑组成一个私有子网,这些电脑共享同一个公网 IP 地址,但是每台电脑都有自己的内网 IP 地址。
举个例子,我在并夕夕上下单了一个人形玩偶,收货地址是宿舍的地址。但快递员在送货时,只会将人形玩偶送到学校快递站。快递站再根据我的具体地址,把人形玩偶交到我手中。同样的,如果我要寄一封情书给南大的女同学,我也应该先把情书交给快递站,然后由快递站统一寄出。
在网络上,公网中任意主机的 IP 地址就是并夕夕商家或者南大女同学的地址,本地子网内的 IP 地址就是我的地址,NAT 路由就是这个快递站。
我们用一个具体的例子来说明这一过程。假设 NAT 路由有一个公网地址 5.5.5.5
,本机分到的内网地址为 1.0.0.2
,需要访问的服务器地址为 2.2.2.2
。
发送报文的过程为:
- 内网主机把数据包的源 IP 地址设为
1.0.0.2
,目的 IP 地址设为2.2.2.2
,并发送出去 - 数据包到达 NAT 路由后,NAT 路由把源 IP 地址改为
5.5.5.5:69
,并记住69
端口对应内网的7.7.7.7
- NAT 路由将报文发给公网的
2.2.2.2
接收报文的过程为:
- 公网服务器把数据包的源 IP 地址设为
2.2.2.2
,目的 IP 地址设为5.5.5.5:69
,并发送出去 - 数据包到达 NAT 路由后,NAT 路由查表发现
69
端口对应着内网的1.0.0.2
- NAT 路由将报文发给内网的
1.0.0.2
如果你想进一步了解 NAT 的具体实践,我推荐你阅读这篇文章:https://www.karlrupp.net/en/computer/nat_tutorial。
在 NAT 中,内网主机可以作为客户端向公网服务器发送报文,并接收服务器的回复。但如果内网主机要作为服务器,则面临两个严峻的问题:
-
NAT 映射:在内网主机不主动发送报文的情况下,公网用户并不知道 NAT 给内网主机分配的端口号。
-
有状态防火墙:NAT 往往是单向的。只有内网主动发送报文后,NAT 才会允许对应的回应报文发往内网。
请注意,这两个问题是整个内网穿透的核心。我们接下来的所有努力都是在解决这两个问题。
假如我们在内网中有一台服务器,内网地址为 1.0.0.2
,在 80
端口上提供 HTTP 服务。公网中有一个地址为 2.2.2.2
的用户想要访问该服务,那么有几种选择:
-
端口映射 (Port Mapping):在 NAT 路由支持的情况下,我们可以让 NAT 路由直接映射需要的端口。我们可以让 NAT 路由开放
80
端口,并将这个端口对应到内网的1.0.0.2:80
。这样,用户只需要使用http://5.5.5.5/
正常访问 NAT 路由即可。通常,这是通过 UPnP (Universal Plug’n’Play)、PMP (Port Mapping Protocol)、PCP (Port Control Protocol) 等协议实现的。如果 NAT 路由开启了相关协议,内网用户就只需要让 NAT 路由把自己的某个端口映射到公网的某个端口即可。当然,出于安全原因,很多时候 NAT 路由并不会开启相关功能。 -
中继服务器 (Relay Server):如果我们钱比较多,那么可以在公网上在搞一台轻量级的服务器用作流量转发,并向用户开放
80
端口。内网服务器首先要和中继服务器建立稳定连接,然后告诉用户中继服务器的地址。用户访问http://3.3.3.3/
时,中继服务器将流量转发给内网服务器;内网服务器回应时,也通过中继服务器转发。这样的模式被称为 TURN (Traversal Using Relays around NAT) -
反向连接 (Reverse Connection):如果我们提前知道谁要和我们通信,那就可以先发制人,主动连接。此时,相当于我们自身成为了客户端。
-
STUN (Session Traversal Utilities for NAT):如果公网上的用户也是自己人,那就可以利用 STUN 协议预先获得 NAT 分配的端口号,把端口号告诉公网用户,然后直接连接。
这些方法可以单独使用,也可以结合使用。在这四种方法中,端口映射和反向连接通常都是条件不允许的;中继服务器往往可行,但会带来额外的开销;只有 STUN 几乎是万能的。我们接下来进一步探究其工作原理。
前面提到,当内网主机发出的报文经过 NAT 时,NAT会将数据包的源地址和端口全部修改,服务器也只能看到修改过后的地址和端口号。那么,如果这台服务器受我们控制,不就可以获得端口号了吗?
因此,我们只需要一台 STUN 服务器,并让其返回收到的地址和端口号。
现在,我们看起来已经可以知道端口号了。但问题还没有解决。
有些 NAT 路由会针对每一条链接分配不同的端口。比如,在和外网主机 A 交互时走的是 69
端口,但和主机 B 交互时走的是 96
端口。如果主机 B 依然把报文发送到 69
端口,那么就会被防火墙拦截。这种更高级、也更安全的防火墙被称为端点相关有状态防火墙 (Endpoint-Dependent firewall)。
这时候,我们就无法提前使用 STUN 服务器获取到开放的端口了。但这也不是没有解决办法。比如,我们可以把 65535 个端口全部扫描一遍,直到找到正确的那个!这样的方法理论上是可行的,但会消耗大量时间,而且很可能会触发 NAT 侧的入侵检测。
减少扫描次数的方法也并非没有。我们可以基于生日悖论,在 NAT 多打几个洞,这样扫描到可用的端口的概率会大幅度提升。
而当扫描完全不可行时,我们就不得不使用中继服务器了。中继模式在绝大多数情况下可以作为一个后备选项——只是其性能太差了一些。
我们前文都在讨论单个 NAT 的情况,接下来我们看点更复杂的。
某些场景下,通信双方都在 NAT 的子网中,比如在家连接办公室网络工作。这时候该如何发送信息呢?
前文讲过,NAT 防火墙的特性是不发送就不肯接收。如果我们在家庭子网向办公网发送一条消息,那么肯定会被办公网的 NAT 挡在外面。但是,如果办公网几乎同时也发了一条消息给家庭网,由于家庭网已经发过消息,所以办公网发来的消息是可以进入家庭网的!这时,由于办公网也发过消息了,所以家庭网后续的消息便也可以进入办公网。
发现问题没有?问题就在于这个“同时”。要想接收消息,就必须发送消息;要想发送消息,又必须接收到消息。事情陷入了死循环。必须有一种方法,能帮助家庭网和办公网完成“同时发送消息”这一目标。
可行的方案只能是持续不断的发包,干等家庭网的连接——这会消耗大量的资源。另一种可行的方案是使用第三方服务器(比如中继服务器)帮助完成协商,这样的方法谈不上很好,但总比不停发包强。
如果两个 NAT 都是端点相关有状态防火墙呢?这时候就麻烦得多了,因为两道防火墙均需要扫描,扫描需要的尝试次数变成了原来的平方。
那如果两个 NAT 路由的防火墙朝向一致——或者说,NAT 里套了一个 NAT 呢?
其实,这对这个信息传输是没有任何影响的,刚刚的方法依然适用。此时,我们操纵的是最靠近内网主机的那台 NAT 路由。但是缺点也是有的,我们在这中情况下,无法操纵到别的 NAT 路由——甚至无法得知它们的存在。特别当外层 NAT 路由是运营商路由 (CG-NAT) 时,我们根本无法控制到,更不用说做端口映射了。因此,嵌套式的 NAT 通常是不被推荐的。
但是,由于 IPv4 极度紧张,CG-NAT 其实较为常见。这时候,便引出了我们看起来最复杂的一个问题:NAT 内的多个 NAT 如何互相穿透?
如果其中一个 NAT 可以进行 NAT 映射,那么很好,问题轻松解决。如果不能,那就要用到外层 NAT 路由的 hairpinning 模式。该模式会把内网的流量通过 NAT 路由的 internel 端口转发到内网。此时,和之前提到的普通的双 NAT 穿透没什么不同。
但是,事情远远没有这么简单。很多 NAT 路由压根不支持 hairpinning 模式,当它们遇到内网到内网的包时,会选择直接扔掉!怎么办?那就不得不使用中继服务器了。
上面各种难题、各种解决方案归根到底还是因为 IPv4 太少了。但现在 IPv6 依然没有普及开来,对于 IPv6-only 的子网,又该如何穿透给 IPv4 公网呢?
NAT64 就是做这个工作的。它可以将 IPv4 和 IPv6 地址互相映射,使得对于内网主机来讲,它看到的全是 IPv6。而如果内网主机需要访问 IPv4 资源,DNS64 会给出一个虚构的 IPv6 地址,并将真正的 IPv4 地址和虚构的 IPv6 地址的对应关系记录下来。内网主机访问该虚构的 IPv6 时,DNS64 会首先介入,将地址转换为真正的 IPv4 地址。
换句话说,对于 IPv6-only 子网内的设备,
- 连接公网 IPv4 设备时
- NAT64 负责修改源地址,用于欺骗公网 IPv4 设备
- DNS 负责修改目的地址,用于欺骗内网 IPv6 设备
- 连接公网 IPv6 设备时,可以直接连接,不需要穿透。
上面这一套技术被称为 CLAT (Customer-side transLATor),移动端设备往往会自带。这是因为不少运营商网络是 IPv6-only 的,移动设备必须实现无感穿透。
以上讲了这么多情况的解决方案,有没有一劳永逸、把所有方案结合到一起的办法呢?这就是 ICE (Interactive Connectivity Establishment) 协议,其会去寻找最好的信道。简单来讲,ICE 协议下的内网穿透分为以下几个步骤:
- 以中继模式建立连接
- 确定自己的信息
- 内网 IPv6 地址
- 内网 IPv4 地址
- 公网 IPv4 地址(通过 STUN 服务器获取 / 通过端口映射获取 / 其它方式)
- 通过中继模式提供的旁路信道(或第三方服务器)和 peer 交换自己的信息
- 不断探测对方给的所有地址,并找出效果最好的那个(LAN > WAN > WAN+NAT)
- 在最好的信道上通信,并不断发送 PING/PONG 保活
- 如果没有找到可用的地址,则在中继模式下进行通信
最后,我们再简单探讨一下内网穿透的安全性。从其原理可以发现,内网穿透本身并没有什么安全问题,其安全性基本取决于上层协议的安全性。内网穿透最可能的安全问题是中继服务器被劫持,但如果使用了 P2P 加密,则无需考虑这个问题。
- https://en.wikipedia.org/wiki/Network_address_translation
- https://www.karlrupp.net/en/computer/nat_tutorial
- https://tailscale.com/blog/how-nat-traversal-works/
- https://en.wikipedia.org/wiki/NAT_traversal
- https://datatracker.ietf.org/doc/html/rfc6886
- https://datatracker.ietf.org/doc/html/rfc5766
- https://openconnectivity.org/developer/specifications/upnp-resources/upnp/internet-gateway-device-igd-v-2-0/
- https://datatracker.ietf.org/doc/html/rfc8445