例程目录文件

- p4app.json或network.py:描述需要创建的拓扑,拓扑由mininet负责创建,既可以使用Python脚本,也可以使用JSON格式
- reflector.p4:P4程序
- send_receive.py:用于发送和接收数据包的Python脚本
拓扑描述文件
传送门:用法 —— P4-Utils1.0文档
JSON格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
"p4_src": "reflector.p4",
"cli": true,
"pcap_dump": true,
"enable_log": true,
"topology": {
"assignment_strategy": "l2",
"links": [["h1", "s1"],
"hosts": {
"h1": {
}
},
"switches": {
"s1": {
}
}
}
}
|
p4_src指出需要编译运行的P4源程序是reflector.p4
pcap_dump指示是否激活交换机端口上的数据包嗅探,嗅探的数据包保存在.pcap文件中
assignment_strategy指示网络运行模式:
- l2表示所有的交换机都在L2链路层工作,所有的hosts都会被放在同一个子网10.0.0.0/16中(IP形如10.0.x.y/16),同时每个host的ARP Table也会被自动填充完成
- l3表示所有交换机位于L3网络层工作,每个接口都属于独立的子网:主机IP为10.x.y.2/24,其相连的交换机端口IP地址为10.x.y.1/24(x为交换机ID,y为主机ID);交换机sw1与交换机sw2连接时,sw1端口的IP地址为20.sw1.sw2.1/24,sw2端口的IP地址为20.sw1.sw2.2/24,sw1为sw1交换机编号,sw2为sw2交换机编号
- mixed表示每个host只能和一个交换机连接,连接在同一个交换机上的host属于1个/24子网中,该交换机作为该host的L3层网络层网关
Python脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
from p4utils.mininetlib.network_API import NetworkAPI
net = NetworkAPI()
# Network general options
net.setLogLevel('info')
# Network definition
net.addP4Switch('s1')
net.setP4Source('s1','reflector.p4')
net.addHost('h1')
net.addLink('s1', 'h1')
# Assignment strategy
net.l2()
# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
net.enableCli()
net.startNetwork()
|
为了构造网络,需要导入相关模块,并构造一个NetworkAPI的对象net,进而依次调用addP4Switch函数添加交换机、setP4Source函数为交换机设置运行的P4程序、addHost函数添加主机、addLink函数添加链路。然后调用l2函数指定网络的运行模式为L2层,随后的enablePcapDumpAll函数、enableLogAll函数、enableCli函数分别与JSON文件中的pcap_dump、enable_log、cli字段对应。最后调用startNetwork函数启动网络。
端侧收发脚本
main函数
main函数每隔0.5秒向目标IP(10.0.0.2)发送数据包,并在指定网卡上启动后台线程实时监听接收数据包,直到手动中断退出。
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
|
#!/usr/bin/env python3
import sys
import socket
import random
import time
from threading import Thread, Event
# Scapy可实现对网络数据包发送、监听、解析等操作
from scapy.all import *
def main():
# 设定目标IP地址为10.0.0.2,用于随后构建待发送的数据包
addr = "10.0.0.2"
# gethostbyname函数尝试将主机名DNS解析为IP地址
# 如果addr是域名,函数则返回解析后的IPv4地址
# 如果addr本身就是合法的IPv4字符串,函数则直接返回该地址
addr = socket.gethostbyname(addr)
# 获取名为eth0的端口
iface = get_if()
# 创建线程,该线程从端口eth0接收数据包
listener = Sniffer(iface)
listener.start()
time.sleep(0.1)
try:
while True:
# 每间隔0.5秒构造、发送一个数据包
send_packet(iface, addr)
time.sleep(0.5)
except KeyboardInterrupt:
# 键盘键入退出事件,关闭Socket接口、结束监听
print("[*] Stop sniffing")
listener.join(2.0)
if listener.isAlive():
listener.socket.close()
if __name__ == '__main__':
main()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def get_if():
# 获取设备上的全部端口列表
ifs = get_if_list()
# 在端口列表中查找目标接口eth0
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
|
发送数据包
1
2
3
4
5
6
7
8
9
10
11
|
def send_packet(iface, addr):
input("Press the return key to send a packet:")
print("Sending on interface %s to %s\n" % (iface, str(addr)))
# 构造数据包的L2层
# 源MAC地址即为eth0端口的MAC地址,可通过get_if_hwaddr函数获得
# 目的MAC地址固定填写为00:01:02:03:04:05
pkt = Ether(src=get_if_hwaddr(iface), dst='00:01:02:03:04:05')
# 构造数据包的L3层,目的IP地址固定填写为设定的10.0.0.2
pkt = pkt / IP(dst=addr)
# 通过端口eth0将构造的数据包发送出去
sendp(pkt, iface=iface, verbose=False)
|
接收数据包
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
|
class Sniffer(Thread):
# Sniffer类用于监听接收到的数据包
def __init__(self, interface="eth0"):
super(Sniffer, self).__init__()
# 要监听的网络端口
self.interface = interface
# 获取监听网络端口的MAC地址
self.my_mac = get_if_hwaddr(interface)
# 设置为守护线程(主线程退出时自动结束)
self.daemon = True
# 后面会保存监听用的网络套接字
self.socket = None
# 线程间通信的事件,用于停止抓包
self.stop_sniffer = Event()
def isNotOutgoing(self, pkt):
# 过滤掉监听网络端口发出的数据包
return pkt[Ether].src != self.my_mac
def run(self):
# 重写Thread的run函数,线程启动时自动执行
# 创建L2层监听套接字,抓取全部以太网帧
self.socket = conf.L2listen(
# 抓取所有协议类型的帧
type=ETH_P_ALL,
# 指定监听的网络端口
iface=self.interface,
# BPF过滤器,只抓取IP包
filter="ip"
)
# opened_socket指定使用已经创建好的套接字
# prn指定回调函数,每当接收到符合条件的数据包时调用回调函数
# lfilter使得每接收到数据包后,先调用指定函数确定是否可以执行进一步的操作,如果返回False则忽略该包
# stop_filter在处理完每个数据包后,调用函数判断是否停止监听抓包
# 在循环中,lfilter类似于处理逻辑前的continue,stop_filter类似于处理逻辑后的break
sniff(opened_socket=self.socket, prn=self.print_packet, lfilter=self.isNotOutgoing,
stop_filter=self.should_stop_sniffer)
def join(self, timeout=None):
# 重写Thread的join函数,停止线程时执行
# 通知run函数停止监听
self.stop_sniffer.set()
# 线程真正停止
super(Sniffer, self).join(timeout)
def should_stop_sniffer(self, packet):
# 如果停止抓包的事件被设置了,就不再抓包
return self.stop_sniffer.isSet()
def print_packet(self, packet):
# 收到数据包后的回调函数,打印接收到的数据包信息
print("[!] A packet was reflected from the switch: ")
# packet.show()
ether_layer = packet.getlayer(Ether)
print(("[!] Info: {src} -> {dst}\n".format(src=ether_layer.src, dst=ether_layer.dst)))
|
交换机程序
反射器程序把将数据包从入端口发送回去,同时调换源MAC地址和目的MAC地址。
main函数
main函数定义了一个基于v1_model架构的交换机逻辑,核心作用是依次调用数据包处理的各个阶段模块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include <core.p4>
#include <v1model.p4>
// main函数依次调用解析器、流水线、封装器
V1Switch(
// 调用解析器进行数据包解析,提取报文头部字段
MyParser(),
// 校验接收到的数据包的校验和,保证包的完整性
MyVerifyChecksum(),
// 入队列处理阶段,基于匹配动作表Match-Action执行转发、修改等策略
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
|
// MAC地址为48位
typedef bit<48> macAddr_t;
// 定义数据包链路层的头部字段
header ethernet_t {
macAddr_t dstAddr;
macAddr_t srcAddr;
bit<16> etherType;
}
// 定义元数据
// 元数据是只存在于交换机或转发器内部的辅助数据结构
// 元数据不会出现在真正发送的网络数据包里
// 常用来在流水线之间传递中间信息,或协助匹配动作表决策或执行复杂逻辑
struct metadata {
/* empty */
}
// 实例化数据包头部
struct headers {
ethernet_t ethernet;
}
|
数据包解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 定义解析器
// packet_in:输入,将要处理的数据包
// out headers:输出,完成解析的数据包头部
// inout metadata:双方向参数,自定义的元数据
// inout standard_metadata_t:双方向参数,P4定义的标准元数据
// 双方向参数指该参数即是输入(从上一个模块接收)、也是输出(输出到下一个模块)
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 解析器采用状态机的方式组织,从start开始
state start{
// 解析提取数据包的头部,并填充进headers的ethernet中
packet.extract(hdr.ethernet);
// 解析器以accept结束,accept表示解析成功
transition accept;
}
}
|
检查校验和
1
2
3
4
|
// 用于检查接收到数据包的校验和,此处跳过了检查过程
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
24
25
|
// 定义在入队列侧的处理逻辑
// inout headers:已经解析好的数据包头
// inout metadata:自定义的元数据
// inout standard_metadata_t:P4定义的标准元数据
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 定义动作
action swap_mac(){
// 定义临时变量tmp
macAddr_t tmp;
// 交换L2链路层头部的源MAC地址与目的MAC地址
tmp = hdr.ethernet.srcAddr;
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = tmp;
}
// 控制逻辑
apply {
// 执行定义的swap_mac动作 以实现源目的MAC地址的对调
swap_mac();
// 在元数据中设置出端口号为入端口号
standard_metadata.egress_spec = standard_metadata.ingress_port;
}
}
|
出队列侧处理
1
2
3
4
5
6
7
|
// 定义在出队列侧的处理逻辑
control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 无处理动作
apply { }
}
|
计算校验和
1
2
3
4
|
// 重新计算数据包校验和,此处省略该过程
control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply { }
}
|
封装数据包
1
2
3
4
5
6
7
|
// 封装器
control MyDeparser(packet_out packet, in headers hdr) {
apply {
// 使用emit函数重新封装headers中的ethernet头部
packet.emit(hdr.ethernet);
}
}
|
执行P4仿真环境
在拓扑描述文件p4app.json所在的目录执行以下命令,启动网络拓扑:
该命令会自动调用Python脚本,解析拓扑描述文件p4app.json,创建基于mininet的虚拟网络环境,随后自动编译P4代码,并配置进软件交换机bmv2中

使用xterm h1命令登陆host1的shell界面,运行命令执行端侧收发Python脚本
1
2
|
xterm h1
python send_receive.py
|

当然也可以新建终端使用mx h1命令登录host1,运行命令执行端侧收发Python脚本:
1
2
|
mx h1
python send_receive.py
|

需要说明的是:当mininet运行过程中,若更改了P4程序,可以直接在mininet中使用以下命令,重新编译加载新的P4程序,而无需关闭正在运行的mininet
1
|
mininet> p4switch_reboot s1
|
