知道 IP 之后,为什么还需要 MAC?
下一跳已经找到了,可它还是不能出发
上一次,数据包终于弄清楚了第一步该往哪走。
对于这次要去的 8.8.8.8,路由表给出了答案:不是本地直连,得走默认路由。下一跳,是 192.168.1.1,从 eth0 出发。
数据包本以为,事情到这里已经结束了。它知道该交给谁了,剩下的就是交出去。
可它试着把数据交出去的时候,卡住了。
192.168.1.1,只是一个 IP 地址。
而这一跳,真正要发生的,是把一份数据从这台主机的网卡,递到 192.168.1.1 所在的那台设备的网卡上。网卡和网卡之间的交付,靠的从来不是 IP 地址。
数据包第一次意识到:
知道该交给谁,和知道怎么把东西递过去,是两件事。
IP 地址和 MAC 地址,到底有什么不同?
先把这件事说清楚,不用比喻,直接从这次遇到的问题出发。
IP 地址,负责的是网络层的寻址。在这次旅行里:
1 |
|
这个地址描述的是最终希望到达的地方。不管中间经过多少设备,这个目的 IP 自始至终都不会变。
MAC 地址,负责的是当前链路上的交付。此刻数据包要解决的问题是:
1 |
|
MAC 地址回答的是一个范围小得多的问题——这一跳的 Frame,应该交给当前这条链路上的哪块网卡。它的意义,只在当前这一跳有效。
这里有一件必须说清楚的事:MAC 地址不是最终目的地址。数据包最终要去的是 8.8.8.8,不是 aa:bb:cc:dd:ee:ff。MAC 地址只是这一跳临时需要用到的、链路层意义上的交付凭证。
数据包第一次听到 ARP
现在,数据包面前只剩一个具体的、可以解决的小问题:
1 |
|
负责解决这个问题的机制叫 ARP(Address Resolution Protocol,地址解析协议)。
它做的事情,可以理解成这样:主机在自己所在的这条链路上问了一句——
“谁是 192.168.1.1?”
这句”问”,在网络里的真实形式是一个 ARP Request,而且它通常是广播出去的:
1 |
|
同一条链路上的所有设备都会收到这个广播,但只有真正拥有 192.168.1.1 这个地址的那台设备,会回应一个 ARP Reply:
1 |
|
这里有一个边界必须划清楚:ARP 广播寻找的,是当前这条链路上、已经确定下来的下一跳 IP 对应的 MAC——不是在寻找最终目的地。 这次的 ARP 请求问的是 192.168.1.1,而不是 8.8.8.8。8.8.8.8 根本不在这条本地链路上,问了也不会有人回应。
答案会被记住
ARP Reply 回来了,数据包终于拿到了它要的东西:
1 |
|
但系统不会每发一次数据都重新广播一遍——这个映射关系,会被暂时记在一个叫邻居表(Neighbor Cache,传统上也叫 ARP Cache)的地方。可以直接看一眼:
1 | ip neigh |
输出类似:
1 |
|
这一行,就是刚才那次 ARP 交换留下的结果。不过要注意,这不是一个永久的绑定关系——邻居表的状态会随时间和链路情况变化,过一段时间没有通信,或者对方设备发生变化,这条记录也会重新被确认或者被丢弃。
终于可以真正封装出去
到这一步,数据包手里已经凑齐了三样东西:
1 |
|
于是,一份真正可以在这条链路上发送的 Ethernet Frame 被组装出来了:
┌──────────────────────────────────────┐
│ Ethernet Frame │
│ │
│ 目的 MAC = aa:bb:cc:dd:ee:ff │
│ 源 MAC = 本机 MAC │
│ │
│ ┌──────────────────────────────────┐ │
│ │ IP Packet │ │
│ │ │ │
│ │ 源 IP = 本机 IP │ │
│ │ 目的 IP = 8.8.8.8 │ │
│ │ │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
这张图里藏着这次旅行最重要的一次认知:
Ethernet Frame 的目的 MAC,是下一跳设备的 MAC——不是最终服务器的 MAC。而被包在里面的 IP Packet,目的 IP 始终是
8.8.8.8,从头到尾没有变过。
也就是说,IP Packet 正朝着最终目的地前进,但 Ethernet Frame,只负责把它送到眼前这一跳。
每一跳,外包装都会变
数据包这才想起,自己第一次出发的时候,曾经隐约看到过一句话:每一跳,链路层的外包装都会发生变化。当时它没太在意,现在,它终于亲眼看清楚了这是什么意思。
在这一跳:
1 |
|
当这个 Frame 到达下一跳设备(通常是一台路由器)以后,事情会这样发生:
1 |
|
于是,下一跳的样子会变成:
数据包这下真正明白了:
IP 层关心的是最终目的地,这个目的地全程不变;链路层关心的只是当前这一跳,每经过一台设备都会被重新包装一次。
同一个 IP Packet,可以在旅途中的不同链路上,被包进完全不同的 Ethernet Frame。
让 Linux 告诉我们这一切真的发生了
实验一:看邻居表
1 | ip neigh |
找到对应行:
1 |
|
这就是 IP 和 MAC 之间那条暂时的映射记录。
实验二:主动触发一次解析
1 | ping -c 1 192.168.1.1 |
再看一次:
1 | ip neigh |
这里要说清楚一件容易搞混的事:ping 用的是 ICMP,它本身并不是 ARP。 只是当主机准备向 192.168.1.1 发送 IP 数据、又发现邻居表里没有对应记录时,内核会先触发一次 ARP 解析,这才让邻居表里多出这条记录。ARP 是这个过程的副产品,不是 ping 本身。
实验三:用 tcpdump 亲眼看到那声”喊话”
如果环境允许,这是本篇最值得做的一个实验:
1 | sudo tcpdump -ni eth0 arp |
然后另开一个终端:
1 | ping -c 1 192.168.1.1 |
如果邻居表里还没有对应记录,大概率能看到:
1 |
|
这两行,就是前面故事里”喊了一声谁是 192.168.1.1”和”对方回答我在这里”的真实样子。
如果邻居表里已经有缓存,可能什么都看不到——因为根本不需要重新问。想重新观察这个过程,可以先删掉这条邻居记录:
1 | sudo ip neigh del 192.168.1.1 dev eth0 |
再重新发一次流量。
(IP 地址、接口名请替换成自己环境里实际的值;也不建议在正通过某条连接远程登录的生产服务器上,随手删除这条连接依赖的邻居项。)
三个问题,三个不同的答案
走到这里,可以把这次旅行里出现过的三件事放在一起看看,它们看起来相似,实际回答的是完全不同层次的问题:
1 |
|
整个过程串起来,就是这次旅行里最重要的一条主线:
1 |
|
(补充一句:如果这次目的地是 IPv6 地址,流程里对应的机制不叫 ARP,而是 Neighbor Discovery——但基本思路是相通的,这里先不展开。)
出发了,但这只是第一跳
Frame 终于封装完成,从网卡送了出去。
数据包松了一口气——这一路走来,它总算把”下一跳 IP”变成了真正可以交付的东西。
但它很快意识到一件事:自己现在离开的,只是本机这一小段链路。而它前面用来判断”下一跳交给谁””这一跳的 MAC 是什么”的这一整套过程——路由查询、ARP 解析、重新封装——刚才在路由器那一侧被自己隐约瞥见过一次。
它开始好奇:
当这个 Frame 抵达下一跳那台路由器时,对方会怎么处理它?它会不会,把刚才这整套判断,原样再做一遍?
这个问题,留给下一次旅行。
知道 IP 之后,为什么还需要 MAC?