Skip to content

L2 learning entry installation

I want to: turn a flood-on-miss data plane into a learning switch by writing forwarding entries from the controller as PacketIns arrive.

The pattern

java
import io.github.zhh2001.jp4.P4Switch;
import io.github.zhh2001.jp4.entity.TableEntry;
import io.github.zhh2001.jp4.match.Match;
import io.github.zhh2001.jp4.types.Mac;

import java.math.BigInteger;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

try (P4Switch sw = P4Switch.connectAsPrimary("127.0.0.1:50051")
        .bindPipeline(p4info, deviceConfig)) {

    Map<Mac, Integer> learned = new ConcurrentHashMap<>();

    sw.onPacketIn(packet -> {
        int ingressPort = packet.metadataInt("ingress_port");
        byte[] frame = packet.payload().toByteArray();
        if (frame.length < 12) return;            // too short to carry src/dst MAC

        byte[] srcBytes = java.util.Arrays.copyOfRange(frame, 6, 12);
        Mac src = Mac.fromBytes(srcBytes);

        if (learned.putIfAbsent(src, ingressPort) != null) return;   // already known

        sw.insert(TableEntry.in("MyIngress.l2_forward")
                .match("hdr.ethernet.dstAddr", new Match.Exact(srcBytes))
                .action("MyIngress.forward").param("port", ingressPort)
                .build());
    });

    // ... drive traffic and observe learning ...
}

Real usage: simple-l2-switch.

Walkthrough

  1. Connect as primary + bind pipeline. The handler can't fire without a bound pipeline because PacketIn metadata can't be parsed without P4Info. The controller in this recipe is the only client; connectAsPrimary is the shorthand.
  2. Register the PacketIn handler with sw.onPacketIn. The handler runs on jp4's single-threaded callback executor — slow handlers delay subsequent dispatch but never block the gRPC inbound thread.
  3. Read the source MAC out of the frame. The L2 src is bytes 6-11 of the Ethernet payload. Use Mac.fromBytes so the comparison key has a typed equals/hashCode.
  4. Idempotent guard via learned.putIfAbsent. Multiple PacketIns for the same src MAC can race; putIfAbsent ensures one insert per src.
  5. Install the forwarding entry. TableEntry.in(name).match(field, MatchKind).action(name).param(name, value).build() is the fluent chain; sw.insert(entry) is blocking and throws P4OperationException with ALREADY_EXISTS if the key already exists — the guard avoids that, but if the data plane state diverges from the controller's learned map, catch and either fall through or sw.modify the entry.

Why this works

The data plane (MyIngress.l2_forward) does an exact-match lookup keyed on hdr.ethernet.dstAddr; a hit forwards, a miss punts the frame to the controller. After the controller installs the entry for srcAddr, subsequent frames destined to that MAC short-circuit through the data plane without a controller hop.

See also

  • Packet I/O — the three PacketIn consumption styles (callback / Flow.Publisher / poll) and how PacketOut interacts.
  • Tables — the full TableEntry builder surface and the five match kinds.
  • Threading model — why a PacketIn handler calling sw.insert doesn't deadlock.
  • simple-l2-switch example — the full runnable program this recipe was extracted from.

Released under the Apache License 2.0.