0.0.1. 问题背景 Q1:ping 本机eth1口的ip地址时,对应的ICMP报文会被发送到eth1口上吗?
0.0.1.1. 实验: ping本机网口地址
通过tcpdump抓包命令,我们发现报文被发送到lo上,而不是对应的物理口 eth1。 虽然ip 地址被配在本机的物理口上,但是报文并没有被发送到物理口。为了理解这个问题,我们需要先看下下面这个问题。
Q2: 当一个IP地址被添加到一个网口后,会引起哪些路由变化呢? 为一个网口增加一个 IP 地址是个很常见的网络操作。 具体添加方式有多种,包括命令行手动添加、通过配置文件在系统启动时候添加,还是通过dhcp自动获取并添加等。 无论哪种场景,添加完IP地址后,系统路由有什么变化。 场景:在本机物理口 eth1上配置192.168.8.8/24,网口状态正常(UP).
0.0.1.2. 增加一个IP地址,内核增加3条路由 下面是路由表在增加 IP 地址前后的对比:
通过对比系统路由表的变化,发现路由表里增加了三条路由。
0.0.1.2.1. 网段路由: 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 里。
0.0.1.2.2. 主机路由: 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.
0.0.1.2.3. 广播路由: 1 broadcast 192.168.8.255 dev eth1 table local proto kernel scope link src 192.168.8.8
TODO:这部分没有深入分析。
0.0.1.2.4. 路由表:local vs main 的对比
local路由的目的地是本地,因此一定会往发往本地,并且使用的网口一定是lo口。 不论报文的目的IP是谁, 不管这个 IP 是被配置到什么类型网口上。
main路由表的目的地是网关或者同网段的其他设备。出口是路由条目里指定的网口。
0.0.1.3. 内核:策略路由与多路由表 我们注意到前面的三条路由并不在一个路由表里,而是分散在 local/main两个单独的路由表里。为什么放到多个路由表里,而不放到一张表里? 单独从路由查找来看,因为路由入口并不冲突,完全可以放到一张路由表里。其实内核也可以支持放到一张路由表。 但为了支持策略路由,内核里需要多路由表(IP_MULTIPLE_TABLES)这个特性。 其中有三个默认存在的路由表, 分别叫做local,main,default。 我们经常用到的是前两个表,如他们名字一样, 分别用于存放 local 和网段路由类型的路由。
0.0.1.3.1. 策略路由 策略路由是一个按优先级排列的规则链表。路由查找时,按优先级顺序遍历链表。 每个链表节点是一个规则。每个节点(规则)其实是个 match+action 的工作模式。
match:可以支持多种匹配条件及组合,如原 IP、IPv6、源端口、目的端口等。 如果满足规则匹配条件就执行规则指定的动作。
action:action 也有多种, 后续文章给你展开讨论。我们这里只讨论”查找指定 table”这一种action。就是在指定的路由表(ID 或者名字)里,查找路由。 策略路由这里不展开介绍, 后续文章在详细说明功能和对应的场景。linux内核多路由表与策略路由的实现
0.0.1.3.2. 内核自动添加的三条策略路由 系统初始化时候,会自动创建三条策略路由。
1 2 3 4 5 root@VM -3 -3 - ubuntu:/ home/ ubuntu# ip rule show 0 : from all lookup local 32766 : from all lookup main32767 : from all lookup default root@VM -3 -3 - ubuntu:/ home/ ubuntu#
默认三条策略路由规则,是任意匹配条件。 规则的动作也是查找指定的路由表。 即:遍历策略路由链表,依次在 local/main/default三张路由表里查询路由。
如果有匹配路由,路由查找成功,停止遍历策略路由链表,并返回对应路由。
没有找到路由,下一张表(即下一个路由策略里指定的表)。
如果最终没有找到(没有缺省路由),返回路由查找失败。
0.0.1.4. 小结 在 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。
0.0.2. 内核代码的实现 0.0.2.1. ip地址与路由回调 内核的路由模块通过一个回调函数,感知IP地址变化,并更新对应的路由表项。 ip 地址管理模块会提供一个回调注册函数。 给需要感知 ip 地址变化的模块。如 fib 路由模块,会调用这个函数注册自己的处理函数。当 ip 地址有变化时候,ip 地址模块就会调用这些被注册的回调函数,通知对应模块,及具体的ip 地址变化信息。
0.0.2.2. 注册路由回调函数 :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 };
0.0.2.3. 消息触发: IP地址更新 1 blocking_notifier_call_chain(&inetaddr_chain, NETDEV_UP, ifa);
1 blocking_notifier_call_chain(&inetaddr_chain, NETDEV_DOWN, ifa1);
0.0.2.4. 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 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 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 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 }
0.0.2.5. 路由增加/删除 路由增删有两类入口:
通过 netlink 配置的路由:如 iproute 命令。
根据 netlink 消息里指定的 table ID,找到对应的 fib table
根据 netlink消息里的msgtype命令类型,对 fib table 执行路由增删。 增加路由:inet_rtm_newroute/fib_table_insert。 删除路由:inet_rtm_delroute/fib_table_delete。 这部分单独解释,不展开讨论。
内核自动生成的:如网口上的增加/删除一个 IP 地址。 入口函数fib_magic, 根据命令分别执行fib_table_insert和fib_table_delete,
0.0.2.6. 特殊的 lo口 loopback口是个特殊场景,详见 lo口与local路由表 。
0.0.2.7. 路由查找 路由查找入口函数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;