From f03c94db6da7835ef93c78839dc6c29080748695 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 3 Oct 2026 00:55:35 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20link-local=20frame=20transparency=20on?= =?UTF-8?q?=20kernel=20links=20=E2=80=94=20spec=20and=20per-datapath=20sec?= =?UTF-8?q?tions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...bridge-link-local-frame-forwarding-spec.md | 266 ++++++++++++++++++ docs/features/docker-kernel-datapath.md | 21 ++ docs/features/dynamips-kernel-datapath.md | 21 ++ .../ethernet-switch-kernel-datapath.md | 26 ++ docs/features/iol-docker-kernel-datapath.md | 21 ++ docs/features/iou-kernel-datapath.md | 21 ++ docs/features/qemu-kernel-datapath.md | 21 ++ 7 files changed, 397 insertions(+) create mode 100644 docs/design/ubridge-link-local-frame-forwarding-spec.md diff --git a/docs/design/ubridge-link-local-frame-forwarding-spec.md b/docs/design/ubridge-link-local-frame-forwarding-spec.md new file mode 100644 index 000000000..427863e4c --- /dev/null +++ b/docs/design/ubridge-link-local-frame-forwarding-spec.md @@ -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 (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 `, 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). diff --git a/docs/features/docker-kernel-datapath.md b/docs/features/docker-kernel-datapath.md index 5ac1a68a6..4134a4c39 100644 --- a/docs/features/docker-kernel-datapath.md +++ b/docs/features/docker-kernel-datapath.md @@ -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 diff --git a/docs/features/dynamips-kernel-datapath.md b/docs/features/dynamips-kernel-datapath.md index 4d731c7b9..baed487c7 100644 --- a/docs/features/dynamips-kernel-datapath.md +++ b/docs/features/dynamips-kernel-datapath.md @@ -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` diff --git a/docs/features/ethernet-switch-kernel-datapath.md b/docs/features/ethernet-switch-kernel-datapath.md index c57e406c2..322e06d43 100644 --- a/docs/features/ethernet-switch-kernel-datapath.md +++ b/docs/features/ethernet-switch-kernel-datapath.md @@ -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 diff --git a/docs/features/iol-docker-kernel-datapath.md b/docs/features/iol-docker-kernel-datapath.md index cbbbcf415..5b7b29aab 100644 --- a/docs/features/iol-docker-kernel-datapath.md +++ b/docs/features/iol-docker-kernel-datapath.md @@ -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 diff --git a/docs/features/iou-kernel-datapath.md b/docs/features/iou-kernel-datapath.md index 65fe4c6ff..eea88f783 100644 --- a/docs/features/iou-kernel-datapath.md +++ b/docs/features/iou-kernel-datapath.md @@ -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 diff --git a/docs/features/qemu-kernel-datapath.md b/docs/features/qemu-kernel-datapath.md index 18a35843e..29f340a66 100644 --- a/docs/features/qemu-kernel-datapath.md +++ b/docs/features/qemu-kernel-datapath.md @@ -110,6 +110,27 @@ Identical to Docker's kernel links, on the TAP: `capture start_kernel ` 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