The P4 language - Overview of P4 programming

Jan 11, 2024 by Nathan Camiola | 557 views

P4

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.

A bit of P4 programming

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:

  • The keywork in indicates that the parameter is an input,
  • the keyword out indicates that the parameter is an output,
  • the keyword 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].

Headers

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;
}

Programmable parser

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 :

parser.png

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.

Programmable Control Block (match-action pipeline)

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].

Programmable Deparser

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);
    }
}

Conclusion

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.


References

[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