9.1 IPv4 首部、CIDR 与最长前缀转发
IP 让数据报跨网络逐跳转发,却不保证送达、顺序、去重或保密。路由器根据目的地址查转发表,选择下一跳。本课先检查一个 IPv4 报文,再用地址集合解释 CIDR 和最长前缀匹配,避免把“路由表里有一行”当成通信已经成功。
1. IPv4 地址用于定位,不等于设备身份
地址配置在接口或逻辑端点上,可以随接入网络改变。一台机器可以有多个地址,多个用户也可能经 NAT 共用一个公网地址。私有地址、链路本地地址、环回、多播和任播部署,都不符合“一台机器一个全球唯一 IP”的简化模型。
文档示例使用 RFC 5737 保留的 192.0.2.0/24、198.51.100.0/24、203.0.113.0/24。这些地址用于说明和隔离实验,不应期待它们在公网可达。IP 地址本身也不能证明用户或设备可信。
2. IPv4 首部有哪些字段
最短首部是 20 字节,IHL 最大可表示 60 字节:
| 字段 | 作用 |
|---|---|
| Version | IPv4 的版本值为 4;值为 6 的报文应交给 IPv6 解析器 |
| IHL | 首部长度,以 32 位字为单位 |
| DSCP/ECN | 服务质量分类与显式拥塞通知 |
| Total Length | 首部加载荷的总字节数 |
| Identification/Flags/Fragment Offset | IPv4 分片与重组,见 8.3 |
| TTL | 路由转发时至少减 1,降至 0 时丢弃,避免无限循环 |
| Protocol | 载荷的协议编号,例如 ICMP=1、TCP=6、UDP=17 |
| Header Checksum | 只覆盖 IPv4 首部;TTL 改变后需要更新 |
| Source/Destination | 32 位源地址与目的地址 |
| Options + padding | 可选字段与填充,使首部按 32 位对齐 |
已经生成的 IPv4 数据报超过出口 MTU 时,要结合 DF 标志决定分片或丢弃并反馈 ICMP。TCP 分段是发送端在构造报文时的另一层处理,路由器不会替 TCP 重新分段。分片与路径 MTU 发现见8.3。
3. 检查 IHL 和 Total Length
下面是 Python 3 的教学解析器,输入从 IPv4 首部开始,不包含以太网头。保存为 ipv4_parse.py 后运行 python3 ipv4_parse.py。末尾构造 IHL=6、载荷为 ping 的测试字节;校验和暂填 0,专门测试长度检查,不是可直接发到网络上的有效 UDP 报文。
from dataclasses import dataclass
import ipaddress
import struct
@dataclass(frozen=True)
class IPv4Packet:
source: ipaddress.IPv4Address
destination: ipaddress.IPv4Address
protocol: int
ttl: int
header: bytes
payload: bytes
def parse_ipv4(raw: bytes) -> IPv4Packet:
if len(raw) < 20:
raise ValueError("shorter than the minimum IPv4 header")
version_ihl = raw[0]
version = version_ihl >> 4
ihl_words = version_ihl & 0x0F
if version != 4:
raise ValueError(f"expected IPv4, got version {version}")
if ihl_words < 5:
raise ValueError("invalid IPv4 IHL")
header_length = ihl_words * 4
if len(raw) < header_length:
raise ValueError("truncated IPv4 options/header")
total_length = struct.unpack("!H", raw[2:4])[0]
if total_length < header_length:
raise ValueError("total length is smaller than header length")
if len(raw) < total_length:
raise ValueError("truncated IPv4 packet")
return IPv4Packet(
source=ipaddress.IPv4Address(raw[12:16]),
destination=ipaddress.IPv4Address(raw[16:20]),
protocol=raw[9],
ttl=raw[8],
header=raw[:header_length],
payload=raw[header_length:total_length],
)
raw = struct.pack(
"!BBHHHBBH4s4s", 0x46, 0, 28, 1, 0, 64, 17, 0,
ipaddress.IPv4Address("192.0.2.1").packed,
ipaddress.IPv4Address("192.0.2.2").packed,
) + b"\x01" * 4 + b"ping"
packet = parse_ipv4(raw)
print(len(packet.header), packet.payload)
try:
parse_ipv4(raw[:-1])
except ValueError as error:
print(error)预期输出:
24 b'ping'
truncated IPv4 packet首部占 24 字节,固定跳过 20 字节会把选项误当载荷;截短一个字节则触发错误。生产解析器还需检查首部校验和、分片标志、选项和抓包截断策略。这里返回的是当前分片的载荷,没有执行重组,也没有验证 UDP/TCP。超出 Total Length 的尾随字节被忽略。
4. CIDR 前缀定义一个地址集合
192.0.2.0/24 固定前 24 位,剩余 8 位可变。IPv4 前缀长度范围为 0–32,不再依赖 A/B/C 类地址边界。下面的 Python 3 示例把主机地址归入所在子网:
import ipaddress
network = ipaddress.ip_network("192.0.2.14/28", strict=False)
print(network.network_address) # 192.0.2.0
print(network.broadcast_address) # 192.0.2.15
print(network.num_addresses) # 16
print(list(network.hosts()))前三行依次输出 192.0.2.0、192.0.2.15 和 16;最后一行列出 .1 到 .14 共 14 个主机地址。strict=False 允许输入带有主机位的地址,并归一化为网络前缀。
“每个子网都减掉网络地址和广播地址”有例外:
- RFC 3021 允许点到点链路把
/31的两个地址都用于端点; /32表示单个地址,常用于环回端点或主机路由;- 云平台还可能保留额外地址,可用数量要按平台规则计算。
IPv6 没有广播地址,不能直接套用 IPv4 的计算口诀。
5. 最长前缀匹配选择转发表项
假设转发信息库(FIB)包含以下三项:
0.0.0.0/0 -> gateway A
203.0.113.0/24 -> gateway B
203.0.113.128/25 -> gateway C203.0.113.200 同时匹配三项,最长的 /25 最具体,所以选择 C。没有更具体的匹配时,才使用默认路由 /0。下面是可直接运行的 Python 3 示例,断言通过时没有输出:
import ipaddress
routes = [
(ipaddress.ip_network("0.0.0.0/0"), "gateway-a"),
(ipaddress.ip_network("203.0.113.0/24"), "gateway-b"),
(ipaddress.ip_network("203.0.113.128/25"), "gateway-c"),
]
def longest_prefix_match(address: str) -> str:
target = ipaddress.ip_address(address)
matches = [(prefix.prefixlen, next_hop) for prefix, next_hop in routes
if target in prefix]
if not matches:
raise LookupError("no route")
return max(matches, key=lambda match: match[0])[1]
assert longest_prefix_match("203.0.113.200") == "gateway-c"
assert longest_prefix_match("198.51.100.8") == "gateway-a"示例对每个前缀只配置一个下一跳,线性扫描的复杂度为 O(n),不实现同前缀选优、策略路由或等价多路径。实际路由器先在路由信息库(RIB)中处理协议和静态配置产生的候选,再把活动转发表项写入 FIB。最长前缀匹配负责逐包查找,不代替 OSPF/BGP 的路由选优。
得到出口与下一跳后,在需要邻居解析的链路上再做 ARP 或 IPv6 的 NDP。跨互联网转发不会一直携带远端主机的 MAC 地址。
6. 路由聚合也是转发责任
192.0.2.0/25 与 192.0.2.128/25 可以聚合成 192.0.2.0/24,减少上游表项。聚合发布后,即使其中一半暂时不可达,上游仍会把流量送来。聚合点通常需要丢弃路由和成员路由的健康策略,避免把无法细分的流量沿默认路由又送回上游。
聚合只适用于对齐、连续的地址范围,任意四个 /24 不能直接变成一个 /22。ipaddress.collapse_addresses() 可以计算保持相同地址集合的最简前缀集合,但是否发布聚合,还要看实际可达性与策略。省下一行路由,转发责任并不会跟着消失。
7. 验收问题
- 解析器为什么检查 IHL,而不能固定跳过 20 字节?
/31和/32为什么不能套“减掉网络与广播地址”的口诀?- RIB 候选选优与 FIB 最长前缀查找有什么区别?
/26与/25都匹配目的地址时,哪一项更具体?- 聚合范围内某条成员路由消失后,流量仍可能被送到哪里?
下一课进入9.2 ICMP、ping 与 traceroute 的证据边界。