阅读视图

发现新文章,点击刷新页面。
🔲 ☆

ARP 问题诊断

这是 网络断断续续 一文的答案。

答案是,在同一个子网内,同一个 IP 地址分配到了 2 个不同的设备上。

对于这种时而正常时而异常的「幽灵问题」,在没有思路的时候,可以通过对比的方法来寻找线索。

有的时候 TCP 能够连通,有的时候无法连通。那么正常的 TCP SYN 包和异常的 TCP 包之间肯定是有什么字段是不一样的。当然,也有可能 SYN 包一模一样,问题出在了其他的网络设备上或者 TCP 的另一端。但是既然题目出给读者了,那么问题的根源肯定是隐藏在抓包文件里面。

通过对比来寻找答案

正常的 TCP 连接可以完成 TCP 的握手和挥手。

异常的 TCP 连接,发出去的 SYN 直接收到了 RST。

通过对比可以发现,两个 SYN 包的 Dst MAC 是不一样的。

通过推理得出答案

如果不通过对比的方法,顺藤摸瓜,可以用下面的思路。

首先,Src IP 和 Dst IP 分别是 10.210.151.187 和 10.210.151.90,很大概率是属于同一个 /24 子网的 IP,不过要想确认的话需要看下服务器的 IP 地址是如何配置的,子网掩码是不是 /24,这点我们从给出的信息无从得知。

假设确实是属于同一个子网,那么 TCP SYN 包不经过网关,直接发给目的地址,二层的 Dst MAC 地址应该是目的 IP 的 MAC 地址。

假设不属于同一个子网,那么 TCP SYN 包经过网关,TCP SYN 包的目的地址应该是路由器的 MAC 地址。

无论是那种情况,正常情况下发送给同一个 Dst IP 的目的 MAC 应该是唯一且稳定的。通过 Conversation 统计可以看出,通讯的 IP 对只有一对,但是 MAC 的确有 2 对,这是不对的。

Statistics > conversation

原因分析

造成这种现象的原因是,IP 分配重复了,在同一个子网内,同一个 IP 地址 10.210.151.90 被分配给了多个机器。

发送 TCP 包之前,需要知道对应的 Dst IP 地址的 Dst MAC 地址,怎么知道呢?Src 机器会在整个子网内广播 ARP 询问,持有这个 IP 的机器会回复 ARP。现在子网内有两个相同的机器是相同的 IP 地址。这两台机器的 IP 地址一样,MAC 地址不一样。如果我们现在 arping 10.210.151.90 ,会得到如下的回复:

$ arping -c 3 10.210.151.90
ARPING 10.210.151.90
42 bytes from 5a:65:98:0c:82:02 (10.210.151.90): index=0 time=944.055 usec
42 bytes from c2:10:70:f2:ae:ab (10.210.151.90): index=1 time=997.915 usec
42 bytes from 5a:65:98:0c:82:02 (10.210.151.90): index=2 time=13.799 usec
42 bytes from c2:10:70:f2:ae:ab (10.210.151.90): index=3 time=109.388 usec
42 bytes from 5a:65:98:0c:82:02 (10.210.151.90): index=4 time=6.670 usec
42 bytes from c2:10:70:f2:ae:ab (10.210.151.90): index=5 time=48.983 usec

--- 10.210.151.90 statistics ---
3 packets transmitted, 6 packets received,   0% unanswered (3 extra)
rtt min/avg/max/std-dev = 0.007/0.353/0.998/0.438 ms

可以看到,我们发出去 3 个询问,却收到 6 个回复。说明每一个询问得到了 2 个答案。(其实,在诊断和验证这个问题的过程中,最快的方式就是用 arping 来测试一下,而不是抓包来分析。但是这里我们主要讨论抓包技术,所以拿来当作一个分析的案例。)

那么 Src 收到两个 ARP 应答,会以哪一个为准呢?答案是以先收到的为准。

$ ip nei show
10.210.151.90 dev h2-eth0 lladdr 5a:65:98:0c:82:02 REACHABLE

但是 ARP 请求广播出去,这两个 ARP 应答哪一个先到是无法确定的,有的时候 5a:65:98:0c:82:02 先到,有的时候 c2:10:70:f2:ae:ab 先到,所以发送给 10.210.151.90 的包,有的时候发给了 MAC 地址为 5a:65:98:0c:82:02 的机器,有的时候发送给了 MAC 地址为 c2:10:70:f2:ae:ab 的机器。

补充一点,在实际的问题排查过程中,我们的 TCP 连接测试失败,最好的排查方法就是去对端进行抓包,看一下对端的机器是否收到了 SYN 包。以及在实际的现象中,客户端收到了 Connection refused ,通常意味着收到了 RST 报文(可能来自真实对端,也可能来自其他设备)。我们在对端抓包的过程中会发现即没有收到 SYN,也没有发出 RST,这说明包到别的地方去了。进而,我们可以发现字网内还有一个「李鬼」的问题。

那么,为什么 ping 的测试完全正常呢?

因为 ICMP 是无连接协议,,ping 的 ICMP 协议回复,是由 Kernel 负责的,所以无论 ICMP 包发送到哪一个机器上,由于它们都设置了这个 IP 地址,所以都可以做出回复。其实抓包中的回复是来自不同的机器,只不过 ping 不知道,只要是收到了回复,就认为一切正常。

可以把 Src MAC 地址添加到 Column 显示,就可以发现其实 ping 的回复来自不同的设备。

最后一个问题,为什么会有 IP 重复的问题呢?

DHCP 可以用来动态分配 IP 地址给设备,但是一般只用于客户端,比如办公网、家用网络中的终端设备,这些设备一般作为连接的发起方,不需要 listen 一个固定的 IP 地址,IP 地址的动态变化对它们影响不大。但是在 IDC 的网络中,每一个服务器的软件都需要固定的 IP 地址,一般不用 DHCP 来动态分配,而是用中心化的 IPAM 系统1追踪 IP 地址的分配情况。如果里面记录的地址不正确,比如,一个地址已经在使用中了,但是并没有在 IPAM 记录,就造成 IP 地址重复的问题。

  1. https://en.wikipedia.org/wiki/IP_address_management ↩

==抓包破案录==

这篇文章是抓包破案录系列文章(之前叫做《计算机网络实用技术》,后来改名了)中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网络抓包与分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

如果您正在阅读的是题目类的文章,这个目录内容正好用来隔离其他读者的评论。读完题目可以稍作暂停,进行思考,继续向下滑动,可能会被其他的读者剧透答案。

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

Warp 2.0 带来 AI 代理工具和团队协作功能

Warp 2.0 带来 AI 代理工具和团队协作功能

这个主要版本提供 AI 代理、更好的工具和更快的流程。

谷歌最近推出其 Gemini CLI,这是一个人工智能驱动的命令行接口,旨在通过提供上下文感知的代码建议、简化复杂命令以及在终端内实现自然语言交互来提升生产力。

在这种日益激烈的竞争中,Warp 2.0 现已发布,将自身重新定义为“代理开发环境(Agentic Development Environment, ADE)”,将 AI 代理直接引入开发者工作流程。

Warp 2.0 带来 AI 代理工具和团队协作功能
Warp 2.0

如果你一直在关注 Warp 的发展,这一举措可能并不令人意外。这是一个重大的变化,从根本上改变 Warp 的能力。通过引入 AI 代理,Warp 从一个终端转变为更加智能的存在。这些代理可以帮助编写代码、自动化任务,甚至在终端内处理部分工作流程。

Warp Drive 是另一个重要的新增功能,它帮助你和你的团队将重要内容(如命令、提示和环境设置)集中在一个地方。AI 代理还可以利用这些信息提供更智能、更实用的建议。Warp 团队并未忘记核心终端体验;现在有了更强大的命令编辑功能、无缝的鼠标支持和语法高亮,使终端工作比以往更简单、更快速。

🔲 ☆

AI + terminal 等于 Warp?

作为一个开发者,终端是你最亲密的伙伴。它可能是你调试代码的战场,也可能是你部署项目的指挥部。但让我们面对现实:大多数人用的终端,要么是 macOS 自带的 Terminal,要么是久负盛名的 iTerm2。这些工具很可靠,但它们也像老派的骑士——忠诚,却不够灵动。然后,Warp 来了,像个不按套路出牌的叛逆者,直接挑战终端的传统定义。

Warp 和 iTerm2,一个是新世代的先锋,一个是久经考验的经典。它们有何不同?Warp 又在哪些地方彻底碾压了 iTerm2?让我们一探究竟。

🔲 ☆

Linux interface Vlan 和 Bond 配置错误问题排查

昨天同事报告了一个 Linux 机器网络问题,现象是:一台服务器无法 ping 192.168.1.253,但是可以 ping 192.168.1.252 和 192.168.1.254. 这三个 IP 都是交换机的 IP,并且和和服务器的 IP 在同一个子网下。

服务器使用了 bond1 分别连接两台交换机2,两台交换机通过 VRRP 协议提供一个高可用的网关 IP3。其中,网段的最高位一般是 VRRP 的 VIP,即 192.168.1.254,而最高位 -1 和 -2 分别是两个交换机的物理 IP,即 192.168.1.253 和 192.168.1.252 分别是两台交换机。

于是,看到这个现象,自然而然地想到是其中一台交换机有问题,192.168.1.253 已经挂了,192.168.1.252 还存活,并且担任了 192.168.1.254 的 VIP 的责任。

先去这台服务器 ping 了一下,果然是 ping 不通的,ping 显示的错误信息是 Destination Host Unreachable。然后在服务器抓包,确认下 ICMP reply 确实没有发送回来。tcpdump -i bond0 icmp. 抓包确实没有看到 ICMP reply 包,但是奇怪的是,居然连 ICMP echo 也没有抓到

之后又去检查了交换机的配置,包括 channel-group,VLAN 配置,ACL 等等,也确认了下两台交换机之间的横连状态是正常的。这时候看起来不像是交换机的问题了。使用另一台服务器 ping 了一下这三个 IP,.252, .253, .254 都是通的。那应该是服务器的问题而不是交换机的问题。

其实这部分有些走弯路,因为 ping 明确显示 Destination Host Unreachable,说明这个包并没有发出去;而且 tcpdump 也没有抓到包,也可以印证。

接下来继续在服务器上定位问题。

ICMP 发包有问题,就先检查一下发包链路。之前遇到过类似错误,是 iptables 的 OUTPUT chain 把包 drop 了,于是先检查了 iptables,确认没有相关的 DROP。

ICMP 是基于 IP 层的协议,IP 层的协议依赖 ARP 协议来找到 MAC 地址,然后封装成二层 Frame,才能发出去,接下来去检查 ARP。(其实上一步直接检查 iptables 是不合理的,ARP 是第一步,有了 ARP 才可能构造出来完整的 Frame 开始发送,应该先从 ARP 开始排查)。

检查 arp -a | grep .253,发现 ARP 的 cache 结果是 <incomplete>. 然后用 arping 192.168.1.253 验证 ARP request 是否能得到正常的 reply,发现结果都是 Timeout。

到这里已经知道为什么 ping 会失败了,因为服务器得不到这个 IP 对应的 ARP 请求,所以 ping 无法将 ICMP request 的包发送出去,直接报错了。

接下来就定位为什么 ARP 会失败。

正常来说,ARP 应该从 bond0 接口发送出去一个 request,然后收到一个 reply,刷新服务器的 ARP cache entry。

服务器的 interface 配置如下,服务器所在的 VLAN 是 1000,和交换机做了 Trunking4,发送包的路由是走 bond0.1000@bond0 这个 interface,bond0.1000@bond0 是一个虚拟 interface,主要的功能是,发包的时候对包进行 802.1Q VLAN 封装,然后通过底层的 interface——在这里是 bond0——发送出去,收包的时候对 VLAN 进行解封装。

root@ubuntu-1:/$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether b6:db:e6:76:dd:8a brd ff:ff:ff:ff:ff:ff
3: bond0.1000@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether b6:db:e6:76:dd:8a brd ff:ff:ff:ff:ff:ff
4: eth0.1000@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether b6:db:e6:76:dd:8a brd ff:ff:ff:ff:ff:ff
143: eth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc fq_codel master bond0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether b6:db:e6:76:dd:8a brd ff:ff:ff:ff:ff:ff
144: eth1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc fq_codel master bond0 state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether b6:db:e6:76:dd:8a brd ff:ff:ff:ff:ff:ff
接口的逻辑图

我首先在 bond0 抓包,确认 ARP 的发送和接收在协议上是正常的。

结果在这一步就发现问题了,bond0 抓包发现,只有发出去的包,没有收到的包。

为啥交换机不响应 ARP 了呢?

这时候又怀疑是交换机的问题,去检查了交换机的两个端口配置。没有发现问题。而且在其他机器上,ping 和 arping 都是没有问题的,交换机设备的问题可能性比较小。

也不会是服务器安全策略的问题,如果是的话,tcpdump 也会先抓到包的,在后面才会被 iptables 之类的 DROP 掉。

于是仔细想一想交换机和服务器之间经过了哪些组件,网卡收包,中断,网卡 driver,bond driver,协议栈处理。抓包都没抓到,说明问题出在协议栈之前,于是怀疑到 bond driver 头上去。

下一步,在物理 interface 上抓包,确认物理 interface 到底收到了 ARP reply 了没有。结果是,发现 eth0 这个 interface 收到了 ARP reply!

ARP reply 在 eth0 上收到了,但是 bond0 上没收到。这下感觉快要得到答案了。bond 有两个 slave,我把 eth0 shutdown 了,只留下 eth1,然后网路正常了。那要么是 bond driver 真的有问题,要么是我们的配置有问题。从经验上看,Linux driver 存在 bug 的概率要远远小于我们的配置错误。于是我去检查 bond 相关的配置。

检查 bond 状态 (/proc/net/bonding/bond0 文件), bond 配置,都没发现问题。可能是 eht0 这个接口有问题?

在重新看 interface 的时候(即上面的 ip link 命令和输出),我发现了可疑的一条 interface:

4: eth0.1000@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether b6:db:e6:76:dd:8a brd ff:ff:ff:ff:ff:ff

这里多出来一个 VLAN interface。

所以,实际上的 interface 配置应该是如下这样。由于 eth0.1000 的存在,我怀疑 eth0 收到的 ARP reply 实际上是送给了 eth0.1000@eth0 而不是 bond0,然后在 ARP 协议处理的时候,Linux 认为我们没有从 eth0.1000 发送出去 ARP request,但是却收到了 ARP 响应,属于 Gratuitous ARP5. 而发送 ARP request 的 bond0,从来没有收到 ARP reply。ARP cache 是 per interface 的,所以 bond0 无法发送 ICMP 出去。

eth0.1000 的配置

证明这个猜测很简单,只要在 eth0.1000@eth0 抓包,看是否有 ARP reply 就好了。抓包发现果然有。

并且把这个接口的 arp_accept 打开,让其接受 Gratuitous ARP,发现 ARP cache 出现了如下记录:

proot@ubuntu-1:/$ arp -a
? (192.168.1.253) at c6:34:22:fc:78:b4 [ether] on eth0.1000

说明这个结论是正确的。到这里就发现,其实问题不仅仅是 ARP 的问题,因为 bond 的两个 slave 有一个不对,收包的时候可能是从 eth0 收,也可能是从 eth1 收,取决于交换机的 hash 策略6。如果从 eth0 进来,那么协议栈的 skb 的 device 就会是 eth0.1000@eth0,所有有连接的协议处理都匹配不上。

于是我 shutdown eth0.1000@eth0 这个接口,理论上机器的配置应该都是对的了。

结果不是,问题依然存在,有点让人怀疑人生。由于接口 down 了就无法抓包了,不太好确认包是不是还在往 eth0.1000@eth0 送了。此处又花了一些时间排查,因为怀疑自己的推论是错误的,是不是有别的地方导致这个问题?一通误打误撞,决定删除这个多余的接口,然后网路就完全恢复了。从结果看,只 shutdown 这个接口不能阻止包往这个 vlan 接口送,得删除才行。

事后我们得知,这台服务器在 infra 团队交付的时候存在问题,应该配置 bonding,但是没有配置,只是在一条线(eth0)上配置了 VLAN。我们的同事拿到机器之后修复了 bonding 问题,但是并没有删除 eth0.1000@eth0 这个 VLAN 虚拟接口,导致产生了非预期的行为。

后来看了下源代码,发现 VLAN 的处理确实优先级比较高,在 __netif_receive_skb_core7 这里就会执行 vlan_do_recieve8,然后会把 device 的 id 设置在 skb 上。这个逻辑比 bond driver 的逻辑靠前,导致后续协议栈的处理,会认为这个包是从 eth0.1000@eth0 收到的,而不是从 bond0 收到的。

  1. 数据中心网络高可用技术之从服务器到交换机:802.3 ad ↩
  2. 数据中心网络高可用技术之从交换机到交换机:MLAG, 堆叠技术 ↩
  3. 数据中心网络高可用技术之从服务器到网关:首跳冗余协议 VRRP ↩
  4. VLAN Trunking Protocol ↩
  5. 特殊的 ARP 用法:Gratuitous ARP, ARP Probe 和 ARP Announce ↩
  6. 数据中心网络高可用技术之从服务器到交换机:链路聚合 (balance-xor, balance-rr, broadcast) ↩
  7. https://elixir.bootlin.com/linux/v6.12.6/source/net/core/dev.c#L5457 ↩
  8. https://elixir.bootlin.com/linux/v6.12.6/source/net/8021q/vlan_core.c#L10 ↩
🔲 ⭐

ARP Flux 问题和解决方法

我们写程序不需要写 ARP request 和 response 相关的逻辑,因为这部分是操作系统帮我们做的。

Linux 处理 ARP 请求的逻辑是:

如果操作系统收到了一个 ARP reuqest,并且本机中某一个接口配置了 ARP request 中请求的 IP,那就回复这个 ARP request,回复的 ARP response 中,使用收到此 ARP request 的 interface 的 MAC 地址作为答案。

这个逻辑看起来没有问题:从 Linux 的视角,既然我能从一个 interface 收到 ARP 请求,那么对方向这个 interface 发送数据包,我也可以从这个 interface 收到,所以,回复 interface 的 MAC 地址即可。

但是,在下面这种情况就会出问题:

  1. Linux 有两个 interface 接入到了同一个交换机上;
  2. 这两个 interface 配置的 IP 地址在同一个广播域;

满足这两个条件的话,当有 LAN 中有主机发送 ARP request 广播包,交换机会转发到自己的所有端口,Linux 会从这两个端口都收到此 ARP request 广播包。按照以上处理逻辑,Linux 会回复 2 个 ARP reply,因为收到了两个 request,两个 interface 各回复一次,并且这两个 ARP reply 分别是两个 interface 的 MAC 地址。

实验环境如下图所示:

ubuntu-1 有两条线接到同一个交换机

配置好 IP 之后,我们在 ubuntu-2 主机发送 ARP request。

root@ubuntu-2:/$ arping 192.168.1.2 -c 3
ARPING 192.168.1.2
42 bytes from aa:b6:66:3c:98:45 (192.168.1.2): index=0 time=1.017 msec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=1 time=1.032 msec
42 bytes from aa:b6:66:3c:98:45 (192.168.1.2): index=2 time=300.636 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=3 time=557.981 usec
42 bytes from aa:b6:66:3c:98:45 (192.168.1.2): index=4 time=308.836 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=5 time=546.011 usec

--- 192.168.1.2 statistics ---
3 packets transmitted, 6 packets received,   0% unanswered (3 extra)
rtt min/avg/max/std-dev = 0.301/0.627/1.032/0.299 ms

一共发送了 3 个 request,却收到了 6 个 ARP reply。并且有两种 MAC 地址。

这个就是 ARP Flux 问题。

解决方法0: 配置不同的 IP subnet

因为 ARP 是在 LAN 内的,如果两个 interface 分别配置在不同的 subnet,就不会有这个问题了。

但是有时候我们的软件守交换机网段的限制,只能配置在一个网段。并且有些场景需要两个线路和两个 IP,比如管理和数据面分开(带外管理),就又会遇到这个问题。

解决方法1: 隐藏其中一个接口

使用 ip link set dev eth0 arp off 可以禁用 interface 的 ARP 恢复,这样,在 LAN 上就相当于把这个 interface 隐藏了,因为没有人可以发现它的 MAC 地址了。在四层负载均衡中,如果使用 DSR 的模式并且不加隧道的话,就需要对 RS 的 VIP 禁用 ARP 回复1

这里有一个有意思的现象:假设 192.168.1.2 配置在 eth0 上,但是我们把 eth0 的 ARP 禁用了。

192.168.1.2 所在的 eth0 ARP禁用

这时候如果请求 192.168.1.2 的地址,会拿到 eth1 的 MAC 地址。参考上文讨论过的 Linux ARP 工作原理。

root@ubuntu-2:/$ arping 192.168.1.2 -c 3
ARPING 192.168.1.2
42 bytes from aa:b6:66:3c:98:45 (192.168.1.2): index=0 time=313.417 usec
42 bytes from aa:b6:66:3c:98:45 (192.168.1.2): index=1 time=240.823 usec
42 bytes from aa:b6:66:3c:98:45 (192.168.1.2): index=2 time=294.223 usec

--- 192.168.1.2 statistics ---
3 packets transmitted, 3 packets received,   0% unanswered (0 extra)
rtt min/avg/max/std-dev = 0.241/0.283/0.313/0.031 ms

这个方法只能通过 interface 级别来设置,如果一个 interface 上有多个 IP 地址,那么只能全部禁用或者开启。

如果要 by IP 来禁用 ARP,可以使用 arptables(8)2 3.

解决方法 2: arp_ignore

直接禁用 ARP 差不多等于把这个 interface 关闭了,大部分情况下不是我们想要的结果。

我们希望的效果是对于 ARP request 只回复一次,并且只有 IP 在自己的 interface 上才回复,不要代替其他 interface 回复。

通过 arp_ignore4 可以改变 Linux 的 ARP 行为:

  • 0 – 默认,只要本地有的 IP 就会回复,无论是哪一个 interface;
  • 1 – 只有当 ARP request 询问的 IP 配置在收到 ARP 的接口上时,才会回复;
  • 2 – 同 1,并且 sender 的 IP 在本地 IP 的相同 subnet 内,才会回复;
  • 3scope host 不会回复,只有 scope linkscope global 才会回复 (可以通过 ip address 命令查看配置的 IP scope);
  • 47 – 保留字段;
  • 8 – 所有 local address 都不回复;

配置方法:通过 sysctl -w 可以修改配置。arp_ignore 是 per interface 的配置。

root@ubuntu-1:/$ sysctl -a | grep arp_ignore
net.ipv4.conf.all.arp_ignore = 0
net.ipv4.conf.default.arp_ignore = 0
net.ipv4.conf.eth0.arp_ignore = 0
net.ipv4.conf.eth1.arp_ignore = 0
net.ipv4.conf.eth2.arp_ignore = 0
net.ipv4.conf.eth3.arp_ignore = 0
net.ipv4.conf.lo.arp_ignore = 0

其中 all 是一个全局变量,最终在 interface 上生效的值是 max(all, eth0)。以最大的为准。

default 是模板变量,在新建一个 interface 的时候,会自动使用这个值。

我们可以给所有的 interface 都设置为 1.

root@ubuntu-1:/$ sysctl -w net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_ignore = 1

然后再发送 arp 请求,就会发现回复之后一个了,并且是正确的那个。

root@ubuntu-2:/$ arping 192.168.1.2 -c 3
ARPING 192.168.1.2
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=0 time=602.926 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=1 time=312.438 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=2 time=236.994 usec

--- 192.168.1.2 statistics ---
3 packets transmitted, 3 packets received,   0% unanswered (0 extra)
rtt min/avg/max/std-dev = 0.237/0.384/0.603/0.158 ms

解决方法3: arp_filter

另一个方法是使用 arp_filter 参数,这个参数是一个 bool,默认是 0 ,如果开启为 1,就意味着,如果收到 ARP request,ARP 请求的 IP 为 A,收到 ARP 的 interface 为 X 和 Y,那么 kernel 就测试路由,假设要发出去 source IP 为 A 的包,从 X 发出去还是从 Y 发出去。如果从 X 发出去,那么只有 X 会回复。(source based rotuing).

举例来说,我们现在有两个接口和两个 IP,回复 192.168.1.10 的 ARP 时,可以测试当前的路由情况:

root@ubuntu-1:/$ ip route get 192.168.1.10 from 192.168.1.2
192.168.1.10 from 192.168.1.2 dev eth0 uid 0
    cache
root@ubuntu-1:/$ ip route get 192.168.1.10 from 192.168.1.3
192.168.1.10 from 192.168.1.3 dev eth0 uid 0
    cache

可以看到无论 source IP 是哪一个,都会从 eth0 口出。

这是因为我们的 ip route 配置:

root@ubuntu-1:/$ ip route show table all
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.2
192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.3

会永远走第一条路由。

所以我们如果发送 ARP request,对 .2.3 两个地址,得到的结果会是一样的,因为都是 eth0 在回复。

root@ubuntu-2:/$ arping 192.168.1.2 -c 3
ARPING 192.168.1.2
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=0 time=165.606 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=1 time=161.927 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.2): index=2 time=179.383 usec

--- 192.168.1.2 statistics ---
3 packets transmitted, 3 packets received,   0% unanswered (0 extra)
rtt min/avg/max/std-dev = 0.162/0.169/0.179/0.008 ms
root@ubuntu-2:/$ arping 192.168.1.3 -c 3
ARPING 192.168.1.3
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.3): index=0 time=230.590 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.3): index=1 time=182.955 usec
42 bytes from 4a:ae:15:cc:55:69 (192.168.1.3): index=2 time=149.800 usec

--- 192.168.1.3 statistics ---
3 packets transmitted, 3 packets received,   0% unanswered (0 extra)
rtt min/avg/max/std-dev = 0.150/0.188/0.231/0.033 ms

如果我们把第一条路由删了,那么现在就都走 eth1 了,这时候 ARP 结果就都是 eth1 了。

root@ubuntu-1:/$ ip route del 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.2
root@ubuntu-1:/$ ip route get 192.168.1.10 from 192.168.1.3
192.168.1.10 from 192.168.1.3 dev eth1 uid 0
    cache
root@ubuntu-1:/$ ip route get 192.168.1.10 from 192.168.1.2
192.168.1.10 from 192.168.1.2 dev eth1 uid 0
    cache

Lookback 接口是否会回复 ARP?

答案是是的(虽然听起来很没道理)。这个主要是跟路由有关。

当我们在给 lo 绑定一个地址的时候,kernel 会默认添加 2 条路由,lo 所在网段和 lo 的地址会被标记成 dev lo 的本地路由。

root@ubuntu-1:/$ ip route show table local
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
local 192.168.1.0/24 dev lo proto kernel scope host src 192.168.1.2
local 192.168.1.2 dev lo proto kernel scope host src 192.168.1.2
broadcast 192.168.1.255 dev lo proto kernel scope link src 192.168.1.2

其实,当我们给 lo 地址添加 192.168.1.2/24 的时候,由于自动添加的 192.168.1.0/24 的 route (第5行),导致我们其他的接口也不会回复这个网段的 ARP 请求了。这个时候从 ubuntu-2 上 arping 192.168.1.10192.168.1.11 都会 timeout。

这个是默认的行为。但是如果我们改一下,删除这两条路由,添加另一条从 eth0 口出的路由的话,就会发现,arping lo 接口的 IP,也会收到 ARP response 了。

root@ubuntu-1:/$ ip route del local 192.168.1.0/24 dev lo proto kernel scope host src 192.168.1.2
root@ubuntu-1:/$ route del local 192.168.1.2 dev lo table local proto kernel scope host src 192.168.1.2
root@ubuntu-1:/$ ip route add local 192.168.1.2 dev eth0 proto kernel scope host src 192.168.1.2
root@ubuntu-1:/$ ip address show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet 192.168.1.2/24 scope global lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever

在 ubuntu-2 可以 ping 通。

root@ubuntu-2:/$ arping 192.168.1.2 -c 3
ARPING 192.168.1.2
42 bytes from 56:6b:38:20:7e:56 (192.168.1.2): index=0 time=274.702 usec
42 bytes from 56:6b:38:20:7e:56 (192.168.1.2): index=1 time=299.748 usec
42 bytes from 56:6b:38:20:7e:56 (192.168.1.2): index=2 time=296.380 usec

--- 192.168.1.2 statistics ---
3 packets transmitted, 3 packets received,   0% unanswered (0 extra)
rtt min/avg/max/std-dev = 0.275/0.290/0.300/0.011 ms

所以说 lo 接口是否回复 ARP request,主要和 ip route 的设置有关。

  1. 四层负载均衡漫谈 ↩
  2. arptables(8) – Linux man page ↩
  3. Using arptables to disable ARP ↩
  4. Kernel 的 ip sysctl 文档 ↩
🔲 ⭐

特殊的 ARP 用法:Gratuitous ARP, ARP Probe 和 ARP Announce

在 Ethernet 环境中,所有的数据最终都要以二层 Ethernet Frame 的形式发送,要添加 Src MAC, Dst MAC, 以及其他的 header,比如 CRC 等,Ethernet 就会把数据送达到目的地。

我们在编程的时候经常指定 IP 和 Port,但是几乎从没有指定过 MAC 地址,那么 Frame 是怎么发出去的呢?这是操作系统帮我做了这件事。如果系统不知道一个 IP 对应的 MAC 地址是什么,系统会发送一个广播的 ARP 请求,(由于这个请求也是要通过 Ethernet 发送出去的,所以也要填充 Dst MAC,Dst MAC 就是 ff:ff:ff:ff:ff:ff。)询问谁有目的 IP,如果有,请回复给 MAC-B。广播域中所有的 Host 都会收到这个请求,只有拥有这个 IP 的 Host 才会回复给 MAC-B 自己的 MAC 地址,这样,B 就知道了这个 IP 对应的 MAC 地址。

这就是普通的 ARP request 和 response 的过程。

其中,里面的几个值得注意的 field 如下:

对于 ARP request 来说:

  • Operation: 1 (1表示 request, 2 表示 response);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:全1,即广播给所有主机;
  • Dst IP:是要询问的 IP;

对于 ARP Response 来说:

  • Operation: 2
  • Src MAC: sender(sender 在这里是响应者) 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:receiver (在这里是 ARP 问题的发出者)的 MAC;
  • Dst IP:receiver IP;

ARP 是一个很简单的协议,但是人们基于交换机和主机的工作原理,设计了其他的用法。这些用法其实并不复杂,最重要的就是理解这些设备最基本的工作原理,这样就可以理解这些特殊的 ARP 设计意图了。

Gratuitous ARP

在之前的博客中介绍过两次使用 Gratuitous ARP 的例子。Gratuitous ARP 的目的是更新其他设备的 ARP cache,或者交换机 mac-address table。

在 VRRP1 中,目的主要是切换网关,更新的对象是 mac-address table。交换机的 mac-address table 主要是通过收到的包的 Src MAC 来学习 MAC 对应的物理 port 的,所以最重要的是 Src MAC,其他的都不重要。

所以在 VRRP 中的 Gratuitous ARP 是这样的:

  • Operation: 1 (request);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:全1,即广播给所有主机;
  • Dst IP:全1;

在 Linux 的 balance-alb bonding 模式2中,ARP 的目的是为了刷新其他主机的 ARP Cache,并且希望不同的主机拿到不一样的 MAC 地址,所以使用的 ARP 是 response,并且是 unicast。

  • Operation: 2 (response);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:bond driver 记录的 MAC;
  • Dst IP:bond driver 记录的 IP;

取决于使用场景,Gratuitous ARP 也是 broadcast 的 reply,将 response 信息广播给所有的主机3

ARP Probe

ARP Probe 的作用是,在分配到一个 IP 之后,但是在 IP 使用之前,可以先用 ARP 协议来检查一下当前的 LAN 内有没有人在使用这个 IP。

检查的方式就是用 ARP 询问这个 IP 的 MAC 地址,如果有主机正在使用,那么就会收到 ARP reply。

但是这里有一个问题。一个主机如果收到了 ARP request,询问谁有 IP X?请发送回复给 IP 地址 Y at MAC 地址 Z。即使这个主机没有 IP X,也不会单纯丢弃这个 ARP request,而是会从这个 ARP request 中学习到,IP Y 对应 MAC Z,会将它放到自己的 ARP cache 中。

这里我们只是想检查一个 IP 是否正在被使用,我们还没有真正地开始使用这个 IP。如果用普通的 ARP 请求来询问,假设这个 IP 已经被使用,那么发出去的 ARP request 就会传达错误的信息,其他主机就会根据这个 ARP request 来更新自己的 ARP cache。

为了解决这个问题,ARP Probe 使用的 request 将 Src IP 设置为 0.0.0.0,这样,这个 ARP request 就完全无害了。

ARP 请求如下(加粗部分是和普通的 ARP request 不一样的部分)。

  • Operation: 1 (request);
  • Src MAC: sender 的 MAC;
  • Src IP: 0.0.0.0
  • Dst MAC:全1,即广播给所有主机;
  • 询问的 IP:自己将要使用的 IP;

ARP Announce

ARP Announce 用于在 ARP Probe 确定没有问题之后,决定使用这个 IP,但是再一次确定唯一性。

ARP Announce 发送一个普通的 ARP request, 在 LAN 内询问自己将要使用的 IP,正常情况下不会收到任何回复,因为这个 IP 是自己的。异常情况下收到回复,那么说明 LAN 已经有人在使用这个 IP 了,需要终止继续使用这个 IP。

ARP Announce 是普通的 ARP 请求,和 Gratuitous ARP 很像。询问的目标 IP 是自己的 IP。

ARP Announce 又叫做 Unsolictied ARP。

ARP Announce 和 ARP Probe 的唯一区别就是 Src IP 不同,ARP Announce 使用自己的 IP 来发送,所以也会更新 LAN 内其他设备的 ARP cache。

  • Operation: 1 (request);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:全1,即广播给所有主机;
  • Dst IP:自己即将使用的 IP;

参考资料:

  1. 首跳冗余协议 VRRP ↩
  2. 数据中心网络高可用技术之从服务器到交换机:balance-tlb 和 balance-alb ↩
  3. Gratuitous ARP https://www.practicalnetworking.net/series/arp/gratuitous-arp/ ↩
🔲 ☆

UGF 源码阅读笔记:(1)安装

UnityGameFramework(UGF) 是由 Ellan Jiang 开发的一个 Unity 游戏开发框架。我决定采用它作为我最近为公司开发的一款 3D 扫雷游戏的开发框架,为此,我觉得有必要仔细阅读它的源码,并做好笔记。另外,我还建了一个仓库去写一些测试代码。

官网已经有教程教用户如何安装框架了。它推荐的安装方式是安装 Unity 插件包,其中核心部分的代码都打包成 DLL 形式了。虽然这种方法方便了用户使用,但我的目的是阅读和调试代码,我得拿到所有的代码。

下面是我的安装方法:

  • 下载某一个版本(如我当前使用的是 v2019.11.09)的 UnityGameFramework,并将它拷贝到新建的 Unity 工程的 Assets 目录之中,如:

  • 删除 UnityGameFramework/Libraries 文件夹下的 GameFramework.dllGameFramework.xml 文件。
  • 下载某一个版本的 GameFramework,并将它的源码拷贝到 Unity 工程的 Assets 目录之中。其存放位置任意,如我就将它放进了 UnityGameFramework 目录之中:

    然后在 GameFramework 文件夹下新建一个 GameFramework.asmdef 文件:

      {
          "name": "GameFramework",
          "references": [],
          "includePlatforms": [],
          "excludePlatforms": [],
          "allowUnsafeCode" : true
      }
    

    然后让 UnityGameFramework.Runtime.asmdef 依赖 GameFramework.asmdef,让 UnityGameFramework.Editor.asmdef 同时依赖 GameFramework.asmdefUnityGameFramework.Runtime.asmdef 即可。

      {
          "name": "UnityGameFramework.Runtime",
          "references": [
              "GameFramework"
          ],
          "includePlatforms": [],
          "excludePlatforms": []
      }
    
      {
          "name": "UnityGameFramework.Editor",
          "references": [
              "UnityGameFramework.Runtime",
              "GameFramework"
          ],
          "includePlatforms": [
              "Editor"
          ],
          "excludePlatforms": []
      }
    

如此,整个框架就安装好了。接下来我们可以新建一个空场景 LaunchScene.unity 作为我们的游戏的启动场景,然后把框架提供的 GameFramework.prefab 拖入到场景中:

现在点击运行按钮就可以让框架代码跑起来了。

❌