libp2p直连原理与Socket Reuse

最近看了一下 libp2p 的直连,里面比较关键的一个点是 NAT Hole Punching。

继续往下看会发现 TCP Hole Punching 里面还有一个很重要的东西:

Socket Reuse

这里简单记录一下 libp2p TCP 直连的过程,以及 Socket Reuse 为什么这么重要。

1. 为什么需要打洞

假设两个节点都在 NAT 后面:

Peer A -> NAT A -> Internet <- NAT B <- Peer B

A 和 B 都可以主动访问公网,但是其他机器不能直接访问它们的内网地址。

例如 A:

192.168.1.10:4001

A 主动建立一个 TCP Connection 后,NAT 可能产生:

192.168.1.10:4001 -> 1.2.3.4:52000

外面的节点真正看到的是:

1.2.3.4:52000

但是 B 知道这个地址,并不代表直接连接它就一定能够成功。

因为对于 NAT 来说,一个没有对应连接状态的外部 SYN 通常不会直接转发给内网机器。

所以需要 Hole Punching。

2. libp2p 怎么做直连

libp2p 的思路是先通过 Relay 让两个 Peer 联系上,然后再尝试建立真正的直连。

sequenceDiagram participant A as Peer A participant R as Relay participant B as Peer B A->>R: Relay Connection B->>R: Relay Connection A<<->>B: DCUtR Note over A,B: 交换 Observed Address Note over A,B: 同步并同时发起 TCP Connect A<<->>B: Direct TCP Connection

整体过程其实就是:

A -> Relay -> B -> 交换地址 -> 同步 -> 同时 Connect -> TCP Direct Connection

Relay 主要解决的是两个 Peer 一开始互相访问不到的问题。

等双方已经可以通过 Relay 通信以后,再通过 DCUtR 协调 Hole Punching。

如果打洞成功,后面的连接就可以直接变成:

A -> B

而不是一直:

A -> Relay -> B

3. 为什么双方要同时 Connect

TCP 打洞里面比较关键的一步,就是双方几乎同时主动连接对方。

假设 A 主动连接 B:

A -> NAT A -> NAT B -> B

A 发出 SYN 后,NAT A 会记录这次主动连接。

与此同时 B 也主动连接 A:

B -> NAT B -> NAT A -> A

NAT B 同样会记录自己的主动连接。

于是整个过程变成:

A 发 SYN -> NAT A 建立状态
B 发 SYN -> NAT B 建立状态
双方的流量都有机会通过对方 NAT

TCP 本身也支持这种情况,叫:

TCP Simultaneous Open

所以 DCUtR 很重要的一件事情,就是协调双方在比较接近的时间发起连接。

4. Socket Reuse 是什么

这里的 Socket Reuse 很容易被理解成“复用一条已经建立的 TCP Connection”。

其实重点不是这个。

这里更重要的是:

复用同一个 Local Port。

比如 libp2p 正在监听:

0.0.0.0:4001

正常使用:

net.Dial("tcp", remote)

操作系统一般会给这个主动连接选择一个临时 Source Port,例如:

192.168.1.10:52731 -> Remote

但 TCP Hole Punching 更希望:

Listen :4001 -> Dial from :4001

也就是监听端口和主动连接使用同一个 Local Port。

这也是 libp2p go-reuseport 这类实现存在的重要原因。

5. 为什么 Socket Reuse 对打洞很重要

假设 libp2p 监听:

:4001

但是主动连接 Relay 时使用了随机端口:

:52731

NAT 建立出来的映射可能是:

192.168.1.10:52731 -> 1.2.3.4:62000

远程 Peer 观察到的是:

1.2.3.4:62000

问题是,这个 NAT Mapping 是 52731 产生的,而 libp2p 真正监听的是 4001

如果后面 Hole Punching 又使用另外一个随机端口:

52731 -> 49120

NAT Mapping 很可能又发生变化。

前面得到的 Observed Address 就不一定还有参考价值。

而使用 Socket Reuse 后,可以尽量做到:

Listen :4001 -> Dial from :4001 -> NAT Mapping -> Observed Address

后面真正开始打洞的时候继续:

Observed Address -> Dial from :4001 -> 尝试复用 NAT Mapping -> Direct Connection

这就是 Socket Reuse 在 TCP Hole Punching 里面非常重要的原因。

它不是为了少创建一个 Socket,而是为了:

尽量让监听端口、主动连接端口和 NAT Mapping 保持一致。

6. 举一个完整的例子

假设 Peer A:

192.168.1.10:4001

经过 NAT 后:

192.168.1.10:4001 -> 1.1.1.1:50001

Peer B:

192.168.2.10:4001

经过 NAT 后:

192.168.2.10:4001 -> 2.2.2.2:60001

双方通过 Relay 交换地址以后得到:

A 知道 B = 2.2.2.2:60001
B 知道 A = 1.1.1.1:50001

DCUtR 再协调双方同时连接:

A:4001 -> 2.2.2.2:60001
B:4001 -> 1.1.1.1:50001

因为双方都在主动向外建立连接:

A Connect -> NAT A 建立连接状态
B Connect -> NAT B 建立连接状态

如果 NAT 行为允许,双方的 SYN 就可能穿过对方 NAT,最后建立 TCP Connection:

Peer A -> Direct TCP Connection -> Peer B

这就是 TCP Hole Punching 最核心的过程。

7. Socket Reuse 也不是万能的

这里还要注意一点。

Socket Reuse 只是让:

Local Port -> NAT Mapping

尽可能稳定。

但是公网端口最终还是由 NAT 决定。

有些 NAT 对不同 Remote Address 可能产生不同的 Mapping:

A:4001 -> Server X = Public:50001
A:4001 -> Server Y = Public:61023

虽然 A 本地一直是 4001,公网 Port 还是变了。

这种网络环境下 Hole Punching 就可能失败。

所以 libp2p 仍然需要 Relay 作为 fallback:

Hole Punching 成功 -> Direct Connection
Hole Punching 失败 -> Relay Connection

Socket Reuse 是提高 TCP 打洞成功率的重要条件,但不是保证所有 NAT 都可以穿透。

总结

libp2p TCP 直连的过程其实可以简单理解成:

Relay 建立通信 -> 交换 Observed Address -> DCUtR 同步 -> Socket Reuse -> 双方同时 Connect -> TCP Hole Punching -> Direct Connection

其中 Socket Reuse 最关键的地方不是“复用 Socket”,而是:

Listen Port -> Dial Source Port -> NAT Mapping

尽量保持在同一套端口关系里面。

如果每次主动 Dial 都随机更换 Source Port:

4001 -> 52731 -> 49120 -> 58321

那么 NAT Mapping 也可能一直变化,前面观察到的公网地址就很难拿来继续打洞。

所以对于 libp2p TCP Hole Punching 来说,我觉得 Socket Reuse 可以理解成一个比较核心的基础能力:

尽量使用同一个 Local Port 主动建立连接,让已经建立的 NAT Mapping 可以继续被 Hole Punching 利用。

理解这一点以后,再看 Relay、DCUtR 和 Observed Address,整个 libp2p TCP 直连流程就比较容易串起来了。

ref

libp2p Hole Punching:

https://docs.libp2p.io/concepts/hole-punching/

DCUtR Specification:

https://github.com/libp2p/specs/blob/master/relay/DCUtR.md

libp2p Hole Punching Specification:

https://github.com/libp2p/specs/blob/master/connections/hole-punching.md

go-reuseport:

https://github.com/libp2p/go-reuseport

发表评论: