mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-10-29 17:54:14 +08:00
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:
1 parent
b83b67d234
commit
c3b4cc893b
31 files changed
+1885
-23
No files matched your search
@@ -59,15 +59,24 @@ $(BUILDDIR)/test_page_json: test_page_json.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR
|
||||
$(BUILDDIR)/test_httpd_tx: test_httpd_tx.c $(SRCROOT)/httpd/httpd.c $(SRCROOT)/uip/uip.c | $(BUILDDIR)
|
||||
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
|
||||
|
||||
# Sandboxes with a Makefile of their own; each fails the run when it fails.
|
||||
SUBTESTS = lacp port_hash
|
||||
|
||||
run: $(TESTS)
|
||||
@fail=0; for t in $(TESTS); do \
|
||||
echo "=== $$t ==="; \
|
||||
$$t || fail=1; \
|
||||
echo; \
|
||||
done; \
|
||||
for d in $(SUBTESTS); do \
|
||||
echo "=== $$d ==="; \
|
||||
$(MAKE) -C $$d test || fail=1; \
|
||||
echo; \
|
||||
done; \
|
||||
if [ $$fail -ne 0 ]; then echo "TESTS FAILED"; exit 1; else echo "ALL TESTS PASSED"; fi
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)
|
||||
@for d in $(SUBTESTS); do $(MAKE) -C $$d clean; done
|
||||
|
||||
.PHONY: all run clean
|
||||
@@ -48,6 +48,8 @@ fires — so it drops straight into CI.
|
||||
| `test_port_tables` | `rtl837x_port.c` | VLAN entry layout and round trip, PVID register sharing, static multicast and management entries, per-port flush, trunk membership and hash seed |
|
||||
| `test_page_json` | `httpd/page_impl.c` + `rtl837x_port.c` | `/vlan.json`, `/vlanlist`, `/l2.json` (walk, wrap marker, paging inside `outbuf`), 64-bit counters in `/status.json` and `/counters.json` |
|
||||
| `test_httpd_tx` | `httpd/httpd.c` + `uip/uip.c` | a GET of a static file against a client that moves its receive window: ACK accounting, continuation chunks out of flash, retransmission |
|
||||
| `lacp/` | `rtl837x_lacp.c` | state machines against a simulated 802.3ad partner: convergence, mis-cabling, expiry, re-election, two groups, LACPDU containment entries |
|
||||
| `port_hash/` | `port_lag_members_set()` | a LAG seeds its own hash register |
|
||||
|
||||
## Adding a test for another module
|
||||
1. Write `test_<module>.c` with `main()` driving the module's entry points and
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "syslog.h"
|
||||
#include "rtl837x_phy.h"
|
||||
#include "support.h"
|
||||
#include "rtl837x_lacp.h"
|
||||
|
||||
/* ---- console: everything lands in out_buf (support.c) ---- */
|
||||
void print_byte(uint8_t v)
|
||||
@@ -124,3 +125,18 @@ uint8_t stp_prio = 0x80, stp_root_port = 0xff, stp_rstp = 1, stp_txhold = 6;
|
||||
uint8_t sfp_buf[16];
|
||||
bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) { (void)slot; (void)reg; (void)len; return false; }
|
||||
void print_phys_port(uint8_t port) { itoa_short(machine.log_to_phys_port[port]); }
|
||||
|
||||
uint8_t lacpEnabled;
|
||||
uint8_t lacp_actor_state[10];
|
||||
uint8_t lacp_partner_state[10];
|
||||
uint8_t lacp_rx_state[10];
|
||||
uint8_t lacp_partner_sys[10][6];
|
||||
uint16_t lacp_rx_count[10];
|
||||
uint8_t lacp_port_lag[10] = {
|
||||
LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE,
|
||||
LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE,
|
||||
};
|
||||
uint16_t lacp_lag_ports[LACP_NUM_LAGS];
|
||||
uint8_t lacp_agg_sys[LACP_NUM_LAGS][6];
|
||||
uint8_t lacp_agg_valid[LACP_NUM_LAGS];
|
||||
uint16_t lacp_members_last[LACP_NUM_LAGS];
|
||||
@@ -0,0 +1 @@
|
||||
build/
|
||||
@@ -0,0 +1,31 @@
|
||||
# Host-side sandbox for the firmware LACP state machines.
|
||||
#
|
||||
# Compiles the UNMODIFIED ../../rtl837x_lacp.c against shim headers: SDCC
|
||||
# storage-class keywords are blanked via -D, and the module copy lives in
|
||||
# build/ so quote-includes resolve to shim/ (its own directory has no real
|
||||
# platform headers) instead of the firmware tree. rtl837x_trap.h is copied
|
||||
# alongside it for the same reason: TRAP_RX_BODY() is what delivery by trap
|
||||
# rests on, so the test must use the real macro, not a shim restating it.
|
||||
CC ?= cc
|
||||
CFLAGS = -std=c11 -Wall -Wextra -Wno-unknown-pragmas \
|
||||
-D__xdata= -D__code= -D__banked= -D__reentrant= \
|
||||
-Ishim -I../..
|
||||
|
||||
all: test
|
||||
|
||||
test: build/lacp_sim
|
||||
./build/lacp_sim
|
||||
|
||||
verbose: build/lacp_sim
|
||||
./build/lacp_sim -v
|
||||
|
||||
build/lacp_sim: lacp_sim.c ../../rtl837x_lacp.c ../../rtl837x_lacp.h ../../rtl837x_trap.h shim/*.h
|
||||
mkdir -p build
|
||||
cp ../../rtl837x_lacp.c build/lacp_under_test.c
|
||||
cp ../../rtl837x_trap.h build/rtl837x_trap.h
|
||||
$(CC) $(CFLAGS) -o $@ lacp_sim.c build/lacp_under_test.c
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
.PHONY: all test verbose clean
|
||||
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* Host-side sandbox for the RTL837x LACP implementation.
|
||||
*
|
||||
* Compiles the UNMODIFIED rtl837x_lacp.c against shim headers (see shim/) and
|
||||
* drives it with synthetic LACPDUs from a simulated partner switch/bond. This
|
||||
* exercises the real state machines that ship in the firmware image - only
|
||||
* the platform below them (frame I/O, trunk registers, console) is mocked.
|
||||
*
|
||||
* Run: make -C test/lacp (exit code 0 = all scenarios pass)
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "rtl837x_common.h"
|
||||
#include "machine.h"
|
||||
#include "uip.h"
|
||||
#include "rtl837x_regs.h"
|
||||
#include "rtl837x_lacp.h"
|
||||
|
||||
/* ---------- platform mocks ---------- */
|
||||
|
||||
uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2];
|
||||
uint16_t uip_len;
|
||||
struct uip_eth_addr uip_ethaddr = { .addr = {0x02,0x11,0x22,0x33,0x44,0x55} };
|
||||
const struct machine machine = { .min_port = 0, .max_port = 7,
|
||||
.log_to_phys_port = {5, 1, 2, 3, 4, 6, 7, 8, 9} };
|
||||
struct machine_runtime machine_detected = { .isRTL8373 = 1 };
|
||||
uint8_t lacpEnabled;
|
||||
uint16_t management_vlan; /* mocked; slow-protocol path zeroes it per-frame */
|
||||
|
||||
static int verbose = 0;
|
||||
void print_string(char *s) { if (verbose) fputs(s, stdout); }
|
||||
void print_byte(uint8_t b) { if (verbose) printf("%02x", b); }
|
||||
void print_short(uint16_t v) { if (verbose) printf("%04x", v); }
|
||||
void write_char(char c) { if (verbose) putchar(c); }
|
||||
|
||||
/* Trunk programming mock: record last mask per LAG + call count.
|
||||
* hw_members mirrors LAG 0 so the single-aggregator scenarios read naturally. */
|
||||
static uint16_t hw_members_lag[4];
|
||||
static int hw_set_calls;
|
||||
#define hw_members hw_members_lag[0]
|
||||
void port_lag_members_set(uint8_t lag, uint16_t members)
|
||||
{
|
||||
if (lag < 4)
|
||||
hw_members_lag[lag] = members;
|
||||
hw_set_calls++;
|
||||
}
|
||||
|
||||
/* Reserved-multicast action mock: delivery of a LACPDU to the CPU rests on
|
||||
* the action of the slow-protocols entry alone, so record what the engine
|
||||
* sets. The real trap_rma_set() touches only the action field. */
|
||||
static uint16_t hw_rma_reg;
|
||||
static uint8_t hw_rma_act;
|
||||
static int hw_rma_calls;
|
||||
void trap_rma_set(uint16_t reg, uint8_t act);
|
||||
void trap_rma_set(uint16_t reg, uint8_t act)
|
||||
{
|
||||
hw_rma_reg = reg;
|
||||
hw_rma_act = act;
|
||||
hw_rma_calls++;
|
||||
}
|
||||
|
||||
/* ---------- on-wire mirrors (independent re-statement of the layout) ---------- */
|
||||
|
||||
struct wire_info {
|
||||
uint16_t sys_prio;
|
||||
uint8_t sys[6];
|
||||
uint16_t key;
|
||||
uint16_t port_prio;
|
||||
uint16_t port;
|
||||
uint8_t state;
|
||||
uint8_t reserved[3];
|
||||
};
|
||||
|
||||
struct wire_pdu_out { /* what lacp_send() writes at uip_buf+12 */
|
||||
uint8_t dst[6];
|
||||
uint8_t src[6];
|
||||
struct rtl_tag rtl_tag;
|
||||
uint16_t ethertype;
|
||||
uint8_t subtype;
|
||||
uint8_t version;
|
||||
uint8_t tlv_actor, actor_len;
|
||||
struct wire_info actor;
|
||||
uint8_t tlv_partner, partner_len;
|
||||
struct wire_info partner;
|
||||
uint8_t tlv_collector, collector_len;
|
||||
uint16_t collector_max_delay;
|
||||
uint8_t collector_reserved[12];
|
||||
uint8_t tlv_terminator, terminator_len;
|
||||
uint8_t terminator_reserved[50];
|
||||
};
|
||||
|
||||
struct wire_pdu_in { /* forwarded form: the hardware inserts a VLAN tag */
|
||||
uint8_t dst[6];
|
||||
uint8_t src[6];
|
||||
struct rtl_tag rtl_tag;
|
||||
struct vlan_tag vlan_tag;
|
||||
uint16_t ethertype;
|
||||
uint8_t subtype;
|
||||
uint8_t version;
|
||||
uint8_t tlv_actor, actor_len;
|
||||
struct wire_info actor;
|
||||
uint8_t tlv_partner, partner_len;
|
||||
struct wire_info partner;
|
||||
};
|
||||
|
||||
_Static_assert(sizeof(struct wire_info) == 18, "info TLV body must be 18");
|
||||
_Static_assert(sizeof(struct wire_pdu_out) == 132, "TX pdu must be 132");
|
||||
_Static_assert(sizeof(struct rtl_tag) == 8, "rtl tag must be 8");
|
||||
|
||||
/* ---------- TX capture ---------- */
|
||||
|
||||
static struct wire_pdu_out last_tx[10];
|
||||
static int tx_count[10];
|
||||
|
||||
void tcpip_output(void)
|
||||
{
|
||||
struct wire_pdu_out *p = (struct wire_pdu_out *)&uip_buf[RTL_FRAME_DESC_SIZE];
|
||||
uint16_t pmask = HTONS(p->rtl_tag.pmask);
|
||||
for (int port = 0; port < 10; port++) {
|
||||
if (pmask & (1u << port)) {
|
||||
last_tx[port] = *p;
|
||||
tx_count[port]++;
|
||||
}
|
||||
}
|
||||
uip_len = 0;
|
||||
}
|
||||
|
||||
/* ---------- simulated partner ---------- */
|
||||
|
||||
/* Deliver a LACPDU into the switch on `port`, from partner system `sys`.
|
||||
* pstate = partner's Actor_State flags. If `echo`, the partner echoes the
|
||||
* switch's last-seen actor block back (fresh view) - required for quiescence. */
|
||||
static void partner_frame_full(uint8_t port, const uint8_t sys[6], uint8_t pstate,
|
||||
int echo, uint16_t len, int tagged)
|
||||
{
|
||||
struct wire_pdu_in in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
|
||||
in.dst[0]=0x01; in.dst[1]=0x80; in.dst[2]=0xc2; in.dst[5]=0x02;
|
||||
memcpy(in.src, sys, 6);
|
||||
in.rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID);
|
||||
in.rtl_tag.pmask = HTONS(port); /* RX: 4-bit ingress port */
|
||||
in.vlan_tag.svlan = HTONS(0x8100); /* the tag's TPID, which marks the body offset */
|
||||
in.ethertype = HTONS(0x8809);
|
||||
in.subtype = 0x01;
|
||||
in.version = 0x01;
|
||||
|
||||
in.tlv_actor = 0x01; in.actor_len = 0x14;
|
||||
in.actor.sys_prio = HTONS(0x8000);
|
||||
memcpy(in.actor.sys, sys, 6);
|
||||
in.actor.key = HTONS(0x0011);
|
||||
in.actor.port_prio = HTONS(0x00ff);
|
||||
in.actor.port = HTONS((uint16_t)port + 101);
|
||||
in.actor.state = pstate;
|
||||
|
||||
in.tlv_partner = 0x02; in.partner_len = 0x14;
|
||||
if (echo) { /* fresh view of us, from our last TX */
|
||||
in.partner.sys_prio = last_tx[port].actor.sys_prio;
|
||||
memcpy(in.partner.sys, last_tx[port].actor.sys, 6);
|
||||
in.partner.key = last_tx[port].actor.key;
|
||||
in.partner.port = last_tx[port].actor.port;
|
||||
in.partner.state = last_tx[port].actor.state;
|
||||
} /* else: zeros = stale view */
|
||||
|
||||
memcpy(uip_buf, &in, sizeof(in));
|
||||
if (!tagged) { /* trapped: no VLAN tag, so the body sits earlier */
|
||||
uint8_t *body = uip_buf + RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE;
|
||||
memmove(body, body + VLAN_TAG_SIZE,
|
||||
sizeof(in) - (RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE + VLAN_TAG_SIZE));
|
||||
len -= VLAN_TAG_SIZE;
|
||||
}
|
||||
uip_len = len;
|
||||
lacp_in();
|
||||
}
|
||||
|
||||
static void partner_frame_len(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo, uint16_t len)
|
||||
{
|
||||
partner_frame_full(port, sys, pstate, echo, len, 1);
|
||||
}
|
||||
|
||||
static void partner_frame(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo)
|
||||
{
|
||||
partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 1);
|
||||
}
|
||||
|
||||
/* The same LACPDU as the reserved-multicast trap delivers it. */
|
||||
static void partner_frame_trap(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo)
|
||||
{
|
||||
partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 0);
|
||||
}
|
||||
|
||||
/* Advance time by n system ticks; the LACP timers step every fourth one */
|
||||
volatile uint32_t ticks;
|
||||
static void step(int n) { while (n--) { ticks++; lacp_timers(); } }
|
||||
|
||||
/* ---------- scenario runner ---------- */
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, name) do { \
|
||||
if (cond) printf("PASS %s\n", name); \
|
||||
else { printf("FAIL %s\n", name); failures++; } \
|
||||
} while (0)
|
||||
|
||||
#define P_ACT LACP_STATE_ACTIVITY
|
||||
#define P_AGG LACP_STATE_AGGREGATION
|
||||
#define P_SYNC LACP_STATE_SYNC
|
||||
#define P_COL LACP_STATE_COLLECTING
|
||||
#define P_DIST LACP_STATE_DISTRIBUTING
|
||||
#define P_TO LACP_STATE_TIMEOUT /* partner requests short (fast) timeout */
|
||||
|
||||
static const uint8_t SYS_A[6] = {0x02,0xaa,0xaa,0xaa,0xaa,0x01};
|
||||
static const uint8_t SYS_B[6] = {0x02,0xbb,0xbb,0xbb,0xbb,0x02};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
verbose = (argc > 1 && !strcmp(argv[1], "-v"));
|
||||
|
||||
/* Boot init first: on host, zeroed globals would otherwise read as "every
|
||||
* port belongs to LAG 0" (the firmware calls this from main() the same way) */
|
||||
lacp_init();
|
||||
|
||||
/* T1: enable announces on every port with sane field contents */
|
||||
lacp_lag_set(0, 0x00ff);
|
||||
step(8);
|
||||
int all_tx = 1, sane = 1;
|
||||
for (int i = machine.min_port; i <= machine.max_port; i++) {
|
||||
if (!tx_count[i]) all_tx = 0;
|
||||
if (last_tx[i].ethertype != HTONS(0x8809) || last_tx[i].subtype != 1
|
||||
|| last_tx[i].dst[5] != 0x02
|
||||
|| !(last_tx[i].actor.state & (P_ACT|P_AGG|P_TO)))
|
||||
sane = 0;
|
||||
}
|
||||
CHECK(all_tx && sane, "T1 enable: LACPDU announced on all ports, well-formed");
|
||||
|
||||
/* T2: partner without SYNC -> actor SYNC only, no members yet */
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO, 1);
|
||||
step(8);
|
||||
CHECK((lacp_actor_state[0] & P_SYNC) && !(lacp_actor_state[0] & (P_COL|P_DIST))
|
||||
&& hw_members == 0,
|
||||
"T2 partner not in sync: actor SYNC only, trunk empty");
|
||||
|
||||
/* T3: partner in sync on ports 0,1 -> full converge, members 0x0003 */
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(hw_members == 0x0003
|
||||
&& (lacp_actor_state[0] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST)
|
||||
&& (lacp_actor_state[1] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST),
|
||||
"T3 convergence: both ports collecting/distributing, trunk 0x0003");
|
||||
|
||||
/* T3b: partner-block echo must be VERBATIM. A Linux 802.3ad partner accepts
|
||||
* our SYNC only if our Partner TLV mirrors its actor identity exactly; a
|
||||
* hardcoded sys_prio/port_prio (instead of the recorded value) makes Linux
|
||||
* clear partner-SYNC and the bond never distributes. Verify we echo the
|
||||
* partner's own priorities back, not a constant. */
|
||||
CHECK(HTONS(last_tx[0].partner.sys_prio) == 0x8000
|
||||
&& HTONS(last_tx[0].partner.port_prio) == 0x00ff
|
||||
&& HTONS(last_tx[0].partner.key) == 0x0011
|
||||
&& HTONS(last_tx[0].partner.port) == (uint16_t)0 + 101,
|
||||
"T3b partner echo: recorded partner priorities/key/port echoed verbatim");
|
||||
|
||||
/* T4: mis-cabling - port 2 sees a DIFFERENT system: must stay out */
|
||||
partner_frame(2, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(hw_members == 0x0003 && !(lacp_actor_state[2] & P_SYNC),
|
||||
"T4 mis-cabling: port with different partner system stays out");
|
||||
|
||||
/* T5: register-write economy: stable state must not rewrite the trunk */
|
||||
int calls_before = hw_set_calls;
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(64);
|
||||
CHECK(hw_set_calls == calls_before,
|
||||
"T5 stable state: no redundant trunk register writes");
|
||||
|
||||
/* T6: stale echo triggers retransmit (update_NTT) */
|
||||
int txc = tx_count[0];
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 0 /* stale view of us */);
|
||||
step(8);
|
||||
CHECK(tx_count[0] > txc, "T6 update_NTT: stale partner view causes retransmit");
|
||||
|
||||
/* T7: expiry - partner silent past the short timeout: trunk drains,
|
||||
* aggregator identity is released */
|
||||
step(4 * LACP_SHORT_TIMEOUT + 64); /* past the short timeout */
|
||||
CHECK(hw_members == 0 && lacp_agg_valid[0] == 0,
|
||||
"T7 expiry: members drop to 0 and aggregator is released");
|
||||
|
||||
/* T8: re-convergence with a NEW partner system after release */
|
||||
/* (needs a fresh announce first so the echo below carries current state) */
|
||||
step(4 * 0x0100 + 8); /* let periodic TX refresh last_tx */
|
||||
partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(hw_members == 0x0008 && (lacp_actor_state[3] & P_COL),
|
||||
"T8 re-election: new partner system forms a fresh aggregate");
|
||||
|
||||
/* T9: lacp off clears the trunk */
|
||||
lacp_off();
|
||||
CHECK(hw_members == 0 && lacpEnabled == 0, "T9 disable: trunk cleared");
|
||||
|
||||
/* ---- per-LAG scenarios: two independent aggregators ---- */
|
||||
|
||||
/* T10: lag 1 lacp {0,1} + lag 2 lacp {2,3}, both to the SAME partner
|
||||
* system (worst case for sibling scoping): each converges into its own
|
||||
* hardware trunk, and the engine reports enabled. */
|
||||
lacp_lag_set(1, 0x0003); /* ports 0,1 */
|
||||
lacp_lag_set(2, 0x000c); /* ports 2,3 */
|
||||
step(8); /* announce */
|
||||
for (int r = 0; r < 2; r++) { /* two rounds so echoes carry fresh state */
|
||||
for (int p = 0; p < 4; p++)
|
||||
partner_frame(p, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
CHECK(lacpEnabled == 1
|
||||
&& hw_members_lag[1] == 0x0003 && hw_members_lag[2] == 0x000c,
|
||||
"T10 two LAGs: independent aggregates converge on their own trunks");
|
||||
|
||||
/* T11: removing one LAG releases only its trunk; the other keeps running */
|
||||
lacp_lag_set(1, 0);
|
||||
CHECK(hw_members_lag[1] == 0 && hw_members_lag[2] == 0x000c
|
||||
&& lacpEnabled == 1,
|
||||
"T11 remove one LAG: its trunk drains, the other LAG unaffected");
|
||||
|
||||
/* T12: a port in no LACP LAG ignores LACPDUs entirely */
|
||||
int rx5_before = tx_count[5];
|
||||
partner_frame(5, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
CHECK(!(lacp_actor_state[5] & P_SYNC) && hw_members_lag[2] == 0x000c,
|
||||
"T12 unassigned port: LACPDU ignored, no state change");
|
||||
(void)rx5_before;
|
||||
|
||||
/* T13: removing the last LACP LAG shuts the engine down */
|
||||
lacp_lag_set(2, 0);
|
||||
CHECK(lacpEnabled == 0 && hw_members_lag[2] == 0
|
||||
&& hw_rma_reg == RTL837X_RMA2_CONF && hw_rma_act == RTL837X_RMA_ACT_DROP,
|
||||
"T13 last LAG removed: engine off, trunk drained, address dropped");
|
||||
|
||||
/* T14: while the engine runs, the slow-protocols address is trapped to
|
||||
* the CPU. The trap keys on the address alone, so delivery needs no L2
|
||||
* entry and nothing written per VLAN or per PVID. */
|
||||
hw_rma_calls = 0;
|
||||
lacp_lag_set(0, 0x0003);
|
||||
CHECK(hw_rma_calls == 1 && hw_rma_reg == RTL837X_RMA2_CONF
|
||||
&& hw_rma_act == RTL837X_RMA_ACT_TRAP,
|
||||
"T14 engine on traps 01:80:c2:00:00:02 to the CPU");
|
||||
|
||||
/* T15: a trapped LACPDU arrives without the VLAN tag a forwarded one
|
||||
* carries, so its body sits four bytes earlier. Both forms must parse. */
|
||||
uint16_t rx_before = lacp_rx_count[0];
|
||||
lacp_partner_state[0] = 0; /* all three are written past the ethertype gate */
|
||||
memset(lacp_partner_sys[0], 0, 6);
|
||||
partner_frame_trap(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
int trap_ok = lacp_rx_count[0] == (uint16_t)(rx_before + 1)
|
||||
&& lacp_partner_state[0] == (P_ACT|P_AGG|P_TO|P_SYNC)
|
||||
&& !memcmp(lacp_partner_sys[0], SYS_A, 6);
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
CHECK(trap_ok && lacp_rx_count[0] == (uint16_t)(rx_before + 2),
|
||||
"T15 trapped LACPDU parses like a forwarded one");
|
||||
|
||||
/* T16: the partner changes one cable at a time. Port 1 starts hearing B
|
||||
* while port 0 still hears A, then A goes quiet and B appears on port 0
|
||||
* too. The aggregator must let go of A once no port hears it, even though
|
||||
* port 1 was CURRENT the whole time, or the LAG never aggregates with B. */
|
||||
lacp_lag_set(0, 0);
|
||||
lacp_lag_set(0, 0x0003);
|
||||
step(8);
|
||||
for (int r = 0; r < 3; r++) {
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
for (int t = 0; t < 40; t++) { /* port 1 moved to B, port 0 still on A */
|
||||
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(4 * 40);
|
||||
}
|
||||
int stayed_out = hw_members == 0x0001 && !(lacp_actor_state[1] & P_SYNC);
|
||||
for (int t = 0; t < 60; t++) /* port 0 unplugged from A, past the short timeout */
|
||||
{ partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); step(4 * 40); }
|
||||
for (int t = 0; t < 40; t++) { /* port 0 now on B as well */
|
||||
partner_frame(0, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(4 * 40);
|
||||
}
|
||||
CHECK(stayed_out && hw_members == 0x0003 && !memcmp(lacp_agg_sys[0], SYS_B, 6),
|
||||
"T16 re-cabling one port at a time: aggregator moves from A to B");
|
||||
|
||||
/* T17: a group that already has a STATIC trunk is put under LACP with the
|
||||
* same ports (the LAG page's Static -> LACP switch sends only "lag N lacp").
|
||||
* The static trunk must go: until the partner converges the ports are not
|
||||
* an aggregate, and after a reboot the config replays only the LACP line. */
|
||||
hw_members_lag[1] = 0x0030; /* static lag 2: ports 4,5 */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
CHECK(hw_members_lag[1] == 0 && lacp_members_last[1] == 0 && lacp_lag_ports[1] == 0x0030,
|
||||
"T17 static trunk taken over: cleared while the group negotiates");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
/* T18: a port named in a second group moves there. The first group's
|
||||
* candidate set drops it, so taking the second group out later leaves no
|
||||
* orphan that group 1 still shows as a candidate but never runs LACP on. */
|
||||
lacp_lag_set(1, 0x0030); /* ports 4,5 */
|
||||
step(8);
|
||||
for (int r = 0; r < 2; r++) {
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
int converged = hw_members_lag[1] == 0x0030;
|
||||
lacp_lag_set(2, 0x0060); /* ports 5,6: port 5 moves from group 2 to 3 */
|
||||
int moved = lacp_lag_ports[1] == 0x0010 && lacp_port_lag[5] == 2
|
||||
&& hw_members_lag[1] == 0x0010;
|
||||
lacp_lag_set(2, 0);
|
||||
int no_orphan = lacp_port_lag[5] == LACP_LAG_NONE && lacp_lag_ports[1] == 0x0010;
|
||||
lacp_lag_set(3, 0x0010); /* the last port leaves group 2 as well */
|
||||
CHECK(converged && moved && no_orphan && lacp_lag_ports[1] == 0 && lacp_agg_valid[1] == 0,
|
||||
"T18 port moves between groups: old group drops it from candidates and trunk");
|
||||
lacp_lag_set(3, 0);
|
||||
|
||||
/* T20: taking a group out of LACP leaves nothing of the partner behind
|
||||
* for "lacp show" to report as if the port still heard it. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
int had_partner = lacp_rx_state[4] == LACP_RX_CURRENT && lacp_rx_count[4] > 0;
|
||||
lacp_lag_set(1, 0);
|
||||
CHECK(had_partner && lacp_partner_state[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE
|
||||
&& lacp_rx_count[4] == 0 && !memcmp(lacp_partner_sys[4], "\0\0\0\0\0\0", 6),
|
||||
"T20 release: partner state, rx state, counter and system cleared");
|
||||
|
||||
/* T21: applying the same candidate set again (the LAG page resends it
|
||||
* with every Apply) must not tear the trunk down and renegotiate. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
for (int r = 0; r < 2; r++) {
|
||||
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
|
||||
step(8);
|
||||
}
|
||||
int before = hw_set_calls;
|
||||
lacp_lag_set(1, 0x0030);
|
||||
CHECK(hw_members_lag[1] == 0x0030 && hw_set_calls == before
|
||||
&& (lacp_actor_state[4] & P_COL),
|
||||
"T21 same ports applied again: trunk and state untouched");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
/* T22: a runt on the slow-protocols address must not be parsed as a
|
||||
* LACPDU; the partner block would be read from stale buffer bytes. */
|
||||
lacp_lag_set(1, 0x0030);
|
||||
step(8);
|
||||
partner_frame_len(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1, 40); /* cut before the partner block */
|
||||
CHECK(lacp_rx_count[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE,
|
||||
"T22 truncated LACPDU ignored: the port does not record it");
|
||||
lacp_lag_set(1, 0);
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "SANDBOX: FAILURES" : "SANDBOX: ALL PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Host-build shim: rtl837x_lacp.c reads min_port/max_port and, for the
|
||||
* console dump, the front-panel numbering. The real map is not the identity
|
||||
* on any machine, so the harness uses a shuffled one to keep a logical index
|
||||
* from passing as a physical port by accident. */
|
||||
#ifndef _SHIM_MACHINE_H_
|
||||
#define _SHIM_MACHINE_H_
|
||||
#include <stdint.h>
|
||||
struct machine { uint8_t min_port; uint8_t max_port; uint8_t log_to_phys_port[9]; };
|
||||
extern const struct machine machine;
|
||||
struct machine_runtime { uint8_t isRTL8373; };
|
||||
extern struct machine_runtime machine_detected;
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Host-build shim for rtl837x_common.h - just enough for rtl837x_lacp.c.
|
||||
* Layouts of rtl_tag/vlan_tag and the frame-desc size mirror the real header;
|
||||
* the harness static-asserts the resulting on-wire struct sizes.
|
||||
*/
|
||||
#ifndef _SHIM_RTL837X_COMMON_H_
|
||||
#define _SHIM_RTL837X_COMMON_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h> /* memcpy/memset - firmware provides its own */
|
||||
|
||||
struct rtl_tag {
|
||||
uint16_t tag;
|
||||
uint8_t version;
|
||||
uint8_t reason;
|
||||
uint16_t flags;
|
||||
uint16_t pmask; /* TX: port bitmask; RX: 4-bit port number */
|
||||
};
|
||||
|
||||
struct vlan_tag {
|
||||
uint16_t svlan;
|
||||
uint16_t vlan;
|
||||
};
|
||||
|
||||
#define RTL_TAG_SIZE (sizeof (struct rtl_tag))
|
||||
#define VLAN_TAG_SIZE (sizeof (struct vlan_tag))
|
||||
#define RTL_FRAME_TAG_ID 0x8899
|
||||
#define RTL_FRAME_TAG_VERSION 0x04
|
||||
#define RTL_FRAME_DESC_SIZE 12
|
||||
#define ETHER_HEADER_SIZE 14
|
||||
|
||||
#define HTONS(n) ((uint16_t)((((uint16_t)(n)) << 8) | (((uint16_t)(n)) >> 8)))
|
||||
|
||||
/* Console output - provided by the harness */
|
||||
void print_string(char *s);
|
||||
void print_byte(uint8_t b);
|
||||
void print_short(uint16_t v);
|
||||
void write_char(char c);
|
||||
|
||||
#endif
|
||||
|
||||
/* Host-build shim: port masks used by lacp_isolation_update() */
|
||||
#ifndef PMASK_CPU
|
||||
#define CPU_PORT 9
|
||||
#define PMASK_9 0x1ff
|
||||
#define PMASK_6 0x1f8
|
||||
#define PMASK_CPU 0x200
|
||||
#endif
|
||||
|
||||
/* Host-build shim: directed-egress/strip-tag bit (bit15 of TX pmask word) */
|
||||
#ifndef RTL_TAG_ALLOW
|
||||
#define RTL_TAG_ALLOW 0x8000
|
||||
#endif
|
||||
|
||||
/* Host-build shim: tag flags word bits (tag_rtl8_4) */
|
||||
#ifndef RTL_TAG_LEARN_DIS
|
||||
#define RTL_TAG_LEARN_DIS 0x0020
|
||||
#define RTL_TAG_KEEP 0x0080
|
||||
#define SYS_TICK_HZ 200
|
||||
extern volatile uint32_t ticks;
|
||||
#define TICKS_DUE(last, period) ((uint8_t)((uint8_t)ticks - (last)) >= (period))
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Host-build shim: the harness mocks trunk programming and records calls */
|
||||
#ifndef _SHIM_RTL837X_PORT_H_
|
||||
#define _SHIM_RTL837X_PORT_H_
|
||||
#include <stdint.h>
|
||||
void port_lag_members_set(uint8_t lag, uint16_t members);
|
||||
void port_isolate(uint8_t port, uint16_t pmask);
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Host-build shim for rtl837x_regs.h - the register numbers rtl837x_lacp.c
|
||||
* touches, with the values from the real header so a wrong constant shows up
|
||||
* as a failing expectation rather than as a silent no-op.
|
||||
*/
|
||||
#ifndef _SHIM_RTL837X_REGS_H_
|
||||
#define _SHIM_RTL837X_REGS_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define RTL837X_RMA2_CONF 0x4ed4
|
||||
#define RTL837X_RMA_ACT_TRAP 0x10
|
||||
#define RTL837X_RMA_ACT_DROP 0x20
|
||||
|
||||
/* Registers touched by lacp_send's CPU-port targeting */
|
||||
#define RTL837X_PORT_ISOLATION_BASE 0x50c0
|
||||
#define RTL837X_REG_NIC_TXCMD 0x7850
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Host-build shim for uip.h - frame buffer + TX hook used by rtl837x_lacp.c */
|
||||
#ifndef _SHIM_UIP_H_
|
||||
#define _SHIM_UIP_H_
|
||||
#include <stdint.h>
|
||||
#define UIP_CONF_BUFFER_SIZE 600
|
||||
struct uip_eth_addr { uint8_t addr[6]; };
|
||||
extern uint16_t uip_len;
|
||||
void tcpip_output(void);
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
test_lag_hash
|
||||
port_lag_members_set.inc
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -148,6 +148,7 @@ void send_mirror(void) { }
|
||||
void send_mtu(void) { }
|
||||
void send_config(void) { }
|
||||
void send_cmd_log(void) { }
|
||||
void send_lacp(void) { }
|
||||
void send_lag(void) { }
|
||||
void send_stp(void) { }
|
||||
void send_stp_counters(void) { }
|
||||
|
||||
Reference in new issue
Block a user