欢迎来到 0xFFFF 论坛的帖子 与我交流

起初啊, 网络是一根长长的电话线, 比特在这头, 输出在那头。
后来啊, 网络是一条拥挤的总线, MAC 地址在这头, 广播风暴在那头。
再后来, 网络是一层虚伪的封装, IP 路由在这头, 以太网帧在那头。
而现在, 网络是一座沉重的旧坟, 理想的 IPv6 在里头, 永远的兼容性在外头。

本篇文章又名「只有 IPv6 在的无聊世界」,希望从一个历史发展的角度,探索整个计算机网络曲折的发展历史,从而明白现代互联网为什么是现在这样的:)

作者在学习计算机网络的时候一直都比较奇怪为什么有这么多看似不一样,实则在做相同功能的设计。IP 地址、MAC 地址、路由器交换机ARPDHCP 这些单独拎出来都能讲得通,但是一旦叠加起来就让人头昏脑胀的例子,直到读到 Tailscale 创始人的 这一篇博客 才豁然开朗。

同时感谢 JackyWang 的 翻译 Blog ,本文对原文进行了精简,梳理,补充。

网络之始

「网络」的本质就是两个相连的节点,把 信息 从一个地方传递到另一个地方。而在计算机网络出现之前我们是通过电话线来传输信息的,自然而然的,早期的网络便复用这些老旧的电话线基建,使用物理上的触点,实现电路交换(OSI 模型的第一层)。这是一条真实存在的传输线路,只需要把比特流从一端塞进去,过一段时间后他们就会从另一端自动的冒出来,而完全不需要「地址」这些东西。

甚至到后来电话公司对此做了优化,加入了时分复用(TDM)和虚拟化的 电路交换 (Virtual circuit multiplexing),可以以一个稍微慢一点的速度同时从几个用户手里,像原来一样接收比特流,通过多路复用(Multiplex)聚合后再发送出去,一起发送到实际的传输线路上,到了目的地之后再拆散成好几个路线送到用户手中,这样便可以占用少几条长途线路。这一套下来相对有些复杂,但是这是电信公司的工作,对于用户来说是透明的。用户的用法还是一样:一头发送比特流一头接收比特流。完全不需要引入「地址」。

在这一思路上继续演进,我们可以把一台计算机加装两三个网口,在正确配置之后可以把流量从一个接口转发到另一个,那么用这个办法就不需要在每两台计算机之间都拉上专线了,这也就是我们熟知的网关 gateway。我们后来熟悉的第三层的概念:IP 地址、子网和路由,也在这时被引入了。即使这样,还是没有 MAC 地址介质 访问控制地址)的概念:你的计算机之间都是用线缆一对一连接的,数据包一头进一头出,同一个通讯介质(那根线)上谁发谁收仍然没有歧义。IP 地址在这里的作用是,让网关知道收到一个数据包之后应该把它转发给哪台计算机。

这是其中一波搞互联网的人的想法,由于早期的信息交流并不通顺,同时又有另一波独立的人在思考怎么小范围的把网络连接起来(即局域网),我们先把目光转向他们

局域网异军突起

与此同时,局域网 (LAN) 作为一个上述办法的 独立的替代方案 被发明了。如果你想把几台放在一起的计算机、主机或者终端机,全部连接起来,像上述一样在一台机器上安装多个网络接口、其他机器以星形拓扑全部连接到这台机器还是比较昂贵且麻烦的。为了省金币,人们希望能建立一个“总线式”的网络(也被叫做“广播域”,这个名字的内涵在之后会变得重要),每台计算机只需要挂接到同一根通讯线缆(同轴电缆)上就可以互相通信。搞这套方案的人和发展互联网的那帮人完全不是同一拨人,所以他们没有用 IP 地址来做总线的寻址方案,而是各自为政发明了各种各样的地址。几年之后,以太网协议 (Ethernet) 被发明出来,因为便宜、简单且带宽巨大(高达 10Mbps 的总线),最终彻底统治了 LAN。为了解决二层地址配置的问题,他们的想法很简单:把地址弄长,长到高达 48 个比特位,以至于这个地址空间之大,能够给从古至今生产的每个设备分配一个独一无二的地址。他们真的这么做了!这就是后来我们熟悉的以太网 MAC 地址。

注:细缆以太网标准 10BASE2 在 80 年代普及,提供 10 Mbps 的带宽。而在 1990 年代初,互联网刚商业化的时候,一个骨干网的带宽可能才 45Mbps,可见在家里有一个 10Mbps 网络是怎样的一个天文数字。

当然,很快问题就出现了,问题来自企业网络和大学网络这种使用场景。在这种大规模组织里,需要接入网络的计算机之多,即使是那疾 风 迅 雷的 10 Mbps 局域网总线也不堪重负,成为了限制网络扩展的巨大瓶颈。为了解决这个问题势必引入多个总线,每个总线只连接一部分计算机,然后再把总线与总线之间连起来——此所谓“互联”网络(Internetwork)(或者是我们现在所熟知的「路由」)。你肯定会想:把小网络互联成大网络?这不得整点 IP 协议?啊,哈,哈,并不。IP 协议,当时仍然不叫这个名儿,还不是非常成熟,没人把它当回事儿。Netware-over-IPX (以及很多当时流行的许多各种局域网协议)才是当时的 靠谱大生意,所以就像其他所有靠谱大生意会做的一样,它们在当时已经广泛普及的以太网技术上加自己的东西。以太网设备已经有地址了(MAC 地址),它基本上就是搞各种局域网技术的人唯一能达成共识的东西,所以他们决定用以太网地址 (A.K.A. MAC 地址) 来实现路由机制。(当然,他们管这叫 桥接/网桥 (bridging) ,而不是 路由 (Routing)。)

不过使用以太网地址的问题在于,它们在出厂的时候已经被设置好了,所以不能在搭建网络的时候配置成有层次结构的样子来反映网络的结构。也就是说,这个所谓的“桥接路由表”并不像现代的 IP 路由表一样拥有良好的性质,匹配一个前缀就能描述到达一整个子网所有机器的路由。为了能有效地做桥接(路由),你需要记住哪个 MAC 地址在哪个总线下。但是人类总是痛恨手工配置东西,所以我们总是想有个什么机制能把这个活儿自动搞定了。这便引出了后来的 生成树协议(Spanning Tree Protocol)。它能够自动产生一些路径,还能防止回环。总而言之,这套玩意儿马马虎虎能工作,但是有些混乱,它会搞得网络里到处都是广播洪泛,路由出来的路径也不一定总是最优的,而且你基本上没办法调试它。(你肯定不能给桥接搞个 traceroute 什么的……因为 traceroute 依赖的所有 IP 网络的机制在以太网上都不存在——桥接器甚至没有自己的 MAC 地址!)

注:更重要的一个问题是,二层的可伸缩性实在太差,在今天这个云计算高密度布置数据中心的场景下就是一场噩梦。这恐怕是业界开始反思这个问题的第一推动力。

万恶之源

局域网技术搞得热火朝天,隔壁做互联网的人对这种多快好省的网络技术当然有所耳闻。互联网的目标于是便从“把单个的主机用点对点长途链路连起来”变成了“把每个局域网用点对点长途链路连起来”。总而言之,你想要一个“长途网桥”。

你可能会想:啊,多大点事,为什么不就直接弄个长途版本的网桥?听起来简单,根本做不了。我现在不具体讲为什么这么做不行,但是总的来说,问题在于 拥塞控制 (在当时是以太网中的 CSMA/CD,现在是 TCP 中的 TCP Sliding Window )。以太网桥接会假设你的所有链路都有差不多的速度,以及/或者,完全不拥堵,因为它根本没设计一个机制来协调这些链路来降低传输速度。它们就仅仅是在以最快的速度往总线里面轰入数据,然后祈祷它能被正确接收。但是当你的以太网工作在 10Mbps 下,而你的互联网点对点链路带宽只有 0.128Mbps,这就玩不下去了。另外,通过洪泛一切链路来寻找哪条路是正确的(桥接的工作原理),会对速度并不快的链路带宽的巨大浪费。更别说桥接还会有路由不最优的问题(生成树协议的原理)——它对于带宽充足、低延迟的局域网来说只是个烦人的小问题,在广域、长途的网络里被放大成了一个灾难。

幸运的是,这些搞互联网的人(如果那个时候已经改名叫互联网了的话)已经深耕这些问题很久了。如果我们用互联网的技术栈来把这些局域网互联在一起,效果就很好。

于是乎这些搞互联网的人设计了一种以太网帧,让以太网(以及 arcnet,以及各种当时还没被恐怖以太网鲨掉的协议)能够传输互联网数据包。

然而,这 就 是 问 题 的 开 始。

第一个问题:当你把一个互联网数据包发送到链路里的时候,这个包到底该让哪台机器收到(或者让哪台机器收到并转发)变得不那么明朗。如果该以太网段里有好几个互联网路由器,你肯定不能让所有的路由器都接收并尝试转发这个数据包,不然这个数据包可能会开始无限增殖形成洪泛,或者产生路由回环。你需要明确选择局域网总线里 哪个路由器 应该接收并转发这个数据包。我们不能用 IP 数据包头的“目的地址”来记录这件事,因为这是留给这个包的“最终地址”的。我们只能通过填写以太网帧头里的“目标 MAC 地址”来表明我们想用局域网里的哪个路由器。

于是乎当你想在计算机上配置本地的 IP 路由表的时候,你会想写下诸如

ip=10.1.1.1 via router at mac=11:22:33:44:55:66

的条目。你的目的地是个 IP 地址,但是你的第一跳路由器是用 MAC 地址指明的,这才是你实际想表达的。然而如果你真的配置过路由表的话,你会注意到没人这么写路由表:因为你的操作系统的 TCP/IP 栈只管 IP 地址而不管 MAC 地址呀,这都不是同一层的东西。所以你真正需要写的东西是

10.1.1.1 via router at 192.168.1.1

现在你的操作系统需要想办法找到 192.168.1.1 的以太网地址,搞清楚它的地址原来是 11:22:33:44:55:66,然后最终才能生成一个目标以太网地址是 11:22:33:44:55:66、目标 IP 地址是 10.1.1.1 的数据包。192.168.1.1 从未在最终生成的数据包里出现,它只是一个人为的抽象,用来找到中间那层物理上的转发是由谁在做。

为了能够完成这毫无意义的中间步骤,你需要引入 ARP 协议 (Address Resolution Protocol),一个简单的非 IP 协议,就为了负责把 IP 地址转换成以太网地址。为了做到这件事,它向以太网总线上的所有计算机发送广播,问它们是否持有某个 IP 地址。如果你使用以太网桥,那么这些桥必须一字不漏把这些广播包给转发到它们的所有接口上。在一个规模较大、流量较多、有很多网桥的以太网里,大量的广播流量会很快整得你头秃。对于 Wi-Fi 来说,这个问题尤为突出。时间久了,人们开始往以太网桥或者交换机里加各种各样的奇技淫巧来避免没必要地转发 ARP 包,来尝试缓解这一问题。有些设备(特别是 Wi-Fi 接入点)直接靠伪造 ARP 应答来尝试缓解问题。即便如此,这还是奇技淫巧,即便它有的时候是必要的。

积重难返

时过境迁。最终(实际上花了很长时间),人们基本上不再在以太网上用任何非 IP 的协议了。于是基本上所有的网络都变成了这样的结构:物理传输线(一层),有一些计算机挂在总线上(二层),然后好几个总线通过网桥连在一起(你猜咋的?还是二层),然后这些“互联”网桥通过 IP 路由器连接在一起。

一段时间以后,人们受够了充满古风的手工配置 IP 地址,希望它们能够自动配置,就像使用以太网时的体验一样。然而这个时候给 IP 引入以太网风格的地址已经太迟了,因为:

  • 网络设备在生产的时候被烧录的是以太网地址,不是 IP 地址;
  • IP 地址只有 32 位,拿去给制造商霍霍根本不够用;
  • 如果我们仅仅是把 IP 地址拿来当流水号发给每个设备,而不是根据子网结构来配置,我们就活回去了:这会成为以太网二号,但是我们已经有以太网了。

这就是 BootpDHCP 的来历。这些协议有些特殊,原因就和 APR 的特殊之处一样(尽管它们尽力装得不那么特殊,技术上还是在使用 IP 数据包)。它们肯定会比较特殊,因为它们想让一个 IP 节点在拿到 IP 地址之前就能发送 IP 数据包(这肯定在普通意义下是不行的),于是乎它们发送的 IP 数据包的包头里基本上都是无意义的信息(不过是由一个 RFC 标准约定好的无意义信息)。(DHCP 服务器在发包的时候甚至需要专门开个 Socket 来手工填写包头,内核网络栈甚至都搞不定这些,由此可见它们发送的“IP 数据包”有多特殊……)但是重新发明一个非 IP 协议来承载这些信息又显得不是很好,所以协议的制定者就如此别扭得假装这是一个 IP 包,然后觉得世界如此美好——美好到你考察 DHCP 的设计过程的时候,能直接感受到制定者体会到的溢出屏幕的美好的程度的美好。

……扯远了。重要的是,和真正的基于 IP 的服务不一样,Bootp 和 DHCP 需要有关于以太网地址的知识,因为毕竟它们的工作就是监听你的以太网地址,然后给你分配一个 IP 地址。它们基本上就像反过来工作的 ARP 协议,不过我们不能这么叫它,因为 Reverse ARP (RARP)还真另有其物。 实际上这个 RARP 工作得挺好,用更简单的设计完成了 Bootp 和 DHCP 想做的事情,但是现在我们暂时不扯这个。

我们提这个是想说,以太网和 IP 网络在这个过程中越来越纠缠在一起。在今天,它们基本上已经分不开了。我们很难想象一个没有 48 位 MAC 地址的网络接口,也很难想象一个没有 IP 地址的网络接口。你用 IP 地址写 IP 路由表,但是当你用 IP 地址指明路由器地址的时候,你心理清楚地知道你在扯淡——你只是在绕着弯子用一个 MAC 地址指代这个路由器。然后你还有 ARP 协议,它会被网桥转发但不完全会,还有 DHCP 协议,它发送的数据包是个 IP 数据包但不完全是(更像个以太网帧),如此等等。

更糟糕的是,桥接和路由这两个玩意儿一直并存,都随着局域网和互联网的发展而变得越发复杂。桥接仍然总体而言是个基于硬件的机制,由制定以太网标准的 IEEE 主导;路由仍然总体而言是个基于软件的机制,由制定互联网标准的 IETF (Not IEEE) 主导。这对欢喜冤家仍然天天假装对方不存在。 IPv4 数据包因为一开始复杂太难被硬件加速,市面上也缺乏能硬件处理 IPv4 数据包的设备;同时配置 DHCP 又是个巨大的噩梦, 于是网络管理员最后还就是成了桥接仙人。现在的大数据中心基本上都搞了 SDN,你也基本上可以在数据中心里完全不担心 IP 路由的事情,因为真的没人在里面搞路由,一切就是一个虚拟的巨型总线网络。

总而言之,这就是个矢山。

先忘掉我讲的这一大堆……

在讲上面的冗长历史故事的时候,有个事我忘说了:其实在某个时间点上,我们就已经完全抛弃了使用总线拓扑。以太网已经不再是一个总线网络了,它仅仅是在 假装 自己是。随着以太网速度不断提高,我们没法再用那个著名的 CSMA/CD 算法了,还是退回到了大力出奇迹的星形拓扑。

实际上,让这个故事更扯的是,连 Wi-Fi——这个骨子里就该是个总线网络的网络,这个所有人都在用同一个介质(以太 Ether)通过广播无线电信号来通讯的网络——我们基本上都在以“基站模式”使用 Wi-Fi,它的工作方式是,模拟一个星形 拓扑……如果你有两个 Wi-Fi 设备连接到了同一个接入点,它们不能直接和对方通信,即使这两个设备可以毫无问题地接收到对方发射的信号。它们需要把数据包发到接入点,但是这个数据包的目的 MAC 地址又是另一个设备的地址。这个数据包会被接入点再用无线电广播出去发给目标设备,绕了一大圈。

这里有个小问题。当一个节点 X 想要给互联网节点 Z 发送消息,途中要依次经过 Wi-Fi 接入点 A 和路由器 Y,那这个数据包该长什么样?画成图是这样:

X --[Wi-Fi]--> A --[Wi-Fi]--> Y --[Internet]--> Z

Z 是 IP 层面的目的地,所以显然这个包的 IP 目标地址应该设置为 Z 的地址。Y 是路由器,根据上文的分析,我们会把这个包的以太网目标地址设置为 Y 的 MAC 地址。但是在 Wi-Fi 的情况下, 因为一些原因,X 就是不能直接给 Y 发送数据包(其中一个原因是,它们不知道对方的 WPA2 密钥)。我们需要将数据包发送到 A。你可能会问,我们应该在哪里写明 A 的地址?

没问题!802.11 标准有个叫“三地址模式”的东西。第三个以太网 MAC 地址被添加到每个以太网帧里,我们借此可以同时指明数据包的真正的以太网目的地,和一个中转目的地。在此之上,他们还添加了表明“这个包是设备发送到接入点还是从接入点发给设备“的比特位。但是在有的情况下这两件事可能都成立——Wi-Fi 中继器就是这么工作的,一个接入点给另一个接入点发送数据包。

啊,说到 Wi-Fi 中继器!如果 A 是一个中继器,当它收到设备发来数据包,它需要把数据包发回它的上游接入点 B。然后我们的情景就变成了这样:

X --[Wi-Fi]--> A --[Wi-Fi 中继]--> B --[Wi-Fi]--> Y --[Internet]--> Z

从 X 到 A 的传输可以用之前提到的三地址模式,但是从 A 到 B 的传输是个问题:数据包的以太网源地址是 X,目标地址是 Y,但是实际上被无线传输的数据包是被从 A 发到了 B,完全不关 X 和 Y 的事。我们可以猜测这里又会有一个“四地址模式”,而且实际上它还真是这么工作的。(在 802.11s 网状网络里,还有“六地址模式”这种惊天地泣鬼神的玩意儿。从这里开始我已经停止思考了。)

终于来到了 IPv6

我提这一大堆事情的意思是,当 IETF 的专家们在考虑 IPv6 的设计的时候,他们目睹了这一乱象,同时可能也预测了之后可能发生的更多乱象(虽然我怀疑他们没预测到 SDN 和 Wi-Fi 中继模式这么离谱的东西),于是他们说:先等下,别继续瞎搞了!我们并不一定要把事情整这么复杂。如果从一开始这个世界是这样的,岂不是一件美事:

  • 别再用物理层面上的总线网络了!(现在已经都是星型连接了)
  • 别再用第二层的互联网络了!(这是第三层的地盘!)
  • 别在搞广播了!(第二层全是点对点的,在这种情况下,广播有什么意义?我们应该用 多播 这个概念来替代它)
  • 别再用 MAC 地址了!(在一个点对点网络里,链路的两头是哪两台主机在通讯这件事是不言自明的,而且你可以用 IP 地址来多播)
  • 别再搞 ARP 和 DHCP 了!(取消了 MAC 地址,就没必要在 IP 地址和 MAC 地址之间建立映射了)
  • 别再把 IP 包头搞得那么复杂了!(这样的话 IP 路由就可以用硬件实现了)
  • 除了在网络核心部分,别搞手工配置 IP 地址了!(以及,通过提供足量的 IP 地址,我们可以一层一层往下分配子网,实现路由)

在这样的一个乌托邦下,所有的设备(包括家里的 Wi-Fi 扩展器、交换机)都升级为 IPv6 路由器。网络不再区分“局域网内部”和“互联网”,每一跳都是路由。不再需要 ARP 也不会有 ARP 风暴,甚至路由器天生具有防环机制(TTL),数据包不会无限循环,网络会极其稳定。网络调试将变得无比透明,因为如果交换机变成了路由器,每一跳都会显示在 traceroute 里,哪里断了一目了然。并且现在过长的 IPv6 数据包也有了合理性,不至于一个数据包里既有 IPv6 地址(很大),又有以太网 MAC 地址。这导致头部开销很大。因为设计者原本打算扔掉以太网头部的。如果能扔掉 MAC 地址,IPv6 增加的那点开销是完全可以接受的。

这个愿景相当美妙,只有一个问题:它从未被落实过。

而如今 IPv6 的 SLAAC 在我看来是最接近这个愿想的,但此时的 MAC 地址还没有被消灭 (Link Local Address)

回到现实

有人曾说过一句很精辟的话: “抽象层次总是被添加,而不是被移除。”

由于 Postel’s Law,即使 IPv6 的普及率已经到了 99%,我们也很难摆脱 IPv4。而如果我们没有摆脱 IPv4,我们就不可能摆脱以太网地址或者 Wi-Fi 地址。而如果我们想保留 IEEE 802.3 和 802.11 的帧格式,我们就永远都没法像之前所说的那样在包里省下那几个字节。我们永远需要那个 NDP 协议,这就是个更复杂的 ARP。即使我们不再需要总线网络了,我们还是会需要去模拟一个出来,因为 ARP 需要依赖广播来工作。我们需要继续在家里维护一个本地的 DHCP 服务器来让家里的古董 IPv4 智能灯泡继续工作。我们需要继续使用 NAT 来让这些古董 IPv4 灯泡能够访问互联网。

这还不是最糟糕的部分——最糟糕的部分是,他们忘了解决“移动 IP 地址“所带来的问题。据我所知,当时他们的想法是:

“我们应该先争取把 IPv6 部署到能用,应该几年就能搞定,然后在 IPv4 和 MAC 地址都被赶尽杀绝之后,再开始处理这个问题,届时这个问题应该就要好解决一点了。而且到那时应该还没什么人拥有移动 IP 地址。讲道理,谁会传文件传到一半扯掉网线扛着计算机跑到另一个地方再插上?有病么这不是?”

当然,二三十年后,我们知道了这个“扛着计算机到处跑”的“无关紧要的”几种场景:你的手机,它会不停连接到不同的 Wi-Fi 接入点。我们天天都在做这样的事。在用 LTE 流量的时候,甚至大部分情况下我们没有遇到问题;在用 Wi-Fi 的时候,由于二层桥接的机制。互联网的路由机制不能正确处理移动设备的漫游,处理不了一点。如果你在一个 IP 网络中漫游,你的 IP 地址会变,然后你所有已建立的连接都会断。到这个地方,我们会忍不住问:为什么事情会变成这样?移动 IP 为什么会这样出故障?

注:起夜级 Wi-Fi 网络实际上是把整个局域网全桥接起来来哄你,这样以来那个巨大的中央 DHCP 服务器就能总是给你分配同样的 IP 地址,不论你是从哪个接入点接入的,然后追着追着把你的数据包塞给你,整个过程最多在网桥重新配置的时候会卡那么几秒。那些最新最热的“全屋 Mesh Wi-Fi“产品其实背后也在干相同的事。但是如果你出门切换到另一个 Wi-Fi 环境下,比如说,你连上了店里的公共 Wi-Fi,啊,那就很不幸了。每个不同的网络都会给你分配不一样的 IP 地址,然后每次你的 IP 地址变化,你所有的连接就会断一次。

LTE 下了血本来解决这个问题。即使你跑了很远,无数次从一个基站切换到另一个,你还是在用同一个 IP 地址(对于移动电信网络,通常给你的是个 IPv6 地址)。怎么做到的?这个嘛……电信公司通常就直接起个隧道把你送回一个中心地点,然后把一切都桥接起来(当然,这里面有巨大多防火墙),形成一个超级巨大的虚拟二层局域网。你建立的连接不会被打断,即便……这个灵车解决方案引入了成吨的复杂性和高到离谱的网络延迟,他们想改善都改善不了。

答案实际上出乎意料的简单。造成这个问题的设计缺陷就是著名的“四元组”(源 IP,源端口,目的地 IP,目的地端口)。我们用这个四元组来区分不同的 TCP 或者 UDP 会话,把数据包正确地分发到处理相应会话的套接字。但是这个四元组包含的信息实际上横跨了两层:互联网络层(三层)和传输层(四层)。如果我们 只用四层的信息 来区分会话,那移动 IP 的问题就能被完美解决。

我们来考虑一个简单的例子。主机 X 通过端口 1111 连接到了服务器 Y 的 80 端口,所有它发送的 TCP 包里包含的四元组是 (X,1111,Y,80)。回复的数据包的四元组应该是(Y,80,X,1111),内核网络栈据此原路返回将回复分发给相应的套接字。当 X 发送更多标记为(X,1111,Y,80)的包,Y 能够把它们都分发到同一个套接字,以此以往。

现在,如果 X 换了 IP 地址,我们给它起个新名字,比如说 Q。现在它会开始发送(Q,1111,Y,80)的数据包。Y 对此完全蒙鼓,拿到这个数据包不知道如何解释,只能丢弃数据包。与此同时,如果 Y 发送标记成(Y,1111,X,80)的数据包,它们甚至无法到达目的地,因为要接收它们的主机 X 已经不存在了。

想象如果我们不用 IP 来标记不同的套接字。我们用一种像是唯一哈希的 ID 进行标识。现在 X 向 Y 发包,使用标记(id,80)。最终生成的数据包仍然包含 IP 二元组(X,Y),但是仅限于第三层——这保证这个包能正确经过互联网路由到目的地。但是内核网络栈不用三层信息来区分这个包该被分发给哪个套接字,只用 ID 来区分。这里的目标端口 80 只是用来在建立新连接的时候,指明客户端希望连接到什么服务,连接建立之后这个信息可以被抛弃。

为了处理 Y 回复 X 的数据包,Y 的内核把最近一次收到的这个 ID 的包对应的 IP 地址缓存下来,然后对着这个 IP 地址发送回复。

现在想象 X 的地址变成了 Q。它仍然向 Y 发送标记为(id,80)的数据包,但是这些包来自新 IP 地址 Q。Y 收到这些包并用 ID 将这些包导向了正确的套接字,注意到了这些包来自一个新的 IP 地址 Q,于是更新前述的缓存,之后回复 X 的时候就把拥有该 UUID 的包发到 Q。一切都很完美!(除了需要设计一点措施来防止有第三者劫持会话)。

原作者注:有些人问我“防止第三者劫持会话的措施”是什么样的。有很多办法,最简单的就是在连接建立的时候搞个类似于 TCP 三次握手的机制。如果 Y 仅仅是信任第一个从新地址 Q 发来的包就是原本客户端发送的,那么随便一个攻击者都可以在随便一个地方发送一个相同 UUID 的包把连接给带歪(虽然猜对一个 256 位的 UUID 是比较困难的)。但是如果 Y 发送一个 cookie 之类的东西来 challenge-response Q,那么这至少能把攻击的范围限制到中间人攻击(至少原味 TCP 也没法防中间人攻击……)。如果你使用一个带加密的传输层协议,比如说 QUIC,那么这个握手过程也可以被会话密钥所加密。

除了 QUIC,还有好几个协议可以做这个事,比如说 MinimaLT。我最开始没有提及 MinimaLT,因为我在和 IETF 的人吹水的时候没提到它,但是我并不是说 QUIC 是解决漫游问题唯一的 TCP 替代品。事实上,MinimaLT 是我听说过的第一个能优雅地解决这个问题的方案。之后被采用的其他解决方案,包括 QUIC,很可能参考了 MinimaLT 的设计。

现在只有一个小问题:UDPTCP 不是像我们说的那样工作的,而且现在去把它们扬了也已经太晚了。去修改 UDP 和 TCP 的下场可能和 IPv4 迁移到 IPv6 的现状一样惨:当年 90 年代的时候觉得很简单,但是很多年之后,迁移率还是不足一半(而且根据经验,完成前一半是最简单的,后一半那才叫真的困难)。

好消息是,我们可以搞点 hack 来让这个迁移少点伤筋动骨。如果我们抛弃 TCP (它已经够老啦!),然后用 QUIC over UDP,那么我们可以在处理会话标签的时候直接忽略 UDP 四元组,接收的时候看到这是个 QUIC over UDP 包就直接解包找到 UUID 标签,能把这个包匹配到正确的套接字。

还有更多好消息,QUIC 已经以 HTTP3 标准的形式普及了。根据 Cloudflare Radar 的统计,2025 年,世界范围内已经有 21% 的 HTTP 流量基于 HTTP3 (虽然 2024 年也是 21%),至少理论上,可以像我们前面所说的那样工作。实际上,如果你想要实现无状态的数据包加密和认证,你总会需要一个会话唯一标识符(在这里也同时是会话加密密钥),然后 QUIC 就是这么做的。所以很可能 QUIC 可以在不怎么折腾的情况下直接支持透明漫游。好时代,来临力!(梅开二度)

等到那个时候,我们只需要把剩下的 UDP 和 TCP 部署都扬了,于是我们对二层桥接最后的依赖也消失了。至此,我们终于可以真的把什么广播、MAC 地址、SDN 和 DHCP 这些妖魔鬼怪全部丢进历史的垃圾堆。

然后互联网就能 再次优雅