使用场景
接上篇文章,我们提到在网口eth1 上增加一个 ip 地址,内核会生成 3 个路由。
- local 路由表:增加/32 主机路由
- main 路由表:增加本网段前缀的网络路由
为什么需要使用local/main 两个路由表呢? ip地址引发的路由变化或者手动添加一个目的网段的路由,这些场景看起来,完全可以使用一个路由表就能满足场景需求。那内核为什么要支持多个路由表?
其中一个场景就是 策略路由。
比如公司有两个网关, 其中一个网关GW1,网络质量比较高。我们希望对网络质量有较高要求的个别网段的外访流量,走GW1.其他网段都用 GW2.
这时,我们就不能仅仅依赖目前 IP 去做路由。我们需要:
- 先根据源 IP 地址做一次筛选,选中的这部分流量,缺省路由到GW1.
- 没有选中的走我们的正常路由查找,其中缺省路由到GW2。
- 这两个缺省路由只能放到两个单独的路由表里。
那下面详细展开介绍下策略路由及内核实现。
策略路由
策略路由是一个按优先级排列的规则链表。路由查找时,按优先级顺序遍历链。
每个策略路由规则,由match+action两部分组成。 match 也被称为SELECTOR。
- 每个规则的match条件,可以支持多种字段,如源IP/协议/协议源端口/目的端口等,也可以这多个字段的组合。如果满足规则匹配条件就执行规则指定的动作。
- 每个策略路由的action有下面几种:
- table X:查找对应的路由表 table X。X 是 table ID 或者 table 名字。
- goto xx:跳转到更低优先级到规则。
- nat …:ip地址替换
- 所有的策略路由通过 IP rule 命令添加。
- 策略路由按照优先级插入到一个链表里。
- 按照优先级从高(数值最小)到低(数值大)按顺序排列。
- 通过
ip rule show命令我们可以看到每条rule对应的优先级。添加rule时候,默认的是当前除0以外最高优先级的值-1, 即默认新建的rule优先级高。
这部分用法详见ip rule命令帮助手册,
https://man7.org/linux/man-pages/man8/ip-rule.8.html
默认的三条策略路由: local/main/default
默认三条规则,是任意匹配条件。 规则的动作也是查找指定的路由表。
即:遍历策略路由链表,依次在 local/main/default三张路由表里查询路由。
- 如果有匹配路由,路由查找成功,停止遍历策略路由链表,并返回对应路由。
- 没有找到路由,下一张表(即下一个路由策略里指定的表)。
- 如果最终没有找到(没有缺省路由),返回路由查找失败。

通过命令,我们可以看到 local表优先级最高。
main 和 default 优先级低。后续新增的策略路由,优先级会是 main 优先级-1(即 32765= 32766-1)
策略路由查找
策略路由本身是有序链表,各个节点按照从高到低的优先级排序。路由查找时,按顺序遍历策略路由节点。遍历到每个节点,先检查是否满足当前节点的match 条件。
- 如果不满足match 条件,那么继续遍历下一个节点,直到全部节点执行完成。
- 如果满足 match 条件,则执行当前节点对应的 action。根据 action 情况,返回查找结果。
- 如果 action 是 goto,那么就跳转到对应的策略路由节点。这里 goto 只能往低优先级跳转,因此不会出现环的问题. goto 会跳过并忽略中间节点。
- 如果 action 是 table,到指定到 table 里去查找路由,
- 如果找到了,就终止遍历,把路由结果返回。
- 如果没有查找到,继续遍历下一个策略路由(更低优先级)。
内核实现

数据结构
struct netns_ipv4
每个netns下有个IPv4相关的路由信息变量struct netns_ipv4 ipv4,
1 2 3 4 5
| 61 struct net { ... 125 struct list_head rules_ops; ... 134 struct netns_ipv4 ipv4;
|
1 2 3 4 5 6 7 8 9 10
| 44 struct netns_ipv4 { ... 59 #ifdef CONFIG_IP_MULTIPLE_TABLES 60 struct fib_rules_ops *rules_ops; 61 struct fib_table __rcu *fib_main; 62 struct fib_table __rcu *fib_default; 63 unsigned int fib_rules_require_fldissect; 64 bool fib_has_custom_rules; 65 #endif 66 bool fib_has_custom_local_routes;
|
struct fib_rules_ops
然后通过rules_ops指向IPv4对应的struct fib_rules_ops.
在这个ops下有一个rules_list链表,串联了所有的策略路由。
这个结构体里,还包括几个非常重要的函数指针,后面结合 IPv4 的 rule op一起解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 60 struct fib_rules_ops { 61 int family; 62 struct list_head list; ... 74 int (*match)(struct fib_rule *, 75 struct flowi *, int); 76 int (*configure)(struct fib_rule *, 77 struct sk_buff *, 78 struct fib_rule_hdr *, 79 struct nlattr **, 80 struct netlink_ext_ack *); ... 92 94 struct list_head rules_list; ... 98 };
|
struct fib_rule
每个rule对应一个结构体struct fib_rule。
1 2 3 4 5 6 7 8 9 10 11
| 20 struct fib_rule { 21 struct list_head list; 22 int iifindex; ... 27 u32 table; 28 u8 action; 33 __be64 tun_id; 34 struct fib_rule __rcu *ctarget; 35 struct net *fr_net; ... 47 };
|
初始化
如果是新建的 netns,会调用fib_net_init >> ip_fib_net_init >> fib4_rules_init
如果是初始 netns,直接调用初始化函数 fib4_rules_init
fib4_rules_init: 初始化
系统初始化时候,在fib4_rules_init里,通过复制模版创建一个ops,并把net信息记录到’ops’里。
这个新创建的’ops’又被挂到’net’下的rule_ops链表里. 最后这个ops被保存到’IPv4’下的rules_ops
每次更新rule规则时候,struct netns_ipv4里的变量fib_has_custom_rules被更新为true,以记录规则路由被启用了。
1 2 3 4 5 6 7 8 9 10
| fib4_rules_init --> fib_rules_register(&fib4_rules_ops_template, net) --> --> ops = kmemdup && ops->fro_net = net ;//复制模版创建一个ops,并把net信息记录到ops里。 --> --> __fib_rules_register --> fib_default_rules_init(ops); // 依次初始化三个路由表 --> --> fib_default_rule_add(ops, 0, RT_TABLE_LOCAL); --> --> fib_default_rule_add(ops, 0x7FFE, RT_TABLE_MAIN); --> --> fib_default_rule_add(ops, 0x7FFF, RT_TABLE_DEFAULT); --> net->ipv4.rules_ops = ops; --> net->ipv4.fib_has_custom_rules = false;
|
1 2 3 4 5 6 7 8 9 10 11
| 503 int __net_init fib4_rules_init(struct net *net) 504 { ... 508 ops = fib_rules_register(&fib4_rules_ops_template, net); ... 512 err = fib_default_rules_init(ops); ... 515 net->ipv4.rules_ops = ops; 516 net->ipv4.fib_has_custom_rules = false; 517 net->ipv4.fib_rules_require_fldissect = 0; 518 return 0;
|
1 2 3 4 5 6 7 8 9 10 11
| 487 static int fib_default_rules_init(struct fib_rules_ops *ops) 488 { ... 491 err = fib_default_rule_add(ops, 0, RT_TABLE_LOCAL); ... 494 err = fib_default_rule_add(ops, 0x7FFE, RT_TABLE_MAIN); ... 497 err = fib_default_rule_add(ops, 0x7FFF, RT_TABLE_DEFAULT); ... 500 return 0; 501 }
|
fib4_rules_ops_template
在系统初始化时候,注册的 IPv4 的策略路由 ops 模版,是非常重要的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 470 static const struct fib_rules_ops __net_initconst fib4_rules_ops_template = { 471 .family = AF_INET, 472 .rule_size = sizeof(struct fib4_rule), 473 .addr_size = sizeof(u32), 474 .action = fib4_rule_action, 475 .suppress = fib4_rule_suppress, 476 .match = fib4_rule_match, 477 .configure = fib4_rule_configure, 478 .delete = fib4_rule_delete, 479 .compare = fib4_rule_compare, 480 .fill = fib4_rule_fill, 481 .nlmsg_payload = fib4_rule_nlmsg_payload, 482 .flush_cache = fib4_rule_flush_cache, 483 .nlgroup = RTNLGRP_IPV4_RULE, 484 .owner = THIS_MODULE, 485 };
|
IPv4 的路由 ops 模板里包含了几个非常重要的函数指针。
fib4_rule_match
fib4_rule_action
fib4_rule_configure
策略路由查找
函数调用
对于策略路由的函数调用关系:
1 2 3 4 5 6 7 8 9 10 11 12
| => fib_lookup => => if (net->ipv4.fib_has_custom_rules) => => => return __fib_lookup(net, flp, res, flags); => => => => err = fib_rules_lookup(net->ipv4.rules_ops, flowi4_to_flowi(flp), 0, &arg); => => => => list_for_each_entry_rcu(rule, &ops->rules_list, list) { => => => => => if (!fib_rule_match(rule, ops, fl, flags, arg)) => => => => => => continue;//跳过 match 不匹配的节点。 => => => => => err = INDIRECT_CALL_MT(ops->action, //匹配的节点执行ops->action, 即fib4_rule_action => => => => => fib4_rule_action: 假如当前规则的action 是FR_ACT_TO_TBL => => => => => => tb_id = fib_rule_get_table(rule, arg); => => => => => => tbl = fib_get_table(rule->fr_net, tb_id); => => => => => => err = fib_table_lookup(tbl, &flp->u.ip4, //这个就是通用的根据一个 table 查到对应的路由。
|
查找入口函数:fib_lookup
协议栈路由查找函数入口:fib_lookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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); 405 return err; 406 }
|
我们这里重点关注配置了策略路由的场景(fib_has_custom_rules:TRUE)。
TODO:支持策略路由,但是没有添加策略路由规则时候,会涉及到多路由表/多table的场景优化。 我们放到跟不支持多路由场景一起讨论。
1 2 3 4 5 6 7
| 83 int __fib_lookup(struct net *net, struct flowi4 *flp, 84 struct fib_result *res, unsigned int flags) 85 { ... 95 err = fib_rules_lookup(net->ipv4.rules_ops, flowi4_to_flowi(flp), 0, &arg); ... 107 }
|
遍历策略规则:fib_rules_lookup
按优先级遍历策略路由规则(节点),直到 match 成功的规则,根据当前规则的 action,
- goto:跳到对应规则继续执行。
- FR_ACT_NOP:忽略,下一个节点
- 其他:执行 action。即
fib4_rule_action
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
| 313 int fib_rules_lookup(struct fib_rules_ops *ops, struct flowi *fl, 314 int flags, struct fib_lookup_arg *arg) 315 { ... 321 list_for_each_entry_rcu(rule, &ops->rules_list, list) { 322 jumped: 323 if (!fib_rule_match(rule, ops, fl, flags, arg)) 324 continue; 325 326 if (rule->action == FR_ACT_GOTO) { 327 struct fib_rule *target; 328 329 target = rcu_dereference(rule->ctarget); 330 if (target == NULL) { 331 continue; 332 } else { 333 rule = target; 334 goto jumped; 335 } 336 } else if (rule->action == FR_ACT_NOP) 337 continue; 338 else 339 err = INDIRECT_CALL_MT(ops->action, 340 fib6_rule_action, 341 fib4_rule_action, 342 rule, fl, flags, arg); ... 358 } ...
|
策略规则action:fib4_rule_action
先处理一些简单 action场景:
- FR_ACT_UNREACHABLE
- FR_ACT_PROHIBIT
- FR_ACT_BLACKHOLE
根据当前规则里定义的table id,找到对应 table,并在 table 里执行路由查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 110 INDIRECT_CALLABLE_SCOPE int fib4_rule_action(struct fib_rule *rule, 111 struct flowi *flp, int flags, 112 struct fib_lookup_arg *arg) 113 { ... 135 tb_id = fib_rule_get_table(rule, arg); 136 tbl = fib_get_table(rule->fr_net, tb_id); 137 if (tbl) 138 err = fib_table_lookup(tbl, &flp->u.ip4, 139 (struct fib_result *)arg->result, 140 arg->flags); 141 142 rcu_read_unlock(); 143 return err; 144 }
|
策略路由增删
策略路由插入:fib_nl_newrule/fib_newrule
1 2 3 4 5
| 996 static int fib_nl_newrule(struct sk_buff *skb, struct nlmsghdr *nlh, 997 struct netlink_ext_ack *extack) 998 { 999 return fib_newrule(sock_net(skb->sk), skb, nlh, extack, false); 1000 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 873 int fib_newrule(struct net *net, struct sk_buff *skb, struct nlmsghdr *nlh, 874 struct netlink_ext_ack *extack, bool rtnl_held) 875 { 889 ops = lookup_rules_ops(net, frh->family); 903 err = fib_nl2rule(net, nlh, extack, ops, tb, &rule, &user_priority); ... 910 err = fib_nl2rule_rtnl(rule, ops, tb, extack); ... 920 err = ops->configure(rule, skb, frh, tb, extack); ... 924 err = call_fib_rule_notifiers(net, FIB_EVENT_RULE_ADD, rule, ops, 925 extack); ... 945 if (last) 946 list_add_rcu(&rule->list, &last->list); 947 else 948 list_add_rcu(&rule->list, &ops->rules_list); ... 980 notify_rule_change(RTM_NEWRULE, rule, ops, nlh, NETLINK_CB(skb).portid);
|
重点关注下ops->configure, 如之前初始化里提到,等同于fib4_rule_configure
1
| 920 err = ops->configure(rule, skb, frh, tb, extack);
|
策略路由删除:fib_nl_delrule/fib_delrule
跟 insert 相对应,逻辑比较简单。
策略路由 dump:fib_nl_dumprule
1 2 3
| => fib_nl_dumprule => => dump_rules => => => fib_nl_fill_rule
|
正常 netlink dump逻辑,不展开讨论。