夜雨聆风学习资料网

ARTICLE · 1196502

让网卡学会闭嘴:四行 ARP 参数的源码真相

让网卡学会闭嘴:四行 ARP 参数的源码真相
让网卡学会闭嘴:四行 ARP 参数的源码真相
上一篇讲 LVS,结尾留了个尾巴:DR 模式下真实服务器必须配四行内核参数(arp_ignore=1、arp_announce=2,all 和 eth0 各写一遍),「一个不能少」。当时没展开——因为这一口值得单独一篇。
今天兑现。这四行参数,内核里管事的代码加起来不到 80 行,全在 net/ipv4/arp.c 一个文件里。先把事故现场还原,再进源码。
一声广播,全网举手
以太网的寻址规则粗暴:想知道一个 IP 归谁,就在整个二层网络里喊广播——「谁是 10.0.0.100?把 MAC 地址报上来」。这声喊话就是 ARP 请求。
按协议设计,只有 IP 的主人该应答。问题出在「主人」怎么认定:Linux 内核的默认答案是——这个 IP 只要配在这台机器的任意一块网卡上,我就应答,不管包从哪块网卡进来。
平时这个默认没毛病,一台机器一个 IP,天经地义。坏在 DR 模式:为了 RS 能直接回包,VIP 被配到了每台真实服务器的回环口 lo 上。于是交换机喊「谁是 VIP」——
交换机 · ARP 广播
"谁是 10.0.0.100(VIP)?"
↓ 未压制:全网抢答
调度器 DS✋ 我有RS-1✋ 我也有RS-2✋ 我还有
交换机 ARP 表:VIP → 最后应答的那台 —— 学歪了
↓ 配上四行参数之后
调度器 DS✋ 我有RS-1🔇 装聋RS-2🔇 装聋
交换机 ARP 表:VIP → DS ✅ 流量老老实实走调度器
三台都答,交换机的 ARP 表就指向了最后应答的那台——流量绕过调度器直奔 RS,负载均衡当场失效。更阴的是这个错误带概率:谁答得晚表就指向谁,症状是「负载忽高忽低」「时好时坏」,翻查日志什么都没有,典型的静默故障。
解法就是那四行参数:让 RS 对 VIP 的问话装聋(arp_ignore),同时自己开口问路时也别报 VIP 的名号(arp_announce)。云厂商的文档——比如 BytePlus ECS 的内核微调指南——都会让你配上它们,但「为什么是这两个值、为什么写四行」,得去内核里找。
arp_ignore:别人问,答不答
内核收到 ARP 请求、决定要不要回话,走的是 arp_process()。剥掉枝节,核心就这几行:
if (addr_type == RTN_LOCAL) {         int dont_send;          dont_send = arp_ignore(in_dev, sip, tip);   // 回不回,这一行说了算         if (!dont_send && IN_DEV_ARPFILTER(in_dev))                 dont_send = arp_filter(sip, tip, dev);         if (!dont_send) {                 /* 更新邻居表,发 ARP 应答 */         } } 
in_dev 是收到这个包的网卡设备,sip 是问话人的 IP,tip 是被问的 IP。dont_send 为 0 就应答。而 dont_send 完全由 arp_ignore() 决定——这个函数 35 行,是全文最值得读的一段内核代码:
static int arp_ignore(struct in_device *in_dev, __be32 sip, __be32 tip) {         struct net *net = dev_net(in_dev->dev);         int scope;          switch (IN_DEV_ARP_IGNORE(in_dev)) {         case 0: /* Reply, the tip is already validated */                 return 0;         case 1: /* Reply only if tip is configured on the incoming interface */                 sip = 0;                 scope = RT_SCOPE_HOST;                 break;         case 2: /*                  * Reply only if tip is configured on the incoming interface                  * and is in same subnet as sip                  */                 scope = RT_SCOPE_HOST;                 break;         case 3: /* Do not reply for scope host addresses */                 sip = 0;                 scope = RT_SCOPE_LINK;                 in_dev = NULL;                 break;         case 4: /* Reserved */         case 5:         case 6:         case 7:                 return 0;         case 8: /* Do not reply */                 return 1;         default:                 return 0;         }         return !inet_confirm_addr(net, in_dev, sip, tip, scope); } 
先看 case 0:直接 return 0,不拦,应答。这就是默认行为——tip 是本机地址就行,配在哪块网卡上不管。
再看 case 1:进 switch 先做一个小动作 sip = 0,把「谁在问」的信息抹掉;然后带着 scope = RT_SCOPE_HOST 落到最后一行,调 inet_confirm_addr()。这个函数干的事:在 in_dev——也就是收到这声 ARP 的那块网卡上——确认 tip(被问的 IP)是不是配置在这里。找到了返回非 0,取反就是 0,应答;找不到就不答。
翻译成人话:arp_ignore=1 之后,内核只认「收包的那块网卡」。IP 配在 eth0、ARP 从 eth0 进来——答;IP 配在 lo 上、ARP 从 eth0 进来——eth0 上查无此 IP,装聋。
DR 场景正好卡在这个缝隙里:VIP 以 /32 掩码配在 RS 的 lo 上,ARP 请求从 eth0 进来。设了 arp_ignore=1,RS 对「谁是 VIP」这声广播就再也不举手了。
八个取值不用都背,表留着备查:
值
行为
0
默认:IP 配在本机任意网卡上都应答
1
只应答「配在收包网卡上」的 IP
2
在 1 的基础上,还要求问话方 IP 与之同网段
3
不应答主机作用域地址(典型:lo 上的 /32)
4–7
保留
8
一律不应答,连自己的正经 IP 也不答
生产里真正在用的就是 0 和 1。2 比 1 更严但依赖网段规划;8 是彻底闭嘴,一般只在排查时临时用。
arp_announce:自己问,报什么身份
arp_ignore 管被动应答,arp_announce 管主动发起——这台机器要发 ARP 去问「网关的 MAC 是多少」时,ARP 包里「发送方 IP」填什么。
直觉上应该填出口网卡的 IP。默认行为偏偏不是。看 arp_solicit()(内核替邻居表发 ARP 探测的函数)里选源地址的这段:
switch (IN_DEV_ARP_ANNOUNCE(in_dev)) { default: case 0:         /* By default announce any local IP */         if (skb && inet_addr_type_dev_table(dev_net(dev), dev,                                           ip_hdr(skb)->saddr) == RTN_LOCAL)                 saddr = ip_hdr(skb)->saddr;         break; case 1:         /* Restrict announcements of saddr in same subnet */         if (!skb)                 break;         saddr = ip_hdr(skb)->saddr;         if (inet_addr_type_dev_table(dev_net(dev), dev,                                      saddr) == RTN_LOCAL) {                 /* saddr should be known to target */                 if (inet_addr_onlink(in_dev, target, saddr))                         break;         }         saddr = 0;         break; case 2:         /* Avoid secondary IPs, get a primary/preferred one */         break; } rcu_read_unlock();  if (!saddr)         saddr = inet_select_addr(dev, target, RT_SCOPE_LINK); 
三个档位:
case 0(默认):只要触发这次 ARP 的那个 IP 包的源地址(ip_hdr(skb)->saddr)是本机地址,就直接拿它当 ARP 的发送方 IP——注意 inet_addr_type_dev_table 查的是全机地址表,不限出口网卡。
case 1:加一道约束,源地址必须和目标 IP 同网段(inet_addr_onlink),否则作废,落到下面的兜底逻辑。
case 2:最干脆——直接忽略包里的源地址,saddr 保持 0,交给 inet_select_addr(dev, target, RT_SCOPE_LINK):在出口网卡上,挑一个和目标同网段的主地址。
坑在 case 0 上。DR 模式下,RS 的应用回包时源 IP 是 VIP(配在 lo 上),内核处理这个回包要发 ARP 问网关,按 case 0 的逻辑,ARP 的发送方 IP 就填成了 VIP——问个路都要报 VIP 的名号,交换机顺手又把「VIP 在这台 RS」学进 ARP 表。刚才 arp_ignore 压下去的抢答,从主动方向又漏了出来。
arp_announce=2 把这个口子也堵上:出口是 eth0,就用 eth0 上的内网 IP 去问路,身份干净。
一个管「答」,一个管「问」,两个参数把一台机器的网络身份收拾利索——这就是四行里的两行。
为什么是四行,不是两行
all 和 eth0 各写一遍,不是保险起见的仪式,内核里真有讲究。
Linux 网络参数有三个作用域:conf/default(新接口出生时的模板)、conf/all(全机层)、conf/具体接口。绝大多数参数的规则是「接口值优先」——接口上设了就用接口的,没设才回退到 all。
arp_ignore 和 arp_announce 属于一个小团体,规则不同。include/linux/inetdevice.h:
#define IN_DEV_MAXCONF(in_dev, attr) \         (max(IPV4_DEVCONF_ALL_RO(dev_net(in_dev->dev), attr), \              IN_DEV_CONF_GET((in_dev), attr)))  #define IN_DEV_ARP_ANNOUNCE(in_dev)  IN_DEV_MAXCONF((in_dev), ARP_ANNOUNCE) #define IN_DEV_ARP_IGNORE(in_dev)    IN_DEV_MAXCONF((in_dev), ARP_IGNORE) 
IN_DEV_MAXCONF 就是一个 max():生效值 = max(conf/all 的值,本接口的值)。
conf/allarp_ignore = 1┐│┌conf/eth0arp_ignore = 0
↓ max()
生效值 = 1(更严格者胜)
也就是说:只写 all=1、eth0 留着 0,max(1,0)=1,照样生效——理论上两行就够。但反过来,接口上残留了一个更大的值,光改 all 是压不下去的;何况 NetworkManager、systemd-networkd 这类工具重建接口时,接口级值可能被重置。写全四行是成本最低的确定性:两层都钉死,谁来 max 都是你要的值。
顺带一个冷知识:走 max() 逻辑的还有 rp_filter、arp_notify 这批「安全属性」参数,方向统一是「更严格者胜」——语义上说得通:安全参数,取松紧更紧的那个。
动手改一遍
临时改(重启即失效,适合先验证再固化):
sysctl -w net.ipv4.conf.all.arp_ignore=1 sysctl -w net.ipv4.conf.eth0.arp_ignore=1 sysctl -w net.ipv4.conf.all.arp_announce=2 sysctl -w net.ipv4.conf.eth0.arp_announce=2 
永久改:写进 /etc/sysctl.conf,或者在 /etc/sysctl.d/ 下单起一个文件(更清爽),然后 sysctl -p 生效:
net.ipv4.conf.all.arp_ignore = 1 net.ipv4.conf.eth0.arp_ignore = 1 net.ipv4.conf.all.arp_announce = 2 net.ipv4.conf.eth0.arp_announce = 2 
验证三件套:
# 1. 看值落没落下去 sysctl net.ipv4.conf.all.arp_ignore net.ipv4.conf.eth0.arp_ignore  # 2. 在另一台同网段机器上问 VIP,看谁应答——金标准 arping -I eth0 -c 3 10.0.0.100  # 3. 在 RS 上抓 ARP 包,确认自己没抢答 tcpdump -i eth0 -n arp 
第 2 步最有说服力:配置正确的话,arping 只有一个应答方——DS。冒出多个应答方,就是有 RS 没压住。
两个容易踩的细节:接口名按实际改,网卡叫 ens5、enp0s3 的机器照抄 eth0 等于没配;conf/default 别拿来当全机开关——它只影响之后新建的接口(VLAN、隧道、容器 veth),对已存在的 eth0 无效。
除了 LVS,还有谁在用
这套参数的适用面比 LVS 宽:
Keepalived / HA 的 VIP:主备机器都「有」VIP,ARP 抑制决定漂移时谁应答(Keepalived 会自己动这些参数,原理相同)。
云主机多网卡 / 辅助 IP:辅助 IP 的 ARP 应答行为影响路由收敛,各云厂商文档——包括开头那篇 BytePlus 的——主要服务这个场景。
容器与 k8s 的静态 VIP:节点上挂了 service 网段地址,同样要管住 ARP。
本质是同一个问题:当一台机器「拥有」多个 IP、而其中一些不该从当前网卡露面时,就得教内核管住嘴。
收个尾
四行参数,两个旋钮:arp_ignore 管「别人问到头上,答不答」——只认收包网卡;arp_announce 管「自己开口问路,报什么身份」——只认出口网卡的主地址。一个 max() 决定了 all 和接口要各写一遍。DR 模式的静默故障,九成九栽在这四行上。
参考:内核源码 net/ipv4/arp.c(arp_process / arp_ignore / arp_solicit)、include/linux/inetdevice.h(IN_DEV_MAXCONF)、Documentation/networking/ip-sysctl.rst——文中代码取自 torvalds/master,这几段十几年没大改,行号会随版本漂移。操作步骤参考 BytePlus ECS 内核微调文档(docs.byteplus.com,搜 arp_announce)。老规矩:原理不过时,命令动手前先看清自己的接口名。
聊两句:你第一次撞上 ARP 抢答是什么场景?我猜大部分人是 DR 环境负载不均、甚至 curl 偶发超时,回头才查到交换机 ARP 表学歪了——评论区说说你的排查路径,被 ARP 表坑过的举个手,我置顶一条我的判断。
下一篇拆 conntrack:那张每天替你记几百万条连接的内核表,满了之后新连接是什么下场——一个 nf_conntrack: table full 告警背后的故事。关注不迷路。

相关学习资料