🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。
🔥 本文定位:认识以太网帧之后,继续沿着"下一跳 IP → ARP 请求 → ARP 应答 → 邻居缓存 → 数据发送"走一遍,再看错误映射怎样把流量引向第三台设备。
💡 学习目标:能手填一组 ARP 请求与应答字段;分清以太网地址与 ARP 内部地址;读懂 Linux 邻居状态;解释双向欺骗、中间人转发与断网之间的关系。
📌 阅读范围:以同一 VLAN 内的 Ethernet II、IPv4 单播为主。基础例子不含 NAT、代理 ARP、隧道和网关冗余;涉及这些情况时单独说明。地址、抓包输出和流量路径均为教学示意,离线实验不会发送网络报文。
假设你正在访问一台远端服务器。路由表已经告诉内核:这份 IP 报文应该从 eth0 发出,下一跳是 192.168.10.1。
到这里,事情还没有完成。因为以太网帧的目的地址需要填 MAC 地址 ,而不是 192.168.10.1 这串 IP 地址。
内核会继续问:谁知道 192.168.10.1 对应哪一个 MAC?
ARP 就是在回答这个问题。但新的问题随之而来:
- 还不知道对方 MAC,第一份请求怎么发出去?
- 以太网首部已经写了 MAC,ARP 里面为什么还要写一次?
- 收到一句"这个 IP 对应我的 MAC",主机凭什么相信?
- 如果有人把网关 IP 对应的 MAC 换成自己的,IP 目的地址明明没变,流量为什么会绕路?
- 绕到了第三台主机,是马上断网,还是仍然可以正常访问?
把正常地址解析和错误地址解析放在同一套地址里观察,这些问题就能连起来。
文章目录
- [先决定问谁:ARP 解析的是当前下一跳](#先决定问谁:ARP 解析的是当前下一跳)
- [拆开 ARP 报文:外层负责送达,内层描述映射](#拆开 ARP 报文:外层负责送达,内层描述映射)
- 完整走一次:从广播请求到真正发送数据
- 邻居缓存不是一张永远不变的通讯录
- [Linux 观察:把路由、缓存和抓包对应起来](#Linux 观察:把路由、缓存和抓包对应起来)
- [ARP 欺骗:改掉映射,改变当前帧的收件人](#ARP 欺骗:改掉映射,改变当前帧的收件人)
- 单向、双向与转发:为什么有时绕路,有时断网
- 离线实验:解析报文,再模拟缓存被改写
- 发现与防护:把异常线索串成证据
- 常见误区与自测:把几个边界真正分清
一、先决定问谁:ARP 解析的是当前下一跳
1.1 先约定整篇文章的地址
为了让每一张图、每一个字段都能对应起来,本文统一使用下面这组地址。
| 设备 | 角色 | IPv4 地址 | MAC 地址 | 图中简称 |
|---|---|---|---|---|
| A | 普通主机 | 192.168.10.10/24 |
02:00:00:00:00:0a |
MAC_A |
| B | 同一子网的主机 | 192.168.10.20/24 |
02:00:00:00:00:0b |
MAC_B |
| R | 默认网关的 LAN 接口 | 192.168.10.1/24 |
02:00:00:00:00:01 |
MAC_R |
| M | 后文模拟的第三台主机 | 192.168.10.66/24 |
02:00:00:00:00:66 |
MAC_M |
| S | 远端服务器 | 203.0.113.80 |
当前局域网不需要知道 | --- |
A、B、R、M 位于同一个以太网广播域,由交换机连接。203.0.113.80 属于文档示例地址,只用于解释跨网段路径,不是供读者直接连通测试的公网服务。
这些 02: 开头的 MAC 是教学用的本地管理单播地址。这里需要记住的是它们彼此不同,而不是把 MAC 当成永不变化、绝对可信的设备身份证。
1.2 同网段:问 B;跨网段:问网关 R
在上述普通路由配置下,A 访问 B 时,内核选中直连路由,下一跳就是 B。于是要解析:
text
192.168.10.20 → MAC_B
A 访问 S 时,内核选中经由 R 的路由,当前链路应该交给 R。此时解析的是:
text
192.168.10.1 → MAC_R
这时发出的 IPv4 报文和以太网帧分别指向:
text
IP 目的地址:203.0.113.80
以太网目的 MAC:02:00:00:00:00:01

所以,"已知目的 IP,查它的 MAC"必须补上前提:这个 IP 是当前链路上需要交付的下一跳。 访问远端网站时,A 通常不会广播询问网站服务器的 MAC。
"是否同网段"是便于入门的描述,真正执行时应以路由查找结果为准。策略路由、主机路由、VPN 和代理 ARP 都可能让实际行为超出一个 /24 示例;排查时先看 ip route get,不要仅凭 IP 前三段相同就下结论。
1.3 ARP 的广播不会随着 IP 路由一路传到服务器
普通二层交换机会在相应 VLAN 内转发广播,但路由器不会把这份 ARP 广播当成普通 IP 报文继续路由到远端。
如果 R 的出接口也使用需要 ARP 的以太网链路,它会按自己的路由选择,在那条链路上解析自己的下一跳。这是另一次本地解析,和 A 查询 R 的过程是两回事。
还有两个范围要分清:ARP 在本文中服务于 IPv4;IPv6 使用邻居发现机制。ARP 也不是 DNS,DNS 解决名称到地址等记录的查询,不负责告诉本地网卡下一帧该交给哪个 MAC。
二、拆开 ARP 报文:外层负责送达,内层描述映射
2.1 ARP 不是"放在 IP 包里的一个小协议"
在本文的 Ethernet II 场景中,ARP 直接作为以太网帧的载荷:
text
以太网首部 | ARP 报文 | 必要的填充 | FCS
它前面没有 IPv4 首部,也没有 TCP/UDP 首部,因此不谈 TCP 端口、UDP 端口或 IP TTL。
最容易混淆的是两个"类型"字段:
| 位置 | 字段 | 本例值 | 回答的问题 |
|---|---|---|---|
| 以太网首部 | EtherType | 0x0806 |
这帧装的是哪种协议?装的是 ARP |
| ARP 内部 | Protocol Type,PTYPE | 0x0800 |
正在解析哪种协议地址?解析的是 IPv4 地址 |
如果一帧外层 EtherType 是 0x0800,表示它的以太网载荷是 IPv4;如果它是 0x0806,而里面的 PTYPE 是 0x0800,则表示"这是一份解析 IPv4 地址的 ARP 报文"。两个字段在不同层次,不能交换。

2.2 28 字节怎样组成
对 Ethernet + IPv4 这个组合,ARP 报文有以下字段。
| 字段 | 长度 | 本例含义与常见取值 |
|---|---|---|
| HTYPE | 2 字节 | 硬件类型,Ethernet 为 1 |
| PTYPE | 2 字节 | 协议类型,IPv4 为 0x0800 |
| HLEN | 1 字节 | 硬件地址长度,MAC 为 6 |
| PLEN | 1 字节 | 协议地址长度,IPv4 为 4 |
| OPER | 2 字节 | 操作码,请求为 1,应答为 2 |
| SHA | 6 字节 | Sender Hardware Address,发送方硬件地址 |
| SPA | 4 字节 | Sender Protocol Address,发送方协议地址 |
| THA | 6 字节 | Target Hardware Address,目标硬件地址 |
| TPA | 4 字节 | Target Protocol Address,目标协议地址 |
总长度是:
text
2 + 2 + 1 + 1 + 2 + 6 + 4 + 6 + 4 = 28 字节
更一般地说,ARP 的长度由地址长度决定,可以写成 8 + 2 × HLEN + 2 × PLEN。因此"ARP 永远是 28 字节"不够严谨;这里的 28 来自 Ethernet 和 IPv4 这两个条件。
在无 VLAN 标签的普通以太网里,14 字节首部加上 28 字节 ARP 只有 42 字节,还需要填充到最小帧要求。按包含 FCS、不包含前导码与帧开始定界符的口径计算:
text
14 字节首部 + 28 字节 ARP + 18 字节填充 + 4 字节 FCS = 64 字节
抓包是否看到填充和 FCS,取决于抓包位置、驱动及硬件。看到 length 28 时,先确定工具描述的是 ARP 长度,别直接认定整条线上只传输了 28 字节。
2.3 为什么 MAC 地址出现了两遍,却不总是一样
以太网首部解决"把这一帧交到哪里";ARP 内部解决"哪一个协议地址对应哪一个硬件地址"。ARP 自己携带地址类型和地址长度,可以适用于不止一种链路环境。
在普通 ARP 请求中,发送方尚不知道目标 MAC,常见填写方式是:
text
以太网目的 MAC = ff:ff:ff:ff:ff:ff # 让本广播域内的设备收到
ARP 的 THA = 00:00:00:00:00:00 # 此时未知,不靠它投递
ARP 的 TPA = 192.168.10.20 # 真正要询问的目标 IP
这里两处"目的 MAC"显然不同。外层广播地址是投递方式,内层全零目标硬件地址表示还未获知目标。 不要把全零 THA 当作广播地址,也不要认为网卡会根据 ARP 内部的 THA 决定是否接收这一帧。
普通应答中,双方地址已经明确,外层地址与内层地址通常对应一致。但字段相同只是常见报文形态,不构成身份认证;伪造者完全可以把外层源 MAC 与 SHA 同时填成自己的 MAC。
字段定义与基础收发算法可对照 RFC 826。
三、完整走一次:从广播请求到真正发送数据
下面让 A 向 B 发送一个普通 IPv4 报文,并假设 A 当前没有可用的 B 邻居映射。
3.1 第一步:查路由,再查邻居
内核先选定出口和下一跳。直连场景中,B 的 192.168.10.20 就是下一跳。随后查询该接口对应的邻居信息。
如果已有可用 MAC,可以直接封装;如果没有,就启动地址解析。等待发送的数据可能被暂存在有容量限制的队列里,因此不能理解成"解析没结束,所有待发报文都能无限等待"。
ARP 也不是每次调用 send() 都发生一次。应用调用、IP 报文、以太网帧和地址解析不是一一对应关系。
3.2 第二步:A 广播询问"谁是 B"
请求可以翻译成一句话:
我是
192.168.10.10,我的 MAC 是 MAC_A。谁拥有192.168.10.20?请告诉我你的 MAC。
字段具体如下。
| 字段 | A 发出的请求 |
|---|---|
| Ethernet Destination | ff:ff:ff:ff:ff:ff |
| Ethernet Source | 02:00:00:00:00:0a |
| EtherType | 0x0806 |
| HTYPE / PTYPE | 1 / 0x0800 |
| HLEN / PLEN | 6 / 4 |
| OPER | 1 |
| SHA / SPA | 02:00:00:00:00:0a / 192.168.10.10 |
| THA / TPA | 00:00:00:00:00:00 / 192.168.10.20 |
交换机把这份广播转发到该 VLAN 中符合转发条件的其他端口。B、R、M 都可能看到请求,但在不考虑代理 ARP、地址冲突等特殊情况时,只有拥有目标 IP 的 B 应当回应这个查询。
"其他设备不应答"不等于"所有其他设备完全不处理任何字段"。是否学习或更新发送者映射,与接收端已有表项、实现和配置有关;不能推导成全网一定自动新增 A 的缓存。
3.3 第三步:B 通常单播回复 A
B 可以从请求里的 SHA、SPA 得到 A 的地址信息,通常不必为了回复 A 再广播查询一次。
应答相当于说:
192.168.10.20对应的 MAC 是 MAC_B;这份答复发给刚才询问的 A。
| 字段 | B 发回的应答 |
|---|---|
| Ethernet Destination | 02:00:00:00:00:0a |
| Ethernet Source | 02:00:00:00:00:0b |
| EtherType | 0x0806 |
| HTYPE / PTYPE | 1 / 0x0800 |
| HLEN / PLEN | 6 / 4 |
| OPER | 2 |
| SHA / SPA | 02:00:00:00:00:0b / 192.168.10.20 |
| THA / TPA | 02:00:00:00:00:0a / 192.168.10.10 |
"请求广播、应答单播"描述的是这里的普通首次解析过程。后续邻居探测可能使用单播请求,地址冲突检测等场景也会出现广播形式的 ARP 报文,不能把这句话当成没有例外的报文分类规则。

3.4 第四步:A 保存映射,再发真正的数据
A 接受应答并得到有效映射后,可以把待发 IPv4 报文封装成:
text
Ethernet Destination = MAC_B
Ethernet Source = MAC_A
EtherType = 0x0800
IPv4 Source = 192.168.10.10
IPv4 Destination = 192.168.10.20
注意 EtherType 从 ARP 阶段的 0x0806 变成了 IPv4 数据阶段的 0x0800。
ARP 请求与应答是为了获得映射,随后才是数据帧。不能把 ARP 应答本身当成刚才准备发送的应用数据。 即使抓包里 ARP 完整成功,也只能说明这一步有进展,不保证 B 的应用正在监听、更不保证应用一定返回正确结果。
3.5 如果是路由器向局域网主机交付呢
换一个观察方向:R 收到一份目的 IP 为 B 的报文,查路由后发现 B 在自己的 LAN 侧直连网络。
如果 R 没有 B 的映射,它同样要询问 192.168.10.20。此时请求中的 SHA、SPA 换成 R 的 LAN 接口 MAC 和 IP,TPA 仍是 B 的 IP。B 回复 R,R 随后封装发给 B 的以太网帧。
所以 ARP 不只在"电脑第一次上网"时出现。凡是需要在这条链路上解析 IPv4 邻居的设备,都可能发起这个过程。
四、邻居缓存不是一张永远不变的通讯录
4.1 先区分三张表
如果把不同设备上的表都叫作"地址表",ARP 欺骗就很容易讲歪。
| 信息 | 常见所在位置 | 典型对应关系 | 解决什么 |
|---|---|---|---|
| 路由表 | 主机、路由器 | 目的前缀 → 出口、下一跳 | 下一步走哪条路 |
| ARP / IPv4 邻居表 | 主机、路由器 | 接口上的 IPv4 邻居 → MAC、状态 | 当前帧交给哪个 MAC |
| MAC 转发表 / FDB | 二层交换机 | VLAN + MAC → 端口 | 从哪个交换机端口转发 |
ARP 欺骗的核心是让受影响设备接受错误的 IP 与 MAC 绑定。交换机可能完全没有被改错:它只是正常把"目的 MAC 为 MAC_M"的帧送到 M 所在端口。
因此,交换机工作正常与终端遭遇 ARP 欺骗,并不矛盾。它们判断的依据不同。
4.2 Linux 中的 STALE 不等于"这条映射已经不能用了"
Linux 把 IPv4 ARP 信息放在邻居子系统中管理。ip neigh 可以看到地址和状态,但状态描述的是可达性判断,不能当作安全认证结果。
| 状态 | 可以怎样理解 |
|---|---|
INCOMPLETE |
尚未完成地址解析 |
REACHABLE |
最近有可达性确认,当前认为邻居可达 |
STALE |
映射仍在,但需要在后续使用中重新确认可达性 |
DELAY |
延迟主动探测,给其他确认机会 |
PROBE |
正在主动探测邻居 |
FAILED |
探测达到失败条件,未能确认邻居 |
PERMANENT |
管理员配置的永久表项,不按普通动态老化处理 |

图中是便于理解的一条典型路径,不是完整内核状态机。实际迁移还受收到的报文、上层确认、定时器和管理员操作影响。STALE 表项通常仍可用于发送,随后再确认;不能看到它就判断已经断网。状态含义可查 ip-neighbour 手册。
缓存也没有适用于所有设备的统一"固定几分钟失效"公式。Linux 的可达性确认和垃圾回收是不同机制,成功通信的上层反馈也可能帮助确认邻居。Linux arp 手册
4.3 为什么允许更新,又为什么留下欺骗空间
设备更换网卡、虚拟机迁移或网络发生正常切换时,同一 IP 对应的 MAC 确实可能改变。如果旧映射永远不允许更新,网络就很难从合法变化中恢复。
问题在于:基础 ARP 报文中没有用来证明"我确实有权代表这个 IP"的密码学认证。发送者可以声明一个 IP 与 MAC 的组合,接收者再根据本地逻辑决定是否采用。
这里既不能夸大成"收到任意 ARP 都会无条件覆盖",也不能简化成"只接收自己刚问过的应答,所以绝对安全"。已有缓存、报文类型、接收策略、静态配置和交换机防护都会影响结果。
例如,Linux 的 arp_accept 涉及免费 ARP 对不存在条目的创建行为,不能把它当成一键启用完整 ARP 身份认证的开关。Linux 内核 ARP 配置说明
4.4 主动宣告和冲突检测不等于攻击
网络中可能看到设备主动宣告自己的地址映射,也可能看到它在使用地址前进行冲突检测。这类流量有正常用途。
以 RFC 5227 的两个报文为例:ARP Probe 使用 SPA=0.0.0.0 探测候选地址;ARP Announcement 则以请求形式宣告已使用的地址,其 SPA 与 TPA 相同。仅凭"没有先看到请求""请求的源 IP 是 0.0.0.0",不能直接定性为欺骗。RFC 5227:IPv4 地址冲突检测
真正要核查的是:宣告是否符合网络设计,IP 的拥有者是否发生合法变化,声明的 MAC 是否能通过独立信息确认。
五、Linux 观察:把路由、缓存和抓包对应起来
5.1 先确定观察的是哪张网卡
在自己的 Linux 测试环境中,可以从这些只读命令开始:
bash
ip -br addr
ip route
ip route get 192.168.10.20
ip route get 203.0.113.80
ip neigh show
ip route get 查询内核会怎样选择路径,本身不等同于向目标发送一次业务请求。如果当前环境没有相应路由,可能直接返回不可达;不要为了得到和文章一样的结果而修改正在使用的网络。
假设本地接口确实叫 eth0,进一步查看:
bash
ip neigh show dev eth0
ip neigh show to 192.168.10.1 dev eth0
教学示意输出:
text
192.168.10.1 dev eth0 lladdr 02:00:00:00:00:01 REACHABLE
192.168.10.20 dev eth0 lladdr 02:00:00:00:00:0b STALE
第一行是网关映射,第二行是 B 的映射。表里没有 S 的地址,在经由网关访问 S 的场景下很正常。
旧工具 arp -a 也能查看 ARP 缓存,但并非每个 Linux 环境都安装了 net-tools。新学习时优先熟悉 ip neigh,也能避免把不同工具的显示格式混为一谈。
5.2 抓包时要同时看到外层 MAC 和 ARP 内容
下面的命令在选定接口上观察 ARP:
bash
sudo tcpdump -i eth0 -nn -e -vvv arp
其中 -i 选择接口,-nn 避免名称和端口名解析,-e 输出链路层首部,-vvv 增加显示细节,最后的 arp 是过滤表达式。按 Ctrl+C 停止即可。
要对照 Ethernet II 字段,尽量使用实际以太网接口;Linux 的 any 抓包可能使用 cooked capture 格式,不适合直接照着普通 Ethernet 首部偏移解析。tcpdump 官方手册
在另一个终端访问自己可控的同网段主机,例如:
bash
ping -c 1 192.168.10.20
如果已有可用邻居缓存,抓不到新的广播请求并不反常。可以先等自然重新解析,或在独立测试环境里观察一个尚未解析的可达邻居;不必清空整张邻居表来强行制造现象。
5.3 两行抓包怎样读回字段
下面是省略时间戳、长度和部分格式的教学输出,并非真实抓包记录:
text
02:00:00:00:00:0a > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806),
Request who-has 192.168.10.20 tell 192.168.10.10
02:00:00:00:00:0b > 02:00:00:00:00:0a, ethertype ARP (0x0806),
Reply 192.168.10.20 is-at 02:00:00:00:00:0b
第一份报文里,who-has 后是 TPA,tell 后是 SPA;第二份报文里,is-at 把发送方协议地址与硬件地址连成了一条声明。
如果只过滤 arp,后续 ping 的 ICMP 数据不会出现在输出里。要观察解析与数据的先后关系,可改为:
bash
sudo tcpdump -i eth0 -nn -e 'arp or icmp'
不要因为过滤器没显示某种流量,就断定网络里没有发生这种流量。
5.4 "看不到"本身也需要解释
ARP 成功但 ping 失败,可能是 ICMP 被过滤;ping 成功但连接服务失败,可能是端口没有监听。它们分别属于不同层次。
另外,交换网络中普通接入口往往看不到其他端口之间的单播。即使开启混杂模式,也不能让交换机自动把所有单播发给观察者。要确认 A 与 R 之间的实际报文,应选择端点抓包、交换机镜像等合适观察位置。
这条边界对后面判断欺骗同样重要:第三台主机没有抓到某条单播应答,既不能证明应答不存在,也不能证明缓存没有变化。
六、ARP 欺骗:改掉映射,改变当前帧的收件人
6.1 正常情况下,双方分别记住谁
现在回到 A 通过网关 R 访问 S 的场景。正常映射应当是:
text
A 的邻居表:192.168.10.1 → MAC_R
R 的邻居表:192.168.10.10 → MAC_A
A 把出站帧交给 R;R 把返回 A 的帧交给 A。M 即便接在同一台交换机上,也不会因为"在同一个局域网"就自动收到全部 A 与 R 之间的单播。
6.2 M 向 A 冒充网关,向 R 冒充 A
经典双向欺骗模型中,M 试图让两端接受两条虚假声明:
| 接收者 | 虚假声明 | ARP 中被冒充的 SPA | 被宣称的 SHA |
|---|---|---|---|
| A | 网关 IP 对应 MAC_M | 192.168.10.1 |
02:00:00:00:00:66 |
| R | A 的 IP 对应 MAC_M | 192.168.10.10 |
02:00:00:00:00:66 |
注意,M 并不是对 A 宣告"我的 IP 是 192.168.10.66"。那只是它正常的地址。欺骗发生在它声称自己可以代表网关的 IP ,并对另一端声称可以代表 A 的 IP。
若两端按其接收策略采用了这些信息,结果就变成:
text
A 的邻居表:192.168.10.1 → MAC_M
R 的邻居表:192.168.10.10 → MAC_M

图中的"采用"是条件,不是收到报文必然发生的动作。防护设备、静态绑定或接收端策略都可能拒绝更新。讨论机制时,应该先明确"错误映射已经被接受"这个前提,再推导流量怎样走。
6.3 目的 IP 没变,为什么帧已经绕路
A 此时仍然认为网关是 192.168.10.1,默认路由也可能完全没变。只是解析到的链路层目的地址错了。
| 信息 | 正常发送 | 错误映射被采用后 |
|---|---|---|
| IP 目的地址 | 203.0.113.80 |
203.0.113.80 |
| 路由下一跳 IP | 192.168.10.1 |
192.168.10.1 |
| Ethernet 目的 MAC | MAC_R | MAC_M |
于是交换机收到目的 MAC 为 MAC_M 的帧,把它送到 M 的端口。这恰恰可能是交换机按规则工作的结果。
这一点也解释了:为什么只检查默认路由不一定能发现问题。路由正确只能说明选中的下一跳 IP 正确,不能证明这个 IP 对应的 MAC 也正确。
6.4 为什么错误映射可能反复出现
合法设备也会继续通信并发送真实信息,缓存还会经历确认、老化与刷新。因此,错误绑定不一定一直稳定存在,有时会在真实 MAC 和异常 MAC 之间来回变化。
如果只删除本地表项,却没有消除错误声明的来源,下一次解析仍可能接受错误结果。这也是"清了缓存暂时恢复,过一会儿又不正常"只能算现象、不能算根因修复的原因。
不要把攻击是否成功归结成"谁发得快谁赢"的唯一规则。顺序和频率可能影响结果,但表项状态、报文是否合法、系统更新策略及交换机检查同样会参与决定。
七、单向、双向与转发:为什么有时绕路,有时断网
7.1 只影响 A,不代表两个方向都经过 M
假设只有 A 把网关映射成 MAC_M,而 R 仍然正确记着 MAC_A。
如果 M 能把从 A 收到的数据继续交给 R,那么出站方向可能是:
text
A → M → R
返回方向却可能仍是:
text
R → A
这就形成了方向不对称。只控制一个端点的邻居映射,不能直接声称已经看到了完整双向会话。
7.2 双向欺骗配合正确转发,才形成经典中间路径
若 A 和 R 都接受错误映射,而且 M 能把数据正确送给真实下一跳,局域网这一段可能变成:
text
出站:A → M → R
入站:R → M → A
M 需要知道真实的 MAC_A 与 MAC_R,并有能力接收、处理和转发这些原本不以自己为最终 IP 目的地的数据。操作系统默认不会因为收到一个发往自身 MAC 的帧,就自动替其中任意目的 IP 完成合适的转发。
对于教学中的不做 NAT 的转发模型,A 发往 S 的 IP 目的地址仍是 S;M 转交给 R 时,当前链路的目的 MAC 改为 MAC_R。若使用三层转发,TTL 等字段会按转发规则变化;二层中继又有不同细节。不要把所有中间人实现都描述成"只复制字节,其他任何字段绝不变化"。

7.3 如果 M 不转发,通常表现为通信受阻
错误映射只负责把帧引向 M,并不负责保证业务继续成功。M 如果丢弃报文、没有转发能力或受到防火墙限制,A 访问外部网络就可能超时。
因此,同一类缓存污染可能造成不同后果:
| 条件 | 可能看到的现象 |
|---|---|
| 只有一个方向的映射受影响 | 部分流量绕行,路径不对称 |
| 双方映射受影响,M 能正确转发 | 业务可能继续运行,但链路中出现第三方 |
| 错误映射被采用,M 不转发 | 丢包、超时或连接中断 |
| 防护拒绝虚假映射 | 欺骗尝试未必改变正常路径 |
"网络还能用"不能排除中间路径;"突然断网"也不能单凭这一条就证明受到 ARP 欺骗。网线、接口、VLAN、地址冲突和普通配置错误都可能制造类似表象。
7.4 经过 M,不等于 HTTPS 内容就能被解密
在正确验证服务端身份、密钥未泄露、终端没有被控制的前提下,TLS 会保护应用数据的机密性和完整性。仅仅因为流量经过 M,M 并不能凭 ARP 欺骗自动得到 HTTPS 明文。
它仍可能看到部分网络元数据,并能丢弃或延迟流量;对于没有加密的协议,风险范围又不同。如果还存在证书信任被篡改、用户忽略认证错误或终端失陷,那是需要额外分析的条件,不能统统归功于 ARP 本身。RFC 8446:TLS 1.3 的安全目标
所以,"防止错误链路绑定"和"保护应用层通信"是两项互相补充的工作。
八、离线实验:解析报文,再模拟缓存被改写
光看表格,很容易把 SHA、SPA、THA、TPA 的顺序记反。这里用一个离线脚本把两件事接起来:先从字节解析真实的字段布局,再在 Python 字典中观察错误绑定的影响。
配套文件:code/arp_lab.py。只使用 Python 3 标准库,不打开套接字,不发送报文,也不读取或修改系统邻居表。
8.1 把 Ethernet 与 ARP 分开解包
核心结构如下;完整脚本还包含输入长度和类型检查、示例构造及自检。
python
import ipaddress
import struct
ETHERNET = struct.Struct("!6s6sH")
ARP_IPV4 = struct.Struct("!HHBBH6s4s6s4s")
def mac_text(value):
return ":".join(f"{byte:02x}" for byte in value)
def decode(frame):
# 教学解析器只接受无 VLAN 标签的 Ethernet + IPv4 ARP。
if len(frame) < ETHERNET.size + ARP_IPV4.size:
raise ValueError("truncated Ethernet/ARP frame")
dst, src, ether_type = ETHERNET.unpack_from(frame)
if ether_type != 0x0806:
raise ValueError("expected EtherType 0x0806")
htype, ptype, hlen, plen, op, sha, spa, tha, tpa = (
ARP_IPV4.unpack_from(frame, ETHERNET.size)
)
if (htype, ptype, hlen, plen) != (1, 0x0800, 6, 4):
raise ValueError("expected Ethernet/IPv4 ARP")
if op not in (1, 2):
raise ValueError("expected ARP request or reply")
return {
"ether_dst": mac_text(dst), "ether_src": mac_text(src),
"op": op, "sha": mac_text(sha), "tha": mac_text(tha),
"spa": str(ipaddress.IPv4Address(spa)),
"tpa": str(ipaddress.IPv4Address(tpa)),
}
格式字符串开头的 ! 表示采用网络字节序及标准尺寸。H 是 2 字节无符号整数,B 是 1 字节整数,6s 与 4s 则是固定长度字节串。
对于无标签 Ethernet II,ARP 从偏移 14 开始。带有 VLAN 标签或采用其他链路捕获格式时,这个起点不能直接套用,因此解析器明确拒绝不支持的外层类型。
8.2 用"发送者声明"理解缓存更新
简化模型只关注发送方字段:
python
def learn_for_demo(table, packet):
# 仅用于演示:真实系统不是无条件执行这一条赋值。
table[packet["spa"]] = packet["sha"]
正常应答让模型记住 B_IP → MAC_B;模拟 M 声称网关 IP 对应 MAC_M,就让 A 的字典中出现 R_IP → MAC_M。
这里的函数特意叫 learn_for_demo:它不是 Linux ARP 接收算法,没有模拟全部邻居状态、有效性检查、接口约束和系统参数。实验说明的是"如果错误绑定已被采用,接下来会发生什么",不是证明任意真实主机会接受任意报文。

8.3 运行与预期结果
进入文章文件夹后运行:
bash
python3 code/arp_lab.py
Windows 中若命令名为 python,可使用:
powershell
python code/arp_lab.py
脚本先打印请求和正常应答的字段,然后打印两条模拟虚假声明。关键结果应是:
text
ARP payload bytes: 28
A before: 192.168.10.1 -> 02:00:00:00:00:01
R before: 192.168.10.10 -> 02:00:00:00:00:0a
A after : 192.168.10.1 -> 02:00:00:00:00:66
R after : 192.168.10.10 -> 02:00:00:00:00:66
IP destination stays: 203.0.113.80
Ethernet destination becomes: 02:00:00:00:00:66
Self-checks passed: 12
脚本构造的是供解析使用的"14 字节 Ethernet 首部 + 28 字节 ARP"字节串,不包含线上的填充和 FCS。它不是可直接照搬为完整线速抓包长度的样本。
可以自己做两个小修改来检验理解:把请求中的 THA 改成其他值,观察外层目的 MAC 是否随之改变,并解释为什么投递仍由外层地址决定;或只更新 A 的字典、不更新 R 的字典,再画出返回路径。前者是分析字段用途,后者是分析方向性,都不需要向实际网络发送任何伪造报文。
九、发现与防护:把异常线索串成证据
9.1 一个 MAC 变化,只是一条线索
"网关 MAC 和昨天不同"值得核查,但合法设备替换、虚拟化迁移、网关冗余切换或代理机制也可能改变观察结果。
同理,"多个 IP 对应同一个 MAC"也不必然是攻击:一台设备可以拥有多个地址,代理 ARP 也可能使用同一个 MAC 回应多个 IP。反过来,REACHABLE 只是可达性状态,不意味着这个绑定已被认证。
更可靠的办法是关联信息:
| 观察项 | 它能说明什么 | 不能单独证明什么 |
|---|---|---|
| 网关邻居项突然改为陌生 MAC | 本地下一跳映射发生变化 | 一定是恶意改写 |
| 多个设备声明同一个 IP | 存在竞争声明或特殊设计 | 一定存在成功的中间人 |
| ARP 映射反复来回变化 | 可能存在地址冲突或竞争更新 | 仅靠频率就能确定原因 |
| 业务仍能访问 | 当前路径可能仍可用 | 路径中没有第三方 |
| 交换机发现陌生接入口 | 可以进一步定位设备 | 这个端口一定是攻击源 |
9.2 沿着"路由 → 绑定 → 声明 → 端口"排查
一个便于操作的顺序是:
- 确认路径:核对出口接口、网关地址及路由,排除连错网络。
- 记录绑定:查看异常发生时的邻居映射与时间,不只看恢复后的状态。
- 对照声明:在合适位置抓取 ARP,比较 SPA、SHA、TPA 和以太网源地址。
- 独立核验:通过网关管理信息、网络资产记录、交换机 MAC 表和变更记录确认真正归属。
- 定位并处置:确认异常设备或配置后,按网络管理流程隔离或纠正,再验证绑定和业务是否稳定。

抓包里的 ARP 声明本身正是被怀疑的对象,不能再拿另一份同样未经验证的声明作为唯一"可信基准"。还要注意,外层源 MAC 与 SHA 一致只能排除一种不一致现象,并不能证明其有权代表 SPA。
9.3 不同防护措施,各自管哪一段
静态邻居绑定适合少量、稳定且有明确维护责任的关键关系。它可以减少相应动态映射被改写的机会,但设备合法更换 MAC 后也需要同步维护。在 A 上固定网关映射,不会自动固定 R 上指向 A 的映射,覆盖范围要按方向考虑。
Dynamic ARP Inspection,DAI 由支持该功能的交换机检查 ARP 声明,通常结合 DHCP Snooping 形成的绑定数据库。静态地址设备需要相应的 ARP ACL 或平台支持的静态绑定策略;信任端口也必须符合真实拓扑,不能把所有端口设成可信。Cisco:Dynamic ARP Inspection
VLAN 划分和接入控制减少不可信设备直接进入相同广播域的机会,但同一 VLAN 内仍需要考虑绑定安全。只启用 MAC 数量限制也不等于已经校验 IP 与 MAC 的关系。
TLS、SSH 等安全协议保护上层数据及对端身份,不能修复所有链路可用性问题;它们依然是减少错误路径影响的重要一层。排查网络异常时,不应为了"先连上"而忽略证书或主机身份告警。
这些措施没有一个可以不看网络结构就一键套用。小规模实验网络和需要高可用切换的生产网络,对静态绑定、可信端口和维护流程的要求并不相同。
9.4 清缓存是动作,消除根因才是恢复
如果问题来自重复 IP,应先纠正地址配置;如果来自合法切换与旧绑定,应核对切换机制;如果确认有异常声明源,应先阻止它继续污染。
随后再按需要刷新相关动态邻居项,重新观察映射、双向通信和业务表现。只做一次 ping 成功就结束,可能漏掉反复污染或只影响部分流量的问题。
十、常见误区与自测:把几个边界真正分清
10.1 八个容易说错的结论
| 容易说错 | 更准确的理解 |
|---|---|
| ARP 根据网站 IP 查询网站 MAC | 先查路由,解析当前链路上的下一跳 |
| ARP 请求的两个目的 MAC 都是广播 | 外层目的通常是广播,内层 THA 常填零 |
| ARP 放在 IPv4 包里面 | 本文场景下直接由以太网承载,EtherType 为 0x0806 |
| 28 字节就是完整以太网帧长度 | 还要考虑外层首部、填充及 FCS 的统计口径 |
| 每发一个 IP 包都要广播 ARP | 可复用邻居缓存,解析与业务发送不是一一对应 |
| STALE 表项不能用了,REACHABLE 就很安全 | 状态反映可达性判断,不是身份认证 |
| 双方都被欺骗就一定能正常上网 | 还取决于中间设备是否正确转发 |
| ARP 欺骗能自动破解 HTTPS | 错误路径本身不提供 TLS 解密能力 |
10.2 自测一:究竟询问哪个 IP
题目: A 的路由结果是 203.0.113.80 via 192.168.10.1 dev eth0。如果没有可用邻居映射,ARP 请求中的 TPA 应填哪个地址?后续 IP 报文的目的地址又是什么?
答案: TPA 填 192.168.10.1,因为当前链路要交给网关;IP 目的地址仍是 203.0.113.80。两者不是同一个层次的目标。
10.3 自测二:改变的到底是哪张表
题目: A 的默认路由未变,邻居表中 192.168.10.1 从 MAC_R 变成 MAC_M。交换机把 A 的出站帧送到了 M。是否说明交换机的 MAC 转发表也被改错?
答案: 不一定。A 已经把目的 MAC 填成 MAC_M,交换机按 MAC_M 所在端口转发完全可能是正常行为。首先要解释的是 A 为何采用了错误 IP 与 MAC 绑定。
10.4 自测三:哪些方向会经过第三方
题目: 只有 A 的网关映射受影响,R 仍然正确映射 A。假设 M 能正常转发 A 发出的数据,返回流量是否也必然经过 M?
答案: 不必然。A 的出站流量可能走 A → M → R,返回流量仍可能走 R → A。要讨论完整双向中间路径,必须把两端映射与转发条件一起说明。
10.5 自测四:看到两个网关 MAC 就能断言攻击吗
题目: 一段观察中,同一个网关 IP 先后对应两个不同 MAC,业务在切换后恢复。是否可以直接认定 ARP 欺骗?
答案: 不能。还要排查合法设备更换、冗余切换、地址冲突以及观察位置的影响,再结合可信配置和交换机端口信息判断。变化是核查入口,不是完整结论。
写在最后
ARP 把一个看起来抽象的"下一跳 IP",落实成网卡真正能够使用的链路层地址。正常时,它完成广播询问、应答学习和后续封装;错误绑定被接受时,同一套发送逻辑又可能把数据交给错误的设备。
读懂 ARP,可以始终追问四句话:当前问的是谁,谁声称自己代表它,系统最终采用了哪条映射,这一帧因此交到了哪里。 把路由、邻居表、以太网封装和转发条件放在一起,既能解释正常通信,也能更准确地定位异常。
愿我们不只记住"ARP 请求广播、应答单播",还能在真正看到一份报文时,把它每一个地址字段的来由讲清楚。