从内网第二网关配置浅谈 ARP Flux 问题

TLDR :在同一个广播域内存在多个路由器/转发器如 VPN 网关、DR 模式的 IPVS 时,需注意内网主机的 ARP 配置,否则默认配置下 Linux 内核会使用所有可用 IP 进行 ARP ,可能引起被称为 ARP Flux 的混乱现象,一个典型现象为:网卡会使用绑定在其他网卡上的 IP 地址进行收发包。 net.ipv4.conf.all.arp_ignorenet.ipv4.conf.all.arp_announcenet.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 作为转发网关且正常情况下不接受入站流量只接受转发流量,大致思路为:

  1. 对 ethB 接口的所有入包打 mark,因为入包目的地址为客户端要访问的公网地址,源地址为内网客户端,报文中没有 ethB 的 IP 192.168.0.b,不能根据 192.168.0.b 进行 match
  2. iprule 匹配带 mark 的报文单独使用一张路由表使用 wghub 作为出站接口,默认路由指向 VPN peer
  3. 将发往 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 打交道的人居然没有研究过这些参数的具体行为,实属丢人,工作还是要求甚解,切记,切记······

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据