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

+2
View File
@@ -0,0 +1,2 @@
test_lag_hash
port_lag_members_set.inc
+6
View File
@@ -0,0 +1,6 @@
test: port_lag_members_set.inc
gcc -Wall -Wextra -O1 -o test_lag_hash test_lag_hash.c && ./test_lag_hash
port_lag_members_set.inc: ../../rtl837x_port.c
awk '/^void port_lag_members_set/,/^}/' $< > $@
clean:
rm -f test_lag_hash port_lag_members_set.inc
+68
View File
@@ -0,0 +1,68 @@
/* Host test for port_lag_members_set(): the default load-balancing hash must be
* seeded into the CURRENT trunk's register (BASE + lag*4), never trunk 0's, and
* must not overwrite a hash that is already set. Compiles the REAL function
* (extracted verbatim from rtl837x_port.c) against register-recording shims. */
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define __banked
#define __xdata
#define __code
/* register defs (mirror rtl837x_regs.h) */
#define RTL837X_TRK_MBR_CTRL_BASE 0x4f38
#define RTL837X_TRK_HASH_CTRL_BASE 0x4f48
#define LAG_HASH_SOURCE_PORT_NUMBER 0x01
#define LAG_HASH_L2_SMAC 0x02
#define LAG_HASH_L2_DMAC 0x04
#define LAG_HASH_L3_SIP 0x08
#define LAG_HASH_L3_DIP 0x10
#define LAG_HASH_L4_SPORT 0x20
#define LAG_HASH_L4_DPORT 0x40
#define LAG_HASH_DEFAULT (LAG_HASH_L2_SMAC|LAG_HASH_L2_DMAC|LAG_HASH_L3_SIP|LAG_HASH_L3_DIP|LAG_HASH_L4_SPORT|LAG_HASH_L4_DPORT)
#define LAG_HASH_RESET (LAG_HASH_SOURCE_PORT_NUMBER|LAG_HASH_L2_SMAC|LAG_HASH_L2_DMAC|LAG_HASH_L3_SIP|LAG_HASH_L3_DIP|LAG_HASH_L4_SPORT)
uint8_t SFR_DATA_24, SFR_DATA_16, SFR_DATA_8, SFR_DATA_0;
uint8_t sfr_data[4];
static uint32_t reg_store[0x10000];
void reg_read_m(uint16_t r){ uint32_t v=reg_store[r];
sfr_data[0]=(v>>24)&0xff; sfr_data[1]=(v>>16)&0xff; sfr_data[2]=(v>>8)&0xff; sfr_data[3]=v&0xff; }
void reg_write(uint16_t r){ reg_store[r]=((uint32_t)SFR_DATA_24<<24)|((uint32_t)SFR_DATA_16<<16)|((uint32_t)SFR_DATA_8<<8)|SFR_DATA_0; }
void print_string(char*s){(void)s;} void print_byte(uint8_t b){(void)b;}
void print_short(uint16_t s){(void)s;} void write_char(char c){(void)c;}
#define REG_SET(r,v) do{ SFR_DATA_24=((uint32_t)(v)>>24)&0xff; SFR_DATA_16=((uint32_t)(v)>>16)&0xff; \
SFR_DATA_8=((uint32_t)(v)>>8)&0xff; SFR_DATA_0=(v)&0xff; reg_write(r); }while(0)
#define REG_WRITE(r,a,b,c,d) do{ SFR_DATA_24=(a); SFR_DATA_16=(b); SFR_DATA_8=(c); SFR_DATA_0=(d); reg_write(r); }while(0)
/* THE REAL FUNCTION (extracted from rtl837x_port.c at build time) */
#include "port_lag_members_set.inc"
static int fails;
#define CK(cond,msg,...) do{ if(cond){printf("PASS " msg "\n",##__VA_ARGS__);} else {printf("FAIL " msg "\n",##__VA_ARGS__); fails++;} }while(0)
int main(void){
for(int lag=0; lag<4; lag++){
memset(reg_store,0,sizeof(reg_store));
port_lag_members_set(lag, 0x0180);
uint16_t h = RTL837X_TRK_HASH_CTRL_BASE + (lag<<2);
uint16_t m = RTL837X_TRK_MBR_CTRL_BASE + (lag<<2);
CK(reg_store[h]==LAG_HASH_DEFAULT, "lag%d: default 0x%02x -> hash reg 0x%04x", lag, LAG_HASH_DEFAULT, h);
CK(reg_store[m]==0x0180, "lag%d: members 0x0180 -> mbr reg 0x%04x", lag, m);
if(lag) CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE]==0, "lag%d: trunk-0 hash NOT clobbered", lag);
}
/* guard: existing hash must survive a later member change */
memset(reg_store,0,sizeof(reg_store));
reg_store[RTL837X_TRK_HASH_CTRL_BASE+(1<<2)] = 0x06; /* lag1 already L2 */
port_lag_members_set(1, 0x00c0);
CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE+(1<<2)]==0x06, "guard: preset lag1 hash 0x06 preserved");
/* the chip's reset value counts as unset and is replaced by the default */
memset(reg_store,0,sizeof(reg_store));
reg_store[RTL837X_TRK_HASH_CTRL_BASE+(2<<2)] = LAG_HASH_RESET;
port_lag_members_set(2, 0x0003);
CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE+(2<<2)]==LAG_HASH_DEFAULT, "reset value 0x%02x replaced by default on lag2", LAG_HASH_RESET);
printf(fails? "\n=== FAIL (%d) ===\n" : "\n=== ALL PASS ===\n", fails);
return fails?1:0;
}