ping本机网口的IP地址,tcpdump在lo口才能抓到对应报文

问题背景

Q1:ping 本机eth1口的ip地址时,对应的ICMP报文会被发送到eth1口上吗?

实验: ping本机网口地址

ping本机网口地址实验

通过tcpdump抓包命令,我们发现报文被发送到lo上,而不是对应的物理口 eth1。
虽然ip 地址被配在本机的物理口上,但是报文并没有被发送到物理口。为了理解这个问题,我们需要先看下下面这个问题。

Q2: 当一个IP地址被添加到一个网口后,会引起哪些路由变化呢?
为一个网口增加一个 IP 地址是个很常见的网络操作。
具体添加方式有多种,包括命令行手动添加、通过配置文件在系统启动时候添加,还是通过dhcp自动获取并添加等。
无论哪种场景,添加完IP地址后,系统路由有什么变化。
场景:在本机物理口 eth1上配置192.168.8.8/24,网口状态正常(UP).

增加一个IP地址,内核增加3条路由

下面是路由表在增加 IP 地址前后的对比:
路由表变化对比

通过对比系统路由表的变化,发现路由表里增加了三条路由。

网段路由:
1
192.168.8.0/24 dev eth1 proto kernel scope link src 192.168.8.8

main表里增加一条到本网段前缀的网段路由,
这个路由对应的路由网段,

  • 网段入口:IP地址和掩码产生的网络前缀。
  • 路由出口:配置 IP 的网口
  • 推荐源 IP:被配置的IP地址

这条路由是如何被使用的?
比如 ping 192.168.8.1时,

  • 内核会通过查找路由表 main,查找到这条匹配的路由,
  • 之后把源 IP 当做 eth/ipv4/icmp 报文里的ipv/src_addr,
  • 最后通过网口 eth1 发送出去(eth头里src/mac是对应网口的mac 地址)。
    总结:`IP地址和掩码产生的网段路由, 出口对应网口,推荐IP是报文的源IP。
    这条路由是增加到路由表 main 里。
主机路由:
1
local 192.168.8.8 dev eth1 table local proto kernel scope host src 192.168.8.8

local表里增加一条到本机的32 位主机路由,

  • 网段入口:IP地址/32。
  • 路由出口:loopback口
    比如 ping 192.168.8.8时,
  • 内核会通过查找路由表 main,查找到这条匹配的路由,
  • 之后把源 IP 当做 eth/ipv4/icmp 报文里的ipv/src_addr,
  • 最后把报文发送给lo口,并重新进入协议栈。
    在 lo 口上tcpdump抓包验证,可以看到报文的mac地址是0.
广播路由:
1
broadcast 192.168.8.255 dev eth1 table local proto kernel scope link src 192.168.8.8

TODO:这部分没有深入分析。

路由表:local vs main 的对比
  1. local路由的目的地是本地,因此一定会往发往本地,并且使用的网口一定是lo口。
    不论报文的目的IP是谁,
    不管这个 IP 是被配置到什么类型网口上。
  2. main路由表的目的地是网关或者同网段的其他设备。出口是路由条目里指定的网口。

内核:策略路由与多路由表

我们注意到前面的三条路由并不在一个路由表里,而是分散在 local/main两个单独的路由表里。为什么放到多个路由表里,而不放到一张表里?
单独从路由查找来看,因为路由入口并不冲突,完全可以放到一张路由表里。其实内核也可以支持放到一张路由表。
但为了支持策略路由,内核里需要多路由表(IP_MULTIPLE_TABLES)这个特性。
其中有三个默认存在的路由表, 分别叫做local,main,default
我们经常用到的是前两个表,如他们名字一样, 分别用于存放 local 和网段路由类型的路由。

策略路由

策略路由是一个按优先级排列的规则链表。路由查找时,按优先级顺序遍历链表。
每个链表节点是一个规则。每个节点(规则)其实是个 match+action 的工作模式。

  • match:可以支持多种匹配条件及组合,如原 IP、IPv6、源端口、目的端口等。 如果满足规则匹配条件就执行规则指定的动作。
  • action:action 也有多种, 后续文章给你展开讨论。我们这里只讨论”查找指定 table”这一种action。就是在指定的路由表(ID 或者名字)里,查找路由。
    策略路由这里不展开介绍, 后续文章在详细说明功能和对应的场景。
    linux内核多路由表与策略路由的实现
内核自动添加的三条策略路由

系统初始化时候,会自动创建三条策略路由。

1
2
3
4
5
root@VM-3-3-ubuntu:/home/ubuntu# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
root@VM-3-3-ubuntu:/home/ubuntu#

默认三条策略路由规则,是任意匹配条件。 规则的动作也是查找指定的路由表。
即:遍历策略路由链表,依次在 local/main/default三张路由表里查询路由。

  1. 如果有匹配路由,路由查找成功,停止遍历策略路由链表,并返回对应路由。
  2. 没有找到路由,下一张表(即下一个路由策略里指定的表)。
  3. 如果最终没有找到(没有缺省路由),返回路由查找失败。

小结

在 eth1 口添加一个IP地址192.168.8.8/24后,内核默认生成三条路由:

  • 主机路由:192.168.8.8/32.
    • 本机产生的目的地192.168.8.8的报文, 查路由,走lo口, 经过 lo口重新发回本机协议栈。
    • 物理口接收到,目的地是192.168.8.8的报文,差路由,经ip_local_deliver,给本机协议栈上层(ICMP/tcp/udp)处理。
  • 网段路由: 192.168.8.0/24. 放往这网段的报文,会通过 eth1 口发给网关处理。
    • 因为 local路由表先于 maiin 路由表查找,所以发往 192.168.8.8会被路由到本机,同网段下的其他 IP 被路由到 eth1口。
    • 本机产生的或者本机其他网口收到的,目的地是192.168.8.0/24网段的报文,查路由被转发到 eth1 口。
  • 组播路由:TODO。

内核代码的实现

ip地址与路由回调

内核的路由模块通过一个回调函数,感知IP地址变化,并更新对应的路由表项。
ip 地址管理模块会提供一个回调注册函数。
给需要感知 ip 地址变化的模块。如 fib 路由模块,会调用这个函数注册自己的处理函数。当 ip 地址有变化时候,ip 地址模块就会调用这些被注册的回调函数,通知对应模块,及具体的ip 地址变化信息。

注册路由回调函数 :register_inetaddr_notifier

fib模块在初始化时,调用register_inetaddr_notifier注册一个 callback 函数fib_inetaddr_event,用来处理ip地址变化的消息。

1
2
3
4
1702 void __init ip_fib_init(void)
1703 {
...
1709 register_inetaddr_notifier(&fib_inetaddr_notifier);
1
2
3
1551 static struct notifier_block fib_inetaddr_notifier = {
1552 .notifier_call = fib_inetaddr_event,
1553 };

消息触发: IP地址更新

1
blocking_notifier_call_chain(&inetaddr_chain, NETDEV_UP, ifa);
1
blocking_notifier_call_chain(&inetaddr_chain, NETDEV_DOWN, ifa1);

fib模块回调函数:fib_inetaddr_event

回调函数fib_inetaddr_event处理地址变化消息,根据事件类型

  • NETDEV_UP: 对应增加 IP 地址场景。调用fib_add_ifaddr
  • NETDEV_DOWN: 对应删除 IP 地址场景。调用fib_del_ifaddr
1
2
3
4
5
6
7
8
9
10
11
==> fib_inetaddr_event
==> ==> case NETDEV_UP
==> ==> ==> fib_add_ifaddr(ifa);
==> ==> ==> ==> fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim, 0);
==> ==> ==> ==> fib_magic(RTM_NEWROUTE, dev->flags & IFF_LOOPBACK ? RTN_LOCAL : RTN_UNICAST,
prefix, ifa->ifa_prefixlen, prim, ifa->ifa_rt_priority);
==> ==> ==> ==> fib_magic(RTM_NEWROUTE, RTN_BROADCAST, ifa->ifa_broadcast, 32, prim, 0);
==> ==> ==> ==> ==> fib_table_insert(RTM_NEWROUTE, ....)
==> ==> ==> fib_del_ifaddr
==> ==> ==> ==> fib_magic(RTM_DELROUTE, dev->flags & IFF_LOOPBACK ? RTN_LOCAL : RTN_UNICAST, ...)
==> ==> ==> ==> ==> fib_table_insert(RTM_DELROUTE, ....)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1461 static int fib_inetaddr_event(struct notifier_block *this, unsigned long event, void *ptr)
1462 {
1463 struct in_ifaddr *ifa = ptr;
1464 struct net_device *dev = ifa->ifa_dev->dev;
1465 struct net *net = dev_net(dev);
1466
1467 switch (event) {
1468 case NETDEV_UP:
1469 fib_add_ifaddr(ifa);
1470 #ifdef CONFIG_IP_ROUTE_MULTIPATH
1471 fib_sync_up(dev, RTNH_F_DEAD);
1472 #endif
1473 atomic_inc(&net->ipv4.dev_addr_genid);
1474 rt_cache_flush(net);
1475 break;
1476 case NETDEV_DOWN:
1477 fib_del_ifaddr(ifa, NULL);
1478 atomic_inc(&net->ipv4.dev_addr_genid);
1479 if (!ifa->ifa_dev->ifa_list) {
1480 /* Last address was deleted from this interface.
1481 * Disable IP.
1482 */
1483 fib_disable_ip(dev, event, true);
1484 } else {
1485 rt_cache_flush(net);
1486 }
1487 break;
1488 }
1489 return NOTIFY_DONE;
1490 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
1138 void fib_add_ifaddr(struct in_ifaddr *ifa)
1139 {
1140 struct in_device *in_dev = ifa->ifa_dev;
1141 struct net_device *dev = in_dev->dev;
1142 struct in_ifaddr *prim = ifa;
1143 __be32 mask = ifa->ifa_mask;
1144 __be32 addr = ifa->ifa_local;
1145 __be32 prefix = ifa->ifa_address & mask;
1146
1147 if (ifa->ifa_flags & IFA_F_SECONDARY) {
1148 prim = inet_ifa_byprefix(in_dev, prefix, mask);
1149 if (!prim) {
1150 pr_warn("%s: bug: prim == NULL\n", __func__);
1151 return;
1152 }
1153 }
1154
1155 fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim, 0);
1156
1157 if (!(dev->flags & IFF_UP))
1158 return;
1159
1160 /* Add broadcast address, if it is explicitly assigned. */
1161 if (ifa->ifa_broadcast && ifa->ifa_broadcast != htonl(0xFFFFFFFF)) {
1162 fib_magic(RTM_NEWROUTE, RTN_BROADCAST, ifa->ifa_broadcast, 32,
1163 prim, 0);
1164 arp_invalidate(dev, ifa->ifa_broadcast, false);
1165 }
1166
1167 if (!ipv4_is_zeronet(prefix) && !(ifa->ifa_flags & IFA_F_SECONDARY) &&
1168 (prefix != addr || ifa->ifa_prefixlen < 32)) {
1169 if (!(ifa->ifa_flags & IFA_F_NOPREFIXROUTE))
1170 fib_magic(RTM_NEWROUTE,
1171 dev->flags & IFF_LOOPBACK ? RTN_LOCAL : RTN_UNICAST,
1172 prefix, ifa->ifa_prefixlen, prim,
1173 ifa->ifa_rt_priority);
1174
1175 /* Add the network broadcast address, when it makes sense */
1176 if (ifa->ifa_prefixlen < 31) {
1177 fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix | ~mask,
1178 32, prim, 0);
1179 arp_invalidate(dev, prefix | ~mask, false);
1180 }
1181 }
1182 }

路由增加/删除

路由增删有两类入口:

  1. 通过 netlink 配置的路由:如 iproute 命令。
    • 根据 netlink 消息里指定的 table ID,找到对应的 fib table
    • 根据 netlink消息里的msgtype命令类型,对 fib table 执行路由增删。
      增加路由:inet_rtm_newroute/fib_table_insert
      删除路由:inet_rtm_delroute/fib_table_delete
      这部分单独解释,不展开讨论。
  2. 内核自动生成的:如网口上的增加/删除一个 IP 地址。
    入口函数fib_magic, 根据命令分别执行fib_table_insertfib_table_delete

特殊的 lo口

loopback口是个特殊场景,详见 lo口与local路由表

路由查找

路由查找入口函数fib_lookup, 启用CONFIG_IP_MULTIPLE_TABLES,
会先检查用户自定义的策略路由,这部分放到策略路由里单独解释。
如果没有用户自定义的策略路由,

  • 先查找 main 表,
  • 再查找 defualt 表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
374 static inline int fib_lookup(struct net *net, struct flowi4 *flp,
375 struct fib_result *res, unsigned int flags)
376 {
...
381 if (net->ipv4.fib_has_custom_rules)
382 return __fib_lookup(net, flp, res, flags);
...
388 tb = rcu_dereference_rtnl(net->ipv4.fib_main);
389 if (tb)
390 err = fib_table_lookup(tb, flp, res, flags);
...
395 tb = rcu_dereference_rtnl(net->ipv4.fib_default);
396 if (tb)
397 err = fib_table_lookup(tb, flp, res, flags);

注意:

  • fib_lookup没有查找 local 表:
    fib路由模块默认创建了三个 table 表,local/main/default,但是在fib_lookup函数中,我们看到代码里只查找了后面两个表,为啥没有查找 local表。在初始化创建 local表时,设置local表的别名为main,因此 local 路由都被加入到 main 表里了。当用户添加自定义策略路由规则后,fib_unmerge 会把 local 路由从 main 表里切割出来,详见 fib_unmerge:把local路由从main表里切割出来

  • ip route show: 为什么 show 命令默认只 show main, 单纯指定 table ID不会有效果,过滤路由类型了?
    ip route命令默认指定的tb id 是 main 表,见代码

1
filter.tb = RT_TABLE_MAIN;