Skip to content

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_port into std.egress_spec, invalidates the controller header.
  • For dataplane packets: sets std.egress_spec = CPU_PORT, validates the packet_in header, stamps ingress_port.

Run it

sudo p4net examples/cpu_punt/topology.py

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:

p4net> s1 packet send ffffffffffff000000000001880b48656c6c6f \
         metadata: egress_port=1
ok

What's interesting

  • The BPF filter trick. When the integration tests want to verify a controller-injected frame arrives at h1, they spawn tcpdump -i h1-eth0 -c 1 ether proto 0x88B5 rather than tcpdump -c 1. Without the filter, IPv6 ND noise consumes the count-1 slot before the test frame arrives. The example uses ethertype 0x88B (a local-experimental EtherType) for exactly this reason.
  • Auto-zero-pad missing metadata. encode_packet_out_metadata iterates every metadata field declared in the P4Info and falls back to metadata.get(name, 0) for missing keys, so _pad0 doesn'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_lpm table whose default_action = punt() to mix programmed forwarding with controller fallback.