docs: link-local frame transparency on kernel links — spec and per-datapath sections

A Linux bridge standing in for a cable does not forward the IEEE 802.1D
reserved range (01:80:c2:00:00:00-0f) by default, so LACP/LLDP/802.1X never
crossed a kernel-datapath link while the UDP relay carried them fine. uBridge
now opens every enslave'd port's IFLA_BRPORT_GROUP_FWD_MASK to 0xfffd at
brctl addif time (branch feature/bridge-tap-l2only, commits 0284500/03a18f1/
cf4e494), which the six kernel-datapath feature docs now document as a
section: STP, LACP, 802.1X and LLDP/DCBX cross; 802.3x PAUSE / PFC stays
dropped — br_handle_frame()'s case 0x01 discards it unconditionally, no mask
can enable it, and PFC labs must use a relay link. The full contract, kernel
references and requirements live in the new design spec, marked landed.
This commit is contained in:
YueGuobin 2026-10-03 00:55:35 +08:00
parent 86651604c4
commit f03c94db6d
No known key found for this signature in database
7 changed files with 397 additions and 0 deletions

View File

@ -0,0 +1,266 @@
# uBridge bridge ports must forward link-local frames (per-port `group_fwd_mask`)
> **Status: landed and verified.** uBridge branch `feature/bridge-tap-l2only`,
> commits `0284500` (feat), `03a18f1` (test suite), `cf4e494` (docs) — the
> implicit `addif` hook of §A.1 plus the `setportgroupfwd` escape hatch of
> §A.3. gns3-server carries **zero product-code changes**: the behavior
> reaches every `brctl addif` call site as specified. Verified by the uBridge
> suite (`tests/brctl/test_linklocal.py`, 28/28) and live through a real
> server by `tests/e2e/test_docker_link_local_frames.py`. The three
> `test_vlan` kernel-state failures seen alongside are pre-existing on the
> parent commit (verified by an fb72758 control run). The version bump (R8)
> is still open, pending review.
## 0. Background & scope
### The datapath this serves
GNS3's kernel datapath wires a link by enslaving each endpoint's host-side
anchor (veth host end, TAP) into a per-link Linux bridge. The same shape is
used by the Ethernet switch node (its bridge absorbs peers' anchors), by
switch-to-switch cascades (a veth pair joining two bridges) and by the Cloud
node (pnet bridges). Functionally these bridges stand in for **a cable** — a
transparent L2 segment — and that is the contract users expect from them.
### The problem: a Linux bridge is not a transparent cable
`br_handle_frame()` in `net/bridge/br_input.c` treats the IEEE 802.1D
reserved link-local range `01:80:c2:00:00:00`–`01:80:c2:00:00:0f` specially
and, by default, does **not** forward it. Measured on a stock mainline kernel
(7.2.6, no patches) with two ports in one bridge:
| Destination MAC | Protocol | Default bridge | Notes |
|---|---|---|---|
| `ff:ff:ff:ff:ff:ff`, `01:00:5e:*`, `33:33:*` | broadcast, IPv4/IPv6 multicast | forwarded | unaffected |
| `01:80:c2:00:00:00` | STP / RSTP BPDU | **forwarded** | special-cased: forwarded while the bridge runs no STP |
| `01:80:c2:00:00:01` | 802.3x PAUSE / 802.1Qbb PFC | not forwarded | kernel drops unconditionally — see §0.3 |
| `01:80:c2:00:00:02` | LACP / slow protocols | not forwarded | |
| `01:80:c2:00:00:03` | 802.1X EAPOL | not forwarded | |
| `01:80:c2:00:00:0e` | LLDP (DCBX) | not forwarded | |
| `01:80:c2:00:00:10` and up | — | forwarded | outside the reserved nibble range |
Consequence: on a kernel-datapath link, two emulated devices that run LACP,
LLDP/DCBX or 802.1X never see each other, while the same lab over the uBridge
UDP relay (a dumb store-and-forward, fully transparent) works. The two
datapaths disagree, and the failure is silent.
### Why the bridge-level knob is not enough
`IFLA_BR_GROUP_FWD_MASK` (the bridge-level attribute, already reachable via
uBridge's `brctl setgroupfwd`) is restricted by `BR_GROUPFWD_RESTRICTED`
(`BR_GROUPFWD_STP | BR_GROUPFWD_MACPAUSE | BR_GROUPFWD_LACP`): writes
containing bits 0/1/2 fail with `EINVAL`. Measured: bridge-level accepts at
most `0xfff8`, which forwards LLDP/EAPOL but **not LACP**.
The per-port attribute `IFLA_BRPORT_GROUP_FWD_MASK` (commit
`5af48b59f35cf712793badabe1a574a0d0ce3bd3`, "net: bridge: add per-port
group_fwd_mask with less restrictions", net-next 2017, Linux 4.15) exists
precisely for this: its commit message names "transparent forwarding of
most link-local frames (e.g. STP, LACP) through tunnels (vxlan, qinq)" and
restricts only MAC PAUSE. Measured: port-level accepts `0xfffd` and then
forwards STP, LACP, EAPOL, LLDP and every other reserved address.
The mask is evaluated on the **ingress** port (setting it on the egress port
only has no effect), so every port of a bridge needs it — which is exactly
what a per-addif hook gives us.
### What this change is — and what it cannot be
PAUSE / PFC is **not** in scope and cannot be delivered by any mask: the
`case 0x01:` branch in `br_handle_frame()` drops unconditionally
(`kfree_skb_reason(..., SKB_DROP_REASON_MAC_IEEE_MAC_CONTROL)`) without
consulting `fwd_mask`, and the port-level setter rejects bit 1 with `EINVAL`.
Forwarding PAUSE would require patching that branch. Emulation of PFC
semantics is out of reach anyway (no hardware pause generation/response, no
per-priority queues, no headroom/watchdog), so the GNS3 documentation will
state PFC as unsupported on kernel links rather than pretending otherwise.
### Goal
A kernel-datapath link (and every other uBridge bridge) carries link-local
frames exactly like the cable it emulates: STP, LACP, EAPOL, LLDP and the
remaining reserved addresses flow; PAUSE/PFC is documented as impossible.
---
## A. The requested change
### A.1 One step in `br_enslave_if()` (`hypervisor_brctl.c:251`)
After the port is enslaved (step 1) — and using the existing u16 port-attribute
helper — set the link-local forwarding mask:
```c
/* Step 3 – link-local transparency. A Linux bridge stands in for a cable
* here: LACP/LLDP/EAPOL/STP must cross it. The kernel restricts the
* *bridge-level* mask to bits 3..15 (BR_GROUPFWD_RESTRICTED), so LACP is
* only reachable per port. 0xfffd = every reserved address except MAC
* PAUSE (bit 1), which the kernel refuses and hard-drops anyway.
* Best-effort: kernels before 4.15 have no such port attribute, and the
* cable simply keeps today's behaviour there. */
if (br_set_port_attr_u16(bridge, port, IFLA_BRPORT_GROUP_FWD_MASK,
LINK_LOCAL_FWD_MASK) < 0)
fprintf(stderr, "ubridge: %s: link-local forwarding mask not set\n", port);
```
with
```c
/* bit 0 STP (harmless: already forwarded while STP is off), bit 2 LACP,
* bit 3 802.1X EAPOL, bit 14 LLDP; bit 1 MAC PAUSE must stay clear or the
* kernel rejects the whole value with EINVAL. */
#define LINK_LOCAL_FWD_MASK 0xfffdu
```
`br_set_port_attr_u16()` (`hypervisor_brctl.c:811`) already does everything
needed: it checks membership via `br_check_master()`, builds the
`IFLA_PROTINFO` nested attribute on `AF_BRIDGE`/`RTM_SETLINK`, and — critically
— encodes the value as **2 bytes**, which is what the kernel's port policy
requires (`NLA_U16`); a 1-byte payload is rejected with `-ERANGE` (the same
trap documented in that helper's comment for `IFLA_BRPORT_PRIORITY`).
### A.2 Why in `br_enslave_if()` rather than in any single caller
* **Every** enslavement in uBridge funnels through this helper — per-link
bridges, the Ethernet switch's own ports, peers absorbed into a switch
bridge, cascades, pnet/Cloud bridges. One hook covers all of them, and
covers future callers too.
* gns3-server **cannot** do it itself: the packaged service runs unprivileged
(`init/gns3.service.systemd`: `User=gns3`) and `CAP_NET_ADMIN` is carried by
the uBridge binary (`base_manager.py:_has_privileged_access()`). A
server-side `ip link set … type bridge_slave …` is not possible in the
supported deployment.
* Doing it at addif time also means ports that are re-attached after a node
restart (the server re-pushes NIOs) are re-covered without extra logic.
### A.3 Alternative interface, if the project prefers explicit policy
If uBridge would rather keep this policy out of `addif`, the equivalent
explicit command is:
```
brctl setportgroupfwd <bridge> <port> <mask> (3 args)
```
implemented as a thin wrapper over `br_set_port_attr_u16(..., (unsigned
short)mask)` and registered next to `setportprio` in the command table
(`hypervisor_brctl.c:2049`). In that case gns3-server will call it at its
five `brctl addif` sites and gate it on a probe of the running build. The
implicit form is preferred by gns3-server because it needs no server change
and cannot be forgotten at a new call site.
---
## B. Requirements (normative)
* **R1** After a successful `brctl addif <bridge> <port>`, the port carries
`group_fwd_mask = 0xfffd` (`LINK_LOCAL_FWD_MASK`).
* **R2** The value must **exclude bit 1 (MAC PAUSE)**. `0xfffe`/`0xffff` are
rejected by the kernel with `EINVAL` and would silently lose the whole
mask — do not use them.
* **R3** The attribute payload must be encoded as u16 (`br_set_port_attr_u16`).
A u8 payload fails `validate_nla` with `-ERANGE` on kernels that enforce the
policy strictly.
* **R4** **Best-effort**: a failed mask write must never fail the `addif`
(kernels < 4.15 have no per-port attribute; unusual deployments may lack
`CAP_NET_ADMIN`). Reply to `addif` is unchanged; the failure is logged to
stderr.
* **R5** Ordering: set the mask after the port is enslaved (both
`br_check_master()` and the kernel's `IFLA_PROTINFO` handling require
membership). The step-2 "bring the port up" may run before or after; both
work.
* **R6** Idempotent: re-`addif` of an already-enslaved port must succeed and
keep the mask set (gns3-server re-attaches links on node start).
* **R7** Applies uniformly to every bridge/port this helper touches — no
per-bridge or per-caller switch (a real switch terminates link-local frames,
but GNS3's Ethernet switch node is used as a transparent segment, so
forwarding is the wanted behaviour there too).
* **R8** Version: bump the reported version so gns3-server can raise its
minimum (the server already gates on `ubridge version X.Y.Z`,
`compute/ubridge/hypervisor.py:_check_ubridge_version`, currently ≥ 1.2.3).
* **R9** Document the PAUSE/PFC exclusion in uBridge's help/docs, so the gap
is not rediscovered as a bug.
---
## C. Implementation notes
* `IFLA_BRPORT_GROUP_FWD_MASK` is defined in `include/uapi/linux/if_link.h`
(Linux ≥ 4.15); guard the build on its presence if uBridge must compile
against older headers — behavior on such kernels is "no mask", which R4
already tolerates.
* The kernel's `br_set_group_fwd_mask()` (port variant) validates against
`BR_GROUPFWD_MACPAUSE`; there is also a follow-up fix
(`16c42db48522`, "bridge: check for attempt to forward STP PDU's with STP
enabled") that moves STP validation into the setter — bit 0 is harmless for
our case because we never enable STP on these bridges.
* No interaction with existing commands: `brctl setgroupfwd` (bridge-level)
stays as is and is unused by gns3-server; `hairpin`/`isolated`/`setportstate`
continue to use their own attributes through the same helper family.
---
## D. Reply contract & error behavior
`addif` replies (`HSC_INFO_OK` / `HSC_ERR_*`) are unchanged. The mask write
does not produce its own reply and does not alter the addif verdict; on
failure it writes one line to stderr (§A.1). No new error codes.
---
## E. Test requirements
The uBridge test suite already drives bridge/veth setups; the following cases
pin the contract (all are cheap AF_PACKET probes — send a frame with a given
destination MAC into port A, observe whether it arrives on port B, using
`PACKET_OUTGOING` filtering so a port's own transmissions do not count):
* **T1 transparency**: with two veths in one bridge created via `brctl`:
`01:00:5e:00:00:01` forwarded, `01:80:c2:00:00:0e` (LLDP) forwarded,
`01:80:c2:00:00:02` (LACP) forwarded, `01:80:c2:00:00:00` (STP) forwarded,
`01:80:c2:00:00:01` (PAUSE) **not** forwarded.
* **T2 ingress semantics**: mask on the egress port only → LACP not forwarded
(documents why every port is set; regression guard against "set it once").
* **T3 best-effort**: force the mask write to fail (fault-injection hook or an
invalid attribute id) and assert `addif` still replies `HSC_INFO_OK`.
* **T4 idempotency**: `addif` twice → both succeed, mask still `0xfffd`.
* **T5 value boundary** (optional, documents R2): a direct write of `0xfffe`
through the port setter fails with `EINVAL`.
---
## gns3-server alignment (informational — not uBridge scope)
* **No server change is required** for the behavior: all five server-side
enslavements (`compute/kernel_datapath.py:165`,
`compute/builtin/nodes/ethernet_switch.py:399,518`,
`compute/builtin/nodes/cloud.py:377`, and anything added later) go through
`brctl addif`.
* If R8's version bump lands, gns3-server can raise
`_check_ubridge_version`'s minimum so that a compute that cannot deliver
the behavior is reported instead of silently degrading.
* GNS3 documentation will state the resulting matrix for kernel links:
LLDP/DCBX ✅, LACP ✅, 802.1X ✅, STP ✅, **PAUSE/PFC ❌** (kernel-level,
not fixable by mask).
* The planned VXLAN datapath inherits the same story: the vxlan device is a
bridge port, so the same per-port mask applies; PAUSE/PFC stays out of
reach there too. That must be decided before VXLAN ships, because today's
cross-compute relay links *do* carry PFC and would silently regress.
## Roadmap note
If PFC emulation ever becomes a hard requirement, the only paths are a
patched kernel (`case 0x01`), a tc/eBPF ingress redirect on the anchor, or a
userspace AF_PACKET pass-through in uBridge (packet sockets see these frames
before the bridge drops them). None of these is part of this spec.
## References
* Kernel: `net/bridge/br_input.c` (`br_handle_frame`, the link-local switch),
`net/bridge/br_private.h` (`BR_GROUPFWD_*`, `BR_GROUPFWD_RESTRICTED`).
* Per-port mask: commit
[`5af48b59f35c`](https://github.com/Freescale/linux-fslc/commit/5af48b59f35cf712793badabe1a574a0d0ce3bd3)
("net: bridge: add per-port group_fwd_mask with less restrictions");
[patchwork entry](https://patchwork.ozlabs.org/project/netdev/patch/1506517964-17479-1-git-send-email-nikolay@cumulusnetworks.com/).
* Measurements: stock mainline 7.2.6-1-default, two veth ports in one Linux
bridge, AF_PACKET probes (2026-10).

View File

@ -333,6 +333,27 @@ Semantics:
nothing — older servers, failed probe — keep the full list;
apply-time validation (409) stays the guard.
## Link-local frames (LACP, LLDP, 802.1X, STP)
The bridge stands in for a cable, but the kernel's `br_handle_frame()` does
not forward the IEEE 802.1D reserved range (`01:80:c2:00:00:00`-`0f`) by
default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while
the UDP relay carries them fine. uBridge therefore opens every port's
per-port `group_fwd_mask` (`IFLA_BRPORT_GROUP_FWD_MASK`) to `0xfffd` at
`brctl addif` time (Linux 4.15+, best-effort on older kernels). The
bridge-level knob cannot do this: `BR_GROUPFWD_RESTRICTED` rejects bits 0-2,
so LACP is reachable per port only.
Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and
LLDP/DCBX all cross; **802.3x PAUSE / PFC does not** — `case 0x01` in
`br_handle_frame()` drops it unconditionally and no mask can enable it.
That is a documented limit, not a regression (PFC's hardware semantics are
out of emulation's reach anyway); a lab that needs PAUSE frames on the wire
must use a relay link. Verified live by
`tests/e2e/test_docker_link_local_frames.py` (per-MAC guest-to-guest
matrix); the contract and kernel references live in
`docs/design/ubridge-link-local-frame-forwarding-spec.md`.
## uBridge command surface
No uBridge changes were needed beyond the marker module's `*_kernel` commands

View File

@ -93,6 +93,27 @@ filters are one tc netem qdisc per anchor plus cls_bpf/eBPF drops. A relay
port keeps its Dynamips NIO and the node's uBridge relay bridge
(`DYNAMIPS-*`) with userspace filters, exactly as before.
## Link-local frames (LACP, LLDP, 802.1X, STP)
The bridge stands in for a cable, but the kernel's `br_handle_frame()` does
not forward the IEEE 802.1D reserved range (`01:80:c2:00:00:00`-`0f`) by
default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while
the UDP relay carries them fine. uBridge therefore opens every port's
per-port `group_fwd_mask` (`IFLA_BRPORT_GROUP_FWD_MASK`) to `0xfffd` at
`brctl addif` time (Linux 4.15+, best-effort on older kernels). The
bridge-level knob cannot do this: `BR_GROUPFWD_RESTRICTED` rejects bits 0-2,
so LACP is reachable per port only.
Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and
LLDP/DCBX all cross; **802.3x PAUSE / PFC does not** — `case 0x01` in
`br_handle_frame()` drops it unconditionally and no mask can enable it.
That is a documented limit, not a regression (PFC's hardware semantics are
out of emulation's reach anyway); a lab that needs PAUSE frames on the wire
must use a relay link. Verified live by
`tests/e2e/test_docker_link_local_frames.py` (per-MAC guest-to-guest
matrix); the contract and kernel references live in
`docs/design/ubridge-link-local-frame-forwarding-spec.md`.
## Relay fallback
A uBridge without the tap module keeps the node relay-only: `_prepare_tap_datapath`

View File

@ -118,6 +118,32 @@ sequence by hand against uBridge (held TAPs, no server involved) did not
reproduce it, and the in-place reconcile removes the operation entirely —
the failure mode no longer exists by construction.
## Link-local frames (LACP, LLDP, 802.1X, STP)
The bridge stands in for a cable, but the kernel's `br_handle_frame()` does
not forward the IEEE 802.1D reserved range (`01:80:c2:00:00:00`-`0f`) by
default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while
the UDP relay carries them fine. uBridge therefore opens every port's
per-port `group_fwd_mask` (`IFLA_BRPORT_GROUP_FWD_MASK`) to `0xfffd` at
`brctl addif` time (Linux 4.15+, best-effort on older kernels). The
bridge-level knob cannot do this: `BR_GROUPFWD_RESTRICTED` rejects bits 0-2,
so LACP is reachable per port only.
Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and
LLDP/DCBX all cross; **802.3x PAUSE / PFC does not** — `case 0x01` in
`br_handle_frame()` drops it unconditionally and no mask can enable it.
That is a documented limit, not a regression (PFC's hardware semantics are
out of emulation's reach anyway); a lab that needs PAUSE frames on the wire
must use a relay link. Verified live by
`tests/e2e/test_docker_link_local_frames.py` (per-MAC guest-to-guest
matrix); the contract and kernel references live in
`docs/design/ubridge-link-local-frame-forwarding-spec.md`.
The switch node's own bridge ports carry the same mask: the builtin
Ethernet switch is a transparent L2 segment in GNS3, not a protocol
endpoint, so LACP/LLDP between two peers crosses it (a physical switch
would terminate them itself).
## Verified
Unit level (all under `tests/`): the switch's kernel-port lifecycle (join

View File

@ -122,6 +122,27 @@ traffic stops both ways. The runner itself is unaware of the link state
(netiomux has no carrier signal), exactly as on the relay datapath where
suspend rode the synthetic frequency_drop filter instead.
## Link-local frames (LACP, LLDP, 802.1X, STP)
The bridge stands in for a cable, but the kernel's `br_handle_frame()` does
not forward the IEEE 802.1D reserved range (`01:80:c2:00:00:00`-`0f`) by
default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while
the UDP relay carries them fine. uBridge therefore opens every port's
per-port `group_fwd_mask` (`IFLA_BRPORT_GROUP_FWD_MASK`) to `0xfffd` at
`brctl addif` time (Linux 4.15+, best-effort on older kernels). The
bridge-level knob cannot do this: `BR_GROUPFWD_RESTRICTED` rejects bits 0-2,
so LACP is reachable per port only.
Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and
LLDP/DCBX all cross; **802.3x PAUSE / PFC does not** — `case 0x01` in
`br_handle_frame()` drops it unconditionally and no mask can enable it.
That is a documented limit, not a regression (PFC's hardware semantics are
out of emulation's reach anyway); a lab that needs PAUSE frames on the wire
must use a relay link. Verified live by
`tests/e2e/test_docker_link_local_frames.py` (per-MAC guest-to-guest
matrix); the contract and kernel references live in
`docs/design/ubridge-link-local-frame-forwarding-spec.md`.
## Relay fallback
Missing either capability, the node runs relay-only: links ride

View File

@ -101,6 +101,27 @@ one tc netem qdisc per anchor plus cls_bpf/eBPF drops — the port's
userspace `iol_bridge add_packet_filter` filters carry relay links only and
are never combined with tc on the same port.
## Link-local frames (LACP, LLDP, 802.1X, STP)
The bridge stands in for a cable, but the kernel's `br_handle_frame()` does
not forward the IEEE 802.1D reserved range (`01:80:c2:00:00:00`-`0f`) by
default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while
the UDP relay carries them fine. uBridge therefore opens every port's
per-port `group_fwd_mask` (`IFLA_BRPORT_GROUP_FWD_MASK`) to `0xfffd` at
`brctl addif` time (Linux 4.15+, best-effort on older kernels). The
bridge-level knob cannot do this: `BR_GROUPFWD_RESTRICTED` rejects bits 0-2,
so LACP is reachable per port only.
Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and
LLDP/DCBX all cross; **802.3x PAUSE / PFC does not** — `case 0x01` in
`br_handle_frame()` drops it unconditionally and no mask can enable it.
That is a documented limit, not a regression (PFC's hardware semantics are
out of emulation's reach anyway); a lab that needs PAUSE frames on the wire
must use a relay link. Verified live by
`tests/e2e/test_docker_link_local_frames.py` (per-MAC guest-to-guest
matrix); the contract and kernel references live in
`docs/design/ubridge-link-local-frame-forwarding-spec.md`.
## Relay fallback
A uBridge without `iol_bridge add_nio_tap` (old build) keeps the node on the

View File

@ -110,6 +110,27 @@ Identical to Docker's kernel links, on the TAP: `capture start_kernel <tap>`
direction is impaired once. `frequency_drop`/`quota`/`window_drop` run as the
eBPF classifier on the anchor's clsact egress.
## Link-local frames (LACP, LLDP, 802.1X, STP)
The bridge stands in for a cable, but the kernel's `br_handle_frame()` does
not forward the IEEE 802.1D reserved range (`01:80:c2:00:00:00`-`0f`) by
default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while
the UDP relay carries them fine. uBridge therefore opens every port's
per-port `group_fwd_mask` (`IFLA_BRPORT_GROUP_FWD_MASK`) to `0xfffd` at
`brctl addif` time (Linux 4.15+, best-effort on older kernels). The
bridge-level knob cannot do this: `BR_GROUPFWD_RESTRICTED` rejects bits 0-2,
so LACP is reachable per port only.
Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and
LLDP/DCBX all cross; **802.3x PAUSE / PFC does not** — `case 0x01` in
`br_handle_frame()` drops it unconditionally and no mask can enable it.
That is a documented limit, not a regression (PFC's hardware semantics are
out of emulation's reach anyway); a lab that needs PAUSE frames on the wire
must use a relay link. Verified live by
`tests/e2e/test_docker_link_local_frames.py` (per-MAC guest-to-guest
matrix); the contract and kernel references live in
`docs/design/ubridge-link-local-frame-forwarding-spec.md`.
## Legacy fallback
A uBridge build without the `tap` module (or without `CAP_NET_ADMIN`) keeps