linux内核多路由表与策略路由的实现

使用场景

接上篇文章,我们提到在网口eth1 上增加一个 ip 地址,内核会生成 3 个路由。

  • local 路由表:增加/32 主机路由
  • main 路由表:增加本网段前缀的网络路由

为什么需要使用local/main 两个路由表呢? ip地址引发的路由变化或者手动添加一个目的网段的路由,这些场景看起来,完全可以使用一个路由表就能满足场景需求。那内核为什么要支持多个路由表?

其中一个场景就是 策略路由
比如公司有两个网关, 其中一个网关GW1,网络质量比较高。我们希望对网络质量有较高要求的个别网段的外访流量,走GW1.其他网段都用 GW2.
这时,我们就不能仅仅依赖目前 IP 去做路由。我们需要:

  • 先根据源 IP 地址做一次筛选,选中的这部分流量,缺省路由到GW1.
  • 没有选中的走我们的正常路由查找,其中缺省路由到GW2。
  • 这两个缺省路由只能放到两个单独的路由表里。

那下面详细展开介绍下策略路由及内核实现。

策略路由

策略路由是一个按优先级排列的规则链表。路由查找时,按优先级顺序遍历链。

每个策略路由规则,由match+action两部分组成。 match 也被称为SELECTOR

  1. 每个规则的match条件,可以支持多种字段,如源IP/协议/协议源端口/目的端口等,也可以这多个字段的组合。如果满足规则匹配条件就执行规则指定的动作。
  2. 每个策略路由的action有下面几种:
    • table X:查找对应的路由表 table X。X 是 table ID 或者 table 名字。
    • goto xx:跳转到更低优先级到规则。
    • nat …:ip地址替换
  3. 所有的策略路由通过 IP rule 命令添加。
  4. 策略路由按照优先级插入到一个链表里。
  5. 按照优先级从高(数值最小)到低(数值大)按顺序排列。
  6. 通过 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三张路由表里查询路由。

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

默认三条策略路由

通过命令,我们可以看到 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逻辑,不展开讨论。