5. Example: A very simple switch
As an example to illustrate the features of architectures, consider implementing a very simple switch in P4. We will first describe the architecture of the switch and then write a complete P4 program that specifies the data plane behavior of the switch. This example demonstrates many important features of the P4 programming language.
\~ Figure { #fig-vssarch; caption: “The Very Simple Switch (VSS) architecture.” } [vssarch] \~ [vssarch]: figs/vssarch.png { width: 100%; page-align: here }
[]{tex-cmd: “”} We call our architecture the “Very Simple Switch” (VSS). Figure [#fig-vssarch] is a diagram of this architecture. There is nothing inherently special about VSS—it is just a pedagogical example that illustrates how programmable switches can be described and programmed in P4. VSS has a number of fixed-function blocks (shown in cyan in our example), whose behavior is described in Section [#sec_vssarch]. The white blocks are programmable using P4.
VSS receives packets through one of 8 input Ethernet ports, through a recirculation channel, or from a port connected directly to the CPU. VSS has one single parser, feeding into a single match-action pipeline, which feeds into a single deparser. After exiting the deparser, packets are emitted through one of 8 output Ethernet ports or one of 3 “special” ports:
- Packets sent to the “CPU port” are sent to the control plane
- Packets sent to the “Drop port” are discarded
- Packets sent to the “Recirculate port” are re-injected in the switch through a special input port
The white blocks in the figure are programmable, and the user must provide a corresponding P4 program to specify the behavior of each such block. The red arrows indicate the flow of user-defined data. The cyan blocks are fixed-function components. The green arrows are data plane interfaces used to convey information between the fixed-function blocks and the programmable blocks—exposed in the P4 program as intrinsic metadata.
The following P4 program provides a declaration of VSS in P4, as it would be provided by the VSS manufacturer. The declaration contains several type declarations, constants, and finally declarations for the three programmable blocks; the code uses syntax highlighting. The programmable blocks are described by their types; the implementation of these blocks has to be provided by the switch programmer.
\~ Begin P4Example // File “very_simple_switch_model.p4” // Very
Simple Switch P4 declaration // core library needed for packet_in and
packet_out definitions # include \<core.p4> /* Various constants and
structure declarations / / ports are represented using 4-bit values /
typedef bit\<4> PortId; / only 8 ports are “real” / const PortId
REAL_PORT_COUNT = 4w8; // 4w8 is the number 8 in 4 bits / metadata
accompanying an input packet / struct InControl { PortId inputPort; }
/ special input port values / const PortId RECIRCULATE_IN_PORT =
0xD; const PortId CPU_IN_PORT = 0xE; / metadata that must be computed
for outgoing packets / struct OutControl { PortId outputPort; } /
special output port values for outgoing packet / const PortId
DROP_PORT = 0xF; const PortId CPU_OUT_PORT = 0xE; const PortId
RECIRCULATE_OUT_PORT = 0xD; / Prototypes for all programmable blocks
*/ /** * Programmable parser. * @param
-
The included file
core.p4is described in more detail in Appendix [#sec-p4-core-lib]. It defines some standard data-types and error codes. -
bit<4>is the type of bit-strings with 4 bits. -
The syntax
4w0xFindicates the value 15 represented using 4 bits. An alternative notation is4w15. In many circumstances the width modifier can be omitted, writing just15. -
erroris a built-in P4 type for holding error codes -
- Next follows the declaration of a parser:
Begin P4Example parser Parser(packet_in b, out H parsedHeaders); End P4Example
This declaration describes the interface for a parser, but not yet its implementation, which will be provided by the programmer. The parser reads its input from apacket_in, which is a pre-defined P4 extern object that represents an incoming packet, declared in thecore.p4library. The parser writes its output (theoutkeyword) into theparsedHeadersargument. The type of this argument isH, yet unknown—it will also be provided by the programmer.
- Next follows the declaration of a parser:
-
- The declaration
Begin P4Example control Pipe(inout H headers, in error parseError, in InControl inCtrl, out OutControl outCtrl); End P4Example
describes the interface of a Match-Action pipeline namedPipe.
- The declaration
The pipeline receives three inputs: the headers headers, a parser
error parseError, and the inCtrl control data. Figure
[#fig-vssarch] indicates the different sources of these pieces of
information. The pipeline writes its outputs into outCtrl, and it must
update in place the headers to be consumed by the deparser.
- The top-level package is called
VSS; in order to program a VSS, the user will have to instantiate a package of this type (shown in the next section). The top-level package declaration also depends on a type variable H: \~ Begin P4Example package VSS\~ End P4Example
A type variable indicates a type yet unknown that must be provided by
the user at a later time. In this case H is the type of the set of
headers that the user program will be processing; the parser will
produce the parsed representation of these headers, and the match-action
pipeline will update the input headers in place to produce the output
headers.
- The
package VSSdeclaration has three complex parameters, of typesParser,Pipe, andDeparserrespectively; which are exactly the declarations we have just described. In order to program the target one has to supply values for these parameters. - In this program the
inCtrlandoutCtrlstructures represent control registers. The content of the headers structure is stored in general-purpose registers. - The
extern Checksum16declaration describes an extern object whose services can be invoked to compute checksums.
In order to fully understand VSS’s behavior and write meaningful P4
programs for it, and for implementing a control plane, we also need a
full behavioral description of the fixed-function blocks. This section
can be seen as a simple example illustrating all the details that have
to be handled when writing an architecture description. The P4 language
is not intended to cover the description of all such functional
blocks—the language can only describe the interfaces between
programmable blocks and the architecture. For the current program, this
interface is given by the Parser, Pipe, and Deparser declarations.
In practice we expect that the complete description of the architecture
will be provided as an executable program and/or diagrams and text; in
this document we will provide informal descriptions in English.
Arbiter block
The input arbiter block performs the following functions:
- It receives packets from one of the physical input Ethernet ports, from the control plane, or from the input recirculation port.
- For packets received from Ethernet ports, the block computes the Ethernet trailer checksum and verifies it. If the checksum does not match, the packet is discarded. If the checksum does match, it is removed from the packet payload.
- Receiving a packet involves running an arbitration algorithm if multiple packets are available.
- If the arbiter block is busy processing a previous packet and no queue space is available, input ports may drop arriving packets, without indicating the fact that the packets were dropped in any way.
- After receiving a packet, the arbiter block sets the
inCtrl.inputPortvalue that is an input to the match-action pipeline with the identity of the input port where the packet originated. Physical Ethernet ports are numbered 0 to 7, while the input recirculation port has a number 13 and the CPU port has the number 14.
Parser runtime block
The parser runtime block works in concert with the parser. It provides an error code to the match-action pipeline, based on the parser actions, and it provides information about the packet payload (e.g., the size of the remaining payload data) to the demux block. As soon as a packet’s processing is completed by the parser, the match-action pipeline is invoked with the associated metadata as inputs (packet headers and user-defined metadata).
Demux block
The core functionality of the demux block is to receive the headers for
the outgoing packet from the deparser and the packet payload from the
parser, to assemble them into a new packet and to send the result to the
correct output port. The output port is specified by the value of
outCtrl.ouputPort, which is set by the match-action pipeline.
- Sending the packet to the drop port causes the packet to disappear.
- Sending the packet to an output Ethernet port numbered between 0 and 7 causes it to be emitted on the corresponding physical interface. The packet may be placed in a queue if the output interface is already busy emitting another packet. When the packet is emitted, the physical interface computes a correct Ethernet checksum trailer and appends it to the packet.
- Sending a packet to the output CPU port causes the packet to be transferred to the control plane. In this case, the packet that is sent to the CPU is the original input packet, and not the packet received from the deparser—the latter packet is discarded.
- Sending the packet to the output recirculation port causes it to appear at the input recirculation port. Recirculation is useful when packet processing cannot be completed in a single pass.
- If the
outputPorthas an illegal value (e.g., 9), the packet is dropped. - Finally, if the demux unit is busy processing a previous packet and there is no capacity to queue the packet coming from the deparser, the demux unit may drop the packet, irrespective of the output port indicated.
Please note that some of the behaviors of the demux block may be unexpected—we have highlighted them in bold. We are not specifying here several important behaviors related to queue size, arbitration, and timing, which also influence the packet processing.
The arrow shown from the parser runtime to the demux block represents an additional information flow from the parser to the demux: the packet being processed as well as the offset within the packet where parsing ended (i.e., the start of the packet payload).
Available extern blocks
The VSS architecture provides an incremental checksum extern block,
called Checksum16. The checksum unit has a constructor and four
methods:
clear(): prepares the unit for a new computationupdate<T>(in T data): add some data to be checksummed. The data must be either a bit-string, a header-typed value, or astructcontaining such values. The fields in the header/struct are concatenated in the order they appear in the type declaration.get(): returns the 16-bit one’s complement checksum. When this function is invoked the checksum must have received an integral number of bytes of data.remove<T>(in T data): assuming thatdatawas used for computing the current checksum,datais removed from the checksum.
Here we provide a complete P4 program that implements basic forwarding
for IPv4 packets on the VSS architecture. This program does not utilize
all of the features provided by the architecture—e.g., recirculation—but
it does use preprocessor #include directives (see Section
[#sec-preprocessor]).
\~ Figure { #fig-vssmau; caption: “Diagram of the match-action pipeline expressed by the VSS P4 program.” } [vssmau] \~ [vssmau]: figs/vssmau.png { width: 100%; page-align: here }
[]{tex-cmd: “”} The parser attempts to recognize an Ethernet header
followed by an IPv4 header. If either of these headers are missing,
parsing terminates with an error. Otherwise it extracts the information
from these headers into a Parsed_packet structure. The match-action
pipeline is shown in Figure [#fig-vssmau]; it comprises four
match-action units (represented by the P4 table keyword):
- If any parser error has occurred, the packet is dropped (i.e., by
assigning
outputPorttoDROP_PORT) - The first table uses the IPv4 destination address to determine the
outputPortand the IPv4 address of the next hop. If this lookup fails, the packet is dropped. The table also decrements the IPv4ttlvalue. - The second table checks the
ttlvalue: if thettlbecomes 0, the packet is sent to the control plane through the CPU port. - The third table uses the IPv4 address of the next hop (which was computed by the first table) to determine the Ethernet address of the next hop.
- Finally, the last table uses the
outputPortto identify the source Ethernet address of the current switch, which is set in the outgoing packet.
The deparser constructs the outgoing packet by reassembling the Ethernet and IPv4 headers as computed by the pipeline.
\~ Begin P4Example // Include P4 core library # include \<core.p4>
// Include very simple switch architecture declarations # include “very_simple_switch_model.p4”
// This program processes packets comprising an Ethernet and an IPv4 // header, and it forwards packets using the destination IP address
typedef bit\<48> EthernetAddress; typedef bit\<32> IPv4Address;
// Standard Ethernet header header Ethernet_h { EthernetAddress dstAddr; EthernetAddress srcAddr; bit\<16> etherType; }
// IPv4 header (without options) header IPv4_h { 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; IPv4Address srcAddr; IPv4Address dstAddr; }
// Structure of parsed headers struct Parsed_packet { Ethernet_h ethernet; IPv4_h ip; }
// Parser section
// User-defined errors that may be signaled during parsing error { IPv4OptionsNotSupported, IPv4IncorrectVersion, IPv4ChecksumError }
parser TopParser(packet_in b, out Parsed_packet p) { Checksum16() ck; // instantiate checksum unit
state start {
b.extract(p.ethernet);
transition select(p.ethernet.etherType) {
0x0800: parse_ipv4;
// no default rule: all other packets rejected
}
}
state parse_ipv4 {
b.extract(p.ip);
verify(p.ip.version == 4w4, error.IPv4IncorrectVersion);
verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported);
ck.clear();
ck.update(p.ip);
// Verify that packet checksum is zero
verify(ck.get() == 16w0, error.IPv4ChecksumError);
transition accept;
}
}
// Match-action pipeline section
control TopPipe(inout Parsed_packet headers, in error parseError, // parser error in InControl inCtrl, // input port out OutControl outCtrl) { IPv4Address nextHop; // local variable
/**
* Indicates that a packet is dropped by setting the
* output port to the DROP_PORT
*/
action Drop_action() {
outCtrl.outputPort = DROP_PORT;
}
/**
* Set the next hop and the output port.
* Decrements ipv4 ttl field.
* @param ipv4_dest ipv4 address of next hop
* @param port output port
*/
action Set_nhop(IPv4Address ipv4_dest, PortId port) {
nextHop = ipv4_dest;
headers.ip.ttl = headers.ip.ttl - 1;
outCtrl.outputPort = port;
}
/**
* Computes address of next IPv4 hop and output port
* based on the IPv4 destination of the current packet.
* Decrements packet IPv4 TTL.
* @param nextHop IPv4 address of next hop
*/
table ipv4_match {
key = { headers.ip.dstAddr: lpm; } // longest-prefix match
actions = {
Drop_action;
Set_nhop;
}
size = 1024;
default_action = Drop_action;
}
/**
* Send the packet to the CPU port
*/
action Send_to_cpu() {
outCtrl.outputPort = CPU_OUT_PORT;
}
/**
* Check packet TTL and send to CPU if expired.
*/
table check_ttl {
key = { headers.ip.ttl: exact; }
actions = { Send_to_cpu; NoAction; }
const default_action = NoAction; // defined in core.p4
}
/**
* Set the destination MAC address of the packet
* @param dmac destination MAC address.
*/
action Set_dmac(EthernetAddress dmac) {
headers.ethernet.dstAddr = dmac;
}
/**
* Set the destination Ethernet address of the packet
* based on the next hop IP address.
* @param nextHop IPv4 address of next hop.
*/
table dmac {
key = { nextHop: exact; }
actions = {
Drop_action;
Set_dmac;
}
size = 1024;
default_action = Drop_action;
}
/**
* Set the source MAC address.
* @param smac: source MAC address to use
*/
action Set_smac(EthernetAddress smac) {
headers.ethernet.srcAddr = smac;
}
/**
* Set the source mac address based on the output port.
*/
table smac {
key = { outCtrl.outputPort: exact; }
actions = {
Drop_action;
Set_smac;
}
size = 16;
default_action = Drop_action;
}
apply {
if (parseError != error.NoError) {
Drop_action(); // invoke drop directly
return;
}
ipv4_match.apply(); // Match result will go into nextHop
if (outCtrl.outputPort == DROP_PORT) return;
check_ttl.apply();
if (outCtrl.outputPort == CPU_OUT_PORT) return;
dmac.apply();
if (outCtrl.outputPort == DROP_PORT) return;
smac.apply();
}
}
// deparser section control TopDeparser(inout Parsed_packet p, packet_out b) { Checksum16() ck; apply { b.emit(p.ethernet); if (p.ip.isValid()) { ck.clear(); // prepare checksum unit p.ip.hdrChecksum = 16w0; // clear checksum ck.update(p.ip); // compute new checksum. p.ip.hdrChecksum = ck.get(); } b.emit(p.ip); } }
// Instantiate the top-level VSS package VSS(TopParser(), TopPipe(), TopDeparser()) main; \~ End P4Example