Skip to content

叙事密度:中 | 主语言:Python | C 差异窗 | Vol 4·计算机网络


元数据卡

属性内容
卷号Vol 4 — 计算机网络
章节第13章:IPv6 基础
前置第9章(网络层深入:IPv4 首部、CIDR、NAT、ICMP)
后置第14章:网络实战与性能优化
理论深度(3/5)
Python 相关度≈60%;socket 编程、scapy 构造 ICMPv6 包
核心概念128 位地址空间、NDP、SLAAC、AAAA 记录、双栈、NAT64
代码量~120 行

你在哪

"驿道面临一个根本性的问题:驿道坐标不够了。IPv4 的 32 位法术坐标早就用完,传送门地址转换(NAT)只是权宜之计。IPv6 用 128 位法阵坐标给每颗魔法沙子都分配一个独一无二的驿道地址。"

第9章你拆开了 IPv4 的法术首部,学会了子网划分、CIDR 聚合、NAT 怎么用一个公网法术坐标养活整座法师塔。但 IPv4 有一个根本性的问题:法阵空间不够了

32 位坐标 ≈ 43 亿。驿道世界上 80 亿人,每人分不到一个。魔导设备、每台法术通讯石、魔法马车、智能法术灯——它们都需要驿道坐标。IPv4 不是没在撑,但已经到了极限——全球 IPv4 法术坐标池在 2019 年已经耗尽,亚太法阵区(APNIC)在 2011 年就用完了。

这一章,我们走进 IPv6 的世界——不是"IPv5 的升级版"(IPv5 是实验性的魔法传送流协议,从未正式部署),而是一个全新设计的驿道层法阵协议,从根源上解决了 IPv4 所有设计上的遗憾。


你的任务

学完本章,你应当:

  1. 说出 IPv4 耗尽的核心原因和 NAT 的代价
  2. 手绘 IPv6 首部:40 字节固定大小,为什么去掉校验和和分片字段
  3. 读写 IPv6 地址:冒号十六进制、:: 简写规则、前缀表示法
  4. 区分三种单播地址:Global Unicast → Unique Local → Link-Local
  5. 理解 NDP 替代 ARP 干了什么:邻居请求/广告、路由器请求/广告、重定向
  6. 解释 SLAAC 的原理:路由器通告 → 接口 ID 生成
  7. 用 Python 写一个绑 IPv6 地址的 socket 程序
  8. 对比双栈、隧道、NAT64 三种过渡技术的优劣

遭遇战 → 获得技能

第1战:IPv4 地址耗尽——不是未雨绸缪,是雨已经淋头了

问题: 你是一座驿道法术灵脉服务商(ISP),2019 年 11 月你收到一封魔力信函:"RIPE(欧洲驿道坐标管理机构)的 IPv4 驿道坐标池已经耗尽。没有新的 /8 坐标块可以分配。"你的新法师客户要用驿道,但你没有 IPv4 坐标可以给他们了。

全球 IPv4 地址耗尽时间线:

2011年2月:IANA(互联网编号管理局)把最后 5 个 /8 分配给了五大 RIR
2011年4月:APNIC(亚太)耗尽
2012年9月:RIPE NCC(欧洲)耗尽
2014年6月:LACNIC(拉美)耗尽
2015年9月:ARIN(北美)耗尽
2019年11月:RIPE 彻底耗尽(连最后 /8 都用完了)

NAT —— 拖延时间的权宜之计

第9章学了 NAT:一个公网 IP 通过端口转换 + 状态表能让几百个内网设备上网。它确实拖延了地址耗尽的速度,但代价不小:

│ NAT 的问题                          │ 实际影响              │
│─────────────────────────────────────│────────────────────────│
│ 破坏了端到端模型(end-to-end)        │ P2P 文件共享难、游戏联机难  │
│ 外部不能主动连接内网                 │ 自建服务器必须端口转发、UPnP │
│ 传输层地址不透明                    │ IP 地址不唯一标识一个设备    │
│ NAT 穿透复杂                       │ STUN/TURN/ICE 才能打通    │
│ 端口限制影响连接数                   │ 一个公网 IP 约 65,535 个端口 │
│ IP 地址不再代表身份                 │ 审计、溯源、策略都很困难    │

IPv6 的核心理念:恢复互联网的原始设计——每个设备一个全球可达的 IP。

其实早在 1998 年,IETF 就发布了 RFC 2460(IPv6 规范)。但真正大规模部署等了超过 20 年。不是因为 IPv6 不好,而是因为 NAT + CIDR 把 IPv4 的寿命拖长了,部署 IPv6 需要全球同步升级硬件和软件——这不只是技术决策,更是经济决策。


第2战:IPv6 头部——砍掉了什么,新增了什么

问题: 如果让你重新设计驿道坐标协议,你会保留 IPv4 头部的哪些字段?IPv4 头部 20-60 魔力字节,驿道信标塔每处理一个法术包都要解析——IHL 决定头部长度、校验和每座信标塔重算、分片字段信标塔帮你做。你能不能让头部长得固定、轻量、信标塔不用算校验和

看一眼 IPv6 头部长什么样(固定 40 字节):

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |Version| Traffic Class |           Flow Label                  |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |         Payload Length        |  Next Header  |   Hop Limit   |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  +                         Source Address                        +
  |                        (128 bits)                             |
  +                                                               |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  +                       Destination Address                     +
  |                        (128 bits)                             |
  +                                                               |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

IPv4 vs IPv6 头部对比:

特性IPv4IPv6
头部大小20-60 字节(有 IHL 和 Options)固定 40 字节
地址长度32 位128 位
校验和每跳重算删掉了(依赖上层校验)
分片路由器可以做发送端做(Path MTU Discovery)
Options头部内,导致变长移出到扩展头部(Next Header 链)
IHL需要删了(固定长度)
流标签新增(QoS 标记)

用 Python 解析一个 IPv6 头部:

python
import struct, socket

class IPv6Header:
    """固定 40 字节的 IPv6 首部"""
    FMT = '!IHBB16s16s'  # 4 + 2 + 1 + 1 + 16 + 16

    def __init__(self, raw: bytes):
        (ver_tc_flow, payload_len, next_header,
         hop_limit, src, dst) = struct.unpack(self.FMT, raw[:40])

        self.version = ver_tc_flow >> 28          # 6
        self.traffic_class = (ver_tc_flow >> 20) & 0xFF
        self.flow_label = ver_tc_flow & 0xFFFFF
        self.payload_length = payload_len          # 不含头部
        self.next_header = next_header             # 6=TCP, 17=UDP, 58=ICMPv6
        self.hop_limit = hop_limit                 # 相当于 IPv4 TTL
        self.src_addr = socket.inet_ntop(socket.AF_INET6, src)
        self.dst_addr = socket.inet_ntop(socket.AF_INET6, dst)

    def __repr__(self):
        return (f"IPv6({self.src_addr}{self.dst_addr} "
                f"next={self.next_header} len={self.payload_length})")

IPv6 核心设计哲学:

  1. 删掉校验和:IPv4 的校验和每跳都得重算(因为 TTL 在变)。IPv6 说——数据链路层(以太网)已经有 CRC,上层(TCP/UDP)也有校验和。IP 层再算一次是多余的。省掉一个 CPU 周期,路由器更快。

  2. 不支持路由器分片:IPv4 的路由器可以在中间分片——但如果一个分片丢了,整个包废了。IPv6 强制发送端做 Path MTU Discovery,找到整条路径的 MTU,然后在这个尺寸以内发送。如果一定要发更大的包,在扩展头部里告知。

  3. Options 变成扩展头部链:IPv4 的 Options 让头部边长,路由器每次都得解析。IPv6 把附加功能放到扩展头部,用 Next Header 字段链起来:

    [IPv6 头部] → [扩展头部 1] → [扩展头部 2] → [TCP/UDP 载荷]
    Next Header=60(Dest Options)    Next Header=44(Fragment)    Next Header=6(TCP)

    路由器不需要解析扩展头部——它只看 IPv6 头部就转发。扩展头部的处理只在发送端和接收端之间。这是性能的巨大提升。

C 差异窗:C 在 <netinet/ip6.h> 里有 struct ip6_hdr,可以直接映射 IPv6 头部。Python 的 struct.unpack 因为没有位域,需要用移位操作提取 Version/Traffic Class/Flow Label。Java 的 java.net.Inet6Address 抽象了地址表示,但你要用 java.nio.ByteBufferpcap4j 才能解析裸 IPv6 头部。


第3战:IPv6 地址表示——128 位怎么写

问题: 32 位的 IPv4 用点分十进制写——192.168.1.1,四个数字,清晰。128 位的 IPv6 如果也用点分十进制——192.168.1.1.192.168.1.1.192.168.1.1.192.168.1.1——你背得下来?

冒号十六进制表示法:

冒号十六进制表示法:

完整形式: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
           ↑ 8 个 16 位段(每段 4 个十六进制数字),用冒号隔开

三条简化规则:

规则 1:每段的前导零可以省略

2001:0db8:0000:0000:0000:0000:0000:0001
→ 2001:db8:0:0:0:0:0:1

规则 2:连续的零段可以用 :: 替换一次(只能一次!)

2001:db8:0:0:0:0:0:1
→ 2001:db8::1

注意::: 只能出现一次——否则你不知道它代表几段零。

❌ 2001::db8::1     ← 歧义!左边多少段?右边多少段?
✅ 2001:0:0:db8::1  ← 唯一解释

规则 3:IPv4 映射地址的混合写法

::ffff:192.168.1.1   = 0000:0000:0000:0000:0000:ffff:c0a8:0101

Python 验证你的 IPv6 地址对不对:

python
import socket

def validate_ipv6(addr: str) -> bool:
    """验证 IPv6 地址字符串是否合法"""
    try:
        socket.inet_pton(socket.AF_INET6, addr)
        return True
    except OSError:
        return False

tests = [
    "2001:db8::1",           # ✅ 标准简写
    "::1",                    # ✅ 环回地址
    "::",                     # ✅ 未指定地址
    "2001:db8::1::1",         # ❌ 两个 ::,非法
    "fe80::1%eth0",           # ✅ Link-Local 带 scope ID
    "2001:0db8:85a3::8a2e:0370:7334",  # ✅
]
for addr in tests:
    print(f"{'✅' if validate_ipv6(addr) else '❌'} {addr}")

输出:

✅ 2001:db8::1
✅ ::1
✅ ::
❌ 2001:db8::1::1
✅ fe80::1%eth0
✅ 2001:0db8:85a3::8a2e:0370:7334

重要地址:

地址含义
::1/128环回地址(相当于 IPv4 的 127.0.0.1
::/128未指定地址(尚未分配,相当于 IPv4 0.0.0.0
2000::/3全局单播地址(Global Unicast)
fc00::/7Unique Local Address(相当于 IPv4 私有地址 10.0.0.0/8 等)
fe80::/10Link-Local Address(链路本地,仅在同一链路上有效)
ff00::/8多播地址
2002::/166to4 隧道地址(过渡技术,已废弃)

前缀表示法(和 CIDR 一样):

2001:db8:1234::/48     → 前 48 位是子网前缀
2001:db8:1234:5678::/64 → 前 64 位是子网前缀(标准 /64 子网)

IPv6 的标准子网大小就是 /64。不是 /24、不是 /48——是 /64。主机部分永远是 64 位。这意味着一台路由器后面有 2^64 个地址——比整个 IPv4 地址空间还大 40 亿倍。


第4战:地址类型——单播、多播、任播

问题: IPv4 有三种驿道地址类型:单播(Unicast)、广播(Broadcast)、多播(Multicast)。广播有严重的效率问题——发一道法术信函给所有人,不想收也得收。IPv6 怎么做?

IPv6 只有三种驿道地址类型(删掉了广播!):

1. 单播(Unicast)——点到点通信

单播再分三种:

Global Unicast(全球单播)——可以在互联网上路由:

2001:db8:1234:5678::1/64
    ↑全球路由前缀(48bit)  ↑子网(16bit)  ↑接口ID(64bit)

Unique Local Address(ULA,唯一本地地址)——相当于 IPv4 私有地址:

fc00::/7  →  例如 fd12:3456:789a::1

只能在内网使用,不会在互联网上路由。每个组织随机生成一个 /48 前缀,碰撞概率极低。

Link-Local Address(链路本地地址)——自动生成,仅同一链路上有效:

fe80::/10  →  例如 fe80::21e:c2ff:fe12:3456

关键理解:每个 IPv6 接口必然至少有两个地址——一个 Link-Local,一个 Global 或 ULA。Link-Local 用于邻居发现(NDP)等本地协议。

python
import socket, struct

def get_ipv6_addresses(ifname: str = "eth0"):
    """获取指定接口的 IPv6 地址"""
    # 读取 /proc/net/if_inet6(Linux 特有)
    try:
        with open("/proc/net/if_inet6") as f:
            for line in f:
                parts = line.strip().split()
                addr_hex = parts[0]       # 32 个 hex 字符 = 128 位
                iface = parts[4]
                scope = parts[2]           # 0x20=global, 0x40=link-local
                if iface == ifname:
                    # 把 hex 转为冒号表示
                    addr = ':'.join(
                        addr_hex[i:i+4] for i in range(0, 32, 4)
                    )
                    # 简化 :: 表示(先去掉前导零)
                    groups = [
                        str(int(g, 16)) for g in addr.split(':')
                    ]
                    # 简单的 :: 压缩
                    addr_parts = ':'.join(
                        hex(int(g, 16))[2:] or '0' for g in groups
                    )
                    scope_name = "Global" if scope == "20" else "Link-Local"
                    yield scope_name, addr_hex, iface
    except FileNotFoundError:
        print("/proc/net/if_inet6 not found (not Linux?)")

for scope, raw, iface in get_ipv6_addresses("eth0"):
    print(f"[{scope}] {raw} on {iface}")

2. 多播(Multicast)——替代 IPv4 的广播

IPv4 的广播(255.255.255.255)会把包发给链路上所有设备,不管它们想不想收。IPv6 删了广播,改用多播。

多播地址范围:

ff02::1      → 所有节点(相当于广播,但协议层实现更高效)
ff02::2      → 所有路由器
ff02::1:ffXX:XXXX  → 请求节点多播地址(用于 NDP 邻居发现)
ff05::1:3    → 所有 DHCP 服务器

多播 v.s. 广播的核心差异:

  • 广播:发出去,网卡必须接收并送到协议栈处理。
  • 多播:网卡可以过滤——只在加入对应多播组时才中断 CPU。

3. 任播(Anycast)——发到一组设备里"最近"的一个

任播是 IPv6 新增的地址类型(虽然 IPv4 也能实现,但 IPv6 正式定义了):多个设备配置相同的 IP 地址,路由器自动把包送到最近的那个。

场景:DNS 根服务器。全球有 13 个逻辑根服务器,但通过任播,每个逻辑根在全球有上百个镜像。你的请求会被路由到距离你最近的镜像。

python
def explain_anycast():
    """任播 vs 单播 vs 多播"""
    return {
        "Unicast":  "1 对 1 —— 你发给一个特定的设备",
        "Anycast":  "1 对最近 —— 你发给一组设备中离你最近的那个",
        "Multicast":"1 对多 —— 你发给所有加入这个多播组的设备",
    }

for k, v in explain_anycast().items():
    print(f"{k:12s}: {v}")

第5战:ICMPv6 与邻居发现协议(NDP)——告别 ARP

问题: IPv4 里,你知道目标的驿道坐标但不知道它的信标标识(MAC)——用 ARP(地址解析咒)广播问"谁有这个驿道坐标?"但 ARP 有几个问题:它是广播(所有人都收到)、无认证(可以被 ARP 欺骗攻击)、只有地址解析功能。

IPv6 用一个统一的协议代替了 ARP 以及更多功能:

NDP(邻居发现咒,Neighbor Discovery Protocol) 通过 ICMPv6 法术报文实现。ICMPv6 的类型代码为 133-137:

NDP 消息ICMPv6 类型作用对应 IPv4 功能
邻居请求(NS)135"谁有地址 X?告诉我你的 MAC"ARP 请求
邻居广告(NA)136"我有地址 X,这是我的 MAC"ARP 应答
路由器请求(RS)133"这里有路由器吗?请发 RA 给我"
路由器广告(RA)134"我是路由器,这是我的配置信息"DHCP 可选
重定向137"别走这条路,走那条更快"ICMP 重定向

用 scapy 构造 ICMPv6 NS(邻居请求)和 NA(邻居广告):

python
# 需要安装 scapy: pip install scapy
try:
    from scapy.all import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, ICMPv6NDOptDstLLAddr
    from scapy.all import Ether, sendp

    # 构造邻居请求:问"谁有 fe80::1 的 MAC 地址?"
    ns_packet = (
        Ether(dst="33:33:ff:00:00:01") /      # 请求节点多播 MAC
        IPv6(dst="ff02::1:ff00:1") /          # 请求节点多播地址
        ICMPv6ND_NS(tgt="fe80::1")           # 目标地址
    )
    print("=== 邻居请求包 ===")
    ns_packet.show()

    # 构造邻居广告:回答"fe80::1 是我,MAC=00:1e:c2:12:34:56"
    na_packet = (
        Ether(dst="00:1e:c2:ab:cd:ef") /
        IPv6(dst="fe80::2") /
        ICMPv6ND_NA(tgt="fe80::1", R=0, S=1, O=1) /
        ICMPv6NDOptDstLLAddr(lladdr="00:1e:c2:12:34:56")
    )
    print("\n=== 邻居广告包 ===")
    na_packet.show()

except ImportError:
    print("需要安装 scapy: pip install scapy")

NS/NA 的交互过程(替代 ARP):

[主机 A fe80::1/64]              [主机 B fe80::2/64]
       |                               |
       |-- ICMPv6 NS (type=135) ------>|   "谁有 fe80::2?"
       |   dst=ff02::1:ff00:0002       |   (请求节点多播地址)
       |   目标地址 = fe80::2           |
       |                               |
       |<-- ICMPv6 NA (type=136) ------|   "是我,fe80::2"
       |   源地址 = fe80::2             |   我的 MAC 是 00:...:56"
       |   目标链路层地址 = 00:...:56   |

为什么 NDP 比 ARP 好:

  1. 不是广播:NS 发到"请求节点多播地址"——只有跟这个 IP 相关的设备收到,不打扰全链路
  2. 有安全基础:可以用 SEND(Secure Neighbor Discovery, RFC 3971)做加密认证
  3. 统一框架:地址解析 + 路由器发现 + 重定向 + 地址自动配置,全部在 ICMPv6 内完成

第6战:SLAAC——无状态地址自动配置

问题: 你的法术卷轴插上法师塔的魔导缆线或连上驿道传送阵。它怎么自动获得一个 IPv6 地址?在 IPv4 的世界里是 DHCP(动态法师塔配置协议),需要法术配置信标塔的存在。能不能不需要信标塔,让设备自己算出自己的驿道地址?

SLAAC(无状态地址自动配置咒) 就是这么来的:

1. 设备发 RS(路由器请求):
   [新设备] → ff02::2 (所有路由器多播) → "这里有没有路由器?请告诉我前缀"

2. 路由器回复 RA(路由器广告):
   [路由器] → ff02::1 (所有节点多播) → "我的前缀是 2001:db8:1234:5678::/64"
                                            还有: MTU、默认路由、DNS 等信息

3. 设备自动生成地址:
   前缀 (64 位) + 接口 ID (64 位)
   = 2001:db8:1234:5678 + <接口 ID>

接口 ID 可以是:

方式一:EUI-64(基于 MAC 地址)

把 MAC 地址 00:1e:c2:12:34:56 转换为 64 位接口 ID:

python
def mac_to_eui64(mac: str) -> str:
    """将 MAC 地址转为 EUI-64 格式的 IPv6 接口 ID"""
    # 去掉分隔符,转为 6 字节
    mac_hex = mac.replace(':', '').replace('-', '')
    mac_bytes = bytes.fromhex(mac_hex)

    # EUI-64: 插入 FFFE 到中间(第3字节和第4字节之间)
    eui64 = mac_bytes[:3] + bytes([0xFF, 0xFE]) + mac_bytes[3:]

    # 翻转 U/L 位(第 7 位从 0→1 表示本地管理)
    eui64_list = list(eui64)
    eui64_list[0] ^= 0x02  # 翻转第7位

    return ':'.join(f'{b:02x}' for b in eui64_list)

mac = "00:1e:c2:12:34:56"
print(f"MAC:         {mac}")
print(f"EUI-64 ID:   {mac_to_eui64(mac)}")
# → 02:1e:c2:ff:fe:12:34:56
#    ↑ 最高字节第7位翻转了的 00→02

最终的地址:2001:db8:1234:5678:021e:c2ff:fe12:3456/64

方式二:隐私扩展(RFC 4941,Privacy Extensions)

EUI-64 的问题:你的 MAC 地址全球唯一 → 你去哪里都是一样的接口 ID → 可以被跟踪。

隐私扩展:接口 ID 定期随机生成,每过一段时间换一个。

python
import os, time

def generate_privacy_address(prefix: str) -> str:
    """生成带隐私扩展的 IPv6 地址(临时地址)"""
    # 随机生成 8 字节的接口 ID
    iid = os.urandom(8)

    # 确保 U/L 位 = 0(全球唯一标记为 0,这是临时地址)
    iid_list = list(iid)
    iid_list[0] &= ~0x02  # 清空 U/L 位

    iid_hex = ':'.join(f'{b:02x}' for b in iid_list)
    return f"{prefix}{iid_hex}"

prefix = "2001:db8:1234:5678:"

print("隐私扩展地址(每几小时更换一次):")
for _ in range(3):
    addr = generate_privacy_address(prefix)
    print(f"  {addr}")
    time.sleep(0.1)

# 输出类似(每次运行不一样):
#   2001:db8:1234:5678:a1b2:c3d4:e5f6:a7b8
#   2001:db8:1234:5678:9a8b:7c6d:5e4f:3a2b
#   2001:db8:1234:5678:0f1e:2d3c:4b5a:6978

Java 差异窗:Java 的 NetworkInterface.getInetAddresses() 可以列出所有 IPv6 地址,包括临时地址。但生成接口 ID 的低层 API 没有暴露——那是内核 NDP 栈做的事。C 可以用 getifaddrs() 获得所有地址,并配合 /proc/net/if_inet6 查看是否是临时地址。


第7战:DNS —— AAAA 记录与 IP6.ARPA

问题: 你已经有 IPv6 驿道地址了,但法师观测镜怎么知道 ipv6.example.com 对应哪个 IPv6 地址?IPv4 用的是 A 记录(IPv4 地址),IPv6 用 AAAA 记录(Quad-A 记录——IPv6 地址长度是 IPv4 的四倍)。

bash
# 用 dig 查询 AAAA 记录
dig AAAA ipv6.google.com +short
# 输出类似:
# 2404:6800:4004:80c::200e

# 反向 DNS 查询(IPv4 用 .in-addr.arpa)
dig -x 2404:6800:4004:80c::200e +short
# IPv6 反向域是 .ip6.arpa

# 用 Python 查
python
import socket

def resolve_ipv6(hostname: str):
    """解析域名的 IPv6 地址(AAAA 记录)"""
    try:
        # getaddrinfo 返回所有地址族
        results = socket.getaddrinfo(hostname, 80,
                                      socket.AF_INET6,
                                      socket.SOCK_STREAM)
        addrs = set()
        for result in results:
            # result[4] 是 (addr, port, flow, scope_id)
            addr = result[4][0]
            addrs.add(addr)
        return list(addrs)
    except socket.gaierror as e:
        return f"DNS 解析失败: {e}"

# 测试
hosts = ["google.com", "ipv6.google.com", "localhost"]
for host in hosts:
    try:
        addrs = resolve_ipv6(host)
        if isinstance(addrs, list):
            print(f"{host}:")
            for a in addrs[:3]:  # 只显示前 3 个
                print(f"  {a}")
        else:
            print(f"{host}: {addrs}")
    except Exception as e:
        print(f"{host}: {e}")

应用程序如何处理双栈:

应用代码通常不知道底层用的是 IPv4 还是 IPv6——现代 socket 库可以同时处理:

python
def connect_to_service(host: str, port: int):
    """尝试 IPv6 优先(Happy Eyeballs 简化版)"""
    results = socket.getaddrinfo(host, port)

    # 找第一个 IPv6 地址
    for family, type_, proto, _, addr in results:
        if family == socket.AF_INET6:
            try:
                s = socket.socket(family, type_, proto)
                s.settimeout(3)
                s.connect(addr)
                print(f"✅ 通过 IPv6 连接: {addr[0]}")
                return s
            except Exception:
                continue

    # IPv6 不行就 fallback 到 IPv4
    for family, type_, proto, _, addr in results:
        if family == socket.AF_INET:
            try:
                s = socket.socket(family, type_, proto)
                s.settimeout(3)
                s.connect(addr)
                print(f"✅ 通过 IPv4 连接: {addr[0]}")
                return s
            except Exception:
                continue

    raise ConnectionError(f"无法连接到 {host}:{port}")

# 这个逻辑和 RFC 6555(Happy Eyeballs)保持一致
# 生产环境推荐直接让 OS 内核处理地址选择

第8战:无 NAT —— 端到端模型恢复

问题: IPv4 用传送门地址转换(NAT)解决了坐标不足的问题,但代价是端到端模型被破坏。IPv6 不需要 NAT,因为地址足够多。这意味着什么?

IPv6 下每座信标塔都有一个全球可达的公网 IPv6 驿道坐标。端口转发、UPnP、STUN/TURN——这些 NAT 时代的工程妥协可以退休了。

但这也带来了新的挑战:

│ IPv6 无 NAT 的挑战            │ 应对方案                  │
│──────────────────────────────│───────────────────────────│
│ 设备暴露在全球互联网上         │ 主机防火墙(每台设备自己做)  │
│ 扫描整个 /64 子网不现实        │ 只能通过 DNS 找到目标      │
│ 内网地址也是全球唯一的         │ ACL(访问控制列表)来隔离   │
│ 隐私:地址和设备绑定           │ 隐私扩展(定期换接口 ID)   │

一个简单的 IPv6 防火墙规则(Linux ip6tables):

bash
# 允许已建立连接的返回流量
ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# 允许 SSH(限制来源)
ip6tables -A INPUT -p tcp --dport 22 -s 2001:db8::/32 -j ACCEPT

# 允许 ICMPv6(NDP 需要)
ip6tables -A INPUT -p ipv6-icmp -j ACCEPT

# 拒绝其他所有入站
ip6tables -A INPUT -j DROP

IPv6 地址扫描的成本:

python
import math

def estimate_scan_time(subnet_prefix: int):
    """估算扫描一个 IPv6 子网需要的时间"""
    host_bits = 128 - subnet_prefix
    total_addrs = 2 ** host_bits

    # 假设每秒能扫描 1,000,000 个地址(非常快)
    scan_per_sec = 1_000_000
    seconds = total_addrs / scan_per_sec
    years = seconds / (365.25 * 24 * 3600)

    print(f"子网 /{subnet_prefix}: 2^{host_bits} = {total_addrs:,} 个地址")
    print(f"每秒 {scan_per_sec:,} 个地址扫描需要: {years:.1f} 年")

    # IPv4 /24 对比
    print(f"\n对比 IPv4 /24: 256 个地址,相同速度: 0.000256 秒")

estimate_scan_time(64)  # 标准 /64 子网

输出:

子网 /64: 2^64 = 18,446,744,073,709,551,616 个地址
每秒 1,000,000 个地址扫描需要: 584,542.0 年

对比 IPv4 /24: 256 个地址,相同速度: 0.000256 秒

这解释了为什么 IPv6 不需要 NAT 也不怕被扫——你的 /64 子网大到扫描器根本找不到你的设备。


第9战:过渡技术——从 IPv4 到 IPv6

问题: 全球几十亿座现有信标塔只支持 IPv4。你不可能在 2026 年 6 月 25 日零点把驿道全部切到 IPv6。过渡期会持续很久——也许是永远。怎么让 IPv4-only 和 IPv6-only 的信标塔互相通信?

三种主要的过渡策略:

策略 1:双栈(Dual Stack)——最推荐

一台机器同时运行 IPv4 和 IPv6 协议栈,两个地址,两套路由。DNS 根据请求方的地址族返回 A 或 AAAA 记录。

[你的电脑] ←→ [路由器(双栈)] ←→ [IPv4 互联网]
                 ↑                   ↓
              [IPv6 互联网]       [IPv4 服务器]

DNS 查询 example.com:
  如果你的网络支持 IPv6 → 返回 AAAA 记录 → 走 IPv6
  如果只支持 IPv4      → 返回 A 记录  → 走 IPv4

双栈的问题: 每个设备需要两个地址,路由器维护两张 FIB(转发信息表),NAT64/DNS64 基础设施还是需要运行。

策略 2:隧道(Tunneling)

把 IPv6 包封装在 IPv4 包里发送,穿越只有 IPv4 的网络。

6to4(RFC 3056)

发送端: [IPv6 头部 | TCP | 数据] → 封装 → [IPv4 头部 | IPv6 包]
         ↑ 原始包                          ↑ 外层 IPv4(目标 6to4 中继)

6to4 使用 2002::/16 地址段——前 16 位 2002,接着 32 位是 IPv4 地址的十六进制表示:

IPv4 地址 203.0.113.5 → 2002:cb00:7105::/48 (cb007105)

Teredo(RFC 4380,已废弃):微软主导的隧道协议,在 NAT 后面也能工作(UDP 封装)。但 2010 年代以后发现安全漏洞太多,微软在 Windows 10 中默认禁用了 Teredo。

隧道的问题(特别是 6to4):

  • NAT 后通常不能工作
  • 额外的封装头部开销(20+ 字节)
  • 安全风险(隧道端点是盲区)
  • 性能劣化(需要进行解封装处理)

策略 3:NAT64/DNS64 —— IPv6-only 访问 IPv4 世界

IPv6-only 的设备不能直接访问 IPv4-only 的网站。NAT64 解决了这个问题:

[IPv6-only 手机] → [NAT64 网关] → [IPv4-only 服务器]
       ↑                 ↑
   源: 2001:db8::1    把 IPv6 包翻译为 IPv4 包
   目: IPv6 地址       维护 IPv6 ↔ IPv4 映射表

DNS64 配合 NAT64 工作——当 DNS 查询只有 A 记录(没有 AAAA 时),DNS64 合成一个虚假的 AAAA 记录:

客户端查询 example.com
  → DNS64 查 AAAA → 没有
  → DNS64 查 A 记录 → 93.184.216.34
  → DNS64 合成 AAAA: 64:ff9b::93.184.216.34
  → 客户端用这个 "IPv6 地址" 发送请求
  → NAT64 网关收到,翻译为 IPv4 包发给 93.184.216.34
python
def nat64_construct(pref64: str = "64:ff9b::", ipv4: str = "93.184.216.34"):
    """生成 NAT64 前缀映射的 IPv6 地址"""
    # 把 IPv4 地址转成 32 位整数
    ipv4_int = int.from_bytes(
        bytes(int(x) for x in ipv4.split('.')), 'big'
    )
    # 拼接到 NAT64 前缀后面
    addr = f"{pref64}{ipv4_int:08x}"
    return addr

ipv4 = "93.184.216.34"
print(f"IPv4: {ipv4}")
print(f"NAT64: {nat64_construct('64:ff9b::', ipv4)}")
# → 64:ff9b::5db8:d822

三种策略对比:

策略部署难度性能兼容性推荐?
双栈中(双协议栈)原生最佳强烈推荐
隧道(6to4/Teredo)封装开销有 NAT 问题已废弃
NAT64/DNS64中(需要网关)转换开销小应用无感知移动网络首选

现实是:2026 年,主流网站和 ISP 已经全面支持 IPv6。Google 的 IPv6 可用性从 2010 年的不到 1% 涨到了 40%+。但完全放弃 IPv4 还早——双栈会是未来 10 年的常态。


第10战:移动 IPv6 —— 比移动 IPv4 简化

问题: 你的法术通讯石从家里的传送阵切换到 5G 魔法网络——驿道坐标变了。正在进行的法术影像通话、远程法阵连接全部中断。能不能让驿道坐标不随移动而变?

移动 IPv6(MIPv6,RFC 6275) 的核心理念:

[归属网络]                     [外地网络]
─────────────────            ─────────────────
  归属代理(HA)                 外地代理
  维护 Home Address(HoA)       给移动节点分配 Care-of Address(CoA)
      ↑                               ↑
      │                               │
      └───────── 移动节点 ─────────────┘
             HoA 不变(归属地址)
             CoA 变化(当前实际位置)

移动 IPv6 vs 移动 IPv4 的简化:

│ 对比维度  │ 移动 IPv4                    │ 移动 IPv6                    │
│──────────│─────────────────────────────│─────────────────────────────│
│ 外地代理  │ 需要                        │ ❌ 不需要(IPv6 用路由优化)       │
│ 三角路由  │ 所有包经过归属代理            │ 可以优化:通信对端直接发到 CoA     │
│ 地址获得  │ 通过外地代理的 DHCP           │ SLAAC + 邻居发现               │
│ 安全    │ 需要外部认证                   │ IPsec 内置                    │
python
def mipv6_binding_update(home_addr: str, care_of_addr: str,
                          sequence: int = 1) -> dict:
    """模拟移动 IPv6 的绑定更新消息结构"""
    return {
        "type": "Binding Update",
        "home_address": home_addr,
        "care_of_address": care_of_addr,
        "sequence": sequence,
        "lifetime": 3600,       # 秒
        "flags": {
            "H": 1,             # 归属注册
            "A": 0,             # 需要确认
            "L": 1,             # 本地管理
        }
    }

bu = mipv6_binding_update(
    home_addr="2001:db8:1::100",
    care_of_addr="2001:db8:2::200"
)
for k, v in bu.items():
    print(f"{k:20s} = {v}")

实际部署中,MIPv6 并没有大规模普及——大多数移动网络选择用应用层解决连接迁移问题(WebSocket 重连、QUIC connection migration),而不是在网络层。


常见陷阱:IPv6 实战中的坑

坑 1:IPv4 地址硬编码

很多代码写着 127.0.0.1255.255.255.2550.0.0.0。这些在 IPv6 世界里要改:

python
# ❌ IPv4-only
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("0.0.0.0", 8080))

# ✅ 双栈兼容(Python 3.x 默认)
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
s.bind(("::", 8080))           # :: 绑定所有 IPv4 + IPv6
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)  # 双栈

配置文件里的 IP 地址也是重灾区——如果配置里写死了 server=192.168.1.100,迁移到纯 IPv6 网络时代码就废了。用域名。

坑 2::: 简写的理解错误

常见错误:

❌ 认为 ::1 == 1.0.0.0...   (错误)
✅ ::1 是 127 个 0 后面跟一个 1 = 环回地址

❌ 写 2001::db8::1  (两个 :: 无法确定)
✅ 2001:0:0:db8::1 或 2001:0:0:db8:0:0:0:1

❌ 在 URL 里写 http://[::1]:8080 时方括号忘了加
✅ 方括号是 RFC 3986 的要求:
   http://[2001:db8::1]:8080/path
   http://[::1]:8080

Link-Local 地址 fe80::/10 只能在同一条链路上使用。你不能把 fe80::1%eth0 写到 /etc/hosts 里。当你从一个网络迁移到另一个网络(比如从 Wi-Fi 切到以太网),Link-Local 地址会变(如果基于 EUI-64),因为接口不同。

python
# Link-Local 地址必须带 scope ID
# ✅ 正确的连接:
# ssh fe80::1%eth0         (Linux:%接口名)
# ssh fe80::1%en0          (macOS)
# ssh fe80::1%1            (Windows:%接口索引号)

# ❌ 错误的连接(没有 scope ID,系统不知道用哪个接口):
# ssh fe80::1

坑 4:防火墙阻断了 6to4 隧道

企业防火墙经常阻断 UDP 端口 3544(Teredo)和协议 41(6to4)。如果你的应用依赖隧道技术,在 NAT 后的企业网络里可能完全不可用。这也是双栈被推荐的根本原因——不需要猜防火墙会怎么处理封装。

坑 5:MTU 问题

IPv6 要求链路 MTU ≥ 1280 字节(IPv4 允许 68 字节)。如果路径上有一段 MTU=1280 以下,PMTUD 会失败——防火墙经常丢弃 ICMPv6 "Packet Too Big" 消息。结果是:连接可以建立,但大包发出去没回应,TCP 连接"悬挂"。

bash
# 检查路径 MTU(Linux)
ping6 -c 3 -M do -s 1452 ipv6.google.com
# -M do = 禁止分片(DF 位),如果 MTU 小于 1500 会提示

通关挑战

🗡 挑战 1:IPv6 地址算一算(5 分钟)

给定以下地址,写出它的简写形式和全写形式:

  1. 2001:0db8:0000:0000:0000:8a2e:0000:7334
  2. fe80:0000:0000:0000:021e:c2ff:fe12:3456
  3. FF02:0000:0000:0000:0000:0000:0000:0001

并判断每个地址的类型(Global/ULA/Link-Local/Multicast/Loopback)。

挑战 2:写一个 IPv6 双栈 Web 服务器(30 分钟)

写一个 Python HTTP 服务器,同时绑定 IPv4 和 IPv6:

python
# 提示:用 socket.AF_INET6 绑定 "::",并设置 IPV6_V6ONLY=0

然后用 curl -6curl -4 分别测试。

挑战 3:观察邻居发现

在 Linux 上执行:

bash
# 查看 IPv6 邻居表
ip -6 neigh show

# 用 tcpdump 看 NDP 消息
sudo tcpdump -i eth0 ip6 and icmp6

# 查看 IPv6 路由表
ip -6 route show

观察:

  • 邻居表里有几个条目?是 Reachable 还是 Stale 状态?
  • NDP 消息的类型是什么(NS/NA/RS/RA)?
  • 路由表里有没有默认路由(::/0)?

挑战 4:排查 IPv6 连接问题

你的同事说:"公司的网站只能通过 IPv4 访问。IPv6 连不上。"你怀疑是 AAAA 记录的问题或者防火墙的问题。用以下工具排查:

bash
# 1. 检查 DNS
dig AAAA yoursite.com +short

# 2. 检查 IPv6 连通性
ping6 yoursite.com

# 3. 检查端口可达性
nc -6 yoursite.com 443

# 4. 查看是否双栈
curl -6 -v https://yoursite.com
curl -4 -v https://yoursite.com

验收标准

完成本章后,你应当:

  • [ ] 说出 IPv4 地址耗尽的核心原因和三个主要过渡技术
  • [ ] 手绘 IPv6 固定 40 字节头部,说出删掉了什么字段
  • [ ] 读写 IPv6 地址并正确使用 :: 简写
  • [ ] 区分 Global Unicast、Unique Local、Link-Local 三种单播地址
  • [ ] 解释 NDP 如何替代 ARP(NS/NA 消息的交互过程)
  • [ ] 用一句话描述 SLAAC 的原理
  • [ ] 用 Python 写一个绑定 IPv6 的 socket 程序
  • [ ] 说出双栈和 NAT64 的核心区别

常见卡点

卡点原因解药
IPv6 地址太长了记不住你不必记,只要知道 2001:db8::/32 是文档地址段就够了实际用 DNS,没人手写 IPv6 地址
fe80:: 地址不能用 ping 测试Link-Local 需要 scope IDping6 fe80::1%eth0 带上接口
fe80:: 在 /etc/hosts 里不好使没有 scope ID 信息不用 hosts 文件,用 DNS
AAAA 记录查询失败,网站解析慢DNS64/NAT64 没配好dig AAAA + dig A 看哪个返回了
程序 bind "0.0.0.0" 在纯 IPv6 环境不工作硬编码了 IPv4 地址统一用 :: 并启用双栈
ip6tables 规则写了不生效忘记放行 ICMPv6(NDP 被阻断)必须 -p ipv6-icmp -j ACCEPT

现在不需要理解

  • SEND(Secure Neighbor Discovery):给 NDP 加上加密认证的扩展。知道 "NDP 可以被伪造" 就够了——具体实现属于安全章节。
  • 源地址选择算法(RFC 6724):当一个 IPv6 设备有多个地址时,系统怎么选择源地址。知道 "系统会自动选最佳源地址" 就够了。
  • DHCPv6:虽然 IPv6 有 SLAAC 可以免配,但企业环境仍然需要 DHCPv6 来分发 DNS 服务器、NTP 等参数。知道 "SLAAC 只给地址和路由,DHCPv6 可以给更多配置" 就够了。
  • IPsec 在 IPv6 中的强制要求:RFC 2460 曾要求所有 IPv6 实现支持 IPsec——但实际上是"应该"支持的,不是强制的。

旅人笔记

IPv6 不是一个 "IPv4 加强版"。它是一个全新的网络层协议的设计,汲取了 IPv4 几十年来所有的经验教训——删掉了校验和、删掉了路由器分片、删掉了广播、删掉了 Options 头部的可变长度问题。结果是:更小、更快、更清晰的协议。

但它最大的敌人不是技术,而是惰性——NAT 和 CIDR 把 IPv4 的寿命延长了二十年,导致双栈可能会伴随我们很久。好消息是,2026 年的今天,你手机自动获得的 IPv6 地址已经是全球可达的。端到端模型正在回归。

一句话消化本章:IPv6 用 128 位地址解决耗尽问题,用更简洁的头部设计提升性能,用 NDP + SLAAC 实现零配置的邻居发现和地址分配——最终目标就是让每台设备重新拥有一个全球可达的 IP。


→ 下一站预告

"网络实战与性能优化"

学完了 13 章网络知识——从物理层到传输层,从 IPv4 到 IPv6。但你有没有一个问题一直想问:"用户说慢——怎么系统地分析和优化?"

下一章,我们把所有知识变成一个工具箱:DNS 解析慢怎么办?TCP 连接建立为什么有延迟?TLS 握手能不能加速?CDN 到底是怎么让页面加载快 10 倍的?——全部用实战工具(curl、iperf、httpstat、ss)来回答。

Built with VitePress | Software Systems Atlas