Note

本文由 99% 人类编写(和复制),由语音输入辅助

引言

Tailscale 是一款非常有名并且非常好用的组网 / 穿透软件,在 OpenClaw 的 onboarding command 上也推荐配置,可见其影响力 。而我个人对其创始人也是十分有好感,上一篇博客 「为什么有了 IP 地址,还需要 MAC 地址?一文看懂网络协议的“屎山”堆叠史」就是从其创始人的一篇博客演化而来的。今天来简单探究一下其组网和打洞的原理。

背景

最早的网络直接通过物理链接,到 TCP IP 诞生之后便确立了地址的概念,再到从 Host 文件发展到 DNS 和整个商业化的互联网浏览器等。再到最后,由于接入设备的暴增,导致我们从 IPv4 + NAT 逐渐开始演化到 IPv6。这一部分内容可以通过 为什么有了 IP 地址,还需要 MAC 地址?一文看懂网络协议的“屎山”堆叠史 来简单浏览。

而 Tailscale 做的工作,是在公网之上重建 「端到端直连」 的虚拟网络,弥补 NAT 时代被破坏的端到端原则。(重现旧时代网络荣光,我辈义不容辞!)。并且不用关心使用的是 IPv4 还是 IPv6,公网可达性,还有防火墙的情况。

本文出于技术兴趣,主要聚焦于 NAT 打洞这一层面。对其他内容只做简要介绍,并且假定读者有一定计算机网络认知。

Tailscale 简介

如果之前比较深度用过 Tailscale 可以跳过本节。

Tailscale 本质上是让两台联网的计算机相连,那么最经典的案例就是 VPN。

image

但是有一个问题是 VPN 是「中心辐射」的,可拓展性较差,时延也比较高,最坏情况是这样

image

而 Tailscale 做的是,任意两个网络之间的组网,类似这样

image
甚至是这样

image

这样做的好处是可拓展性极强,并且相对来说性能会很优秀。

技术上,我们都知道网络的七层模型(Network Layer),既然要建设虚拟网络,那么不可避免的需要修改 L3 层,添加一个虚拟网卡。负责分配虚拟 IP、封装 IP 报文、维护路由表;对操作系统表现为一张虚拟网卡(utun/wg0),上层 TCP/UDP 无感知。

在 L4 传输层主要是用 WireGuard 传输数据,走的是 UDP。

控制面(OIDC ,WireGuard 密钥交换,ACL 等)属于非常业务的东西,咱就忽略了。

网络穿透(网络打洞)

理想很美好,但是在很多情况下,我们都是藏在 NAT 加防火墙背后的。
image

这时候我们就需要 NAT 穿透,并且还需要绕过防火墙。所以其实打洞包含了两大部分,一个是 NAT 穿透,一个是防火墙穿透。

image

Aside: 穿透其实不是什么很新的技术,比如说有十几年历史的 WebRTC,就可以做到在浏览器之间发送点对点的音频、视频和数据。VoIP 电话和一些游戏也使用类似的技术。甚至百度网盘、爱奇艺等也有用类似技术来作恶:)

防火墙穿透

我们当前假设两台电脑都不在 NAT 后。

防火墙有很多种,比如 Windows Defender 防火墙、Linux 的 iptables/nftables、BSD 的 pf(macOS 也用)以及 AWS 的安全组。

最常见的配置是允许所有“出站”连接,并阻止所有“入站”连接。但是我们都知道,网络连接是一个双向的过程,防火墙是怎么知道哪些是出站,哪些是入站呢?这是因为防火墙其实大多数都是 有状态 stateful 的,会记住它们过去看到过的数据包,并可以利用这些知识来决定如何处理新出现的数据包。

对于 UDP,规则非常简单:防火墙允许入站 UDP 包,如果它之前看到匹配的出站包。例如,如果我们电脑防火墙看到 UDP 包从 2.2.2.2:1234 离开到 5.5.5.5:5678 ,它会记录从 5.5.5.5:5678 到 2.2.2.2:1234 的入站包也是允许的。显然如果我们打算与 5.5.5.5:5678 通信,我们也应该让他们回复。

而 TCP 则不同,防火墙还会额外追踪一个连接的状态机(序列号、窗口校验、握手阶段等信息)。因此对于 TCP 来说并不适合用于打洞,目前所有打洞都是基于 UDP 完成的。如果需要有状态的链接,可以使用 QUIC 这一传输协议,属于是用 UDP 重新发明了 TCP :)

回到 UDP,我们只需要把自己的网络信息发送给 Tailscale 的中继服务器,另一端也通过这个中继服务器交换即可得到对方的 IP:Port,这样虽然首几包一定会被丢失,但是这样就能做到稳定的防火墙打洞。

下面放一个 Tailscale 博客里提供的例子。

image

Laptop 的第一个数据包,从 2.2.2.2:1234 到 7.7.7.7:5678 ,通过 Windows Defender 防火墙发送到互联网。另一端的公司防火墙阻止了该数据包,因为它没有 7.7.7.7:5678 与 2.2.2.2:1234 通信的记录。然而,Windows Defender 现在记住了它应该期望并允许从 7.7.7.7:5678 到 2.2.2.2:1234 的响应。

image

接下来,Server 的第一个数据包从 7.7.7.7:5678 到 2.2.2.2:1234 经过公司防火墙并穿越互联网。当它到达 Laptop 时,Windows Defender 认为“啊,这是对我看到的那个出站请求的响应”,并让数据包通过!此外,公司防火墙现在记住它应该期望来自 2.2.2.2:1234 到 7.7.7.7:5678 的响应,并且那些数据包也是允许的。

image

受到从 Server 收到数据包的鼓励,Laptop 发送另一个数据包回去。它通过 Windows Defender 防火墙,通过公司防火墙(因为这是对先前发送数据包的“响应”),并到达工作站。

拿下防火墙,轻而易举啊!

NAT 穿透

那根据 NAT 的原理,我们知道:我们往公网发的链接,其实被转了一道,是通过一个中间的,公网可达的服务器代我们转发的。

于是乎,我们只需要遵循某个协议,往公网发包,获得自己在公网上的 NAT 映射地址和端口,并且对方也是需要用同样的方式,获得自己在公网上的 NAT 映射地址和端口,那双方就能通过这两个「洞」来进行通信。这样就退化到了防火墙的例子。

这个协议就是 STUN,stun 还有很多细节的内容,但细节在此并不重要,不赘述。

Aside: 注意,我们用于 NAT 穿透的 socket 和传输数据使用的 socket 必须是同一个 socket,相当于必须要做 Socket Programming。

以上是最美好的情况,但是还有一些 NAT 设备非常特殊,会为不同的通信目标创建完全不同的 NAT 映射。在这种 NAT 设备上,如果我们使用同一个 socket 向 5.5.5.5:1234 和 7.7.7.7:2345 发送数据,最终会在 2.2.2.2 上得到两个不同的端口,每个目标对应一个端口。如果你用错误的端口回复,就无法穿透。

image
这种 NAT 的不同可以被分为 4 级

类型映射行为 (对不同目标)过滤行为 (谁能回包)可否打洞
Full Cone固定映射任何人都能回✅ 最易
Restricted Cone固定映射只有 ” 我发过包的那个 IP” 能回✅ 可以
Port-Restricted Cone固定映射只有 ” 我发过包的那个 IP: 端口” 能回✅ 可以
Symmetric每个目标换新映射只有对应目标能回❌ 通常不行

前三种 NAT 类型基本都是遵循着同一种思想,因此都可以打洞,只有第四种不行。

DERP:最终的兜底

除了因为 NAT 类型而无法打洞,还有一些是完全阻止所有 UDP 流量的情况也显然无法打洞。Tailscale 的最终兜底是使用 DERP (Detoured Encrypted Routing Protocol) 来负责中继流量,此时就回退到传统的 VPN 了。