Skip to content

Quick start (port swap)

Two hosts on a single switch with a static port-swap pipeline. No runtime table programming. The "hello world" of p4net.

What you'll see

A successful pingall between two hosts whose dataplane is a 30-line P4 program that swaps ports 1 ↔ 2 unconditionally.

Topology

examples/quick_start/quick_start.py:

"""Minimal p4net quick-start: two hosts plus one BMv2 switch.

The bundled `quick_start.p4` is a port-2-port swap (port 1 <-> port 2),
so no runtime table programming is needed for hosts on opposite ports
to reach each other. Run as root so the orchestrator can create
namespaces and veth pairs:

    sudo python examples/quick_start/quick_start.py
    sudo p4net examples/quick_start/quick_start.py

The first form is a self-contained script. The second uses the `p4net`
console script (installed by `pip install -e .`) to load this file as a
topology module: `topology` and `setup(net)` are the two named hooks
that the console script looks for.
"""

from __future__ import annotations

from pathlib import Path

from p4net import Network
from p4net.network import RunningHost
from p4net.topo import Topology

HERE = Path(__file__).resolve().parent


def _build_topology() -> Topology:
    topo = Topology()
    h1 = topo.add_host("h1", ip="10.0.0.1/24", mac="00:00:00:00:00:01")
    h2 = topo.add_host("h2", ip="10.0.0.2/24", mac="00:00:00:00:00:02")
    s1 = topo.add_switch("s1", p4_src=HERE / "quick_start.p4")
    topo.add_link(h1, s1, port_b=1)
    topo.add_link(h2, s1, port_b=2)
    return topo


# Module-level `topology` for the `p4net` console script.
topology = _build_topology()


def setup(net: Network) -> None:
    """Pre-seed static ARP for both hosts; called by the console script
    after Network.start() and before the shell.

    The same logic also runs inside this script's `__main__` block.
    """
    _add_static_arp(net.host("h1"), "10.0.0.2", "00:00:00:00:00:02")
    _add_static_arp(net.host("h2"), "10.0.0.1", "00:00:00:00:00:01")


def _add_static_arp(host: RunningHost, target_ip: str, target_mac: str) -> None:
    iface = next(iter(host.interfaces))
    host.exec(
        [
            "ip",
            "neigh",
            "replace",
            target_ip,
            "lladdr",
            target_mac,
            "dev",
            iface,
            "nud",
            "permanent",
        ]
    )


def main() -> None:
    """Same flow as `p4net <this file>` but self-contained for direct invocation."""
    with Network(topology) as net:
        setup(net)
        print("hosts:", list(net.hosts))
        print("switches:", list(net.switches))
        print("pingall:", net.pingall())


if __name__ == "__main__":
    main()

The interesting bits:

  • setup(net) is the hook the p4net console script calls between bring-up and shell. Static ARP is seeded here so the first ICMP doesn't have to resolve.
  • The same file works under python quick_start.py (the if __name__ == "__main__" block) or under p4net quick_start.py (the module-level topology and setup).

P4 program

examples/quick_start/quick_start.p4:

#include <core.p4>
#include <v1model.p4>

header ethernet_t {
    bit<48> dstAddr;
    bit<48> srcAddr;
    bit<16> etherType;
}

struct headers {
    ethernet_t ethernet;
}

struct metadata {}

parser MyParser(packet_in pkt, out headers hdr, inout metadata meta,
                inout standard_metadata_t std) {
    state start {
        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 == 1) {
            std.egress_spec = 2;
        } else if (std.ingress_port == 2) {
            std.egress_spec = 1;
        } else {
            mark_to_drop(std);
        }
    }
}

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

V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(),
         MyComputeChecksum(), MyDeparser()) main;

The ingress control sets std.egress_spec based on ingress_port — no tables, no runtime control plane needed.

Run it

sudo p4net examples/quick_start/quick_start.py

Then in the shell:

p4net> hosts
name  primary_ip   primary_ip6  interfaces
h1    10.0.0.1/24  -            h1-eth0
h2    10.0.0.2/24  -            h2-eth0

p4net> pingall
H \ H   h1   h2
   h1    -    1
   h2    1    -
2/2 succeeded

What's interesting

  • It's the smallest possible working program. If pingall succeeds here, the rest of the toolchain (p4c, BMv2, namespaces, veth pairs, P4Runtime) is operational.
  • The dataplane has no notion of L3 — no IPv4 header parsing, no ARP. Static ARP in setup(net) is what makes the L3 ping work.

Variations to try

  • Add a third host on port 3. Without table programming, packets to port 3 hit the implicit drop (since the port-swap covers only 1 ↔ 2).
  • Replace the conditional with a single mark_to_drop(std) and watch pingall produce all X cells.
  • Set a Link(..., loss_pct=20.0) and observe the success rate drop in pingall 10 1.