- Created by Fréderic LOUI, last modified on Sep 24, 2020
After having followed P4Lang P4 for dummies [ #002 ] article, you should have now a working P4 development environment.
Requirement
|
Overview
Let's start writing. compiling and running our first P4 program.
Article objective
This 3rd article propose to write your first P4 program based on P4Lang P4 for dummies [ #001 ] my_program.p4 specification.
Diagram: my_program.p4
[ #003 ] - Cookbook
P4 program specification
my_program.p4 packet progressing logic: "all packets arriving at port 4 are switched/forwarded to port 8"
- In this example, the switch has 8 ports
- A ingress packet arrives at port 4
- the ingress port is then checked
- If it is port 4, then the packet is switched to port 8
- my_program.p4 does not implement a default condition, so all the packets not arriving on port 4 are then dropped
- the ingress packets arrived with a header with charateristics set by the previous node
- if needed, my_program.p4 is able to set modify the egress packet header for further processing by the next network node (example of in-band network Telemetry)
Let's first create the P4 program environment:
mkdir -p ~/my_program/bin ~/my_program/p4src ~/my_program/p4rt_python ~/my_program/build
tree -d my_program/ my_program/ <------- top folder ├── bfrt_python <------- python/scapy folder containg tests scripts ├── bin <------- executable binary folder ├── build <------- containing p4 compilation artefacts results └── p4src <------- containing p4 code
/* * P4 language version: P4_16 */ /* * include P4 core library */ #include <core.p4> /* * include P4 v1model library implemented by simple_switch */ #include <v1model.p4> #define PORT_4 4 #define PORT_8 8 /* * egress_spec port encoded using 9 bits */ typedef bit<9> nexthop_id_t; /* * metadata type */ struct metadata_t { nexthop_id_t nexthop_id; } /* * Our P4 program header structure */ struct headers { } /* * V1Model PARSER */ parser prs_main(packet_in packet, out headers hdr, inout metadata_t md, inout standard_metadata_t std_md) { state start { transition select(std_md.ingress_port) { PORT_4: prs_port_4; default: accept; } } state prs_port_4 { md.nexthop_id = PORT_8; transition accept; } } /* * V1Model CHECKSUM VERIFICATION */ control ctl_verify_checksum(inout headers hdr, inout metadata_t metadata) { apply { } } /* * V1Model INGRESS */ control ctl_ingress(inout headers hdr, inout metadata_t md, inout standard_metadata_t std_md) { apply { if (std_md.ingress_port == PORT_4) { std_md.egress_spec = md.nexthop_id; } } } /* * V1Model EGRESS */ control ctl_egress(inout headers hdr, inout metadata_t md, inout standard_metadata_t std_md) { apply { } } /* * V1Model CHECKSUM COMPUTATION */ control ctl_compute_checksum(inout headers hdr, inout metadata_t md) { apply { } } /* * V1Model DEPARSER */ control ctl_deprs(packet_out packet, in headers hdr) { apply { /* * emit hdr */ packet.emit(hdr); } } /* * V1Model P4 Switch define in v1model.p4 */ V1Switch( prs_main(), ctl_verify_checksum(), ctl_ingress(), ctl_egress(), ctl_compute_checksum(), ctl_deprs() ) main;
p4c --std p4-16 --target bmv2 --arch v1model -I ./include -o ./build --p4runtime-files ./build/my_program.json ./p4src/my_program.p4m
Verification
floui@ubi16:~/my_program$ ls -l build/ total 44 -rw-rw-r-- 1 floui floui 7532 Jul 24 14:23 my_program.json <------ output used when launching bmv2 -rw-rw-r-- 1 floui floui 35462 Jul 24 14:23 my_program.p4ip <------ other usage (not taken into account by the examplr)
Create veth pair before ...
Before launching our BMv2 virtual switch we need to create the veth pair that will be bound the P4 switch.
for that we will reuse bash scripts from Andy Fingerhut public GitHub Repository:
cd ~/my_program/bin wget https://raw.githubusercontent.com/jafingerhut/p4-guide/master/bin/veth_setup.sh wget https://raw.githubusercontent.com/jafingerhut/p4-guide/master/bin/veth_teardown.sh chmod u+x ./veth_setup.sh chmod u+x ./veth_teardown.sh sudo ./veth_setup.sh ip link | grep veth 4: veth1@veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 5: veth0@veth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 6: veth3@veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 7: veth2@veth3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 8: veth5@veth4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 9: veth4@veth5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 10: veth7@veth6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 11: veth6@veth7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 12: veth9@veth8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 13: veth8@veth9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 14: veth11@veth10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 15: veth10@veth11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 16: veth13@veth12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 17: veth12@veth13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 18: veth15@veth14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 19: veth14@veth15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 20: veth17@veth16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 21: veth16@veth17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
we can now launch BMv2 simple_switch and bind the 8 veth pairs we just configured
sudo simple_switch --log-console -i 1@veth2 -i 2@veth4 -i 3@veth6 -i 4@veth8 -i 5@veth10 -i 6@veth12 -i 7@veth14 -i 8@veth16 ./build/my_program.json Calling target program-options parser [14:28:41.364] [bmv2] [D] [thread 15917] Set default default entry for table 'tbl_my_program76': my_program76 - Adding interface veth2 as port 1 [14:28:41.364] [bmv2] [D] [thread 15917] Adding interface veth2 as port 1 Adding interface veth4 as port 2 [14:28:41.415] [bmv2] [D] [thread 15917] Adding interface veth4 as port 2 Adding interface veth6 as port 3 [14:28:41.455] [bmv2] [D] [thread 15917] Adding interface veth6 as port 3 Adding interface veth8 as port 4 [14:28:41.503] [bmv2] [D] [thread 15917] Adding interface veth8 as port 4 Adding interface veth10 as port 5 [14:28:41.547] [bmv2] [D] [thread 15917] Adding interface veth10 as port 5 Adding interface veth12 as port 6 [14:28:41.587] [bmv2] [D] [thread 15917] Adding interface veth12 as port 6 Adding interface veth14 as port 7 [14:28:41.635] [bmv2] [D] [thread 15917] Adding interface veth14 as port 7 Adding interface veth16 as port 8 [14:28:41.683] [bmv2] [D] [thread 15917] Adding interface veth16 as port 8 [14:28:41.727] [bmv2] [I] [thread 15917] Starting Thrift server on port 9090 [14:28:41.728] [bmv2] [I] [thread 15917] Thrift server was started ...
sudo tcpdump -i veth8 ...
sudo tcpdump -i veth16 ...
Now you need to find a way to:
- send a packet to simple_switch@PORT_4 (veth8)
- send another packet to simple_switch@PORT_1 (veth2)
We will use scapy for that:
pip3 install --pre scapy[complete]
Run scapy with sufficient privileges to send packets on specific interface
/usr/lib/python3/dist-packages/IPython/utils/module_paths.py:29: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses import imp aSPY//YASa apyyyyCY//////////YCa | sY//////YSpcs scpCY//Pp | Welcome to Scapy ayp ayyyyyyySCP//Pp syY//C | Version 2.4.3~bionic AYAsAYYYYYYYY///Ps cY//S | pCCCCY//p cSSps y//Y | https://github.com/secdev/scapy SPPPP///a pP///AC//Y | A//A cyP////C | Have fun! p///Ac sC///a | P////YCpc A//A | Craft packets like I craft my beer. scccccp///pSP///p p//Y | -- Jean De Clerck sY/////////y caa S//P | cayCyayP//Ya pY/Ya sY/PsY////YCc aC//Yp sc sccaCY//PCypaapyCP//YSs spCPY//////YPSps ccaacs using IPython 5.5.0 >>>
>>> sendp(IP(dst="1.2.3.4")/ICMP(),iface="veth8") . Sent 1 packets. >>>
floui@ubi16:~$ sudo tcpdump -i veth8 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on veth8, link-type EN10MB (Ethernet), capture size 262144 bytes 14:58:23.404299 00:00:40:01:9d:d2 (oui Unknown) > 45:00:00:1c:00:01 (oui Unknown), ethertype Unknown (0xc1e0), length 28: 0x0000: 1728 0102 0304 0800 f7ff 0000 0000 .(............
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on veth16, link-type EN10MB (Ethernet), capture size 262144 bytes 14:58:23.406042 00:00:40:01:9d:d2 (oui Unknown) > 45:00:00:1c:00:01 (oui Unknown), ethertype Unknown (0xc1e0), length 28: 0x0000: 1728 0102 0304 0800 f7ff 0000 0000 .(............ Conclusion
Congratulations !
You have successfully written, compiled, load your program P4Lang P4 virtual switch ! In addition, you also checked that the logic of your program is implemented correctly by sending a packet to PORT_4 using scapy python3 tool. You then checked with tcpdump that your packet ingressed the P4 switch via PORT_4 and egressed via PORT_8 as it was expected.
What's happening to other packets arriving on a port that is different from PORT_4 ?
Let's try to find out. In that situation, let's send an ingress packet to PORT_1 (veth2) of the switch and see what's happening.
>>> sendp(IP(dst="1.2.3.4")/ICMP(),iface="veth2") . Sent 1 packets. >>>
In that case we don't know what is the egress port so let's look at simple_switch console.
floui@ubi16:~/my_program$ sudo simple_switch --log-console -i 1@veth2 -i 2@veth4 -i 3@veth6 -i 4@veth8 -i 5@veth10 -i 6@veth12 -i 7@veth14 -i 8@veth16 ./build/my_program.json Calling target program-options parser [15:10:55.525] [bmv2] [D] [thread 16129] Set default default entry for table 'tbl_my_program76': my_program76 - Adding interface veth2 as port 1 [15:10:55.525] [bmv2] [D] [thread 16129] Adding interface veth2 as port 1 Adding interface veth4 as port 2 [15:10:55.555] [bmv2] [D] [thread 16129] Adding interface veth4 as port 2 Adding interface veth6 as port 3 [15:10:55.603] [bmv2] [D] [thread 16129] Adding interface veth6 as port 3 Adding interface veth8 as port 4 [15:10:55.651] [bmv2] [D] [thread 16129] Adding interface veth8 as port 4 Adding interface veth10 as port 5 [15:10:55.691] [bmv2] [D] [thread 16129] Adding interface veth10 as port 5 Adding interface veth12 as port 6 [15:10:55.739] [bmv2] [D] [thread 16129] Adding interface veth12 as port 6 Adding interface veth14 as port 7 [15:10:55.791] [bmv2] [D] [thread 16129] Adding interface veth14 as port 7 Adding interface veth16 as port 8 [15:10:55.839] [bmv2] [D] [thread 16129] Adding interface veth16 as port 8 [15:10:55.879] [bmv2] [I] [thread 16129] Starting Thrift server on port 9090 [15:10:55.880] [bmv2] [I] [thread 16129] Thrift server was started [15:11:00.449] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Processing packet received on port 1 [15:11:00.449] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Parser 'parser': start [15:11:00.449] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Parser 'parser' entering state 'start' [15:11:00.449] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Parser state 'start': key is 0001 [15:11:00.449] [bmv2] [T] [thread 16135] [0.0] [cxt 0] Bytes parsed: 0 [15:11:00.449] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Parser 'parser': end [15:11:00.449] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Pipeline 'ingress': start [15:11:00.450] [bmv2] [T] [thread 16135] [0.0] [cxt 0] ./p4src/my_program.p4(75) Condition "std_md.ingress_port == 4" (node_2) is false [15:11:00.450] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Pipeline 'ingress': end [15:11:00.450] [bmv2] [D] [thread 16135] [0.0] [cxt 0] Egress port is 0 [15:11:00.450] [bmv2] [D] [thread 16136] [0.0] [cxt 0] Pipeline 'egress': start [15:11:00.450] [bmv2] [D] [thread 16136] [0.0] [cxt 0] Pipeline 'egress': end [15:11:00.450] [bmv2] [D] [thread 16136] [0.0] [cxt 0] Deparser 'deparser': start [15:11:00.450] [bmv2] [D] [thread 16136] [0.0] [cxt 0] Deparser 'deparser': end [15:11:00.450] [bmv2] [D] [thread 16140] [0.0] [cxt 0] Transmitting packet of size 28 out of port 0
So in that case we see that line: "Egress port is 0", which is a special port number that designate the null0 interace. (packet dropped)
Let's now resent a packet to PORT_4 and observe simple_switch console log.
sudo simple_switch --log-console -i 1@veth2 -i 2@veth4 -i 3@veth6 -i 4@veth8 -i 5@veth10 -i 6@veth12 -i 7@veth14 -i 8@veth16 ./build/my_program.json Calling target program-options parser [15:14:51.047] [bmv2] [D] [thread 16151] Set default default entry for table 'tbl_my_program76': my_program76 - Adding interface veth2 as port 1 [15:14:51.048] [bmv2] [D] [thread 16151] Adding interface veth2 as port 1 Adding interface veth4 as port 2 [15:14:51.099] [bmv2] [D] [thread 16151] Adding interface veth4 as port 2 Adding interface veth6 as port 3 [15:14:51.139] [bmv2] [D] [thread 16151] Adding interface veth6 as port 3 Adding interface veth8 as port 4 [15:14:51.175] [bmv2] [D] [thread 16151] Adding interface veth8 as port 4 Adding interface veth10 as port 5 [15:14:51.207] [bmv2] [D] [thread 16151] Adding interface veth10 as port 5 Adding interface veth12 as port 6 [15:14:51.239] [bmv2] [D] [thread 16151] Adding interface veth12 as port 6 Adding interface veth14 as port 7 [15:14:51.271] [bmv2] [D] [thread 16151] Adding interface veth14 as port 7 Adding interface veth16 as port 8 [15:14:51.319] [bmv2] [D] [thread 16151] Adding interface veth16 as port 8 [15:14:51.347] [bmv2] [I] [thread 16151] Starting Thrift server on port 9090 [15:14:51.348] [bmv2] [I] [thread 16151] Thrift server was started [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Processing packet received on port 4 [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser 'parser': start [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser 'parser' entering state 'start' [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser state 'start': key is 0004 [15:14:58.053] [bmv2] [T] [thread 16158] [0.0] [cxt 0] Bytes parsed: 0 [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser 'parser' entering state 'prs_port_4' [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser set: setting field 'scalars.userMetadata.nexthop_id' to 8 [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser state 'prs_port_4' has no switch, going to default next state [15:14:58.053] [bmv2] [T] [thread 16158] [0.0] [cxt 0] Bytes parsed: 0 [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Parser 'parser': end [15:14:58.053] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Pipeline 'ingress': start [15:14:58.054] [bmv2] [T] [thread 16158] [0.0] [cxt 0] ./p4src/my_program.p4(75) Condition "std_md.ingress_port == 4" (node_2) is true [15:14:58.054] [bmv2] [T] [thread 16158] [0.0] [cxt 0] Applying table 'tbl_my_program76' [15:14:58.054] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Looking up key: [15:14:58.054] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Table 'tbl_my_program76': miss [15:14:58.054] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Action entry is my_program76 - [15:14:58.054] [bmv2] [T] [thread 16158] [0.0] [cxt 0] Action my_program76 [15:14:58.054] [bmv2] [T] [thread 16158] [0.0] [cxt 0] ./p4src/my_program.p4(76) Primitive std_md.egress_spec = md.nexthop_id [15:14:58.054] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Pipeline 'ingress': end [15:14:58.054] [bmv2] [D] [thread 16158] [0.0] [cxt 0] Egress port is 8 [15:14:58.054] [bmv2] [D] [thread 16159] [0.0] [cxt 0] Pipeline 'egress': start [15:14:58.054] [bmv2] [D] [thread 16159] [0.0] [cxt 0] Pipeline 'egress': end [15:14:58.054] [bmv2] [D] [thread 16159] [0.0] [cxt 0] Deparser 'deparser': start [15:14:58.054] [bmv2] [D] [thread 16159] [0.0] [cxt 0] Deparser 'deparser': end [15:14:58.054] [bmv2] [D] [thread 16163] [0.0] [cxt 0] Transmitting packet of size 28 out of port 8
We clearly confirmed what tcpdump what putting in evidence: ingress PORT_4 leads to a packet switched to PORT_8
Conclusion
In this article you:
- wrote your first P4 program
- use p4c in order to compile it
- learned how to instantiate virtual ethernet pair in order to bind them with simple_switch
- launch simple_switch and load your program on it
- set up a test environment using scapy
- and verify your program using a combination a scapy and tcpdump
P4Lang P4 for dummy [ #002 ] - key take-away
- my_program.p4 is written following V1Model definition that defines:
- a parsing stage
- a checksum verification stage
- an ingress packet processing control stage
- an egress packet processing control stage
- a checksum computation stage
- deparser stages
V1Switch( prs_main(), ctl_verify_checksum(), ctl_ingress(), ctl_egress(), ctl_compute_checksum(), ctl_deprs() ) main;
It is described by the diagram below:
In a subsequent article we will dissect my_program.p4, but as you could observe, P4 programming is quite intuitive as it is all about switching a packet based on intrinsic ingress packet header and metadata (like packet ingress port) value.