Skip to content

L3 forwarding

Two hosts on one switch, with the dataplane forwarding via an ipv4_lpm table that's programmed from Python at startup time.

What you'll see

pingall succeeds because the controller installs /32 routes for both hosts before the shell opens.

Topology

examples/l3_forwarding/topology.py:

"""Two hosts on a /24, ipv4_lpm forwarding programmed via P4Runtime.

The P4 program (`ipv4_lpm.p4`) defines a single LPM table that matches the
destination IPv4 address and sets an egress port. Forwarding entries are
installed at runtime by `setup(net)`, which also pre-seeds static ARP so
ICMP unicast does not have to resolve neighbours at test time.

Run as root:

    sudo python examples/l3_forwarding/topology.py
    sudo p4net examples/l3_forwarding/topology.py
"""

from __future__ import annotations

from pathlib import Path

from p4net import Network
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")
h2 = topology.add_host("h2", ip="10.0.0.2/24", mac="00:00:00:00:00:02")
s1 = topology.add_switch("s1", p4_src=HERE / "ipv4_lpm.p4")
topology.add_link(h1, s1, port_b=1)
topology.add_link(h2, s1, port_b=2)


def setup(net: Network) -> None:
    """Pre-seed static ARP and install ipv4_lpm forwarding entries."""
    h1 = net.host("h1")
    h2 = net.host("h2")
    h1.exec(
        [
            "ip",
            "neigh",
            "replace",
            "10.0.0.2",
            "lladdr",
            "00:00:00:00:00:02",
            "dev",
            "h1-eth0",
            "nud",
            "permanent",
        ]
    )
    h2.exec(
        [
            "ip",
            "neigh",
            "replace",
            "10.0.0.1",
            "lladdr",
            "00:00:00:00:00:01",
            "dev",
            "h2-eth0",
            "nud",
            "permanent",
        ]
    )

    s1 = net.switch("s1")
    s1.client.insert_table_entry(
        table="MyIngress.ipv4_lpm",
        match={"hdr.ipv4.dstAddr": "10.0.0.1/32"},
        action="MyIngress.set_egress_port",
        params={"port": 1},
    )
    s1.client.insert_table_entry(
        table="MyIngress.ipv4_lpm",
        match={"hdr.ipv4.dstAddr": "10.0.0.2/32"},
        action="MyIngress.set_egress_port",
        params={"port": 2},
    )


if __name__ == "__main__":
    from p4net.cli.main import main

    raise SystemExit(main([__file__]))

setup(net) issues two client.insert_table_entry(...) calls — one per host — naming the table by its fully qualified P4Info name.

P4 program

examples/l3_forwarding/ipv4_lpm.p4:

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

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

header ipv4_t {
    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;
    bit<32> srcAddr;
    bit<32> dstAddr;
}

const bit<16> ETHERTYPE_IPV4 = 0x0800;

struct headers {
    ethernet_t ethernet;
    ipv4_t     ipv4;
}

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 select(hdr.ethernet.etherType) {
            ETHERTYPE_IPV4: parse_ipv4;
            default: accept;
        }
    }
    state parse_ipv4 {
        pkt.extract(hdr.ipv4);
        transition accept;
    }
}

control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply {} }

control MyIngress(inout headers hdr, inout metadata meta,
                  inout standard_metadata_t std) {
    counter(256, CounterType.packets) ingress_pkts;

    action drop() {
        mark_to_drop(std);
    }

    action set_egress_port(bit<9> port) {
        std.egress_spec = port;
        ingress_pkts.count((bit<32>) port);
    }

    table ipv4_lpm {
        key = {
            hdr.ipv4.dstAddr: lpm;
        }
        actions = {
            drop;
            set_egress_port;
            NoAction;
        }
        default_action = NoAction();
        size = 1024;
    }

    apply {
        if (hdr.ipv4.isValid()) {
            ipv4_lpm.apply();
        }
    }
}

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);
        pkt.emit(hdr.ipv4);
    }
}

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

The ingress control applies ipv4_lpm only when an IPv4 header is present — non-IPv4 traffic (e.g. ARP) hits the default NoAction and goes nowhere. ARP works because setup(net) seeds it statically.

Run it

sudo p4net examples/l3_forwarding/topology.py

Then in the shell:

p4net> s1 table dump MyIngress.ipv4_lpm
#0
  table:    MyIngress.ipv4_lpm
  match:    {'hdr.ipv4.dstAddr': '10.0.0.1/32'}
  action:   MyIngress.set_egress_port
  params:   {'port': '1'}
#1
  table:    MyIngress.ipv4_lpm
  match:    {'hdr.ipv4.dstAddr': '10.0.0.2/32'}
  action:   MyIngress.set_egress_port
  params:   {'port': '2'}

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

p4net> s1 counter MyIngress.ingress_pkts 1
pkts=1 bytes=98

The match value renders as 10.0.0.1/32 — that's decode_match turning P4Runtime canonical bytes back into a human IPv4 string.

What's interesting

  • Same dataplane handles a 5-host topology, a 100-host topology, and a different L3 design — only the table programming changes.
  • s1.client.insert_table_entry(...) accepts plain Python types (strings, dicts, ints); the P4InfoIndex translates them into P4Runtime FieldMatch and Action protos based on the loaded P4Info.

Variations to try

  • Add a third host on 10.0.0.3/24 and install a third LPM entry. No P4 changes needed.
  • Replace the /32 entries with a /24 covering the whole subnet via the same egress port. Verify pingall still works.
  • Add a host on 10.1.0.0/24 and drop its traffic with the MyIngress.drop action — watch the LPM resolve from longest to shortest prefix.