为了能够更好的练习 P4 这门语言,这一篇文章我们一起配置一下环境,实战两个简单的练习项目。
目录
环境配置
一个完整的 P4 开发环境里,我们需要编译 P4 的代码(用 p4c 编译[1]),把编译好的文件跑在软件交换机上(我们会用 bmv2[2]),在创建的虚拟网络拓扑结构中验证我们实现的逻辑是否是正确的。
所以我们需要的安装的工具依次有:
- 编译器 p4c
- 软件交换机 BEHAVIORAL MODEL (bmv2)
- 用于构建虚拟网络的 Mininet[3]
- 用来抓包的 Wireshark
- ETHz NSG 网络团队开发的用于高效开发的 P4 utils[4]
当然,安装这一堆文件还是会很痛苦的,所以 ETHz 的大佬们给大家提供了安装好所有需要的工具的虚拟机 ova 文件,可以在这里科学下载。下载好了就可以直接用 VirtualBox 打开了!友情提醒,VirtualBox 记得安装 guest additions,这样会获得正确的分辨率,保护眼睛,从我做起。
环境配置好,我们就可以下载一下实战需要的源代码啦!我们用到的练习文件在这个 P4-learning 的 repository 里,在虚拟机里下载一份:
$ git clone https://github.com/nsg-ethz/p4-learning.git
然后我们的练习就在其中的 exercises
文件夹里。
以下的练习都在虚拟机里实现的,如果大家决定自己安装所有的工具,请按照自己的配置进行调整。
Packet Reflector
第一个练习很简单,我们在前一篇文章已经见过了它的代码实现,本质上其实就是把源地址和目标地址调换一下,再把 packet 从哪里来的就送回哪里去。但这个练习的主要目的,是为了让我们熟悉开发环境,包括如何建立一个虚拟的网络。
文件描述
这个练习用到的文件有三个:
p4app.json
:这个文件用来描述我们要建立的网络拓扑结构;send_receive.py
:这段 python 脚本用于发送和接收 packets;reflector.p4
:这个就是我们要完成的 P4 代码。
首先我们看一下第一个 json 文件:
{
"program": "reflector.p4",
"switch": "simple_switch",
"compiler": "p4c",
"options": "--target bmv2 --arch v1model --std p4-16",
"switch_cli": "simple_switch_CLI",
"cli": true,
"pcap_dump": true,
"enable_log": true,
...
"topology": {
"assignment_strategy": "l2",
"links": [["h1", "s1"]],
"hosts": {
"h1": {
}
},
"switches": {
"s1": {
}
}
}
}
里面规定了使用 p4c
作为编译器,用 bmv2 的 v1model 作为目标架构,使用 P4 16 的语言标准等等,具体的用法可以参考 p4 utils 的官方文档。但其中拓扑结构的部分,也就是 topology
那一段,直接定义了我们的目标结构。
其中 l2
的 assignment_strategy
表示我们假设所有的交换机都在链路层(layer 2)工作,所有的 hosts 都会被放在同一个子网中。每个 host 的 ARP table 也会被自动填好它的所有邻居的 MAC 地址。除了 l2
,还有 mixed
和 l3
两种选项。
使用 mixed
的时候,每个 host 只能和一个交换机相连,连接在同一个交换机上的 host 属于同一个 /24
子网中,这个交换机于是就作为了 host 的网络层网关(layer 3 gateway)。
使用 l3
的时候,每个交换机都在网络层(layer 3)工作,所以每个 interface 都属于一个独立的子网。至于具体的 IP 分配,可以参考 p4 utils 的官方文档。
第二个文件,send_receive.py
,使用 scapy
这个库里的大部分函数,关于其中主要的步骤,在下面加了一些注释。
#!/usr/bin/env python
import sys
import socket
import random
import time
from threading import Thread, Event
from scapy.all import *
class Sniffer(Thread):
'''
创建一个 Sniffer 的类,抓包
'''
def __init__(self, interface="eth0"):
super(Sniffer, self).__init__()
self.interface = interface
self.my_mac = get_if_hwaddr(interface)
self.daemon = True
self.socket = None
self.stop_sniffer = Event() # 创建一个停止抓包的 Event
def isNotOutgoing(self, pkt):
# 如果 packet 的源 MAC 地址不是
return pkt[Ether].src != self.my_mac
def run(self):
# 创建一个 Layer 2 的 socket,只保留 IP packet
self.socket = conf.L2listen(
type=ETH_P_ALL,
iface=self.interface,
filter="ip"
)
sniff(opened_socket=self.socket, prn=self.print_packet, lfilter=self.isNotOutgoing, stop_filter=self.should_stop_sniffer)
def join(self, timeout=None):
# 终止我们的 sniffer
self.stop_sniffer.set()
super(Sniffer, self).join(timeout)
def should_stop_sniffer(self, packet):
# 如果停止抓包的 Event 被设置了,就不再抓包
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))
def get_if():
ifs=get_if_list()
iface=None # "h1-eth0" 是我们的目标 interface,代表 host 1 上的 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 send_packet(iface, addr):
'''
构造一个 Ethernet + IP 的 packet
'''
raw_input("Press the return key to send a packet:")
print "Sending on interface %s to %s\n" % (iface, str(addr))
pkt = Ether(src=get_if_hwaddr(iface), dst='00:01:02:03:04:05') # 设置 Ethernet header
pkt = pkt /IP(dst=addr) # 再加上 IP header
sendp(pkt, iface=iface, verbose=False)
def main():
# 目标地址
addr = "10.0.0.2"
addr = socket.gethostbyname(addr)
iface = get_if() # 获取当前的 interface
listener = Sniffer(iface)
listener.start()
time.sleep(0.1)
try:
while True:
# 每半秒发送一个 packet
send_packet(iface, addr)
time.sleep(0.5)
except KeyboardInterrupt:
print("[*] Stop sniffing")
listener.join(2.0)
if listener.isAlive():
listener.socket.close()
if __name__ == '__main__':
main()
第三个文件,reflector.p4
,就是我们要修改的 P4 源码了。其中,所有的模块按照顺序,都排列在 main 函数里。这个练习里,我们要做的,是 Parser、Ingress、和 Deparser 部分。
/*************************************************************************
*********************** S W I T C H *******************************
*************************************************************************/
V1Switch(
MyParser(), // TODO
MyVerifyChecksum(),
MyIngress(), // TODO
MyEgress(),
MyComputeChecksum(),
MyDeparser() // TODO
) main;
开发步骤
简单的看过了需要的文件之后,我们可以开始一步一步开始开发了。
第一步,把 p4app.json
里定义的网络拓扑跑起来,只需要在它所在的路径下跑:
$ sudo p4run
这行命令会自动调用一个 python 脚本,解析 p4app.json
,创建 mininet 的虚拟网络环境,编译 P4 代码,并安装在 bmv2 的软件交换机里。具体的流程感兴趣的话可以参考 p4 utils 的官方文档。
跑完之后,就有 mininet 的 CLI 了:
Mininet CLI
比如我们可以看一下正在运行的网络拓扑,我们就知道有一个 host,一个 switch 分别记作 h1 和 s1,并且它们之间的 link 是联通的。
mininet> nodes
h1 s1
mininet> links
h1-eth0<->s1-eth1 (OK OK)
关于 mininet 的更多使用方法可以参考自带的 help:
Mininet CLI Help
第二步,从 mininet 的 CLI 里,我们可以用 xterm h1
登陆 host h1,得到一个 shell 的界面。在这里,我们可以运行 python send_receive.py
从 h1 向 s1 发送 packet。但因为我们还没有改写 reflector.p4
这个文件,所以不会收到返回来的 packet:
接下来,我们看一下该怎么修改 reflector.p4
这个文件才能收到返回的 packet。
解决方案
在第一个 TODO 的地方,我们要做的只是简单的解析 header:
/* -*- P4_16 -*- */
#include <core.p4>
#include <v1model.p4>
/*************************************************************************
*********************** H E A D E R S ***********************************
*************************************************************************/
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;
}
/*************************************************************************
*********************** 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 1: parse ethernet header */
packet.extract(hdr.ethernet); // 使用 extract 就可以得到 ethernet header
transition accept;
}
}
第二个 TODO 的部分,也就是 Ingress,我们可以直接把交换地址的过程写在 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) {
apply {
/* TODO 2: swap mac addresses */
macAddr tmp = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
hdr.ethernet.srcAddr = tmp;
/* TODO 3: set output port */
standard_metadata.egress_spec = standard_metadata.ingress_port;
}
}
也可以单独写一个 action 出来:
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action swap_mac() {
macAddr tmp = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
hdr.ethernet.srcAddr = tmp;
}
apply {
/* TODO 2: swap mac addresses */
swap_mac();
/* TODO 3: set output port */
standard_metadata.egress_spec = standard_metadata.ingress_port;
}
}
第三个 TODO,类似上一篇讲 Deparser 的过程,不需要特殊的操作,只需要一个 emit
:
/*************************************************************************
*********************** D E P A R S E R *******************************
*************************************************************************/
control MyDeparser(packet_out packet, in headers hdr) {
apply {
/* TODO 4: deparse ethernet header */
packet.emit(hdr.ethernet);
}
}
写完所有的 TODO 之后,我们就可以再次编译了。重新编译的时候,我们不需要关掉已经在运行的 mininet,只需要:
mininet> p4switch_reboot s1;
就可以直接加载新写的 P4 代码了,节约时间。
编译完成之后,我们再在 h1 上跑 send_receive.py
,就能收到镜像回来的 packet 了:
这个练习到这里就结束了。如果大家心满意足了,就可以在 CLI 里退出了,用 quit
或者 exit
或者 Ctrl-D。
Repeater
第二个练习,我们要实现的是一个简单的双端口交换机,能把 packet 在两个 hosts 之间进行传递。我们的网络拓扑结构如下图所示:
Repeater Exercise: Topology
在配置文件 p4app.json
中,拓扑结构体现为:
"topology": {
"assignment_strategy": "l2",
"links": [
["h1", "s1"],
["h2", "s1"]
],
"hosts": {
"h1": {},
"h2": {}
},
"switches": {
"s1": {}
}
}
和上一个练习相比,这个结构多了一个 host h2,也多了一个 link。
我们的目标就是实现 h1 和 h2 之间能 ping 通。测试的时候在 mininet 的 cli 里直接用 mininet> h1 ping h2
测试就好了。
为了实现这样一个 repeater,我们有两种解决方案,第一种解决方案,就是在 Ingress 的部分写一个静态的规则。
// solution 1
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;
}
}
}
我们跑上 sudo p4run
之后,来到 mininet 的 CLI 之后,可以直接实验,ping 通就对了!
Ping test
当然,除了 ping
,我们还可以用 iperf
,测一下 throughput:
Iperf test
练习里还提供了两段 python,一个是 send.py
,一个是 receive.py
,我们打开 h2 的终端,跑上 python receive.py
,再打开一个 h1 的终端,跑上 python send.py 10.0.0.2 "[whatever message you want to send here]"
,也可以验证:
Python send-receive test
当然,除了第一种“静态”的解决方案,我们还有一个用表来解决的方法。
首先,我们在 repeater.p4
里的 Ingress 部分建一个表,就叫 repeater,只做最基本的 Match-Action:
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
// 定义一个 action,确定出口端
action forward(bit<9> egress_port){ // 这里的参数是无方向的,因为是来查表得到的
standard_metadata.egress_spec = egress_port;
}
table repeater {
key = {
standard_metadata.ingress_port: exact; // 完全 match 才可以
}
actions = { // 两种动作
forward; // 一定是之前已经声明的 action
NoAction;
}
size = 2; // 只需要两个端口的规则,所以表里只需要两个 entries
default_action = NoAction;
}
apply {
repeater.apply();
}
}
有了这个代码,我们就相当于告诉了交换机,每次收到 packet 的时候,要去查表,再决定怎么转发。但到这里,我们的表还是空的。所以现在我们要在 repeater 的表里填上我们想要的规则。
第一步就是写一个简单的 txt 文件,比如叫 s1-commands.txt
,里面写上:
table_add repeater forward 1 => 2
table_add repeater forward 2 => 1
这两行就会在 repeater 这个表里,填好两行 Match-Action 的规则了。这两行的语法,就是在表里添加规则的语法:
tabel_add <table_name> <action_name> <match_fields> => <action_parameters>
这里我们的 table_name
是 repeater
,我们创建的 action_name
也就是那个“函数”就是 forward
。更多关于这部分的语法可以参考这个文档。
写好了这个文件之后,只要最后在 p4app.json
的配置文件里,对我们的 switch把这个 txt 文件作为 cli_input
就好了:
"topology": {
"assignment_strategy": "l2",
"links": [
["h1", "s1"],
["h2", "s1"]
],
"hosts": {
"h1": {},
"h2": {}
},
"switches": {
"s1": {
"cli_input": "s1-commands.txt"
}
}
}
这样,我们第二种方法也可以获得和第一种方法一样的效果。
小结
这篇文章,我们正式开始了 P4 的实战部分,熟悉了实验需要的 virtualbox、p4utils、mininet 等环境和工具。前两个练习,相对简单,但帮助我们复习了 P4 的基础语法和架构,比如 Parser->Ingress->Egress->Deparser 的流水线、standard_metadata
包含的数据、 table 的定义和使用等等。
之后的文章会继续实战,上手更复杂一点的习题。
参考
- ^P4 Reference Compiler https://github.com/p4lang/p4c
- ^Behavioral Model (bmv2) https://github.com/p4lang/behavioral-model
- ^Mininet http://mininet.org/
- ^P4 Utils https://github.com/nsg-ethz/p4-utils