距离上次更新本文已经过去了 326 天,文章部分内容可能已经过时,请注意甄别

1. 引入

如果你折腾过 NAS 或者 BT 下载等等玩意,可能听说过 “P2P 打洞” 这一技术名词。简单来说,P2P 打洞可以让我们直接在外网访问内网的设备,从而让没有公网 IP 的家庭设备也能获得 “公网直连” 的速度。

比如绿联、极空间等国产 NAS 的客户端,在外网访问的时候,都会先尝试 P2P 打洞让你和你的 NAS 实现 P2P 直连,打洞失败的时候才会采用服务器转发的方式;

plaintext
1
2
P2P打洞后:你的当前设备和家里的NAS直接通信
采用服务器转发:报文从家里NAS - 服务器 - 你的当前设备

因为服务器转发的速度直接依赖于服务器的出口带宽,所以使用服务器转发的用户越多,速度也就越慢,有的时候你会发现服务器转发的速度甚至不如没开会员的某度云盘。

2. 前置技术了解

在学习 P2P 打洞之前,你需要了解以下知识点。注意,本文只是对 P2P 打洞的简单理解和说明,深入了解底层原理请自行百度其他更详细的文章!

2.1. 什么是 P2P?

P2P 即 Peer to Peer,是一种对等连接方式,纯 P2P 架构包含如下内容

  • 没有总是在线的服务器
  • 任意端之间直接通信
  • 对等方之间可以间断链接,并可以动态改变 IP 地址

P2P 技术的实际的用例如下:

  • 文件分发(BT 下载)
  • 流媒体
  • VolP
  • 内网穿透式访问(建立 P2P 链接,直接和内网主机通信)即本文即将要讲述的 P2P 打洞。

2.2. NAT 类型

参考本站 IP 层和数据链路层 博客中 4.3 节对 NAT 技术的解析。

2.2.1. NAT2:地址限制锥形 NAT

所谓地址限制锥形 NAT,即 NAT 映射之后的外网端口和 IP 地址对请求具有一定的限制,这个限制体现为它只允许已建立链接的主机与之进行通信。

  • 局域网主机 C 需要和公网服务器 S1 进行通信
  • 局域网主机 C 发送请求报文,通过运营商路由器转发,抵达 S1 服务器
  • 这个过程中每一层的路由器都建立了一个 NAT 转发表
  • 如果是限制锥形 NAT,运营商的路由器将会限制该 NAT 地址的外网访问 IP 地址:
    • 只有 S1 服务器发来的报文能被正常往内网转发(端口不限制),发送给给局域网主机 C;
    • 其他公网服务器(和 S1 不同 IP 地址)如果企图向该 NAT 地址发送报文,会被运营商路由器拒绝;

举个具体的的例子,在地址限制锥形 NAT 下

  • C 请求 1.1.1.1:8080,会建立一个 NAT 映射,假设是 2.2.2.2:9000
  • 只有 1.1.1.1 这个 IP 能发送报文给 NAT 地址,对来源端口不限制;
  • 其他来源 IP 都会被 NAT 地址拒绝。

2.2.2. NAT3:端口限制锥形 NAT

在限制锥形 NAT 的基础上,添加了端口的限制,即只有局域网设备 C 请求过的 S1 的端口,才能发送信息给局域网主机。

plaintext
1
2
3
4
端口限制锥形NAT下
C请求 1.1.1.1:8080
只有 1.1.1.1:8080 能发送报文给C的NAT地址
其他来源IP或者1.1.1.1上不同的端口发来的报文都会被拒绝

2.2.3. NAT1:完全锥形 NAT

完全锥形 NAT 的映射过程同上,但是不会对外网访问做任何限制。

即 NAT 地址建立后,任何外网主机都可以发送信息给这个 NAT 地址

2.2.4. NAT4:对称式 NAT(动态 NAT)

对称式 NAT 把内网 IP 和端口到相同目的地址和端口的所有请求,都映射到同一个公网地址和端口;同一个内网主机,用相同的内网 IP 和端口向另外一个目的地址发送报文,则会用不同的映射(比如映射到不同的端口)。

和端口限制式 NAT 不同的是,端口限制式 NAT 是所有请求映射到相同的公网 IP 地址和端口,而对称式 NAT 是为不同的请求建立不同的映射。它具有端口受限锥型的受限特性,内部地址每一次请求一个特定的外部地址,都可能会绑定到一个新的端口号。也就是请求不同的外部地址映射的端口号是可能不同的,甚至请求同一个主机的不同端口也会映射到不同的 NAT 地址上。

说白了就是,客户端请求目标的端口不同就会更换映射;甚至请求的端口相同时都有可能更换 NAT 的映射。

2.3. NAT 类型 IP 端口映射示意图

根据 NAT 类型,可能产生如下映射表,表中 <-> 符号左侧表示内网主机 IP 和端口,<-> 符号右侧表示 NAT 的外网 IP 和端口,@符号右表示限制条件:外网主机地址 IP 和端口。

image.png

2.4. 如何验证 NAT 类型

通过这个表,也能反推出我们应该如何验证一个 NAT 的类型

  • 局域网客户端 C 向服务器 S1 发起请求,服务器 S1 能得到一个 C 的 NAT 地址和端口;
  • 服务器 S1 让 C 向自己的另外几个端口发起请求,如果这些请求的来源 IP 和端口都一致,则代表 C 是锥形 NAT,如果出现了不同的端口,则代表是对称 NAT
  • 服务器 S1 将这个地址和端口交给服务器 S2,S2 向 C 发送一个报文;
  • 如果 C 收到了这个 S2 发来的报文,代表是完全锥形 NAT,不对外网来源做限制;
  • 如果 C 没有收到,说明是限制锥形 NAT;
  • 服务器 S1 换一个端口向 C 的 NAT 地址和端口发起请求;
  • 如果 C 收到了,则代表是地址限制锥形 NAT,对端口不做限制;
  • 如果 C 没有收到,则代表是端口限制锥形 NAT,对地址和端口都做了限制;
  • 服务器 S1 尝试让客户端 C 用相同的本地端口,给 S1 的另外一个端口发送请求,如果两次请求的来源端口出现了变化,说明是对称式 NAT

这里客户端和服务器都是我们单独实现的程序,所以可以通过自定义特定的字段来让客户端、服务器做不同的操作。验证客户端的 NAT 类型至少需要两个不同 IP 的公网服务器。

博客:NAT 打洞_nat1 打洞 - CSDN 博客 中有一个不错的流程图,在此引用一下

image.png

现在假设 192.168.0.100 通过 8000 端口 访问 114.135.246.90 的 9001 端口

经过 NAT 之后,会形成这样一种结果:

  • 内网的 192.168.0.100:8000 ↔ 120.230.117.10:9999 NAT 公网 IP,形成绑定关系
  • 服务端 114.135.246.90:9001 被 120.230.117.10:9999 NAT 公网 IP,访问

然后如果客户端 192.168.0.100 的 8000 端口:

  • 8000 端口 能收到 紫色 发来的消息,就是 NAT1
  • 8000 端口 收不到 紫色 发来的消息,却能收到 浅蓝色 线发来的消息,就是 NAT2
  • 8000 端口 收不到 紫色 和 浅蓝色 线发来的消息,只能收到 绿色 线返回来的消息,就是 NAT3 | NAT4

NAT3 和 NAT4 的区别:

  • NAT3 是 8000 端口与 9999 端口形成绑定关系,不管通过 8000 端口访问谁,・都是从 9999 端口出去
  • NAT4 是 8000 端口与 9999 端口形成的绑定关系是有条件的,8000 端口访问同一 ip:port 时,都会从 9999 端口出去;但通过 8000 端口访问其他 ip:port 时,就会绑定 NAT 其他端口,比如 9990(目标端口变化 NAT 就会变化)

有流程图片,看起来会更清楚一点。

你可以下载微力同步(这是一个 P2P 的远程加密备份软件),在它的设置里,就可以看到你当前的网络环境是什么类型的 NAT。

image.png

3. P2P 打洞以及穿透性

3.1. P2P 如何打洞?

如果想实现 P2P 打洞,那么当前局域网内设备必须是在锥形 NAT 下,才能实现直接通信。因为在对称 NAT 下,NAT 的端口地址会经常变化,很难实现稳定的连接。

对于锥形 NAT,比较好打洞的自然是完全锥形NAT 了,因为它对外网的来源 IP 和端口都不做限制,那么就可以用下面的流程来实现打洞

  • 需要中间服务器 S
  • 客户端 C1 和 C2 都是完全锥形 NAT
  • 客户端 C1 请求和 C2 实现 P2P 连接
  • 客户端 C1 向中间服务器 S 发起请求,NAT 会为 C1 维护一个端口和地址
  • 服务器 S 收到 C1 的请求,并对内网客户端 C2 下发一个通知,让 C2 也来请求 S
  • C2 也向服务器发起一个请求,NAT 为 C2 也维护了一个地址
  • 此时服务器 S 就同时知道 C1 和 C2 通过 NAT 出来的一个公网端口和 IP 地址
  • 服务器 S 将 C1/C2 的 IP:端口互相告知对方
  • C1 和 C2 都得到了对方的 IP 和端口,因为是完全锥形 NAT,双方可以直接通信
  • 打洞完成

这个过程中,中间服务器 S 是不可或缺的,如果没有这个中间服务器,C1 和 C2 就很难知道对方的 NAT 公网 IP 和端口,也就没有办法直接实现通信。

个人理解,只要 C1 和 C2 有一方是完全锥形 NAT,那么 P2P 打洞就是比较容易实现的,假设 C1 是完全锥形 NAT,C2 是有限制的 NAT:

  • 服务器 S 知道了 C1 的 NAT 的公网 IP 地址和端口后,将其告知 C2;
  • C2 重新对 C1 的完全锥形 NAT 发送请求,限制锥形 NAT 会对 C2 的新请求更新自己的限制表中的白名单,将 C1 的 IP 地址和端口也加入到了这个白名单中
  • 此时限制锥形 NAT 的限制就消失了,C1 和 C2 依旧可以点对点发起通信。

上述过程还是比较好弄明白的。

3.2. NAT 穿透性表格

下表展示了不同 NAT 组合的穿透性。

image.png

但是我没想明白两边都是端口限制型 NAT 如何打洞?百度也没找到好的说明。

搞清楚这个问题后我会回来补充本文。

3.3. NAT3 如何进行打洞?

2024.05.04 更新:之前没有弄明白两端都是端口限制 NAT(即 NAT3)如何进行打洞,经过一位大佬的指点,现在弄明白了。

假设 A 和 B 都是端口限制锥形 NAT,为了简单起见我直接用客户端的名字来代指最后映射的 NAT 公网 IP 地址,S 代指中间服务器。

plaintext
1
2
A通过内网端口1000给S:80发送请求,建立映射 A:公网8000 - S:80
B通过内网端口2000给S:80发送请求,建立映射 B:公网9000 - S:80

假设是 A:内网1000S:80 通信,建立了 A:公网8000 -> S:80 的 NAT3 映射,同时 B 建立了 B:公网9000 -> S:80 的 NAT3 映射。

这时候就需要 A:内网1000 发送一个消息给 B:公网9000,此时因为 A 的内网端口没有变化,所以 A 的公网的端口也不会变(还是 A:公网8000),A 端的 NAT 设备就会将 B:公网9000 加入到请求历史中,即允许了 B:公网9000 发送消息给 A:公网8000 -> A:内网1000

但是因为 B 端的 NAT 设备不认识 A:公网8000,这个请求会被直接抛弃。

这时候让 B 端也用原本的内网端口 2000 发送一个消息给 A:公网8000(这个消息可以正常送到 A 端),就相当让 B 端的 NAT 设备也把 A:公网8000 加入到允许列表中,这时候 A 和 B 就可以直接通过 A:公网8000B:公网9000 进行通信了,打洞成功!

3.4. 打洞失败怎么办?

如果打洞失败,C1 和 C2 就得借助中间服务器 S 的转发来实现通信,此时通信的速度就会出现限制。同时,中间服务器 S 的带宽 / 流量资费也比较感人。

这也是为什么绿联和极空间虽然提供了中间服务器转发的方式,但依旧会想办法让你和你的 NAS 能实现 P2P 直接通信。除了能提高连接速度,更多的是节省它们服务器的带宽和资费。

4. The end

P2P 打洞的大概原理就是如此,更深层次的就不再研究了~你看明白了吗?