叙事密度:中 | 主语言: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 所有设计上的遗憾。
你的任务
学完本章,你应当:
- 说出 IPv4 耗尽的核心原因和 NAT 的代价
- 手绘 IPv6 首部:40 字节固定大小,为什么去掉校验和和分片字段
- 读写 IPv6 地址:冒号十六进制、:: 简写规则、前缀表示法
- 区分三种单播地址:Global Unicast → Unique Local → Link-Local
- 理解 NDP 替代 ARP 干了什么:邻居请求/广告、路由器请求/广告、重定向
- 解释 SLAAC 的原理:路由器通告 → 接口 ID 生成
- 用 Python 写一个绑 IPv6 地址的 socket 程序
- 对比双栈、隧道、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 头部对比:
| 特性 | IPv4 | IPv6 |
|---|---|---|
| 头部大小 | 20-60 字节(有 IHL 和 Options) | 固定 40 字节 |
| 地址长度 | 32 位 | 128 位 |
| 校验和 | 每跳重算 | 删掉了(依赖上层校验) |
| 分片 | 路由器可以做 | 发送端做(Path MTU Discovery) |
| Options | 头部内,导致变长 | 移出到扩展头部(Next Header 链) |
| IHL | 需要 | 删了(固定长度) |
| 流标签 | 新增(QoS 标记) |
用 Python 解析一个 IPv6 头部:
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 核心设计哲学:
删掉校验和:IPv4 的校验和每跳都得重算(因为 TTL 在变)。IPv6 说——数据链路层(以太网)已经有 CRC,上层(TCP/UDP)也有校验和。IP 层再算一次是多余的。省掉一个 CPU 周期,路由器更快。
不支持路由器分片:IPv4 的路由器可以在中间分片——但如果一个分片丢了,整个包废了。IPv6 强制发送端做 Path MTU Discovery,找到整条路径的 MTU,然后在这个尺寸以内发送。如果一定要发更大的包,在扩展头部里告知。
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.ByteBuffer或pcap4j才能解析裸 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:0101Python 验证你的 IPv6 地址对不对:
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::/7 | Unique Local Address(相当于 IPv4 私有地址 10.0.0.0/8 等) |
fe80::/10 | Link-Local Address(链路本地,仅在同一链路上有效) |
ff00::/8 | 多播地址 |
2002::/16 | 6to4 隧道地址(过渡技术,已废弃) |
前缀表示法(和 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)等本地协议。
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 个逻辑根服务器,但通过任播,每个逻辑根在全球有上百个镜像。你的请求会被路由到距离你最近的镜像。
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(邻居广告):
# 需要安装 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 好:
- 不是广播:NS 发到"请求节点多播地址"——只有跟这个 IP 相关的设备收到,不打扰全链路
- 有安全基础:可以用 SEND(Secure Neighbor Discovery, RFC 3971)做加密认证
- 统一框架:地址解析 + 路由器发现 + 重定向 + 地址自动配置,全部在 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:
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 定期随机生成,每过一段时间换一个。
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:6978Java 差异窗: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 的四倍)。
# 用 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 查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 库可以同时处理:
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):
# 允许已建立连接的返回流量
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 DROPIPv6 地址扫描的成本:
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.34def 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 内置 │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.1、255.255.255.255、0.0.0.0。这些在 IPv6 世界里要改:
# ❌ 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坑 3:Link-Local 地址范围
Link-Local 地址 fe80::/10 只能在同一条链路上使用。你不能把 fe80::1%eth0 写到 /etc/hosts 里。当你从一个网络迁移到另一个网络(比如从 Wi-Fi 切到以太网),Link-Local 地址会变(如果基于 EUI-64),因为接口不同。
# 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 连接"悬挂"。
# 检查路径 MTU(Linux)
ping6 -c 3 -M do -s 1452 ipv6.google.com
# -M do = 禁止分片(DF 位),如果 MTU 小于 1500 会提示通关挑战
🗡 挑战 1:IPv6 地址算一算(5 分钟)
给定以下地址,写出它的简写形式和全写形式:
2001:0db8:0000:0000:0000:8a2e:0000:7334fe80:0000:0000:0000:021e:c2ff:fe12:3456FF02:0000:0000:0000:0000:0000:0000:0001
并判断每个地址的类型(Global/ULA/Link-Local/Multicast/Loopback)。
挑战 2:写一个 IPv6 双栈 Web 服务器(30 分钟)
写一个 Python HTTP 服务器,同时绑定 IPv4 和 IPv6:
# 提示:用 socket.AF_INET6 绑定 "::",并设置 IPV6_V6ONLY=0然后用 curl -6 和 curl -4 分别测试。
挑战 3:观察邻居发现
在 Linux 上执行:
# 查看 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 记录的问题或者防火墙的问题。用以下工具排查:
# 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 ID | ping6 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)来回答。