P4学习-2:P4 basic实验

实验目标

  1. 写一个P4程序完成基本的IPV4转发,也就是交换机需要:1.更新源和目的MAC地址 2. 在IP报头中减少 time-to-live(TTL) 3.将数据包转发到适当的端口

    交换机有一个简单的table,控制平面将根据基本的规则更新它。每个规则将一个IP地址映射到下一跳的MAC地址和输出端口。我们已经定义了控制平面规则,所以你只需要实现你的P4程序的数据平面逻辑。

  2. 拓扑如下:

    pod-topo

  3. 实验basic.p4给了一个p4程序,关键部分被TODO代替,这个程序主要由以下部分组成:

    1. Ethernet (ethernet_t) and IPv4 (ipv4_t)两个header type的类型定义

    2. TODO:用于以太网和IPv4的Parser,它通过解析数据包填充了上述两个header;

    3. 一个丢包的动作,用了mark_to_drop()

    4. TODO:一个动作(ipv4_forward):

      1. 设置下一跳的出口端口。
      2. 更新以太网目的地址为下一跳地址。
      3. 将以太网源地址更新为交换机地址。
      4. TTL衰减。
    5. TODO: 一个control:

      1. 定义一个表,该表将读取IPv4目的地址,并调用drop 或者ipv4_forward
      2. 应用这个表的一个 apply block
    6. 选择字段插入出数据包的顺序的Deparser。

    7. 实例化部分

      通常,包还需要校验和验证和重新计算控件的实例。这些对于本教程来说不是必需的,它们将被空控件的实例化所取代。

代码部分

本部分根据上面对程序的描述一一进行分析:

headers部分:
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
/* -*- P4_16 -*- */ # 声明语言是p4-16
#include <core.p4>
#include <v1model.p4>

const bit<16> TYPE_IPV4 = 0x800;

/*************************************************************************
*********************** H E A D E R S ***********************************
*************************************************************************/

typedef bit<9> egressSpec_t;
typedef bit<48> macAddr_t;
typedef bit<32> ip4Addr_t;

header ethernet_t { # header数据结构相当于c语言里面的 packed struct,它有一个隐藏的参数validity,如果解析正确那么是true,可以通过isValid()获得,比如hdr.ipv4.isValid();
macAddr_t dstAddr;# macAddr_t是typedef bit<48> 的自定义类型
macAddr_t srcAddr;
bit<16> etherType;
}

header ipv4_t {
bit<4> version;#4bit的数据
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数据结构相当于python里面的 dictionary
/* empty */ #这个实验用不到
}

struct headers {
ethernet_t ethernet;
ipv4_t ipv4;
}

其中V1model的结构如下图所示:

image-20210222224801495

总的来说这是一个模板,大致由下图组成:

image-20210222224659076

主要由HEADERS,PARSER,CHECKSUMVERFICATION,INGRESSPROCESSING,EGRESSPROCESSING,CHECKSUM UPDATE,DEPARSER,SWITCH组成;大概实例化的解释如下:

1
2
3
4
5
6
7
8
V1Switch(
MyParser(), // 解析数据包,提取包头
MyVerifyChecksum(), // 校验和验证
MyIngress(), // 输入处理
MyEgress(), // 输出处理
MyComputeChecksum(), // 计算新的校验和
MyDeparser() // 逆解析器
) main;
Parser部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*************************************************************************
*********************** P A R S E R ***********************************
*************************************************************************/

parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {

state start {
/* TODO: add parser logic */
transition accept;#表示接受,也就是程序终止
}
}

解答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*************************************************************************
*********************** P A R S E R ***********************************
*************************************************************************/

parser MyParser(packet_in packet,
out headers hdr, #out相当于输出的数据,然后它的type是headers
inout metadata meta, #inout同时作为输入和输出值,类似 c++ 里面的引用
inout standard_metadata_t standard_metadata) {

state start {
transition parse_ethernet;#转移到解析以太包头阶段
}
state parse_ethernet{
packet.extract(hdr.ethernet); #把packet提取到hdr的ethernet里面,这里的过程可以理解为根据ethernet的长度截取一段数据
transition select(hdr.ethernet.etherType){#根据etherType的值选择进入的状态
TYPE_IPV4: parse_ipv4;#是IPV4包,进入解析ipv4的状态
default: accept;
}
}
state parse_ipv4{
packet.extract(hdr.ipv4); #接着解析ipv4部分,这里可以理解为指针又往前移动了
transition accept;
}
}

Ingress部分
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

/*************************************************************************
************ C H E C K S U M V E R I F I C A T I O N *************
*************************************************************************/

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


/*************************************************************************
************** I N G R E S S P R O C E S S I N G *******************
*************************************************************************/

control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() {
mark_to_drop(standard_metadata);#内置函数,将当前数据包标记为即将丢弃的数据包,standard_metadata的解释见PS部分
}

action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
/* TODO: fill out code in action body */
}

table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm;
}
actions = {
ipv4_forward;
drop;
NoAction;
}
size = 1024;
default_action = NoAction();
}

apply {
/* TODO: fix ingress control logic
* - ipv4_lpm should be applied only when IPv4 header is valid
*/
ipv4_lpm.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
/*************************************************************************
************** I N G R E S S P R O C E S S I N G *******************
*************************************************************************/

control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() {
mark_to_drop(standard_metadata);#内置函数,将当前数据包标记为即将丢弃的数据包,standard_metadata的解释见PS部分
}

action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
standard_metadata.egress_spec=port; #egress_spec可以在输入代码中指定一个值来控制数据包将去哪个输出端口
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr; #原数据包的源地址改为目的地址
hdr.ethernet.dstAddr = dstAddr; #目的地址改为控制面传入的新的地址
hdr.ipv4.ttl = hdr.ipv4.ttl - 1; #ttl要减去1

}

table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm;
}
actions = {
ipv4_forward;
drop;
NoAction;
}
size = 1024;
default_action = NoAction();
}

apply {
if (hdr.ipv4.isValid()) {# 隐藏的参数,判断解析是否成功
ipv4_lpm.apply();
}
}
}

PS:standard_metadata是v1model.p4里面定义的数据结构,在simple_switch的README里面进行了详细的解释,simple_switch是p4语言的一种架构,它大概的定义如下,其中v1m表示v1model,p4-16的一种模型,sm14是p4-14里面定义的模型

1
2
3
4
5
6
7
8
9
ingress_port (sm14, v1m) - For new packets, the number of the ingress port on which the packet arrived to the device. Read only.
packet_length (sm14, v1m) - For new packets from a port, or recirculated packets, the length of the packet in bytes. For cloned or resubmitted packets, you may need to include this in a list of fields to preserve, otherwise its value will become 0.
egress_spec (sm14, v1m) - Can be assigned a value in ingress code to control which output port a packet will go to. The P4_14 primitive drop, and the v1model primitive action mark_to_drop, have the side effect of assigning an implementation specific value DROP_PORT to this field (511 decimal for simple_switch by default, but can be changed through the --drop-port target-specific command-line option), such that if egress_spec has that value at the end of ingress processing, the packet will be dropped and not stored in the packet buffer, nor sent to egress processing. See the "after-ingress pseudocode" for relative priority of this vs. other possible packet operations at end of ingress. If your P4 program assigns a value of DROP_PORT to egress_spec, it will still behave according to the "after-ingress pseudocode", even if you never call mark_to_drop (P4_16) or drop (P4_14).
egress_port (sm14, v1m) - Only intended to be accessed during egress processing, read only. The output port this packet is destined to.
egress_instance (sm14) - Renamed egress_rid in simple_switch. See egress_rid below.
instance_type (sm14, v1m) - Contains a value that can be read by your P4 code. In ingress code, the value can be used to distinguish whether the packet is newly arrived from a port (NORMAL), it was the result of a resubmit primitive action (RESUBMIT), or it was the result of a recirculate primitive action (RECIRC). In egress processing, can be used to determine whether the packet was produced as the result of an ingress-to-egress clone primitive action (INGRESS_CLONE), egress-to-egress clone primitive action (EGRESS_CLONE), multicast replication specified during ingress processing (REPLICATION), or none of those, so a normal unicast packet from ingress (NORMAL). Until such time as similar constants are pre-defined for you, you may copy this list of constants into your code.
parser_status (sm14) or parser_error (v1m) - parser_status is the name in the P4_14 language specification. It has been renamed to parser_error in v1model. The value 0 (sm14) or error.NoError (P4_16 + v1model) means no error. Otherwise, the value indicates what error occurred during parsing.
parser_error_location (sm14) - Not present in v1model.p4, and not implemented in simple_switch.
checksum_error (v1m) - Read only. 1 if a call to the verify_checksum primitive action finds a checksum error, otherwise 0. Calls to verify_checksum should be in the VerifyChecksum control in v1model, which is executed after the parser and before ingress.
Egress部分
1
2
3
4
5
6
7
8
9
/*************************************************************************
**************** E G R E S S P R O C E S S I N G *******************
*************************************************************************/

control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply { }
}
Checksum 部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*************************************************************************
************* C H E C K S U M C O M P U T A T I O N **************
*************************************************************************/

control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply {
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);
}
}# 内置函数
Deparser部分
1
2
3
4
5
6
7
8
9
/*************************************************************************
*********************** D E P A R S E R *******************************
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
apply {
/* TODO: add deparser logic */
}
}

解答:

1
2
3
4
5
6
7
8
9
10
/*************************************************************************
*********************** D E P A R S E R *******************************
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet);#按照顺序封装,emit的含义是发射
packet.emit(hdr.ipv4);
}
}
实例化部分
1
2
3
4
5
6
7
8
9
10
11
12
13

/*************************************************************************
*********************** S W I T C H *******************************
*************************************************************************/

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

实验结果:

pingall测试:

image-20210223225105957
image-20210223225105957