Lab details

PPPoE Docker Interop Lab

This peer-isolated Docker lab proves both Ze PPPoE roles against independent implementations. Ze acts as a client against accel-ppp, then as an access concentrator against pppd with the rp-pppoe plugin.

Overview

Each scenario runs privileged containers on an isolated bridge because PPPoE uses Ethernet discovery and session frames (EtherTypes 0x8863 and 0x8864). The host kernel must provide /dev/ppp and the pppoe pppox module.

The client scenario requires PADI/PADO/PADR/PADS discovery, LCP, CHAP-MD5, IPCP, one kernel pppN interface, traffic to the access concentrator, the accel-ppp session view, and clean teardown. The access-concentrator scenario requires the same discovery and NCP path from an independent pppd trace. It also dials a wrong credential and requires CHAP refusal before proving that all session state disappeared.

Layout

test/interop-pppoe/
  Dockerfile.ze        Ze image used in both roles (copies the ze-linux the
                       suite preflight cross-compiles)
  ze-linux             the staged daemon, git-ignored and rewritten each run
  Dockerfile.accel     accel-ppp access-concentrator image
  Dockerfile.client    pppd and rp-pppoe client image
  scenarios/
    01-pppoe-chap-ipv4/         Ze client, accel-ppp concentrator
    02-ze-ac-pppd-client/       Ze concentrator, pppd client
    pppoe-empty-service-name/  Ze concentrator, pppd client, no service-name configured
    pppoe-padr-replay/         Ze concentrator, pppd client, max-sessions-per-mac 1
    ipv6cp-zero-identifier/    Ze concentrator, pppd client offering a zero IPv6CP identifier
    ipv6cp-missing-option/     Ze concentrator, pppd client whose IPv6CP request carries no identifier option
internal/le/interoplab/pppoe/
  pppoe.go             Native images, preflight, selection, and lifecycle
  scenarios.go         Role-selected container plans and mounts
  check_client.go      Ze client assertions
  check_ac.go          Ze access-concentrator assertions
  check_service_name.go  pppoe-empty-service-name: wire-level Service-Name proof
  check_padr_replay.go   pppoe-padr-replay: wire-level replay and per-MAC cap proof
  check_ipv6cp.go        ipv6cp-zero-identifier, ipv6cp-missing-option: wire-level
                         IPv6CP proof, BLOCKED until plan/spec-l2tp-ipv6-subscriber.md
                         lands (see the file's own header comment)

The Dockerfiles keep their small amount of peer initialisation in the image entrypoint command. There are no separate runner or entrypoint scripts.

Prerequisites

The native preflight probes /dev/ppp and the pppoe module from a temporary privileged container. It exits non-zero when either is absent, and setting ZE_PPPOE_SKIP_KERNEL_PROBE or ze.pppoe.skip-kernel-probe causes an immediate refusal.

The probe decides whether Docker can carry the proof. Docker Desktop on macOS was measured on 2026-08-01 and its Linux VM supplied both requirements on that machine. The QEMU action remains available for a host where the probe refuses.

Running

./le test deployment docker-pppoe-accel-test
ZE_PPPOE_INTEROP_SCENARIO=01-pppoe-chap-ipv4 ./le test deployment docker-pppoe-accel-test
ZE_PPPOE_INTEROP_SCENARIO=02-ze-ac-pppd-client ./le test deployment docker-pppoe-accel-test
ZE_PPPOE_INTEROP_SCENARIO=pppoe-empty-service-name ./le test deployment docker-pppoe-accel-test
ZE_PPPOE_INTEROP_SCENARIO=pppoe-padr-replay ./le test deployment docker-pppoe-accel-test
ZE_PPPOE_INTEROP_SCENARIO=ipv6cp-zero-identifier ./le test deployment docker-pppoe-accel-test
ZE_PPPOE_INTEROP_SCENARIO=ipv6cp-missing-option ./le test deployment docker-pppoe-accel-test

01-pppoe-chap-ipv4 and 02-ze-ac-pppd-client predate the naming rule and keep their numeric prefixes. A scenario added since carries none: interoplab.Discover and ZE_PPPOE_INTEROP_SCENARIO both match a scenario by its directory name, and a number is a reservation a later scenario can take, or a hole nothing tells apart from one (ai/rules/interop-and-goal-validation.md).

NO_BUILD skips image builds, SESSION_TIMEOUT changes the default 90-second scenario bound, and ZE_PPPOE_INTEROP_SUFFIX provides parallel-run isolation.

The QEMU proof for Ze as a client against accel-ppp is:

./le test qemu run kernel tmp/kernel/build/vmlinuz \
  packages "iproute2 iputils accel-ppp ppp kmod" \
  command "./le test qemu pppoe-accel-test"

The host qemu run action boots the runtime kernel and installs the guest packages. The guest pppoe-accel-test action then runs Ze and accel-ppp in two network namespaces joined by a veth. Invoking the guest action on the host does not boot a VM. This proof covers the client role only. The Docker suite carries both roles.

Scenarios

01-pppoe-chap-ipv4

Ze authenticates as alice with CHAP-MD5. IPCP assigns 10.11.0.2 with 10.11.0.1 as its peer. The checker requires one Ze pppN interface with that point-to-point address, a route through it, ICMP to the peer, an accel-ppp session for alice, and removal of that session after Ze stops.

02-ze-ac-pppd-client

The independent pppd client requests service internet, authenticates as alice, negotiates IPCP, and pings Ze through its PPP interface. The checker reads pppd's discovery, bidirectional LCP, CHAP, and IPCP trace and compares it with Ze's REST session table. It then requires PADT and empty state before repeating the dial with wrong-secret; the second trace must reach CHAP and receive a refusal without creating a session.

pppoe-empty-service-name

Ze's AC carries no service-name leaf. The independent pppd client dials with no requested service (RFC 2516 Section 5.1's "any service is acceptable"). The checker captures the discovery exchange on the wire (tcpdump inside the client container, decoded with internal/core/pcap and internal/component/l2tp/pppoe.ParseDiscovery) and requires exactly one Service-Name tag on the PADO and exactly one on the PADS, alongside the same LCP/CHAP/IPCP proof 02-ze-ac-pppd-client performs. A session coming up is not enough evidence here: the unit tests already pin BuildPADO/BuildPADS always writing one tag, so this scenario's job is proving a real peer reads the same bytes off Ze's own wire, not proving the encoder again.

pppoe-padr-replay

Ze's AC carries max-sessions-per-mac 1. The checker dials one session with the independent pppd client, waits until it is live in PPP, then replays the exact captured PADR frame from inside the client container's own network namespace (interoplab.SendFrameInNamespace, the same AF_PACKET/netns mechanism the BGP lab's IS-IS purge injector uses). It requires the PADS that replay provokes to carry the SAME session id, never a second one. It then dials a second, genuinely independent session from the same MAC and requires the PADS it provokes to carry session id 0x0000 and an AC-System-Error tag, read off the captured frame -- a session count that stayed at one is not enough evidence on its own for either claim (ai/rules/interop-and-goal-validation.md, "Prove a scenario discriminates"). spec-pppoe-padr-replay-allocates-unbounded-sessions.

ipv6cp-zero-identifier and ipv6cp-missing-option

Written and registered against RFC 5072 Section 4.1's zero-identifier Nak and its missing-option one-shot Nak, driven by a real pppd 2.5.1 client and read off the wire (check_ipv6cp.go), the same shape pppoe-padr-replay established. Neither can pass today. poolPlugin.handle (internal/component/l2tp/plugins/pool/register.go) answers every EventIPRequest whose family is not IPv4 with Accept: false, and runNCPPhase (internal/component/l2tp/ppp/ncp.go) reads that decline before the session reads a single client frame, so Ze's IPv6CP FSM never starts, on any configuration. Both scenarios run against pppd 2.5.1 only: this lab's client image carries pppd and rp-pppoe, and accel-ppp-as-client is not a role its images support. Both checkers return a citing error at the first missing wire evidence rather than passing vacuously. plan/spec-l2tp-ipv6-subscriber.md is what switches them on.

Relationship to other evidence

Evidence Ze role Independent peer Kernel PPPoE
test/pppoe/pppoe-basic.ci Access concentrator Functional fixture ✕
test/pppoe/pppoe-vlan.ci Access concentrator on VLAN Functional fixture ✕
test/pppoe/pppoe-service-name.ci Access concentrator Functional fixture ✕
./le test deployment docker-pppoe-accel-test Client and access concentrator accel-ppp and pppd Host kernel
./le test qemu pppoe-accel-test Client accel-ppp Runtime kernel