IP 层:路由、分片与 ICMP
#网络 · #IP · #路由 · #分片 · #ICMP · #MTU
IP(Internet Protocol)是 TCP/IP 协议栈的核心,负责将数据包从源主机路由到目标主机。它不保证可靠性(交给 TCP),不保证顺序,只做"尽力而为"的交付。理解 IP 的分片与重组、路由选择、ICMP 诊断,是网络排查的基础技能。
1. IP 数据报格式
IPv4 Header(20~60 字节):
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| IHL | DSCP/ECN | Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live (TTL) | Protocol| Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if IHL > 5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| 字段 | 大小 | 说明 |
|---|---|---|
| Version | 4 bit | 4 = IPv4, 6 = IPv6 |
| IHL | 4 bit | Header 长度(单位 4 字节),最小 5(20B) |
| Total Length | 16 bit | 数据报总长度(含 Header),最大 65535 |
| Identification | 16 bit | 分片标识:同一数据报的分片 ID 相同 |
| Flags | 3 bit | DF(不分片), MF(更多分片) |
| Fragment Offset | 13 bit | 分片偏移(单位 8 字节) |
| TTL | 8 bit | 跳数限制,每过一个路由器减 1,归零则丢弃 |
| Protocol | 8 bit | 上层协议:1=ICMP, 6=TCP, 17=UDP |
| Header Checksum | 16 bit | 仅校验 Header,每跳重新计算(TTL 会变) |
| Source/Dest IP | 32 bit | IPv4 地址 |
2. IP 分片与重组
2.1 为什么需要分片
链路层 MTU(Maximum Transmission Unit)限制:
- 以太网 MTU = 1500 字节
- IP 数据报 > MTU → 必须分片
例:发送 4000 字节的数据报,经过 MTU=1500 的链路:
原始数据报:4000B = 20B(Header) + 3980B(Payload)
↓
分片 1: ID=x, Offset=0, MF=1, Length=1500 (20+1480)
分片 2: ID=x, Offset=185, MF=1, Length=1500 (20+1480)
分片 3: ID=x, Offset=370, MF=0, Length=1040 (20+1020)
↓
目标主机重组:按 Offset 拼接2.2 分片相关字段
c
// Identification: 标识同一数据报,各分片相同
// Flags:
// bit 0: Reserved
// bit 1: DF (Don't Fragment) — 置 1 则路由器不分片,返回 ICMP 差错
// bit 2: MF (More Fragments) — 1 表示后面还有分片
// Fragment Offset: 本分片在原始数据报中的位置(8 字节为单位)
// 例:Offset=185 → 实际偏移 = 185 × 8 = 1480 字节2.3 Path MTU Discovery
工作原理:
1. 发送方设置 DF=1,发送数据报
2. 路由器发现 MTU 不够 → 丢弃 → 返回 ICMP "Fragmentation Needed"
(ICMP 消息中附带该链路的 MTU)
3. 发送方降低包大小,重试
4. 重复直到到达目的地
实现:
- IPv4: Path MTU Discovery (RFC 1191),可选
- IPv6: 强制支持,路由器永远不分片(由发送方负责)2.4 实战查看 MTU
bash
# 查看接口 MTU
ip link show eth0 | grep mtu
# 测试路径 MTU(发送 DF=1 的包,逐步调整大小)
ping -M do -s 1472 8.8.8.8 # -M do = DF=1, -s = payload 大小
# 1472 + 20(IP) + 8(ICMP) = 1500(标准以太网 MTU)
# traceroute 同时测 MTU
tracepath 8.8.8.8 # 自动探测路径 MTU3. IP 路由
3.1 路由表
bash
# Linux 路由表
ip route show
# default via 192.168.1.1 dev eth0 ← 默认网关
# 192.168.1.0/24 dev eth0 proto kernel ← 直连网络
# 10.0.0.0/8 via 192.168.1.254 dev eth0 ← 到 10.x 走 192.168.1.2543.2 路由选择算法
收到 IP 数据报 → 查路由表:
1. 最长前缀匹配(Longest Prefix Match)
- 目标 10.1.2.3 命中 10.1.0.0/16 和 10.1.2.0/24
- 选择 /24(前缀更长 → 更具体)
2. 如果多处匹配:
- Metric 最小的路由优先
- 等 Metric 则 ECMP(多路负载均衡)
3. 目的地址分类:
127.0.0.0/8 → lo(本地回环)
224.0.0.0/4 → 多播
169.254.0.0/16 → link-local(DHCP 失败时自动分配)3.3 策略路由
bash
# 多出口场景:不同源 IP 走不同出口
# /etc/iproute2/rt_tables
echo "200 isp1" >> /etc/iproute2/rt_tables
echo "201 isp2" >> /etc/iproute2/rt_tables
ip route add default via 1.1.1.1 table isp1
ip route add default via 2.2.2.1 table isp2
# 策略:来自 192.168.1.0/24 走 isp1
ip rule add from 192.168.1.0/24 table isp1
# 来自 192.168.2.0/24 走 isp2
ip rule add from 192.168.2.0/24 table isp24. ICMP(Internet Control Message Protocol)
4.1 报文类型
| Type | Code | 含义 | 示例场景 |
|---|---|---|---|
| 0 | 0 | Echo Reply | ping 回复 |
| 3 | 0 | Network Unreachable | 无路由 |
| 3 | 1 | Host Unreachable | ARP 失败 |
| 3 | 3 | Port Unreachable | UDP 端口未监听 |
| 3 | 4 | Fragmentation Needed (DF set) | Path MTU Discovery |
| 4 | 0 | Source Quench(已废弃) | 拥塞通知 |
| 5 | 0/1 | Redirect | 更优路径通知 |
| 8 | 0 | Echo Request | ping 请求 |
| 11 | 0 | TTL Exceeded | traceroute |
| 11 | 1 | Fragment Reassembly Time Exceeded | 分片重组超时 |
4.2 ping 原理
bash
ping -c 3 8.8.8.8
# 发送 ICMP Echo Request (Type 8)
# 期待 ICMP Echo Reply (Type 0)
# 计算 RTTgo
// Go 中 ping 的实现(使用 ICMP,需 root/特权)
import "golang.org/x/net/icmp"
import "golang.org/x/net/ipv4"
conn, _ := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte("HELLO"),
},
}
b, _ := msg.Marshal(nil)
conn.WriteTo(b, &net.IPAddr{IP: net.ParseIP("8.8.8.8")})4.3 traceroute 原理
bash
traceroute 8.8.8.8
# 原理:利用 TTL 递增
# TTL=1 → 第一跳路由器收到,TTL 减为 0 → 丢弃 → 返回 ICMP Time Exceeded → 知道第一跳 IP
# TTL=2 → 第二跳返回 Time Exceeded → 知道第二跳 IP
# ...重复直到到达目标
# 每跳发 3 个包(测 RTT 波动)
# 如果某跳显示 * * *:可能路由器不回复 ICMP(防火墙)5. ARP(Address Resolution Protocol)
5.1 工作原理
场景:192.168.1.10 要发数据给 192.168.1.20
知道目标 IP,但需要目标 MAC
ARP 流程:
1. 192.168.1.10 广播 ARP Request:
"Who has 192.168.1.20? Tell 192.168.1.10"
2. 192.168.1.20 单播 ARP Reply:
"192.168.1.20 is at aa:bb:cc:dd:ee:ff"
3. 192.168.1.10 缓存到 ARP 表5.2 ARP 缓存
bash
# 查看 ARP 表
arp -n
ip neigh show
# 条目状态:
# REACHABLE — 正常可用
# STALE — 超时未使用,下次使用前需确认
# DELAY — 正在等待确认
# PROBE — 正在探测
# FAILED — 不可达
# 手动操作
arp -d 192.168.1.20 # 删除条目
arp -s 192.168.1.20 aa:bb:cc:dd:ee:ff # 添加静态条目5.3 ARP 欺骗
攻击者向受害者发送伪造的 ARP Reply:
"网关 IP 的 MAC 地址 = 攻击者的 MAC"
→ 受害者流量经过攻击者 → 中间人攻击(MITM)
防护:
- 交换机端口安全(Port Security)
- 动态 ARP 检测(DAI)
- 静态 ARP 绑定6. IPv6 对比
| 维度 | IPv4 | IPv6 |
|---|---|---|
| 地址长度 | 32 bit(4.3×10⁹) | 128 bit(3.4×10³⁸) |
| Header 长度 | 20~60 字节 | 40 字节(固定) |
| 分片 | 路由器可分片 | 路由器不可分片(端到端) |
| Checksum | 有(每跳计算) | 无(上层协议负责) |
| 广播 | 有 | 无(用多播替代) |
| ARP | 需要 | 不需要(用 NDP 替代) |
| NAT | 普遍使用 | 不需要(地址充足) |
| 地址表示 | 192.168.1.1 | 2001:db8::1 |
bash
# IPv6 邻居发现(NDP — 替代 ARP)
ip -6 neigh show
# ping IPv6
ping6 2001:4860:4860::8888 # Google DNS7. 常见网络问题诊断
7.1 排查流程
mermaid
flowchart TB
A["网络不通"] --> B["ping 127.0.0.1<br/>(本机协议栈)"]
B -->|"通"| C["ping 本机 IP<br/>(网卡驱动)"]
B -->|"不通"| B1["TCP/IP 栈问题"]
C -->|"通"| D["ping 网关<br/>(局域网)"]
C -->|"不通"| C1["网卡/IP配置问题"]
D -->|"通"| E["ping 8.8.8.8<br/>(互联网)"]
D -->|"不通"| D1["交换机/ARP/VLAN问题"]
E -->|"通"| F["ping google.com<br/>(DNS)"]
E -->|"不通"| E1["路由器/ISP/WAN 问题"]
F -->|"不通"| F1["DNS 问题"]
F -->|"通"| G["网络正常,检查应用层"]7.2 常用命令速查
bash
# IP 配置
ip addr show # 所有接口 IP
ip link set eth0 up/down # 启用/禁用接口
# 路由
ip route show # 路由表
ip route get 8.8.8.8 # 查看到目标的实际路由
# ARP
ip neigh show # ARP 表
# 连通性
ping -c 3 8.8.8.8
traceroute 8.8.8.8
tracepath 8.8.8.8 # 同时测 MTU
# 抓包
tcpdump -i eth0 host 8.8.8.8 and icmp
tcpdump -i eth0 arp
# 查看丢包
netstat -s | grep -i "packet receive errors"
ip -s link show eth0 # 接口统计(丢包/错误)7.3 常见问题与对策
| 现象 | 可能原因 | 诊断 |
|---|---|---|
| ping 网关不通 | IP 冲突 / ARP 问题 | arp -a 检查 / tcpdump arp |
| 丢包(间歇性) | MTU 问题 / 网卡故障 | tracepath 测 MTU / ip -s link 看错误 |
| 延迟抖动 | 路由变化 / 拥塞 | mtr 8.8.8.8 持续监控 |
| 部分网站不通 | MTU 黑洞 | 降低 MTU / clamp mss to pmtu |
| TTL exceeded | 路由环路 | traceroute 看跳数 |
8. 路由协议简介
IP 路由表不是凭空产生的——它通过路由协议动态学习和更新。理解路由协议是理解"互联网如何联通"的关键。
8.1 路由协议分层
mermaid
flowchart TB
subgraph IGP["IGP: 内部网关协议 — 同一 AS 内"]
RIP["RIP: 距离矢量<br/>跳数最小 → Bellman-Ford"]
OSPF["OSPF: 链路状态<br/>最短路径 → Dijkstra"]
IS-IS["IS-IS: 链路状态<br/>运营商常用"]
end
subgraph EGP["EGP: 外部网关协议 — AS 之间"]
BGP["BGP-4: 路径矢量<br/>策略路由, 非最短路径"]
end
EGP <-->|"AS 边界路由器"| IGP8.2 三大协议核心对比
| 维度 | RIP | OSPF | BGP |
|---|---|---|---|
| 算法 | Bellman-Ford(D-V) | Dijkstra(SPF) | Path Vector |
| 度量 | 跳数(最大 15) | 链路成本(带宽) | AS-PATH 长度 + 策略 |
| 收敛速度 | 慢(计数到无穷) | 快(洪泛 LSA) | 慢(策略评估) |
| 适用规模 | 小型网络 | 中型到大型企业 | 互联网骨干 |
| 可靠性 | ❌ 路由环路风险 | ✅ 无环路 | ✅ AS-PATH 防环路 |
| 策略控制 | ❌ 无 | 有限 | ✅✅ 强大的策略引擎 |
| 协议类型 | UDP 520 | IP 协议 89 | TCP 179 |
8.3 OSPF 工作原理
OSPF 的核心是维护一个全网拓扑图(LSDB),然后对每个目的地
运行 Dijkstra 计算最短路径树。
1. Hello 协议: 发现邻居,选举 DR/BDR(广播网络)
2. LSA 洪泛: 链路状态变化时,洪泛 LSA 到整个区域
3. SPF 计算: 每个路由器独立运行 Dijkstra
4. 路由表注入: 将计算结果写入转发表
区域划分:
Backbone (Area 0) ← 必须存在,连接所有其他区域
Stub Area ← 不接收外部路由,用默认路由替代
NSSA ← 允许注入有限的外部路由
为什么需要区域?
- 减少 LSA 洪泛范围
- 减少 LSDB 大小
- 减少 SPF 计算开销8.4 BGP 的路径选择
BGP 不是选"最短路径"——它选"最优路径",按以下优先级(简化):
1. 最高 Local Preference(本地优先级)
2. 最短 AS-PATH(经过的 AS 最少)
3. 最低 Origin 类型(IGP < EGP < Incomplete)
4. 最低 MED(Multi-Exit Discriminator)
5. eBGP 优先于 iBGP
6. 最低 IGP 度量到下一跳
7. 最低 Router ID
这解释了为什么两个国家之间的流量可能绕半个地球——
BGP 更关心策略(省钱、合规),而非延迟。9. NAT 穿透详解
NAT 让内网主机共享一个公网 IP 访问互联网。但 P2P、VoIP、游戏联机等场景需要"穿透"NAT。
9.1 NAT 类型(RFC 4787)
| 类型 | 映射行为 | 过滤行为 | P2P 穿透难度 |
|---|---|---|---|
| Full Cone | 内网(IP:Port)→固定外网(IP:Port) | 无过滤 | ⭐ 最简单 |
| Restricted Cone | 同上 | 只接受该主机发过的 IP | ⭐⭐ |
| Port Restricted Cone | 同上 | 只接受该主机发过的 IP:Port | ⭐⭐⭐ |
| Symmetric NAT | 内网(IP:Port)→不同外网 Port(按目标不同) | 同 Port Restricted | ❌ 最难 |
9.2 STUN / TURN / ICE
mermaid
flowchart TB
A["A (NAT 后)"] -->|"1. 发到 STUN 服务器"| STUN["STUN Server<br/>返回 A 的公网 IP:Port"]
B["B (NAT 后)"] -->|"2. B 也获取公网地址"| STUN
A -->|"3. 尝试直连 B 的公网地址"| NAT_B["B 的 NAT"]
NAT_B -->|"成功 (Cone NAT)"| B
NAT_B -->|"失败 (Symmetric NAT)"| TURN["TURN Server<br/>中转流量 (最坏方案)"]
TURN --> B
style STUN fill:#4CAF50,color:#fff
style TURN fill:#FF9800,color:#fff- STUN:告诉客户端"你在公网上长什么样",免费但只能穿透 Cone NAT
- TURN:中继服务器,总能通但带宽成本高
- ICE:综合 STUN + TURN,优先直连,失败回退中继
WebRTC 使用 ICE 框架,浏览器内置 STUN/TURN 客户端。
10. iptables NAT 与 Go ICMP ping 实战
iptables NAT 完整配置
bash
# 1. 启用 IP 转发
sysctl -w net.ipv4.ip_forward=1
# 2. SNAT — 内网共享公网 IP 上网
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
# 3. DNAT — 端口映射(公网 80 → 内网 10.0.0.5:8080)
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 \
-j DNAT --to-destination 10.0.0.5:8080
# 4. 保存规则(持久化)
iptables-save > /etc/iptables/rules.v4
# 5. 查看 NAT 表规则
iptables -t nat -L -n -v
# 6. 查看当前 NAT 连接跟踪
conntrack -L -n
conntrack -L -n | wc -l # 连接跟踪表大小bash
# 排查: 策略路由 + NAT 的包流向
# 1. 查看包走哪条路由
ip route get 8.8.8.8 from 192.168.1.10 iif eth0
# 2. 跟踪 NAT 转换
conntrack -E -p tcp --dport 80
# 3. iptables 日志追踪
iptables -t nat -A PREROUTING -p tcp --dport 80 -j LOG --log-prefix "DNAT: "
dmesg | tailGo ICMP ping 完整实现
go
package main
import (
"fmt"
"net"
"os"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
func ping(addr string) error {
// 监听 ICMP
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return fmt.Errorf("listen: %w (需要 root 权限)", err)
}
defer conn.Close()
dst, err := net.ResolveIPAddr("ip4", addr)
if err != nil {
return err
}
for seq := 0; seq < 4; seq++ {
// 构造 ICMP Echo Request
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: seq,
Data: []byte("ping"),
},
}
b, _ := msg.Marshal(nil)
start := time.Now()
_, err := conn.WriteTo(b, dst)
if err != nil {
return err
}
// 读取 Echo Reply
reply := make([]byte, 1500)
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
n, peer, err := conn.ReadFrom(reply)
if err != nil {
fmt.Printf("seq=%d timeout\n", seq)
continue
}
rtt := time.Since(start)
rm, _ := icmp.ParseMessage(1, reply[:n])
echo := rm.Body.(*icmp.Echo)
fmt.Printf("seq=%d from=%s rtt=%v ttl=?\n",
echo.Seq, peer.String(), rtt)
}
return nil
}| 系统调用 | 说明 |
|---|---|
icmp.ListenPacket("ip4:icmp") | 创建原始 ICMP socket,需 root/CAP_NET_RAW |
ipv4.ICMPTypeEcho | Type=8 Echo Request |
ipv4.ICMPTypeEchoReply | Type=0 Echo Reply |
SetReadDeadline | 每次等待最多 3s |
登录后即可发表评论 👇