TCP实现P2P(NAT3-NAT4)

TCP实现P2P(NAT3-NAT4)

前言这篇讲的主要是在原来的基础上,对于 NAT4 方面的 实现思路,不想看过程可以 省流

如果你对 NAT 完全不了解,可以到这里 NAT打洞 看一下其中的 初识部分如果你对 NAT 有些了解,但不知道如何让他们之间进行通信,可以看一下这篇 TCP打洞,实现P2P

可运行的代码放在了 Github仓库

当你开始阅读本文时,默认读者已具备一定的网络基础以及对 NAT 有一定深度的理解那我们开始!

NAT3 和 NAT4 的区别先说明一下:L:A — NL:H —— S:Z —— NR:J — R:U

L

NL

S

NR

R

L主机

L侧的NAT

服务端

R侧的NAT

R主机

端口就不用怎么说明了吧……

NAT3NAT3 是短时间内 协议+端口 是固定不变的协议主要是分UDP和TCP,端口变化 比如说L主机用 A端口 TCP 访问:S的Z端口、R主机的U端口、R主机的V端口,就会有以下三条记录:

123L:A - NL:H - S:Z // S的 Z 端口L:A - NL:H - R:U // R主机的 U 端口L:A - NL:H - R:V // R主机的 V 端口

flowchart LR

subgraph L

A

end

subgraph NL

H

end

subgraph R

U

V

end

subgraph S

Z

end

A --> H --> Z

H --> U

H --> V

只要用 L:A 发出数据,都会从 NL:H 出去

NAT4NAT4 是不管访问谁,只要 协议+对端IP+对端端口有一个不一样,NL就 不会 用同一个端口将请求发出,上面的记录就会变成

123L:A - NL:H - S:Z // S的 Z 端口L:A - NL:G - R:U // R主机的 U 端口L:A - NL:F - R:V // R主机的 V 端口

flowchart LR

subgraph L

A

end

subgraph "NL"

F

G

H

end

subgraph R

U

V

end

subgraph S

Z

end

A --> H --> Z

A --> G --> U

A --> F --> V

所以可以大致的认为:

处于NAT3的主机,本地端口不变,NAT的出端口也不变

处于NAT4的主机,不管怎样,建立新的连接时 端口都会变

打洞谁来打洞打洞是为了让外面的 SYN 进来,那维持端口的稳定保持不变,以及让端口长时间存活就成了关键,同时为了提高成功率和缩短连接,端口应该尽可能让建立连接的一方知道

简单来说就是,看上文出现的三条记录:

假设让 NAT3 一方进行打洞

graph LR

A[L:A] --> B((NL:H))

B --> Z(S:Z)

B --> U(R:U)

B --> V(R:V)

这时只要通过L:A 向 任意主机 发送数据都可以来维持 NL:H,同时因为H端口是 “共用” 的,所以 H 的端口信息可以被 S 知晓后,S可以同步给R,让 R 主动给 H 发送数据包 L:A <--x-- NL:H <-- R:*

假设让 NAT4 一方进行打洞

graph LR

A[L:A] --> H((NL:H))

A[L:A] --> G((NL:G))

A[L:A] --> F((NL:F))

H --> D(S:Z)

G --> E(R:U)

F --> V(R:V)

这时 L 想要维持洞口,需要维持3个端口,想要维持 H端口 ,只能通过L:A不断向 S:Z 发出数据,同理G、F亦是如此而且三个端口的信息并不能互通 “共用”,互通了也无法使用(就是 R:V 给 H/G 发送消息也收不到)

因此让处于 NAT3 进行打洞,更为合适

给谁打洞现在知道是由 NAT3 的一方进行打洞了,但对方是 NAT4 ,端口是每次连接都会变化的,那给谁打洞呢?这个问题,一些小伙伴其实已经发现 上面已经出现了解决方案了,还是这两条记录并且我们假设让 L 处于 NAT3 中,R 处于 NAT4 中,加上 NR,把原来中的 S 去掉,再简化一下,就会呈现:

flowchart LR

subgraph NAT3

L:A --> NL:H

end

subgraph NAT4

subgraph R

U

...

V

end

subgraph NR

J

xxx

K

end

end

V --> K

U --> J

NL:H --SYN--> K -.-x V

xxx --- ...

NL:H --SYN--> J -.-x U

这时候对于 NAT3 来说,不管 J 还是 K 发来的数据包 它都会放行

假设现在让 R 用 任意端口 ... 向 H 发出数据,就会出现 H <-- NR:* <-- R:* 的这么一条记录但这时 NAT3 并不会给 NR:* 发来的数据放行,因为没有 NR:* 的记录

那我们怎么添加这么一条记录呢?现在我们设想一个极端的情况:

如果 L:A - NL:H 给 NR 的 全端口 都发送了 SYN那是不是 R 不管从哪个端口发出 SYN只要是通过 NR 的 任意端口 出来的,都会被 NL:H 放行

实际上也确实如此,NAT3 一方打洞,与之前不同的是,这次不是只打一个洞,而是留出了多个洞口以供放行

但实际上我们不需要给全端口都发送 SYN一个是工作量大,可能后面发完 前面的洞口又维持不住了另一个是可能会被对方的 NAT 认为是扫描端口之类的活,被短时间封了就不好了

建立连接注意:与之前的连接不同,这次我们 L、R 都保持一条与 S服务端 的连接用于交换信息

蓝色块:代表专门与 服务端 通信,交换信息的连接黄色块:代表此次通信所带携带的信息实线:表示 主动 建立 新连接

sequenceDiagram

participant L

participant NL

participant S

participant NR

participant R

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

rect rgb(205, 235, 255)

par

L ->> +S : Step1: TCP - L:A > NL:H > S:Z

Note left of S: NL:H;注册信息

S-->> L:

Note right of L: NAT3;等待连接

R ->>S : Step2: TCP - S:Z < NR:K < R:J

Note right of S: NR:K想要与 L 连接

S-->> -L: Step3

Note right of L: R请求连接;打洞ID

end

end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

L ->> +S: Step4: TCP - L:B > NL:T > S:Z

Note left of S: NL:T同意R连接;打洞ID

rect rgb(205, 235, 255)

par 可选

S -->> R: Step4-1【可选】

Note left of R: NAT4;L 同意连接ID;等待发起连接

R ->> S: ① S:Z < NR:* < S:*

R ->> S: ② S:Z < NR:* < S:*

%% Note right of S: 判断这几次连接端口变换的规律

S -->> S: 判断端口变换的规律,确定打洞范围

end

end

%% S-->> -L: Step4: TCP - L:B < NL:T < S:Z

Note right of L: 该ID打洞范围:[NR:K-20, NR:K+30]

%% Note over NL: 该ID打洞范围 [NR:K-20, NR:K+30]

S-->> -L:

L ->> +NR: Step5: L:B > NL:T > NR:K±x(打洞)

NR --x L:

L->>L: Step6: ① Destroy TCP L:B② CreateServer Listen L:B

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

rect rgb(205, 235, 255)

par

L -->> +S: Step7: TCP - L:A > NL:H > S:Z

Note left of S: 打洞完成;打洞ID

Note left of R: 发起连接;NL:T

S -->> -R:

end

end

R ->> NR: Step8: NR:? < R:U

NR ->> -NL: Step8: TCP L:B < NL:T < NR:? < R:U

NL ->> NL: If ? ∈ [NR:K-20, NR:K+30]

NL ->> L:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Note over L,R: Step8: TCP - L:B < NL:T < NR:? < R:U

L-->>R: ACK

R-->>L: ACK

Step1-2: L 和 R 建立起与 S 的连接 用于交换信息,并且双方表示连接的意愿(后续可以以此来组网)这时服务端还可以充当 STUN 来判断他们所属的 NAT网络

Step3: S 判断需要打洞的一方(这里是L),并将请求连接的消息传给 L

Step4: L 将 “同意连接 表示可以打洞” 的信息,用新的端口 L:B 与 S 建立连接,并告知 S,这时 S 知道接下来打洞的端口是 NL:T

Step4-1【可选】: 这时 S 可以把同意连接的信息返回给 R,并让R给自己发起多次连接,S 就可以通过判断 R 端口的变化找出规律(比如最简单的单调递增 递减,又或者双端甚至多端的递增递减)

之后 S 将 需要打洞的端口 返回给 L

Step5: L 结束与 S 的连接,并尝试用 L:B ,向 S 返回的 需要打洞的端口 发起连接(这步并不会收到返回信息)

Step6: 随后 L 立即断开通过 L:B 发起的TCP连接,并开启 TCP Server 监听 B 端口

Step7: L (用Step1建立的连接) 向 S 发送 “打洞完成” 的信息

Step8: S 将 “打洞完成” 的信息 以及 NL:T 的信息发送给 R

Step9: R 收到信息后,会用 任意端口 直接向 NL:T 发起TCP连接(指SYN)

如果 NR:? 恰好是上面 “需要打洞的端口” 的话,NL:T 就会放行这个 SYN

如果 ? ∉ "需要打洞的端口",则任意一方都 没有 任何的回应

主要问题说了这么多,但这些都只是给大家提供的 实现思路 ,虽然我确实也 成功实现 并建立连接了

但真实的网络环境要复杂的多的多的多,举几个实现过程中可能遇到的问题:

Step8 中尝试建立连接,单一一条数据包成功率的肯定会低,可以尝试 多线程 多几个端口同时发出 效果肯定是更好的,说到底还是 “只要将一个 SYN 送进 NAT” 就能成,多发点没坏

最难的也不是 服务端 预测端口,但这个端口的范围肯定是越小越精确是越好的

最难的其实是 Step5 打洞这一步,这里并不仅仅只是 把数据包发送出去就完事了 这么简单, 而是端口的状态很难维持。

由于这个是套接字,端口会被绑定占用,本地想要快速发送,甚至可以很暴力的通过 Error 干掉线程 结束占用,然后快速的发送下一个数据包

但对于 NAT 来说就不是这样了,你要是过快,它可能还处于 SYN_SENT 状态,或者也极有可能处在 *_WAIT 状态这时它就会给你绑定一个 新的端口 与远端建立连接,这个时候就已经失去意义了

又或者是有些 NAT,你与下一个端口建立连接了,他就不放行之前发送过/连接过的数据包了就是 H 先给 J 发送,再给 K 发送,然后它就只允许 K 返回,J 返回的数据包直接丢弃

还有就是,什么时候将端口 由 Client 主动建立连接,转为 Server 来监听端口也是很关键

过早:还没打完洞 / 端口还被占用着

过晚:端口已经被弃了、别人的SYN 请求数据包已经发过了

甚至还有的 NAT 会将外部的主动发起的 SYN 给过滤掉,只允许同侧的、同区域的、甚至是同运营商的通过,这就让被动接收的可能性变得很小

总之,这些都是问题,我也只是在网络环境稳定,端口变化极小的情况下 (就是凌晨) 才得以成功,而且基本上也都还是要各种重连尝试,成功率很低很低,基本可以考虑放弃了。

但我很不爽,于是我又写了个 udp 的版本,当个爽局 (*^▽^*)^.^这里再贴个 Github仓库

UDP 实现采用 UDP 的话,对于 NAT3 端来说,可以说是毫无压力了呀因为可以开着 “Server” 监听,然后一直往外发数据就完了,真就给对面 全端口 发数据包能连进来的都加到一个队列里发心跳包,就可以一直维持了

sequenceDiagram

participant L

participant NL

participant S

participant NR

participant R

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

R ->> +S : Step2: UDP - S:Z < NR:K < R:J

Note right of S: NR:K想要与 L 连接

L ->> S : Step1: UDP - L:A > NL:H > S:Z

Note left of S: NL:H;注册信息

S-->> L: Step3

Note right of L: R请求连接;NR:K

L -->> S: Step4

Note left of S: 同意R连接;开始打洞

S -->> -R:

Note right of NR: NL:H;L 同意连接

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

L ->> +NR: Step5: UDP - L:A > NL:H > NR:K-x

L ->> NR: Step5: UDP - L:A > NL:H > NR:K±x

L ->> NR: Step5: UDP - L:A > NL:H > NR:K+x

R ->> NR: Step6: NR:? < R:U

R ->> NR: Step6: NR:? < R:U

R ->> NR: Step6: NR:? < R:U

NR ->> -NL: Step6: UDP L:A < NL:H < NR:? < R:U

NL ->> NL: If ? ∈ [NR:K-x, NR:K+x]

NL ->> L:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Note over L,R: Step8: TCP - L:B < NL:T < NR:? < R:U

L-->>R: heart

R-->>L: heart

Step1-2: 同理,无关先后,交换信息,让 S 知道 NL:H 和 NR:K

Step3: S 把 NR:K 带给 L

Step4: L 表示同意连接,同时注意需要维护与 S 的心跳包,S 也通知 R 开始给 NL:H 发起连接

Step5: 同意之后不需要等待S的回复,直接往 NR:K 附近的端口发数据包就行 直接 全端口 发送数据包 (确信)

Step6: R 侧不断给 NL:H 发出数据包,直到有响应 则代表连接成功

不需要维护端口 套接字就是简单得多,监听端口的同时,只管发就行,不需要顾及 Timing需要注意的是,维护与S的心跳包,可以让S主动给自己发送消息,比如说更为精确的洞口范围等等例如极端点的,R 长时间连不上,就会给 S 发送消息,S 可以让 L 重新往新的 NR:K 发送信息

虽然说 NAT4 通常是在变端口,但也不能完全排除 IP 也在变的情况,恰巧就碰上了呢,有些 NAT 就是会一段时间过后,在 IP池 重新选一个当出口

最终实验的结果,一般是两到三次就能成功连上了,然后耗时大概在 3-5s 左右,还是可以接受的最后还是建议直接 运行代码,实践才是硬道理

总结极致的省流:

NAT3侧 尝试给对面全端口发送数据包,等对面来连遇到 NAT4 不要用 TCP,改用 UDP ~O(∩_∩)O 哈哈~

最后说几句只能说,用 TCP 实现 NAT4 是真的很难,这个基本上就是瞎蒙,而且这还是 NAT3 - NAT4,还没到 NAT4 - NAT4

然后这个系列后面应该不会出 NAT4-NAT4 的了,主要是两个原因,一个是真的很难,就是碰运气其次是意义不大,目前 NAT4 绝大多是 移动网络 里使用,就是手机开数据其他的物联设备不太清楚,但物联设备也没有直连的意义,通常都是通过主机或是sink节点下发指令我能想到的场景可能就是两台电脑都分别连上两个手机热点,然后两台电脑开始尝试直连…….

后续有时间可能会出个组网的,老规矩先挖坑 然后咕咕咕毕竟大内网的情况下 NAT3 - NAT3 的场景还是居多,用 TCP 实现可以进行一些比如大文件的同步、传输等等

目前绝大多数 远程桌面控制 的软件都是基于UDP魔改的数据传输协议,比如向日葵的HSKRC这种,主要是提高成功率的同时,让 UDP 变得更可靠,同时也不至于像 TCP 那样 “太过可靠”至于画面什么的可以流畅就行,UDP 本就很适合;指令的数据包很小,改用 TCP 甚至走转发都可以

就是首先能连上了,之后怎样传输 还是有很大的魔改的空间的嘛

最后还提一嘴就是,虽然 去NAT化 的政策已经下来,IPv6 的推进速度也在不断加快,但 v4 和 v6 共存依旧是我的观点,而且内网的概念,不管是 v4 还是 v6 都会一直在。所有设备全公网那是不现实、不可靠的,处在内网的设备 或是 处在网关内的设备 仍将是大部分。既然说到 IPv6 了,感觉之后还可以谈谈我对 IPv6 的一些学习和理解,一直挖坑一直爽哈哈哈哈哈

相关创作

比利时国家队世界杯参赛次数
beat365网站地址

比利时国家队世界杯参赛次数

07-02 👁 4618
微信理财通4大基金哪个好 比一比就清楚了
beat365网站地址

微信理财通4大基金哪个好 比一比就清楚了

07-13 👁 2552