问题背景
Q1:ping 本机eth1口的ip地址时,对应的ICMP报文会被发送到eth1口上吗?
实验: 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 的对比
- local路由的目的地是本地,因此一定会往发往本地,并且使用的网口一定是lo口。
不论报文的目的IP是谁,
不管这个 IP 是被配置到什么类型网口上。 - main路由表的目的地是网关或者同网段的其他设备。出口是路由条目里指定的网口。
内核:策略路由与多路由表
我们注意到前面的三条路由并不在一个路由表里,而是分散在 local/main两个单独的路由表里。为什么放到多个路由表里,而不放到一张表里?
单独从路由查找来看,因为路由入口并不冲突,完全可以放到一张路由表里。其实内核也可以支持放到一张路由表。
但为了支持策略路由,内核里需要多路由表(IP_MULTIPLE_TABLES)这个特性。
其中有三个默认存在的路由表, 分别叫做local,main,default。
我们经常用到的是前两个表,如他们名字一样, 分别用于存放 local 和网段路由类型的路由。
策略路由
策略路由是一个按优先级排列的规则链表。路由查找时,按优先级顺序遍历链表。
每个链表节点是一个规则。每个节点(规则)其实是个 match+action 的工作模式。
- match:可以支持多种匹配条件及组合,如原 IP、IPv6、源端口、目的端口等。 如果满足规则匹配条件就执行规则指定的动作。
- action:action 也有多种, 后续文章给你展开讨论。我们这里只讨论”查找指定 table”这一种action。就是在指定的路由表(ID 或者名字)里,查找路由。
策略路由这里不展开介绍, 后续文章在详细说明功能和对应的场景。
linux内核多路由表与策略路由的实现
内核自动添加的三条策略路由
系统初始化时候,会自动创建三条策略路由。
1 | root@VM-3-3-ubuntu:/home/ubuntu# ip rule show |
默认三条策略路由规则,是任意匹配条件。 规则的动作也是查找指定的路由表。
即:遍历策略路由链表,依次在 local/main/default三张路由表里查询路由。
- 如果有匹配路由,路由查找成功,停止遍历策略路由链表,并返回对应路由。
- 没有找到路由,下一张表(即下一个路由策略里指定的表)。
- 如果最终没有找到(没有缺省路由),返回路由查找失败。
小结
在 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 | 1702 void __init ip_fib_init(void) |
1 | 1551 static struct notifier_block fib_inetaddr_notifier = { |
消息触发: 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 | ==> fib_inetaddr_event |
1 | 1461 static int fib_inetaddr_event(struct notifier_block *this, unsigned long event, void *ptr) |
1 | 1138 void fib_add_ifaddr(struct in_ifaddr *ifa) |
路由增加/删除
路由增删有两类入口:
- 通过 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,
特殊的 lo口
loopback口是个特殊场景,详见 lo口与local路由表。
路由查找
路由查找入口函数fib_lookup, 启用CONFIG_IP_MULTIPLE_TABLES,
会先检查用户自定义的策略路由,这部分放到策略路由里单独解释。
如果没有用户自定义的策略路由,
- 先查找 main 表,
- 再查找 defualt 表。
1 | 374 static inline int fib_lookup(struct net *net, struct flowi4 *flp, |
注意:
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; |