P4学习-3:P4 basic tunnel实验

实验目标

  1. 在本练习中,我们将向IP添加对基本隧道协议的支持

    你在上一个作业中完成的路由器。基本交换机根据目的IP地址进行转发。您的工作是定义一个新的头类型来封装IP包并修改交换代码,这样它就可以使用一个新的隧道头来决定目的端口。

    新的头类型将包含协议ID,它指示被封装的包的类型,以及用于路由的目的地ID。

  2. P4程序定义了一个包处理管道,但是每个表中的规则是由控制平面插入的。当一个规则匹配一个包时,它的操作将被控制平面作为规则的一部分提供的参数调用。

    对于本练习,我们已经添加了必要的静态控制平面条目。作为启动Mininet实例的一部分,make run命令将在每个交换机的表中安装包处理规则。这些是在sX-runtime中定义的json files,其中X对应开关号。

    因为控制平面试图访问myTunnel_exact表,而这个表还不存在,所以make run命令不能与启动器代码一起工作。

    PS:这里使用P4Runtime来安装规则,文件内容在sX-runtime.json里面

  3. 拓扑如下:

    pod-topo

  4. basic_tunnel.p4文件包含了一个基本的IP路由器的实现,完整的实现版本将能够转发基于自定义封装头的内容,以及如果封装头在数据包中不存在,那么将执行正常的IP转发。工作有以下几个部分:

    1. NOTE:添加了一个新的头类型称作myTunnel_t ,包含了proto_id and dst_id
    2. NOTE:myTunnel_t 已经加入了myTunnel_t header
    3. TODO:更新解析器,根据以太网头中的etherType字段提取myTunnel头或ipv4头。myTunnel报头对应的etherType是’ 0x1212 ‘。如果proto_id == TYPE_IPV4 '即0x0800),解析器还应该在myTunnel 头之后提取ipv4头。
    4. TODO: 定义了一个新的action称作 myTunnel_forward ,它设置出口端口(即standard_metadata总线的egress_spec字段)到控制平面提供的端口号。
    5. TODO:定义一个名为myTunnel_exact的新表,它对myTunnel报头的dst_id字段执行精确匹配。如果表中有匹配项,该表应该调用myTunnel_forward操作,否则它应该调用drop操作。
    6. TODO:如果myTunnel报头有效,更新MyIngress控制块中的apply语句,以应用新定义的myTunnel_exact表。否则,如果ipv4报头有效,则调用ipv4_lpm表。
    7. 更新deparser以发出’ ethernet ‘,然后是’ myTunnel ‘,然后是’ ipv4 ‘头。请记住,deparser只会在消息头有效时发出消息头。头的隐式有效性位由解析器在提取时设置。这里不需要检查头的有效性。
    8. 为新定义的表添加静态规则,以便交换机能够正确转发’ dst_id ‘的每个可能值。请参阅下面的图,了解拓扑的端口配置以及我们将如何为主机分配id。在此步骤中,您需要将转发规则添加到“sX-runtime”中。json文件。

代码部分

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
44
45
46
47
48
49
50
51
52
53
/* -*- P4_16 -*- */
#include <core.p4>
#include <v1model.p4>

// NOTE: new type added here
const bit<16> TYPE_MYTUNNEL = 0x1212;
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 {
macAddr_t dstAddr;
macAddr_t srcAddr;
bit<16> etherType;
}

// NOTE: added new header type
header myTunnel_t {//新的header type
bit<16> proto_id; //包含了proto_id以及dst_id
bit<16> dst_id;
}

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 {
/* empty */
}

// NOTE: Added new header type to headers struct
struct headers {
ethernet_t ethernet;
myTunnel_t myTunnel; //新添加的字段
ipv4_t ipv4;
}
Parser部分:
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
/*************************************************************************
*********************** P A R S E R ***********************************
*************************************************************************/

// TODO: Update the parser to parse the myTunnel header as well
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {

state start {
transition parse_ethernet;
}

state parse_ethernet {
packet.extract(hdr.ethernet);
transition select(hdr.ethernet.etherType) {
TYPE_IPV4 : parse_ipv4;
default : accept;
}
}

state parse_ipv4 {
packet.extract(hdr.ipv4);
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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*************************************************************************
*********************** P A R S E R ***********************************
*************************************************************************/

// TODO: Update the parser to parse the myTunnel header as well
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {

state start {
transition parse_ethernet;
}

state parse_ethernet {
packet.extract(hdr.ethernet);
transition select(hdr.ethernet.etherType) {
TYPE_IPV4 : parse_ipv4;
TYPE_MYTUNNEL : parse_mytunnel;
default : accept;
}
}

state parse_ipv4 {
packet.extract(hdr.ipv4);
transition accept;
}
state parse_mytunnel{
packet.extract(hdr.myTunnel);
transition select(hdr.myTunnel.proto_id){ //满足需求:如果`proto_id ` == ` TYPE_IPV4 '`即0x0800),解析器还应该在`myTunnel 头`之后提取`ipv4 `头。
TYPE_IPV4 : parse_ipv4;
default : accept;
}
transition accept;

}


}
Ingress部分(以及checksum部分):
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

/*************************************************************************
************ 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);
}

action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
standard_metadata.egress_spec = port;
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = dstAddr;
hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
}

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

// TODO: declare a new action: myTunnel_forward(egressSpec_t port)


// TODO: declare a new table: myTunnel_exact
// TODO: also remember to add table entries!


apply {
// TODO: Update control flow
if (hdr.ipv4.isValid()) {
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
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

/*************************************************************************
************ 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);
}

action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
standard_metadata.egress_spec = port;
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = dstAddr;
hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
}

table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm;//最长前缀匹配
}
actions = {
ipv4_forward;
drop;
NoAction;
}
size = 1024;
default_action = drop();
}

// TODO: declare a new action: myTunnel_forward(egressSpec_t port)
action myTunnel_ford(egressSpec_t port){
standard_metadata.egress_spec = port;//设置端口号
}

// TODO: declare a new table: myTunnel_exact
table myTunnel_exact{
key={
hdr.myTunnel.dst_id: exact;//精准匹配
}
actions={
myTunnel_ford;
drop;//如果表中有匹配项,该表应该调用` myTunnel_forward `操作,否则它应该调用` drop `操作。
}
size = 1024;
default_action = drop();
}
// TODO: also remember to add table entries!


apply {
// TODO: Update control flow
if (hdr.ipv4.isValid()) {
ipv4_lpm.apply();
}
if(hdr.myTunnel.isValid()){//模仿ipv4写的
myTunnel_exact.apply();
}
}
}

PS: match_kind: 这是match_action table里面的一种类型,比如

1
2
3
key={
hdr.myTunnel.dst_id: exact;//精准匹配
}

里面的exact就是一种match_kind,p4c/p4include/core.p4有三种:

1
2
3
4
5
6
7
8
match_kind {
/// Match bits exactly.精准匹配
exact,
/// Ternary match, using a mask. 把值和一个 mask 比较,比如 0x01020304 符合 mask 0x0F0F0F0F
ternary,
/// Longest-prefix match.最长前缀匹配
lpm
}

p4c/p4include/v1model.p4里面有一下几种:

1
2
3
4
5
6
7
8
match_kind {
//检查是否值在一个范围里,比如取 0x01020304 - 0x010203FF 之间的值
range,
// Either an exact match, or a wildcard (matching any value).精准匹配或者通配
optional,
// Used for implementing dynamic_action_selection 用于实现dynamic_action_selection
selector
}

因为在include上述两个文件的情况下一共有六种

Egress部分(以及checksum部分):
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
/*************************************************************************
**************** 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 { }
}

/*************************************************************************
************* 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
10
11
/*************************************************************************
*********************** D E P A R S E R *******************************
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet);
// TODO: emit myTunnel header as well
packet.emit(hdr.ipv4);
}
}

解答:

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

control MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet);
// TODO: emit myTunnel header as well
packet.emit(hdr.myTunnel);
packet.emit(hdr.ipv4);

}
}

实例化部分:

1
2
3
4
5
6
7
8
9
10
11
12
/*************************************************************************
*********************** S W I T C H *******************************
*************************************************************************/

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

实验结果

  1. make run

    这会进行以下几步:

    1. 编译basic_tunnel.p4
    2. 启动一个Mininet实例,其中三个交换机(s1s2s3)配置在一个三角形中,每个交换机连接到一个主机(h1h2h3)。
    3. 这些主机的ip地址设置为10.0.1.1, 10.0.2.2, and 10.0.3.3.

    然后进入mininet界面

  2. 使用mininet的xterm功能:

    1
    mininet> xterm h1 h2
  3. 在h2的界面输入 :

    1
    ./receive.py

    在h1的界面输入:

    1
    ./send.py 10.0.2.2 "P4 is cool"

    这是不经过my_tunnel的测试;

    如果您检查接收到的数据包,您应该会看到它由一个以太网报头、一个IP报头、一个TCP报头和消息组成。如果您更改了目的IP地址(例如试图发送到’ 10.0.3.3 ‘),则该消息不应该被’ h2 ‘接收,而将被’ h3 ‘接收。

    结果如下图:

    image-20210228215634236

    发给10.0.3.3之后:

    h1有相似的显示,h2没有变化

  4. 下面测试my_tunnel

    在h1的界面输入:

    1
    ./send.py 10.0.2.2 "P4 is cool" --dst_id 2

结果如下图:

image-20210228215915163

数据包在h2处接收。它由一个以太网报头、一个隧道报头、一个IP报头、一个TCP报头和消息组成。隧道报头就是那个###[MyTunnel]在IP报头上面;

  1. 在h1的界面输入:

    1
    ./send.py 10.0.3.3 "P4 is cool" --dst_id 2

    结果如下:

    image-20210228220230000

    即使IP地址是h3的地址,数据包也应该在h2处接收。这是因为当MyTunnel头在包中时,交换机不再使用IP头进行路由。

    一开始我也不理解上面这句话,后来我看了s1-runtime文件,这个文件规定了交换机的静态规则,将这个文件和basic里面的同名文件比较会发现里面多了:

    image-20210228221250171

    这部分内容结合刚刚./receive代码里面的—dst 2后缀可以得知刚刚那个包在进入交换机s1之后会因为p4的解包触发走myTunnel_forward的规则然后发给S1的2端口,结合拓扑图可以看出确实是发给h2的

解析其他内容

my_tunnel.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

from scapy.all import *
import sys, os

TYPE_MYTUNNEL = 0x1212
TYPE_IPV4 = 0x0800

class MyTunnel(Packet):
name = "MyTunnel"
fields_desc = [
ShortField("pid", 0),
ShortField("dst_id", 0)
]
def mysummary(self):
return self.sprintf("pid=%pid%, dst_id=%dst_id%")


bind_layers(Ether, MyTunnel, type=TYPE_MYTUNNEL)
bind_layers(MyTunnel, IP, pid=TYPE_IPV4)

因为原来的scapy只支持ipv4,不支持我们的myTunnel协议,因此需要重新定义,上面的文件就重新定义了MyTunnel;

每一个协议层都是Packet类的子类。协议层背后所有逻辑的操作都是被Packet类和继承的类所处理的。一个简单的协议层是被一系列的字段构成,他们关联在一起组成了协议层,解析时拆分成一个一个的字符串。这些字段都包含在名为fields_desc的属性中。每一个字段都是一个field类的实例:

上面MyTunnel的协议层有两个字段分别是pid和dst_id,默认值都是0;

UDP的协议层定义如下:

1
2
3
4
5
6
class UDP(Packet):
name = "UDP"
fields_desc = [ ShortEnumField("sport", 53, UDP_SERVICES),
ShortEnumField("dport", 53, UDP_SERVICES),
ShortField("len", None),
XShortField("chksum", None), ]

最后两句是绑定协议层;

Scapy在解析协议层时一个很酷的特性是他试图猜测下一层协议是什么。连接两个协议层官方的方法是bind_layers():

send.py
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
#!/usr/bin/env python
import argparse
import sys
import socket
import random
import struct
import argparse

from scapy.all import sendp, send, get_if_list, get_if_hwaddr, hexdump
from scapy.all import Packet
from scapy.all import Ether, IP, UDP, TCP
from myTunnel_header import MyTunnel

def get_if():
ifs=get_if_list() # # type: () -> List[str] """Return a list of interface names""",返回接口(网卡)名字
iface=None # "h1-eth0"
for i in get_if_list():
if "eth0" in i:
iface=i
break;
if not iface:
print "Cannot find eth0 interface"
exit(1)
return iface

def main():
parser = argparse.ArgumentParser()#argsparse是python的命令行解析的标准模块,相当于就是解析./send.py后面的参数
parser.add_argument('ip_addr', type=str, help="The destination IP address to use")#type是要传入的参数的数据类型 help是该参数的提示信息,使用python send.py -h可以看到
parser.add_argument('message', type=str, help="The message to include in packet")
parser.add_argument('--dst_id', type=int, default=None, help='The myTunnel dst_id to use, if unspecified then myTunnel header will not be included in packet')
args = parser.parse_args()##获得传入的参数

addr = socket.gethostbyname(args.ip_addr)## 获取ip_addr的主机名
dst_id = args.dst_id#获得dst_id
iface = get_if()#获取网卡的名字

if (dst_id is not None):#包装
print "sending on interface {} to dst_id {}".format(iface, str(dst_id))
pkt = Ether(src=get_if_hwaddr(iface), dst='ff:ff:ff:ff:ff:ff')
pkt = pkt / MyTunnel(dst_id=dst_id) / IP(dst=addr) / args.message
else:
print "sending on interface {} to IP addr {}".format(iface, str(addr))
pkt = Ether(src=get_if_hwaddr(iface), dst='ff:ff:ff:ff:ff:ff')
pkt = pkt / IP(dst=addr) / TCP(dport=1234, sport=random.randint(49152,65535)) / args.message

pkt.show2()
# hexdump(pkt)
# print "len(pkt) = ", len(pkt)
sendp(pkt, iface=iface, verbose=False)


if __name__ == '__main__':
main()

大概流程就是,先分析参数,然后包装成Ether的格式然后用scapy发包;

receive.py
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
#!/usr/bin/env python
import sys
import struct
import os

from scapy.all import sniff, sendp, hexdump, get_if_list, get_if_hwaddr
from scapy.all import Packet, IPOption
from scapy.all import ShortField, IntField, LongField, BitField, FieldListField, FieldLenField
from scapy.all import IP, TCP, UDP, Raw
from scapy.layers.inet import _IPOption_HDR
from myTunnel_header import MyTunnel

def get_if(): # 获得网卡接口
ifs=get_if_list()
iface=None
for i in get_if_list():
if "eth0" in i:
iface=i
break;
if not iface:
print "Cannot find eth0 interface"
exit(1)
return iface

def handle_pkt(pkt):
if MyTunnel in pkt or (TCP in pkt and pkt[TCP].dport == 1234):
print "got a packet"
pkt.show2()
# hexdump(pkt)
# print "len(pkt) = ", len(pkt)
sys.stdout.flush()


def main():
ifaces = filter(lambda i: 'eth' in i, os.listdir('/sys/class/net/'))
iface = ifaces[0]
print "sniffing on %s" % iface
sys.stdout.flush()
sniff(iface = iface,
prn = lambda x: handle_pkt(x))

if __name__ == '__main__':
main()

首先获得网卡,发出“sniffing on….”信息,并且立刻打出来,然后把收到的包里面的东西立刻打印出来

原文部分

Introduction

In this exercise, we will add support for a basic tunneling protocol to the IP
router that you completed in the previous assignment. The basic switch
forwards based on the destination IP address. Your jobs is to define a new
header type to encapsulate the IP packet and modify the switch code, so that it
instead decides the destination port using a new tunnel header.

The new header type will contain a protocol ID, which indicates the type of
packet being encapsulated, along with a destination ID to be used for routing.

Spoiler alert: There is a reference solution in the solution
sub-directory. Feel free to compare your implementation to the reference.

The starter code for this assignment is in a file called basic_tunnel.p4 and
is simply the solution to the IP router from the previous exercise.

A note about the control plane

A P4 program defines a packet-processing pipeline, but the rules within each
table are inserted by the control plane. When a rule matches a packet, its
action is invoked with parameters supplied by the control plane as part of the
rule.

For this exercise, we have already added the necessary static control plane
entries. As part of bringing up the Mininet instance, the make run command
will install packet-processing rules in the tables of each switch. These are
defined in the sX-runtime.json files, where X corresponds to the switch
number.

Since the control plane tries to access the myTunnel_exact table, and that
table does not yet exist, the make run command will not work with the starter
code.

Important: We use P4Runtime to install the control plane rules. The content
of files sX-runtime.json refer to specific names of tables, keys, and
actions, as defined in the P4Info file produced by the compiler (look for the
file build/basic.p4info after executing make run). Any changes in the P4
program that add or rename tables, keys, or actions will need to be reflected
in these sX-runtime.json files.

参考博客: