Linux MPLS功能详解

使用mpls功能,首先需要加载mpls相关的模块:
$ sudo modprobe mpls_gso
$ sudo modprobe mpls_iptunnel
$ sudo modprobe mpls_router  

使能mpls的接收和设置labels表项的数量,默认情况下内核不接收mpls报文,如果不使能此项,在如下使用ip命令配置本机环回lo接口接收mpls数据包时就会失败。labels转发表项的数量初始为0,将会导致不能配置label转发表项。

sysctl -w net.mpls.conf.lo.input=1
sysctl -w net.mpls.platform_labels=1048575


配置1:到网络10.1.1.0/30的数据包增加200和300两个label

$ sudo ip route add 10.1.1.0/30 encap mpls 200/300 via 192.168.1.1 dev ens33    

$ ip route 
10.1.1.0/30  encap mpls  200/300 via 192.168.1.1 dev ens33 

使用ping测试,tcpdump抓包内容可见,增加了200和300两个label,标签300设置了标签栈底标志[S]。
$ ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
$
$ sudo tcpdump mpls -vve
tcpdump: listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
23:36:30.813147 00:0c:29:74:7f:04 (oui Unknown) > 00:90:27:fe:c9:34 (oui Unknown), ethertype MPLS unicast (0x8847), length 106: 
MPLS (label 200, exp 0, ttl 64)
     (label 300, exp 0, [S], ttl 64)
     (tos 0x0, ttl 64, id 36751, offset 0, flags [DF], proto ICMP (1), length 84)
    localhost > localhost: ICMP echo request, id 6907, seq 1, length 64

配置2:标签为500的mpls数据包送到本机的环回接口lo:

$ sudo ip -f mpls route add 500 dev lo
$
$ip -f mpls route show
500 dev lo 
$

配置3:标签为100的数据包替换为标签600,发往192.168.1.2。

$ sudo ip -f mpls route add 100 as to 600 via inet 192.168.1.2
$
$ip -f mpls route show
100 as to 600 via inet 192.168.1.2 dev ens33 
500 dev lo 
$


配置4:指定多个下一跳地址,为每个下一跳指定不同的(90/91)标签:

$ sudo ip -f mpls route add 80 nexthop as to 90 via inet 192.168.1.2 nexthop as to 91 via inet 192.168.1.3  

$ ip -d -f mpls route show
unicast 80 proto boot scope global 
        nexthop as to 90 via inet 192.168.1.2  dev ens33
        nexthop as to 91 via inet 192.168.1.3  dev ens33
$

 

MPLS的路由结构

内核函数mpls_route_add处理MPLS路由的添加工作。所有的MPLS路由全部组织在网络命名空间的成员platform_label指针数组(指针的指针)中,通过标签值可找到对应的MPLS路由。全部路由不超过platform_labels指定的最大数量。

struct netns_mpls {
    size_t platform_labels;
    struct mpls_route __rcu * __rcu *platform_label;
}

一个mpls_route路由表项所占空间与其所有的下一跳mpls_nh的空间,以及每个下一跳的所有label空间之总和不超过4K字节大小。mpls_route路由表项结构图如下:

Linux MPLS功能详解

另外,内核注册了netdev通知链函数mpls_dev_notify处理mpls路由的失效和活动状态。

static struct notifier_block mpls_dev_notifier = {
    .notifier_call = mpls_dev_notify,
};

当接收到设备关闭NETDEV_DOWN事件时,调用mpls_ifdown遍历所有的mpls路由表项,将出口设备为此设备的表项标记为不可用(RTNH_F_DEAD)。如果接收到设备NETDEV_UP的事件,同样的遍历所有的表项,经出口设备为此设备的表项标记为可用,即去掉RTNH_F_DEAD标志。

static int mpls_dev_notify(struct notifier_block *this, unsigned long event, void *ptr)
{
    struct net_device *dev = netdev_notifier_info_to_dev(ptr);

    switch (event) {
    case NETDEV_DOWN:
        mpls_ifdown(dev, event);
        break;
    case NETDEV_UP:
        mpls_ifup(dev, RTNH_F_DEAD | RTNH_F_LINKDOWN);
        break;
	}
}

MPLS数据帧处理

Linux内核协议栈注册了一个ETH_P_MPLS_UC(0x8847)类型的数据包处理函数(mpls_packet_type)处理MPLS报文。
           
static struct packet_type mpls_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_MPLS_UC),
    .func = mpls_forward,
};

首先通过MPLS标签解析mpls_entry_decode函数,得到数据包中MPLS标签的4个字段:标签值、TTL、流量类别和标签栈底标志位。标签值字段占用20个bit位,其最大值为2**20 -1,由于mpls协议存在预留值,合法的标签值从16开始(MPLS_LABEL_FIRST_UNRESERVED)。

static inline struct mpls_entry_decoded mpls_entry_decode(struct mpls_shim_hdr *hdr)
{    
    struct mpls_entry_decoded result;
    unsigned entry = be32_to_cpu(hdr->label_stack_entry);

    result.label = (entry & MPLS_LS_LABEL_MASK) >> MPLS_LS_LABEL_SHIFT;
    result.ttl = (entry & MPLS_LS_TTL_MASK) >> MPLS_LS_TTL_SHIFT;
    result.tc =  (entry & MPLS_LS_TC_MASK) >> MPLS_LS_TC_SHIFT;
    result.bos = (entry & MPLS_LS_S_MASK) >> MPLS_LS_S_SHIFT;

    return result;
}

其次通过得到的MPLS标签值,查询mpls路由表。有之前介绍的mpls路由表结构可知,以标签值做索引即可找到对应的路由项:

static struct mpls_route *mpls_route_input_rcu(struct net *net, unsigned index)
{
    struct mpls_route __rcu **platform_label = rcu_dereference(net->mpls.platform_label);
    rt = rcu_dereference(platform_label[index]);
    return rt;
}

对于仅有一个下一跳的路由表项,直接使用。对于多个下一跳地址,需要在找到的路由项中,选择一个下一跳地址,函数(mpls_select_multipath)完成此功能。检查如果活动的下一跳的个数为0,返回空。否则根据数据包中的以下字段计算hash值:最多4个标签值、IP头的源IP地址、目的IP地址和协议号。得到的hash值与当前活动的下一跳数量(alive)取余得到下一跳地址的索引值。mpls_get_nexthop函数取出mpls路由的下一跳。

如果活动的下一跳数量与下一跳总数相等,说明根据下一跳索引nh_index可以直接取到下一跳结构。否则,需要遍历所有活动的下一跳列表,找到此下一跳。

    alive = READ_ONCE(rt->rt_nhn_alive);
    if (alive == 0) return NULL;

    hash = mpls_multipath_hash(rt, skb);
    nh_index = hash % alive;
    if (alive == rt->rt_nhn) goto out;
    for_nexthops(rt) {
        unsigned int nh_flags = READ_ONCE(nh->nh_flags);
        if (nh_flags & (RTNH_F_DEAD | RTNH_F_LINKDOWN))  continue;
        if (n == nh_index) return nh;
        n++;
    } endfor_nexthops(rt);
out:
    return mpls_get_nexthop(rt, nh_index);

如果配置了标签替换,仅替换数据包中的顶层标签。以下代码将mpls路由表项中下一跳对应的标签栈写入到数据包头中。

    hdr = mpls_hdr(skb);
    bos = dec.bos;
    for (i = nh->nh_labels - 1; i >= 0; i--) {
        hdr[i] = mpls_entry_encode(nh->nh_label[i], dec.ttl, 0, bos);
        bos = false;
    }

环境

iproute2版本:4.17.0
内核版本:  Linux-4.15