ARP 内核参数
{Back to Index}
Table of Contents
1 简介
arp_ignore
和 arp_announce
这两个参数主要用于控制系统返回 ARP 响应和发送 ARP 请求时的行为。
在系统中这两个参数的值分别有 all
和对应具体网卡的,
比如 /proc/sys/net/ipv4/conf/all/arp_ignore
, /proc/sys/net/ipv4/conf/eth0/arp_ignore
, /proc/sys/net/ipv4/conf/lo/arp_ignore
等。
对于某个网卡而言,实际生效的值为 相应网卡的值与 all 值两者中的最大值 。
2 arp_ignore 1
这个参数的作用是控制系统在收到 ARP 请求时,是否要返回此响应。
2.1 arp_ignore=0
当接收到 ARP 请求,不管目的 IP 是否存在于接收该请求的网卡上,都会响应。
Figure 1: arp_ignore=0
2.2 arp_ignore=1
仅当 ARP 请求的目的 IP 存在于接收该请求的网卡上时,才会响应。
Figure 2: arp_ignore=1
3 arp_announce
这个参数的作用是控制系统在 对外发送 ARP 请求时,如何选择请求数据包的 源 IP 地址 。
ARP 请求的源 IP 是可以选择的,选择的逻辑就是此参数决定的。
3.1 arp_announce=0
使用需要发送的数据包的源 IP 。
Figure 3: arp_announce=0
这里需要配置下 rp_filter
(反向路径过滤)内核参数,该参数的作用是:
如果一台主机从接口 A 收到一个包,其源地址为 <srcip>, 如果启用反向路径过滤功能,它就会以 <srcip> 作为目标 IP 去查找路由表, 如果得到的输出接口不为 A ,则认为反向路径过滤检查失败,内核就会丢弃该包。
从上图的拓扑中可以看出,只有关闭此内核约束,才能 ping 通。
3.2 arp_announce=2
从发送网卡上选择最合适的地址作为 ARP 请求的源 IP 地址。
Figure 4: arp_announce=2
4 一个由 ping 引出的问题
之前学习 veth 的用法时,偶然通过 ping 命令发现一个难以解释的现象,实验环境如下图所示: (Linux Kernel 4.x)
Figure 5: 能 ping 通,但虚拟设备上抓不到 ICMP Echo Reply 包
运行 ping -I veth0 192.168.1.3
,ping 成功,但是在 veth0 上抓包,发现没有 Echo Reply 包。分析原理如下:
- ping 进程构造 ICMP echo 请求包,发给协议栈,协议栈将该数据包交给了 veth0 ,由于 veth0 的另一端连的是 veth1 ,所以 ICMP echo 请求包就转发给了 veth1
- veth1 收到 ICMP echo 请求包后,转交给另一端的协议栈,协议栈查看路由表,发现回给 192.168.1.2 的数据包应该走 lo 口,于是将应答包交给 lo 设备
- lo 接到协议栈的应答包后,把数据包还给了协议栈
- 协议栈处理应答包,发现有进程在等待该包,于是交给了相应的进程,即 ping 进程
ping 通的前提条件是要先设置一些内核参数:
echo 1 > /proc/sys/net/ipv4/conf/veth1/accept_local echo 1 > /proc/sys/net/ipv4/conf/veth0/accept_local echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter echo 0 > /proc/sys/net/ipv4/conf/veth0/rp_filter echo 0 > /proc/sys/net/ipv4/conf/veth1/rp_filter
设置 accept_local
参数的原因是: Linux IP 路由实现中有个限制, 即任何从非 loopback 网卡进来的任何数据包的源地址不能是本机地址。
4.1 提出新问题
后来发现网上有人也做过类似的实验,但不同的是 lo 设备是 DOWN 的: 2
Figure 6: 关闭环回设备
在这个拓扑下,就算设置好内核参数,也无法 ping 通,其实原因也很明显,既然回给 192.168.1.2 的数据包应该走 lo 口,而此时 lo 是 DOWN 的, 那自然收不到。
于是尝试着先不看博客上的解释和结论,自己试试看能否通过一些网络技巧来 ping 通这条链路。
4.1.1 尝试一 (添加路由)
既然回送的数据不能再走 lo ,那很容易就想到添加两条路由:
ip r add 192.168.1.2/32 dev veth1 ip r add 192.168.1.3/32 dev veth0
发现不能 ping 通,于是使用 iptables 对数据包的路径添加些 log 来帮助分析:
iptables -t nat -I PREROUTING -j LOG --log-prefix "DBG@PREROUTING: " --log-level 4 # 这里选择 mangle 表而不是 nat 表,原因是 dest ip 为非本机地址才会进入 nat:POSTROUTING iptables -t mangle -I POSTROUTING -j LOG --log-prefix "DBG@POSTROUTING: " --log-level 4 iptables -I INPUT ! -d 10.0.2.0/24 -j LOG --log-prefix "DBG@INPUT: " --log-level 4 iptables -I OUTPUT ! -d 10.0.2.0/24 -j LOG --log-prefix "DBG@OUTPUT: " --log-level 4
一次 ping 之后看到的 log 为:
# 从 veth0 流入 [ 2245.272582] DBG@OUTPUT: IN= OUT=veth0 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=62202 DF PROTO=ICMP TYPE=8 CODE=0 ID=3786 SEQ=1 # 从 veth1 流出 [ 2245.272593] DBG@INPUT: IN=veth1 OUT= MAC=ca:ee:ea:18:bf:5a:7a:a6:be:51:43:cf:08:00 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=62202 DF PROTO=ICMP TYPE=8 CODE=0 ID=3786 SEQ=1 [ 2245.272600] DBG@OUTPUT: IN= OUT=lo SRC=192.168.1.3 DST=192.168.1.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=43628 PROTO=ICMP TYPE=0 CODE=0 ID=3786 SEQ=1
看到最终的路由决策结果还是 lo ,这才想起还有 local 路由表这回事,也就是说之前添加的两条路由是在 default 路由表中的,优先级低于 local 表,根本没机会参与决策,这也是为什么 ip r get 192.168.1.2
的结果还是:
bash-4.4# ip r get 192.168.1.2
local 192.168.1.2 dev lo src 192.168.1.2 uid 0
cache <local>
4.1.2 尝试二 (自定义路由策略)
接下来的思路就是把自定义的路由表优先级提前:
# 将 local 表的优先级降低 bash-4.4# ip rule add prio 1000 tab local bash-4.4# ip rule del prio 0 # 自定义 rule ,优先级比 local 高,并在 table 100 中增加两条路由 bash-4.4# ip rule add to 192.168.1.0/24 prio 100 table 100 bash-4.4# ip r add 192.168.1.2/32 dev veth1 tab 100 bash-4.4# ip r add 192.168.1.3/32 dev veth0 tab 100
查看下结果:
bash-4.4# ip rule 100: from all to 192.168.1.0/24 lookup 100 1000: from all lookup local 32766: from all lookup main 32767: from all lookup default bash-4.4# ip r show tab 100 192.168.1.2 dev veth1 scope link 192.168.1.3 dev veth0 scope link bash-4.4# ip r get 192.168.1.2 192.168.1.2 dev veth1 table 100 src 192.168.1.3 uid 0 cache
还是 ping 不通,看下 log :
[ 6983.698353] DBG@OUTPUT: IN= OUT=veth0 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=45146 DF PROTO=ICMP TYPE=8 CODE=0 ID=3897 SEQ=1 [ 6983.698357] DBG@POSTROUTING: IN= OUT=veth0 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=45146 DF PROTO=ICMP TYPE=8 CODE=0 ID=3897 SEQ=1 [ 6983.698368] DBG@INPUT: IN=veth1 OUT= MAC=ca:ee:ea:18:bf:5a:7a:a6:be:51:43:cf:08:00 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=45146 DF PROTO=ICMP TYPE=8 CODE=0 ID=3897 SEQ=1 [ 6983.698375] DBG@OUTPUT: IN= OUT=veth1 SRC=192.168.1.3 DST=192.168.1.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=55609 PROTO=ICMP TYPE=0 CODE=0 ID=3897 SEQ=1 [ 6983.698377] DBG@POSTROUTING: IN= OUT=veth1 SRC=192.168.1.3 DST=192.168.1.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=55609 PROTO=ICMP TYPE=0 CODE=0 ID=3897 SEQ=1
没错啊,最后一条记录显示路由决策确实是 veth1 啊,肯定还有一些细节没考虑到。 而且这里还有一处可疑的地方,就是最后数据从 veth1 出去,之后理应看到一条 In=veth0 的 log ,但是并没有。 因此肯定是系统认为该数据包不满足某些条件或是约束,被 drop 掉了。
想不出原因,还是老老实实看下人家博客怎么说的。
4.1.3 ARP Reply 约束
当收到 ARP Request,是否回复 ARP Reply 取决于 Request 中的目标 IP 是否为本机地址, 具体表现为是否能在路由表中查找到类型为 local 的条目(而这种条目只记录在 local 表中,由系统维护,用户不能修改)。
static int arp_process(struct net *net, struct sock *sk, struct sk_buff *skb) { if (arp->ar_op == htons(ARPOP_REQUEST) && ip_route_input_noref(skb, tip, sip, 0, dev) == 0) rt = skb_rtable(skb); addr_type = rt->rt_type; if (addr_type == RTN_LOCAL) { // 约束 int 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) { n = neigh_event_ns(&arp_tbl, sha, &sip, dev); if (n) { arp_send_dst(ARPOP_REPLY, ETH_P_ARP, // 发送 sip, dev, tip, sha, dev->dev_addr, sha, reply_dst); neigh_release(n); } } ... } }
上面尝试使用路由策略自定义路由表的方式, 终归 只能返回普通路由,不能返回 local 类型的路由,这就是为什么 ping 不通的原因。
bash-4.4# ip r get 192.168.1.2 192.168.1.2 dev veth1 table 100 src 192.168.1.3 uid 0 cache # 需要返回类似 local 192.168.1.2 dev veth1 table 100 src 192.168.1.3 uid 0
4.1.4 最终方案
如何能在收到 ARP Request 时查到 local 类型的路由信息,同时又要在发送前的路由决策中使用正确的设备?博客中给出的思路是对数据包 打标签 。
观察iptables架构可以发现当数据包从协议层下发的时候,唯一可以改变路由策略的地方就是 OUTPUT 链。 即在 OUTPUT 链中对数据包打标签,并在路由决策时使用基于标签的策略就能改变数据包最终的发送设备。
bash-4.4# ip rule add fwmark 100 pref 10 tab 10
bash-4.4# ip route add 192.168.1.2/32 dev veth1 tab 10
bash-4.4# iptables -t mangle -A OUTPUT -d 192.168.1.0/24 -j MARK --set-mark 100
bash-4.4# ip rule
10: from all fwmark 0x64 lookup 10
1000: from all lookup local
32766: from all lookup main
32767: from all lookup default
bash-4.4# ip r get 192.168.1.2
local 192.168.1.2 dev lo table local src 192.168.1.2 uid 0
cache <local>
这样就大功告成了。最后再看一下 log :
[ 6689.075574] DBG@OUTPUT: IN= OUT=veth0 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=53879 DF PROTO=ICMP TYPE=8 CODE=0 ID=3877 SEQ=1 MARK=0x64 [ 6689.075578] DBG@POSTROUTING: IN= OUT=veth0 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=53879 DF PROTO=ICMP TYPE=8 CODE=0 ID=3877 SEQ=1 MARK=0x64 [ 6689.075586] DBG@INPUT: IN=veth1 OUT= MAC=ca:ee:ea:18:bf:5a:7a:a6:be:51:43:cf:08:00 SRC=192.168.1.2 DST=192.168.1.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=53879 DF PROTO=ICMP TYPE=8 CODE=0 ID=3877 SEQ=1 [ 6689.075592] DBG@OUTPUT: IN= OUT=lo SRC=192.168.1.3 DST=192.168.1.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=22519 PROTO=ICMP TYPE=0 CODE=0 ID=3877 SEQ=1 MARK=0x64 # 这中间经历了路由决策,将 OUT 由上面的 lo 改为 veth1 [ 6689.075594] DBG@POSTROUTING: IN= OUT=veth1 SRC=192.168.1.3 DST=192.168.1.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=22519 PROTO=ICMP TYPE=0 CODE=0 ID=3877 SEQ=1 MARK=0x64 [ 6689.075598] DBG@INPUT: IN=veth0 OUT= MAC=7a:a6:be:51:43:cf:ca:ee:ea:18:bf:5a:08:00 SRC=192.168.1.3 DST=192.168.1.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=22519 PROTO=ICMP TYPE=0 CODE=0 ID=3877 SEQ=1
上述实验过程可以参考这里。