DHCP 的麻烦事

事情要从科大更新网络接入管理设备说起。

CaptureCapture

公共上网区域之所以要统一分配 IP 地址,是因为早先各楼各自为政、零散分配的 IP 地址段不够用了。几年前上网的主力是台式机和笔记本,也不可能时时刻刻开机;但现在人人都有智能终端,有可能还不止一台,到哪里就会连上哪里的 Wi-Fi。很多地方早先绰绰有余的 /24 地址段(256 个 IP)在高峰时段出现了分配不到 IP 的情况。而科大的 IP 地址池大小是有限的,集中分配就解决了地址不够的问题。

这本来是件皆大欢喜的事情,但新设备带来了新的问题。采用网络启动的图书馆查询机隔一段时间就卡死了,原因是母系统启动阶段与子系统启动阶段所分配到的 IP 地址不同,而这种不同又是由于 BRAS 网络接入管理设备的 bug。

CaptureCapture

局域网内的很多问题都跟 DHCP 有关,例如私设 DHCP 服务器,伪造大量的 DHCP 客户端导致 IP 地址池耗尽,还有最近爆出的 bash 漏洞使得 DHCP 服务器可以在客户端上执行任意代码。DHCP 服务器本身也是相对复杂的,需要保存已经分配地址的状态,一旦 DHCP 服务器挂掉重启,丢掉了状态,整个局域网就将陷入混乱(为此还要引入 ping 检测等机制)。

多数人都把 DHCP 分配 IP 地址看成是天经地义。但我们仔细想想,为什么局域网内一定要有一台地位特殊的服务器给大家分配 IP 地址?网卡有独一无二的硬件 MAC 地址,为什么不直接用网卡的 MAC 地址呢?计算机网络教科书会告诉我们,这是为了路由,也就是 IP 地址采用全球范围内自顶向下逐级分配的方式,就像邮政地址一样。而零散的 MAC 地址就像手机号,我们显然无法方便地根据手机号找到一个人的确切位置。

IPv6 = 网络号 + 硬件地址

教科书是对的。不过我们为什么不把这两种方案结合起来,在全球范围内的广域网中使用逐级分配的地址,而在局域网内直接使用硬件 MAC 地址呢?IPv6 就是这样工作的。IPv6 协议并不仅仅是把 32 位的地址扩展到了 128 位,还做了很多细节的改进,把 IPv4 协议中的很多历史遗毒清理掉了。IP 地址的分配就是其一。

当一个 IPv6 客户端接入网络时,它会根据自己的硬件 MAC 地址自动生成一个局域网内使用的链路本地(link local)地址,这样局域网内就可以通信了。例如硬件地址为 00:d0:b7:27:2b:92,生成的链路本地地址就是 fe80::2d0:b7ff:fe27:2b92。也就是说,局域网内的通信不依赖任何 “地址分配服务器”。事实上,在 IPv4 的世界里,很多操作系统在检测不到 DHCP 的环境下(例如双机网线直连)会根据 RFC3927 协议分配一个 169.254.. 的 “链路本地” IP 地址,当然这个过程需要协商,不像 IPv6 这样直接根据硬件地址就生成了。

IPv6 客户端的下一件事情是发出广播消息寻找邻居,看哪个邻居愿意充当它的网关,为它提供局域网以外的访问。这就是 Router Solicitation。局域网中的网关会回复 Router Advertisement,表示 “我愿意给你提供局域网以外的访问”,并把这个局域网在全球有效的 “网络号” 告诉客户端(例如,在少年班学院上网,网络号就是 2001:da8:d800:701)。客户端随后就记住了网关的地址,并把 “网络号” 与网卡的硬件 MAC 地址拼接起来,构成一个全球唯一的 IPv6 地址。

CaptureCapture

IPv6 的网络号与 IPv4 一样,采用全球范围内自顶向下分配的方式,这样世界上任何一台主机给它发数据包的时候,网络中的各级路由器只要像邮局一样,逐级投递这个数据包就行了。到达它所在的局域网后,数据包就会被根据硬件地址投递到相应的主机。(当然,事实上网关上维护着一个硬件地址和 IPv6 地址的对应表,不要在意这些细节……)

画外音:如果 IPv6 客户端想联系的本来就是局域网内的服务器,则使用的是另一对请求与回应消息:Neighbor Solicitation 与 Neighbor Advertisement,类似 IPv4 中的 ARP 协议。IPv6 里的这些东西都是基于 ICMPv6 协议的,而 ICMPv6 运行在 IPv6 协议之上,不再像 IPv4 那样 ARP 与 IPv4 并列了。ICMPv6 协议承载了这么重要的网络功能,可不是只用来 ping 网络测试,因此写 ip6tables 防火墙规则的时候千万别把它封掉了。

IPX = 网络号 + 硬件地址 + 端口号

IPv6 这么好的设计,为什么当年没人想到呢?这就要请出我们今天的主角:IPX 协议了。它基本上已经成为历史了,如果您听说过它,那多半是在星际争霸游戏里或者计算机历史书里。

ipx1ipx1

IPX 协议里的 “IP 地址” 有 12 字节,由三部分构成:网络号(32 位)、硬件地址(48 位)、端口号(16 位)。其中的网络号与硬件地址酷似 IPv6 协议里的概念。而端口号则对应着需要访问网络的应用程序,每个应用程序占用一个端口号。

ipx_ipx_structipx_ipx_struct

在 TCP/IP 的世界里,我们对端口号并不陌生。端口号在 TCP 和 UDP 协议里,也是用来标识应用程序的。IPX 协议似乎只是把传输层(TCP 和 UDP 所在的层次)的端口号 “下放” 到了 IP 层,这有啥大不了的?

顽固的 TCP

TCP 的作用是在不可靠的网络中实现可靠传输,还要在不同的应用和主机间共享网络资源。事实证明这件事很难做好,而且没有唯一的“最佳”方式把它做好。TCP 协议经历了几十年的演进,变得越来越复杂。用于区分应用程序的端口号概念是在 TCP 层而非 IP 层,从而一台主机上的应用程序必须共享同一套 TCP 实现,也就是 TCP 实现在内核里而不能由应用程序自行修改。

CaptureCapture

很多网络领域的研究者抱怨,TCP 方面的论文何止千篇,其中只有几篇能被主流系统采用,这就是因为 TCP 在内核里不方便修改,更不方便根据应用程序的需求来定制。Google 实在无法忍受 TCP 的种种问题了,最近开始推基于 UDP 的 QUIC,相当于是把传输层协议架空了。各种国产下载软件很多也使用了 UDP 和自己的拥塞控制算法来作为传输协议(当然,某些不使用拥塞控制的流氓协议是应当批评的)。如果当年端口号是在 IP 层,传输层可以由应用程序定制该多好!

此外,从控制理论的角度看,一个环的系统是比较稳定的。(下图中 S 表示远端的服务器,R 表示应用程序)

CaptureCapture

TCP 在内核里实现,增加了一层缓冲区,就把这个闭环拆成了两个闭环,这就不如单个闭环的系统稳定。(下图中 K 表示内核)

CaptureCapture

IPX 协议并不是对 DHCP 和网络性能的问题先知先觉,而是从设计的时候就把协议的组成部分放在了正确的层次,从而绕开了可能的问题。换句话说,系统中的很多棘手问题都是最初放错了协议层次带来的后遗症。

不过我们不必过于悲观,是金子总会发光的。随着应用程序对高性能高速网络的需求,现在越来越多的高端网卡开始硬件支持 socket,也就是硬件直接支持多个环形缓冲区,每个应用程序可以访问自己的缓冲区,而可靠传输、拥塞控制全部由应用程序控制,操作系统内核中的网络协议栈变成了薄薄一层。IPX “端口与传输协议解耦” 的设计理念在高性能网络中又获得了重生。

结语

坏的设计也许风行一时,但好的设计不会随时间风化。商业世界往往只考虑眼前的市场,而往往无暇从长计议,这也是纯种技术人无可奈何的。作为协议设计者,为了不让自己拍脑袋的设计成为成千上万码农日后的负担,务必多读读计算机系统和网络的历史,多问自己几个为什么。不过话说回来,没有这些补丁套补丁的设计,大多数码农靠什么吃饭呢?

参考文献

Comments