mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5389 字
15 分钟
ARP与首跳路由:数据包找到出路
2022-03-03

你的主机要给 192.168.1.1 发一个数据包。IP 地址有了,目的端口有了,应用层的数据也装好了——但网卡不认 IP 地址。网卡只认 MAC 地址。以太网帧头里填的源和目的,是六字节的硬件地址,不是四字节的 IP。

这个鸿沟,就是 ARP 要填的坑。

本章从”IP 和 MAC 为什么不能统一”这个问题出发,逐步展开 ARP 协议的工作机制、路由表的查找逻辑,最终追踪一个数据包从应用程序的 send() 调用出发,穿过内核协议栈、经过 ARP 解析、封装成以太网帧、到达交换机、被转发到路由器的完整旅程。

一、问题:IP 地址与 MAC 地址的鸿沟#

1.1 两套地址,两个世界#

网络层用 IP 地址寻址,链路层用 MAC 地址寻址。这两套地址体系服务于不同的目标:

  • IP 地址是逻辑地址,标识”你在网络中的位置”。它随拓扑变化——你从家里换到公司,IP 地址就变了。IP 地址的核心功能是路由:让数据包跨越多个网络,从源端走到目的端。
  • MAC 地址是物理地址,标识”你是哪块网卡”。它烧录在网卡的 ROM 里,全球唯一(理论上)。MAC 地址的核心功能是局部交付:在同一个广播域内,把帧从一台主机送到另一台主机。

为什么不能只用 IP?因为以太网——以及几乎所有链路层技术——在物理层面只认硬件地址。交换机根据 MAC 地址转发帧,不解析 IP 头。你可以在以太网上跑 IPv4、IPv6 甚至 IPX,交换机统统不关心——它只看帧头里的 MAC 地址。

为什么不能只用 MAC?因为 MAC 地址是扁平的,没有层次结构。00:1a:2b:3c:4d:5e 这串数字不包含任何”你在哪个网络”的信息。路由器如果按 MAC 地址转发,就需要一张包含全世界每块网卡地址的转发表——这在规模上不可行。

Note

IP 地址的层次结构(网络号 + 主机号)使得路由表只需记录”网络号 → 下一跳”的映射,而非每台主机的映射。这是互联网能扩展到数十亿设备的关键设计。关于 IP 地址的层次结构,详见 IP地址与子网

1.2 发送数据包时的核心问题#

当主机 A 要向主机 B 发送 IP 数据包时,内核的发送流程大致如下:

  1. 查路由表,确定下一跳的 IP 地址
  2. 根据下一跳 IP,查找对应的 MAC 地址
  3. 用这个 MAC 地址封装以太网帧
  4. 网卡将帧发送到物理介质

第 2 步就是 ARP 的工作。ARP(Address Resolution Protocol,地址解析协议)解决的核心问题只有一句话:已知下一跳的 IP 地址,如何获得对应的 MAC 地址?

二、ARP 协议:IP 到 MAC 的翻译器#

2.1 ARP 解析的完整过程#

假设主机 A(IP: 192.168.1.100,MAC: aa:aa:aa:aa:aa:aa)要向网关路由器(IP: 192.168.1.1)发送数据包,但 A 的 ARP 缓存中没有 192.168.1.1 的 MAC 地址。

sequenceDiagram participant A as 主机A<br/>192.168.1.100 participant SW as 交换机 participant B as 网关路由器<br/>192.168.1.1 participant C as 主机C<br/>192.168.1.50 participant D as 主机D<br/>192.168.1.75 A->>SW: ARP请求(广播)<br/>谁有192.168.1.1?告诉192.168.1.100 SW->>B: 转发ARP请求 SW->>C: 转发ARP请求 SW->>D: 转发ARP请求 Note over C,D: IP不匹配,丢弃 B->>SW: ARP应答(单播)<br/>192.168.1.1的MAC是bb:bb:bb:bb:bb:bb SW->>A: 转发ARP应答 Note over A: 更新ARP缓存<br/>192.168.1.1 → bb:bb:bb:bb:bb:bb A->>SW: 以太网帧(单播)<br/>目的MAC: bb:bb:bb:bb:bb:bb SW->>B: 转发数据帧

整个过程的关键点:

  1. ARP 请求是广播帧——目的 MAC 填 ff:ff:ff:ff:ff:ff,同一广播域内所有主机都会收到
  2. ARP 应答是单播帧——只有被请求的主机回复,直接发给请求者
  3. 收到 ARP 请求的主机都会更新缓存——即使你没有应答,你也能从请求中提取”谁在问”的 IP-MAC 映射
  4. ARP 是无状态协议——没有握手、没有确认,请求和应答之间没有连接

2.2 ARP 报文格式#

ARP 报文直接封装在以太网帧中,以太网类型字段为 0x0806。报文本身非常简洁,总共 28 字节:

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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 硬件类型(htype) | 协议类型(ptype) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| hlen | plen | 操作码(opcode) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 发送方MAC地址(前4字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 发送方MAC(后2字节) | 发送方IP地址(前2字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 发送方IP(后2字节) | 目标MAC地址(前2字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 目标MAC地址(后4字节) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 目标IP地址 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

ARP 请求与应答使用相同的报文格式,区别只在操作码和填充内容:

字段ARP 请求ARP 应答
硬件类型(htype)1(以太网)1(以太网)
协议类型(ptype)0x0800(IPv4)0x0800(IPv4)
硬件地址长度(hlen)66
协议地址长度(plen)44
操作码(opcode)1(请求)2(应答)
发送方MAC主机 A 的 MAC网关的 MAC
发送方IP主机 A 的 IP网关的 IP
目标MAC00:00:00:00:00:00(未知)主机 A 的 MAC
目标IP网关的 IP主机 A 的 IP
以太网目的MACff:ff:ff:ff:ff:ff(广播)主机 A 的 MAC(单播)

注意一个容易忽略的细节:ARP 请求中目标 MAC 填全零(因为还不知道),但以太网帧头的目的 MAC 填全 F(广播)。这两层目的地址的含义不同——帧头的全 F 是告诉交换机”这个帧要发给所有人”,ARP 报文里的全零是告诉接收方”我不知道你的 MAC,请告诉我”。

2.3 用 tcpdump 抓 ARP 包#

# 在主机A上抓取ARP报文
sudo tcpdump -i eth0 -nn arp
# 输出示例:
# ARP, Request who-has 192.168.1.1 tell 192.168.1.100, length 28
# ARP, Reply 192.168.1.1 is-at bb:bb:bb:bb:bb:bb, length 28

加上 -e 选项可以看到以太网帧头:

sudo tcpdump -i eth0 -nn -e arp
# 输出示例:
# aa:aa:aa:aa:aa:aa > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806),
# length 42: Request who-has 192.168.1.1 tell 192.168.1.100
# bb:bb:bb:bb:bb:bb > aa:aa:aa:aa:aa:aa, ethertype ARP (0x0806),
# length 42: Reply 192.168.1.1 is-at bb:bb:bb:bb:bb:bb

注意帧长度是 42 字节——14 字节以太网帧头 + 28 字节 ARP 报文。但以太网要求最小帧长 64 字节,不足的部分由网卡自动填充。

2.4 ARP 缓存表#

每次 ARP 解析都需要广播一次请求,代价不小。所以主机把解析结果缓存起来,后续发送直接查缓存:

# 查看ARP缓存表
arp -a
# 输出示例:
# ? (192.168.1.1) at bb:bb:bb:bb:bb:bb [ether] on eth0
# ? (192.168.1.50) at cc:cc:cc:cc:cc:cc [ether] on eth0
# ? (10.0.0.1) at dd:dd:dd:dd:dd:dd [ether] on eth1

Linux 上更推荐使用 ip neigh 命令:

# 查看邻居表(ARP缓存的现代替代)
ip neigh show
# 输出示例:
# 192.168.1.1 dev eth0 lladdr bb:bb:bb:bb:bb:bb REACHABLE
# 192.168.1.50 dev eth0 lladdr cc:cc:cc:cc:cc:cc STALE
# 10.0.0.1 dev eth1 lladdr dd:dd:dd:dd:dd:dd DELAY

ip neigh 的输出比 arp -a 多了一个状态字段,这个状态来自 Linux 内核的邻居状态机:

  • REACHABLE:可达,缓存有效,可直接使用
  • STALE:过期,缓存超时但尚未验证,发送数据前需重新确认
  • DELAY:延迟等待,已发送单播探测,等待回应
  • PROBE:探测中,正在发送 ARP 请求
  • FAILED:解析失败

2.5 ARP 超时与更新#

ARP 缓存不是永久的。Linux 内核中,REACHABLE 状态的默认超时时间是 30 秒(由 gc_stale_time 控制),超时后转为 STALE。STALE 状态的条目不会立即删除——下次发送数据时,内核会先发一个单播 ARP 探测,如果对方回应则刷新缓存,否则进入完整的 ARP 解析流程。

# 手动删除ARP缓存条目(强制下次重新解析)
sudo arp -d 192.168.1.1
# 或者用ip命令
sudo ip neigh del 192.168.1.1 dev eth0
# 验证删除成功
ip neigh show | grep 192.168.1.1
# (无输出表示已删除)
Warning

删除 ARP 缓存会导致短暂的通信中断——在新的 ARP 解析完成之前,发往该 IP 的数据包无法封装以太网帧。在生产环境中操作前,确保你了解后果。

2.6 用 arping 主动探测#

arping 工具可以主动发送 ARP 请求,用于检测 IP 冲突或手动触发 ARP 解析:

# 向192.168.1.1发送ARP请求
sudo arping -I eth0 192.168.1.1
# 输出示例:
# ARPING 192.168.1.1 from 192.168.1.100 eth0
# Unicast reply from 192.168.1.1 [bb:bb:bb:bb:bb:bb] 0.695ms
# Unicast reply from 192.168.1.1 [bb:bb:bb:bb:bb:bb] 0.712ms
# Sent 2 probes (1 broadcast(s))
# Received 2 response(s)
# 检测IP冲突:如果收到多个不同MAC的应答,说明存在冲突
sudo arping -D -I eth0 192.168.1.100
# 返回0表示无冲突,返回1表示有冲突

三、ARP 的变体:免费 ARP 与代理 ARP#

3.1 免费 ARP(Gratuitous ARP)#

免费 ARP 是一种特殊的 ARP 请求:主机询问自己的 IP 地址对应的 MAC。看起来荒谬——自己问自己?但这个操作有两个实际用途:

地址冲突检测:主机配置 IP 地址时,发送免费 ARP 请求。如果网络中有另一台主机使用了相同的 IP,它会回应——这就检测到了 IP 冲突。Linux 在设置 IP 地址时会自动做这个检测。

通知 MAC 变更:当主机的 MAC 地址发生变化(比如更换网卡、高可用集群的 VIP 漂移),发送免费 ARP 可以让网络中其他主机更新它们的 ARP 缓存。否则,其他主机还会用旧的 MAC 地址发包,导致通信中断。

# 手动发送免费ARP
sudo arping -A -I eth0 -c 3 192.168.1.100
# -A: 使用ARP应答格式(而非请求)
# -c 3: 发送3次
# 这会广播"192.168.1.1的MAC是bb:bb:bb:bb:bb:bb"
# 所有收到此包的主机都会更新ARP缓存

免费 ARP 在高可用场景中特别重要。当 Keepalived 或 VRRP 执行主备切换时,新的主节点会立即广播免费 ARP,让交换机和其他主机将 VIP 的 MAC 地址更新为新主节点的 MAC,从而实现秒级切换。

3.2 代理 ARP(Proxy ARP)#

代理 ARP 让路由器代替远端主机应答 ARP 请求。考虑这个场景:主机 A(192.168.1.100/24)要访问主机 B(10.0.0.50),但 B 不在 A 的子网内。正常情况下,A 应该查路由表、找到默认网关、ARP 解析网关的 MAC。但如果 A 没有配置默认网关呢?

这时,路由器可以开启代理 ARP。当路由器收到 A 的 ARP 请求(“谁有 10.0.0.50?”),它发现 10.0.0.50 在自己的另一个接口可达,于是代替 B 回应:“10.0.0.50 的 MAC 是路由器自己的 MAC。“A 收到应答后,把数据包发给了路由器,路由器再转发给 B。

# 在Linux路由器上开启代理ARP
sudo sysctl -w net.ipv4.conf.eth0.proxy_arp=1
# 查看代理ARP状态
cat /proc/sys/net/ipv4/conf/eth0/proxy_arp
# 1 = 开启, 0 = 关闭

3.3 免费 ARP vs 普通 ARP vs 代理 ARP#

特性普通 ARP免费 ARP代理 ARP
发送方IP = 目标IP
操作码1(请求)/ 2(应答)1 或 22(应答)
应答者目标主机本身无人应答(或冲突主机应答)路由器代替目标主机
以太网目的广播(请求)/ 单播(应答)广播单播(给请求者)
核心用途解析 IP→MAC冲突检测 + MAC 变更通知代替远端主机应答
安全风险可被欺骗可被滥用

代理 ARP 看起来方便——主机不需要配置网关也能跨网段通信。但它有几个严重问题:

  1. 广播域扩大:每个跨网段通信都会触发 ARP 广播,增加网络负担
  2. 故障排查困难:ARP 缓存中远端主机的 MAC 实际上是路由器的 MAC,排查时容易混淆
  3. 子网规划失效:原本通过子网划分隔离的广播域被代理 ARP 打通了
  4. 安全风险:恶意主机可以伪造代理 ARP 应答,劫持流量
Note

现代网络中,代理 ARP 已不推荐使用。正确做法是给主机配置默认网关,让主机明确知道”出网段的流量发给路由器”。代理 ARP 主要存在于一些遗留网络和特定场景(如 OpenStack Neutron 的网络模型)中。

四、路由表:数据包的导航地图#

ARP 解决了”已知 IP 如何获取 MAC”的问题,但在此之前还有一个前置问题:数据包应该发给谁? 答案在路由表里。

4.1 路由表的结构#

# 传统格式查看路由表
route -n
# 输出示例:
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
# 10.0.0.0 192.168.1.1 255.255.0.0 UG 0 0 0 eth0
# 0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0
# 推荐的现代格式
ip route show
# 输出示例:
# default via 192.168.1.1 dev eth0
# 10.0.0.0/16 via 192.168.1.1 dev eth0
# 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100

每条路由记录包含以下关键字段:

  • 目的网络:数据包要去的网络地址
  • 下一跳/网关:数据包应该转发给谁(0.0.0.0 表示直连,不需要网关)
  • 子网掩码/前缀长度:决定这条路由的匹配范围
  • 出接口:从哪个网卡发出去
  • 度量值(Metric):相同目的网络有多条路由时,优先选度量值小的

4.2 最长前缀匹配#

路由表中可能有多条路由都能匹配目的地址。比如目的地址 10.0.1.50 同时匹配 10.0.0.0/1610.0.1.0/24,选哪条?

答案是最长前缀匹配(Longest Prefix Match):选掩码最长、最精确的那条。10.0.1.0/24 的前缀长度是 24,比 10.0.0.0/16 的 16 更长,所以选 /24 那条。

flowchart TD START["查找路由表<br/>目的地址: 10.0.1.50"] --> M1{"匹配<br/>10.0.1.0/24?"} M1 -->|是| L1["前缀长度: 24"] M1 -->|否| M2{"匹配<br/>10.0.0.0/16?"} M2 -->|是| L2["前缀长度: 16"] M2 -->|否| M3{"匹配<br/>0.0.0.0/0<br/>默认路由?"} M3 -->|是| L3["前缀长度: 0"] M3 -->|否| DROP["丢弃<br/>网络不可达"] L1 --> CMP{"比较前缀长度<br/>选最长"} L2 --> CMP L3 --> CMP CMP --> RESULT["选择10.0.1.0/24<br/>下一跳: 192.168.1.2"] style START fill:#e3f2fd,stroke:#1565c0 style RESULT fill:#e8f5e9,stroke:#2e7d32 style DROP fill:#fce4ec,stroke:#c62828

最长前缀匹配是路由查找的核心算法。它允许路由表中同时存在粗粒度和细粒度的路由——默认路由覆盖”所有其他”,而更具体的路由覆盖特定子网。这种分层设计让路由表既紧凑又灵活。

4.3 默认路由 0.0.0.0/0#

默认路由是前缀长度为 0 的路由——它匹配任何目的地址。当所有更具体的路由都不匹配时,数据包就走默认路由。对于大多数终端主机,路由表只有两条:

# 典型终端主机的路由表
ip route show
# default via 192.168.1.1 dev eth0 # 默认路由:其他都走网关
# 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 # 直连路由

直连路由是内核自动生成的——当你给网卡配置 IP 地址时,内核会自动添加一条指向该子网的路由。默认路由通常由 DHCP 下发,或者手动配置。

4.4 路由表的生成:静态配置 vs 动态协议#

特性静态路由动态路由(OSPF/BGP/EIGRP)
配置方式手动添加 ip route add路由协议自动学习
拓扑变化响应不自动更新,需人工修改自动收敛,秒级~分钟级
适用规模小型网络(几十条路由)大型网络(数千~数百万条路由)
资源消耗无额外开销CPU、内存、带宽开销
可靠性依赖管理员操作协议保证冗余和备份路径
典型场景末梢网络、默认路由企业骨干、ISP、数据中心
# 添加静态路由
sudo ip route add 10.0.0.0/16 via 192.168.1.1 dev eth0
# 删除静态路由
sudo ip route del 10.0.0.0/16
# 添加默认路由
sudo ip route add default via 192.168.1.1 dev eth0
# 持久化静态路由(Ubuntu/Debian)
# 写入 /etc/network/interfaces 或 Netplan 配置

4.5 一个路由查找的完整过程#

假设主机 A(192.168.1.100)要向 10.0.1.50 发送数据包,路由表如下:

default via 192.168.1.1 dev eth0
10.0.0.0/16 via 192.168.1.2 dev eth0
10.0.1.0/24 via 192.168.1.3 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100

查找过程:

  1. 目的地址 10.0.1.50,逐条匹配
  2. 192.168.1.0/24:不匹配(10 开头,不是 192.168.1)
  3. 10.0.1.0/24:匹配!前缀长度 24
  4. 10.0.0.0/16:也匹配,但前缀长度 16 < 24,不如上一条精确
  5. default:也匹配,前缀长度 0,最不精确
  6. 选择 10.0.1.0/24,下一跳 192.168.1.3

接下来,内核需要 ARP 解析 192.168.1.3 的 MAC 地址——这就回到了上一节的内容。

五、首跳:从主机到路由器#

5.1 完整链路追踪#

“首跳”指的是数据包从源主机出发的第一步——到达默认网关。这一步涉及应用层、内核协议栈、ARP、以太网封装、交换机转发等多个环节。追踪一个完整的 ping 10.0.0.1 的首跳过程:

flowchart LR subgraph 主机A["主机A (192.168.1.100)"] APP["应用层<br/>ping 10.0.0.1"] --> RT["查路由表<br/>10.0.0.1 → 默认网关<br/>192.168.1.1"] RT --> ARP["ARP解析<br/>192.168.1.1 →<br/>bb:bb:bb:bb:bb:bb"] ARP --> ENC["封装以太网帧<br/>src: aa:aa...<br/>dst: bb:bb..."] ENC --> NIC["网卡发送"] end subgraph 交换机["交换机"] SW["查MAC地址表<br/>bb:bb... → 端口3<br/>转发到端口3"] end subgraph 路由器["路由器 (192.168.1.1)"] RCV["接收帧<br/>检查目的MAC=自己"] --> DEC["解封装<br/>提取IP数据包"] DEC --> RRT["查路由表<br/>10.0.0.1 → 转发"] end NIC --> SW --> RCV style 主机A fill:#e3f2fd,stroke:#1565c0 style 交换机 fill:#fff3e0,stroke:#e65100 style 路由器 fill:#e8f5e9,stroke:#2e7d32

5.2 逐步拆解#

第 1 步:应用调用 send()

用户执行 ping 10.0.0.1,ping 程序构造 ICMP Echo Request,调用系统调用发送数据。

第 2 步:内核查路由表

内核收到目的地址 10.0.0.1,查找路由表:

default via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100

10.0.0.1 不在 192.168.1.0/24 内,匹配默认路由,下一跳为 192.168.1.1

第 3 步:ARP 解析网关 MAC

内核检查 ARP 缓存中是否有 192.168.1.1 的 MAC 地址。如果有,直接使用;如果没有,发起 ARP 解析(见第二节的过程)。

第 4 步:封装以太网帧

内核构造以太网帧:

字段说明
以太网目的 MACbb:bb:bb:bb:bb:bb网关的 MAC(ARP 解析得到)
以太网源 MACaa:aa:aa:aa:aa:aa主机 A 的 MAC
EtherType0x0800IPv4
IP 源地址192.168.1.100主机 A 的 IP
IP 目的地址10.0.0.1最终目的地(不是网关的 IP!)
IP TTL64初始 TTL

注意一个关键点:以太网帧的目的 MAC 是网关的,但 IP 包的目的地址是最终目的地。这就是”逐跳转发”——每一跳只关心下一跳的 MAC,IP 层的源和目的地址在整个传输过程中不变(NAT 除外)。

第 5 步:发送到交换机

网卡将帧编码为电信号(或光信号),通过网线发送到交换机。

第 6 步:交换机转发

交换机收到帧后,执行两个操作:

  1. 学习:记录源 MAC aa:aa:aa:aa:aa:aa 从端口 1 收到,更新 MAC 地址表
  2. 转发:查 MAC 地址表,bb:bb:bb:bb:bb:bb 在端口 3,将帧从端口 3 发出

如果 MAC 地址表中没有 bb:bb:bb:bb:bb:bb 的记录,交换机会将帧从所有端口(除接收端口外)泛洪——这就是广播域内交换机处理未知单播帧的方式。

第 7 步:路由器接收

路由器从端口 3 收到帧,检查目的 MAC 是自己的 MAC,接收此帧。解封装提取 IP 数据包,查路由表决定下一步转发。

5.3 用 Wireshark 抓包验证#

在主机 A 上用 Wireshark 抓取 ping 10.0.0.1 的首跳过程,可以看到以下关键帧:

# 先清空ARP缓存,强制触发ARP解析
sudo ip neigh del 192.168.1.1 dev eth0
# 开始抓包
sudo tcpdump -i eth0 -nn -e -vv
# 另一个终端执行ping
ping -c 1 10.0.0.1

抓包输出(简化):

# 帧1: ARP请求(广播)
aa:aa:aa:aa:aa:aa > ff:ff:ff:ff:ff:ff, ARP, Request who-has 192.168.1.1 tell 192.168.1.100
# 帧2: ARP应答(单播)
bb:bb:bb:bb:bb:bb > aa:aa:aa:aa:aa:aa, ARP, Reply 192.168.1.1 is-at bb:bb:bb:bb:bb:bb
# 帧3: ICMP Echo Request
aa:aa:aa:aa:aa:aa > bb:bb:bb:bb:bb:bb, IPv4, 192.168.1.100 > 10.0.0.1, ICMP Echo Request
# 帧4: ICMP Echo Reply(路由器转发回来)
bb:bb:bb:bb:bb:bb > aa:aa:aa:aa:aa:aa, IPv4, 10.0.0.1 > 192.168.1.100, ICMP Echo Reply

帧 1 和帧 2 是 ARP 解析过程,帧 3 和帧 4 是实际的 ICMP 通信。注意帧 3 的以太网目的 MAC 是网关的 bb:bb:bb:bb:bb:bb,而 IP 目的地址是 10.0.0.1——这就是”以太网层逐跳、IP 层端到端”的体现。

六、ARP 安全与防御#

6.1 ARP 欺骗原理#

ARP 协议设计时没有考虑安全——它信任所有收到的 ARP 报文。任何人都可以发送伪造的 ARP 应答,声称”某个 IP 的 MAC 是我的 MAC”,从而将流量劫持到自己这里。

sequenceDiagram participant A as 主机A<br/>192.168.1.100 participant C as 攻击者<br/>192.168.1.200 participant GW as 网关<br/>192.168.1.1 Note over A,GW: 正常状态:A的ARP缓存<br/>192.168.1.1 → GW的MAC C->>A: 伪造ARP应答<br/>192.168.1.1的MAC是攻击者的MAC Note over A: A更新ARP缓存<br/>192.168.1.1 → 攻击者的MAC A->>C: 发往192.168.1.1的流量<br/>实际到了攻击者 C->>GW: 攻击者转发流量到网关<br/>(中间人攻击) Note over A,GW: 攻击者可以:<br/>1. 窃听所有流量<br/>2. 篡改数据<br/>3. 丢弃数据包(拒绝服务)

ARP 欺骗之所以容易得手,是因为:

  1. ARP 是无状态协议——没有请求也可以发应答,主机无法区分合法应答和伪造应答
  2. ARP 缓存可被覆盖——收到新的 ARP 应答就更新缓存,即使旧条目还在有效期内
  3. ARP 没有认证机制——报文中没有任何字段可以验证发送者的身份

6.2 用 scapy 构造 ARP 包#

scapy 是一个强大的 Python 网络包操作库,可以用来构造和发送自定义 ARP 报文:

from scapy.all import *
# 构造ARP请求
arp_request = ARP(
pdst="192.168.1.1" # 目标IP
)
# scapy会自动填充:
# hwdst=00:00:00:00:00:00(未知)
# psrc=本机IP
# hwsrc=本机MAC
# op=1(请求)
# 发送并接收应答
ans, unans = srp(
Ether(dst="ff:ff:ff:ff:ff:ff") / arp_request,
timeout=2,
iface="eth0"
)
# 打印结果
for sent, received in ans:
print(f"{received.psrc} 的MAC地址是 {received.hwsrc}")
# 构造ARP应答(用于测试,勿用于攻击)
# 以下代码仅用于理解协议,实际使用可能违反法律
arp_reply = ARP(
op=2, # 应答
pdst="192.168.1.100", # 目标主机IP
hwdst="aa:aa:aa:aa:aa:aa", # 目标主机MAC
psrc="192.168.1.1", # 伪装的IP(网关)
hwsrc="cc:cc:cc:cc:cc:cc" # 攻击者的MAC
)

6.3 静态 ARP 绑定#

最简单的防御手段是静态绑定——手动指定 IP 和 MAC 的对应关系,禁止动态更新:

# 添加静态ARP条目
sudo arp -s 192.168.1.1 bb:bb:bb:bb:bb:bb
# 验证
arp -a | grep 192.168.1.1
# ? (192.168.1.1) at bb:bb:bb:bb:bb:bb [ether] PERM on eth0
# 注意 PERM 标记,表示永久条目
# 删除静态条目
sudo arp -d 192.168.1.1

静态绑定的缺点显而易见:不适合大型网络,MAC 地址变更时需要手动更新所有主机的绑定。它只适用于关键设备(网关、服务器)的防护。

6.4 DAI 动态 ARP 检测#

DAI(Dynamic ARP Inspection)是交换机层面的防御机制。它的核心思路是:交换机只转发合法的 ARP 报文

DAI 的工作流程:

  1. 交换机维护一个 DHCP Snooping 绑定表,记录每个端口通过 DHCP 获得的 IP-MAC 映射
  2. 当交换机收到 ARP 报文时,检查报文中的 IP-MAC 对是否在绑定表中
  3. 如果匹配,转发;如果不匹配,丢弃
# Cisco交换机配置DAI
ip arp inspection vlan 10
ip arp inspection validate src-mac dst-mac ip
# 信任端口(如连接路由器的端口)
interface GigabitEthernet0/1
ip arp inspection trust

DAI 的前提是网络使用 DHCP。对于使用静态 IP 的设备,需要手动配置允许列表。

6.5 为什么 ARP 是链路层安全的薄弱环节#

ARP 的安全问题不是实现缺陷,而是协议设计的固有局限。1982 年 RFC 826 定义 ARP 时,网络规模小、设备可信,安全不是优先考虑。ARP 的三个根本性弱点:

  1. 无认证:任何主机都可以发送 ARP 应答,接收方无法验证其真实性
  2. 可覆盖:新的 ARP 信息无条件覆盖旧信息,即使旧信息来自可信源
  3. 广播域内全局影响:一个伪造的 ARP 应答可以影响整个广播域的通信

这三个弱点叠加,使得 ARP 欺骗成为局域网攻击的常见手段。彻底解决需要部署 DAI 等交换机层面的防护,或者迁移到 IPv6——IPv6 用 NDP(Neighbor Discovery Protocol)替代 ARP,NDP 基于 ICMPv6,支持 IPsec 认证,安全性显著提升。

七、本章小结#

概念要点
IP 与 MAC 的鸿沟网络层用 IP 寻址(层次化,可路由),链路层用 MAC 寻址(扁平,局部交付),发送数据包时必须将 IP 映射到 MAC
ARP 协议请求广播、应答单播,28 字节报文,操作码区分请求(1)和应答(2),缓存超时约 30 秒
免费 ARP询问自己的 IP,用于地址冲突检测和高可用切换时的 MAC 变更通知
代理 ARP路由器代替远端主机应答 ARP,不推荐使用,应配置默认网关替代
路由表目的网络 + 下一跳 + 出接口 + 度量值,内核自动生成直连路由,默认路由匹配所有
最长前缀匹配多条路由匹配时选前缀最长的,实现粗细粒度路由共存
首跳过程send() → 查路由表 → ARP 解析 → 封装以太网帧 → 交换机转发 → 路由器接收
逐跳转发以太网目的 MAC 每跳变化,IP 目的地址端到端不变
ARP 欺骗伪造 ARP 应答劫持流量,根源是 ARP 无认证、可覆盖、广播域全局影响
防御手段静态 ARP 绑定(简单但不扩展)、DAI 动态检测(需交换机支持)、IPv6 NDP(根本解决)

本章追踪了数据包从应用层到路由器的第一步。下一章深入路由器内部——IP地址与子网,看看互联网的坐标系统如何设计、CIDR 如何让路由表可扩展。


参考#

  • RFC 826 — An Ethernet Address Resolution Protocol

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

ARP与首跳路由:数据包找到出路
https://blog.souloss.com/posts/internet-architecture/arp-and-first-hop-routing/
作者
Souloss
发布于
2022-03-03
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐