Jan 11, 2024 by Nathan Camiola | 943 views
https://cylab.be/blog/317/the-p4-language-overview-of-p4-programming
This second blog post gives an overview of P4 programming by illustrating some of the key concepts found in most P4 programs, from header declaration to packets deparsing.
Of course, as explained in the previous blog post The basics of P4 language, the blocks and their order may vary from one architecture to another. In our case, we’re still using the V1Model architecture.
The code snippets come from the official P4 project documentation’s Github.
In order to program the different programmable blocks, and to use standard metadata
and intrinsic metadata
, each P4 program will start by importing the architecture file v1model.p4
as well as the core.p4
which defines some standard data-types and error codes, but we will come back to this later.
# include "v1model.p4"
# include "core.p4"
As a quick reminder, each of these programmable functional blocks will be clearly defined in the architecture and will be declared by keywords such as parser
or control
specifying the interfaces between the block and the other components in the architecture [2].
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
When these blocks are declared, there are also various keywords relating to their parameters: in
, out
, inout
or in_packet
and out_packet
. As their names suggest:
in
indicates that the parameter is an input,out
indicates that the parameter is an output,inout
indicates that the parameter is an input and an ouput.The other keywords come from core.p4
mentioned above :
packet_in
represents incoming packets that is processed.packet_out
represents outgoing packets that is processed.A parser should have at least one argument of type packet_in
while a deparser should have at least one argument of type packet_out
[2].
Since P4 is protocol-independent, it does not recognize any incoming header
. It is therefore necessary to describe each field with their size of the protocol header. So, each different protocol used in the program must be declared.
header ipv4_t {
bit<4> version;
bit<4> ihl;
bit<8> diffserv;
bit<16> totalLen;
bit<16> identification;
bit<3> flags;
bit<13> fragOffset;
bit<8> ttl;
bit<8> protocol;
bit<16> hdrChecksum;
bit<32> srcAddr;
bit<32> dstAddr;
}
But since it does not recognize any incoming header, it is also possible to declare custom headers which do not correspond to any existing protocol.
Here an example for a basic tunneling p4 program where a new header type is used to encapsulate the IP packet.
header myTunnel_t {
bit<16> proto_id;
bit<16> dst_id;
}
All parsers start with a state start
pointing to the first header to be extracted. Then, based on extractions, we transition
to other states.
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
state start {
transition parse_ethernet;
}
state parse_ethernet {
packet.extract(hdr.ethernet);
transition select(hdr.ethernet.etherType) {
TYPE_IPV4: parse_ipv4;
default: accept;
}
}
state parse_ipv4 {
packet.extract(hdr.ipv4);
transition accept;
}
}
The parser tries to extract
the fields included in the header definition. Extractions are performed in the same order as defined in the code. If it succeeds, it sets a validity bit for this header [2;3].
The select
keyword is used here to create an if else
condition (the if else as, we know it, cannot be used in parsers but only in the match-action pipeline) [2;3]. The condition is the etherType
; if it is equal to TYPE_IPV4
(which of course had to be declared in the code), we transition to the parse_ipv4
state until we accept
or reject
the packet.
Here a summary diagram :
We could go even further and try to extract TCP and UDP headers and so on. In this case, they would end up after the extraction of IPv4 headers since they are encapsulated in IP packets.
As a quick reminder, the match-action pipeline (blocks indicates by keyword control
) contains entries (ip address, id, …) and matches with keys. Depending on the match, several actions are possible. Note that an action
is identical to C functions [2;3].
The line hdr.ipv4.dstAddr: lpm;
try to match an entry, the IP destination address, with the Longest Prefix Match (lpm). Lpm mechanism is call a match-kind
. The basic P4 Language (core.p4) include three match-kind (exact, ternary (mask) and lpm). Other match-kind are defined by the architecture (e.g range and selector for the v1model.p4 architecture) [2;3].
In the case of a match with the lpm, the ipv4_forward
action is executed. This function performs basic ip forwarding and defines the packet fields.
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() {
mark_to_drop(standard_metadata);
}
action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) { // the action
standard_metadata.egress_spec = port;
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = dstAddr;
hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
}
table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm; // match the destination address with the lpm
}
actions = { // Actions that can be call
ipv4_forward;
drop;
NoAction;
}
size = 1024; // Max number of table entries
default_action = NoAction(); // Default action
}
apply {
if (hdr.ipv4.isValid()) { //Check the header validity bit
ipv4_lpm.apply();
}
}
}
Finally, the block must be applied. We notice that the isValid()
method is used on the ipv4 header. This method verifies the value of the validity bit by returning its value. There are other methods acting on validity bit: SetValid()
and SetInvalid()
. As their names suggest, they set the header’s validity bit to true and false respectively. These methods can only be applied to l-values.
In P4, “l-values are expressions that may appear on the left side of an assignment operation or as arguments corresponding to out and inout function parameters. An l-value represents a storage reference” [2].
Finally, by calling the emit
method, we will serialize the header only if it is valid (validity bit set to true) by inserting all the fields of the new header into a packet. As with extraction, emit writes the fields in the order defined in the code [2].
control MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet);
packet.emit(hdr.ipv4);
}
}
This concludes the second blog post on P4 Language. In this second post, we gave a brief overview of how a P4 programming works. Of course, we strongly recommend to refer to the official language specification for more details on P4 programming. Further posts on its mechanics and code will follow. In the meantime, if you’re interested, the P4 project’s Gitlab is very well supplied with practical exercises, tutorials and other code examples.
[1] p4lang, “P4lang/Tutorials: P4 language tutorials,” GitHub, https://github.com/p4lang/tutorials (accessed Dec. 18, 2023).
[2] Language specification, https://p4.org/p4-spec/docs/P4-16-v1.2.3.pdf (accessed Dec. 18, 2023).
[3] S. Laki, “Programmable Networks Lecture 2 – P4 basics & lookups” (accessed Dec. 18, 2023)
This blog post is licensed under CC BY-SA 4.0