Featured image of post P4示例程序-6 MPLS

P4示例程序-6 MPLS

MPLS是一种高效且可扩展的数据转发机制,它不再根据IP包头里的IP地址来做路由查找,而是根据给数据包加上的短标签来快速转发。

例程拓扑

MPLS(多协议标签交换)是一种高效且可扩展的数据转发机制,位于第二层(数据链路层)和第三层(网络层)之间,有时被称为第2.5层协议。它的核心思想是:不再根据IP包头里的IP地址来做路由查找,而是根据给数据包加上的短标签Label来快速转发。

  1. 当一个数据包进入MPLS网络,边缘的路由器(称为LER, Label Edge Router)根据数据包的目的地或者其他属性,给数据包打上一个或多个标签;

  2. 数据包进入MPLS网络后,中间的路由器(称为LSR, Label Switch Router)只看标签,不再关心IP地址。根据标签做快速查表决定下一跳,并根据需要替换标签、删除标签或添加新标签;

  3. 当数据包离开MPLS网络时,出口边缘路由器(即LER)会去掉MPLS标签,还原为普通的IP包,发送到最终目的地。

拓扑描述文件

此处仅提供Python脚本形式的拓扑描述文件,其中的setP4SourceAll函数可以便捷的为全部交换机设置运行的P4程序。此外需要注意的是为节点之间建立链路时使用了addLink函数的可选参数port1、port2,这明确指示将node1上的port1与node2上的port2连接,明确的拓扑连接关系有助于后续MPLS规则的设置。

 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
30
31
32
33
34
35
36
37
38
39
40
41
from p4utils.mininetlib.network_API import NetworkAPI

net = NetworkAPI()

// Network general options
net.setLogLevel('info')

// Network definition
net.addP4Switch('s1', cli_input='sx-commands/s1-commands.txt')
net.addP4Switch('s2', cli_input='sx-commands/s2-commands.txt')
net.addP4Switch('s3', cli_input='sx-commands/s3-commands.txt')
net.addP4Switch('s4', cli_input='sx-commands/s4-commands.txt')
net.addP4Switch('s5', cli_input='sx-commands/s5-commands.txt')
net.addP4Switch('s6', cli_input='sx-commands/s6-commands.txt')
net.addP4Switch('s7', cli_input='sx-commands/s7-commands.txt')
net.setP4SourceAll('stacked.p4')

net.addHost('h1')
net.addHost('h2')
net.addHost('h3')

net.addLink("h1", "s1", port2=1)
net.addLink("s1", "s2", port1=2, port2=1)
net.addLink("s1", "s3", port1=3, port2=1)
net.addLink("s2", "s4", port1=2, port2=1)
net.addLink("s3", "s4", port1=2, port2=2)
net.addLink("s4", "s5", port1=3, port2=1)
net.addLink("s4", "s6", port1=4, port2=1)
net.addLink("s5", "s7", port1=2, port2=1)
net.addLink("s6", "s7", port1=2, port2=2)
net.addLink("s7", "h2", port1=3)
net.addLink("s7", "h3", port1=4)

// Assignment strategy
net.l3()

// Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
net.enableCli()
net.startNetwork()

交换机程序

main函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//include <core.p4>
//include <v1model.p4>

V1Switch(
    MyParser(),
    MyVerifyChecksum(),
    MyIngress(),
    MyEgress(),
    MyComputeChecksum(),
    MyDeparser()
) main;

定义头部

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// EtherType值为0x0800时表示是IPv4包
const bit<16> TYPE_IPV4 = 0x0800;
// EtherType值为0x8847时表示是MPLS包
const bit<16> TYPE_MPLS = 0x8847;

// 后续MPLS相关的匹配动作表大小
//define CONST_MAX_LABELS 	128
// 最多支持多少个MPLS标签,即最多会有多少跳
//define CONST_MAX_MPLS_HOPS 8

typedef bit<9>  egressSpec_t; // 出端口编号
typedef bit<48> macAddr_t;    // 48位的MAC地址
typedef bit<32> ip4Addr_t;    // 32位的IPv4地址
typedef bit<20> label_t;      // 20位的MPLS标签

// 定义以太网头部的各字段
header ethernet_t {
    macAddr_t dstAddr;
    macAddr_t srcAddr;
    bit<16>   etherType;
}

// 定义MPLS标签的各字段
header mpls_t {
    bit<20>   label; // MPLS标签值,决定转发出端口的关键字段
    bit<3>    exp;   // 实验字段
    bit<1>    s;     // 栈底标志位(如果是最后一个MPLS标签,就设为1)
    bit<8>    ttl;   // 生存时间
}

// 定义IPv4头部的各字段
header ipv4_t {
    bit<4>    version;
    bit<4>    ihl;
    bit<8>    diffserv;
    bit<16>   totalLen;
    bit<16>   identification;
    bit<3>    flags;
    bit<13>   fragOffset;
    bit<8>    ttl;
    bit<8>    protocol;
    bit<16>   hdrChecksum;
    ip4Addr_t srcAddr;
    ip4Addr_t dstAddr;
}

// 无需自定义的元数据,但需要定义
struct metadata {

}

// 实例化数据包头部
struct headers {
    ethernet_t                      ethernet;
    mpls_t[CONST_MAX_MPLS_HOPS]     mpls;
    ipv4_t                          ipv4;
}

数据包解析

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {
    // 数据包解析的入口
    state start {
        // 直接跳转至parse_ethernet,进而解析以太网头部
        transition parse_ethernet;
    }
    
    // 解析以太网头部各字段
    state parse_ethernet {
        // 提取以太网头部各字段
        packet.extract(hdr.ethernet);
        // 根据以太网头部指示的帧类型执行不同的解析动作
        transition select(hdr.ethernet.etherType) {
            // 为MPLS包,跳转至parse_mpls,进而解析MPLS标签
            TYPE_MPLS: parse_mpls;
            // 为IPv4包,跳转至parse_ipv4,进而解析IPv4头部
            TYPE_IPV4: parse_ipv4;
            // 若为其它未知类型,则结束解析
            default: accept;
        }
    }

    // 解析MPLS标签
    state parse_mpls {
        // 由于mpls是一个数组,提取一个新的MPLS标签到数组的下一个位置
        // 其中的next表示下一个还没提取的位置
        packet.extract(hdr.mpls.next);
        // 根据s字段判断是否是最后一个MPLS标签
        // 其中的last表示最后一个被提取出来的头部
        transition select(hdr.mpls.last.s) {
            // 如果解析到了最后一个MPLS标签,就接着去解析后面的IPv4头部
            1: parse_ipv4;
            // 如果后续仍有MPLS标签,则调用parse_mpls继续解析
            default: parse_mpls;
        }
    }

    // 解析IPv4头部
    state parse_ipv4 {
        // 提取IPv4头部各字段
        packet.extract(hdr.ipv4);
        // 解析结束
        transition accept;
    }
}

检查校验和

1
2
3
control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
    apply { }
}

入队列侧处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    action drop() {
        mark_to_drop(standard_metadata);
    }
    
    // FEC_tbl匹配动作表相关
    // mpls_tbl匹配动作表相关
    
    apply {
        // 如果存在IPv4头部
        if(hdr.ipv4.isValid()){
            // 查询FEC_tbl匹配动作表,直接转发纯IP包还是为其封装MPLS标签
            FEC_tbl.apply();
        }
        // 如果存在MPLS标签
        if(hdr.mpls[0].isValid()){
            // 查询mpls_tbl匹配动作表,根据MPLS标签进行转发
            mpls_tbl.apply();
        }
    }
}
 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// FEC_tbl匹配动作表相关

// 针对纯IPv4数据包执行转发动作
action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
    // 修改以太网头的源MAC为原目的MAC
    hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
    // 修改以太网头的目的MAC为传入参数dstAddr
    hdr.ethernet.dstAddr = dstAddr;

    // 设置数据包的出端口为传入参数port
    standard_metadata.egress_spec = port;
    // 生存时间减1
    hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
}

// 给IPv4包封装一个MPLS标签
action mpls_ingress_1_hop(label_t label_1) {
    // 设置以太网类型字段为MPLS 
    hdr.ethernet.etherType = TYPE_MPLS;
	
    // 向hdr.mpls标签栈前端插入一个标签
    hdr.mpls.push_front(1);
    // 设置该标签有效
    hdr.mpls[0].setValid();
    // 填写标签号
    hdr.mpls[0].label = label_1;
    // 设置TTL值
    hdr.mpls[0].ttl = hdr.ipv4.ttl - 1;
    // 只有最底层的MPLS标签会标记为1
    hdr.mpls[0].s = 1;
}

// 给IPv4包封装多个MPLS标签
action mpls_ingress_4_hop(label_t label_1, label_t label_2, label_t label_3, label_t label_4) {
    hdr.ethernet.etherType = TYPE_MPLS;

    hdr.mpls.push_front(1);
    hdr.mpls[0].setValid();
    hdr.mpls[0].label = label_1;
    hdr.mpls[0].ttl = hdr.ipv4.ttl - 1;
    hdr.mpls[0].s = 1;

    hdr.mpls.push_front(1);
    hdr.mpls[0].setValid();
    hdr.mpls[0].label = label_2;
    hdr.mpls[0].ttl = hdr.ipv4.ttl - 1;
    hdr.mpls[0].s = 0;

    hdr.mpls.push_front(1);
    hdr.mpls[0].setValid();
    hdr.mpls[0].label = label_3;
    hdr.mpls[0].ttl = hdr.ipv4.ttl - 1;
    hdr.mpls[0].s = 0;

    hdr.mpls.push_front(1);
    hdr.mpls[0].setValid();
    hdr.mpls[0].label = label_4;
    hdr.mpls[0].ttl = hdr.ipv4.ttl - 1;
    hdr.mpls[0].s = 0;
}

// 匹配表FEC_tbl
// 根据目的地址匹配,选择是否要直接转发IPv4数据包,是否要封装MPLS,以及封装几个标签
table FEC_tbl {
    // 基于IPv4目的地址进行最长前缀匹配
    key = {
        hdr.ipv4.dstAddr: lpm;
    }
    actions = {
        ipv4_forward;
        mpls_ingress_1_hop;
        mpls_ingress_2_hop;
        mpls_ingress_3_hop;
        mpls_ingress_4_hop;
        mpls_ingress_5_hop;
        NoAction;
    }
    default_action = NoAction();
    size = 256;
}
 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// mpls_tbl匹配动作表相关

// 正常标签转发动作,弹出栈顶标签并转发
action mpls_forward(macAddr_t dstAddr, egressSpec_t port) {
    // 将源MAC地址改成之前的目的MAC地址
    hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
    // 将目的MAC地址改为传入的参数dstAddr
    hdr.ethernet.dstAddr = dstAddr;

    // 设置出口端口为传入的参数port
    standard_metadata.egress_spec = port;
    // TTL递减
    hdr.mpls[1].ttl = hdr.mpls[0].ttl - 1;
    // 从标签栈顶弹出1个标签,即去除最顶层的MPLS标签
    hdr.mpls.pop_front(1);
}

// 用于剥除MPLS标签恢复为正常IPv4数据包
action penultimate(macAddr_t dstAddr, egressSpec_t port) {
    // 将以太网类型改回IPv4
    hdr.ethernet.etherType = TYPE_IPV4;
    
    // 将源MAC地址改成之前的目的MAC地址
    hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
    // 将目的MAC地址改为传入的参数dstAddr
    hdr.ethernet.dstAddr = dstAddr;
    
    // 设置出口端口为传入的参数port
    standard_metadata.egress_spec = port;
     // TTL递减
    hdr.ipv4.ttl = hdr.mpls[0].ttl - 1;
    // 从标签栈顶弹出1个标签,即去除最顶层的MPLS标签
    hdr.mpls.pop_front(1);
}

// 匹配表mpls_tbl
// 用于中间节点根据MPLS标签执行转发
table mpls_tbl {
    key = {
        hdr.mpls[0].label: exact;
        hdr.mpls[0].s: exact;
    }
    actions = {
        mpls_forward;
        penultimate;
        NoAction;
    }
    default_action = NoAction();
    size = CONST_MAX_LABELS;
}

出队列侧处理

1
2
3
4
5
control MyEgress(inout headers hdr,
                 inout metadata meta,
                 inout standard_metadata_t standard_metadata) {
    apply { }
}

计算校验和

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
control MyComputeChecksum(inout headers  hdr, inout metadata meta) {
    apply {
        // 由于修改了IPv4中的字段(包括TTL),因此需要重新计算校验和
        update_checksum(
                hdr.ipv4.isValid(),
                { hdr.ipv4.version,
                hdr.ipv4.ihl,
                hdr.ipv4.diffserv,
                hdr.ipv4.totalLen,
                hdr.ipv4.identification,
                hdr.ipv4.flags,
                hdr.ipv4.fragOffset,
                hdr.ipv4.ttl,
                hdr.ipv4.protocol,
                hdr.ipv4.srcAddr,
                hdr.ipv4.dstAddr },
                hdr.ipv4.hdrChecksum,
                HashAlgorithm.csum16);
    }
}

封装数据包

1
2
3
4
5
6
7
control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.mpls);
        packet.emit(hdr.ipv4);
    }
}

运行测试

配置匹配表

1
2
3
4
5
6
7
8
9
# S1交换机
# 针对发完主机H2的IPv4数据包添加MPLS标签
table_add FEC_tbl mpls_ingress_4_hop 10.7.2.0/24 => 2 3 2 2
# 针对发完主机H3的IPv4数据包添加MPLS标签
table_add FEC_tbl mpls_ingress_4_hop 10.7.3.0/24 => 2 3 2 2
# MPLS标签指示从端口2转发,将到达交换机S2
table_add mpls_tbl mpls_forward 2 0 => 00:00:00:02:01:00 2
# 正常的IPv4数据包转发,从端口1转发进而到达主机H1
table_add FEC_tbl ipv4_forward 10.1.1.0/24 => 00:00:0a:01:01:02 1
1
2
3
# S2交换机
# 剥离顶层MPLS标签,并从端口2转发至下一跳交换机S4
table_add mpls_tbl mpls_forward 2 0 => 00:00:00:04:01:00 2
1
2
3
# S3交换机
# 剥离全部MPLS标签、恢复为IPv4数据包,并从1端口转发至交换机S1
table_add mpls_tbl penultimate 1 1 => 00:00:00:01:03:00 1
1
2
3
4
5
# S4交换机
# 剥离顶层MPLS标签,并从端口3转发至下一跳交换机S5
table_add mpls_tbl mpls_forward 3 0 => 00:00:00:05:01:00 3
# 剥离顶层MPLS标签,并从端口2转发至下一跳交换机S3
table_add mpls_tbl mpls_forward 2 0 => 00:00:00:03:02:00 2
1
2
3
# S5交换机
# 剥离全部MPLS标签、恢复为IPv4数据包,并从2端口转发至交换机S7
table_add mpls_tbl penultimate 2 1 => 00:00:00:07:01:00 2
1
2
3
# S6交换机
# 剥离顶层MPLS标签,并从端口1转发至下一跳交换机S4
table_add mpls_tbl mpls_forward 1 0 => 00:00:00:04:04:00 1
1
2
3
4
5
6
7
8
9
# S7交换机
# 针对发完主机H1的IPv4数据包添加MPLS标签
table_add FEC_tbl mpls_ingress_4_hop 10.1.1.0/24 => 1 2 1 2
# MPLS标签指示从端口2转发,将到达交换机S6
table_add mpls_tbl mpls_forward 2 0 => 00:00:00:06:02:00 2
# 正常的IPv4数据包转发,从端口3转发进而到达主机H2
table_add FEC_tbl ipv4_forward 10.7.2.0/24 => 00:00:0a:07:02:02 3
# 正常的IPv4数据包转发,从端口4转发进而到达主机H3
table_add FEC_tbl ipv4_forward 10.7.3.0/24 => 00:00:0a:07:03:02 4

运行测试

Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计