如果你刚接触网络编程,八成会先遇到一个绕不开的问题:我要给一台主机发包,明明是"IP 到 IP"的通信,为什么内核非得先去查一个叫"MAC 地址"的东西?又为什么会有"ARP 欺骗"这种听起来像黑客电影里才能出现的攻击,而且击中的偏偏是解析 MAC 这个过程?

这篇文章就用一节课的篇幅,把 ARP(Address Resolution Protocol,地址解析协议)从头到尾掰开揉碎。我们从"为什么需要它"讲起,拆解 ARP 请求/应答的报文格式,画清楚广播与单播的交互时序,再讲 ARP 缓存怎么记住、记住多久,然后是那个"没被问也能答"的免费 ARP,最后落到 ARP 欺骗的攻击原理、两种典型危害(中间人与断网)以及对应的防护手段。一些聪明的实现细节和容易踩的坑,我会穿插在对应章节里强调。所有术语第一次出现我都会顺带翻译并解释。

看完你会理解一个底层真相:ARP 是一个建立在"信任"之上的协议,它之所以能被轻易欺骗,正是因为它压根没有设计认证;而防护的本质,就是把这句无条件的信任,改成一段"你得先证明自己是谁"的对话。

你应该有的知识准备

在往下读之前,我建议你已经有下面这几个概念的印象,做一句话回顾,忘了可以回头翻:

  • IP 地址:网络层(第三层)的地址,用来在全球/子网内唯一标识一台主机,是个"逻辑地址",可以随手改、随处换。
  • MAC 地址:数据链路层(第二层)的地址,出厂时烧在网卡上(世界唯一,理论上是),是个"物理地址",在同一局域网里直接用于"帧"的递送。
  • 以太网帧:局域网里真正在线缆上跑的东西,帧头里必须写清楚"源 MAC"和"目的 MAC"。
  • 广播与单播:广播是把一帧发到同一广播域里的每一台机器;单播是把一帧只发给某个明确的 MAC 对应那一台机器。

有了这些地基,我们正式开始。

为什么需要 ARP:IP 和 MAC 是两张不同的"住址系统"

先回答开头那个问题:局域网里两台主机通信,本质上走的是以太网帧,也就是说最终真正决定"这一帧该递给哪台机器"的,是 MAC 地址。IP 地址是给人、给应用、给路由用的逻辑地址;而当你走到具体一段线缆上,负责递送的是二层,二层认的是 MAC。

对,这就像一套完整的"上线和下线的双地址系统":

  • 网络层:ping 192.168.1.20;路由、子网划分、跨网段,全都看 IP。
  • 数据链路层:真实帧头里的 dst MAC: XX:XX:XX:XX:XX:XX;同一广播域内的实际投递,全看 MAC。

你说是用 IP 说话,可到了线缆上,网卡和交换机只认 MAC。于是系统必须提供一种"翻译"能力:给定一个 IP,告诉我对应的 MAC 是什么。 这个"IP 到 MAC 的翻译官",就是 ARP。

"Address Resolution"(地址解析)这个名字很贴切——解析,就是把一种地址"解开"成另一种地址。ARP 只服务于一个明确的目标范围:

ARP 只在"同一局域网(同一广播域)内"才工作。 它的职责是把"本网段内的一台主机的 IP"翻译成"这台主机的 MAC"。

如果对方在别的网段,怎么办?那是 ICMP 重定向、默认网关、路由这些网络层机制的事,不是 ARP 的管辖。ARP 顶多把你的 IP 翻译成"默认网关的 MAC",至于网关怎么把包送到千里之外,与 ARP 无关。

顺带埋一个伏笔:既然 ARP 这么重要、又这么"朴素",一旦有人伪造"某个 IP 对应的 MAC",所有拿 IP 说话的主机就都会被带偏。这,就是 ARP 欺骗的种子。

ARP 报文格式:把"谁有这个 IP"写进一张表

知道了"要解析",下一步是看它怎么解析。ARP 报文不长,加上以太网首部总共 42 字节左右,但如果把它拆开看,结构其实很有讲究——因为ARP 这个协议在设计时故意做成"不绑定以太网"的样子,它能工作在多种二层链路上。

下面是站在"以太网 + IPv4"这个常见组合下,一个完整的 ARP 报文长相(左边是以太网帧头,右边是 ARP 本体):

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        目的MAC(dst)  =  FF:FF:FF:FF:FF:FF (广播) 或对端MAC       |  ← 以太网首部
+                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               |       源MAC(src)= 自己MAC       |  ← 以太网首部
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   0x0806   |       硬件类型(HTYPE) = 0x0001        |   协议类型(PTYPE) = 0x0800   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| HLEN=6      |  PLEN=4      |           操作码(OP)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        发送方硬件地址(SHA) = 发送者MAC,6字节                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        发送方协议地址(SPA) = 发送者IP,4字节                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        目标硬件地址(THA),回应时是目的MAC,请求时通常填 0            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        目标协议地址(TPA) = 你想找的那台机器的IP,4字节             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

对照着逐字段讲:

  • 以太网首部里的类型字段是 0x0806。以太网帧头里有个"类型"字段,0x0800 表示这一帧装的是 IPv4 数据包,0x0806 表示装的是 ARP 报文。网卡先看类型再决定把帧交给谁处理。
  • 硬件类型(HTYPE):指链路层网络类型,1 表示以太网。这一位是为了说明"底下跑的是哪种二层介质"。
  • 协议类型(PTYPE):指要"转换成"的协议地址类型,0x0800 表示 IP 地址。意思是"我要把以太网地址,翻译成 IP 地址"。
  • 硬件地址长度(HLEN):对于以太网地址是 6 字节,所以 HLEN = 6。
  • 协议地址长度(PLEN):对于 IP 地址是 4 字节,所以 PLEN = 4。
  • 操作码(OP):这是 ARP 的灵魂,1 表示 ARP 请求(Request),2 表示 ARP 应答(Reply)。接收方先看 OP,才知道该当"谁在问我"来处理,还是当"有人告诉我答案"来处理。

这里请注意一个很多人读源码时困惑的细节:

源 MAC、目的 MAC 在以太网首部和 ARP 请求体中"各出现了一次"。 这是不是冗余?

是,也不全是。对于链路层恰好是以太网的情况,这两个地址确实重复了,属于冗余;但 ARP 被设计成"二层无关"的通用协议——在令牌环、FDDI 等其它二层介质上,以太网首部的地址字段和 ARP 内部的地址字段可能长得不一样、甚至未必都有这样的字段。所以把地址再写进 ARP 体里一份,是为了让 ARP 报文"自包含",能在别的链路上也能被读懂。这是老的教材里反复强调、但很多人只见过以太网所以觉得多余的知识点。

同理,ARP 里有 SHA(Sender Hardware Address,发送方硬件地址)、SPA(Sender Protocol Address,发送方协议地址)、THA(Target Hardware Address,目标硬件地址)、TPA(Target Protocol Address,目标协议地址)这四个地址位。梳理一下:

  • ARP 请求里,发送方填"自己"(SHA=自己的MAC,SPA=自己的IP),目标里 TPA 填"我想找的IP",而 THA 通常填全 0(因为我还不知道对方的 MAC,这正是我想问的东西)。
  • ARP 应答里,应答方把自己"对号入座":SHA=自己的MAC、SPA=自己(也就是被问到的那台)的IP,THA=提问方的MAC、TPA=提问方的IP。

一句话总结报文:请求是"谁有这个 IP 呀?我是 XYZ",应答是"我就是那个 IP,我在这"。报文里明明白白写清"谁发的、要找谁、我是谁",剩下的交给接收方决定采不采纳。

一次完整的 ARP 生命周期:请求、应答、广播与单播

纸上谈兵半天,我们还是把教科书上那一幕完整演一遍。两台机器:

  • 主机 A:IP 192.168.1.10,MAC AA:AA:AA:AA:AA:01(记 A)
  • 主机 B:IP 192.168.1.20,MAC BB:BB:BB:BB:BB:02(记 B)

现在 A 想给 B 发包,可它翻了翻自己的网络协议栈:知道 B 的 IP 是 192.168.1.20,却不知道 B 的 MAC。于是它发起一次 ARP:

主机A(192.168.1.10)                主机B(192.168.1.20)       其它主机(C)
    |                                  |                         |
    | ① ARP Request 广播               |                         |
    |  "谁有 192.168.1.20? 告诉        |                         |
    |   192.168.1.10 我在这里"          |                         |
    +----------------------------------+------------------------>| C也收到,但不是自己
    |  dst=FF:FF:FF:FF:FF:FF(广播)      |                         | 悄悄丢弃,不响应
    |  op=1                             |                         |
    |---------------------------------->|                         |
    |                                  |                         |
    |                                  | ② ARP Reply 单播        |
    |                                  | "192.168.1.20 就是"     |
    |                                  |    BB:BB:BB:BB:BB:02"    |
    |<---------------------------------+                         |
    |  dst=AA:AA:AA:AA:AA:01(单播给A)    |                         |
    |  op=2                             |                         |
    |                                  |                         |
    | ③ 用拿到的MAC封装真实数据帧        |                         |
    |  dst=BB:BB:BB:BB:BB:02 发送        |                         |
    +----------------------------------->|                        |
    |                                  |                         |

这个时序图里藏着四个关键知识点:

第一,ARP 请求用"广播"发。 帧头的 dst 是 FF:FF:FF:FF:FF:FF。因为 A 不知道 B 在哪台机器上,也不知道它的 MAC,索性把"谁有 192.168.1.20"喊给同一广播域里所有机器听。收到广播的每台机器都会把这一帧交给自己的 ARP 协议栈去核对,只有 IP 恰好等于 192.168.1.20 的那台(B)才会理睬。

第二,ARP 应答用"单播"回。 B 已经知道 A 的 MAC(从刚才的请求里 SHA 拿到),所以它不需要广播,直接把应答帧的 dst 填成 AA:AA:AA:AA:AA:01 发回给 A。这就是"为什么能单播应答"的答案:应答方手里已经有了提问方的 MAC,完全没必要再广播一次,广播只会徒增无谓的干扰。广播是"我不知道你是谁"时的无奈之举,一旦知道了,立刻回到精准的单播。

第三,其它主机收到请求但不响应。 广播域里除了 A 和 B 还有 C,C 也收到了 A 的 ARP 请求,但发现 TPA(192.168.1.20)不是自己,就把这帧默默丢弃,也不回任何东西。这说明 ARP 请求是"一对多地问,只有一个对的答"。

第四,真实数据帧紧随其后。 A 拿到 B 的 MAC 后,立刻用它去封装真正要发的那一帧数据,dst 填 BB:BB:BB:BB:BB:02。后续这一路的数据都会以这个 MAC 为目标。

思考题 1:为什么广播域之外的主机不会收到 ARP 请求?

答案:因为 ARP 请求以广播帧发出,而广播帧只在同一"广播域"(同一广播域内只有自己的物理网段)内传播。 路由器默认不会转发目的 MAC 为 FF:FF:FF:FF:FF:FF 的帧(它会终结二层的广播,只转发三层的 IP 包)。所以当目标 IP 不在同一网段时,主机根本不会发 ARP 去问"谁有这个目标 IP",而是会去问"谁是网关",把包里给网关,由网关负责跨网段转发。这也是为什么 ARP 从来只解决"本网段内 IP 到 MAC"的翻译。

思考题 2:假设主机 B 不在线,A 的 ARP 请求会怎样?

答案:A 会把请求广播出去,等待一段时间(Linux 里一般重发若干次,比如默认重试约 2 到 3 次,邻居条目保持 INCOMPLETE 状态),如果始终没有收到应答,A 会判定该 IP 不可达,把那个邻居条目标记为 FAILED。 之后当 A 想给 192.168.1.20 发包时,会在 IP 层收到类似"Destination Host Unreachable"(目标主机不可达)的 ICMP 报错。也就是说,ARP 不仅负责"翻译",还隐式承担了"探测邻居在不在线"的职责——正因为这一点,后面你会看到它如何被攻击者利用来"关停"一台主机的连通性。

ARP 缓存:谁来记住,记住多久

如果每次都广播一遍"谁有这个 IP",整个局域网会被这种呼叫声吵死。所以现代操作系统都维护一张 ARP 缓存(ARP cache):一张"IP → MAC"的映射表,翻译过一次后记录下来,下次先查表,命中就直接用,不再广播。

在 Linux 上查看这张表:

$ ip neigh show
192.168.1.1  dev eth0 lladdr 00:50:56:c0:00:08 REACHABLE
192.168.1.20 dev eth0 lladdr bb:bb:bb:bb:bb:02 STALE

老的 arp -an 命令也能看,但 ip neigh 是当前主流写法,而且它还能显示 Linux 特有的"邻居状态"。

问题来了:缓存不能永远有效。 因为 IP 和 MAC 的绑定是可变的——一台主机可能换了网卡(MAC 变了)、可能 DHCP 重新分配了 IP、可能关机后 IP 被别的机器复用。所以任何动态缓存的表项都必须"过期"。这就引出本文最容易被忽略、但在排障和攻防中都极其关键的一块:缓存的生存期到底怎么算。

这里要区分两个层面:

层面一:通用的"多久过期"直觉。 不同操作系统、不同版本差别很大。Windows 的动态 ARP 表项一般存活约 1 到 2 分钟(旧的静态条目约 2 分钟,动态更短);而 Linux 根本不是简单的"一条倒计时",下面单独讲。你只需要建立"表项会过期、过期后就失效"的直觉,别把它当永久记忆。

层面二(重点):Linux 的邻居表其实是"状态机"而不是"倒计时条"。 这是 Linux 内核用一套叫 NUD(Neighbor Unreachability Detection,邻居不可达检测) 的状态机来管理 ARP 缓存。我不展开全部状态,但把几个最常见的、排障时你一定会见到的讲清楚:

  发送ARP请求(还没收到应答)      收到应答/上层确认可达
INCOMPLETE ────────────────▶ REACHABLE
                                  │ 超过 base_reachable_time(默认约30秒,带随机扰动)
                                  ▼
                              STALE ──(要发包时)──▶ DELAY(等5秒,盼着IPC ACK来"白嫖"确认)
                                  │                    │ 收不到确认
                                  │                    ▼
                                  │                 PROBE(单播探测,多次无果)──▶ FAILED
                                  │
                                  └── 静默,供后续复用;真正删除受路由缓存引用、
                                      gc_thresh 阈值、gc_stale_time(默认60秒)多重控制

几个排障关心的数字(不同内核版本默认值可能略异,以下为常见默认):

  • REACHABLE → STALE:超过 base_reachable_time_ms(默认约 30000ms,约 30 秒)后,如果这段时间没有被"上层正向反馈"(比如一次成功的 TCP ACK 就能刷新并保持 REACHABLE),条目降级为 STALE。到这一步,条目还没有失效删除,只是"老了、不靠谱了"。
  • STALE → 想再用它时:STALE 只是一个"还留着备用"的状态。内核要发包时会先走 DELAY 阶段等一小会儿,看能不能用地上的 ARP 应答或上层 ACK 白嫖一次免费确认;不行就发单播探测(PROBE);实在没有就 FAILED,以及最终由垃圾回收删除。
  • 真正"消失":由 gc_stale_time(默认 60 秒)、路由缓存的引用、以及 gc_thresh1/gc_thresh2 等垃圾回收阈值共同决定,网上实测常在"5 至 10 分钟"这个区间才会真正把一条不再使用的 STALE 条目删除。

这段为什么要讲这么细? 两个现实原因。第一,排障时你 ip neigh 看到 STALE 不叫"坏了",看到 INCOMPLETE/FAILED 才说明解析没成功。第二,缓存会过期这件事,正是 ARP 欺骗能一而再、再而三生效的温床——攻击者只要周期性重播伪造应答,就能把受害者缓存里"正确的 MAC"反复刷成"自己的 MAC"。这个呼应,等讲欺骗时你会恍然大悟。

思考题 3:为什么 ARP 把"REACHABLE 约 30 秒"当成一个合理的老化时间,而不是设成 10 年?

答案:因为 IP 和 MAC 的绑定随时可能变化,缓存必须尽快"承认自己可能过时",才能及时发现邻居的更换。 但同时它又不能太短——否则每隔几秒就要广播一次 ARP,太吵、太费。所以这是一个"新鲜度 vs 开销"的折中。Linux 还聪明地加了"上层正向反馈刷新"机制:像 TCP 三次握手、成功的 ACK 这类"我确实和这台机器刚通过信"的证据,能持续把条目钉在 REACHABLE,从而在频繁通信时就极少发 ARP 广播,只在空闲一段时间后才老化。简单说就是"你在用它,就不用问;你不理它,它才怀疑"。

免费 ARP:一个"没被问也能答"的特殊广播

说完请求/应答,我们说第三种 ARP 报文——它甚至不需要有人问,主机自己会主动广播。这就是 免费 ARP(Gratuitous ARP,GARP)。

什么叫"免费"?英文 gratuitous 是"无理由的、主动送出的"意思。免费 ARP 是指:主机主动广播一个 ARP 请求,问的是"谁有我这个 IP",而这个 IP 正是它自己。 你没看错——它问的是一个自己已知答案的问题。

它长这样(还是用 A 举例):

主机A(192.168.1.10, MAC AA:..:01)
    |
    |  Free ARP Request(广播, op=1)
    |  SPA = 192.168.1.10   (发送方IP = 我自己的IP)
    |  SHA = AA:AA:AA:AA:AA:01 (发送方MAC = 我自己的MAC)
    |  TPA = 192.168.1.10   (目标IP = 还是我自己)
    |  THA = 00:00:00:00:00:00
    +------------------------------▶ 广播给网段内所有机器

它看起来像个普通 ARP 请求(op=1、广播),但 Q:为什么一台知道自己 MAC 的主机要问"谁有我自己的 IP"?因为它的目的根本不在"问",而在"让所有听到的人更新缓存"。它至少干三件事:

  1. 让局域网里所有主机把"192.168.1.10 ⇨ AA:..:01"这一条刷新进自己的 ARP 缓存。 相当于不打一声招呼就告诉大家"以后发给我 A 的包,就送这个 MAC"。
  2. 顺便检测 IP 冲突(Duplicate Address Detection,DAD):如果网段里竟然有人应答"这个 IP 是我的",说明你的 IP 分配撞车了。
  3. 主动广播绑定关系:这在故障切换场景里有奇效。

我把免费 ARP 的场景单拎出来,是为了让你记住一个关键结论,它直接通向后面的攻击:

接收免费 ARP 的主机,即使"没人问过我"(我没有发出过对应的 ARP 请求),也往往会把报文的 SPA→SHA 映射更新进自己的 ARP 缓存。

也就是说,免费的、主动推送给你的"IP 是 MAC",收到了就会采用。这在协议设计上是为了"让全网快速收敛到新 MAC",但也埋下了巨大的安全隐患——因为如果这个"我是 XX IP"的免费 ARP 是伪造的呢?

思考题 4:为什么会需要主机主动广播免费 ARP?给个真实场景。

答案:典型的场景是"故障切换/高可用"。 用 keepalived 做虚拟 IP(VIP)热备时,主备两台机器共用一个虚拟 IP。当主节点接管 VIP 时,它会立刻广播一条免费 ARP,声明"这个 VIP ⇨ 现在这台主节点的 MAC",从而让全网(网关、交换机、其它主机)的 ARP 缓存立刻指向新主节点,而不必等各自缓存老化。如果不发这条免费 ARP,别人的缓存里还留着"VIP ⇨ 旧主节点 MAC",那么即使 VIP 已经切走,别人发到旧 MAC 的包也会被旧的机器收走或丢弃,切换根本无法收敛,业务就断了。这就是"免费 ARP 用于加速收敛"最经典的用途,也是它在 Layer2 负载均衡(如 MetalLB 的 layer2 模式)里被周期性广播的原因。

ARP 欺骗的原理:为什么不认证的信任可以被投毒

现在进入本文的高潮。我们已经集齐了所有拼图:

  • 主机收到"IP 在 MAC 处"的 ARP 应答,无脑更新缓存;
  • 甚至收到"没被问的免费 ARP",也无脑更新缓存;
  • 缓存里的映射,就是这台机器将来发数据帧时填写的真实目的 MAC。

把这三条连起来,答案已经呼之欲出:只要我能伪造一条"网关的 IP 在某处"或"某台的 IP 在某处"的 ARP 报文送到你面前,你就会被带偏。 而 ARP 协议本身,从 RFC 826 设计至今,没有提供任何一丁点认证机制——它不校验报文是不是"本网段真实合法设备"发的,也不校验"这个 IP 到底归不归你这个 MAC"。它完全信任报文中自带的四个地址字段。

这种"不加任何验证、看见就信"的信任模型,正是 ARP 欺骗(ARP spoofing,也叫 ARP 中毒 / ARP poisoning) 能横行的根本。所谓欺骗,就是一句话:

攻击者伪造虚假的"IP→MAC"映射报文(通常是伪造的免费 ARP 或伪造的 ARP 应答),投给受害者,让受害者的 ARP 缓存把某个合法 IP(最常见的是网关 IP)映射到攻击者的 MAC 上。

一旦受害者缓存里"网关 IP = 攻击者 MAC",那么受害者发给网关的所有帧,在二层上都会被送进攻击者的网卡。攻击者就成了受害者与外界的"必经之口"——这就是中间人(Man-in-the-Middle,简称 MITM)的雏形。而如果攻击者更进一步,把自己伪装成两台主机之间的"中继",它还能顺手"关死"某一方的连通,造成"断网"。

需要特别强调一个很多新手困惑的点:ARP 欺骗为什么能生效,哪怕是"没人来问过我"?

因为之前讲过,主机接受 ARP 更新时并不要求"先有对应的请求"。攻击者发一条伪造的免费 ARP(或者干脆发一条伪造的应答),受害者收到后照样更新缓存。协议设计者的初衷是"让全网快速收敛到新 MAC",可这个便利没有配任何验证,于是"便利"直接变成了"漏洞"。用一句话说清楚防护的反面:只要协议不验证"你凭什么能宣告这个 IP",任何一台网卡都能宣告任何 IP。

攻击者的三步走:从伪造应答到中间人/断网

把原理落到实操,典型的 ARP 欺骗(以"骗受害者、冒充网关"为例)可以拆成三步。先看拓扑:

        +-------------------------+
        |   默认网关 (192.168.1.1)  |
        |   MAC G:...:00           |
        +-------------+-----------+
                      |  (局域网交换机/集线器)
   +------------------+------------------+
   |                  |                  |
+----+            +----+             +----+
| A  |            |  M  |           ...(其它)
|10  |            | 攻击者 |
|MAC1|            | MACX |
+----+            +----+
  受害者             攻击者
  • 受害者 A:IP 192.168.1.10,MAC AA:AA:AA:AA:AA:01
  • 攻击者 M:IP 192.168.1.99,MAC CC:CC:CC:CC:CC:FF
  • 网关 G:IP 192.168.1.1,MAC 00:50:56:c0:00:08

步骤一:攻击者冒充网关,投毒给受害者。 攻击者广播(或单播给 A)一条伪造的免费 ARP:SPA=192.168.1.1,SHA=CC:CC:CC:CC:CC:FF,即声称"网关 IP 在攻击者 MAC 处"。

M(攻击者 CC:..:FF)  ──广播伪造FreeARP/应答──▶  A(受害者)
      "192.168.1.1 在 CC:CC:CC:CC:CC:FF"
   结果: A 的缓存里  192.168.1.1 ⇨ CC:CC:CC:CC:CC:FF  (被投毒!)

步骤二:攻击者再冒充受害者,投毒给网关。 攻击者又发一条伪造报文给网关:SPA=192.168.1.10,SHA=CC:CC:CC:CC:CC:FF,即声称"受害者 A 的 IP 在攻击者 MAC 处"。

M(CC:..:FF)  ──伪造ARP──▶  网关(192.168.1.1)
      "192.168.1.10 在 CC:CC:CC:CC:CC:FF"
   结果: 网关缓存里  192.168.1.10 ⇨ CC:CC:CC:CC:CC:FF  (被投毒!)

步骤三(两种结局,取决于攻击者的选择):

  • 中间人(监听/篡改):A 发给网关的帧 → 送到 M;网关回给 A 的帧 → 也送到 M。M 两头都收,观察、甚至改写后再转发出去。A 和网关都以为在直接通信,实际全被 M"夹在中间"监控着。这就是典型的 中间人(MITM)。
   A ──(发给网关注)──▶ M ──(观察/改写后)──▶ 网关
   A ◀──(来自网关?)── M ◀──(观察/改写后)── 网关
        双向流量都流经攻击者
  • 断网(拒绝服务 DoS):M 只投毒、却"拒绝转发"。于是 A 出不去,网关回不来,双向全断。受害者表现为"能连网线,却上不了网"——这正是很多校园网、办公室"突然断网又查不出原因"的经典症状之一,因为到 IP 层一切无恙,坏的是二层的缓存,普通排障根本找不到。

注意一个现实细节:交换机会因为 MAC 地址表的抖动而反复"改道"。在交换机正常的网络里,攻击者如果只是双向投毒,交换机会把"去往该 MAC"的帧在学习后送往对应端口,配合攻击者的改包转发,中间人是能做到的。而在更早的共享介质(集线器)上,所有人天然共用一个冲突域,抓包更是毫不费力。

思考题 5:为什么"断网型"ARP 欺骗那么难排查?

答案:因为受害者的 IP 层一切正常,坏的是二层。 受害者 ping 网关 时,IP 仍然认为网关是 192.168.1.1,非常"正常";真正出错的是 192.168.1.1 对应的 MAC 被换成了攻击者的 MAC,而 IP/路由层根本看不到 MAC。所以 ip addr、ip route 全部显示正常,只有去 ip neigh 或 win 下 arp -a 对比网关的 MAC 与真实网关 MAC 才发现端倪。而且这种断网往往时断时续(随攻击者刷新周期、随受害者的缓存老化),极不稳定,进一步加大排查难度。这也说明了为什么网络安全上常说"ARP 是二层最容易被下手、最难被察觉的点"。

防护:把"无认证的信任"关进笼子里

原理是把信任关进笼子,防护的本质就是拿掉那句无条件的信任。下面按"由简到繁、由终端到网络设备"的顺序,给出完整有效的手段,并说明各自的适用边界与代价。

1. 静态 ARP 绑定(静态 ARP / arp -s)。 手动把"网关 IP ⇨ 网关真 MAC"写死在机器里,声明它是"永久表项",静态表项不会被任何动态 ARP 报文覆盖。这是最直接、对单台机器最有效的办法,尤其适合"保护网关"这一个点。

$ sudo arp -s 192.168.1.1 00:50:56:c0:00:08    # Linux 手动绑定网关
$ arp -a                                       # 查看,可看到 static/PERMANENT
  • 优点:简单、零成本、单机有效,能挡住"冒充网关"这类最常见的攻击。
  • 缺点:只防"冒充固定 IP",不防"攻击者互骗";而且要一台台机器手动维护,IP/MAC 一变就要手工更新,在大规模动态环境里不现实。它适合"关键服务器 + 网关"这种数量少、地址固定的节点,作为最后一道兜底,而不是大规模推广的银弹。

2. 端口安全(Port Security)。 这是交换机侧的功能:限制"某个物理端口上只能学习到若干个(比如 1 个)MAC 地址",或者写死允许的 MAC。攻击者像插新网卡换 MAC、或一个端口同时冒充多台机器的行为,会被交换机层面的 MAC 学习限制拦下来(可配置为丢弃并告警、或直接 shutdown 端口)。

  • 优点:能挡住"物理端口被接上多台设备/替换 MAC"这类滥用,是防 MAC 洪泛的基础。
  • 缺点:它防的是 MAC 层面的滥用,不一定能拦下"合法端口上用报文中填写伪造 IP"的欺骗——攻击者可能就在自己正常的端口上用伪造 ARP 报文。所以单靠端口安全不够,还需要下面这招。

3. DAI(Dynamic ARP Inspection,动态 ARP 检测)+ DHCP Snooping。 这是目前对付 ARP 欺骗最有效的组合拳,也是企业级交换机上真正"治本"的功能。它的思路不再是"信,但要小心",而是**"先验证,后相信"**。

原理分两半:

  • DHCP Snooping(DHCP 侦听):交换机在一条腿上"偷听"网络中的 DHCP 交互,把"哪台 MAC 从哪个端口、拿到了哪个 IP"记进一张 DHCP Snooping Binding Table(DHCP 绑定表),同时把连接"合法 DHCP 服务器/上行口"的端口标记为信任端口(trusted port),其余连接终端的口记为非信任端口(untrusted port)。伪装 DHCP 服务器的响应会在非信任口被丢弃。
  • DAI:基于这张绑定表,交换机对非信任端口收到的每一个 ARP 报文做合法性校验——把 ARP 里的源 IP、源 MAC、来源端口,跟绑定表逐一对;匹配才放行,不匹配直接丢弃并记日志(syslog)。
             DHCP 绑定表(交换机维护)
   IP             MAC             端口
192.168.1.10   AA:AA:AA:AA:AA:01  gi1/0/1
192.168.1.99   CC:CC:CC:CC:CC:FF  gi1/0/2
 
  A 从 gi1/0/1 发出 ARP 声称 "我 SPA=192.168.1.1, SHA=CC:..:FF"
        → DAI 校验: 该端口该IP对应MAC要一致 → 不匹配 → 丢弃!
  • 优点:对"冒充网关"和"冒充任意主机"都能有效拦截,自动化程度高,适合大规模网络。
  • 缺点:必须依赖 DHCP(或为静态 IP 主机手动建静态绑定),要求交换机支持 DAI,且要正确配置 DHCP Snooping 的信任端口。 如果网络大量使用手工静态 IP,就要配合 ACL / 手动绑定表来兜底。

4. VLAN 隔离(划分广播域)与 802.1X / 入网认证。 VLAN 把一个大广播域切成多个小广播域,ARP 广播只在各自的 VLAN 内传播,即便某个 VLAN 里发生了投毒,影响范围也被锁在那一个小网段里,不会毒到全网。802.1X、入网认证则从源头拒绝"不明设备"上场,让攻击者根本没有插入网络的机会。

5. 终端侧的 ARP 防火墙 / 防 ARP 软件。 对无法控制交换机的场景(比如公共 WiFi、家宽),可以在每台终端上装 ARP 防护软件,它们会主动广播正确的本机 IP-MAC、监测异常 ARP(比如多次声称自己是网关但 MAC 不匹配)并拦截、锁定网关 MAC 的正确性。

给一个落地建议的优先级:

场景优先手段
单台关键服务器/网关静态 ARP 绑定(arp -s)兜底
交换机能管控的企业网DAI + DHCP Snooping 为主,端口安全 + VLAN 隔离为辅
无法掌控网络的公共网终端 ARP 防火墙 / 加密通信(HTTPS/SSH)
任何场景的通放松重要数据走加密(HTTPS/SSH/TLS),即使被 MITM 也很难被读

最后提醒一点:加密(HTTPS/SSH)是 ARP 欺骗之外的"纵深防御"。它不阻止投毒本身,但即便攻击者把流量拦下来,没有私钥他也读不懂密文、改不了内容。治标(堵住投毒)与治本(让投毒无利可图)要结合起来。

总结

我们来把这一课串成一条线:网络层用 IP 说话,而真正在链路上递送帧要靠 MAC,这个"翻译"由 ARP 完成。它的请求靠广播问出"谁有这个 IP",应答靠单播精准回给提问者(因为已知对方 MAC);好消息是答案会被存进 ARP 缓存,坏消息是缓存会过期(Linux 里是一套 REACHABLE/STALE/DELAY/PROBE 的邻居状态机)。有些情况下主机还会主动广播免费 ARP,不请自来地宣布"我是某某 IP、某某 MAC",以便全网快速更新。而这套协议没有认证,谁都能宣告任意 IP,于是只要攻击者伪造一条"网关 IP 在攻击者 MAC",就能让不设防的接收方中毒,要么被中间人监听、篡改流量,要么被恶意"拒收"造成断网。防护的关键,就是从"无条件信任"改成"先验证再相信":静态 ARP 定向兜底、端口安全防 MAC 滥用、DAI + DHCP Snooping 在交换机上做逐包校验、VLAN 缩小爆炸半径,再加一层加密通信作为最后的纵深防线。

这一全过程,也是理解《Linux 网络编程》后续章节(原始套接字、数据链路层抓包、如何自己构造 ARP 报文、如何写一个简易的 ARP 欺骗/防护工具)的绝佳地基——因为一旦你看懂了"为什么这么容易中毒",你就能用同样的知识去写安全工具,也能在真实网络中被投毒时,第一时间从 ip neigh 里找到真凶。

最后留几道自测题,答案见下,建议你合上文章先自己推一遍。

自测与思考题(含详解答案)

自测 1:一条 ARP 请求为什么必须用广播发出去?

答案:请求方"只知道要找谁的 IP,却不知道该 IP 主机的 MAC"。它无法把帧单播到目标(没有目标 MAC),只能用广播帧(FF:FF:FF:FF:FF:FF)让同一广播域内所有机器都收到,再由各机核对 TPA 是不是自己,是则应答、否则丢弃。广播是"未知即广播"的必然选择;一旦请求方通过来源拿到了对方 MAC,后续(包括应答)就是单播。

自测 2:为什么 ARP 应答可以只用单播回,而不必广播?

答案:应答方的目的只是"告诉请求者我的 MAC"。请求者发来的 ARP 请求里已经携带了 SHA=请求者自己的 MAC,应答方手里握有这个明确目标,直接把帧单播回给这个 MAC 即可,无需广播、也避免惊扰网段里其它无关机器。单播比广播更精准、开销更小。

自测 3:在 Linux 上看到 ip neigh 里某条目是 STALE,它还算数吗?一定会立刻删掉吗?

答案:STALE 表示该条目"曾可达、但现在没被确认","算数但靠不住"——它仍存在于缓存中,可被复用,但内核在要用它之前会先走 DELAY(短暂等待上层确认或免费 ARP)→ 必要时 PROBE(单播探测)来重新确认有效性。它不会一到 STALE 就立刻被删;真正删除受 gc_stale_time(默认约 60 秒)、路由缓存是否仍引用该邻居、以及 gc_thresh1 等垃圾回收阈值共同控制,实测一条长期不再使用的 STALE 条目往往要到约 5 至 10 分钟才可能被回收。排障时别把"STALE"误判为"故障",INCOMPLETE/FAILED 才表示解析确实出了问题。

自测 4:攻击者只发伪造的 ARP 应答/免费 ARP,受害者明明没有发过请求,为什么还会上当?

答案:因为 ARP 实现对"单向宣告"是无条件信任的——主机收到任何"某个 IP 在某个 MAC"的 ARP 报文(无论它是否曾发出对应请求),都可能据此更新自己的 ARP 缓存。免费 ARP(op=1、SPA=TPA=自己)正是利用这个"被动的更新"特性,来向全网宣告自身映射;攻击者则是把这种特性变成了漏洞:伪造一份"网关 IP 在攻击者 MAC"的报文送过去,受害者不设防地采纳,缓存被投毒。之所以如此"天真",根源在于 RFC 826 没有给 ARP 加入任何认证机制。

自测 5:为什么让 DAI 生效,通常必须先开 DHCP Snooping?

答案:DAI 的"合法性判定"依赖一张"IP + MAC + 端口"的合法对应表,而这张表的权威来源,就是 DHCP Snooping 在监听 DHCP 交互时建立的 DHCP 绑定表(DHCP Snooping Binding Table),同时它还负责把连接合法 DHCP 服务器、上行口的端口标为信任口,拦截来自非信任口的伪造 DHCP 响应。没有这张绑定表(或未为静态 IP 主机手动补充静态绑定),DAI 就无从比对,也就无法判断一个 ARP 报文是该放行还是该丢弃。因此"先有 DHCP Snooping 的合法绑定,再有 DAI 的逐包校验"是标准配置顺序。


关于 Linux ARP 邻居状态机与老化参数(base_reachable_time、gc_stale_time、gc_thresh*),可参考 arp(7) 手册页、Linux 内核邻居表 NUD 状态迁移的实际说明,以及多篇对内核 ARP 老化机制的实测(如《Linux 实现的 ARP 缓存老化时间原理解析》与《Linux 内核什么时候会删除老化的 ARP 条目?》);关于 DAI/DHCP Snooping/端口安全/静态 ARP 的成因与取舍,可参见主流网络设备厂商的 ARP 攻击防御技术白皮书,以及 Dynamic ARP Inspection (DAI) 工作原理。不同内核版本与厂商设备的默认参数可能略有差异,生产环境请以本机 cat /proc/sys/net/ipv4/neigh/*/... 与设备 CLI 实际输出为准。