说来惭愧,我直到最近,2023 年了,才终于正式用上了原生 IPv6 网络(之前只用过 Hurricane Electric 和 Cloudflare 的隧道)。十几年根深蒂固的 IPv4 思维让我在了解和学习 IPv6 的过程中充满了惊奇和欣喜。

本文试图用小黄鸭也能听懂的方式,从较为简单的 IPv4 基础知识开始由浅入深地讲到 IPv6。

一、IP 地址的写法

IPv4

IP 地址对于计算机来说是一串 0 和 1 组成的二进制数字。IPv4 地址是 32 bit 的,即由 32 个 0 和 1 组成。对于人类来说,常用的写法是把这 32 个 bit 分成 4 组,每组 8 bit,转成十进制,中间用点隔开——即所谓的「点分十进制」。因为每组是 8 bit,所以每个十进制数字的范围在 0 到 255 之间。

比如 Google 的 IPv4 地址(之一):

142.251.215.238

点分十进制用对人类比较友好的方式,简明地表达了从最小的 0.0.0.0 到最大的 255.255.255.255 一共 42 亿个地址。

一群计算机组成一个网络,这个网络中最小的地址用来表示这个网络本身。比如以下 256 个地址组成了一个网络:

142.251.215.0
142.251.215.1
142.251.215.2
...
142.251.215.253
142.251.215.254
142.251.215.255

……则用 142.251.215.0 表示这个网络本身。

CIDR 用来简明地表示一个 IP 地址与其在的网络的关系。上述网络中的某个地址 142.251.215.42 可以表示为:

142.251.215.42/24

其中 /24 代表前缀长度。在这个例子的 256 个地址中,点分十进制的前三段都是相同的,变化的只是最后一段。前三段每段是 8 bit,总共是 24 bit 用于表示网络,因此前缀长度是 /24。前缀长度越大,固定的 bit 越多,变化的 bit 越少,这个网络就越小。此处前缀长度是 /24,则可变的部分是 8 bit,总共 256 个地址,符合预期。

在 IPv4 中,用 CIDR 可以表示从 /0 到 /32 各种大小的网络,其中 /0 代表整个 IPv4 地址空间,/32 代表大小为 1 的网络。

IPv6

IPv6 地址的写法与 IPv4 不同,似乎没有一个专门的名称。硬要和 IPv4 对应的话,我会把它叫作「冒号分隔的十六进制」。IPv6 地址是 128 bit 的,长度是 IPv4 地址的 4 倍,人类将其分成 8 组,每组 16 bit,转成十六进制数字,再用冒号隔开。

比如 Facebook 的 IPv6 地址(之一):

2a03:2880:f101:0083:face:b00c:0000:25de # 注意其中有 face:b00c 这样的彩蛋

每段开头的零可以省略,并且连续的全部是零的段可以用双冒号省略,比如:

2001:0db8:0000:0000:0000:0000:0000:0042

……可以简写成

2001:db8::42

CIDR 的写法与 IPv4 是一样的,即在地址后面加上斜线和数字代表前缀长度。由于 IPv6 有 128 bit,所以前缀长度从 /0 到 /128 不等。

二、IPv4 与 NAT

IPv4 地址由 32 个 bit 组成,所以总地址空间是 2³²,约等于 42 亿,去掉一些保留地址,实际可用的数量要少一些。理想环境下,每台能上网的设备都应该分到一个 IP 地址,但现在全球网民数量都不止 42 亿,更别提很多人还有多个上网设备,还有很多非人类的上网设备(IoT),所以这 42 亿地址是肯定不够用的。为了解决这个问题,人类发明了 NAT 这种邪恶的存在。

在理想世界里,每个家庭根据上网设备的数量分到一个对应大小的网络,家庭网络管理员把将这个网络里的地址分发给各个上网设备使用。在互联网的田园时代,当年的网民们就是这么分的。比如 1990 年时候 Apple 公司分得了 17.0.0.0/8 这个网络。根据上文的知识计算可得,这个网络有约 1678 万个 IP 地址,非常巨大。像这样巨大的网络,美国国防部分到了 14 个。还有很多类似的大网络被互联网起源地美国的公司和大学等机构瓜分(后来有些机构退还了一些)。

一共 42 亿地址,早年财大气粗地分配,后来全球网民数量又爆炸式增长,所以 IPv4 地址数量很快就不够分了。一个家庭没法再根据上网设备数量分配到对应数量的地址了,取而代之的是,运营商给一个家庭只分配一个地址,然后由路由器给家里的上网设备分配一套另一个网络里的地址只用于家庭内部通信。当家里的设备需要和外界通信的时候,就紧巴巴地共享运营商给的那个地址。这种情况下,家里那套地址就被称为内网地址,外部的地址就被称为外网地址公网地址。理论上内网地址用什么都可以,但如果正好和公网某个网站相同,那就冲突了,所以内网地址通常是使用保留地址如 192.168.0.0/16 里的地址。这些保留地址不会被任何公网网站使用,因此不会有冲突的风险。

为什么 NAT 的邪恶的?因为上述多个设备可怜兮兮地共享一个地址的方案只能用于访问别人,不能用于被别人访问。生活在 NAT 背后的设备在网络上低人一等,它们无法直接被外网设备访问,而要通过端口转发、NAT 打洞、反向代理等各种辅助方式才能有限程度地被访问到。

NAT 只是暂时缓解 IPv4 不够的问题。刚刚说到运营商给每个家庭只分配一个公网地址,当这户人家不上网的时候,这个地址甚至可以被分配给别的家庭。但随着接入信息高速公路的家庭数量越来越多,运营商连每个家庭一个公网地址都给不起了。最终 CGNAT 被开发出来了——运营商层面再做一次 NAT。

什么比 NAT 更邪恶?多层 NAT。

三、IPv6 与 SLAAC

小时候一直听说 IPv6 地址空间非常庞大,类似「整个宇宙每一颗沙粒都能分到一个地址」这种类比。但我实际学习了 IPv6 的应用之后,我发现 IPv6 地址空间的确是很大,但并不是每个地址都能用来分给沙粒。

IPv4 组建的家庭网络常见的前缀长度是 /24,这样的网络有 256 个地址,可供 200 多台设备上网。由于不同的设备必须使用不同的地址,网络管理员需要精准地管理这些地址。DHCP 通常被用于自动分配 IPv4 地址。新接入网络的设备(比如通过 Wi-Fi 接入的手机)向 DHCP 服务器(通常在路由器上)请求一个地址,DHCP 服务器从地址池里挑选一个空闲的地址租给上网设备,并保证该在其租期内不分配给其他设备,以免造成冲突。

IPv6 组建的家庭网络中常见的前缀长度是 /64,这也是标准规定的最小的网络尺寸。这样的网络有 2⁶⁴ = 18446744073709551616 ≈ 1.84 × 10¹⁹ 个地址。地址数量是如此之多,以至于根本不需要 DHCP 那样的中心化管理机制!目前 IPv6 常见的地址分配方式是 SLAAC,即让上网设备自己随机生成一个,然后再检测一下这个地址是不是已经有人用了。由于 /64 的 IPv6 网络实在是太大了,所以只要生成算法够随机,第一次生成的地址几乎一定是没人用的。

支持 IPv6 的运营商会分配一个 /64 甚至 /56 的网络给家庭路由器,于是 128 bit 的前一半就确定了。家庭路由器通过 PD (prefix delegation) 向家里的设备宣布「请在这个 /64 网络里随机挑选一个地址」,设备随机生成了后一半的地址,拼上路由器给的前一半,就得到了完整的 128 bit 的 IPv6 地址。设备有了这个地址之后,就可以开始上网了。并且这个地址是全球唯一的公网地址,一切仿佛回到了互联网田园时代那样,每个设备都有公网地址,每个设备在网络上都能被访问到!

习惯了贫瘠的 IPv4 地址空间的我,第一次了解到 SLAAC 的时候非常震惊。在 IPv4 里,家庭路由器能分到一个珍贵的公网地址已经很不容易了,想要更多的地址往往要加钱。而在 IPv6 里,运营商直接给路由器发 /56 的网络,即 256 个 /64 网络,而一个 /64 网络就有 2⁶⁴ 个地址,是整个 IPv4 地址空间(2³²)的无数倍,大到可以在里面随机挑选地址用而不碰撞,并且所有的地址都是公网地址…… o_O

四、全球唯一的地址

NAT 不再是挡箭牌

NAT 的一个副产物是天然防火墙。生活在 NAT 后面的内网设备不能直接被公网设备访问到,默认也就阻隔了绝大多数来自公网的网络攻击。NAT 的这一副产物在家庭网络中有一定的好处,但同时也惯坏了一些人(比如我),觉得躲在 NAT 后面,不需要防火墙也可以躲避来自互联网的网络攻击。

然而 IPv6 网络是没有 NAT 的(严格来说有,但不是脑子正常的人应该配的)。一切就像田园时代的互联网一样,每台上网设备都有一个全球唯一的 IP 地址,所有设备理论上都能访问到其他设备。为了不让坏人访问家里设备,网络管理员应当在路由器上配置 IPv6 防火墙,只放行合法的流量。

但从另一个角度来说,只要防火墙允许,任意两个 IPv6 设备之间都可以互联。想在家里搭个网站不再需要搞端口转发或反向代理之类的东西了,只要有 IPv6 地址,然后在防火墙上加一条规则就行了——前提是你的网站的访客也得使用 IPv6。

隐私问题

上文说到,家庭网络中上网设备的 IPv6 地址由路由器给的前半段,加上自己随机生成的后半段组成。在早期标准中,后半段是由网卡的 MAC 地址衍生的。由于网卡地址是固定的,最终生成出来的 IPv6 地址也是固定的。对于机房里的一台服务器来说,这样生成出来的地址填到 DNS 里,就可以让全球的上网设备通过 IPv6 访问自己了。

这对服务器来说是省配置的好事,但对普通网民来说就不一定了。由于后半段固定,哪怕网民更换运营商,或是移动到别的网络(如手机和笔记本电脑使用不同场所的 Wi-Fi),其 IPv6 地址的后半段都是一样的,这使得网站可以长期追踪用户。

为了解决这个隐私问题,现在主流操作系统一般使用双地址来解决这个问题——一个相对固定的 IPv6 地址用来供别人访问,一个随机生成并定期更换的 IPv6 地址用来上网。

好算的 CIDR

IPv6 的「冒号分隔的十六进制」写法要比 IPv4 的「点分十进制」长不少,但同时,CIDR 的计算变得更加容易了。在点分十进制里,每段是 8 bit,因此只有当前缀长度是 8 的倍数的时候,网络与主机的分割线才划在「点」的位置。除了 /24 和 /16 这种阅读起来比较舒服的前缀长度,诸如 /20 和 /28 这种也是很常见网络 IPv4 前缀长度。由于这时候这时候分割线并不在点的位置,分出来的子网也就没那么直观了。我每次都记不住而用 ipcalc(1) 去算。

IPv6 有 128 bit,所以大家分网的时候也没那么抠门,从最小的 /64 往上,至少都是每次 8 bit 增长,只要是 4 的倍数,都能干净地分割到字符边界,阅读和计算起来很方便。

好看的地址

SLAAC 生成的地址是随机的,往往不怎么能简写。对于一些网络服务,比如 DNS 来说,往往需要一个简短又好看的地址方便人类记忆,这时候往往就是从庞大的地址空间里手动指定一个 IPv6 地址了。比如 Google Public DNS 的 IPv6 地址是:

# 简写
2001:4860:4860::8888
2001:4860:4860::8844

# 完整写法
2001:4860:4860:0000:0000:0000:0000:8888
2001:4860:4860:0000:0000:0000:0000:8844

通过选取有大量连续零的地址,Google 获得了简写较短的 IPv6 地址。其最后一段的 8888 和 8844 则是对应其 IPv4 地址 8.8.8.8 和 8.8.4.4,更加方便记忆。

由于 IPv6 地址通常使用十六进制数字书写,这 a-f 六个英文字母也可以被用来整活。比如上述 Facebook 的地址就嵌入了 face:b00c 这样的彩蛋。

对于想在手动指定的 IPv6 地址上整活的读者来说,可以参考 Hexspeak 获取灵感。