Skip to content

门泊吴船亦已谋

NAT 穿透

P2P 网络需要考虑的一个非常折磨的细节就是并非每个节点都有固定的公网 IP。 于是就要开始考虑 NAT 穿透的问题了。

因为大多数用户都没有公网 IP 的需求,运营商通常默认是不会提供公网 IP 的。 于是也很少有人专门去折腾公网 IP,还能省点网费。 这个时候内网的主机想要跟公网的主机交互就需要使用 NAT 穿透来获取一个公网 IP。

基本 NAT

静态 NAT

最简单的就是静态 NAT,就是给你主机的内网 IP 直接绑定到一个固定的公网 IP 上。 往外界发包的时候,源 IP 就会被交换机或者路由器替换为这个公网 IP。 外界的主机向你发送信息时,目标 IP 最初也是这个公网 IP,然后会被替换回内网 IP,最后才发回给你的主机。 在整个过程中你的主机只知道自己的内网 IP,对于这个公网 IP 是完全不知情的。 当然,运营商不可能真的给每个内网 IP 都配一个公网 IP。

动态 NAT

所以接下来就是动态 NAT。 打个比方,整个小区可能只持有小区里电脑数量十分之一的公网 IP。 考虑到并不是每台电脑都时刻需要跟外界交互。 于是运营商只有在某台电脑向外界发包的时候才借一个公网 IP 给他,也就是在内网 IP 到公网 IP 的映射表中添加一条记录。 当交换机认定这个内网 IP 跟外界的通讯结束之后,这条记录就会被删除,也就是把借给它的公网 IP 强行要回来。

NAPT

基本 NAT 没有对端口进行映射。 然后我们发现实际上可用的端口空间是很大的,也就是说很多端口都是空闲的。 于是就考虑更细的粒度:把端口也做一个映射。 比方说来自内网中不同电脑的两个包,我可以直接给他映射到同一个公网 IP 上,随便给他分两个不同的端口就行了。 当然,同一台电脑通过不同端口发出的包也有可能被映射到不同的公网 IP 上。

完全圆锥型 NAT

一台内网机器向外网发出一个包,这个包的源 IP(内网)和源端口就会被固定映射到一个公网 IP 和端口,这个映射关系会维持足够长的时间。 外网的机器主动访问这个公网 IP 和端口也能够被转发到这个包的源 IP 和源端口,最终正确的被内网机器接收到。

受限圆锥型 NAT

在完全圆锥型的基础上,外网机器就算知道映射关系,往正确的 IP 和端口发包,这个包也会被交换机拦截。 只有在内网机器主动向这台外网机器发包之后,交换机才不会拦截这个包。

因为只有内网机器主动联系外网,映射关系才会建立起来。 并且大部分情况下,只有内网机器主动联系外网机器,这台外网机器才有可能知道正确的 IP 和端口。 所以交换机一旦发现有个外网的机器擅自向这个 IP 发包,它根本无法分辨到底是正常的请求还是 DDoS 攻击,为了安全性考虑,应当直接拒绝。

端口受限圆锥型 NAT

比普通的受限圆锥型多一个约束:外网主机的端口必须是固定的。

也就是说只有内网主机向外网主机的某个端口发包之后,从外网主机的这个端口发来的包才不会被拒绝。

对称 NAT

非常离谱,只有内部 IP、内部端口、外部 IP、外部端口都相同的时候,这个包才会被映射到固定的公网 IP 和端口。 如果一台机器向不同外网 IP 或不同的端口发包(包初始的源 IP 和源端口都固定),会产生不同的映射,也就是说这些外网机器收到的包的源 IP 和源端口是不同的。

NAT 穿透

接下来我们就要考虑不同内网中的两台机器之间建立连接的问题了,也就是 NAT 穿透的问题。 首先两台机器不知道自己映射的公网 IP 和端口,也不知道对方的,发包都不知道往哪里发。

因此只有这两台肯定是不行的,这个时候我们需要一台有公网 IP 的公共服务器。 假设这两台内网的机器是 A 和 B,公共服务器是 C。 首先 A 和 B 各自跟 C 建立连接。 于是 C 就拿到了 A 和 B 的公网 IP 和端口,它只要把这些信息告诉 A 和 B,理想情况下 A 和 B 就能互相连接了。 没错,这是理想情况,比如说基本 NAT 或者完全圆锥型 NAT。 而操蛋情况,比方说受限圆锥型,如果 A 没有主动发包,B 即使对着 A 的正确地址发包,也会被交换机拒绝。 这个时候就需要打洞了。

假设机器 A 的 NAT 是受限圆锥型,C 就需要通知 A 向 B 的方向打洞,也就是说 A 需要发一个以 B 的公网 IP 为目标 IP 的包。 这样 A 那边的交换机就会允许 B 的这个公网 IP 向 A 发包。 于是 B 直接对着 A 的公网 IP 和端口发包,A 就能收到了。

如果 A 的 NAT 是端口受限圆锥型,C 就需要向 A 传达 B 跟 C 建立连接时用的端口,然后 A 要向 B 的公网 IP 的这个端口发包。 最后 B 需要使用跟 C 建立连接的端口向 A 的公网 IP 和端口发包,才能跟 A 建立连接。

如果 A 那边的 NAT 是对称 NAT,那么就不用考虑让 A 打洞了。 因为 B 如果要跟 A 建立连接,交换机会给 A 映射到另一个地址,C 和 B 都没有办法知道。 这个时候只能考虑让 B 打洞,然后要求 A 主动去连 B。 如果很倒霉 B 那边也是对称 NAT,那就没救了。

UDP 通信

上述打洞的方法实际上是针对 UDP 来说的。 因为 UDP 允许在同一个端口上开多个套接字。 所以,A 和 B 用来跟 C 通信的端口同时还能用来进行 A 和 B 之间的通信。

TCP 通信

如果想要 A 和 B 进行 TCP 通信就麻烦了,因为 TCP 不允许多个套接字绑定到同一个端口上。 实际上这并不是协议的问题,而是接口的问题。 不过好在很多操作系统实际上是支持一种特殊的 TCP 套接字选项,一般叫做 SO_REUSEADDR。 这个选项允许多个套接字绑定到同一个局部地址上,前提是这些套接字必须全部都启用这个选项。 把相关的套接字都开启 SO_REUSEADDR 之后,A 和 B 就能以类似于 UDP 的方式建立 TCP 通信。