P4 在软件交换机和 Mininet 的编程实例
1. 编写一个P4程序
最小的P4程序将仅处理基于以太网的IPv4数据包,并且将仅包含一个表,该表对目标IP地址进行最长前缀匹配查找以决定传出端口。
-
为P4程序创建一个目录:
mkdir ~/test cd ~/test
-
使用编辑器创建具有以下内容的〜/ test / test.p4程序:
输入:
vi test.p4
即可创建文件#include <core.p4> #include <v1model.p4> typedef bit<48> EthernetAddress; typedef bit<32> IPv4Address; header ethernet_t { EthernetAddress dst_addr; EthernetAddress src_addr; bit<16> ether_type; } header ipv4_t { bit<4> version; bit<4> ihl; bit<8> diffserv; bit<16> total_len; bit<16> identification; bit<3> flags; bit<13> frag_offset; bit<8> ttl; bit<8> protocol; bit<16> hdr_checksum; IPv4Address src_addr; IPv4Address dst_addr; } struct headers_t { ethernet_t ethernet; ipv4_t ipv4; } struct metadata_t { } error { IPv4IncorrectVersion, IPv4OptionsNotSupported } parser my_parser(packet_in packet, out headers_t hd, inout metadata_t meta, inout standard_metadata_t standard_meta) { state start { packet.extract(hd.ethernet); transition select(hd.ethernet.ether_type) { 0x0800: parse_ipv4; default: accept; } } state parse_ipv4 { packet.extract(hd.ipv4); verify(hd.ipv4.version == 4w4, error.IPv4IncorrectVersion); verify(hd.ipv4.ihl == 4w5, error.IPv4OptionsNotSupported); transition accept; } } control my_deparser(packet_out packet, in headers_t hdr) { apply { packet.emit(hdr.ethernet); packet.emit(hdr.ipv4); } } control my_verify_checksum(inout headers_t hdr, inout metadata_t meta) { apply { } } control my_compute_checksum(inout headers_t hdr, inout metadata_t meta) { apply { } } control my_ingress(inout headers_t hdr, inout metadata_t meta, inout standard_metadata_t standard_metadata) { bool dropped = false; action drop_action() { mark_to_drop(standard_metadata); dropped = true; } action to_port_action(bit<9> port) { hdr.ipv4.ttl = hdr.ipv4.ttl - 1; standard_metadata.egress_spec = port; } table ipv4_match { key = { hdr.ipv4.dst_addr: lpm; } actions = { drop_action; to_port_action; } size = 1024; default_action = drop_action; } apply { ipv4_match.apply(); if (dropped) return; } } control my_egress(inout headers_t hdr, inout metadata_t meta, inout standard_metadata_t standard_metadata) { apply { } } V1Switch(my_parser(), my_verify_checksum(), my_ingress(), my_egress(), my_compute_checksum(), my_deparser()) main;
在文件末尾输入
:wq
即可保存并退出文件这是一个简单的程序。它只有一个查找表,该表对接收到的数据包中的目标IP地址执行最长前缀匹配(LPF)。操作是丢弃数据包,或将数据包转发到特定的输出端口。
2. 编译P4程序
编译P4程序。在下面的命令中,该-b
选项选择bmv2
(行为模型版本2)作为目标,这是我们将用来运行P4程序的软件开关。
p4c -b bmv2 test.p4 -o test.bmv2
该编译器生成目录test.bmv2,其中包含一个文件test.json
,该文件包含由软件开关运行的生成的“可执行”代码。
3. 创建虚拟以太网接口
现在将创建三对虚拟以太网接口
一个veth
接口是一个虚拟的(即假货)以太网接口上的应用程序(在我们的情况下,软件开关)可以发送和接收以太网数据包以同样的方式AA真正以太网接口。但是,在第ve个接口的情况下,数据包不会流出实际的以太网端口。相反,veth
接口总是成对创建。我们将创建三个对:veth0-veth1
,veth2-veth3
,和veth4-veth5
。当应用程序在第九个接口上发送数据包时,它将到达该对中的另一个veth
接口。
创建三对虚拟以太网接口。我们将每个接口的消息传输单元(MTU)设置为9500,以允许我们发送和接收巨型数据包。并且我们在每个接口上禁用了IPv6,以阻止内核发送路由器请求和多播侦听器报告(这不会阻止软件交换机通过该接口发送IPv6数据包)。
sudo ip link add name veth0 type veth peer name veth1
sudo ip link set dev veth0 up
sudo ip link set dev veth1 up
sudo ip link set veth0 mtu 9500
sudo ip link set veth1 mtu 9500
sudo sysctl net.ipv6.conf.veth0.disable_ipv6=1
sudo sysctl net.ipv6.conf.veth1.disable_ipv6=1
sudo ip link add name veth2 type veth peer name veth3
sudo ip link set dev veth2 up
sudo ip link set dev veth3 up
sudo ip link set veth2 mtu 9500
sudo ip link set veth3 mtu 9500
sudo sysctl net.ipv6.conf.veth2.disable_ipv6=1
sudo sysctl net.ipv6.conf.veth3.disable_ipv6=1
sudo ip link add name veth4 type veth peer name veth5
sudo ip link set dev veth4 up
sudo ip link set dev veth5 up
sudo ip link set veth4 mtu 9500
sudo ip link set veth5 mtu 9500
sudo sysctl net.ipv6.conf.veth4.disable_ipv6=1
sudo sysctl net.ipv6.conf.veth5.disable_ipv6=1
4. 启动软件开关并执行P4程序
在下面步骤中,需要打开多个终端。
第一个终端中(即刚才使用的界面,终端1)作为后台进程启动软件切换:
sudo simple_switch --interface 0@veth0 --interface 1@veth2 --interface 2@veth4 test.bmv2/test.json &
下面打开新终端(终端2),启动软件交换机的命令行界面(CLI):
simple_switch_CLI
运行后会得到一个RunTimeCmd提示符:
Obtaining JSON from switch...
Done
Control utility for runtime P4 table manipulation
RuntimeCmd:
至此,我们有了一个运行中的P4软件交换机,该交换机具有3个接口,如下图所示。
5. 一些CLI命令
在软件交换机的CLI中,即RuntimeCmd:
之后输入help
查看可用命令
RuntimeCmd: help
Documented commands (type help <topic>):
========================================
act_prof_add_member_to_group set_crc16_parameters
act_prof_create_group set_crc32_parameters
act_prof_create_member set_queue_depth
act_prof_delete_group set_queue_rate
act_prof_delete_member shell
act_prof_dump show_actions
act_prof_dump_group show_ports
act_prof_dump_member show_tables
[...]
使用show_tables
可以查看当前拥有的表
RuntimeCmd: show_tables
my_ingress.ipv4_match [implementation=None, mk=ipv4.dst_addr(lpm, 32)]
tbl_test87 [implementation=None, mk=]
table_info
命令可以报告表的更多信息
6. 将路由添加到路由表:
在软件交换机CLI中,使用以下table_add
命令将四个路由添加到路由表:
- 前缀为10.10.0.0/16的所有流量都发送到端口0
- 所有前缀为11.11.0.0/16的流量都发送到端口1
- 所有前缀为12.12.0.0/16的流量都发送到端口2
- 丢弃所有前缀为20.20.20.0/24的流量
- 由于表的默认操作为drop,因此也会删除所有与上述前缀都不匹配的流量。
RuntimeCmd: table_add ipv4_match to_port_action 10.10.0.0/16 => 0
Adding entry to lpm match table ipv4_match
match key: LPM-0a:0a:00:00/16
action: to_port_action
runtime data: 00:00
Entry has been added with handle 0
RuntimeCmd: table_add ipv4_match to_port_action 11.11.0.0/16 => 1
Adding entry to lpm match table ipv4_match
match key: LPM-0b:0b:00:00/16
action: to_port_action
runtime data: 00:01
Entry has been added with handle 1
RuntimeCmd: table_add ipv4_match to_port_action 12.12.0.0/16 => 2
Adding entry to lpm match table ipv4_match
match key: LPM-0c:0c:00:00/16
action: to_port_action
runtime data: 00:02
Entry has been added with handle 2
RuntimeCmd: table_add ipv4_match drop_action 20.20.20.0/24 =>
Adding entry to lpm match table ipv4_match
match key: LPM-14:14:14:00/24
action: drop_action
runtime data:
Entry has been added with handle 3
使用table_dump ipv4_match
可以显示我们添加到表中的条目
RuntimeCmd: table_dump ipv4_match
==========
TABLE ENTRIES
**********
Dumping entry 0x0
Match key:
* ipv4.dst_addr : LPM 0a0a0000/16
Action entry: my_ingress.to_port_action - 00
**********
Dumping entry 0x1
Match key:
* ipv4.dst_addr : LPM 0b0b0000/16
Action entry: my_ingress.to_port_action - 01
**********
Dumping entry 0x2
Match key:
* ipv4.dst_addr : LPM 0c0c0000/16
Action entry: my_ingress.to_port_action - 02
**********
Dumping entry 0x3
Match key:
* ipv4.dst_addr : LPM 14141400/24
Action entry: my_ingress.drop_action -
==========
Dumping default entry
Action entry: my_ingress.drop_action -
==========
7. 观察软件开关的行为
在单独的终端窗口中(终端3),发出以下命令以转储到达接口veth1的数据包:
sudo tcpdump -n -i veth3
可以得到输出:
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth3, link-type EN10MB (Ethernet), capture size 262144 bytes
8. 向软件交换机注入一些数据包
在一个新的终端窗口(终端4)启动scapy,我们将使用它们将数据包注入到软件交换机中:
sudo scapy
会收到一条欢迎消息,一些警告和提示:
INFO: Can't import matplotlib. Won't be able to plot.
INFO: Can't import PyX. Won't be able to use psdump() or pdfdump().
WARNING: No route found for IPv6 destination :: (no default route?)
INFO: Can't import python ecdsa lib. Disabled certificate manipulation tools
Welcome to Scapy (2.3.3)
>>>
使用Scapy,将具有目标IP地址11.11.1.1的数据包注入接口veth1中:
>>> p = Ether()/IP(dst="11.11.1.1")/UDP()
>>> sendp(p, iface="veth1")
.
Sent 1 packets.
数据包到达端口0(接口veth0
)上的软件交换机。它与路由11.11.0.0/16匹配,并转发到端口1(接口veth2,该接口连接到接口veth3)。
在运行tcpdump的终端(终端3)中,我们可以看到数据包已到达:
21:27:13.778127 IP 192.168.112.129.53 > 11.11.1.1.53: domain [length 0 < 12] (invalid)
回到运行scapy的会话,在接口veth0上发送目标IP地址为12.12.1.1的数据包:
>>> p = Ether()/IP(dst="12.12.1.1")/UDP()
>>> sendp(p, iface="veth1")
.
Sent 1 packets.
这次,数据包转发到端口2(接口veth4,该接口连接到接口veth5)。Tcpdump端口不报告任何接收到的数据包,因为它正在监视接口veth3而不是接口veth5。
9. P4官方网站相关链接
- Github库Bmv2 https://github.com/p4lang/behavioral-model
- Github库p4c https://github.com/p4lang/p4c
- P4官网 http://p4.org/
- P4语言规范 https://p4lang.github.io/p4-spec/
- P4邮件列表 http://lists.p4.org/mailman/listinfo/
- Barefoot官网 https://www.barefootnetworks.com/technology/
- Github库ProtoBuf https://github.com/google/protobuf/blob/master/src/README.md