gns3-server/docs/design/ubridge-link-local-frame-forwarding-spec.md
YueGuobin f03c94db6d
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.
2026-10-06 00:21:10 +08:00

267 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).