TLDR :在同一个广播域内存在多个路由器/转发器如 VPN 网关、DR 模式的 IPVS 时,需注意内网主机的 ARP 配置,否则默认配置下 Linux 内核会使用所有可用 IP 进行 ARP ,可能引起被称为 ARP Flux 的混乱现象,一个典型现象为:网卡会使用绑定在其他网卡上的 IP 地址进行收发包。net.ipv4.conf.all.arp_ignore
,net.ipv4.conf.all.arp_announce
,net.ipv4.conf.all.arp_filter
等参数可以调整这一行为
背景,内网一台虚拟机用 wireguard 和外部一个 CN2 出口打了隧道,希望将这台虚拟机配置为内网 VPN 网关,其他机器将任意 IP 的路由指向此虚拟机(以下简称 gw)即可实现走 CN2 访问目的 IP。
基本步骤
wireguard 的配置可以使用 https://github.com/burghardt/easy-wg-quick 等简化,以下步骤皆假定 wireguard 已经正确配置,MTU 等配置不在此讨论。 在 gw 上配置内核参数:
为 gw 配置两个网络接口 ethA 和 ethB,分别配置 IP 192.168.0.a/24 和 IP 192.168.0.b/24 且在同一子网,隧道接口为 wghub,peer IP 为另一个网段的地址 c.c.c.c ,ethA 用作 SSH 登入和其他一些对内网服务的入口,ethB 作为转发网关且正常情况下不接受入站流量只接受转发流量,大致思路为:
- 对 ethB 接口的所有入包打 mark,因为入包目的地址为客户端要访问的公网地址,源地址为内网客户端,报文中没有 ethB 的 IP 192.168.0.b,不能根据 192.168.0.b 进行 match
- iprule 匹配带 mark 的报文单独使用一张路由表使用 wghub 作为出站接口,默认路由指向 VPN peer
- 将发往 VPN 接口的包进行一次 SNAT 将源地址改为 c.c.c.c
完成上述配置后,内网其他机器 ip route add 1.1.1.1 via c.c.c.c
并 mtr 理论上可以看见流量经过 c.c.c.c 并从 VPN peer 出站,抓包可观察到流量从 ethB 入,而 wghub 出包源地址为 c.c.c.c,watch iptables -t [nat|mangle] -nvL
可见计数器变化。
如果出现问题,ip route flush cache
可能有效,可在 gw 和客户端机器分别进行抓包调试,使用最新版本 libpcap 编译 tcpdump 支持 LINUX_SLL2,可显示每个包的入站接口,便于排查问题。
ARP Flux 问题
在实际操作中,发现一个现象,按照上述流程配置过 iptables 和路由规则后,有时仍会发现内网的包不会按照预期转发到 gw 的 wghub 接口,此时在 gw 抓包会发现:内网其他主机 forward 往 192.168.0.b 的包没有从 ethB 进入,而是从 ethA 进入,然而 192.168.0.b 并没有绑定在 ethA 上,查看内网其他主机的 ARP ,发现 ARP 缓存是错误的,ip neigh flush all
后使用 arping
进行测试,发现无论请求 192.168.0.a 还是 192.168.0.b,gw 都会同时将两张网卡的 MAC 地址作为响应,且响应返回时先后顺序固定,因此不明就里的客户端就会取第一个响应记录到 ARP 表,借用网络上一张图直观的描述遇到的现象(此时 Host B 即为我们环境中的 gw):
经过进一步的测试发现,gw 返回 ARP 响应的先后顺序和路由表中两条本地子网路由的顺序有关,而这两条路由的顺序又和两张网卡的启动顺序有关,最后结论上造成了 gw 上两张网卡启动顺序变化有可能导致网关配置失效这种奇妙的影响。
更进一步地,Linux 处理网络包甚至不考虑这个网卡设备的 UP/DOWN 状态,即使网卡状态标记为 DOWN,只要设备配置了 IP,这个设备上的 IP 即可被内核响应,比如在我们的测试环境里,执行:
这个 PING 是可以收到回复的,甚至 TCP 连接也是可以正常建立的,注意这里不能使用 ifdown ethB
操作,因为 ifdown
不仅仅会将接口标记为 DOWN,还会进行删除路由、删除接口上的 IP 等操作,那自然不会有响应。
经过一通搜索,最后发现这个现象有一个名称:ARP Flux,http://linux-ip.net/html/ether-arp.html#ether-arp-flux 较为详细的描述了这个问题和一些通用场景下的解决方法。
造成这一现象的根本逻辑是:“IP addresses are owned by the complete host on Linux, not by particular interfaces”(来自 kernel.org 关于 arp_filter
参数的说明),即 IP 地址属于内核,而不属于接口,使用这样的响应可以提高网络可达的可能性,Linux 还有一些其他行为也符合这一逻辑,如从本机访问任一网卡接口上的 IP 时,走的实际都是 lo 而不是对应接口。
这一设计的初衷是美好的,但是在一些实际应用中则会遇到问题,除了我们的所述的环境,另外一个需要改变这个行为才能正常工作的典型场景就是 DR 模式的 IPVS。DR 模式的 IPVS 要求 Director 和 Realserver 在一个二层,而 Realserver 需要配置相关内核参数,防止 Realserver 内核使用物理网卡抢答目标地址为 Virtual IP 的 ARP 请求,以确保请求会被发到 Director 而不是直接被发往 Realserver,关于 IPVS DR 模式的原理参考资料很多此处不多展开。
ARP Flux 相关内核参数
首先,lo、VPN 接口的 ARP 配置是没有意义的,逻辑接口并不存在 ARP 问题,以下讨论中的 interface 皆默认为物理接口。
arp_filter
定义了内核是否可以使用其他网卡上的地址进行 ARP 响应,conf/{all,interface}/arp_filter
中最大的值将会生效,即开启 all 的 arp filter 时,将会忽略每个 interface 的配置:
- 0:默认值, 允许内核使用其他网卡上的地址进行 ARP 响应
- 1:内核将根据路由查找结果来响应,kernel.org 提示必须配合 source based routing 使用。我们的测试环境两个接口都路由可达,内核将永远选中路由规则优先的接口进行回复,开启
arp_filter=1
会导致两个接口有一个不再响应 ARP,除非在 gw 上对每个需要使用 VPN 网关的主机都手动添加静态路由,这不符合我们的需求
arp_ignore
定义了本机收到 ARP 请求时进行响应的规则,conf/{all,interface}/arp_ignore
中最大的值将会生效,即 all 的 ignore level 必须小于等于每个 interface 的 ignore level,否则全局配置会覆盖 interface 的配置:
- 0:默认值,响应本机所有网络接口上对所有 IP 地址的 ARP 查询请求
- 1:只响应入包端口对应 IP 地址的 ARP 请求
- 2:同上,且仅响应同子网请求
- 3:不响应任何 ARP
arp_announce
参数由2.6.4内核专为解决 ipvs 在 DR 模式下的 ARP Flux 问题而引入,这一参数替代了原本的 arp hidden
标记,定义了本机发送 ARP 请求时选取源 IP 地址的规则, 发送 ARP 请求时,需要在请求包中标记发送方的 IP 地址,这个参数会影响发出 ARP 请求时使用的源地址,conf/{all,interface}/arp_announce
中的最大值将会生效,同上所述,每个 interface 的配置有可能被全局配置覆盖:
- 0:默认值,可以使用任意端口上配置的任意 IP 进行发送 ARP,实际选取哪一个取决于路由选择过程
- 1:检查本机所有网卡的子网,只选取和目的 IP 在同一子网的网卡上地址进行发送,如果没有满足要求的网卡,则按参数为2的规则响应
- 2:此参数下内核将遍历所有网卡上的主 IP,根据子网选取内核认为最有可能收到回复的 IP 进行发送,如果没有合适的本地地址,则选取出站端口上的第一个 IP 进行发送
虽然 ARP Flux 问题并不算太常见,然而我作为一个每天和 IPVS 打交道的人居然没有研究过这些参数的具体行为,实属丢人,工作还是要求甚解,切记,切记······
本文链接:https://www.starduster.me/2019/09/27/arp-flux-problem/
本站基于 Creactive Commons BY-NC-SA 4.0 License 允许并欢迎您在注明来源和非商业使用前提下自由地对本文进行复制、分享或基于本文进行创作。
请注意:受限于笔者水平,本站内容可能存在主观臆断或事实错误,文中信息也可能因时间推移而不再准确,在此提醒读者结合自身判断谨慎地采纳。