The ONOS+P4 tutorial (materials from the Open Networking Foundation) sets a concrete goal: acquire enough knowledge to build full-stack network applications — to go from a P4 idea to an end-to-end solution. The full stack is four layers, and the whole chapter is about the interfaces between them:
The bottom is familiar: a P4 pipeline is a pipeline of match-action tables (chapter 6), realised on an ASIC, FPGA, NPU or CPU. What is new here is everything above the switch.
The tutorial asks the obvious question — can't we reuse OpenFlow or SAI? — and answers it with a table that is the clearest single justification for P4Runtime. Three kinds of independence are wanted, and only P4Runtime has all three:
| Target-independent | Protocol-independent | Pipeline-independent | |
|---|---|---|---|
| OpenFlow | yes | no — headers and actions are hard-coded in the spec | yes, only with TTP |
| Switch Abstraction Interface (SAI) | yes | no — designed for legacy L2/L3/ACL pipelines | no — implicit fixed-function pipeline |
| P4Runtime | yes | yes | yes, with P4 |
The three independences read as: the same API works with different switches from different vendors (target); the same API controls any data-plane protocol, standard or custom (protocol); the same API controls many arbitrary pipelines, formally specified (pipeline). The properties of P4Runtime that deliver this: it is designed around the PSA architecture but extensible to others; it is community-developed (initial contribution by Google and Barefoot); it is gRPC/protobuf-based; it is P4 program-independent — the API does not change with the P4 program — and it enables field reconfigurability, the ability to push a new P4 program and re-configure the pipeline without recompiling the switch software stack.
Read this table against chapter 3's OpenFlow field-count table. OpenFlow's "no" under protocol-independence is precisely the 12-to-41-fields problem: the protocol is baked into the specification. P4Runtime moves the protocol definition into the P4 program and out of the API — which is why the same API survives a program change.
P4Runtime is built on two Google technologies, and the tutorial teaches just enough of each.
protobuf is a language for describing data for serialization in a structured way: strongly typed, platform-neutral, extensible and backwards compatible, with code generators for a long list of languages (C, C++, Java, Python, Go, Rust, …). You describe a message once and auto-generate the code to serialize and deserialize it.
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
gRPC uses protobuf to define a service API and its messages, and auto-generates native client and server code in many languages. It transports over HTTP/2.0 and TLS, an efficient single-TCP-connection implementation that supports bidirectional streaming. That streaming is what lets a controller and a switch exchange packet-in / packet-out and notifications over one channel.
The P4Runtime service itself, p4runtime.proto, is a gRPC service that lets a local or remote control plane load the pipeline, read and write pipeline state, exchange packets, and arbitrate mastership for distributed control planes:
service P4Runtime {
rpc Write(WriteRequest) returns (WriteResponse) {}
rpc Read(ReadRequest) returns (stream ReadResponse) {}
rpc SetForwardingPipelineConfig(...) returns (...) {}
rpc GetForwardingPipelineConfig(...) returns (...) {}
rpc StreamChannel(stream StreamMessageRequest)
returns (stream StreamMessageResponse) {}
}
Installing a rule means sending a WriteRequest containing one or more Updates, each an INSERT, MODIFY or DELETE of an Entity. The entity can be many things — a table entry, an action-profile member or group, a meter entry, a counter entry, a register entry, a packet-replication-engine entry — which is the API being pipeline-independent in one message definition.
message Update {
enum Type { UNSPECIFIED = 0; INSERT = 1; MODIFY = 2; DELETE = 3; }
Type type = 1;
Entity entity = 2;
}
To add a table entry the control plane must know three kinds of thing, and this list is the reason P4Info (next section) has to exist:
A logical table entry such as hdr.ipv4.dstAddr = 10.0.1.1/32 -> ipv4_forward(00:00:00:00:00:10, 7) becomes a protobuf TableEntry whose fields are all numeric IDs and raw bytes:
table_entry {
table_id: 33581985
match {
field_id: 1
lpm { value: "\n\000\001\001" prefix_len: 32 }
}
action {
action_id: 16786453
params { param_id: 1 value: "\000\000\000\000\000\n" }
params { param_id: 2 value: "\000\007" }
}
}
Notice that the TableEntry above contained no names, only numbers: table_id: 33581985, action_id: 16786453. Something has to translate the human names in the P4 program into those IDs, and that something is P4Info.
The P4 compiler generates two outputs, and this pairing is the heart of the whole architecture:
| Output | Role |
|---|---|
| Target-specific binaries | Realise the switch pipeline — a binary config for an ASIC, a bitstream for an FPGA, the BMv2 JSON for the software switch. |
| P4Info file | Describes the "schema" of the pipeline for runtime control: the program's tables, actions and parameters. Protobuf-based and target-independent — the same P4Info for a software switch, an ASIC, anything. |
The P4Info entry for the ipv4_forward action and the ipv4_lpm table records exactly the names, IDs, bit widths and match types the control plane needs:
actions {
id: 16786453
name: "ipv4_forward"
params { id: 1 name: "dstAddr" bitwidth: 48 }
params { id: 2 name: "port" bitwidth: 9 }
}
tables {
id: 33581985
name: "ipv4_lpm"
match_fields { id: 1 name: "hdr.ipv4.dstAddr" bitwidth: 32 match_type: LPM }
action_ref_id: 16786453
}
State the role of P4Info in one sentence: it is the contract between the control plane and the data plane, generated by the P4 compiler, needed by the control plane to format the body of P4Runtime messages (for example to add a table entry). It maps the names in the P4 program to the numeric IDs on the wire, and it is target-independent — the same P4Info describes the program on a BMv2 switch and on a Tofino ASIC.
Pushing a whole P4 program to a switch — the field reconfigurability that distinguishes P4Runtime — is done with SetForwardingPipelineConfig. The request bundles the P4Info and the target-specific configuration bits into one ForwardingPipelineConfig, and an Action field controls the commit semantics (VERIFY, VERIFY_AND_SAVE, VERIFY_AND_COMMIT, COMMIT, RECONCILE_AND_COMMIT).
message ForwardingPipelineConfig {
config.P4Info p4info = 1;
bytes p4_device_config = 2; // target-specific P4 configuration
}
The tutorial's punchline is silicon-independent remote control: the same target-independent protobuf table entry, sent over gRPC, reaches a programmable Tofino from vendor A, a fixed-function switch from vendor B and another from vendor C — each with its own P4Runtime server and target driver, each carrying its own P4Info. One controller, many silicons, one message format.
ONOS — the Open Network Operating System — provides the control plane for a software-defined network: a logically centralised remote controller, with APIs that make it easy to write apps to control a network. It focuses on service-provider access/edge applications, and it runs as a distributed system across many servers for scalability, high availability and performance. It is open source (created by ON.Lab, hosted by the Linux Foundation). P4Runtime support first arrived in the Loon (1.11.0) release, August 2017.
Its architecture is layered, and the layering is what lets one app control OpenFlow and P4Runtime devices alike:
ONOS interacts with devices through behaviors — Java interfaces — bundled into drivers. Named behaviors include DeviceDescriptionDiscovery (read device info and ports), FlowRuleProgrammable (write/read flow rules), PortStatisticsDiscovery (port packet/byte counters) and Pipeliner (FlowObjective-to-FlowRules mapping). A driver is a collection of behavior implementations that use ONOS's protocol libraries to reach the device — and the key takeaway is that apps are independent of the switch control protocol: the same app can work with OpenFlow and P4Runtime devices.
ONOS offers two network-programming APIs, and choosing between them is choosing between control and portability:
| FlowRule API | FlowObjective API |
|---|---|
| Pipeline-dependent. You write a flow rule for a specific pipeline; you get precise control, but the rule is tied to how that pipeline is structured. | Pipeline-independent. You state an objective; drivers translate one FlowObjective into many FlowRules via the pipeline's Pipeliner. The same objective works on switches with different pipelines — including switches running different P4 programs. |
The FlowObjective API is what enables application portability: an app using FlowObjectives can work with switches whose pipelines differ. The mechanism is a three-phase translation, and it is worth knowing as three concrete rewrites:
Pipeliner).ETH_DST → hdr.ethernet.dst_addr (the Pipeline Interpreter).Step 2 exposes a genuine impedance mismatch the tutorial flags: P4 allows only one action per table entry, while ONOS (like OpenFlow) allows many. A rule that rewrites a header and outputs on a port is two actions in ONOS but must become one P4 action with two parameters. Resolving that needs interpretation logic — actual Java code in the Pipeline Interpreter — which is why "bring your own pipeline" is a programming task, not a configuration file.
How does ONOS learn to control an arbitrary P4 pipeline? Through a pipeconf: a package that bundles everything ONOS needs to understand, control and deploy a pipeline, provided to ONOS as an app (distributable as an .oar binary). A pipeconf has three parts:
| Part | What it is |
|---|---|
| 1. Pipeline model | The description of the pipeline understood by ONOS, automatically derived from P4Info. |
| 2. Target-specific binaries | To deploy the pipeline to the device — BMv2 JSON, Tofino binary, FPGA bitstream. |
| 3. Pipeline-specific driver behaviors | The Pipeliner (map FlowObjectives to the P4 pipeline) and the Pipeline Interpreter (map ONOS known headers/actions to P4 program entities). |
The workflow recap ties the whole chapter together: write the P4 program and compile it (obtaining P4Info and target binaries); create the pipeconf (implement the pipeliner and interpreter behaviors in Java); then either use existing pipeline-agnostic apps that program the network with FlowObjectives, or write new pipeline-aware apps that use the same string names of tables, headers and actions as in the P4 program.
A note of realism from the ONOS 1.14 (Owl) support matrix: not everything was supported. Table entries, packet-in/out, action profiles, meters (indirect) and counters mapped to ONOS services, but parser value sets, registers, digests and clone sessions were listed as unsupported, "community help needed" — a useful reminder that the stack is real software with real gaps.
The CCNC tutorial guide walks through a concrete P4Runtime controller application (a Go P4Runtime client) that combines the data plane and a machine-learning back end. It is the clearest demonstration in the course of why you would push work into the data plane at all.
The setup runs a data plane, a controller, and a CNN (a convolutional neural network detector, "lucid"), then launches an attack from the CIC2017 dataset — which contains a single malicious flow, attacker 172.16.0.1, victim 192.168.10.50. The result is inspected at localhost:3333/ddos.
| Exercise 1a — no pre-filtering | Exercise 1b — count-min pre-filtering in P4 |
|---|---|
| Despite there being only a few genuinely suspect flows, an unexpectedly high number of flows are deemed suspect or malicious. The cause named in the guide: the traffic is not pre-filtered on the data plane, so noise reaches the detector. | A count-min-filtered P4 program (p4_packet_management_countmin_filtered.p4) filters on the data plane first. With the pre-filtering stage the number of false positives drops drastically. |
This is the whole thesis of the programmable data plane made measurable. The neural network did not get smarter between 1a and 1b; the data plane got a count-min sketch that shed noise at line rate before it ever reached the detector. Moving a little state into the switch (a sketch — a register array, chapter 7) improved an ML pipeline's accuracy. Speed and visibility in the data plane translate directly into quality upstream.
The second exercise (simple_digest.p4) sends aggregated data from the data plane to the control plane: the details of 1 out of every 10 packets are sent to the controller — here just the source IP and port and the destination IP and port, though more could be included. The guide names it precisely: this is the building component that makes the interaction between the data plane and the CNN possible. A digest is P4's answer to chapter 6's limitation "pure P4 cannot communicate to the control plane" — an external object, a learning provider, that samples and reports.
Your app (SDN apps) on top, ONOS (SDN control plane), P4Runtime (runtime control), and your P4 program on the P4 switch at the bottom. Chapters 6–7 built the bottom layer; this chapter builds the three interfaces above it.
Because it is target-, protocol- and pipeline-independent all at once. OpenFlow is not protocol-independent (headers and actions are hard-coded in the spec); SAI is neither protocol- nor pipeline-independent (legacy L2/L3/ACL, implicit fixed pipeline). P4Runtime achieves all three, with pipeline-independence "with P4".
protobuf — a strongly-typed, platform-neutral language for describing serializable messages, with multi-language code generation. gRPC — uses protobuf to define the service API, auto-generates client/server code, and transports over HTTP/2 and TLS with bidirectional streaming (used for packet-in/out and notifications).
The IDs of the P4 entities (tables, field matches, actions, parameters); the field matches for the particular table (match type, bit width); and the parameters for the particular action. All of these come from the P4Info file.
P4Info is one of the two P4-compiler outputs (the other being target-specific binaries). It is a protobuf describing the pipeline schema — tables, actions, parameters, with names, IDs, bit widths and match types — and is the contract the control plane uses to format P4Runtime messages. It is target-independent because it describes the program, not the hardware: the same P4Info applies whether the program runs on BMv2 or a Tofino ASIC.
It pushes a whole P4 program to a switch — the field-reconfigurability feature. The request carries a ForwardingPipelineConfig bundling the P4Info and the target-specific config bits, plus an Action controlling commit semantics (VERIFY, VERIFY_AND_SAVE, VERIFY_AND_COMMIT, COMMIT, RECONCILE_AND_COMMIT).
ONOS is the Open Network Operating System: a logically centralised, distributed SDN control plane with app-friendly APIs. It stays device-agnostic through a layered architecture — a device/protocol-agnostic northbound API, a distributed core with a global topology view, device drivers made of behaviors, and shared protocol libraries (OpenFlow, P4Runtime, NETCONF). The same app can control OpenFlow and P4Runtime devices.
FlowRule is pipeline-dependent: precise control, tied to a specific pipeline. FlowObjective is pipeline-independent: one objective is translated by the Pipeliner into many flow rules, so the same app works on switches with different pipelines (including different P4 programs). FlowObjective enables application portability.
Flow Objective → Flow Rule (Pipeliner, one-to-many); Flow Rule → Table Entry (Pipeline Interpreter maps standard headers/actions to P4 names, e.g. ETH_DST → hdr.ethernet.dst_addr); Table Entry → P4Runtime message (P4 names to P4Info IDs). The mismatch: P4 allows only one action per entry, while ONOS/OpenFlow allow many, so a rewrite-plus-output becomes one P4 action with two parameters — needing interpretation logic in Java.
(1) the pipeline model, automatically derived from P4Info; (2) target-specific binaries to deploy the pipeline (BMv2 JSON, Tofino binary, FPGA bitstream); (3) pipeline-specific driver behaviors — the Pipeliner and the Pipeline Interpreter. It is delivered to ONOS as an app, distributable as an .oar.
Because in exercise 1a the traffic was not pre-filtered on the data plane, so noise reached the CNN and inflated the count of suspect/malicious flows. In 1b a count-min-filtered P4 program shed that noise at line rate before it reached the detector, drastically reducing false positives — the detector was unchanged; the data plane did the cleaning.
A digest sends aggregated data from the data plane to the control plane — in the exercise, the source/destination IP and port of 1 in every 10 packets. It is the building component that makes data-plane-to-CNN interaction possible, and it is P4's external-object answer to the language limitation that pure P4 cannot communicate with the control plane.