CPU punt¶
One host, one switch, every dataplane packet punted to the controller
via the CPU port (510). The controller can also inject packets via
PacketOut with explicit egress port metadata.
What you'll see¶
s1 packet listen displays incoming punts in real time as the host
emits ARP / IPv6 ND / ICMP traffic. s1 packet send injects a
controller-built frame back into the dataplane.
Topology¶
examples/cpu_punt/topology.py:
"""One host, one switch, all dataplane traffic punted to CPU.
Run with:
sudo p4net examples/cpu_punt/topology.py
Then in the shell:
h1 cmd ping -c 3 -W 1 10.0.0.99 # generates ARP traffic
s1 packet listen count=3 timeout=5 # observe punted packets
Or send a packet from controller to host 1:
s1 packet send ffffffffffff000000000001880b48656c6c6f \\
metadata: egress_port=1
"""
from __future__ import annotations
from pathlib import Path
from p4net.topo import Topology
HERE = Path(__file__).resolve().parent
topology = Topology()
h1 = topology.add_host("h1", ip="10.0.0.1/24", mac="00:00:00:00:00:01")
s1 = topology.add_switch(
"s1",
p4_src=HERE / "cpu_punt.p4",
cpu_port=510,
)
topology.add_link(h1, s1, port_b=1)
if __name__ == "__main__":
from p4net.cli.main import main
raise SystemExit(main([__file__]))
cpu_port=510 on the switch is what wires the CPU port to BMv2.
P4 program¶
examples/cpu_punt/cpu_punt.p4:
/* CPU-punt demo pipeline.
*
* Every dataplane packet is punted to the controller (via the CPU port).
* Packets injected from the controller carry a `packet_out` header that
* names the desired egress port; the ingress control copies that into
* `std.egress_spec` and invalidates the header before the packet is
* deparsed onto the wire.
*
* Pairs with `examples/cpu_punt/topology.py`, which sets `cpu_port=510`
* on the BMv2 switch.
*/
#include <core.p4>
#include <v1model.p4>
const bit<9> CPU_PORT = 510;
@controller_header("packet_in")
header packet_in_t {
bit<9> ingress_port;
bit<7> _pad0;
}
@controller_header("packet_out")
header packet_out_t {
bit<9> egress_port;
bit<7> _pad0;
}
header ethernet_t {
bit<48> dstAddr;
bit<48> srcAddr;
bit<16> etherType;
}
struct headers {
packet_in_t packet_in;
packet_out_t packet_out;
ethernet_t ethernet;
}
struct metadata {}
parser MyParser(packet_in pkt, out headers hdr, inout metadata meta,
inout standard_metadata_t std) {
state start {
transition select(std.ingress_port) {
CPU_PORT: parse_packet_out;
default: parse_ethernet;
}
}
state parse_packet_out {
pkt.extract(hdr.packet_out);
transition parse_ethernet;
}
state parse_ethernet {
pkt.extract(hdr.ethernet);
transition accept;
}
}
control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyIngress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) {
apply {
if (std.ingress_port == CPU_PORT) {
// Controller-injected packet: forward as instructed and strip
// the controller header before deparsing.
std.egress_spec = hdr.packet_out.egress_port;
hdr.packet_out.setInvalid();
} else {
// Dataplane packet: punt to controller; stamp ingress_port.
std.egress_spec = CPU_PORT;
hdr.packet_in.setValid();
hdr.packet_in.ingress_port = std.ingress_port;
hdr.packet_in._pad0 = 0;
}
}
}
control MyEgress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) { apply {} }
control MyComputeChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyDeparser(packet_out pkt, in headers hdr) {
apply {
pkt.emit(hdr.packet_in);
pkt.emit(hdr.ethernet);
}
}
V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(),
MyComputeChecksum(), MyDeparser()) main;
Two @controller_header declarations define the metadata layout for
PacketIn (controller-bound) and PacketOut (controller-injected). The
parser discriminates on std.ingress_port == CPU_PORT to extract
the packet_out header before ethernet. The ingress control:
- For controller-injected packets: copies
egress_portintostd.egress_spec, invalidates the controller header. - For dataplane packets: sets
std.egress_spec = CPU_PORT, validates thepacket_inheader, stampsingress_port.
Run it¶
In the shell:
p4net> s1 packet listen count=3 timeout=5
[ingress_port=1] 333300000016000000000001...
[ingress_port=1] 333300000016000000000001...
[ingress_port=1] ff02000000000000000000010002...
The [ingress_port=1] prefix is the decoded packet_in controller
header. The hex payload is truncated at 64 chars in the CLI; full
payload is available via client.expect_packet_in() from Python.
To inject a frame from the controller toward h1:
What's interesting¶
- The BPF filter trick. When the integration tests want to
verify a controller-injected frame arrives at
h1, they spawntcpdump -i h1-eth0 -c 1 ether proto 0x88B5rather thantcpdump -c 1. Without the filter, IPv6 ND noise consumes the count-1 slot before the test frame arrives. The example uses ethertype0x88B(a local-experimental EtherType) for exactly this reason. - Auto-zero-pad missing metadata.
encode_packet_out_metadataiterates every metadata field declared in the P4Info and falls back tometadata.get(name, 0)for missing keys, so_pad0doesn't need to be specified by the caller.
Variations to try¶
- Register a Python handler with
s1.client.on_packet_in(handler)and parse the punted Ethernet frame to drive a learning switch. - Use
s1.client.send_packet_out(payload, {"egress_port": 1})from a setup script to inject a sequence of probe frames. - Add an
ipv4_lpmtable whosedefault_action = punt()to mix programmed forwarding with controller fallback.