经过前两篇的P4理论介绍,相信大家已经对P4有个基本的了解了,本片文章为大家带来P4语言编程实战。
1、系统环境安装
P4项目的官方文档上都是以Ubuntu为例,笔者惯用的linux系统也是Ubuntu,因此本篇文章中的实验都基于Ubuntu14.04完成的。开始安装环境之前,记得先下载P4项目源码(https://github.com/p4lang)。
本篇文章主要介绍如何手动编译安装P4开发环境并使用虚拟交换机(bmv2)进行实验,如果不想手动下载源码或单独编译、安装每个模块,也可以选择下载已经集成了P4编译开发环境Ubuntu系统镜像。传送门:https://drive.google.com/file/d/0BxHYRsv-PNVvaEpZYnJ5LTAyZkk/edit。注:请自备翻墙梯子。
2、环境依赖包
P4项目主要由C++和Python语言开发,同时需要对源码进行编译安装,所以需要安装许多环境依赖包。当然,这里并不需要手动将安装每个依赖包,运行p4factory目录下的install_deps.sh脚本,可以一键安装所有的依赖包,包括运行bmv2所必须的thrift、nanomsg和nnpy等组件。
Java
1 | ./install_deps.sh |
由于我们是在虚拟环境下使用进行实验,所以需要创建多个虚拟以太网口。
Java
1 | ./sudo p4factory/tools/veth_setup.sh |
3、模块依赖
P4项目由很多模块组成,部分模块与模块之间存在依赖关系,安装也就必须按照依赖关系先后安装。每个子项目的作用在上篇文章《P4语言编程详解》中已介绍,这里不再赘述。图1中展示了P4项目主要模块的依赖关系,在进行编译安装时可以参考该依赖关系选择安装顺序。这里需要注意模块依赖图与P4架构图的区别。
图1 主要模块依赖关系图
To make your life easier,P4项目中对各个子模块的安装提供了完备的脚本,安装了各个子模块后就能直接使用对应模块的CLI脚本,CLI的详细使用方法可以使用-h参数查看命令帮助。
(1)安装hlir
Java
1 2 | cd p4-hlir sudo python ./setup install |
安装后提供的CLI:p4-validate,p4-shell, p4-graphs。
(2)安装p4c-bm
Java
1 2 3 | cd p4c-bm sudo pip install –r requirements.txt sudo python setup.py install |
安装后提供的CLI:p4c-bmv2。
(3)安装behavioral-model(bmv2)
Java
1 2 3 4 | cd behavioral-model ./autogen.sh ./configure make |
图2 bmv2运行时CLI
4、p4factory
P4项目中同时也提供了p4factory模块,该模块可以单独编译运行,目的就是方便用户/开发者快速开始。
Java
1 2 3 4 5 | cd p4factory ./autogen.sh ./configure //创建虚拟以太网口 ./sudo p4factory/tools/veth_setup.sh |
5、动动手
环境安装完毕之后,就可以开始着手运行一些P4程序示例了。本章主要从源码-运行-抓包-脚本等方面介绍simple_router示例实验。
图3 P4程序定义数据平面流程
这里介绍一下笔者在看P4程序时候的方法,仅供大家参考:首先,从流控制程序(control)开始,查看流水线(pipeline)中应用的匹配-动作表(table)有哪些;然后,依次查看每个table中可以匹配的字段(field)以及可以执行的动作(action),理清pipline中的match-action模型;最后,查看包头(header)和解析器(parser),获悉在pipeline中进行match-action的包头实例中存储的数据。
(1)源码
simple_router是一个最简三层路由,P4程序源码在p4factory/targets/simple_router/目录下,代码如下。
#include "includes/headers.p4"
#include "includes/parser.p4"
action _drop() {
drop();
}
header_type routing_metadata_t {
fields {
nhop_ipv4 : 32;
}
}
metadata routing_metadata_t routing_metadata;
action set_nhop(nhop_ipv4, port) {
modify_field(routing_metadata.nhop_ipv4, nhop_ipv4);
modify_field(standard_metadata.egress_spec, port);
add_to_field(ipv4.ttl, -1);
}
table ipv4_lpm {
reads {
ipv4.dstAddr : lpm;
}
actions {
set_nhop;
_drop;
}
size: 1024;
}
action set_dmac(dmac) {
modify_field(ethernet.dstAddr, dmac);
}
table forward {
reads {
routing_metadata.nhop_ipv4 : exact;
}
actions {
set_dmac;
_drop;
}
size: 512;
}
action rewrite_mac(smac) {
modify_field(ethernet.srcAddr, smac);
}
table send_frame {
reads {
standard_metadata.egress_port: exact;
}
actions {
rewrite_mac;
_drop;
}
size: 256;
}
control ingress {
apply(ipv4_lpm);
apply(forward);
}
control egress {
apply(send_frame);
}
J
按照上述的P4程序阅读方法,可以获取以下信息:
a)simple_router.p4定义的数据面中包换两条pipeline:ingress和egress。
b)ingress中有两级match-action,分别为ipv4_lpm和forward表。
c)egress中有一级match-action,send_frame表。
d)ipv4_lpm table中通过对下一跳IPv4地址(nhop_ipv4)进行最长前缀匹配(lpm)后修改下一跳地址(nhop_ipv4)或修改出端口(egress_port)。
e)forward table中通过对下一跳IPv4地址(nhop_ipv4)进行精确匹配(exact)后可以修改以太网帧的目的mac(dstAddr)或者将包丢弃(drop)。
f)send_frame table中通过对egress_port进行精确匹配后修改源mac地址(srcAddr)或将包丢弃。
parser.p4代码如下
parser start {
return parse_ethernet;
}
#define ETHERTYPE_IPV4 0x0800
header ethernet_t ethernet;
parser parse_ethernet {
extract(ethernet);
return select(latest.etherType) {
ETHERTYPE_IPV4 : parse_ipv4;
default: ingress;
}
}
header ipv4_t ipv4;
field_list ipv4_checksum_list {
ipv4.version;
ipv4.ihl;
ipv4.diffserv;
ipv4.totalLen;
ipv4.identification;
ipv4.flags;
ipv4.fragOffset;
ipv4.ttl;
ipv4.protocol;
ipv4.srcAddr;
ipv4.dstAddr;
}
field_list_calculation ipv4_checksum {
input {
ipv4_checksum_list;
}
algorithm : csum16;
output_width : 16;
}
calculated_field ipv4.hdrChecksum {
verify ipv4_checksum;
update ipv4_checksum;
}
parser parse_ipv4 {
extract(ipv4);
return ingress;
}
l2_switch示例中的解析器(parser)只是将header中的数据完全提取出来,并存放到实例中,simple_router的parser相对复杂一些.
a)parser都是以start方法开始,以ingress结束。
b)return parse_ethernet表示进入parse_ethernet解析器。
c)parse_ethernet解析器提取(extract)以太网帧包头(header ethernet)数据,根据以太网类型(etherType)进行判断,如果etherType为0x0800(IPv4),则进入pasrse_ipv4解析器。
d)parse_ipv4解析器提取ipv4包头(header ipv4)数据,并通过ingress塞入pipeline。
e)field_list_calculation 是输入一个字段有序字段列表(field-list不含字段长度,区别于header),指定算法(algorithm)和输出长度,然后获取计算结果。parser.p4程序片段中展示的是ipv4校验和(ipv4_checksum),其中的算法是P4语言内置支持(当然也需要硬件支持)的。
simple_router中定义的header描述了以太网帧和ipv4包的结构,比较清晰易懂,代码如下:
header_type ethernet_t {
fields {
dstAddr : 48;
srcAddr : 48;
etherType : 16;
}
}
header_type ipv4_t {
fields {
version : 4;
ihl : 4;
diffserv : 8;
totalLen : 16;
identification : 16;
flags : 3;
fragOffset : 13;
ttl : 8;
protocol : 8;
hdrChecksum : 16;
srcAddr : 32;
dstAddr: 32;
}
}
(2)运行
看完实例源码后,我们需要通过运行simple_router示例进行验证工作。虽然这边p4项目中对此也提供了完备的脚本,我们可以一键运行,但笔者这里除了介绍一键脚本运行还是想和大家一起看看这些脚本中到底干了些啥。
启动simple_router:
cd p4factory/targets/simple_router
make bm
sudo ./ behavioral-model
运行behavioural-model程序会在本地启动一个bmv2,默认PD RPC server地址为127.0.0.1:9090。当在同一台宿主机上启动多个bmv2时,可以使用--pd-server=IP:PORT指定不同的端口,以防止端口占用冲突。更多参数可以使用-h参数查看CLI帮助。启动simple_router之后可以通过运行run_cli.bash脚本,访问simple_router的运行时CLI。
run_cli.bash:
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils $@"
run_cli.bash脚本中通过pd_cli.py连接到simple_router的运行时CLI,如下图所示。
图4:simple_router运行时CLI
pd_cli.py可以通过不同的参数指定p4名称,Thrift服务器地址等。
也可以通过P4项目中提供的run_demo.bash脚本一键启动simple_router示例。
run_demo.bash:
sudo python ../../mininet/1sw_demo.py --behavioral-exe $PWD/behavioral-model
run_demo.bash中运行了1sw_demo.py脚本,如果运行脚本时没有添加参数,则默认创建了一个1switch-2host的mininet仿真网络拓扑,其中的switch用bmv2替代,PD RPC server地址,也可以通过参数控制,默认为127.0.0.1:22222。mininet不作为本文重点,这里不附上脚本源码,有兴趣的读者请自行查看。
此时server端口不再是9090,直接运行run_cli.bash脚本就无法连接到运行时CLI(run_cli.bash脚本中没有指定server地址,默认为127.0.0.1:9090)。需要修改run_cli.bash内容,添加脚本运行时参数,修改后的脚本如下所示:
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils –c 127.0.0.1:22222 $@"
或者直接运行pd_cli.py脚本并添加运行时参数,如下所示:
./run_cli.bash –c 127.0.0.1:22222
(3)验证
至此,我们的创建好一个通过P4语言定义的虚拟数据平面。
通过bmv2运行时CLI查看:
图5 表(table)详情
当然仅有数据平面是无法正确工作的,此时使用mininet中的h1 ping h2是无法ping通的,还需要控制平面的配合,也就是run_add_demo_entries.bash脚本,这个脚本扮演了控制平面这一角色,向simple_router下发流表。
run_add_demo_entries.bash:
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry send_frame 1 rewrite_mac 00:aa:bb:00:00:00" -c localhost:22222
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry send_frame 2 rewrite_mac 00:aa:bb:00:00:01" -c localhost:22222
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry forward 10.0.0.10 set_dmac 00:04:00:00:00:00" -c localhost:22222
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry forward 10.0.1.10 set_dmac 00:04:00:00:00:01" -c localhost:22222
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry ipv4_lpm 10.0.0.10 32 set_nhop 10.0.0.10 1" -c localhost:22222
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry ipv4_lpm 10.0.1.10 32 set_nhop 10.0.1.10 2" -c localhost:22222
run_add_demo_entries.bash脚本中向simple_router中添加了6条流表,这6条流表针对P4程序中定义的forward、ipv4_lpm和send_frame表(table)提供了具体的转发控制。
除了使用pd_cli.py脚本下发流表,也可以通过运行时CLI下发:
/behavioral-model/tools/runtimeCLI –c 127.0.0.1:22222 < command.txt
command.txt:
table_set_default send_frame _drop
table_set_default forward _drop
table_set_default ipv4_lpm _drop
table_add send_frame rewrite_mac 1 => 00:aa:bb:00:00:00
table_add send_frame rewrite_mac 2 => 00:aa:bb:00:00:01
table_add forward set_dmac 10.0.0.10 => 00:04:00:00:00:00
table_add forward set_dmac 10.0.1.10 => 00:04:00:00:00:01
table_add ipv4_lpm set_nhop 10.0.0.10/32 => 10.0.0.10 1
table_add ipv4_lpm set_nhop 10.0.1.10/32 => 10.0.1.10 2
从这些流表下发方法中也可以认识到以下几点:
a)没有控制面下发的流表,仅数据面无法工作。
b)控制面下发的流表必须与P4程序中定义的table相吻合,从匹配字段(match-field)到动作(action).
c)P4定义的数据面所对应的控制面,可以是控制器、运行时CLI,也可以是SAI等。
流表下发后可以通过CLI查看,如图6所示:
图6 流表详情
图7 h1 ping h2
6、动动手plus
以上内容都是运行的P4项目中已经提供的P4程序,现在展示笔者做的一个小实验:为simple_router添加一个计数器(counter),该计数器附加(attach)到send_frame表上,每当有数据包通过send_frame表匹配成功并发送数据包时,就会触发计数器计数。同时,将发往1口(s1-eth1)的所有数据包的data字段前32位数值改为下发流表时指定的数值。
(1)修改header
首先,需要修改ipv4 header定义,simple_router.p4中只将定义了从version到dstAddr之间的120bit的结构,而其后的data段的数据并没有定义。这里将data段也纳入P4程序的header中。
header.p4:
header_type ipv4_t {
fields {
version : 4;
ihl : 4;
diffserv : 8;
totalLen : 16;
identification : 16;
flags : 3;
fragOffset : 13;
ttl : 8;
protocol : 8;
hdrChecksum : 16;
srcAddr : 32;
dstAddr: 32;
data: 32;//data段远不止32位,这里仅取前32位以作实验
}
}
Java
(2)添加indrect counter
计数类型为packets,每匹配一个packet进行一次计数。
1 2 3 4 | counter ingress_addr_count { type : packets; instance_count: 16384; } |
(3)添加action
新添加的set_addr_count 操作需要传入3个操作数:idx,smac,data。
idx是counter计数索引,也可通过此索引调用;
smac是源mac地址,因为这里使用set_adrd_count操作替换来原来的rewrite操作,所以需要重新实现rewrite操作;
data是写入data段的数据。
action set_addr_count(idx,smac,data) {
count(ingress_addr_count, idx);//调用计数器计数
modify_field(ethernet.srcAddr, smac);//修改包头源mac
modify_field(ipv4.data,data);//修改data段数据
}
(4)修改流表
因为修改了send_frame表的actions,所以对应的流表也要根据action进行修改,set_addr_count操作后面加上对应的3个传入参数。这里笔者设置了counter的idx为1,写入data段的数据设置为0xffffffff。
run_add_demo_entries.bash:
python ../../cli/pd_cli.py -p simple_router -i p4_pd_rpc.simple_router -s $PWD/tests/pd_thrift:$PWD/../../testutils -m "add_entry send_frame 1 set_addr_count 1 00:aa:bb:00:00:00 0xffffffff" -c localhost:22222
(4)验证
登录mininet的CLI,使用h1 ping h2.在添加流表前,h1无法ping h2,然后运行 run_add_demo_entries.bash脚本,并进行对s1-eth1口进行抓包。此时h1能够ping通h2,分析抓取到的数据包,也可以看到原先IPv4包头的data段数据的前32位已被改写成ffffffff。
图8 wireshark抓包分析
7、结语
以上根据个人的理解所写,希望能让不了解P4的人能有个基本的认识,同时起到抛砖引玉的作用。对P4感兴趣的同学可以联系笔者加入到P4微信交流群中与大牛们一起讨论。