Linux VRF(Virtual Routing Forwarding)的原理和实现

VRF顾名思义就是虚拟路由转发(Virtual Routing Forwarding),简单点来讲,就是把一台路由器当多台虚拟路由器来用,这种技术在二层就是VLan技术,或者更加通用的虚拟交换机(在多网卡的Linux BOX上,创建多个bridge设备并把不同的网卡加入其中,就是一台携带多个虚拟交换机的物理交换机),我把二层的方案列如下:
在这里插入图片描述
那么,三层的VRF自然而然就是下面

在这里插入图片描述
现在考虑一个非常常见的场景,即OpenVPN典型的多处理拓扑,在一台服务器上会构建超级多的TAP网卡,每个TAP服务一个特定的VPN客户端,有了VRF的支撑后,不同的VPN节点便可以使用相同的IP网段了,所要做的仅仅是将不同的TAP网卡置入不同的VRF域即可。

大致明白了VRF的原理之后,我们来看一下VRF和策略路由的关系。
   我们已经知道VRF是一台物理路由器当多台虚拟路由器使用的,那么在这多台虚拟路由器之间就必须做到从物理层到三层路由的全部隔离(请注意,路由器是个三层设备),而策略路由仅仅是路由表的隔离,甚至在仅仅使能了策略路由的路由器上,其二层三层之间的邻居表都是共享的。不过,策略路由确实是实现VRF的组件之一,它确实可以完成VRF的路由表隔离,本文的后续可以看到,Linux正是使用策略路由来实现VRF路由表隔离的。

但是仅仅依靠策略路由机制是无法构建完整的VRF体系的,为了实现二层隔离以及网卡的隔离,还需要另外的技术,Linux的VRF实现中,采用了一种叫做L3mdev的技术来支持一种三层的虚拟网卡,利用这种虚拟网卡来隐藏网卡之间的可见性。

(2). Linux VRF的使用和配置
本节是例行的一节,如果你都没让系统跑起来就去研究它的原理,那势必是本末倒置的,研究技术的目的是更好地使用技术,所以必然要先用起来VRF。
   但我不准备在本节着墨太多,因此我选择了引用的方式。因为几个老外已经把使用和配置写得淋漓尽致了,这些东西,我再写也只是复制粘贴,显得毫无意义,因此我引用他们写好的东西,同时,作为补充,本节以下我写一些他们没有提到的东西。
   本节的重点就是下面几个链接,文章都不长,全部都是Step by step式的Howto,请把它们看完,你将学会Linux VRF的使用方法!

Virtual Routing and Forwarding (VRF) Document:https://www.kernel.org/doc/Documentation/networking/vrf.txt
Virtual Routing and Forwarding (VRF):https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF)
Lwn-net: VRF support:https://lwn.net/Articles/632522/
Working with VRF on Linux:http://www.routereflector.com/2016/11/working-with-vrf-on-linux/
Using VRFs with linux:https://andir.github.io/posts/linux-ip-vrf/

(3). Linux VRF实现概览
要说实现,虚拟网卡!本文提到的VRF也是基于虚拟网卡实现,每一个VRF域都表现为一个虚拟网卡,然后将具体的物理网卡(或者是别的虚拟网卡)添加到特定的VRF虚拟网卡,从而实现隔离:
在这里插入图片描述
从OpenVPN使用Tap/Tun,IPSec VTI,到Bridge,Bonding,VLan,VXLan…这些在Linux系统中,全部基于虚拟网卡来实现!总体来讲,所有这些虚拟网卡,最终都要有真实的东西附着在上面,比如:

附着字符设备在上面的:OpenVPN的Tun/Tap网卡
附着物理网卡的:Bridge,Bonding,VLan
附着加密引擎的:VTI

可见,VRF虚拟网卡实现了三层逻辑,该三层逻辑就是VRF路由表隔离,具体的实现细节下面的小节会结合代码详述,本节只是给出概览。

(4) Linux VRF的实现细节

VRF实现两阶段:
4.3内核~4.8内核:第一阶段基础版。基础设施不完善,需要外部策略路由规则配合才可用
4.8内核以后:第二阶段完备版。引入了L3mdev,完善的基础设施支持
在这里插入图片描述
数据包在被物理网卡ethX接收后,在netif_receive_skb中,会有rx_handler回调来截获数据包。VRF会注册一个rx_handler回调,该回调中将skb的dev字段替换为VRF虚拟网卡Device对象,这个处理是和Bridge处理一致的。接下来,系统依赖以下的事实来实现VRF逻辑:
数据包skb的dev字段已经是VRF设备,表明数据包看起来是通过VRF X来接收的用户显式配置的Policy Routing的rule要求来自VRF X接收的数据包要查询X号路由表.
这就实现了VRF逻辑。

可以看到,用户需要自己来手工完成策略路由表的定向操作,这个配置是重点,没有它的话,便不会让VRF网卡接收的数据包查询策略路由表。
  这明显是一种很初级但可用(我的做法确实Low,但是就是可用!这是Linux的一种典型的文化)的做法。很自然的,Linux 4.8内核开始,VRF有了第二阶段的实现方法,省略了用户自己手工配置策略路由这个步骤。

VRF第二阶段 L3mdev 机制
在Linux 4.8内核以及以后,系统提供了一种更加优雅的做法,即引入了一个叫做L3mdev(Layer 3 master device)的机制,有了这个L3mdev机制,便省去了显式配置策略路由的必要。在创建一个VRF虚拟网卡的时候,系统便将其与一个特定的策略路由表自动关联,L3mdev机制基于这种关联来完成策略路由表的定向操作。
  这种L3mdev机制事实上相当于又一个3层的Hook,该Hook将感兴趣流量伪装成从一个虚拟网卡接收,用该虚拟网卡来实现一些特定于3层的处理逻辑,这种风格实际上只是利用了虚拟网卡,而并不是非用不可。  在这里插入图片描述
对比一下4.8内核之前的处理方法,就知道如今的L3mdev有多么高尚了,老的注册rx_handler回调的方法如下图:
在这里插入图片描述
现在,我给出采用新的L3mdev机制的VRF实现框图(请与老的rx_handler回调方式做对比):
在这里插入图片描述
以上就是VRF实现的总体架构了,接下来我会给出一些细节,在分析这些细节之前,我先将VRF的实现分成了两个部分:

控制路径部分
这部分主要控制VRF虚拟网卡的创建,策略路由表的关联等。
数据路径部分
这部分主要描述数据时如何通过VRF路由的。

(5). 控制路径的实现逻辑
当我们创建了一个VRF域的时候,发生了什么?我想下图是可以解释的:
在这里插入图片描述
(6). 数据路径的实现逻辑
以下(即Linux 4.8及以后的VRF内核实现)来用两个情景分析的方式阐述VRF的实现,通过这两个情景分析,基本上VRF的实现也就了然于胸了。(如果我这里的论述显得晦涩,那就权当是提纲吧,这部分代码很简单,自己走读一下即可)

从独立的物理网卡,比如eth0收到数据包,依次经过网卡驱动程序,netif_receive_skb,ip_rcv等调用,直到ip_rcv_finish被调用之前,VRF的逻辑和非VRF逻辑并没有任何不同支持,在ip_rcv_finish中,事情起了变化:

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    const struct iphdr *iph = ip_hdr(skb);
    struct rtable *rt;
    struct net_device *dev = skb->dev;
 
    /* if ingress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    // 这里增加了这么一个L3mdev调用,正是该L3mdev逻辑的处理,实现了VRF的核心:路由表使用与VRF域关联的策略路由表
    skb = l3mdev_ip_rcv(skb);
    // 后面的逻辑直到定位路由表,VRF逻辑和常规逻辑没有任何不同
    ...
}

具体来讲,l3mdev_ip_rcv的逻辑非常简单,对于VRF而言,实现l3mdev_l3_rcv回调函数完成的功能仅仅是:
将skb的dev字段重新修改为该dev的master设备,即该物理网卡附着的VRF虚拟网卡设备。

我们接下来看下上述的l3mdev_ip_rcv里做的关于skb的dev字段的修改在什么时候会用到。可以想象的是,当然是在定位策略路由表的时候用的咯,问题是,在代码层面这是怎么实现的。

我们跳过中间步骤,直达策略路由表的定位,在定位路由表的时候,实际上是在定位一个rule,系统会遍历所有的rules链表,比较典型的rules链表可以通过下面的命令查看:ip rule ls
  一般情况下,我们会看到以下结果:

0:      from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default 

然而,当我们配置了至少一个VRF域的时候,我们将会看到如下的结果:

0:      from all lookup local 
1000:   from all lookup [l3mdev-table] 
32766:  from all lookup main 
32767:  from all lookup default  

嗯,多了一个l3mdev-table,系统在遍历rules链表的时候,遇到[l3mdev-table]表的时候,采用的是一种隐式的处理方式。意思是说,即便你创建了多个VRF域,关联了多张策略路由表,系统中依然只能看到一张[l3mdev-table]表。既然这样,系统又是如何定位到与特定的VRF域关联的那张路由表呢?比如说,vrf-blue关联了路由表10,vrf-red关联了路由表20,此时正在处理的包属于vrf-blue,系统如何定位到要查找路由表10而不是路由表20呢?
为了了解[l3mdev-table]的工作方式,就需要看下fib_rule_match的逻辑:

static int fib_rule_match(struct fib_rule *rule, struct fib_rules_ops *ops,
              struct flowi *fl, int flags,
              struct fib_lookup_arg *arg)
{
    int ret = 0;
 
    ...
    // 前面的逻辑是常规的iif,oif,mark等匹配,以下的这个l3mdev不一般!
    // rule->l3mdev即ip rule ls看到的那个[l3mdev-table]
    if (rule->l3mdev && !l3mdev_fib_rule_match(rule->fr_net, fl, arg))
        goto out;
 
    ret = ops->match(rule, fl, flags);
out:
    return (rule->flags & FIB_RULE_INVERT) ? !ret : ret;
}

我们重点看下l3mdev_fib_rule_match:

int l3mdev_fib_rule_match(struct net *net, struct flowi *fl,
              struct fib_lookup_arg *arg)
{
    struct net_device *dev;
    int rc = 0;
 
    rcu_read_lock();
    ... // 我们仅仅分析iif情景
 
    dev = dev_get_by_index_rcu(net, fl->flowi_iif);
    // 如果这个dev是一个VRF master虚拟网卡设备
    if (dev && netif_is_l3_master(dev) &&
        dev->l3mdev_ops->l3mdev_fib_table) {
        // 那么便通过其自身的回调函数取出和该VRF关联的策略路由表!
        arg->table = dev->l3mdev_ops->l3mdev_fib_table(dev);
        rc = 1;
        goto out;
    }
 
out:
    rcu_read_unlock();
 
    return rc;
}

到此为止呢,我们的一幅图景就闭合了:
首先在ip_rcv_finish的l3mdev_ip_rcv调用中定位到与收包物理网卡关联的VRF master虚拟网卡;
然后在fib_lookup内层的l3mdev_fib_rule_match调用中取出与VRF master虚拟网卡关联的策略路由表;
最终在该特定的路由表中进行路由查找。

(7). 本地始发包的VRF实现分析
由于本地的数据包全部来自于某个socket而不是网卡,socket是不和网卡关联的,所以IP层在查路由之前无法知道一个数据包和哪个网卡关联,就更别提去选择使用哪张路由表了…
因此,如若一个socket程序想使用VRF机制,就必须为一个socket去绑定一个特定的网卡:

setsockopt(sd, SOL_SOCKET, SO_BINDTODEVICE, vrf_dev, strlen(vrf_dev)+1);

有了这个调用,在ip_queue_xmit中查找路由的时候,自然就会在fib_rule_match中定位到与参数vrf_dev关联的策略路由表了,然而,如果socket程序并不知情,它只是bind了一个隶属于该vrf_dev的slave物理网卡,比如eth0,那要怎么处理呢?
显然SO_BINDTODEVICE参数会为数据包在__ip_route_output_key_hash调用中的路由查找失败而负责,为其暂时绑定一个dummy dst_entry,从而逻辑可以到达__ip_local_out:

int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    struct iphdr *iph = ip_hdr(skb);
 
    iph->tot_len = htons(skb->len);
    ip_send_check(iph);
 
    /* if egress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    skb = l3mdev_ip_out(sk, skb);
    ...
}

和网卡收包过程的l3mdev调用一样,我们看下这个与l3mdev_ip_rcv相对的l3mdev_ip_out调用里做了什么文章,实现对应回调函数的是vrf_ip_out:

static struct sk_buff *vrf_ip_out(struct net_device *vrf_dev,
                  struct sock *sk,
                  struct sk_buff *skb)
{
    struct net_vrf *vrf = netdev_priv(vrf_dev);
    ...
    // 取出VRF关联的那个唯一的dst_entry,以便将数据包定向到vrf_xmit
    rth = rcu_dereference(vrf->rth);
    if (likely(rth)) {
        dst = &rth->dst;
        dst_hold(dst);
    }
    ...
    skb_dst_set(skb, dst);
 
    return skb;
}

很简单,在这里仅仅是将skb的那个dummy dst_entry换成了和VRF设备绑定的那个dst_entry。

事实上,与VRF设备绑定的dst_entry只有一个且只做一件事,那就是,调用VRF设备的dev_hard_xmit回调函数,VRF机制正是在该dev_hard_xmit回调函数中实现了真正的路由查询,在dev_hard_xmit回调中真正做事的函数是vrf_process_v4_outbound:

static netdev_tx_t vrf_process_v4_outbound(struct sk_buff *skb,
                       struct net_device *vrf_dev)
{
    struct iphdr *ip4h = ip_hdr(skb);
    int ret = NET_XMIT_DROP;
    struct flowi4 fl4 = {
        /* needed to match OIF rule */
        // 这个会让l3mdev_fib_rule_match定位到正确的路由表
        .flowi4_oif = vrf_dev->ifindex,
        .flowi4_iif = LOOPBACK_IFINDEX,
        .flowi4_tos = RT_TOS(ip4h->tos),
        .flowi4_flags = FLOWI_FLAG_ANYSRC | FLOWI_FLAG_SKIP_NH_OIF,
        .daddr = ip4h->daddr,
    };
    struct net *net = dev_net(vrf_dev);
    struct rtable *rt;
    // 真实查路由
    rt = ip_route_output_flow(net, &fl4, NULL);
    ...
    // 真实发送skb
    ret = vrf_ip_local_out(dev_net(skb_dst(skb)->dev), skb->sk, skb);
    ...
}

整个流程就这样结束了!

(8).总结

到此为止,我可以给出一幅整个的图景了,结合源代码观摩,定有收获:
在这里插入图片描述

关于Linux VRF的小Trick
突然觉得Linux自2.6.32以来真的更新了太多,和本文相关的,有两个问题,首先是local路由表的问题,其次是TCP Listener的问题,和本文不相关的还有一个命令行问题,听我细说。

Local路由表问题
早在几年前的时候,我曾经抱怨说:
数据包来了之后首先要看看是不是本地接收的
要想理解这个,首先你得知道Linux系统中策略路由表匹配的原理。很简单,就是一个典型的switch-case语句:

switch elements:
    case rule1:
        table=tab1
        break;
    case rule1:
        table=tab1
        //fall through; 即匹配下一条rule
    case rule1:
        table=tab1
        break;
    default:
        unreachable? // 或者任意别的什么...

这在Linux的网络协议栈实现中体现为要先强制查询Local路由表,然后再查Main路由表…这个顺序并不意味着什么大不了的事,关键在于,曾经Local路由表全系统仅此一张,因此,如果本机的应用层有两个属于不同VRF域但是却侦听同一个IP地址的服务,势必会造成混乱,早先的系统中,唯一的Local路由表是IP路由系统第一个无条件要查询的,它可以绕过任何其它的路由判断,包括策略路由表,即Local路由表必须在策略路由表之前被匹配,如果不成功才Fall through到后面的路由表。

然而,在引入VRF后,Local表不再是全局唯一的了,每一个VRF域都有一张Local路由表,当你把一个网卡添加进一个VRF域的时候,此网卡相关的Local路由将全部重置到此VRF域的Local路由表中。

然而,全局来看,系统中还是会有一张Local路由表,它看起来在所有rule匹配之前被匹配,不过事情起了变化。当你看到下面的策略路由表时:

0:      from all lookup local 
1000:   from all lookup [l3mdev-table] 
32766:  from all lookup main 
32767:  from all lookup default

你可通过下面的命令删除Local表(请注意,这在低版本的内核以及iproute2中是不允许的):

ip rule del pref 0

但为了避免你使用的ssh即时中断(其实是为了做到无缝切换),你要在del之前先add,因此整个切换过程的脚本变成了下面的样子:

ip rule add pref 30000 table local
ip rule del pref 0

即你在添加一条策略路由前,必须先添加一条。
这也是人之常情了。然后,你会发现,事情的局面变成了下面的样子(当你再次执行ip rule ls时):

1000:   from all lookup [l3mdev-table] 
30000:  from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default 

了解了这个事实之后,我们明白了,解决之道当然是隔离。看懂了这个ip rule ls的显式,我们的心情安顿了,是的,Linux的VRF做到了完全的三层隔离。

TCP Listener问题
对于本地始发的流量,我们知道,必须要为其通过显式的setsockopt调用绑定一个VRF设备才能终安,道理是这样,事实也是这样。

由于绑定VRF虚拟网卡设备的setsockopt是针对特定socket的,如果我们仅仅针对Listener socket做了这样的setsocketopt(事实上我们也仅仅能这么做),那么对于别的那些被这个Listener socket给Accept的那些socket,谁来负责呢(需要一种机制,让所有相关的socket都绑定VRF网卡)?

嗯,我们实在没有必要在accept返回后为每一个返回的client socket都去bind一个VRF device,系统可以自动做到这一点:

sysctl -w net.ipv4.tcp_l3mdev_accept=0

OK,这就好了!
该内核参数的另一个作用是,自动识别那些从隶属于某个VRF域的网卡上收到的数据包所属的具体VRF域

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值