Linux内核轻量级隧道

目前内核支持的封装类型由枚举类型lwtunnel_encap_types定义,如下所示支持MPLS、IP、ILA、IP6、SEG6、BPF和SEG6_LOCAL等7种类型。函数lwtunnel_valid_encap_type负责检验用户配置(通过netlink接口)的封装类型是否合法,合法的封装类型必须大于LWTUNNEL_ENCAP_NONE,并且小于__LWTUNNEL_ENCAP_MAX;而且内核必须已经注册了相应封装类型的操作函数集(由结构体lwtunnel_encap_add_ops表示),其通过函数lwtunnel_encap_add_ops进行注册。

enum lwtunnel_encap_types {
    LWTUNNEL_ENCAP_NONE,
    LWTUNNEL_ENCAP_MPLS,
    LWTUNNEL_ENCAP_IP,
    LWTUNNEL_ENCAP_ILA,
    LWTUNNEL_ENCAP_IP6,
    LWTUNNEL_ENCAP_SEG6,
    LWTUNNEL_ENCAP_BPF,
    LWTUNNEL_ENCAP_SEG6_LOCAL,
    __LWTUNNEL_ENCAP_MAX,
}; 

内核使用结构体lwtunnel_encap_ops表示特定的封装类型操作函数集,系统中所有注册的封装类型操作函数集保存在全局数组lwtun_encaps中。

static const struct lwtunnel_encap_ops __rcu *lwtun_encaps[LWTUNNEL_ENCAP_MAX + 1] __read_mostly;

下表列出了内核中封装类型和操作函数集的对应信息:

 +---------------------------+-------------------+--------------------+
 |    LWTUNNEL_ENCAP_MPLS    |  mpls_iptun_ops   |  mpls_iptunnel.c   |
 +---------------------------+-------------------+--------------------+
 |    LWTUNNEL_ENCAP_IP      |  ip_tun_lwt_ops   |  ip_tunnel_core.c  |
 +---------------------------+-------------------+--------------------+
 |    LWTUNNEL_ENCAP_ILA     |  ila_encap_ops    |  ila_lwt.c         |
 +---------------------------+-------------------+--------------------+
 |    LWTUNNEL_ENCAP_IP6     |  ip6_tun_lwt_ops  |  ip_tunnel_core.c  |
 +---------------------------+-------------------+--------------------+
 |    LWTUNNEL_ENCAP_SEG6    |  seg6_iptun_ops   |  seg6_iptunnel.c   |
 +---------------------------+-------------------+--------------------+
 |    LWTUNNEL_ENCAP_BPF     |  bpf_encap_ops    |  lwt_bpf.c         |
 +---------------------------+-------------------+--------------------+
 | LWTUNNEL_ENCAP_SEG6_LOCAL |  seg6_local_ops   |  seg6_local.c      |
 +---------------------------+-------------------+--------------------+

路由系统隧道配置

LWTUNNEL系统的功能实现绑定在内核路由系统上。如下IP命令用于配置封装类型为IP或者GENEVE的轻量级隧道。

使用external关键字配置IP隧道与GENEVE隧道:
ip link add vxlan1 type vxlan dstport 4789 external
ip link set dev vxlan1 up
ip addr add 20.1.1.1/24 dev vxlan1
ip route add 10.1.1.1 encap ip id 30001 dst 20.1.1.2 dev vxlan1

ip link add gnv0 type geneve external
ip link set dev gnv0 up
ip route add 172.16.20.1/32 encap ip id 1234 dst 192.168.100.3 dev gnv0

不使用external配置vxlan与geneve隧道:
ip link add dev vxlan1 type vxlan id 30001 remote 20.1.1.2 dstport 4789
ip route add 10.1.1.1 dev vxlan1

ip link add dev gnv0 type geneve remote 192.168.100.3 vni 1234 dstport 5678           
ip route add 172.16.20.1 dev gnv0

与路由系统结合在一起的LWTUNNEL轻量级隧道,将隧道信息保存在路由系统中。内核中函数inet_rtm_newroute负责处理以上的ip route命令新添加的路由表项:其首先调用函数rtm_to_fib_config将netlink消息中的数据转换保存到内核的fib_config结构体中,成员fc_encap_type保存封装类型,成员fc_encap保存整个封装数据:

static int rtm_to_fib_config(struct net *net, struct sk_buff *skb, struct nlmsghdr *nlh, struct fib_config *cfg, ...)
{
    nlmsg_for_each_attr(attr, nlh, sizeof(struct rtmsg), remaining) {
        switch (nla_type(attr)) {
        case RTA_ENCAP:
            cfg->fc_encap = attr;
            break;
        case RTA_ENCAP_TYPE:
            cfg->fc_encap_type = nla_get_u16(attr);
            err = lwtunnel_valid_encap_type(cfg->fc_encap_type, extack);
            break;
        }
    }
} 

其次调用fib_table_insert函数将路由表项插入到内核FIB表中,fib_create_info负责在此之前创建要插入的fib_info结构信息。其检查fib_config结构的成员fc_encap是否有值,有值表明配置了隧道封装信息。随即创建特定隧道封装的lwtunnel_state信息,并且保存在fib_info的成员fib_nh下一跳数据结构中,以供路由匹配到相应流量时使用。

struct fib_info *fib_create_info(struct fib_config *cfg, struct netlink_ext_ack *extack)
{    
    struct fib_nh *nh = fi->fib_nh;
    if (cfg->fc_encap) {
        struct lwtunnel_state *lwtstate;

        err = lwtunnel_build_state(cfg->fc_encap_type, cfg->fc_encap, AF_INET, cfg, &lwtstate, extack);
        nh->nh_lwtstate = lwtstate_get(lwtstate);
    }
}

以IP封装类型为例,其build_state函数为ip_tun_build_state,除了要分配通用的lwtunnel_state数据结构外,还分配了特定于IP封装的ip_tunnel_info结构体(紧随lwtunnel_state结构体空间之后),特定于IP封装类型的信息保存在ip_tunnel_info私有结构体中,如封装的id、dst等信息。每种特定的封装如有必要都会分配私有的数据结构,如MPLS封装,在mpls_build_state函数中,分配mpls_iptunnel_encap数据结构以及label所需空间。

static int ip_tun_build_state(...)
{
    struct ip_tunnel_info *tun_info;
    struct lwtunnel_state *new_state;

    new_state = lwtunnel_state_alloc(sizeof(*tun_info));
    new_state->type = LWTUNNEL_ENCAP_IP;
    tun_info = lwt_tun_info(new_state);

    if (tb[LWTUNNEL_IP_ID])
        tun_info->key.tun_id = nla_get_be64(tb[LWTUNNEL_IP_ID]);
}

轻量级隧道路由入口

进入本机的数据包,在经过NF_INET_PRE_ROUTING hook点之后,交由内核函数ip_rcv_finish处理,其通过查找路由表决定数据包的流向,即ip_route_input_noref函数查询入口路由,决定是送往本地应用还是转发。在查询到路由表项后使用__mkroute_input函数创建路由缓存结构rtable。

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    if (!skb_valid_dst(skb)) {
        err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, dev);
    }
    return dst_input(skb);
}
static int __mkroute_input(struct sk_buff *skb, const struct fib_result *res, struct in_device *in_dev, __be32 daddr, __be32 saddr, u32 tos)
{
    rth->dst.input = ip_forward;

    rt_set_nexthop(rth, daddr, res, fnhe, res->fi, res->type, itag, do_cache);
    set_lwt_redirect(rth);
    skb_dst_set(skb, &rth->dst);
}

其中rt_set_nexthop函数,根据路由的下一跳信息,找到在上一节中使用ip route命令配置封装路由时,创建的lwtunnel_state结构。其保存有指定的封装类型相关的信息,赋值给路由缓存结构中的lwtstate成员。

static void rt_set_nexthop(...)
{
    if (fi) {
        struct fib_nh *nh = &FIB_RES_NH(*res);
        rt->dst.lwtstate = lwtstate_get(nh->nh_lwtstate);
    }
}

如果配置了路由相关的轻量级隧道,路由的结果出了本地上送和转发之后,第三条路就是重定向到轻量级隧道中,即函数set_lwt_redirect,如果此关联隧道为输入方向(设置了LWTUNNEL_STATE_INPUT_REDIRECT标志),将其input函数指针修改为隧道自身的输入函数lwtunnel_input,并且保留原有的input指针函数到orig_input成员中,对于像BPF类型的隧道,在其进行完bpf相关处理后,还会调用orig_input指针函数继续被重定向之前的接收流程。

static void set_lwt_redirect(struct rtable *rth)
{
    if (lwtunnel_output_redirect(rth->dst.lwtstate)) {
        rth->dst.lwtstate->orig_output = rth->dst.output;
        rth->dst.output = lwtunnel_output;
    }
    if (lwtunnel_input_redirect(rth->dst.lwtstate)) {
        rth->dst.lwtstate->orig_input = rth->dst.input;
        rth->dst.input = lwtunnel_input;
    }
}

最终,在路由查询完成之后,ip_rcv_finish函数调用路由指定的input函数dst_input,此处调用的为lwtunnel_input函数。


轻量级隧道路由出口

出口函数的设置位于出口路由的生成函数__mkroute_output中。即在查找到出口路由之后,生成rtable路由缓存项时,检查rtable下一跳中是否关联有轻量级隧道状态信息lwtunnel_state,如果存在并且此隧道为出口方向(置位LWTUNNEL_STATE_OUTPUT_REDIRECT),路由缓存结构的output指针指向隧道的lwtunnel_output函数。此过程与LWTUNNEL路由入口的处理方式一致,仅是方向上的差别。

static struct rtable *__mkroute_output(const struct fib_result *res, const struct flowi4 *fl4, ...)
{
    struct fib_info *fi = res->fi;
    struct fib_nh_exception *fnhe;

    rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0, do_cache);
    set_lwt_redirect(rth);
}

rth->dst.output函数由dst_output进行封装调用,对于源自本机的数据包,调用位置在NF_INET_LOCAL_OUT hook点之后,参见函数__ip_local_out,遍历完此hook点挂载的函数之后,调用dst_output,实际为调用lwtunnel_output函数。

int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
               net, sk, skb, NULL, skb_dst(skb)->dev, dst_output);
}

对于转发的数据包,其调用位置位于函数ip_forward_finish中,在hook点NF_INET_FORWARD遍历完成之后调用。

int ip_forward(struct sk_buff *skb)
{
    return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
               net, NULL, skb, skb->dev, rt->dst.dev, ip_forward_finish);
}
static int ip_forward_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    return dst_output(net, sk, skb);
}

传输类型XMIT

之前阐述的通过set_lwt_redirect函数重定向的轻量级隧道为INPUT或者OUTPUT类型,通过dst_input或者dst_output函数调用嫁接在内核网络协议栈中。此处的XMIT传输类型,直接编码在ip_finish_output2函数中,此处理位于NF_INET_POST_ROUTING hook点之后,目前仅有MPLS隧道在使用,其通过mpls_xmit函数为数据包增加MPLS标签,之后将数据包发送到网络中。

static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    if (lwtunnel_xmit_redirect(dst->lwtstate)) {
        int res = lwtunnel_xmit(skb);

        if (res < 0 || res == LWTUNNEL_XMIT_DONE)
            return res;
    }
}

内核版本

Linux-4.15

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页