又称,“只有 IPv6 知道的世界”,或者,“历史包袱太多啦”

译者序: 本文译自 Avery Pennarun(后称原作者) 于2017年8月10日所写的一篇雄文

译者在学习计算机网络的时候一直遇到这样那样的疑惑,特别是对于 IP 地址、MAC 地址,路由器、交换机这样的双轨制,以及 ARP、NDP 和交换机监听 ARP 等等看起来非常奇技淫巧的 hack,但是因为身边的人对此总是表现得天经地义本该如此,就总安慰自己“这背后自有它的道理,搞成这样是为了通用型和可扩展性,之后能通过排列组合产生灵活的使用方式,只是我目前不知道!”。

但是时间一长,这个“我大互联网自有网情在此 ”的解释逐渐使得无力,这样的机制是历史遗留问题而非精巧设计的蛛丝马迹逐渐显露出来。原作者找来 IETF 的当事人,在探索汇总这些蛛丝马迹之后,讲述了其中的原委。

原作者 Avery Pennarun 是 Tailscale 的合伙人,想必他在设计 Tailscale 的时候,融入了很多本文对互联网架构的看法。

好久没更新博客了,空下来之后却迟迟动不了笔。和原文类似的想法想写成博客很久了,所以不再重新发明轮子,拿来翻译一下,权当是个写作复健训练

去年11月我第一次去了 IETF 会议。这是一个挺有意思的会:似乎有三分之一的内容是协议维护的冗杂之事,有三分之一是扩展现有的东西,剩下三分之一则是星辰大海的 幻想时间。我去参加这个会主要是为了看看大家对新出的 TCP BBR 算法 都有什么看法。(结论:大多数人对此的回应比较积极,但也有人江信江疑,主要是它听起来太美好了,听起来像电信诈骗。)

总之,这个会议里有很多演讲都是关于 IPv6 的,我们的 明日之星 ,将来 将要 能够替代现在劳苦功高、撑起了整个互联网的 IPv4。(有人声称这个替换过程还在进行,有人则声称这个替换已然发生了)。除了有这些关于 IPv6 的演讲,有很多人都觉得 IPv6 简直妙极了,他们非常确信有朝一日 IPv6 一定能一统天下,而 IPv4 只是一大堆奇技淫巧,需要赶紧清理掉才能 让互联网再次优雅

译者注:救世啊!

我觉得这是个搞清楚到底现在是个什么情况的好机会。为什么 IPv6 比起 IPv4 复杂了这么多?为什么不能仅仅把 IPv4 地址搞长点儿?啊,那可差远了。IPv6 完全不能理解为一个加长版的 IPv4。

译者注:曾看到一个犀利的评论:“IPv4 和 IPv6 之间的相似之处只有‘I’、‘P’和‘v’。”

下面让我来讲讲我的发现。

我用了总线拓扑,这辈子有了!

很久很久以前,电话网络大多使用物理实现的电路交换。这意味着打个电话背后真的在接通一些触点,在你和你的呼叫对象之间形成一个单独的电路(“OSI 模型第一层“)。你可以找电话公司拉条专线,它真的是一条真实存在的很长的传输线,当你把比特流塞进其中一端,在等待固定的一段时间之后,它们就会从另一端冒出来。你完全不需要 地址 这种东西,毕竟这就是个一头儿进一头儿出的固定管道,无论你怎么发送信息,接收者都只能是线路那头的计算机,没有任何歧义。

后来电话公司对这个图景做了点优化,加入了时分复用和虚拟化的电路交换。电信公司以一个稍微慢一点的速度,同时从几个用户手里像原来一样接收比特流,然后用多路复用器把多个用户的流量打包在一起发到实际的传输线上,到了目的地再散开成好多个线路送到用户手里,这样可以少占用几条长途线路。把这一套折腾对有些复杂,但是这是电信公司的工作,对于用户来说,用法还是一样:一头发送比特流一头接收比特流。完全不需要引入地址。

互联网(当时还不叫互联网)最初是建立在这些只管一头发送比特流一头接收比特流的电话线路基础上的。如果一台计算机有两三个网络接口,那在正确配置之后它可以做到把流量从一个接口转发到另一个。用这个办法你就不必每两台计算机之间都拉专线了(译者注:只把一台计算机接入专线,让它把流量转发到你的其他计算机上。对于子网中的其他计算机,这台计算机有个很生动的名字,叫 网关(Gateway)。)我们后来熟悉的第三层的概念:IP 地址、子网和路由,此时就被引入了。即使这样,还是没有 MAC 地址(介质 访问控制地址)的份:你的计算机之间都是用线缆一对一连接的,数据包一头进一头出,同一个通讯介质(那根线)上谁发谁收仍然没有歧义。IP 地址在这里的作用是,让网关知道收到一个数据包之后应该把它转发给哪台计算机。

与此同时,局域网作为一个上述办法的 替代方案 被发明了。如果你想把这几台放在一起的计算机、主机或者终端机,全部连接起来,像上述一样在一台机器上安装多个网络接口、其他机器以星形拓扑全部连接到这台机器还是比较麻烦的。为了省金币,人们希望能建立一个“总线式”的网络(也被叫做“广播域”,这个名字的内涵在之后会变得重要),每台计算机只需要挂接到同一根通讯线缆上。搞这套方案的人和发展互联网的那帮人完全不是同一拨人,所以他们没有用 IP 地址来做总线的寻址方案,而是各自为政发明了各种各样的地址。

有一个早期的总线式局域网叫 arcnet,对我来说非常亲切(我在1990年代写了我的第一个 arcnet 驱动和一首关于 arcnet 的诗,那个时候 arcnet 已经被弃用很久了)(译者注:原作者似乎热爱把文档写成诗,同时喜欢玩点一语双关把所有的“协议(protocol)”一词替换成“诗(poem)“,以后不再赘述)。Arcnet 的二层地址设计得很简单:只有8个比特位,直接在网卡上通过搭跳线或者拨 DIP 开关来设置地址。作为网络管理员,你需要手工配置每个网卡的地址并保证地址不冲突,否则整个网络会立即变得奥妙重重。这有点恶心,不过因为 arcnet 网络规模通常很小,所以这仅仅是 有点 恶心。

几年之后,以太网被发明了。为了解决二层地址配置的问题,他们的想法很简单:把地址弄长,长到高达48个比特位,以至于这个地址空间之大,能够给从古至今生产的每个设备分配一个独一无二的地址。他们真的这么做了!这就是后来我们熟悉的以太网 MAC 地址。

那些年各种局域网技术换了好几轮,比如说 IPX (Internetwork Packet Exchange,虽然它和互联网(Internet)一毛钱关系都没有)和 Netware,这是我最喜欢的技术栈之一。它们在所有的服务器和客户端都在同一条总线上的小网络里工作得非常完美,你永远不用手工配置地址,永远不用。非常に安定で、非常に綺麗い。好时代,来临力!

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

不过使用以太网地址的问题在于,它们在出厂的时候已经被设置好了,所以不能在搭建网络的时候配置成有层次结构的样子来反映网络的结构。

译者注:虽然理论上来说不是那么自然,但是很多有路由机制的网络协议的地址会有意引入有层次结构的格式,来将网络本身的结构信息预先打表打进地址里。虽然理论上因为计算机网络不一定非要是一个无环图,导致这样的层次结构不一定良定义,但是这种“前缀路由”算是一个和“在随便一个无向图里路由”相比,牺牲不那么大的近似。这个近似在工程上被当成了理所当然的事,给当初自学这部分的译者造成了相当的困惑。

也就是说,这个所谓的“桥接路由表”并不像现代的 IP 路由表一样拥有良好的性质,匹配一个前缀就能描述到达一整个子网所有机器的路由。为了能有效地做桥接,你需要记住哪个 MAC 地址在哪个总线下。但是人类总是痛恨手工配置东西,所以我们总是想有个什么机制能把这个活儿自动搞定了。如果你有一个很复杂的“互联网络”,里面有许多局域网桥,那这个机制就会有些复杂。据我所知,这引出了后来的生成树之诗。对,在计算机网络的领域里,诗歌很重要!

总而言之,这套玩意儿马马虎虎能工作,但是有些混乱,它会搞得网络里到处都是广播洪泛,路由出来的路径也不一定总是最优的,而且你基本上没办法调试它。(你肯定不能给桥接搞个 traceroute 什么的……因为 traceroute 依赖的所有 IP 网络的机制在以太网上都不存在——桥接器甚至没有自己的 MAC 地址!)

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

另一方面,所有的桥接设备的逻辑都是硬件实现的。这整个技术栈基本上都是硬件人搞的,网络里的这些总线和桥接器对于使用网络的软件来说基本上是透明的,所以也无从对特定情况做出软件层面的优化。不过,用专用硬件实现桥接逻辑,性能能做到很高,以太网有多少带宽它就能跑满多大的带宽。现在我们看来这并不显得有多厉害,但是在当时这简直NB坏了。以太网当时提供了 10 Mbps 带宽,是因为可能在你往总线里面接入了一堆机器之后可能会把它占满,而不是任何单一一台计算机能把这个带宽吃满。这纯粹是天方夜谭。

译者注:细缆以太网标准 10BASE2 在80年代普及,提供 10 Mbps 的带宽。而在1990年代初,互联网刚商业化的时候,一个骨干网的带宽可能才45Mbps,可见 10Mbps 是怎样的一个天文数字。这么看来,让所有计算机共享这么一条总线,猫叫鸡犬相闻,在当时也不是那么不可理喻的事。

在那个互联网带宽窄得可笑的年代,在自己的local site内折腾一个 10Mbps 局域网是多么安心而满足的事!

总而言之,桥接就是一团糟,而且还没法调试,但是它就是快

基于总线的互联网

局域网技术搞得热火朝天,隔壁做互联网的人对这种多快好省的网络技术当然有所耳闻。我印象中在这个时间点 ARPANET 终于被改名叫“互联网”,但是我不是很确定。啊,算了,就当它是吧,毕竟为了能把这个故事讲好听,我得显得很自信

曾几何时,互联网的目标从“把单个的主机用点对点长途链路连起来”变成了“把每个局域网用点对点长途链路连起来”。总而言之,你想要一个“长途网桥”。

你可能会想:啊,多大点事,为什么不就直接弄个长途版本的网桥?听起来简单,根本做不了。我现在不具体讲为什么这么做不行,但是总的来说,问题在于拥塞控制。以太网桥接的黑暗♂深邃秘密在于它假设你的所有链路都有差不多的速度,以及/或者,完全不拥堵,因为它根本没设计一个机制来协调这些链路来降低传输速度。它们就仅仅是在以最快的速度往总线里面轰入数据,然后祈祷它能被正确接收。但是当你的以太网工作在 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 栈抱残守缺,你需要写的东西是

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 地址拿来当流水号发给每个设备,而不是根据子网结构来配置,我们就活回去了:这会成为以太网二号,但是我们已经有以太网了。

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

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

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

更糟糕的是,桥接和路由这两个玩意儿一直并存,都随着局域网和互联网的发展而变得越发复杂。桥接仍然总体而言是个基于硬件的机制,由制定以太网标准的 IEEE 主导;路由仍然总体而言是个基于软件的机制,由制定互联网标准的 IETF 主导。这对欢喜冤家仍然天天假装对方不存在。网络管理员基本上是根据他们想要多少性能和他们有多痛恨配置 DHCP 服务器(他们真的很痛恨配置 DHCP 服务器)来决定使用路由还是桥接,所以他们基本上能用桥接就用桥接,除非万不得已,不上路由。

事实上,桥接技术现在攒了如此多的奇技淫巧,以至于人们开始决定把桥接转发决策给抽出来放到更高的层面,来让这样的配置能被统一管理(诶,怎么分发这些配置呢?那当然是走 I P)。(译者注:讲个笑话,层次模型)。这可是(某个年代的)最新最热,软件定义网络(SDN)。这玩意儿,比起放任你的交换机和网桥自由发挥,相当好用;但是从根本上看,这样搞有点蠢:你想要软件定义网络?你想要的是不是:IP。这玩意儿从头到尾就是个实现软件定义网络的协议。直接给咱干回来了。但问题是,处理 IPv4 数据包因为一开始复杂太难被硬件加速,所以总之市面上缺乏能硬件处理 IPv4 数据包的设备;同时配置 DHCP 又是个巨大的噩梦, 于是网络管理员最后还就是成了桥接仙人。现在(译者注:2017年)的大数据中心基本上都搞了 SDN,你也基本上可以在数据中心里完全不担心 IP 路由的事情,因为真的没人在里面搞路由,一切就是一个虚拟的巨型总线网络。

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

好了,现在先忘掉我讲的这一大堆……

故事讲得不错,对吧?现在假设上面的一切曲折故事都从未发生,我们回到了1990年代早期。虽然在这个时候,之前讲的这些故事都已经发生了,但是我们假设 IETF 的人们都在假装这些事情从未发生,然后我们可以避免再后来发生的这些灾难。我们来想点好的!

在讲上面的冗长历史故事的时候,有个事我忘说了:在某个时间点上,我们其实完全抛弃了使用总线拓扑。以太网已经不再是一个总线网络了,它仅仅是在 假装 自己是。随着以太网速度不断提高,我们没法再用那个著名的 CSMA/CD 算法了,还是退回到了大力出奇迹的星形拓扑。我们从交换机引出大腿粗的一捆电缆,里面每根网线将一个单独的计算机连接到一个中心点。立面背后、天花板上面、地板下面塞满了这么一捆一捆昂贵的以太网线缆束,因为在物理层,我们已经摸到总线网络的吞吐量的天花板了。仔细想想,这个情景真的非常滑稽。

……如果你是觉得悲剧很滑稽的那种人的话。

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

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

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 路由器。还有Wi-Fi 接入点、以太网交换机和 SDN,都是路由器。APR 风暴将不复存在;什么“IGMP 嗅探桥”将不复存在;桥接回环将不复存在。所有路由存在的问题都可以用 traceroute 来调试。更重要的是,我们可以从每个以太网帧里省下来 12 字节(源地址、目标地址),或者从每个 Wi-Fi 帧里省下来 18 字节(源地址、目标地址、接入点地址)。诚然,IPv6 往数据包里额外加了 24 字节的地址信息(和 IPv4 相比),但是在省下这 12 字节之后,额外的开销就只有 12 字节——和使用两个 64 位 IP 地址但是保留以太网帧头的方案相比,已经很有竞争力了。这个“完全抛弃以太网地址”的思路,可以解释为什么 IPv6 地址空间被设计得那——么大。

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

译者注:原作者所说的愿景可能是这样的:每个设备在生产的时候都被预先赋予了一个64位的主机 ID,然后在接入子网的时候由该子网的路由器下发一个64位的网络 ID,拼成一个完整的128位的 IPv6 地址。用这种办法,NDP 协议里的“邻居发现”部分,也就是事实上复刻了 ARP 协议建立 MAC 到 IP 地址映射的那个部分,就可以从根本上被去掉了。这么想也能解释为什么 IPv6 标准里只存在/64子网(注意,这个子网需要和 前缀 的概念区分开。IPv6 前缀只是许多子网的集合,是为了方便写路由表而使用的便捷记法)。

熟悉 IPv6 的读者可能会说,这不就是 SLAAC 么?实际上,这个做法和 SLAAC 有本质上的不同,甚至可以说 SLAAC 才是那个向现状妥协的办法:MAC 地址仍然没有被消灭,还在 IP 地址的地盘上有了自己的代理人(Link-local 地址)。

如前所述,为了能够避免手工配置地址,以太网采取了生产网卡时预先烧录地址的办法。这样的办法是相当高效的,除了在实现跨多个网段间的路由的时候(或者应该叫 桥接,唉,本来是同一件事。讨厌的术语差异)会有困难,大家搞2层互联的时候最头疼的问题就是 MAC 地址不能体现出地址和子网之间的从属关系。IPv6 在设计的时候借鉴并发扬了这一思路,把 128 位的超长地址砍成 64 位的网络 ID 和主机 ID 两部分,位数管够,主机 ID 可以像 MAC 地址一样使用全局唯一编号,(网络 ID 需要手工配置或者靠什么外部的办法下发,这个没办法)入网的时候再拼起来。

梦里啥都有!

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

刚才说的所有这一切美好都建立于一个前提之上,就是我们有机会重开,把原来的那些历史遗留问题全部扬了。然而很不幸的是,这么做几乎不可能。即使 IPv6 的普及率已经到了 99%,我们也尚未摆脱 IPv4。而如果我们没有摆脱 IPv4,我们就不可能摆脱以太网地址或者 Wi-Fi 地址。而如果我们想保留 IEEE 802.3 和 802.11 的帧格式,我们就永远都没法像之前所说的那样在包里省下那几个字节。我们永远需要那个 “IPv6 邻居发现协议”,这就是个更复杂的 ARP(译者注:见上一个批注)。即使我们不再需要总线网络了,我们还是会需要去模拟一个出来,因为 ARP 需要依赖广播来工作。我们需要继续在家里维护一个本地的 DHCP 服务器来让家里的古董 IPv4 智能灯泡继续工作。我们需要继续使用 NAT 来让这些古董 IPv4 灯泡能够访问互联网。

译者注:“古董 IPv4 灯泡”,这句话说出来好喜感

这还不是最糟糕的部分——最糟糕的部分是,我们仍然必须用到那恶心到家的2层桥接,拜 IPv6 设计团队的一个 天大的疏忽 所赐。当他们在90年代做这些明日畅想的时候,他们忘了解决“移动 IP 地址“所带来的问题。据我所知,当时他们的想法是:

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

是啊,谁能料到40年之后是个什么样子呢?现在的小孩啊……

是啊,谁能料到40年之后是个什么样子呢?现在的小孩啊……

杀 手 级 应 用:移动互联网 IP

当然,二三十年后,我们知道了这个“扛着计算机到处跑”的“无关紧要的”几种场景:你的手机,它会不停连接到不同的网线插座 Wi-Fi 接入点。我们天天都在做这样的事。在用 LTE 流量的时候,甚至大部分情况下我们没有遇到问题;在用 Wi-Fi 的时候,有时候它也是能正确工作的。听起来不错,不是么?

……不太是。这些东西之所以能工作,还就是靠着互联网的深邃黑暗羞耻小秘密:二层桥接。互联网的路由机制不能正确处理移动设备的漫游,处理不了一点。如果你在一个 IP 网络中漫游,你的 IP 地址会变,然后你所有已建立的连接都会断。

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

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

译者注:这也是漫游中的海外运营商的手机流量能❄️❄️❄️的原因。

把移动 IP 真的搞对

原作者注:实际上这一段讲的东西完全不需要依赖 IPv6。只用 IPv4 和 NAT 也能实现它,甚至能实现在好几个 NAT 之间漫游。

总而言之,这是一个冗长的故事,不过我还是成功地通过四处采访 IETF 的专家们把这个故事凑圆了。到这个地方,我们会忍不住问:为什么事情会变成这样?移动 IP 为什么会这样出故障?

答案实际上出乎意料的简单。造成这个问题的设计缺陷就是著名的“四元组”(源 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 来标记不同的套接字。为此我们需要大得多的端口号(现在只有16位)。我们把它整到,比如说,128或者256位。这更像是某种唯一哈希。

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

译者注:原本传输层端口号的设计其实杂糅了“会话标签”和“服务标签”这两个功能,所以才有了这么别扭的监听逻辑:服务端监听 80 端口,收到客户端以 1111 端口发起的请求之后,移步一个新端口 2222 来建立连接,最后的连接是客户端 1111 对服务端 2222。译者当初学的时候被这个搞得很迷惑,感觉这里既有杂糅的问题,引入两个端口号也显得不那么自然。后来想象,这真的可能是时代局限性导致的(会话标签不能做太长不然开销太大;但是又为了能保证能建立足够多的连接,把两端的端口号拼起来做会话标签,能区分可观数量的会话)。

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

现在想象 X 的地址变成了 Q。它仍然向 Y 发送标记为(uuid,80)的数据包,但是这些包来自新 IP 地址 Q。Y 收到这些包并用 UUID 将这些包导向了正确的套接字,注意到了这些包来自一个新的 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 的设计。

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

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

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

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

然后互联网就能 再次优雅

译者注:在翻译的过程中, 告诉译者,这种(至少在面向用户层面上)全三层的网络实际上已经在有些云服务平台里落地了,比如说 Azure


原作者后记:我在 Tailscale 博客上发表了更多对于 IPv4/IPv6 迁移的想法。


译后记:和上次更新博客刚好差了一年,好歹凑出来个年更(笑)

这么一年发生了很多事情,不过我仍然感到空虚,因为有太多事情是被裹挟而去,当反问自己留下了什么的时候,心里就开始不舒服了。 最终还是写作最能让人安定下来,仿佛写下来的东西会成为实实在在能留下来(一厢情愿)、证明自己的意志曾存在过的证据,不至于一朝失败,意义被消解殆尽,就像我曾经历的数字黑暗时代。

本来在动笔前我曾对翻译这篇文章的意义而踌躇——毕竟对这篇文章感兴趣的读者大多可以自行阅读原文。不过事实证明这样做是相当有意义的:我搞明白了很多之前懵懵懂懂、原作者也没写明白的部分,并且把这部分理解批注在了正文里。

🔏 Jack Wang @ San Jose, CA, PST 2025/01/01


2025/01/02 更新:修改了几个排版错误和错别字。感谢 南科大 CRA 的同学帮忙勘误。