例程拓扑

该例程实现了简单的双端口交换机,使得两个主机能够进行通信,即类似于中继器的功能。
静态配置方案
拓扑描述文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
"p4_src": "repeater.p4",
"cli": true,
"pcap_dump": true,
"enable_log": true,
"topology": {
"assignment_strategy": "l2",
"links": [["h1", "s1"], ["h2", "s1"],
"hosts": {
"h1": {},
"h2": {}
},
"switches": {
"s1": {
}
}
}
}
|
相较于“P4示例程序-1 数据包反射器”中的拓扑配置文件,只需额外配置主机h1、h2,并添加主机与交换机之间的链路连接。Python脚本的方式同理,在此省略。
交换机程序
由于无需对数据包进行解析、更改,因此无需定义数据包格式、解器器、封装器、校验值的校验与计算,只需对入队列侧的处理逻辑进行编程,从P4的标准元数据中提取数据包的入端口,并填写出端口。
1
2
3
4
5
6
7
8
9
|
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 无线对数据包进行解析,因此直接accept结束即可
state start{
transition accept;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply {
// 如果入端口是1号端口 => 从2号端口转发
if (standard_metadata.ingress_port == 1){
standard_metadata.egress_spec = 2;
}
// 如果入端口是2号端口 => 从1号端口转发
else if (standard_metadata.ingress_port == 2){
standard_metadata.egress_spec = 1;
}
}
}
|
运行测试
在mininet中进行Ping测试与带宽测试:
1
2
|
mininet > pingall
mininet > iperf
|

在新窗口使用以下命令进入host2的命令行,并运行接收数据包的Python脚本:
1
2
|
mx h2
python receive.py
|
在新窗口使用以下命令进入host1的命令行,并运行发送数据包的Python脚本:
1
2
|
mx h1
python send.py 10.0.0.2 "HelloWorld"
|

基于表的配置方案
交换机程序
1
2
3
4
5
6
7
8
9
|
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 无线对数据包进行解析,因此直接accept结束即可
state start{
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
|
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 定义名为forward的动作
// 注意这里的参数egress_port为从匹配表中查到的值,因此无需表明方向
action forward(bit<9> egress_port){
// 将匹配中查找到的值设置出端口号
standard_metadata.egress_spec = egress_port;
}
// 定义名为repeater的匹配表
table repeater {
// 定义匹配表的键为P4标准元数据中的ingress_port
// exact指示为精确匹配,其余的匹配方式包括最长前缀字段匹配lpm、三元匹配ternary
key = {
standard_metadata.ingress_port: exact;
}
// 设置动作集,可能的动作包括forward与NoAction
// NoAction是P4内置的动作
actions = {
forward;
NoAction;
}
// 匹配表的大小为2
// 一个是从Host1发向Host2的规则,一个是Host2发向Host1的规则
size = 2;
// 设置匹配表的默认动作是NoAction
default_action = NoAction;
}
apply {
// 查询repeater匹配表
repeater.apply();
}
}
|
基于Thrift的数据平面配置
Thrift客户端
P4-Utils包含了Thrift框架,可以通过Thrift客户端(控制平面)向Thrift服务端(运行BMv2交换机的数据平面)发送指令,进而修改数据平面的表项。在执行p4run命令启动网络时,在输出的信息中可以查看到Thrift服务端的端口号为9090。

因此新建命令行,使用以下命令启动Thrift客户端simple_switch_CLI、并连接到服务端,可以通过?查询支持的命令,并通过help查询命令的具体用法。
1
2
3
|
simple_switch_CLI --thrift-port 9090 --thrift-ip 127.0.0.1
RuntimeCmd: ?
RuntimeCmd: help table_add
|


根据提示,使用以下命令:在repeater表中添加表项,其查询键为1、执行的动作为forward、为执行动作forward传的参为2。
1
2
|
RuntimeCmd: table_add repeater forward 1 => 2
RuntimeCmd: table_add repeater forward 2 => 1
|

文本输入
创建文件s1-commands.txt,将上述在命令行键入的命令可以直接放入文本文件。
1
2
|
table_add repeater forward 1 => 2
table_add repeater forward 2 => 1
|
为将配置匹配表的文本送入,需要在拓扑描述文件中为S1交换机使用cli_input字段引用文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{
"p4_src": "repeater_with_table.p4",
"cli": true,
"pcap_dump": true,
"enable_log": true,
"topology": {
"assignment_strategy": "l2",
"links": [["h1", "s1"], ["h2", "s1"],
"hosts": {
"h1": {},
"h2": {}
},
"switches": {
"s1": {
"cli_input": "s1-commands.txt"
}
}
}
}
|
类似的若使用Python脚本作为拓扑描述文件,需要使用setP4CliInput函数为交换机S1指定文件:
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
|
from p4utils.mininetlib.network_API import NetworkAPI
net = NetworkAPI()
# Network general options
net.setLogLevel('info')
net.enableCli()
# Network definition
net.addP4Switch('s1')
net.setP4Source('s1','repeater_with_table.p4')
net.setP4CliInput('s1', 's1-commands.txt')
net.addHost('h1')
net.addHost('h2')
net.addLink('s1', 'h1')
net.addLink('s1', 'h2')
# Assignment strategy
net.l2()
# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
# Start network
net.startNetwork()
|
基于P4Runtime
P4Runtime是P4社区定义的一个控制平面与数据平面之间的标准化接口。
控制平面
可以通过Python脚本连接P4Runtime服务端,并配置匹配表。
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
|
from p4utils.utils.helper import load_topo
from p4utils.utils.sswitch_p4runtime_API import SimpleSwitchP4RuntimeAPI
# 加载网络拓扑配置文件,获取拓扑
# topology.json配置文件在网络启动时自动生成
topo = load_topo('topology.json')
# 此处个人认为命名为switches更易于理解
controllers = {}
# 遍历拓扑中所有支持P4Runtime的交换机
for switch, data in topo.get_p4rtswitches().items():
# 为每一个支持P4Runtime的交换机实例化一个SimpleSwitchP4RuntimeAPI对象,用于建立连接
controllers[switch] = SimpleSwitchP4RuntimeAPI(
data['device_id'], # 交换机的设备ID,从0开始编号
data['grpc_port'], # 交换机监听的gRPC端口
p4rt_path=data['p4rt_path'], # P4Runtime协议文件路径,描述P4程序结构
json_path=data['json_path'] # BMv2的JSON配置文件路径,数据平面行为定义
)
# 获取名为s1的交换机控制对象
controller = controllers['s1']
# 清空该交换机repeater表中的全部表项
controller.table_clear('repeater')
# 向repeater表添加一条规则:匹配ingress_port=1时,执行forward动作,参数egress_port=2
controller.table_add('repeater', 'forward', ['1'], ['2'])
# 向repeater表添加另一条规则:匹配ingress_port=2时,执行forward动作,参数egress_port=1
controller.table_add('repeater', 'forward', ['2'], ['1'])
|
拓扑描述文件:Python脚本
使用Python脚本是一种简单的方式,主要变动在于:
- setCompiler(p4rt=True):指示编译P4时生成P4Runtime所需的JSON与P4Info;
- execScript(‘python controller.py’, reboot=True):指示在网络启动后自动运行controller.py脚本(即控制平面配置匹配表的脚本),reboot为True指示网络重启后也重新运行脚本;
- addP4RuntimeSwitch(‘s1’):添加一个支持P4Runtime的交换机,命名为S1。
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
|
from p4utils.mininetlib.network_API import NetworkAPI
net = NetworkAPI()
# Network general options
net.setLogLevel('info')
net.setCompiler(p4rt=True)
net.execScript('python controller.py', reboot=True)
net.enableCli()
# Network definition
net.addP4RuntimeSwitch('s1')
net.setP4Source('s1','repeater.p4')
net.addHost('h1')
net.addHost('h2')
net.addLink('s1', 'h1')
net.addLink('s1', 'h2')
# Assignment strategy
net.l2()
# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
# Start network
net.startNetwork()
|
拓扑描述文件:JSON格式
使用JSON格式定义拓扑文件稍微复杂些:
- switch_node:指示创建交换机时,使用p4utils.mininetlib.node模块(Python模块)中的P4RuntimeSwitch类,该类支持P4Runtime;
- compiler_module:指示编译P4时生成P4Runtime所需的JSON与P4Info;
- exec_scripts:指示在网络启动后自动运行controller.py脚本(即控制平面配置匹配表的脚本),reboot为True指示网络重启后也重新运行脚本。
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
|
{
"p4_src": "repeater.p4",
"cli": true,
"pcap_dump": true,
"enable_log": true,
"switch_node":
{
"module_name": "p4utils.mininetlib.node",
"object_name": "P4RuntimeSwitch"
},
"compiler_module":
{
"options":
{
"p4rt": true
}
},
"exec_scripts":
[
{
"cmd": "python controller.py",
"reboot_run": true
}
],
"topology": {
"assignment_strategy": "l2",
"links": [["h1", "s1"], ["h2", "s1"]],
"hosts": {
"h1": {
},
"h2": {
}
},
"switches": {
"s1": {
}
}
}
}
|
补充:多键多动作参数的匹配表
匹配表可以具有多个键,并为动作提供多个参数,例如:
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
|
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 定义动作,该动作包含两个参数
// egress_port用于确定数据包的出端口
// priority用来打标签,标签打在数据包的etherType字段
action forward_and_tag(bit<9> egress_port, bit<3> priority) {
standard_metadata.egress_spec = egress_port;
hdr.ethernet.etherType = priority;
}
// 匹配表
table mac_table {
key = {
standard_metadata.ingress_port : exact; // 第一个键:数据包的入端口
hdr.ethernet.dstAddr : exact; // 第二个键:目的MAC地址
}
actions = {
forward_and_tag;
NoAction;
}
size = 1024;
default_action = NoAction;
}
apply {
mac_table.apply();
}
}
|
此时配置匹配表的文本文件可以写入:
1
|
table_add mac_table forward_and_tag 1 00:aa:bb:cc:dd:ee => 2 5
|
附录 端侧收发函数
数据包发送脚本
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
|
#!/usr/bin/env python
import sys
import socket
import random
from subprocess import Popen, PIPE
import re
from scapy.all import sendp, get_if_list, get_if_hwaddr
from scapy.all import Ether, IP, UDP, TCP
def get_if():
ifs=get_if_list()
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 get_dst_mac(ip):
try:
pid = Popen(["arp", "-n", ip], stdout=PIPE)
s = str(pid.communicate()[0])
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})", s).groups()[0]
return mac
except:
return None
def main():
if len(sys.argv)<3:
print('pass 2 arguments: <destination> "<message>"')
exit(1)
addr = socket.gethostbyname(sys.argv[1])
iface = get_if()
if len(sys.argv) > 3:
tos = int(sys.argv[3]) % 256
else:
tos = 0
ether_dst = get_dst_mac(addr)
if not ether_dst:
print("Mac address for %s was not found in the ARP table" % addr)
exit(1)
print("Sending on interface %s to %s" % (iface, str(addr)))
pkt = Ether(src=get_if_hwaddr(iface), dst=ether_dst)
pkt = pkt /IP(dst=addr,tos=tos) / sys.argv[2]
sendp(pkt, iface=iface, verbose=False)
if __name__ == '__main__':
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
|
#!/usr/bin/env python
import sys
import os
from scapy.all import sniff, get_if_list, Ether, get_if_hwaddr, IP, Raw
def get_if():
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 isNotOutgoing(my_mac):
my_mac = my_mac
def _isNotOutgoing(pkt):
return pkt[Ether].src != my_mac
return _isNotOutgoing
def handle_pkt(pkt):
print("Packet Received:")
ether = pkt.getlayer(Ether)
ip = pkt.getlayer(IP)
msg = ip.payload
print("###[ Ethernet ]###")
print(" src: {}".format(ether.src))
print(" dst: {}".format(ether.dst))
print("###[ IP ]###")
print(" src: {}".format(ip.src))
print(" dst: {}".format(ip.dst))
print("###[ MESSAGE ]###")
print(" msg: {}".format(str(msg)))
print()
def main():
ifaces = [i for i in os.listdir('/sys/class/net/') if 'eth' in i]
iface = ifaces[0]
print("sniffing on %s" % iface)
sys.stdout.flush()
my_filter = isNotOutgoing(get_if_hwaddr(get_if()))
sniff(filter="ip", iface = iface,
prn = lambda x: handle_pkt(x), lfilter=my_filter)
if __name__ == '__main__':
main()
|