mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 21:45:41 +03:00
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:
parent
86651604c4
commit
f03c94db6d
266
docs/design/ubridge-link-local-frame-forwarding-spec.md
Normal file
266
docs/design/ubridge-link-local-frame-forwarding-spec.md
Normal 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).
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user