lacp: implement IEEE 802.3ad link aggregation

Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.

LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.

The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.

The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.

A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
This commit is contained in:
DrDoof authored and d00f committed 2026-10-12 15:09:57 +02:00
1 parent b83b67d234
commit c3b4cc893b
31 files changed
+1885 -23

No files matched your search

+49
View File
@@ -227,3 +227,52 @@ iperf Done.
```
As you can see, the total throughput was 4.71 GBit/sec which is close to the
maximum possible with a single 5GBit link.
## LACP: how the PDUs reach the CPU
Slow-protocol frames go to the reserved group `01:80:C2:00:00:02`, and the
ASIC's default action for it is *drop*. The engine sets the action to *trap*
while it runs. A trap hands the frame to the port named in `EXT_CPU_CTRL`,
which names no port out of reset - that is why the trap first looked like no
help at all: the receive counters stayed frozen while the partner kept sending,
and the frames were counted as ingress discards. `trap_init()` points the
register at the CPU port once at startup.
The *forward* action would reach the CPU as well, since the CPU port is in the
forwarding domain, but it leaves the frame in the L2 lookup and so floods it
across the ingress VLAN. That leaks LACPDUs to unrelated ports, and the damage
is real: a bond's own ports see each other's PDUs and Linux reports an illegal
loopback, a second bond on the same switch is poisoned, and the flood escapes
through uplinks. Containing it would take a static L2 multicast entry with a
CPU-only mask for every PVID in use, since untagged LACPDUs classify into the
ingress port's PVID and the forward action is not limited to the LACP ports.
The trap keys on the address instead, before the VLAN lookup and before the
ingress filters, which settles three things at once:
* Nothing is written per VLAN or per PVID, so delivery no longer depends on
which PVIDs the ports happen to carry.
* A port that admits tagged frames only still receives: an untagged forwarded
LACPDU is dropped by the acceptable-frame-type filter, a trapped one is not.
* A port STP holds in blocking still receives, so a group converges without
waiting out the forward delay.
A trapped frame arrives without the VLAN tag the hardware inserts into a
forwarded one, so the PDU body sits four bytes earlier; `TRAP_RX_BODY()` gives
the offset for both forms.
`lacp off` puts the action back to *drop*, which acts before the L2 lookup.
## LACP: what the implementation leaves out
The four state machines of Clause 43 are present in simplified form. Mux control
is coupled rather than independent, one partner system is elected per LAG, and
there are no churn detection machines.
One interoperability rule is worth stating because getting it wrong is silent.
The Partner block of an outgoing PDU has to echo the peer's own actor identity
verbatim, meaning system and priority, key, port and port priority, and the
aggregation flag. A Linux partner's `__record_pdu()` accepts our SYNC bit only
when that block mirrors what it sent. Hardcoding the priorities to zero
mismatches Linux's default of 0xffff, which clears partner SYNC, moves the state
byte from 0x3f to 0x37, and the bond never reaches collecting and distributing.