From f88f655f5a13b8cc7808f7bcd0794442e192b257 Mon Sep 17 00:00:00 2001 From: DrDoof <8052722+DrDoof@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:24:05 +0200 Subject: [PATCH 01/20] igmp: leave groups in 224.0.0.0/24 to flooding The switch keeps flooding 224.0.0.0/24 with IGMP on, but a report for a group in that block still made an entry for it, and the entry limited the group to the ports that reported it. A host announcing mDNS on 224.0.0.251 thereby cut mDNS off from every other port. Such reports are now ignored, as RFC 4541 asks. --- doc/igmp.md | 3 +++ rtl837x_igmp.c | 2 ++ rtl837x_igmp.h | 1 + 3 files changed, 6 insertions(+) diff --git a/doc/igmp.md b/doc/igmp.md index 1584287f..f54f8bfa 100644 --- a/doc/igmp.md +++ b/doc/igmp.md @@ -85,6 +85,9 @@ for frames to 01:00:5e:xx:xx:xx and returns 0 when the frame is not a trapped IG so that it is passed on to the IP stack. IGMPv1/v2 reports (0x12, 0x16) join the group given in the IGMP header, IGMPv2 leaves (0x17) leave it, IGMPv3 reports use the first group record (type 4 joins, type 3 leaves). Trapped IGMP frames arrive without a VLAN tag. +Reports for groups in 224.0.0.0/24 are ignored: the switch floods that block +even with IGMP on, and an entry for a group such as 224.0.0.251 (mDNS) would +otherwise limit it to the ports that reported it, as RFC 4541 warns against. `igmp_show()` prints out the IGMP configuration on the CLI. diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index 3ef2b2ec..51ed3abf 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -250,6 +250,8 @@ uint8_t igmp_packet_handler(void) __banked #ifdef DEBUG print_string("IGMP join "); print_byte(igmp_join); write_char('\n'); #endif + if (uip_buf[igmp_grp] == IGMP_LOCAL_NET_0 && !uip_buf[igmp_grp + 1] && !uip_buf[igmp_grp + 2]) + return 1; #ifdef IPMC_USES_L3MC memset((__xdata uint8_t *)&entry, 0, sizeof(struct ipmc_table_entry)); diff --git a/rtl837x_igmp.h b/rtl837x_igmp.h index d6d28f81..1b56e367 100644 --- a/rtl837x_igmp.h +++ b/rtl837x_igmp.h @@ -10,6 +10,7 @@ #define IGMP_V3_REPORT 0x22 #define IGMP_V3_TO_INCLUDE 3 #define IGMP_V3_TO_EXCLUDE 4 +#define IGMP_LOCAL_NET_0 224 void igmp_setup(void) __banked; void igmp_enable(void) __banked; From 24c0a957343f0e323428a2b0d6ca3235d62a9bd2 Mon Sep 17 00:00:00 2001 From: DrDoof <8052722+DrDoof@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:43:04 +0200 Subject: [PATCH 02/20] igmp: stop looking up IPv4 multicast in the L2 table on igmp off igmp_setup(), which `igmp off` also calls, enabled the lookup of IPv4 multicast in the L2 table. Groups learned while IGMP was on therefore kept going only to the ports that had reported them, and with reports no longer trapped nothing updated those entries until a reboot. The lookup is now enabled in igmp_enable() and disabled in igmp_setup(), so with IGMP off all IP multicast is flooded again. --- doc/igmp.md | 3 ++- rtl837x_igmp.c | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/igmp.md b/doc/igmp.md index 1584287f..adce94f2 100644 --- a/doc/igmp.md +++ b/doc/igmp.md @@ -73,7 +73,8 @@ uint8_t igmp_packet_handler(void) __banked; void igmp_show(void) __banked; ```c `igmp_setup()` is called at boot-time and configures flooding of all IP-MC packets by -default, as otherwise no IP-MC would be possible in the network. +default, as otherwise no IP-MC would be possible in the network. `igmp off` calls it as +well; groups learned while IGMP was on stay in the table, but are no longer looked up. `igmp_enable()`starts IGMP which cause IGMP packets to be handled by the CPU and forwarding of IP-MC packets to be limited to only subscribed ports. diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index 3ef2b2ec..69cd8568 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -107,8 +107,7 @@ void igmp_setup(void) __banked REG_SET(RTL837X_IPV4_UNKN_MC_FLD_PMSK, mask); REG_SET(RTL837X_IPV6_UNKN_MC_FLD_PMSK, mask); - // Enable lookup of IPv4 MC addresses in table - reg_bit_set(RTL837X_L2_CTRL, L2_CTRL_LUT_IPMC_HASH); + reg_bit_clear(RTL837X_L2_CTRL, L2_CTRL_LUT_IPMC_HASH); /* Configure per-port IGMP operations when protocol messages are received * bits 0-9 enable MC protocol snooping @@ -145,6 +144,8 @@ void igmp_enable(void) __banked { print_string("igmp_enable called\n"); igmpEnabled = 1; + // Enable lookup of IPv4 MC addresses in table + reg_bit_set(RTL837X_L2_CTRL, L2_CTRL_LUT_IPMC_HASH); // Configure trapping of unhandled IGMP protocol packets to CPU REG_SET(RTL837X_IGMP_TRAP_CFG, IGMP_CPU_PORT | IGMP_TRAP_PRIORITY); From c3b4cc893b3a9b4cae6d4e85e484834569262c90 Mon Sep 17 00:00:00 2001 From: DrDoof <8052722+DrDoof@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:13:30 +0200 Subject: [PATCH 03/20] 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 lacp "; 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". --- Makefile | 1 + README.md | 1 + cmd_parser.c | 29 +- doc/CpuPort.md | 5 + doc/lacp.md | 231 ++++++++++++ doc/link_aggregation.md | 49 +++ html/app.js | 52 ++- html/index.html | 5 + httpd/httpd.c | 2 + httpd/page_impl.c | 81 ++++- httpd/page_impl.h | 1 + rtl837x_igmp.c | 4 +- rtl837x_lacp.c | 607 ++++++++++++++++++++++++++++++++ rtl837x_lacp.h | 101 ++++++ rtl837x_regs.h | 4 + rtlplayground.c | 25 +- test/Makefile | 9 + test/README.md | 2 + test/env_tables.c | 16 + test/lacp/.gitignore | 1 + test/lacp/Makefile | 31 ++ test/lacp/lacp_sim.c | 464 ++++++++++++++++++++++++ test/lacp/shim/machine.h | 12 + test/lacp/shim/rtl837x_common.h | 63 ++++ test/lacp/shim/rtl837x_port.h | 7 + test/lacp/shim/rtl837x_regs.h | 19 + test/lacp/shim/uip.h | 9 + test/port_hash/.gitignore | 2 + test/port_hash/Makefile | 6 + test/port_hash/test_lag_hash.c | 68 ++++ test/test_httpd_tx.c | 1 + 31 files changed, 1885 insertions(+), 23 deletions(-) create mode 100644 doc/lacp.md create mode 100644 rtl837x_lacp.c create mode 100644 rtl837x_lacp.h create mode 100644 test/lacp/.gitignore create mode 100644 test/lacp/Makefile create mode 100644 test/lacp/lacp_sim.c create mode 100644 test/lacp/shim/machine.h create mode 100644 test/lacp/shim/rtl837x_common.h create mode 100644 test/lacp/shim/rtl837x_port.h create mode 100644 test/lacp/shim/rtl837x_regs.h create mode 100644 test/lacp/shim/uip.h create mode 100644 test/port_hash/.gitignore create mode 100644 test/port_hash/Makefile create mode 100644 test/port_hash/test_lag_hash.c diff --git a/Makefile b/Makefile index 8c8ca72c..68d416f4 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ SRCS += \ rtl837x_stp.c \ rtl837x_lldp.c \ rtl837x_storm.c \ + rtl837x_lacp.c \ rtl837x_trap.c SRCS += \ httpd/httpd.c \ diff --git a/README.md b/README.md index 6eaf74e8..b605ca5f 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,7 @@ The following documents give further documentation on specific features of the R - [IGMP (IP-MC streaming)](doc/igmp.md) - [SFP+ ports](doc/sfp.md) - [Trunking aka. port aggregation](doc/trunking.md) +- [LACP (802.3ad link aggregation)](doc/lacp.md) - [VLAN](doc/vlan.md) - [Storm control](doc/storm_control.md) - [Modifications and Flash replacement](doc/mods.md) diff --git a/cmd_parser.c b/cmd_parser.c index e7daa9e8..951d6be5 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -14,6 +14,7 @@ #include "rtl837x_regs.h" #include "rtl837x_sfr.h" #include "rtl837x_stp.h" +#include "rtl837x_lacp.h" #include "rtl837x_igmp.h" #include "rtl837x_bandwidth.h" #include "rtl837x_lldp.h" @@ -537,7 +538,18 @@ void parse_lag(void) return; } + /* "lag lacp [port]..." - run LACP on this LAG with the given candidate + * ports; "lag lacp off" (or no ports) removes the LAG from LACP. */ uint8_t w = 2; + __xdata uint8_t lacp_mode = 0; + if (cmd_words_len > 2 && cmd_compare(2, "lacp")) { + lacp_mode = 1; + w = 3; + if (cmd_words_len > 3 && cmd_compare(3, "off")) { + lacp_lag_set(group, 0); + return; + } + } while (w < cmd_words_len) { // Parse port. if (cmd_parse_port_separator(cmd_words_b[w++]) == 0) @@ -545,10 +557,13 @@ void parse_lag(void) members |= ((uint16_t)1) << atoi_results_u8; } - port_lag_members_set(group, members); + if (lacp_mode) + lacp_lag_set(group, members); /* 0 ports also valid: removes the LAG */ + else + port_lag_members_set(group, members); return; err: - cmd_error("lag (show | <1-4> (d | ...))\n"); + cmd_error("lag (show | <1-4> (d | [lacp] ...))\n"); } @@ -1972,11 +1987,19 @@ void cmd_parser(void) __banked } else if (cmd_compare(0, "health")) { health_show(); #endif + } else if (cmd_compare(0, "lacp")) { + if (cmd_compare(1, "show")) + lacp_show(); + else if (cmd_compare(1, "off")) + lacp_off(); + else + cmd_error("lacp (show | off); a group starts LACP with: lag <1-4> lacp ...\n"); } else if (cmd_compare(0, "pvid")) { if (cmd_words_len == 3 && cmd_parse_port_separator(cmd_words_b[1]) != 0 && atoi_short(cmd_words_b[2]) && atoi_results_short && atoi_results_short <= 4094) + { port_pvid_set(atoi_results_u8, atoi_results_short); - else { + } else { cmd_error("pvid <1-4094>\n"); } } else if (cmd_compare(0, "vlan")) { diff --git a/doc/CpuPort.md b/doc/CpuPort.md index c10d0a15..f5e9859b 100644 --- a/doc/CpuPort.md +++ b/doc/CpuPort.md @@ -79,6 +79,11 @@ bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX All fields are in network byte order. +This word has to be written through `HTONS` like every other field of the tag. +Writing a constant raw puts the bits in the wrong byte, so `0x0020` reaches the +wire as `0x2000`, which is EFID rather than LEARN_DIS. The ASIC then fails to +parse the tag and forwards the frame with the `0x8899` header still on it. + * `EFID_EN`, `EFID`: look the destination up under this filtering ID instead of the port's own * `PRI_EN`, `PRI`: force the given priority on the frame diff --git a/doc/lacp.md b/doc/lacp.md new file mode 100644 index 00000000..1b78459e --- /dev/null +++ b/doc/lacp.md @@ -0,0 +1,231 @@ +# LACP (Link Aggregation Control Protocol, IEEE 802.3ad) + +A static link aggregation group works only if both ends agree about it in +advance. Nothing checks that they do: cable a member to the wrong neighbour and +the group keeps hashing frames onto a link that goes somewhere else. LACP is the +protocol that makes the two ends agree, and keeps agreeing — it discovers which +links reach the same partner, aggregates only those, and drops a link out of the +group when the partner stops answering. + +The switch runs LACP in software on the 8051. Frames are the Slow Protocols +group: destination `01:80:C2:00:00:02`, ethertype `0x8809`, subtype 1. The +implementation covers the parts that decide whether an aggregate forms: +the Receive machine and the Selection and Mux machines of 802.3ad clause 43, +`recordPDU` and `update_NTT`, and the mis-cabling protection that refuses to +aggregate links whose partners differ. It does not implement the Churn Detection +machines or the Marker protocol, and the Mux machine is the coupled variant, so +collecting and distributing are enabled together rather than in sequence. + +Up to `LACP_NUM_LAGS` (4) groups can be placed under LACP; the rest stay static. + +## Getting the frames to the CPU + +Reserved multicast addresses are handled by the RMA block before the L2 lookup +happens. Each address has its own register and the action field sits in bits +[5:4]: 0 forward, 1 trap to CPU, 2 drop, 3 forward excluding the CPU. The +registers are packed and consecutive by *entry* rather than by address byte, so +the step of 4 counts entries, not bytes of the address: `...:00` sits at +`0x4ecc` and `...:02` at `0x4ed4`, but `...:08` sits at `0x4ee0`. + +``` +#define RTL837X_RMA0_CONF 0x4ecc /* 01:80:C2:00:00:00, entry 0 */ +#define RTL837X_RMA2_CONF 0x4ed4 /* 01:80:C2:00:00:02, Slow Protocols */ +#define RTL837X_RMA_ACT_MASK 0x30 +#define RTL837X_RMA_ACT_TRAP 0x10 +#define RTL837X_RMA_ACT_DROP 0x20 +``` + +The engine puts this address on *trap* while it runs. A trap takes the frame +out of the forwarding path and hands it to the port named in `EXT_CPU_CTRL` +(`0x6724`), whose reset value names no port at all: the destination is meant to +be an *external* CPU on a physical port, which these boards do not have. That +is why the trap first looked like a dead end - the receive counters stayed +frozen while the partner kept sending, and the ingress discard counter grew +instead. `trap_init()` points the register at the CPU port once at startup, and +the same frames then arrive. + +`FORWARD` reaches the CPU too, because the CPU port is in the forwarding +domain, but a forwarded frame is subject to the L2 lookup and on its own floods +to every other port in the VLAN. That leaks a neighbour's LACPDUs to hosts that +must not see them and, with two bonds on one switch, poisons both; a reserved +link-local group must not be forwarded at all. Containing it takes a static L2 +multicast entry with a CPU-only member mask, and because lookups are IVL on +this chip, one entry per distinct pvid over all front panel ports - a VID 0 +entry is never matched, and a port outside the LACP groups needs its pvid +covered as well, or a LACPDU arriving there misses the lookup and is flooded +through its VLAN. + +The trap needs none of that and reaches further, because it acts on the address +before the VLAN lookup and before the ingress filters. An untagged LACPDU is +delivered on a port that admits tagged frames only, where a forwarded one is +dropped by the acceptable-frame-type filter; and it is delivered on a port STP +holds in blocking, so a group converges without waiting out the forward delay. +Both were measured against forward on the same ports of an SWTGW218AS. + +A trapped frame arrives without the VLAN tag the hardware inserts into a +forwarded one, so the LACPDU body sits four bytes earlier. `TRAP_RX_BODY()` +gives the offset of the body for either form, and `lacp_in()` reads the PDU +from there. + +Neighbouring addresses in the same block still differ in other respects - +`...:00` is exempt from the ingress STP-state check and `...:02` is not, which +the trap makes moot only because it acts before that check - so measure before +carrying a conclusion about one of them across to another. + +When LACP is switched off the action goes to `DROP`, which acts before the L2 +lookup, so there is nothing to undo. + +## What has to match across the members + +802.3ad requires that every link in an aggregate be full duplex and run at the +same speed. Beyond the standard, anything that changes how a frame is treated +has to be identical across members, because the hash picks the member per frame +and a difference would make the behaviour depend on that choice: + +* must match: speed and duplex, VLAN membership and pvid, ingress filtering, + port isolation +* may differ: EEE, LED configuration, counters, cable diagnostics + +None of this is enforced yet — `port_lag_members_set()` writes a member mask and +a hash setting and validates nothing. See #377 for the discussion of whether a +mismatch should be rejected or applied to the whole group. + +## Protocol timing + +``` +#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ +#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ +#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ +#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */ +``` + +One unit is one step of the LACP timers, taken every four system ticks, so 50 +per second whatever the pace of the main loop; 50 units came out as 1.00 s on +the wire in all 116 intervals of a two minute capture. + +The two transmit periods are worth a word, because they are the one thing here +that is not ours to choose freely. Our own timeouts only decide how patient we +are with a partner that has gone quiet, and being slow there costs nobody +anything. The transmit period is the opposite: it decides when the *partner* +gives up on us. 802.3ad pairs a 1 s fast period with a 3 s short timeout, and a +30 s slow period with a 90 s long timeout, and the ratio is the whole point. + +Getting that wrong does not break the aggregate, which is what makes it easy to +miss. An earlier version of this code transmitted every 2 to 3 s while the +partner was asking for the fast rate. The links still aggregated and still +carried traffic, but every time a gap crossed 3 s the partner's receive machine +timed us out and recovered: 13 of its 93 LACPDUs came back carrying `EXPIRED` +and reporting our SYNC as cleared. Since a received actor state is recorded +verbatim as the partner state and sent back out, the flap was visible from both +ends, and the bond's churn machines never settled. With the periods above, the +same capture shows no expiry at all and both churn states read `none`. + +Our own timeouts sit one period above the standard's 3 s and 90 s, which +absorbs a single lost LACPDU plus the tick jitter. A partner whose LACP has +stopped while its link stayed up is out of the trunk after that; anything +longer keeps hashing traffic onto a member that no longer agrees to carry it. + +## LACP API + +``` +void lacp_init(void) __banked; /* boot init: clear per-LAG state */ +void lacp_off(void) __banked; +void lacp_lag_set(uint8_t lag, uint16_t ports) __banked; +void lacp_in(void) __banked; +void lacp_timers(void) __banked; +void lacp_show(void) __banked; +``` + +`lacp_init()` has to run before the startup configuration replays, because xdata +is not zeroed and the port-to-LAG map uses `0xff` as "no LAG"; a stray zero would +make a port look like it belongs to LAG 0. + +## LACP configuration on the serial console + +A group is put under LACP by adding `lacp` to the `lag` command: + +``` +lag 1 lacp 7 8 # ports 7 and 8 become LACP candidates of group 1 +lag 1 lacp off # group 1 goes back to static (so does "lag 1 lacp") +lag 1 7 8 # plain static aggregation, no protocol +``` + +A group put under LACP gives up the static trunk it had: the hardware trunk is +cleared and rebuilt from the ports that converge. A port named in another LACP +group moves to the new one. The protocol starts with the first group put under +LACP and stops when the last one is taken out; there is no separate switch for +it. Two commands act on +the whole engine: + +``` +lacp off # every group back to static, protocol stopped +lacp show +``` + +`lacp show` prints the engine state, then one line per group with its candidate +ports, the elected aggregator and the trunk members actually programmed into the +hardware, then one line per port with the actor and partner state bytes and the +receive-machine state. + +## LACP configuration via the Web Interface + +Each group on the LAG page has a Static/LACP mode next to its member ports; +for an LACP group it shows the elected aggregator and the active members. Below +the groups a table has one row per LACP port with its group, actor and partner +state, receive state, LACPDU count and partner system. The page reads +`/lacp.json` every three seconds while it is open. The member checkboxes of a +static group come from the hardware registers in `/lag.json`. + +## A test with a Linux 802.3ad bond + +Two ports of the switch to two interfaces of one host: + +``` +ip link add bond0 type bond mode 802.3ad +ip link set enp1s0 down && ip link set enp1s0 master bond0 +ip link set enp2s0 down && ip link set enp2s0 master bond0 +ip link set bond0 up +``` + +On the switch, with those two ports as the group: + +``` +lag 1 lacp 7 8 +``` + +`lacp show` should then give both ports an actor state of `0x3f`, which is +ACTIVITY, TIMEOUT, AGGREGATION, SYNC, COLLECTING and DISTRIBUTING together, a +partner state with SYNC in it, and the partner's system MAC. `lag show` should +list both ports as trunk members, since that reads the hardware rather than the +protocol's opinion of itself. + +The host's view is the one worth trusting, in `/proc/net/bonding/bond0`. Both +slaves want the **same `Aggregator ID`**; two different numbers mean the bond +has not aggregated anything and each link is sitting in its own group, which +looks healthy at a glance and is not. Both churn states should read `none`. +`monitoring` that never settles is the signature of our transmit period being +too slow for the rate the partner asked for. + +Three things are worth checking past convergence, because each of them has been +broken here at some point: + +* `tcpdump -i enp1s0 ether proto 0x8809` on each slave should show the switch's + LACPDUs and that slave's own, and never the other slave's. A sibling leak is + what the trap prevents: the PDU never enters the forwarding path at all. +* No `illegal loopback` in the host's log. +* In a hex dump of a frame from the switch, the three reserved bytes after the + actor state should be zero. They used to carry whatever the transmit buffer + held from the previous frame. + +Pulling one slave down and putting it back is the cheapest failure test. The +group narrows to the surviving member and comes back within tens of seconds, +and traffic across the transition loses a few percent at the edges rather than +half of everything, which is what it would lose if the ASIC kept hashing onto +the dead member. + +## Known limits + +* No Churn Detection machines and no Marker protocol. +* Coupled Mux: collecting and distributing are enabled together. +* One aggregator per group. +* Member compatibility is not validated; see #377. diff --git a/doc/link_aggregation.md b/doc/link_aggregation.md index b725e419..462b66de 100644 --- a/doc/link_aggregation.md +++ b/doc/link_aggregation.md @@ -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. diff --git a/html/app.js b/html/app.js index d737810b..4c7ed7f5 100644 --- a/html/app.js +++ b/html/app.js @@ -67,6 +67,9 @@ iso_title:"Port isolation",iso_from:"From port",iso_to:"To port",iso_all:"Allow iso_intro:"A frame entering a port may only leave through the ports ticked in its row. Isolation is decided before forwarding, so a port that is not ticked is unreachable directly, whatever the address table says.", iso_legend:"The cell of the port itself is always allowed. A port number in red marks a port that is not fully open.", lag_hash:"Hash:",lag_note:"A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.", +lacp_heading:"LACP (802.3ad)",lag_mode:"Mode",lag_static:"Static",lacp_neg:"negotiating",lacp_agg:"aggregator", +lacp_members:"active members",lacp_hint:"LACP: select candidate ports and press Apply to start negotiation", +lacp_actor:"Actor",lacp_partner:"Partner",lacp_rxstate:"RX state",lacp_rx:"LACPDUs received",lacp_psys:"Partner system", lag_clear_q:"Clear LAG {n}?",lag_clear_d:"All member ports return to normal operation.", e_title:"Energy Efficient Ethernet",e_adv:"Advertised",e_lp:"Link partner",e_active:"Active",e_enable:"Enable", e_note:"Advertised and link-partner flags per speed: 100M, 1G, 2.5G. SFP ports do not support EEE.", @@ -808,7 +811,7 @@ var CONF_CMDS=[ /^port\s+\d{1,2}\s+lldp\s+(permit|block)+$/, /^eee\s+(on|off)(\s+\d{1,2})?(\s+(100m|1g|2g5))?$/, /^mirror(\s+\d{1,2})(\s+\d{1,2}[tr]?)+$/,/^mirror\s+off$/, - /^lag\s+[1-4](\s+\d{1,2})+$/,/^lag\s+[1-4]\s+d$/,/^laghash\s+[1-4](\s+\w+)+$/, + /^lag\s+[1-4](\s+\d{1,2})+$/,/^lag\s+[1-4]\s+d$/,/^lag\s+[1-4]\s+lacp(\s+\d{1,2})+$/,/^lag\s+[1-4]\s+lacp\s+off$/,/^laghash\s+[1-4](\s+\w+)+$/, /^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/, /^stp\s+(on|off)$/,/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/, /^stp\s+version\s+(rstp|stp)$/, @@ -1814,6 +1817,15 @@ function buildLag(){ for(var g=1;g<=4;g++)(function(g){ var card=h("div",{class:"card"}); card.appendChild(h("h2",{text:"LAG "+g})); + var mr=h("div",{style:"display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px",class:"small"}); + mr.appendChild(h("span",{class:"mut",text:t("lag_mode")})); + var ms=h("select",{class:"in",id:"lgm"+g}); + ms.appendChild(h("option",{value:"static",text:t("lag_static")})); + ms.appendChild(h("option",{value:"lacp",text:"LACP"})); + ms.addEventListener("change",function(){$("lgs"+g).textContent=ms.value==="lacp"?t("lacp_hint"):""}); + mr.appendChild(ms); + card.appendChild(mr); + card.appendChild(h("p",{class:"small mono",id:"lgs"+g,style:"margin-bottom:10px"})); var pr=h("div",{style:"display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px"}); for(var p=1;p<=S.n;p++)pr.appendChild(h("label",null,[ h("input",{type:"checkbox",id:"lg"+g+"p"+p}),document.createTextNode(" "+p+" "), @@ -1826,14 +1838,33 @@ function buildLag(){ }); card.appendChild(hr); card.appendChild(h("button",{class:"ctl pri",text:t("c_apply"),onclick:function(){lagApply(g)}})); + card.addEventListener("change",function(){card.dataset.dirty="1"}); w.appendChild(card); })(g); } +var lacpCfg=[0,0,0,0,0]; function lagLoad(){ - return getJSON("/lag.json").then(function(s){ + return getJSON("/lacp.json").then(function(c){ + var tb=$("lacptbl").tBodies[0]; + tb.innerHTML=""; + c.lags.forEach(function(lg,i){ + var g=i+1; + lacpCfg[g]=parseInt(lg.cfg,16); + if($("lgm"+g).closest(".card").dataset.dirty)return; + $("lgm"+g).value=lacpCfg[g]?"lacp":"static"; + $("lgs"+g).textContent=lacpCfg[g]?"LACP: "+t("lacp_agg")+" "+(lg.aggValid?lg.agg:"("+t("lacp_neg")+")")+", "+t("lacp_members")+" 0x"+lg.members:""; + }); + if(c.on)c.ports.forEach(function(p){ + if(p.lag===255)return; + var tr=tb.insertRow(); + [p.p,p.lag+1,p.a,p.pt,p.rs,parseInt(p.rx,16),p.psys].forEach(function(v){tr.insertCell().textContent=v}); + }); + return getJSON("/lag.json"); + }).then(function(s){ s.forEach(function(l){ var g=l.lagNum+1; - var members=parseInt(l.members,2); + if($("lgm"+g).closest(".card").dataset.dirty)return; + var members=lacpCfg[g]||parseInt(l.members,2); for(var p=1;p<=S.n;p++) $("lg"+g+"p"+p).checked=!!((members>>S.physToLog[p-1])&1); var hash=parseInt(l.hash,16); @@ -1842,21 +1873,25 @@ function lagLoad(){ }); } function lagApply(g){ - var cmd="lag "+g,n=0; + var lacp=$("lgm"+g).value==="lacp",cmd="lag "+g+(lacp?" lacp":""),n=0,pre=[]; + delete $("lgm"+g).closest(".card").dataset.dirty; for(var p=1;p<=S.n;p++)if($("lg"+g+"p"+p).checked){cmd+=" "+p;n++;} - if(!n){ + if(!lacp&&lacpCfg[g])pre.push("lag "+g+" lacp off"); + if(lacp&&!n)cmd="lag "+g+" lacp off"; + if(!n&&!lacp){ confirmModal(t("lag_clear_q",{n:g}),t("lag_clear_d"),function(){ - postCmd("lag "+g+" d").then(lagLoad).catch(function(){}); + postCmds(pre.concat(["lag "+g+" d"])).then(lagLoad).catch(function(){}); }); return; } var hcmd="laghash "+g,nh=0; HASHF.forEach(function(f){if($("lg"+g+"h"+f).checked){hcmd+=" "+f;nh++;}}); - var cmds=[cmd]; + var cmds=pre.concat([cmd]); if(nh)cmds.push(hcmd); postCmds(cmds).then(lagLoad).catch(function(){}); } -tabHooks.lag={enter:function(){needPorts(function(){buildLag();lagLoad().catch(function(){})})}}; +var lagPoller=new Poller(function(){return lagLoad().catch(function(){})},3000); +tabHooks.lag={enter:function(){needPorts(function(){buildLag();lagPoller.start()})},leave:function(){lagPoller.stop()}}; function lldpLoad(){ @@ -2245,6 +2280,7 @@ function mergeConf(base,texts){ else drop(/^eee /); conf.push(line);return; } + if((m=line.match(/^lag (\d) lacp off$/))){drop(new RegExp("^lag "+m[1]+" lacp( |$)"));return;} if(line==="mirror off"){drop(/^mirror /);return;} if(!isConfCmd(line))return; if((m=line.match(/^ingress (.+)$/))){ diff --git a/html/index.html b/html/index.html index f896dd13..21b7d17f 100644 --- a/html/index.html +++ b/html/index.html @@ -356,6 +356,11 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}
+

LACP (802.3ad)

+
+ +
PortLAGActorPartnerRX stateLACPDUs receivedPartner system
+

A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.

diff --git a/httpd/httpd.c b/httpd/httpd.c index aec2eaf1..f2ca2d36 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -1036,6 +1036,8 @@ void httpd_appcall(void) send_stp(); } else if (is_word(q, "/lldp.json")) { send_lldp(); + } else if (is_word(q, "/lacp.json")) { + send_lacp(); } else if (is_word(q, "/vlanlist")) { send_vlanlist(); } else if (is_word(q, "/config")) { diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 2d58457e..44c9ddcb 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -14,6 +14,7 @@ #include "version.h" #include "machine.h" #include "rtl837x_stp.h" +#include "rtl837x_lacp.h" #include "page_impl.h" #include "syslog.h" @@ -408,14 +409,19 @@ void send_l2(uint16_t idx) */ __xdata uint16_t entry = idx & 0xfff; __xdata uint16_t first_entry = 0xffff; // An illegal entry index + __xdata uint16_t mbr; __bit first = true; char_to_html('['); while (1) { entries_left--; uint8_t port = 0; uint8_t lag; + uint8_t mc = 0; reg_read_m(RTL837x_TBL_DATA_0); - REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_L2UC << 6), sfr_data[3]); + // Iterate with "next address" (method 2) so static multicast entries - + // an IGMP group entry, say - are listed too, not only learned or + // static unicast ones. + REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_ADDRESS << 6), sfr_data[3]); REG_WRITE(RTL837X_TBL_CTRL, entry >> 8, entry, TBL_L2_UNICAST, TBL_EXECUTE); do { @@ -423,7 +429,7 @@ void send_l2(uint16_t idx) } while (sfr_data[3] & TBL_EXECUTE); reg_read_m(RTL837x_L2_DATA_OUT_B); - __bit valid = (sfr_data[0] & 0x20) != 0; + __bit valid = (sfr_data[0] & 0x20) && !(sfr_data[0] & 0x10); if (valid) { /* separator + 74-byte worst-case entry + closing "]" */ if (slen + 76 > TCP_OUTBUF_SIZE) @@ -432,6 +438,8 @@ void send_l2(uint16_t idx) char_to_html(','); first = false; + mc = sfr_data[2] & 0x01; // multicast MAC: member mask, no SPA/age + // VLAN, taken from the read above instead of reading the register twice slen += strtox(outbuf + slen, "{\"vlan\":\""); charhex_to_html(sfr_data[0] & 0x0f); @@ -450,12 +458,23 @@ void send_l2(uint16_t idx) // type reg_read_m(RTL837x_L2_DATA_OUT_C); - if (sfr_data[1] & 0x1) + if (mc || (sfr_data[1] & 0x1)) slen += strtox(outbuf + slen, "\",\"type\":\"s\",\"port\":"); else slen += strtox(outbuf + slen, "\",\"type\":\"l\",\"port\":"); - port |= (sfr_data[3] & 0x3) << 2; + if (mc) { + // Report the lowest member of the mask (an entry holding only + // the CPU port is rendered as "CPU" by the UI) + mbr = port | ((uint16_t)sfr_data[3] << 2); + port = 0; + while (mbr && !(mbr & 1)) { + mbr >>= 1; + port++; + } + } else { + port |= (sfr_data[3] & 0x3) << 2; + } itoa_html(port); slen += strtox(outbuf + slen, ",\"lag\":"); lag = port_lag_of(port); @@ -736,6 +755,60 @@ void send_stp(void) slen += strtox(outbuf + slen, "]}"); } +/* LACP protocol status for the LAG page ("/lacp.json"). State variables are + * exported read-only via rtl837x_lacp.h; hardware trunk membership itself is + * already visible through send_lag() (it reads the trunk registers). */ +void send_lacp(void) +{ + dbg_string("send_lacp called\n"); + slen = strtox(outbuf, HTTP_RESPONCE_JSON); + + slen += strtox(outbuf + slen, "{\"on\":"); + bool_to_html(lacpEnabled); + /* Per-LAG section: candidate ports, elected aggregator, trunk members. */ + slen += strtox(outbuf + slen, ",\"lags\":["); + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { + slen += strtox(outbuf + slen, "{\"cfg\":\""); + byte_to_html(lacp_lag_ports[l] >> 8); + byte_to_html(lacp_lag_ports[l]); + slen += strtox(outbuf + slen, "\",\"aggValid\":"); + bool_to_html(lacp_agg_valid[l]); + slen += strtox(outbuf + slen, ",\"agg\":\""); + for (uint8_t j = 0; j < 6; j++) + byte_to_html(lacp_agg_sys[l][j]); + slen += strtox(outbuf + slen, "\",\"members\":\""); + byte_to_html(lacp_members_last[l] >> 8); + byte_to_html(lacp_members_last[l]); + slen += strtox(outbuf + slen, "\"},"); + } + slen -= 1; // remove comma + slen += strtox(outbuf + slen, "],\"ports\":["); + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + slen += strtox(outbuf + slen, "{\"p\":"); + itoa_html(machine.log_to_phys_port[i]); + /* Which LACP LAG this port belongs to; 255 = none */ + slen += strtox(outbuf + slen, ",\"lag\":"); + itoa_html(lacp_port_lag[i]); + slen += strtox(outbuf + slen, ",\"a\":\""); + byte_to_html(lacp_actor_state[i]); + slen += strtox(outbuf + slen, "\",\"pt\":\""); + byte_to_html(lacp_partner_state[i]); + slen += strtox(outbuf + slen, "\",\"rs\":"); + itoa_html(lacp_rx_state[i]); + /* rx as 4-digit hex: itoa16_html() only renders values up to 9999 + * correctly (see its VLAN-ID comment), the counter goes to 65535 */ + slen += strtox(outbuf + slen, ",\"rx\":\""); + byte_to_html(lacp_rx_count[i] >> 8); + byte_to_html(lacp_rx_count[i]); + slen += strtox(outbuf + slen, "\",\"psys\":\""); + for (uint8_t j = 0; j < 6; j++) + byte_to_html(lacp_partner_sys[i][j]); + slen += strtox(outbuf + slen, "\"},"); + } + slen -= 1; // remove comma + slen += strtox(outbuf + slen, "]}"); +} + void send_lldp(void) { dbg_string("send_lldp called\n"); diff --git a/httpd/page_impl.h b/httpd/page_impl.h index fd51c8c6..5133d2eb 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -24,6 +24,7 @@ void send_cmd_log(void); void send_lag(void); void send_stp(void); void send_lldp(void); +void send_lacp(void); void send_vlanlist(void); /* Convert only the lower nibble to ascii HEX char. diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index 3ef2b2ec..39b53405 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -22,8 +22,8 @@ extern __xdata uint8_t igmpEnabled; #include "uip.h" #include "uip_arp.h" -#pragma codeseg BANK1 -#pragma constseg BANK1 +#pragma codeseg BANK3 +#pragma constseg BANK3 extern __xdata uint8_t cpuPort; extern __xdata uint8_t sfr_data[4]; diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c new file mode 100644 index 00000000..b4874f16 --- /dev/null +++ b/rtl837x_lacp.c @@ -0,0 +1,607 @@ +/* + * LACP (IEEE 802.3ad Clause 43) driver for the RTL837x platform. + * This code is in the Public Domain. + */ + +// #define DEBUG + +#pragma codeseg BANK3 +#pragma constseg BANK3 + +#include +#include "rtl837x_common.h" +#include "rtl837x_regs.h" +#include "rtl837x_lacp.h" +#include "rtl837x_trap.h" +#include "rtl837x_port.h" +#include "uip.h" +#include "machine.h" + +extern __code const struct machine machine; +extern __xdata struct machine_runtime machine_detected; /* runtime chip detection, owned by rtl837x_port.c */ +extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for slow protocols */ +extern __xdata struct uip_eth_addr uip_ethaddr; +extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; +/* lacpEnabled: extern in rtl837x_lacp.h, owned by rtlplayground.c */ + +/* 20-byte Actor/Partner information body (802.3ad 43.4.2.2), network byte order */ +struct lacp_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]; +}; + +/* Outgoing LACPDU (no VLAN tag on TX, as in stp_pkt) */ +struct lacpdu { + uint8_t dst[6]; + uint8_t src[6]; + struct rtl_tag rtl_tag; + uint16_t ethertype; /* 0x8809 */ + uint8_t subtype; /* 0x01 */ + uint8_t version; /* 0x01 */ + uint8_t tlv_actor; /* 0x01 */ + uint8_t actor_len; /* 0x14 */ + struct lacp_info actor; + uint8_t tlv_partner; /* 0x02 */ + uint8_t partner_len; /* 0x14 */ + struct lacp_info partner; + uint8_t tlv_collector; /* 0x03 */ + uint8_t collector_len; /* 0x10 */ + uint16_t collector_max_delay; + uint8_t collector_reserved[12]; + uint8_t tlv_terminator; /* 0x00 */ + uint8_t terminator_len; /* 0x00 */ + uint8_t terminator_reserved[50]; +}; + +/* Incoming LACPDU, split because a VLAN tag sits between the two only when the + * frame was forwarded: a trapped one carries none (cf. stp_pkt_in) */ +struct lacpdu_in_hdr { + uint8_t dst[6]; + uint8_t src[6]; + struct rtl_tag rtl_tag; +}; + +struct lacpdu_in { + uint16_t ethertype; + uint8_t subtype; + uint8_t version; + uint8_t tlv_actor; + uint8_t actor_len; + struct lacp_info actor; + uint8_t tlv_partner; + uint8_t partner_len; + struct lacp_info partner; +}; + +__xdata uint8_t lacp_rxbody; /* offset of the LACPDU body: the RTL tag, plus a VLAN tag when forwarded */ + +#define LACP_O ((__xdata struct lacpdu *)&uip_buf[RTL_FRAME_DESC_SIZE]) +#define LACP_H ((__xdata struct lacpdu_in_hdr *)&uip_buf[0]) +#define LACP_I ((__xdata struct lacpdu_in *)&uip_buf[lacp_rxbody]) + +/* --- Per-port participant state (802.3ad 43.4.7). Indexed by physical port. --- */ +__xdata uint8_t lacp_actor_state[10]; /* our Actor_State flags */ +__xdata uint8_t lacp_rx_state[10]; /* LACP_RX_* receive machine */ +__xdata uint8_t lacp_partner_state[10]; /* last Partner_State we saw */ +__xdata uint8_t lacp_partner_sys[10][6]; /* partner System ID */ +__xdata uint16_t lacp_partner_key[10]; /* partner Key */ +__xdata uint16_t lacp_partner_port[10]; /* partner Port number */ +/* The Partner block echoes the peer's actor identity verbatim, see + * doc/link_aggregation.md. */ +__xdata uint16_t lacp_partner_sys_prio[10]; /* partner System priority */ +__xdata uint16_t lacp_partner_port_prio[10]; /* partner Port priority */ +__xdata uint16_t lacp_periodic[10]; /* down-counter to next TX */ +__xdata uint16_t lacp_timeout[10]; /* down-counter to partner expiry*/ +__xdata uint8_t lacp_ntt[10]; /* Need-To-Transmit flag */ + +/* The timers step every four system ticks (50 Hz) whatever the pace of the + * main loop; a long pass is caught up on, a few steps at a time. */ +#define LACP_TICK_STEP (SYS_TICK_HZ / 50) +#define LACP_CATCH_UP 8 +__xdata uint8_t lacp_tick_last; +extern volatile __xdata uint32_t ticks; + +/* Per-LAG state: the hardware trunk groups run LACP independently. */ +__xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; +__xdata uint8_t lacp_port_lag[10]; +__xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +__xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; +__xdata uint16_t lacp_members_last[LACP_NUM_LAGS]; + +__xdata uint16_t lacp_scratch_mask; +__xdata uint8_t lacp_scratch_flag; +__xdata uint8_t lacp_scratch_lag; + +/* Per-port RX LACPDU count - primarily a hardware bring-up diagnostic to + * verify slow-protocol frames actually reach the CPU port (see "lacp show") */ +__xdata uint16_t lacp_rx_count[10]; + + +void lacp_mux_update(void) __banked; /* defined below, used from lacp_in */ + +static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b) +{ + for (uint8_t i = 0; i < 6; i++) { + if (a[i] != b[i]) + return 0; + } + return 1; +} + + +/* + * Selection logic (simplified 43.4.14): a port may join the aggregator when + * we have fresh partner info, the partner considers the link aggregatable, + * and the partner is the same system the aggregator was elected for. + */ +static uint8_t lacp_port_selected(uint8_t port) +{ + uint8_t lag = lacp_port_lag[port]; + if (lag == LACP_LAG_NONE) /* port not in any LACP LAG */ + return 0; + if (lacp_rx_state[port] != LACP_RX_CURRENT) + return 0; + if (!(lacp_partner_state[port] & LACP_STATE_AGGREGATION)) + return 0; + if (!lacp_agg_valid[lag]) + return 0; + return lacp_sys_eq(lacp_partner_sys[port], lacp_agg_sys[lag]); +} + + +/* + * Mux machine (simplified coupled control, 43.4.15): SYNC follows selection; + * COLLECTING+DISTRIBUTING additionally require the partner to be in sync. + * Returns 1 if the actor state changed (caller sets NTT). + */ +static uint8_t lacp_mux_machine(uint8_t port) +{ + uint8_t old = lacp_actor_state[port]; + uint8_t new = old; + + if (lacp_port_selected(port)) { + new |= LACP_STATE_SYNC; + if (lacp_partner_state[port] & LACP_STATE_SYNC) + new |= LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING; + else + new &= ~(LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING); + } else { + new &= ~(LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING); + } + + if (new == old) + return 0; + lacp_actor_state[port] = new; + return 1; +} + +#define port_bit(p) (((uint8_t)1) << (p)) + + +/* Build and emit one LACPDU out physical port `port` (802.3ad 43.4.2). */ +void lacp_send(uint8_t port) __banked +{ + memset((__xdata uint8_t *)LACP_O, 0, sizeof(struct lacpdu)); + LACP_O->dst[0] = 0x01; LACP_O->dst[1] = 0x80; LACP_O->dst[2] = 0xc2; + LACP_O->dst[3] = 0x00; LACP_O->dst[4] = 0x00; LACP_O->dst[5] = LACP_DST5; + memcpy(LACP_O->src, uip_ethaddr.addr, 6); + + LACP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); + LACP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; + LACP_O->rtl_tag.reason = 0x00; + /* Must go through HTONS like every other tag field: writing 0x0020 raw put + * the bits in the wrong byte (on the wire 0x2000 = EFID, not LEARN_DIS), so + * the ASIC could not parse the tag and forwarded the frame with the 0x8899 + * header still attached - the partner then saw ethertype 0x8899 instead of + * 0x8809 and ignored the LACPDU. KEEP additionally stops the switch from + * rewriting the 802.1Q tag format of a CPU-injected frame (as mainline). */ + LACP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS | RTL_TAG_KEEP); + /* ALLOW cleared => this word is the *forwarding* port mask, i.e. directed + * egress to exactly this port (what mainline does). Setting ALLOW instead + * makes it an allowance/permission mask on top of a normal lookup, which + * for a one-hot mask resolves to an empty egress set and the frame is + * silently dropped - verified on hardware. */ + LACP_O->rtl_tag.pmask = HTONS(port_bit(port)); /* egress this port only */ + + LACP_O->ethertype = HTONS(SLOW_PROTO_ETHERTYPE); + LACP_O->subtype = SLOW_PROTO_SUBTYPE_LACP; + LACP_O->version = LACP_VERSION; + + /* Actor TLV */ + LACP_O->tlv_actor = LACP_TLV_ACTOR; + LACP_O->actor_len = LACP_TLV_LEN_INFO; + LACP_O->actor.sys_prio = HTONS(LACP_SYS_PRIO); + memcpy(LACP_O->actor.sys, uip_ethaddr.addr, 6); + /* Per-LAG Actor Key so a partner never merges ports of our different LAGs + * into one aggregate (only ever called for ports in a LACP LAG). */ + LACP_O->actor.key = HTONS((uint16_t)(lacp_port_lag[port] + 1)); + LACP_O->actor.port_prio = HTONS(LACP_DEF_PORT_PRIO); + LACP_O->actor.port = HTONS((uint16_t)port + 1); /* 1-based port id */ + LACP_O->actor.state = lacp_actor_state[port]; + + /* Partner TLV: echo the last partner info we recorded, VERBATIM. A Linux + * 802.3ad partner (__record_pdu) only accepts the SYNC bit we set above if + * this block mirrors its own actor identity exactly - system+priority, key, + * port+priority. Priorities must be echoed, not hardcoded: Linux defaults to + * system priority 0xffff, so a hardcoded 0 here cleared its partner-SYNC + * (0x3f->0x37) and the aggregate never collected/distributed. */ + LACP_O->tlv_partner = LACP_TLV_PARTNER; + LACP_O->partner_len = LACP_TLV_LEN_INFO; + LACP_O->partner.sys_prio = HTONS(lacp_partner_sys_prio[port]); + memcpy(LACP_O->partner.sys, lacp_partner_sys[port], 6); + LACP_O->partner.key = HTONS(lacp_partner_key[port]); + LACP_O->partner.port_prio = HTONS(lacp_partner_port_prio[port]); + LACP_O->partner.port = HTONS(lacp_partner_port[port]); + LACP_O->partner.state = lacp_partner_state[port]; + + /* Collector TLV + Terminator */ + LACP_O->tlv_collector = LACP_TLV_COLLECTOR; + LACP_O->collector_len = LACP_TLV_LEN_COLLECTOR; + LACP_O->collector_max_delay = 0; + LACP_O->tlv_terminator = LACP_TLV_TERMINATOR; + LACP_O->terminator_len = 0x00; + + lacp_ntt[port] = 0; + /* Slow-protocol frames are link-local and must egress untagged. With a + * management VLAN set, tcpip_output() splices an 802.1Q tag after the SA, + * shifting our in-frame rtl_tag out of the position the ASIC expects - so it + * fails to parse/strip the CPU tag and the 0x8899 header leaks onto the wire + * (the partner then ignores the LACPDU). Suppress the VLAN insert for this + * frame only; management traffic keeps its tag. Verified on HW: with the + * insert suppressed the LACPDU egresses as clean 0x8809 and the bond + * partner converges (partner MAC becomes the switch, both links aggregate). */ + uint16_t saved_mgmt_vlan = management_vlan; + management_vlan = 0; + uip_len = sizeof(struct lacpdu); + tcpip_output(); + management_vlan = saved_mgmt_vlan; +} + + +/* + * Process an incoming LACPDU. Called from the rtlplayground.c dispatch when a + * frame with DA 01:80:C2:00:00:02 and ethertype 0x8809/subtype 0x01 arrives. + * The ingress port is the low nibble of rtl_tag.pmask on RX. + */ +void lacp_in(void) __banked +{ + lacp_rxbody = TRAP_RX_BODY(); + if (uip_len < lacp_rxbody + sizeof(struct lacpdu_in)) { + uip_len = 0; + return; + } + uip_len = 0; + + if (LACP_I->ethertype != HTONS(SLOW_PROTO_ETHERTYPE) + || LACP_I->subtype != SLOW_PROTO_SUBTYPE_LACP) + return; + + /* Per rtl837x_common.h, pmask carries a 4-bit port number on RX + * (hardware-verified on RTL8373: per-port rx counters track the actual + * ingress port through full LACP convergence). */ + uint8_t port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; + if (port < machine.min_port || port > machine.max_port) + return; + + /* Only ports assigned to a LACP-mode LAG participate in the protocol. */ + uint8_t lag = lacp_port_lag[port]; + if (lag == LACP_LAG_NONE) + return; + + +#ifdef DEBUG + print_string("LACP in, port "); print_byte(port); + print_string(" partner_state "); print_byte(LACP_I->actor.state); write_char('\n'); +#endif + + lacp_rx_count[port]++; + + /* Record partner = the remote's Actor block (802.3ad 43.4.9 recordPDU) */ + memcpy(lacp_partner_sys[port], LACP_I->actor.sys, 6); + lacp_partner_key[port] = HTONS(LACP_I->actor.key); + lacp_partner_port[port] = HTONS(LACP_I->actor.port); + lacp_partner_sys_prio[port] = HTONS(LACP_I->actor.sys_prio); + lacp_partner_port_prio[port] = HTONS(LACP_I->actor.port_prio); + lacp_partner_state[port] = LACP_I->actor.state; + + /* Receive machine -> CURRENT, (re)arm partner timeout (43.4.12) */ + lacp_rx_state[port] = LACP_RX_CURRENT; + lacp_timeout[port] = (LACP_I->actor.state & LACP_STATE_TIMEOUT) + ? LACP_SHORT_TIMEOUT : LACP_LONG_TIMEOUT; + + /* Elect this LAG's aggregator partner system on first contact (43.4.14) */ + if (!lacp_agg_valid[lag]) { + memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, 6); + lacp_agg_valid[lag] = 1; + } + + /* Selection + Mux for this port; state change => Need-To-Transmit */ + if (lacp_mux_machine(port)) + lacp_ntt[port] = 1; + + /* update_NTT (43.4.12): if the partner's view of us is stale (their + * Partner block does not match our actor state/port), tell them again. */ + if (LACP_I->partner.state != lacp_actor_state[port] + || HTONS(LACP_I->partner.port) != (uint16_t)port + 1) + lacp_ntt[port] = 1; + + lacp_mux_update(); +} + + +/* Recompute each LACP LAG's hardware trunk membership: a port joins its LAG's + * trunk once it is fully in sync (SYNC+COLLECTING+DISTRIBUTING and the partner + * in SYNC). Each LACP-mode LAG is programmed independently (simplified 43.4.15). */ +void lacp_mux_update(void) __banked +{ + for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + if (!lacp_lag_ports[lag]) /* LAG not under LACP management */ + continue; + lacp_scratch_mask = 0; + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] != lag) + continue; + if ((lacp_actor_state[i] & LACP_STATE_FULL) == LACP_STATE_FULL + && (lacp_partner_state[i] & LACP_STATE_SYNC)) + lacp_scratch_mask |= port_bit(i); + } + /* Program the trunk only when this LAG's membership actually changed + * - this runs on every timer tick. */ + if (lacp_scratch_mask != lacp_members_last[lag]) { + lacp_members_last[lag] = lacp_scratch_mask; + port_lag_members_set(lag, lacp_scratch_mask); + } + } +} + + +static void lacp_step(void) +{ + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] == LACP_LAG_NONE) /* not in a LACP LAG */ + continue; + + /* Periodic transmit machine (802.3ad 43.4.13) */ + if (lacp_periodic[i]) { + lacp_periodic[i]--; + } else { + lacp_periodic[i] = (lacp_partner_state[i] & LACP_STATE_TIMEOUT) + ? LACP_FAST_PERIODIC : LACP_SLOW_PERIODIC; + lacp_ntt[i] = 1; + } + + /* Partner timeout -> DEFAULTED (802.3ad 43.4.12): the mux machine + * then drops SYNC/COLLECTING/DISTRIBUTING and the port leaves the + * trunk. TODO(43.4.12): churn detection is diagnostics-only, skipped. */ + if (lacp_timeout[i]) { + if (!--lacp_timeout[i]) { + lacp_rx_state[i] = LACP_RX_DEFAULTED; + lacp_partner_state[i] = LACP_STATE_DEFAULTED; + if (lacp_mux_machine(i)) + lacp_ntt[i] = 1; + } + } + + if (lacp_ntt[i]) + lacp_send(i); + } + + /* Release each LAG's aggregator identity once none of its ports still + * hears the elected partner, so a re-cabled setup can elect a new one + * (43.4.14). A port that is CURRENT with some other system must not keep + * the old identity alive: it would never be selected, and neither would + * the rest of the LAG once it moves to that system too. */ + for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + if (!lacp_agg_valid[lag]) + continue; + uint8_t any_current = 0; + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] == lag && lacp_rx_state[i] == LACP_RX_CURRENT + && lacp_sys_eq(lacp_partner_sys[i], lacp_agg_sys[lag])) { + any_current = 1; + break; + } + } + if (!any_current) + lacp_agg_valid[lag] = 0; + } + + lacp_mux_update(); +} + + +void lacp_timers(void) __banked +{ + uint8_t n = LACP_CATCH_UP; + while (n-- && TICKS_DUE(lacp_tick_last, LACP_TICK_STEP)) { + lacp_tick_last += LACP_TICK_STEP; + lacp_step(); + } +} + + +/* Bring one port up as an active-fast LACP participant (43.4.12 init). */ +static void lacp_port_init(uint8_t port) +{ + lacp_actor_state[port] = LACP_STATE_ACTIVITY | LACP_STATE_AGGREGATION + | LACP_STATE_TIMEOUT; /* active + fast */ + lacp_rx_state[port] = LACP_RX_INITIALIZE; + lacp_partner_state[port] = LACP_STATE_DEFAULTED; + memset(lacp_partner_sys[port], 0, 6); + lacp_partner_key[port] = 0; + lacp_partner_port[port] = 0; + lacp_periodic[port] = LACP_FAST_PERIODIC; + lacp_timeout[port] = 0; + lacp_ntt[port] = 1; /* announce ourselves immediately */ + lacp_rx_count[port] = 0; +} + +/* Return a port to non-LACP state. (Port isolation is no longer touched - a + * trapped LACPDU never enters the forwarding path, so a sibling cannot see + * it, which is what the isolation workaround was for.) */ +static void lacp_port_release(uint8_t port) +{ + lacp_actor_state[port] = 0; + lacp_ntt[port] = 0; + lacp_partner_state[port] = 0; + lacp_rx_state[port] = LACP_RX_INITIALIZE; + lacp_rx_count[port] = 0; + memset(lacp_partner_sys[port], 0, 6); +} + +/* True while any LAG is under LACP management (drives the global RMA trap). */ +static uint8_t lacp_any_lag(void) +{ + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) + if (lacp_lag_ports[l]) + return 1; + return 0; +} + +/* + * Turn the LACP engine on: trap the Slow-Protocols group to the CPU port. The + * trap acts on the address before the VLAN lookup and before the ingress + * filters, so delivery needs no L2 entry and no PVID of its own. + */ +static void lacp_engine_on(void) +{ + lacp_tick_last = (uint8_t)ticks; + lacpEnabled = 1; + trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_TRAP); +} + +/* Turn the LACP engine off: stop delivering slow-protocols to the CPU. */ +static void lacp_engine_off(void) +{ + trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_DROP); + lacpEnabled = 0; +} + +/* One-time boot init: no LAG runs LACP yet, so every port maps to LACP_LAG_NONE. + * MUST run before any "lag ... lacp" config replay - xdata is not zeroed to the + * 0xff sentinel, and a stray 0 would make a port look like it belongs to LAG 0. */ +void lacp_init(void) __banked +{ + for (uint8_t i = 0; i < 10; i++) { + lacp_port_lag[i] = LACP_LAG_NONE; + lacp_actor_state[i] = 0; + lacp_ntt[i] = 0; + } + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { + lacp_lag_ports[l] = 0; + lacp_agg_valid[l] = 0; + lacp_members_last[l] = 0; + } + lacpEnabled = 0; +} + +/* + * Assign a candidate-port mask to LACP-mode LAG `lag` (`lag lacp `). + * ports == 0 removes the LAG from LACP management. Ports leaving the LAG are + * returned to normal switching; ports joining start the protocol immediately. + * The engine's RMA trap is enabled on the first LACP LAG and torn down with the + * last. Never wipes a static "lag" the user configured on a different group. + */ +void lacp_lag_set(uint8_t lag, uint16_t ports) __banked +{ + if (lag >= LACP_NUM_LAGS) + return; + if (ports && ports == lacp_lag_ports[lag]) + return; + lacp_scratch_flag = lacp_any_lag(); + + /* Drop ports that were in this LAG but are not in the new mask. */ + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] == lag && !(ports & port_bit(i))) { + lacp_port_lag[i] = LACP_LAG_NONE; + lacp_port_release(i); + } + } + /* Release this LAG's trunk: LACP's own, or the static one a group put + * under LACP still has. Taking the group out (ports == 0) leaves a static + * trunk alone. */ + if (lacp_members_last[lag] || ports) { + lacp_members_last[lag] = 0; + port_lag_members_set(lag, 0); + } + lacp_agg_valid[lag] = 0; + lacp_lag_ports[lag] = ports; + + if (ports && !lacp_scratch_flag) /* first LACP LAG: bring the engine up first */ + lacp_engine_on(); + + /* Adopt the new member ports (with the engine already up). A port that + * belonged to another LACP LAG leaves that LAG's candidate set first. */ + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (!(ports & port_bit(i))) + continue; + lacp_scratch_lag = lacp_port_lag[i]; + if (lacp_scratch_lag != LACP_LAG_NONE && lacp_scratch_lag != lag) { + lacp_lag_ports[lacp_scratch_lag] &= ~port_bit(i); + if (!lacp_lag_ports[lacp_scratch_lag]) { + lacp_agg_valid[lacp_scratch_lag] = 0; + if (lacp_members_last[lacp_scratch_lag]) { + lacp_members_last[lacp_scratch_lag] = 0; + port_lag_members_set(lacp_scratch_lag, 0); + } + } + } + lacp_port_lag[i] = lag; + lacp_port_init(i); + } + + if (!lacp_any_lag() && lacp_scratch_flag) /* removed the last LACP LAG */ + lacp_engine_off(); + + lacp_mux_update(); /* a LAG that lost a port to this one drops it from its trunk */ +} + + +/* "lacp off": remove every LACP LAG (releases their trunks, stops the engine). + * There is no "lacp on": the engine comes up with the first "lag lacp". */ +void lacp_off(void) __banked +{ + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) + if (lacp_lag_ports[l]) + lacp_lag_set(l, 0); +} + + +/* "lacp show": per-port protocol state - the primary bring-up diagnostic. + * A stuck rx=0 counter means slow-protocol frames never reach the CPU port + * (see the RMA note in lacp_engine_on()). */ +void lacp_show(void) __banked +{ + print_string("LACP "); print_string(lacpEnabled ? "on" : "off"); write_char('\n'); + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { + if (!lacp_lag_ports[l]) + continue; + print_string("lag "); print_byte(l + 1); + print_string(" ports "); print_short(lacp_lag_ports[l]); + print_string(" aggregator "); + if (lacp_agg_valid[l]) { + for (uint8_t j = 0; j < 6; j++) + print_byte(lacp_agg_sys[l][j]); + } else { + print_string("(none)"); + } + print_string(" members "); print_short(lacp_members_last[l]); write_char('\n'); + } + + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + print_string("port "); print_byte(machine.log_to_phys_port[i]); + print_string(" actor "); print_byte(lacp_actor_state[i]); + print_string(" partner "); print_byte(lacp_partner_state[i]); + print_string(" rxst "); print_byte(lacp_rx_state[i]); + print_string(" rx "); print_short(lacp_rx_count[i]); + print_string(" psys "); + for (uint8_t j = 0; j < 6; j++) + print_byte(lacp_partner_sys[i][j]); + write_char('\n'); + } +} diff --git a/rtl837x_lacp.h b/rtl837x_lacp.h new file mode 100644 index 00000000..5f03a16e --- /dev/null +++ b/rtl837x_lacp.h @@ -0,0 +1,101 @@ +#ifndef _RTL837X_LACP_H_ +#define _RTL837X_LACP_H_ + +/* + * LACP (IEEE 802.3ad Clause 43) for the RTL837x platform. + * This code is in the Public Domain. + */ + +#include + +void lacp_init(void) __banked; /* boot init: clear per-LAG state */ +void lacp_in(void) __banked; +void lacp_timers(void) __banked; +void lacp_off(void) __banked; +void lacp_show(void) __banked; /* "lacp show" - per-port state + RX counters */ +/* Assign a candidate-port mask to a LACP-mode LAG (`lag lacp `). + * ports == 0 removes the LAG from LACP management. Enables the engine on first + * LACP LAG and tears it down when the last one goes away. */ +void lacp_lag_set(uint8_t lag, uint16_t ports) __banked; + +/* + * Per-LAG LACP: each of the 4 hardware trunk groups (0-3) can independently run + * LACP on an admin-assigned set of candidate ports, forming its own aggregator + * (its own elected partner System). A port participates in LACP only if it has + * been assigned to a LACP-mode LAG (lacp_port_lag[port] != LACP_LAG_NONE). + * Ports of the same LAG that converge with a consistent partner join that LAG's + * hardware trunk. This is the port-channel model: `lag lacp `. + */ +#define LACP_NUM_LAGS 4 +#define LACP_LAG_NONE 0xff /* port not assigned to any LACP LAG */ + +/* Protocol state, exposed read-only for the web UI (page_impl.c send_lacp()) + * and the serial console. Owned by rtl837x_lacp.c / rtlplayground.c. */ +extern __xdata uint8_t lacpEnabled; +extern __xdata uint8_t lacp_actor_state[10]; +extern __xdata uint8_t lacp_partner_state[10]; +extern __xdata uint8_t lacp_rx_state[10]; +extern __xdata uint8_t lacp_partner_sys[10][6]; +extern __xdata uint16_t lacp_rx_count[10]; +extern __xdata uint8_t lacp_port_lag[10]; /* LAG (0-3) a port runs LACP on, or LACP_LAG_NONE */ +extern __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; /* admin candidate-port mask per LACP LAG (0 = LAG not LACP) */ +extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; /* elected partner System per LAG */ +extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; /* aggregator elected for this LAG */ +extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS];/* trunk members we last programmed per LAG */ + +/* Slow-Protocols / LACPDU identifiers (802.3ad 43.4) */ +#define SLOW_PROTO_ETHERTYPE 0x8809 +#define SLOW_PROTO_SUBTYPE_LACP 0x01 +#define LACP_VERSION 0x01 +#define LACP_DST5 0x02 /* 01:80:C2:00:00:02 last octet */ + +/* LACPDU TLV framing (802.3ad 43.4.2): type bytes + fixed TLV lengths. */ +#define LACP_TLV_ACTOR 0x01 +#define LACP_TLV_PARTNER 0x02 +#define LACP_TLV_COLLECTOR 0x03 +#define LACP_TLV_TERMINATOR 0x00 +#define LACP_TLV_LEN_INFO 0x14 /* Actor/Partner Information TLV length */ +#define LACP_TLV_LEN_COLLECTOR 0x10 /* Collector TLV length */ + +/* Our aggregation identity: System priority is a constant (Linux 802.3ad + * defaults to the same 0xffff); the default Actor port priority likewise. */ +#define LACP_SYS_PRIO 0xffff +#define LACP_DEF_PORT_PRIO 0x00ff + +/* Actor_State / Partner_State flag bits (802.3ad 43.4.2) */ +#define LACP_STATE_ACTIVITY 0x01 /* 1 = Active LACP (send periodically) */ +#define LACP_STATE_TIMEOUT 0x02 /* 1 = Short (fast) timeout */ +#define LACP_STATE_AGGREGATION 0x04 /* 1 = link is aggregatable */ +#define LACP_STATE_SYNC 0x08 /* 1 = in sync with partner */ +#define LACP_STATE_COLLECTING 0x10 +#define LACP_STATE_DISTRIBUTING 0x20 +#define LACP_STATE_DEFAULTED 0x40 /* partner info is administrative default*/ +#define LACP_STATE_EXPIRED 0x80 /* wire-format doc; unused by the simplified RX machine */ + +/* Fully-participating actor: in sync AND collecting AND distributing. */ +#define LACP_STATE_FULL (LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING) + +/* + * Timer units: one step of the LACP timers, taken every four system ticks, + * 50 per second. A long pass of the main loop is caught up on a few steps at + * a time, so a period is a range and the worst case is what matters. + * + * The transmit periods are not free to choose. Our own timeouts only decide + * how patient we are, but our TX period decides when the PARTNER gives up on + * us: 802.3ad pairs fast periodic 1 s with a 3 s short timeout, and slow + * periodic 30 s with a 90 s long timeout. A period whose worst case reaches + * the partner's threshold makes it expire us intermittently, which reads as + * an aggregate that works but never stops churning. */ +#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ +#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ +#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ +#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */ + +/* Per-port LACP receive-machine state (802.3ad 43.4.12) */ +#define LACP_RX_INITIALIZE 0 +#define LACP_RX_PORT_DISABLED 1 /* numbering reserved (exported via /lacp.json "rs") */ +#define LACP_RX_EXPIRED 2 /* numbering reserved (exported via /lacp.json "rs") */ +#define LACP_RX_DEFAULTED 3 +#define LACP_RX_CURRENT 4 + +#endif diff --git a/rtl837x_regs.h b/rtl837x_regs.h index dc4b2dc1..1eb865a4 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -284,6 +284,10 @@ * Loop detection / STP */ #define RTL8373_RLDP_TIMER 0x1074 +/* Per reserved-multicast-address action. The registers are packed and + * consecutive by ENTRY rather than by the byte of the address: entry 0 is + * 01:80:C2:00:00:00 at 0x4ecc and entry 2 is ...:02 (Slow Protocols / LACP) + * at 0x4ed4, but entry 5 is ...:08, so the step of 4 counts entries. */ #define RTL837X_RMA0_CONF 0x4ecc #define RTL837X_RMA2_CONF 0x4ed4 #define RTL837X_RMA_ACT_MASK 0x30 diff --git a/rtlplayground.c b/rtlplayground.c index 3f7860dc..f61140a2 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -13,6 +13,7 @@ #include "rtl837x_port.h" #include "rtl837x_trap.h" #include "rtl837x_stp.h" +#include "rtl837x_lacp.h" #include "rtl837x_igmp.h" #include "rtl837x_leds.h" #include "rtl837x_bandwidth.h" @@ -133,6 +134,7 @@ __xdata bool stp_enabled; __xdata uint8_t igmpEnabled; __xdata bool lldp_enabled; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ +__xdata uint8_t lacpEnabled; __code const uint16_t bit_mask[16] = { 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, @@ -1059,12 +1061,18 @@ void handle_rx(void) print_byte(uip_buf[3]); print_byte(uip_buf[4]); print_byte(uip_buf[5]); write_char('\n'); print_string(" MGMT-VLAN: "); print_short(management_vlan); write_char('\n'); #endif - if (stp_enabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet? - && uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x00) { - stp_in(); - if (uip_len) { - print_string("STP TX\n"); - tcpip_output(); + if ((stp_enabled || lacpEnabled) && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 + && uip_buf[2] == 0xc2 && uip_buf[3] == 0x00 && uip_buf[4] == 0x00) { // 01:80:C2:00:00:0x + if (stp_enabled && uip_buf[5] == 0x00) { // STP/RSTP BPDU + stp_in(); + if (uip_len) { + print_string("STP TX\n"); + tcpip_output(); + } + } else if (lacpEnabled && uip_buf[5] == 0x02) { // LACPDU (slow protocols) + lacp_in(); + if (uip_len) + tcpip_output(); } } else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? && igmp_packet_handler()) { @@ -1290,6 +1298,9 @@ static void handle_tick(void) stp_timers(); } } + // If LACP enabled, drive its machines (own tick divider lives in the banked module) + if (lacpEnabled) + lacp_timers(); health_phase(HEALTH_PH_STP); lldp_tick(); health_phase(HEALTH_LLDP); @@ -1792,6 +1803,8 @@ void main(void) #endif stp_enabled = 0; stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ + lacpEnabled = 0; + lacp_init(); /* clear per-LAG state (port->LAG map = NONE) before any config replay */ nic_setup(); REG_SET(RTL837X_NIC_INT_MSK, NIC_INT_RXIE); trap_init(); diff --git a/test/Makefile b/test/Makefile index ae3884ac..e47cbf4e 100644 --- a/test/Makefile +++ b/test/Makefile @@ -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 diff --git a/test/README.md b/test/README.md index 08ec0cef..4f040d5a 100644 --- a/test/README.md +++ b/test/README.md @@ -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_.c` with `main()` driving the module's entry points and diff --git a/test/env_tables.c b/test/env_tables.c index 41694b72..6a841a19 100644 --- a/test/env_tables.c +++ b/test/env_tables.c @@ -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]; diff --git a/test/lacp/.gitignore b/test/lacp/.gitignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/test/lacp/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/test/lacp/Makefile b/test/lacp/Makefile new file mode 100644 index 00000000..856f21e5 --- /dev/null +++ b/test/lacp/Makefile @@ -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 diff --git a/test/lacp/lacp_sim.c b/test/lacp/lacp_sim.c new file mode 100644 index 00000000..a833ac9f --- /dev/null +++ b/test/lacp/lacp_sim.c @@ -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 +#include +#include +#include + +#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; +} diff --git a/test/lacp/shim/machine.h b/test/lacp/shim/machine.h new file mode 100644 index 00000000..7f6612fb --- /dev/null +++ b/test/lacp/shim/machine.h @@ -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 +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 diff --git a/test/lacp/shim/rtl837x_common.h b/test/lacp/shim/rtl837x_common.h new file mode 100644 index 00000000..b21c7141 --- /dev/null +++ b/test/lacp/shim/rtl837x_common.h @@ -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 +#include /* 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 diff --git a/test/lacp/shim/rtl837x_port.h b/test/lacp/shim/rtl837x_port.h new file mode 100644 index 00000000..646d5253 --- /dev/null +++ b/test/lacp/shim/rtl837x_port.h @@ -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 +void port_lag_members_set(uint8_t lag, uint16_t members); +void port_isolate(uint8_t port, uint16_t pmask); +#endif diff --git a/test/lacp/shim/rtl837x_regs.h b/test/lacp/shim/rtl837x_regs.h new file mode 100644 index 00000000..2dc92d8d --- /dev/null +++ b/test/lacp/shim/rtl837x_regs.h @@ -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 + +#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 diff --git a/test/lacp/shim/uip.h b/test/lacp/shim/uip.h new file mode 100644 index 00000000..0d5a51ec --- /dev/null +++ b/test/lacp/shim/uip.h @@ -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 +#define UIP_CONF_BUFFER_SIZE 600 +struct uip_eth_addr { uint8_t addr[6]; }; +extern uint16_t uip_len; +void tcpip_output(void); +#endif diff --git a/test/port_hash/.gitignore b/test/port_hash/.gitignore new file mode 100644 index 00000000..cf8f788c --- /dev/null +++ b/test/port_hash/.gitignore @@ -0,0 +1,2 @@ +test_lag_hash +port_lag_members_set.inc diff --git a/test/port_hash/Makefile b/test/port_hash/Makefile new file mode 100644 index 00000000..feeee940 --- /dev/null +++ b/test/port_hash/Makefile @@ -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 diff --git a/test/port_hash/test_lag_hash.c b/test/port_hash/test_lag_hash.c new file mode 100644 index 00000000..205d3fc8 --- /dev/null +++ b/test/port_hash/test_lag_hash.c @@ -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 +#include +#include + +#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; +} diff --git a/test/test_httpd_tx.c b/test/test_httpd_tx.c index 4df05472..3f056030 100644 --- a/test/test_httpd_tx.c +++ b/test/test_httpd_tx.c @@ -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) { } From edf6351d85281f0dc79a7d7bba87e6bf46f1e781 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:27:19 +0200 Subject: [PATCH 04/20] lacp: arm the partner timeout from our own LACP_Timeout The receive machine armed current_while from the partner's TIMEOUT bit, so against a partner on the slow rate a port whose LACPDUs stopped with the link still up stayed in the trunk for the long timeout, two minutes. 802.3ad 43.4.12 starts the timer from the actor's own LACP_Timeout, and we always advertise the short one: the partner sends every second and is let go after the short timeout. Sandbox T23 plays a slow-rate partner that goes quiet and fails on the code before this. --- rtl837x_lacp.c | 3 +-- test/lacp/lacp_sim.c | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c index b4874f16..4f0fa44d 100644 --- a/rtl837x_lacp.c +++ b/rtl837x_lacp.c @@ -311,8 +311,7 @@ void lacp_in(void) __banked /* Receive machine -> CURRENT, (re)arm partner timeout (43.4.12) */ lacp_rx_state[port] = LACP_RX_CURRENT; - lacp_timeout[port] = (LACP_I->actor.state & LACP_STATE_TIMEOUT) - ? LACP_SHORT_TIMEOUT : LACP_LONG_TIMEOUT; + lacp_timeout[port] = LACP_SHORT_TIMEOUT; /* Elect this LAG's aggregator partner system on first contact (43.4.14) */ if (!lacp_agg_valid[lag]) { diff --git a/test/lacp/lacp_sim.c b/test/lacp/lacp_sim.c index a833ac9f..7efa67c6 100644 --- a/test/lacp/lacp_sim.c +++ b/test/lacp/lacp_sim.c @@ -458,6 +458,23 @@ int main(int argc, char **argv) "T22 truncated LACPDU ignored: the port does not record it"); lacp_lag_set(1, 0); + /* T23: the partner asks for the long timeout (Linux's default slow rate). + * current_while is armed from OUR LACP_Timeout, which is short, so a + * partner that stops speaking LACP with the link up is dropped after the + * short timeout, not after two minutes. */ + lacp_lag_set(1, 0x0030); + step(8); + for (int r = 0; r < 2; r++) { + partner_frame(4, SYS_A, P_ACT|P_AGG|P_SYNC, 1); + partner_frame(5, SYS_A, P_ACT|P_AGG|P_SYNC, 1); + step(8); + } + int slow_converged = hw_members_lag[1] == 0x0030; + step(4 * LACP_SHORT_TIMEOUT + 64); + CHECK(slow_converged && hw_members_lag[1] == 0, + "T23 partner on slow rate goes quiet: dropped after our short timeout"); + 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; From 25040070e5ef6777c30bf14befe0425e487133e8 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:27:20 +0200 Subject: [PATCH 05/20] lacp: "lag d" and a static member list take the group out of LACP Both paths wrote the trunk registers directly and left the protocol running on the group, so "lag show" said disabled while the partner kept both links in one aggregate, and re-sending the LACP line did not repair it because the same-ports shortcut returns early. Each path now releases the group from LACP first; for a group that never ran LACP that is a no-op. --- cmd_parser.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 951d6be5..09621fc3 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -534,6 +534,7 @@ void parse_lag(void) goto err; if (cmd_compare(2, "d")) { + lacp_lag_set(group, 0); port_lag_members_set(group, 0); return; } @@ -557,10 +558,12 @@ void parse_lag(void) members |= ((uint16_t)1) << atoi_results_u8; } - if (lacp_mode) + if (lacp_mode) { lacp_lag_set(group, members); /* 0 ports also valid: removes the LAG */ - else + } else { + lacp_lag_set(group, 0); port_lag_members_set(group, members); + } return; err: cmd_error("lag (show | <1-4> (d | [lacp] ...))\n"); From 0df48fbb1cd47fda7971fa93ebd19751fec93cf5 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:27:21 +0200 Subject: [PATCH 06/20] httpd: leave the L2 listing as it is on main Listing by next-address also returned the reserved-multicast entries STP writes, shown as port 1 with a delete button, while l2_delete() still looks up by next-unicast and so removed the unicast entry after that index instead. The change was there to show the LACP steering entries; delivery by trap writes none, so it goes. --- httpd/page_impl.c | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 44c9ddcb..94ccb483 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -409,19 +409,14 @@ void send_l2(uint16_t idx) */ __xdata uint16_t entry = idx & 0xfff; __xdata uint16_t first_entry = 0xffff; // An illegal entry index - __xdata uint16_t mbr; __bit first = true; char_to_html('['); while (1) { entries_left--; uint8_t port = 0; uint8_t lag; - uint8_t mc = 0; reg_read_m(RTL837x_TBL_DATA_0); - // Iterate with "next address" (method 2) so static multicast entries - - // an IGMP group entry, say - are listed too, not only learned or - // static unicast ones. - REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_ADDRESS << 6), sfr_data[3]); + REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_L2UC << 6), sfr_data[3]); REG_WRITE(RTL837X_TBL_CTRL, entry >> 8, entry, TBL_L2_UNICAST, TBL_EXECUTE); do { @@ -429,7 +424,7 @@ void send_l2(uint16_t idx) } while (sfr_data[3] & TBL_EXECUTE); reg_read_m(RTL837x_L2_DATA_OUT_B); - __bit valid = (sfr_data[0] & 0x20) && !(sfr_data[0] & 0x10); + __bit valid = (sfr_data[0] & 0x20) != 0; if (valid) { /* separator + 74-byte worst-case entry + closing "]" */ if (slen + 76 > TCP_OUTBUF_SIZE) @@ -438,8 +433,6 @@ void send_l2(uint16_t idx) char_to_html(','); first = false; - mc = sfr_data[2] & 0x01; // multicast MAC: member mask, no SPA/age - // VLAN, taken from the read above instead of reading the register twice slen += strtox(outbuf + slen, "{\"vlan\":\""); charhex_to_html(sfr_data[0] & 0x0f); @@ -458,23 +451,12 @@ void send_l2(uint16_t idx) // type reg_read_m(RTL837x_L2_DATA_OUT_C); - if (mc || (sfr_data[1] & 0x1)) + if (sfr_data[1] & 0x1) slen += strtox(outbuf + slen, "\",\"type\":\"s\",\"port\":"); else slen += strtox(outbuf + slen, "\",\"type\":\"l\",\"port\":"); - if (mc) { - // Report the lowest member of the mask (an entry holding only - // the CPU port is rendered as "CPU" by the UI) - mbr = port | ((uint16_t)sfr_data[3] << 2); - port = 0; - while (mbr && !(mbr & 1)) { - mbr >>= 1; - port++; - } - } else { - port |= (sfr_data[3] & 0x3) << 2; - } + port |= (sfr_data[3] & 0x3) << 2; itoa_html(port); slen += strtox(outbuf + slen, ",\"lag\":"); lag = port_lag_of(port); From 4ccbf5197613b903e39e84849eec12f1d0e7c529 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:27:22 +0200 Subject: [PATCH 07/20] lacp: ignore a LACPDU that carries our own system id Two ports of one group cabled to each other heard themselves and the group aggregated with itself. Sandbox T24. --- rtl837x_lacp.c | 2 ++ test/lacp/lacp_sim.c | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c index 4f0fa44d..e2ede9e3 100644 --- a/rtl837x_lacp.c +++ b/rtl837x_lacp.c @@ -292,6 +292,8 @@ void lacp_in(void) __banked uint8_t lag = lacp_port_lag[port]; if (lag == LACP_LAG_NONE) return; + if (lacp_sys_eq(LACP_I->actor.sys, uip_ethaddr.addr)) + return; #ifdef DEBUG diff --git a/test/lacp/lacp_sim.c b/test/lacp/lacp_sim.c index 7efa67c6..13d571cd 100644 --- a/test/lacp/lacp_sim.c +++ b/test/lacp/lacp_sim.c @@ -475,6 +475,15 @@ int main(int argc, char **argv) "T23 partner on slow rate goes quiet: dropped after our short timeout"); lacp_lag_set(1, 0); + /* T24: a LACPDU carrying our own system id (two ports of one group + * cabled to each other) must not be taken for a partner. */ + lacp_lag_set(1, 0x0030); + step(8); + partner_frame(4, uip_ethaddr.addr, P_ACT|P_AGG|P_TO|P_SYNC, 1); + CHECK(lacp_rx_count[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE, + "T24 own system id: LACPDU ignored"); + 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; From 4e4a2ea4db2cedb7595371f232efdf86b112e205 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:27:22 +0200 Subject: [PATCH 08/20] lacp: review follow-ups lacp_in() always leaves uip_len at 0, so the tcpip_output() after it never ran. The LAG page names the active members by front-panel port instead of a logical mask. The partner key is not part of selection; say so under Known limits. --- doc/lacp.md | 2 ++ html/app.js | 3 ++- rtlplayground.c | 2 -- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/lacp.md b/doc/lacp.md index 1b78459e..3160469a 100644 --- a/doc/lacp.md +++ b/doc/lacp.md @@ -228,4 +228,6 @@ the dead member. * No Churn Detection machines and no Marker protocol. * Coupled Mux: collecting and distributing are enabled together. * One aggregator per group. +* Selection compares the partner's system only, not its key: two aggregates + that one partner system offers on the same group land in one trunk here. * Member compatibility is not validated; see #377. diff --git a/html/app.js b/html/app.js index 4c7ed7f5..34e9713e 100644 --- a/html/app.js +++ b/html/app.js @@ -1843,6 +1843,7 @@ function buildLag(){ })(g); } var lacpCfg=[0,0,0,0,0]; +function maskPorts(m){var r=[];for(var p=1;p<=S.n;p++)if((m>>S.physToLog[p-1])&1)r.push(p);return r.length?r.join(" "):"-"} function lagLoad(){ return getJSON("/lacp.json").then(function(c){ var tb=$("lacptbl").tBodies[0]; @@ -1852,7 +1853,7 @@ function lagLoad(){ lacpCfg[g]=parseInt(lg.cfg,16); if($("lgm"+g).closest(".card").dataset.dirty)return; $("lgm"+g).value=lacpCfg[g]?"lacp":"static"; - $("lgs"+g).textContent=lacpCfg[g]?"LACP: "+t("lacp_agg")+" "+(lg.aggValid?lg.agg:"("+t("lacp_neg")+")")+", "+t("lacp_members")+" 0x"+lg.members:""; + $("lgs"+g).textContent=lacpCfg[g]?"LACP: "+t("lacp_agg")+" "+(lg.aggValid?lg.agg:"("+t("lacp_neg")+")")+", "+t("lacp_members")+" "+maskPorts(parseInt(lg.members,16)):""; }); if(c.on)c.ports.forEach(function(p){ if(p.lag===255)return; diff --git a/rtlplayground.c b/rtlplayground.c index f61140a2..38e322e7 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -1071,8 +1071,6 @@ void handle_rx(void) } } else if (lacpEnabled && uip_buf[5] == 0x02) { // LACPDU (slow protocols) lacp_in(); - if (uip_len) - tcpip_output(); } } else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? && igmp_packet_handler()) { From 2e6425f5e63510a3401fd0d8fb80741ee511397b Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:21:07 +0200 Subject: [PATCH 09/20] lacp: keep lacp_in() and lacp_lag_set() out of the data segment The port and group of lacp_in(), the member mask passed to lacp_lag_set() and the compiler temporaries both functions spilled took six bytes of DSEG, and with them six bytes of stack. The locals and the mask live in xdata now, the partner fields go through the scratch word, and lacp_lag_set() records the new group of a port before it releases the old one. One byte is left: the group number passed in. --- rtl837x_lacp.c | 25 +++++++++++++++---------- rtl837x_lacp.h | 3 +-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c index e2ede9e3..f03ef8ef 100644 --- a/rtl837x_lacp.c +++ b/rtl837x_lacp.c @@ -284,12 +284,12 @@ void lacp_in(void) __banked /* Per rtl837x_common.h, pmask carries a 4-bit port number on RX * (hardware-verified on RTL8373: per-port rx counters track the actual * ingress port through full LACP convergence). */ - uint8_t port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; + __xdata uint8_t port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; if (port < machine.min_port || port > machine.max_port) return; /* Only ports assigned to a LACP-mode LAG participate in the protocol. */ - uint8_t lag = lacp_port_lag[port]; + __xdata uint8_t lag = lacp_port_lag[port]; if (lag == LACP_LAG_NONE) return; if (lacp_sys_eq(LACP_I->actor.sys, uip_ethaddr.addr)) @@ -305,10 +305,14 @@ void lacp_in(void) __banked /* Record partner = the remote's Actor block (802.3ad 43.4.9 recordPDU) */ memcpy(lacp_partner_sys[port], LACP_I->actor.sys, 6); - lacp_partner_key[port] = HTONS(LACP_I->actor.key); - lacp_partner_port[port] = HTONS(LACP_I->actor.port); - lacp_partner_sys_prio[port] = HTONS(LACP_I->actor.sys_prio); - lacp_partner_port_prio[port] = HTONS(LACP_I->actor.port_prio); + lacp_scratch_mask = HTONS(LACP_I->actor.key); + lacp_partner_key[port] = lacp_scratch_mask; + lacp_scratch_mask = HTONS(LACP_I->actor.port); + lacp_partner_port[port] = lacp_scratch_mask; + lacp_scratch_mask = HTONS(LACP_I->actor.sys_prio); + lacp_partner_sys_prio[port] = lacp_scratch_mask; + lacp_scratch_mask = HTONS(LACP_I->actor.port_prio); + lacp_partner_port_prio[port] = lacp_scratch_mask; lacp_partner_state[port] = LACP_I->actor.state; /* Receive machine -> CURRENT, (re)arm partner timeout (43.4.12) */ @@ -508,7 +512,7 @@ void lacp_init(void) __banked * The engine's RMA trap is enabled on the first LACP LAG and torn down with the * last. Never wipes a static "lag" the user configured on a different group. */ -void lacp_lag_set(uint8_t lag, uint16_t ports) __banked +void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked { if (lag >= LACP_NUM_LAGS) return; @@ -542,9 +546,11 @@ void lacp_lag_set(uint8_t lag, uint16_t ports) __banked if (!(ports & port_bit(i))) continue; lacp_scratch_lag = lacp_port_lag[i]; + lacp_port_lag[i] = lag; if (lacp_scratch_lag != LACP_LAG_NONE && lacp_scratch_lag != lag) { - lacp_lag_ports[lacp_scratch_lag] &= ~port_bit(i); - if (!lacp_lag_ports[lacp_scratch_lag]) { + lacp_scratch_mask = lacp_lag_ports[lacp_scratch_lag] & ~port_bit(i); + lacp_lag_ports[lacp_scratch_lag] = lacp_scratch_mask; + if (!lacp_scratch_mask) { lacp_agg_valid[lacp_scratch_lag] = 0; if (lacp_members_last[lacp_scratch_lag]) { lacp_members_last[lacp_scratch_lag] = 0; @@ -552,7 +558,6 @@ void lacp_lag_set(uint8_t lag, uint16_t ports) __banked } } } - lacp_port_lag[i] = lag; lacp_port_init(i); } diff --git a/rtl837x_lacp.h b/rtl837x_lacp.h index 5f03a16e..514f9e35 100644 --- a/rtl837x_lacp.h +++ b/rtl837x_lacp.h @@ -16,7 +16,7 @@ void lacp_show(void) __banked; /* "lacp show" - per-port state + RX counters */ /* Assign a candidate-port mask to a LACP-mode LAG (`lag lacp `). * ports == 0 removes the LAG from LACP management. Enables the engine on first * LACP LAG and tears it down when the last one goes away. */ -void lacp_lag_set(uint8_t lag, uint16_t ports) __banked; +void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked; /* * Per-LAG LACP: each of the 4 hardware trunk groups (0-3) can independently run @@ -89,7 +89,6 @@ extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS];/* trunk members we las #define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ #define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ #define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ -#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */ /* Per-port LACP receive-machine state (802.3ad 43.4.12) */ #define LACP_RX_INITIALIZE 0 From 64efd3fb9fc29bc2f34d54ddbd321f8a2c784324 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:21:07 +0200 Subject: [PATCH 10/20] lacp: drop the unused long timeout and an unrelated pvid hunk --- cmd_parser.c | 3 +-- doc/lacp.md | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 09621fc3..3cfb9007 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -2000,9 +2000,8 @@ void cmd_parser(void) __banked } else if (cmd_compare(0, "pvid")) { if (cmd_words_len == 3 && cmd_parse_port_separator(cmd_words_b[1]) != 0 && atoi_short(cmd_words_b[2]) && atoi_results_short && atoi_results_short <= 4094) - { port_pvid_set(atoi_results_u8, atoi_results_short); - } else { + else { cmd_error("pvid <1-4094>\n"); } } else if (cmd_compare(0, "vlan")) { diff --git a/doc/lacp.md b/doc/lacp.md index 3160469a..390261da 100644 --- a/doc/lacp.md +++ b/doc/lacp.md @@ -96,7 +96,6 @@ mismatch should be rejected or applied to the whole group. #define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ #define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ #define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ -#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */ ``` One unit is one step of the LACP timers, taken every four system ticks, so 50 From 1bfe63217400fcc960676c7cb33f9f3e6096fa15 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:59:08 +0200 Subject: [PATCH 11/20] doc: lacp: lag d and a static list leave LACP, new lacp_lag_set() prototype --- doc/lacp.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/lacp.md b/doc/lacp.md index 390261da..ab09db39 100644 --- a/doc/lacp.md +++ b/doc/lacp.md @@ -129,7 +129,7 @@ longer keeps hashing traffic onto a member that no longer agrees to carry it. ``` void lacp_init(void) __banked; /* boot init: clear per-LAG state */ void lacp_off(void) __banked; -void lacp_lag_set(uint8_t lag, uint16_t ports) __banked; +void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked; void lacp_in(void) __banked; void lacp_timers(void) __banked; void lacp_show(void) __banked; @@ -145,19 +145,20 @@ A group is put under LACP by adding `lacp` to the `lag` command: ``` lag 1 lacp 7 8 # ports 7 and 8 become LACP candidates of group 1 -lag 1 lacp off # group 1 goes back to static (so does "lag 1 lacp") +lag 1 lacp off # group 1 leaves LACP, its trunk is cleared (so does "lag 1 lacp") +lag 1 d # group 1 disabled, whether it ran LACP or not lag 1 7 8 # plain static aggregation, no protocol ``` A group put under LACP gives up the static trunk it had: the hardware trunk is -cleared and rebuilt from the ports that converge. A port named in another LACP -group moves to the new one. The protocol starts with the first group put under -LACP and stops when the last one is taken out; there is no separate switch for -it. Two commands act on -the whole engine: +cleared and rebuilt from the ports that converge. The other way round, `lag 1 d` +and a static member list take the group out of LACP before they touch the +trunk. A port named in another LACP group moves to the new one. The protocol +starts with the first group put under LACP and stops when the last one is taken +out; there is no separate switch for it. Two commands act on the whole engine: ``` -lacp off # every group back to static, protocol stopped +lacp off # every group leaves LACP, protocol stopped lacp show ``` From 4a14f5093227be17995e54463499fd0b9b94b106 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:02:50 +0200 Subject: [PATCH 12/20] test: build the LACP test on the real headers, drop the port_hash test The LACP test now compiles with the shared harness flags and sdcc_shim.h against the firmware headers, so the copies under test/lacp/shim are gone. test_port_tables already covers the trunk hash seed that test/port_hash checked against its own register copies. lacp_in() declares its locals at the top, which the stricter host build needs. --- rtl837x_lacp.c | 6 ++- test/Makefile | 16 +++---- test/README.md | 3 +- test/lacp/.gitignore | 1 - test/lacp/Makefile | 31 ------------ test/lacp/shim/machine.h | 12 ----- test/lacp/shim/rtl837x_common.h | 63 ------------------------- test/lacp/shim/rtl837x_port.h | 7 --- test/lacp/shim/rtl837x_regs.h | 19 -------- test/lacp/shim/uip.h | 9 ---- test/port_hash/.gitignore | 2 - test/port_hash/Makefile | 6 --- test/port_hash/test_lag_hash.c | 68 --------------------------- test/{lacp/lacp_sim.c => test_lacp.c} | 12 +---- 14 files changed, 12 insertions(+), 243 deletions(-) delete mode 100644 test/lacp/.gitignore delete mode 100644 test/lacp/Makefile delete mode 100644 test/lacp/shim/machine.h delete mode 100644 test/lacp/shim/rtl837x_common.h delete mode 100644 test/lacp/shim/rtl837x_port.h delete mode 100644 test/lacp/shim/rtl837x_regs.h delete mode 100644 test/lacp/shim/uip.h delete mode 100644 test/port_hash/.gitignore delete mode 100644 test/port_hash/Makefile delete mode 100644 test/port_hash/test_lag_hash.c rename test/{lacp/lacp_sim.c => test_lacp.c} (97%) diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c index f03ef8ef..276e80d5 100644 --- a/rtl837x_lacp.c +++ b/rtl837x_lacp.c @@ -270,6 +270,8 @@ void lacp_send(uint8_t port) __banked */ void lacp_in(void) __banked { + __xdata uint8_t port, lag; + lacp_rxbody = TRAP_RX_BODY(); if (uip_len < lacp_rxbody + sizeof(struct lacpdu_in)) { uip_len = 0; @@ -284,12 +286,12 @@ void lacp_in(void) __banked /* Per rtl837x_common.h, pmask carries a 4-bit port number on RX * (hardware-verified on RTL8373: per-port rx counters track the actual * ingress port through full LACP convergence). */ - __xdata uint8_t port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; + port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; if (port < machine.min_port || port > machine.max_port) return; /* Only ports assigned to a LACP-mode LAG participate in the protocol. */ - __xdata uint8_t lag = lacp_port_lag[port]; + lag = lacp_port_lag[port]; if (lag == LACP_LAG_NONE) return; if (lacp_sys_eq(LACP_I->actor.sys, uip_ethaddr.addr)) diff --git a/test/Makefile b/test/Makefile index e47cbf4e..cdb27355 100644 --- a/test/Makefile +++ b/test/Makefile @@ -28,7 +28,8 @@ TESTS = $(BUILDDIR)/test_cmd_editor \ $(BUILDDIR)/test_port_tables \ $(BUILDDIR)/test_page_json \ $(BUILDDIR)/test_httpd_tx \ - $(BUILDDIR)/test_tick_gate + $(BUILDDIR)/test_tick_gate \ + $(BUILDDIR)/test_lacp # The table tests link the register mock and one environment file; stub/ # stands in for the two headers a firmware build generates. @@ -54,29 +55,24 @@ $(BUILDDIR)/test_tick_gate: test_tick_gate.c support.c $(SRCROOT)/cmd_editor.c | $(BUILDDIR)/test_page_json: test_page_json.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR) $(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS) +# rtl837x_lacp.c is compiled UNMODIFIED from the firmware tree. +$(BUILDDIR)/test_lacp: test_lacp.c $(SRCROOT)/rtl837x_lacp.c | $(BUILDDIR) + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) + # httpd/httpd.c and uip/uip.c are compiled UNMODIFIED from the firmware tree; # this test carries the environment below them itself. $(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 diff --git a/test/README.md b/test/README.md index 4f040d5a..a79b3242 100644 --- a/test/README.md +++ b/test/README.md @@ -48,8 +48,7 @@ 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 | +| `test_lacp` | `rtl837x_lacp.c` | state machines against a simulated 802.3ad partner: convergence, mis-cabling, expiry, re-election, two groups, delivery by trap | ## Adding a test for another module 1. Write `test_.c` with `main()` driving the module's entry points and diff --git a/test/lacp/.gitignore b/test/lacp/.gitignore deleted file mode 100644 index 567609b1..00000000 --- a/test/lacp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/test/lacp/Makefile b/test/lacp/Makefile deleted file mode 100644 index 856f21e5..00000000 --- a/test/lacp/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -# 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 diff --git a/test/lacp/shim/machine.h b/test/lacp/shim/machine.h deleted file mode 100644 index 7f6612fb..00000000 --- a/test/lacp/shim/machine.h +++ /dev/null @@ -1,12 +0,0 @@ -/* 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 -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 diff --git a/test/lacp/shim/rtl837x_common.h b/test/lacp/shim/rtl837x_common.h deleted file mode 100644 index b21c7141..00000000 --- a/test/lacp/shim/rtl837x_common.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 -#include /* 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 diff --git a/test/lacp/shim/rtl837x_port.h b/test/lacp/shim/rtl837x_port.h deleted file mode 100644 index 646d5253..00000000 --- a/test/lacp/shim/rtl837x_port.h +++ /dev/null @@ -1,7 +0,0 @@ -/* Host-build shim: the harness mocks trunk programming and records calls */ -#ifndef _SHIM_RTL837X_PORT_H_ -#define _SHIM_RTL837X_PORT_H_ -#include -void port_lag_members_set(uint8_t lag, uint16_t members); -void port_isolate(uint8_t port, uint16_t pmask); -#endif diff --git a/test/lacp/shim/rtl837x_regs.h b/test/lacp/shim/rtl837x_regs.h deleted file mode 100644 index 2dc92d8d..00000000 --- a/test/lacp/shim/rtl837x_regs.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 - -#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 diff --git a/test/lacp/shim/uip.h b/test/lacp/shim/uip.h deleted file mode 100644 index 0d5a51ec..00000000 --- a/test/lacp/shim/uip.h +++ /dev/null @@ -1,9 +0,0 @@ -/* Host-build shim for uip.h - frame buffer + TX hook used by rtl837x_lacp.c */ -#ifndef _SHIM_UIP_H_ -#define _SHIM_UIP_H_ -#include -#define UIP_CONF_BUFFER_SIZE 600 -struct uip_eth_addr { uint8_t addr[6]; }; -extern uint16_t uip_len; -void tcpip_output(void); -#endif diff --git a/test/port_hash/.gitignore b/test/port_hash/.gitignore deleted file mode 100644 index cf8f788c..00000000 --- a/test/port_hash/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -test_lag_hash -port_lag_members_set.inc diff --git a/test/port_hash/Makefile b/test/port_hash/Makefile deleted file mode 100644 index feeee940..00000000 --- a/test/port_hash/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -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 diff --git a/test/port_hash/test_lag_hash.c b/test/port_hash/test_lag_hash.c deleted file mode 100644 index 205d3fc8..00000000 --- a/test/port_hash/test_lag_hash.c +++ /dev/null @@ -1,68 +0,0 @@ -/* 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 -#include -#include - -#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; -} diff --git a/test/lacp/lacp_sim.c b/test/test_lacp.c similarity index 97% rename from test/lacp/lacp_sim.c rename to test/test_lacp.c index 13d571cd..f0ab5f3f 100644 --- a/test/lacp/lacp_sim.c +++ b/test/test_lacp.c @@ -1,13 +1,3 @@ -/* - * 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 #include #include @@ -31,7 +21,7 @@ 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_string(const 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); } From 73b23808cf0e99d8b104dbc695c4e30b37159142 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:05:56 +0200 Subject: [PATCH 13/20] lacp: keep comments to the frame layout The comments repeated the documentation, described tests or explained the code to its author. What stays describes the LACPDU structures. The timer constants are decimal, in units of the LACP timer. --- cmd_parser.c | 4 +- doc/CpuPort.md | 5 -- doc/link_aggregation.md | 49 ------------ httpd/page_impl.c | 7 -- rtl837x_lacp.c | 160 +++++++--------------------------------- rtl837x_lacp.h | 75 ++++++------------- rtl837x_regs.h | 4 - rtlplayground.c | 9 +-- 8 files changed, 52 insertions(+), 261 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 3cfb9007..160f4011 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -539,8 +539,6 @@ void parse_lag(void) return; } - /* "lag lacp [port]..." - run LACP on this LAG with the given candidate - * ports; "lag lacp off" (or no ports) removes the LAG from LACP. */ uint8_t w = 2; __xdata uint8_t lacp_mode = 0; if (cmd_words_len > 2 && cmd_compare(2, "lacp")) { @@ -559,7 +557,7 @@ void parse_lag(void) members |= ((uint16_t)1) << atoi_results_u8; } if (lacp_mode) { - lacp_lag_set(group, members); /* 0 ports also valid: removes the LAG */ + lacp_lag_set(group, members); } else { lacp_lag_set(group, 0); port_lag_members_set(group, members); diff --git a/doc/CpuPort.md b/doc/CpuPort.md index f5e9859b..c10d0a15 100644 --- a/doc/CpuPort.md +++ b/doc/CpuPort.md @@ -79,11 +79,6 @@ bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX All fields are in network byte order. -This word has to be written through `HTONS` like every other field of the tag. -Writing a constant raw puts the bits in the wrong byte, so `0x0020` reaches the -wire as `0x2000`, which is EFID rather than LEARN_DIS. The ASIC then fails to -parse the tag and forwards the frame with the `0x8899` header still on it. - * `EFID_EN`, `EFID`: look the destination up under this filtering ID instead of the port's own * `PRI_EN`, `PRI`: force the given priority on the frame diff --git a/doc/link_aggregation.md b/doc/link_aggregation.md index 462b66de..b725e419 100644 --- a/doc/link_aggregation.md +++ b/doc/link_aggregation.md @@ -227,52 +227,3 @@ 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. diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 94ccb483..e1f43f6e 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -737,9 +737,6 @@ void send_stp(void) slen += strtox(outbuf + slen, "]}"); } -/* LACP protocol status for the LAG page ("/lacp.json"). State variables are - * exported read-only via rtl837x_lacp.h; hardware trunk membership itself is - * already visible through send_lag() (it reads the trunk registers). */ void send_lacp(void) { dbg_string("send_lacp called\n"); @@ -747,7 +744,6 @@ void send_lacp(void) slen += strtox(outbuf + slen, "{\"on\":"); bool_to_html(lacpEnabled); - /* Per-LAG section: candidate ports, elected aggregator, trunk members. */ slen += strtox(outbuf + slen, ",\"lags\":["); for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { slen += strtox(outbuf + slen, "{\"cfg\":\""); @@ -768,7 +764,6 @@ void send_lacp(void) for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { slen += strtox(outbuf + slen, "{\"p\":"); itoa_html(machine.log_to_phys_port[i]); - /* Which LACP LAG this port belongs to; 255 = none */ slen += strtox(outbuf + slen, ",\"lag\":"); itoa_html(lacp_port_lag[i]); slen += strtox(outbuf + slen, ",\"a\":\""); @@ -777,8 +772,6 @@ void send_lacp(void) byte_to_html(lacp_partner_state[i]); slen += strtox(outbuf + slen, "\",\"rs\":"); itoa_html(lacp_rx_state[i]); - /* rx as 4-digit hex: itoa16_html() only renders values up to 9999 - * correctly (see its VLAN-ID comment), the counter goes to 65535 */ slen += strtox(outbuf + slen, ",\"rx\":\""); byte_to_html(lacp_rx_count[i] >> 8); byte_to_html(lacp_rx_count[i]); diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c index 276e80d5..5a14e3b4 100644 --- a/rtl837x_lacp.c +++ b/rtl837x_lacp.c @@ -18,11 +18,10 @@ #include "machine.h" extern __code const struct machine machine; -extern __xdata struct machine_runtime machine_detected; /* runtime chip detection, owned by rtl837x_port.c */ -extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for slow protocols */ +extern __xdata struct machine_runtime machine_detected; +extern __xdata uint16_t management_vlan; extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; -/* lacpEnabled: extern in rtl837x_lacp.h, owned by rtlplayground.c */ /* 20-byte Actor/Partner information body (802.3ad 43.4.2.2), network byte order */ struct lacp_info { @@ -78,35 +77,29 @@ struct lacpdu_in { struct lacp_info partner; }; -__xdata uint8_t lacp_rxbody; /* offset of the LACPDU body: the RTL tag, plus a VLAN tag when forwarded */ +__xdata uint8_t lacp_rxbody; #define LACP_O ((__xdata struct lacpdu *)&uip_buf[RTL_FRAME_DESC_SIZE]) #define LACP_H ((__xdata struct lacpdu_in_hdr *)&uip_buf[0]) #define LACP_I ((__xdata struct lacpdu_in *)&uip_buf[lacp_rxbody]) -/* --- Per-port participant state (802.3ad 43.4.7). Indexed by physical port. --- */ -__xdata uint8_t lacp_actor_state[10]; /* our Actor_State flags */ -__xdata uint8_t lacp_rx_state[10]; /* LACP_RX_* receive machine */ -__xdata uint8_t lacp_partner_state[10]; /* last Partner_State we saw */ -__xdata uint8_t lacp_partner_sys[10][6]; /* partner System ID */ -__xdata uint16_t lacp_partner_key[10]; /* partner Key */ -__xdata uint16_t lacp_partner_port[10]; /* partner Port number */ -/* The Partner block echoes the peer's actor identity verbatim, see - * doc/link_aggregation.md. */ -__xdata uint16_t lacp_partner_sys_prio[10]; /* partner System priority */ -__xdata uint16_t lacp_partner_port_prio[10]; /* partner Port priority */ -__xdata uint16_t lacp_periodic[10]; /* down-counter to next TX */ -__xdata uint16_t lacp_timeout[10]; /* down-counter to partner expiry*/ -__xdata uint8_t lacp_ntt[10]; /* Need-To-Transmit flag */ +__xdata uint8_t lacp_actor_state[10]; +__xdata uint8_t lacp_rx_state[10]; +__xdata uint8_t lacp_partner_state[10]; +__xdata uint8_t lacp_partner_sys[10][6]; +__xdata uint16_t lacp_partner_key[10]; +__xdata uint16_t lacp_partner_port[10]; +__xdata uint16_t lacp_partner_sys_prio[10]; +__xdata uint16_t lacp_partner_port_prio[10]; +__xdata uint16_t lacp_periodic[10]; +__xdata uint16_t lacp_timeout[10]; +__xdata uint8_t lacp_ntt[10]; -/* The timers step every four system ticks (50 Hz) whatever the pace of the - * main loop; a long pass is caught up on, a few steps at a time. */ #define LACP_TICK_STEP (SYS_TICK_HZ / 50) #define LACP_CATCH_UP 8 __xdata uint8_t lacp_tick_last; extern volatile __xdata uint32_t ticks; -/* Per-LAG state: the hardware trunk groups run LACP independently. */ __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; __xdata uint8_t lacp_port_lag[10]; __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; @@ -117,12 +110,10 @@ __xdata uint16_t lacp_scratch_mask; __xdata uint8_t lacp_scratch_flag; __xdata uint8_t lacp_scratch_lag; -/* Per-port RX LACPDU count - primarily a hardware bring-up diagnostic to - * verify slow-protocol frames actually reach the CPU port (see "lacp show") */ __xdata uint16_t lacp_rx_count[10]; -void lacp_mux_update(void) __banked; /* defined below, used from lacp_in */ +void lacp_mux_update(void) __banked; static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b) { @@ -134,15 +125,10 @@ static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b) } -/* - * Selection logic (simplified 43.4.14): a port may join the aggregator when - * we have fresh partner info, the partner considers the link aggregatable, - * and the partner is the same system the aggregator was elected for. - */ static uint8_t lacp_port_selected(uint8_t port) { uint8_t lag = lacp_port_lag[port]; - if (lag == LACP_LAG_NONE) /* port not in any LACP LAG */ + if (lag == LACP_LAG_NONE) return 0; if (lacp_rx_state[port] != LACP_RX_CURRENT) return 0; @@ -154,11 +140,6 @@ static uint8_t lacp_port_selected(uint8_t port) } -/* - * Mux machine (simplified coupled control, 43.4.15): SYNC follows selection; - * COLLECTING+DISTRIBUTING additionally require the partner to be in sync. - * Returns 1 if the actor state changed (caller sets NTT). - */ static uint8_t lacp_mux_machine(uint8_t port) { uint8_t old = lacp_actor_state[port]; @@ -183,7 +164,6 @@ static uint8_t lacp_mux_machine(uint8_t port) #define port_bit(p) (((uint8_t)1) << (p)) -/* Build and emit one LACPDU out physical port `port` (802.3ad 43.4.2). */ void lacp_send(uint8_t port) __banked { memset((__xdata uint8_t *)LACP_O, 0, sizeof(struct lacpdu)); @@ -194,42 +174,22 @@ void lacp_send(uint8_t port) __banked LACP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); LACP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; LACP_O->rtl_tag.reason = 0x00; - /* Must go through HTONS like every other tag field: writing 0x0020 raw put - * the bits in the wrong byte (on the wire 0x2000 = EFID, not LEARN_DIS), so - * the ASIC could not parse the tag and forwarded the frame with the 0x8899 - * header still attached - the partner then saw ethertype 0x8899 instead of - * 0x8809 and ignored the LACPDU. KEEP additionally stops the switch from - * rewriting the 802.1Q tag format of a CPU-injected frame (as mainline). */ LACP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS | RTL_TAG_KEEP); - /* ALLOW cleared => this word is the *forwarding* port mask, i.e. directed - * egress to exactly this port (what mainline does). Setting ALLOW instead - * makes it an allowance/permission mask on top of a normal lookup, which - * for a one-hot mask resolves to an empty egress set and the frame is - * silently dropped - verified on hardware. */ - LACP_O->rtl_tag.pmask = HTONS(port_bit(port)); /* egress this port only */ + LACP_O->rtl_tag.pmask = HTONS(port_bit(port)); LACP_O->ethertype = HTONS(SLOW_PROTO_ETHERTYPE); LACP_O->subtype = SLOW_PROTO_SUBTYPE_LACP; LACP_O->version = LACP_VERSION; - /* Actor TLV */ LACP_O->tlv_actor = LACP_TLV_ACTOR; LACP_O->actor_len = LACP_TLV_LEN_INFO; LACP_O->actor.sys_prio = HTONS(LACP_SYS_PRIO); memcpy(LACP_O->actor.sys, uip_ethaddr.addr, 6); - /* Per-LAG Actor Key so a partner never merges ports of our different LAGs - * into one aggregate (only ever called for ports in a LACP LAG). */ LACP_O->actor.key = HTONS((uint16_t)(lacp_port_lag[port] + 1)); LACP_O->actor.port_prio = HTONS(LACP_DEF_PORT_PRIO); - LACP_O->actor.port = HTONS((uint16_t)port + 1); /* 1-based port id */ + LACP_O->actor.port = HTONS((uint16_t)port + 1); LACP_O->actor.state = lacp_actor_state[port]; - /* Partner TLV: echo the last partner info we recorded, VERBATIM. A Linux - * 802.3ad partner (__record_pdu) only accepts the SYNC bit we set above if - * this block mirrors its own actor identity exactly - system+priority, key, - * port+priority. Priorities must be echoed, not hardcoded: Linux defaults to - * system priority 0xffff, so a hardcoded 0 here cleared its partner-SYNC - * (0x3f->0x37) and the aggregate never collected/distributed. */ LACP_O->tlv_partner = LACP_TLV_PARTNER; LACP_O->partner_len = LACP_TLV_LEN_INFO; LACP_O->partner.sys_prio = HTONS(lacp_partner_sys_prio[port]); @@ -239,7 +199,6 @@ void lacp_send(uint8_t port) __banked LACP_O->partner.port = HTONS(lacp_partner_port[port]); LACP_O->partner.state = lacp_partner_state[port]; - /* Collector TLV + Terminator */ LACP_O->tlv_collector = LACP_TLV_COLLECTOR; LACP_O->collector_len = LACP_TLV_LEN_COLLECTOR; LACP_O->collector_max_delay = 0; @@ -247,14 +206,6 @@ void lacp_send(uint8_t port) __banked LACP_O->terminator_len = 0x00; lacp_ntt[port] = 0; - /* Slow-protocol frames are link-local and must egress untagged. With a - * management VLAN set, tcpip_output() splices an 802.1Q tag after the SA, - * shifting our in-frame rtl_tag out of the position the ASIC expects - so it - * fails to parse/strip the CPU tag and the 0x8899 header leaks onto the wire - * (the partner then ignores the LACPDU). Suppress the VLAN insert for this - * frame only; management traffic keeps its tag. Verified on HW: with the - * insert suppressed the LACPDU egresses as clean 0x8809 and the bond - * partner converges (partner MAC becomes the switch, both links aggregate). */ uint16_t saved_mgmt_vlan = management_vlan; management_vlan = 0; uip_len = sizeof(struct lacpdu); @@ -263,11 +214,6 @@ void lacp_send(uint8_t port) __banked } -/* - * Process an incoming LACPDU. Called from the rtlplayground.c dispatch when a - * frame with DA 01:80:C2:00:00:02 and ethertype 0x8809/subtype 0x01 arrives. - * The ingress port is the low nibble of rtl_tag.pmask on RX. - */ void lacp_in(void) __banked { __xdata uint8_t port, lag; @@ -283,14 +229,10 @@ void lacp_in(void) __banked || LACP_I->subtype != SLOW_PROTO_SUBTYPE_LACP) return; - /* Per rtl837x_common.h, pmask carries a 4-bit port number on RX - * (hardware-verified on RTL8373: per-port rx counters track the actual - * ingress port through full LACP convergence). */ port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; if (port < machine.min_port || port > machine.max_port) return; - /* Only ports assigned to a LACP-mode LAG participate in the protocol. */ lag = lacp_port_lag[port]; if (lag == LACP_LAG_NONE) return; @@ -305,7 +247,6 @@ void lacp_in(void) __banked lacp_rx_count[port]++; - /* Record partner = the remote's Actor block (802.3ad 43.4.9 recordPDU) */ memcpy(lacp_partner_sys[port], LACP_I->actor.sys, 6); lacp_scratch_mask = HTONS(LACP_I->actor.key); lacp_partner_key[port] = lacp_scratch_mask; @@ -317,22 +258,17 @@ void lacp_in(void) __banked lacp_partner_port_prio[port] = lacp_scratch_mask; lacp_partner_state[port] = LACP_I->actor.state; - /* Receive machine -> CURRENT, (re)arm partner timeout (43.4.12) */ lacp_rx_state[port] = LACP_RX_CURRENT; lacp_timeout[port] = LACP_SHORT_TIMEOUT; - /* Elect this LAG's aggregator partner system on first contact (43.4.14) */ if (!lacp_agg_valid[lag]) { memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, 6); lacp_agg_valid[lag] = 1; } - /* Selection + Mux for this port; state change => Need-To-Transmit */ if (lacp_mux_machine(port)) lacp_ntt[port] = 1; - /* update_NTT (43.4.12): if the partner's view of us is stale (their - * Partner block does not match our actor state/port), tell them again. */ if (LACP_I->partner.state != lacp_actor_state[port] || HTONS(LACP_I->partner.port) != (uint16_t)port + 1) lacp_ntt[port] = 1; @@ -341,13 +277,10 @@ void lacp_in(void) __banked } -/* Recompute each LACP LAG's hardware trunk membership: a port joins its LAG's - * trunk once it is fully in sync (SYNC+COLLECTING+DISTRIBUTING and the partner - * in SYNC). Each LACP-mode LAG is programmed independently (simplified 43.4.15). */ void lacp_mux_update(void) __banked { for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { - if (!lacp_lag_ports[lag]) /* LAG not under LACP management */ + if (!lacp_lag_ports[lag]) continue; lacp_scratch_mask = 0; for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { @@ -357,8 +290,6 @@ void lacp_mux_update(void) __banked && (lacp_partner_state[i] & LACP_STATE_SYNC)) lacp_scratch_mask |= port_bit(i); } - /* Program the trunk only when this LAG's membership actually changed - * - this runs on every timer tick. */ if (lacp_scratch_mask != lacp_members_last[lag]) { lacp_members_last[lag] = lacp_scratch_mask; port_lag_members_set(lag, lacp_scratch_mask); @@ -370,10 +301,9 @@ void lacp_mux_update(void) __banked static void lacp_step(void) { for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - if (lacp_port_lag[i] == LACP_LAG_NONE) /* not in a LACP LAG */ + if (lacp_port_lag[i] == LACP_LAG_NONE) continue; - /* Periodic transmit machine (802.3ad 43.4.13) */ if (lacp_periodic[i]) { lacp_periodic[i]--; } else { @@ -382,9 +312,6 @@ static void lacp_step(void) lacp_ntt[i] = 1; } - /* Partner timeout -> DEFAULTED (802.3ad 43.4.12): the mux machine - * then drops SYNC/COLLECTING/DISTRIBUTING and the port leaves the - * trunk. TODO(43.4.12): churn detection is diagnostics-only, skipped. */ if (lacp_timeout[i]) { if (!--lacp_timeout[i]) { lacp_rx_state[i] = LACP_RX_DEFAULTED; @@ -398,11 +325,6 @@ static void lacp_step(void) lacp_send(i); } - /* Release each LAG's aggregator identity once none of its ports still - * hears the elected partner, so a re-cabled setup can elect a new one - * (43.4.14). A port that is CURRENT with some other system must not keep - * the old identity alive: it would never be selected, and neither would - * the rest of the LAG once it moves to that system too. */ for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { if (!lacp_agg_valid[lag]) continue; @@ -432,11 +354,10 @@ void lacp_timers(void) __banked } -/* Bring one port up as an active-fast LACP participant (43.4.12 init). */ static void lacp_port_init(uint8_t port) { lacp_actor_state[port] = LACP_STATE_ACTIVITY | LACP_STATE_AGGREGATION - | LACP_STATE_TIMEOUT; /* active + fast */ + | LACP_STATE_TIMEOUT; lacp_rx_state[port] = LACP_RX_INITIALIZE; lacp_partner_state[port] = LACP_STATE_DEFAULTED; memset(lacp_partner_sys[port], 0, 6); @@ -444,13 +365,10 @@ static void lacp_port_init(uint8_t port) lacp_partner_port[port] = 0; lacp_periodic[port] = LACP_FAST_PERIODIC; lacp_timeout[port] = 0; - lacp_ntt[port] = 1; /* announce ourselves immediately */ + lacp_ntt[port] = 1; lacp_rx_count[port] = 0; } -/* Return a port to non-LACP state. (Port isolation is no longer touched - a - * trapped LACPDU never enters the forwarding path, so a sibling cannot see - * it, which is what the isolation workaround was for.) */ static void lacp_port_release(uint8_t port) { lacp_actor_state[port] = 0; @@ -461,7 +379,6 @@ static void lacp_port_release(uint8_t port) memset(lacp_partner_sys[port], 0, 6); } -/* True while any LAG is under LACP management (drives the global RMA trap). */ static uint8_t lacp_any_lag(void) { for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) @@ -470,11 +387,6 @@ static uint8_t lacp_any_lag(void) return 0; } -/* - * Turn the LACP engine on: trap the Slow-Protocols group to the CPU port. The - * trap acts on the address before the VLAN lookup and before the ingress - * filters, so delivery needs no L2 entry and no PVID of its own. - */ static void lacp_engine_on(void) { lacp_tick_last = (uint8_t)ticks; @@ -482,16 +394,12 @@ static void lacp_engine_on(void) trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_TRAP); } -/* Turn the LACP engine off: stop delivering slow-protocols to the CPU. */ static void lacp_engine_off(void) { trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_DROP); lacpEnabled = 0; } -/* One-time boot init: no LAG runs LACP yet, so every port maps to LACP_LAG_NONE. - * MUST run before any "lag ... lacp" config replay - xdata is not zeroed to the - * 0xff sentinel, and a stray 0 would make a port look like it belongs to LAG 0. */ void lacp_init(void) __banked { for (uint8_t i = 0; i < 10; i++) { @@ -507,13 +415,6 @@ void lacp_init(void) __banked lacpEnabled = 0; } -/* - * Assign a candidate-port mask to LACP-mode LAG `lag` (`lag lacp `). - * ports == 0 removes the LAG from LACP management. Ports leaving the LAG are - * returned to normal switching; ports joining start the protocol immediately. - * The engine's RMA trap is enabled on the first LACP LAG and torn down with the - * last. Never wipes a static "lag" the user configured on a different group. - */ void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked { if (lag >= LACP_NUM_LAGS) @@ -522,16 +423,12 @@ void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked return; lacp_scratch_flag = lacp_any_lag(); - /* Drop ports that were in this LAG but are not in the new mask. */ for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { if (lacp_port_lag[i] == lag && !(ports & port_bit(i))) { lacp_port_lag[i] = LACP_LAG_NONE; lacp_port_release(i); } } - /* Release this LAG's trunk: LACP's own, or the static one a group put - * under LACP still has. Taking the group out (ports == 0) leaves a static - * trunk alone. */ if (lacp_members_last[lag] || ports) { lacp_members_last[lag] = 0; port_lag_members_set(lag, 0); @@ -539,11 +436,9 @@ void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked lacp_agg_valid[lag] = 0; lacp_lag_ports[lag] = ports; - if (ports && !lacp_scratch_flag) /* first LACP LAG: bring the engine up first */ + if (ports && !lacp_scratch_flag) lacp_engine_on(); - /* Adopt the new member ports (with the engine already up). A port that - * belonged to another LACP LAG leaves that LAG's candidate set first. */ for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { if (!(ports & port_bit(i))) continue; @@ -563,15 +458,13 @@ void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked lacp_port_init(i); } - if (!lacp_any_lag() && lacp_scratch_flag) /* removed the last LACP LAG */ + if (!lacp_any_lag() && lacp_scratch_flag) lacp_engine_off(); - lacp_mux_update(); /* a LAG that lost a port to this one drops it from its trunk */ + lacp_mux_update(); } -/* "lacp off": remove every LACP LAG (releases their trunks, stops the engine). - * There is no "lacp on": the engine comes up with the first "lag lacp". */ void lacp_off(void) __banked { for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) @@ -580,9 +473,6 @@ void lacp_off(void) __banked } -/* "lacp show": per-port protocol state - the primary bring-up diagnostic. - * A stuck rx=0 counter means slow-protocol frames never reach the CPU port - * (see the RMA note in lacp_engine_on()). */ void lacp_show(void) __banked { print_string("LACP "); print_string(lacpEnabled ? "on" : "off"); write_char('\n'); diff --git a/rtl837x_lacp.h b/rtl837x_lacp.h index 514f9e35..44e3ad4b 100644 --- a/rtl837x_lacp.h +++ b/rtl837x_lacp.h @@ -8,92 +8,61 @@ #include -void lacp_init(void) __banked; /* boot init: clear per-LAG state */ +void lacp_init(void) __banked; void lacp_in(void) __banked; void lacp_timers(void) __banked; void lacp_off(void) __banked; -void lacp_show(void) __banked; /* "lacp show" - per-port state + RX counters */ -/* Assign a candidate-port mask to a LACP-mode LAG (`lag lacp `). - * ports == 0 removes the LAG from LACP management. Enables the engine on first - * LACP LAG and tears it down when the last one goes away. */ +void lacp_show(void) __banked; void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked; -/* - * Per-LAG LACP: each of the 4 hardware trunk groups (0-3) can independently run - * LACP on an admin-assigned set of candidate ports, forming its own aggregator - * (its own elected partner System). A port participates in LACP only if it has - * been assigned to a LACP-mode LAG (lacp_port_lag[port] != LACP_LAG_NONE). - * Ports of the same LAG that converge with a consistent partner join that LAG's - * hardware trunk. This is the port-channel model: `lag lacp `. - */ #define LACP_NUM_LAGS 4 -#define LACP_LAG_NONE 0xff /* port not assigned to any LACP LAG */ +#define LACP_LAG_NONE 0xff -/* Protocol state, exposed read-only for the web UI (page_impl.c send_lacp()) - * and the serial console. Owned by rtl837x_lacp.c / rtlplayground.c. */ extern __xdata uint8_t lacpEnabled; extern __xdata uint8_t lacp_actor_state[10]; extern __xdata uint8_t lacp_partner_state[10]; extern __xdata uint8_t lacp_rx_state[10]; extern __xdata uint8_t lacp_partner_sys[10][6]; extern __xdata uint16_t lacp_rx_count[10]; -extern __xdata uint8_t lacp_port_lag[10]; /* LAG (0-3) a port runs LACP on, or LACP_LAG_NONE */ -extern __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; /* admin candidate-port mask per LACP LAG (0 = LAG not LACP) */ -extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; /* elected partner System per LAG */ -extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; /* aggregator elected for this LAG */ -extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS];/* trunk members we last programmed per LAG */ +extern __xdata uint8_t lacp_port_lag[10]; +extern __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; +extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; +extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS]; -/* Slow-Protocols / LACPDU identifiers (802.3ad 43.4) */ #define SLOW_PROTO_ETHERTYPE 0x8809 #define SLOW_PROTO_SUBTYPE_LACP 0x01 #define LACP_VERSION 0x01 -#define LACP_DST5 0x02 /* 01:80:C2:00:00:02 last octet */ +#define LACP_DST5 0x02 -/* LACPDU TLV framing (802.3ad 43.4.2): type bytes + fixed TLV lengths. */ #define LACP_TLV_ACTOR 0x01 #define LACP_TLV_PARTNER 0x02 #define LACP_TLV_COLLECTOR 0x03 #define LACP_TLV_TERMINATOR 0x00 -#define LACP_TLV_LEN_INFO 0x14 /* Actor/Partner Information TLV length */ -#define LACP_TLV_LEN_COLLECTOR 0x10 /* Collector TLV length */ +#define LACP_TLV_LEN_INFO 0x14 +#define LACP_TLV_LEN_COLLECTOR 0x10 -/* Our aggregation identity: System priority is a constant (Linux 802.3ad - * defaults to the same 0xffff); the default Actor port priority likewise. */ #define LACP_SYS_PRIO 0xffff #define LACP_DEF_PORT_PRIO 0x00ff -/* Actor_State / Partner_State flag bits (802.3ad 43.4.2) */ -#define LACP_STATE_ACTIVITY 0x01 /* 1 = Active LACP (send periodically) */ -#define LACP_STATE_TIMEOUT 0x02 /* 1 = Short (fast) timeout */ -#define LACP_STATE_AGGREGATION 0x04 /* 1 = link is aggregatable */ -#define LACP_STATE_SYNC 0x08 /* 1 = in sync with partner */ +#define LACP_STATE_ACTIVITY 0x01 /* active LACP, sends periodically */ +#define LACP_STATE_TIMEOUT 0x02 /* short timeout requested */ +#define LACP_STATE_AGGREGATION 0x04 /* link can be aggregated */ +#define LACP_STATE_SYNC 0x08 /* in sync with the partner */ #define LACP_STATE_COLLECTING 0x10 #define LACP_STATE_DISTRIBUTING 0x20 -#define LACP_STATE_DEFAULTED 0x40 /* partner info is administrative default*/ -#define LACP_STATE_EXPIRED 0x80 /* wire-format doc; unused by the simplified RX machine */ +#define LACP_STATE_DEFAULTED 0x40 /* partner information is the default */ +#define LACP_STATE_EXPIRED 0x80 -/* Fully-participating actor: in sync AND collecting AND distributing. */ #define LACP_STATE_FULL (LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING) -/* - * Timer units: one step of the LACP timers, taken every four system ticks, - * 50 per second. A long pass of the main loop is caught up on a few steps at - * a time, so a period is a range and the worst case is what matters. - * - * The transmit periods are not free to choose. Our own timeouts only decide - * how patient we are, but our TX period decides when the PARTNER gives up on - * us: 802.3ad pairs fast periodic 1 s with a 3 s short timeout, and slow - * periodic 30 s with a 90 s long timeout. A period whose worst case reaches - * the partner's threshold makes it expire us intermittently, which reads as - * an aggregate that works but never stops churning. */ -#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ -#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ -#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ +#define LACP_FAST_PERIODIC 50 +#define LACP_SLOW_PERIODIC 1500 +#define LACP_SHORT_TIMEOUT 200 -/* Per-port LACP receive-machine state (802.3ad 43.4.12) */ #define LACP_RX_INITIALIZE 0 -#define LACP_RX_PORT_DISABLED 1 /* numbering reserved (exported via /lacp.json "rs") */ -#define LACP_RX_EXPIRED 2 /* numbering reserved (exported via /lacp.json "rs") */ +#define LACP_RX_PORT_DISABLED 1 +#define LACP_RX_EXPIRED 2 #define LACP_RX_DEFAULTED 3 #define LACP_RX_CURRENT 4 diff --git a/rtl837x_regs.h b/rtl837x_regs.h index 1eb865a4..dc4b2dc1 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -284,10 +284,6 @@ * Loop detection / STP */ #define RTL8373_RLDP_TIMER 0x1074 -/* Per reserved-multicast-address action. The registers are packed and - * consecutive by ENTRY rather than by the byte of the address: entry 0 is - * 01:80:C2:00:00:00 at 0x4ecc and entry 2 is ...:02 (Slow Protocols / LACP) - * at 0x4ed4, but entry 5 is ...:08, so the step of 4 counts entries. */ #define RTL837X_RMA0_CONF 0x4ecc #define RTL837X_RMA2_CONF 0x4ed4 #define RTL837X_RMA_ACT_MASK 0x30 diff --git a/rtlplayground.c b/rtlplayground.c index 38e322e7..4a8b4ec7 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -1062,14 +1062,14 @@ void handle_rx(void) print_string(" MGMT-VLAN: "); print_short(management_vlan); write_char('\n'); #endif if ((stp_enabled || lacpEnabled) && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 - && uip_buf[2] == 0xc2 && uip_buf[3] == 0x00 && uip_buf[4] == 0x00) { // 01:80:C2:00:00:0x - if (stp_enabled && uip_buf[5] == 0x00) { // STP/RSTP BPDU + && uip_buf[2] == 0xc2 && uip_buf[3] == 0x00 && uip_buf[4] == 0x00) { + if (stp_enabled && uip_buf[5] == 0x00) { stp_in(); if (uip_len) { print_string("STP TX\n"); tcpip_output(); } - } else if (lacpEnabled && uip_buf[5] == 0x02) { // LACPDU (slow protocols) + } else if (lacpEnabled && uip_buf[5] == 0x02) { lacp_in(); } } else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? @@ -1296,7 +1296,6 @@ static void handle_tick(void) stp_timers(); } } - // If LACP enabled, drive its machines (own tick divider lives in the banked module) if (lacpEnabled) lacp_timers(); health_phase(HEALTH_PH_STP); @@ -1802,7 +1801,7 @@ void main(void) stp_enabled = 0; stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ lacpEnabled = 0; - lacp_init(); /* clear per-LAG state (port->LAG map = NONE) before any config replay */ + lacp_init(); nic_setup(); REG_SET(RTL837X_NIC_INT_MSK, NIC_INT_RXIE); trap_init(); From d525229b423574e6928dd1d272f7179bd4e2580a Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:05:56 +0200 Subject: [PATCH 14/20] doc: lacp: shorten, point to stp.md for the trap The trap is described with STP, so the LACP page only says that it is used. The LACP sections leave link_aggregation.md, which now points to lacp.md, and the timing section gives the values in seconds and the 802.3ad requirements without the test history. --- doc/lacp.md | 170 +++++++++++----------------------------- doc/link_aggregation.md | 4 + 2 files changed, 50 insertions(+), 124 deletions(-) diff --git a/doc/lacp.md b/doc/lacp.md index ab09db39..b116ef9a 100644 --- a/doc/lacp.md +++ b/doc/lacp.md @@ -1,79 +1,23 @@ # LACP (Link Aggregation Control Protocol, IEEE 802.3ad) -A static link aggregation group works only if both ends agree about it in -advance. Nothing checks that they do: cable a member to the wrong neighbour and -the group keeps hashing frames onto a link that goes somewhere else. LACP is the -protocol that makes the two ends agree, and keeps agreeing — it discovers which -links reach the same partner, aggregates only those, and drops a link out of the -group when the partner stops answering. +A static link aggregation group works only if both ends are configured for the +same links, and nothing checks that they are. LACP lets the two ends agree on +which links reach the same partner, aggregates only those, and takes a link out +of the group when the partner stops answering. -The switch runs LACP in software on the 8051. Frames are the Slow Protocols -group: destination `01:80:C2:00:00:02`, ethertype `0x8809`, subtype 1. The -implementation covers the parts that decide whether an aggregate forms: -the Receive machine and the Selection and Mux machines of 802.3ad clause 43, -`recordPDU` and `update_NTT`, and the mis-cabling protection that refuses to -aggregate links whose partners differ. It does not implement the Churn Detection -machines or the Marker protocol, and the Mux machine is the coupled variant, so -collecting and distributing are enabled together rather than in sequence. - -Up to `LACP_NUM_LAGS` (4) groups can be placed under LACP; the rest stay static. +The switch runs LACP in software on the 8051. LACP frames belong to the Slow +Protocols, with destination `01:80:C2:00:00:02` and ethertype `0x8809`; the +subtype of LACP frames is 1. The implementation has the Receive, Selection and +Mux machines of 802.3ad clause 43 and does not aggregate links whose partners +differ. Up to `LACP_NUM_LAGS` (4) groups can run LACP; the others stay static. ## Getting the frames to the CPU -Reserved multicast addresses are handled by the RMA block before the L2 lookup -happens. Each address has its own register and the action field sits in bits -[5:4]: 0 forward, 1 trap to CPU, 2 drop, 3 forward excluding the CPU. The -registers are packed and consecutive by *entry* rather than by address byte, so -the step of 4 counts entries, not bytes of the address: `...:00` sits at -`0x4ecc` and `...:02` at `0x4ed4`, but `...:08` sits at `0x4ee0`. - -``` -#define RTL837X_RMA0_CONF 0x4ecc /* 01:80:C2:00:00:00, entry 0 */ -#define RTL837X_RMA2_CONF 0x4ed4 /* 01:80:C2:00:00:02, Slow Protocols */ -#define RTL837X_RMA_ACT_MASK 0x30 -#define RTL837X_RMA_ACT_TRAP 0x10 -#define RTL837X_RMA_ACT_DROP 0x20 -``` - -The engine puts this address on *trap* while it runs. A trap takes the frame -out of the forwarding path and hands it to the port named in `EXT_CPU_CTRL` -(`0x6724`), whose reset value names no port at all: the destination is meant to -be an *external* CPU on a physical port, which these boards do not have. That -is why the trap first looked like a dead end - the receive counters stayed -frozen while the partner kept sending, and the ingress discard counter grew -instead. `trap_init()` points the register at the CPU port once at startup, and -the same frames then arrive. - -`FORWARD` reaches the CPU too, because the CPU port is in the forwarding -domain, but a forwarded frame is subject to the L2 lookup and on its own floods -to every other port in the VLAN. That leaks a neighbour's LACPDUs to hosts that -must not see them and, with two bonds on one switch, poisons both; a reserved -link-local group must not be forwarded at all. Containing it takes a static L2 -multicast entry with a CPU-only member mask, and because lookups are IVL on -this chip, one entry per distinct pvid over all front panel ports - a VID 0 -entry is never matched, and a port outside the LACP groups needs its pvid -covered as well, or a LACPDU arriving there misses the lookup and is flooded -through its VLAN. - -The trap needs none of that and reaches further, because it acts on the address -before the VLAN lookup and before the ingress filters. An untagged LACPDU is -delivered on a port that admits tagged frames only, where a forwarded one is -dropped by the acceptable-frame-type filter; and it is delivered on a port STP -holds in blocking, so a group converges without waiting out the forward delay. -Both were measured against forward on the same ports of an SWTGW218AS. - -A trapped frame arrives without the VLAN tag the hardware inserts into a -forwarded one, so the LACPDU body sits four bytes earlier. `TRAP_RX_BODY()` -gives the offset of the body for either form, and `lacp_in()` reads the PDU -from there. - -Neighbouring addresses in the same block still differ in other respects - -`...:00` is exempt from the ingress STP-state check and `...:02` is not, which -the trap makes moot only because it acts before that check - so measure before -carrying a conclusion about one of them across to another. - -When LACP is switched off the action goes to `DROP`, which acts before the L2 -lookup, so there is nothing to undo. +While LACP runs, the reserved multicast action for `01:80:C2:00:00:02` is +*trap*, the same mechanism STP uses for its BPDUs, see [stp.md](stp.md). A +trapped LACPDU has no VLAN tag after the RTL tag, and `TRAP_RX_BODY()` gives the +offset of the LACPDU in the received frame. When the last group leaves LACP, +the action goes back to *drop*. ## What has to match across the members @@ -92,42 +36,30 @@ mismatch should be rejected or applied to the whole group. ## Protocol timing -``` -#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ -#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ -#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ -``` +The timer values are in units of the LACP timer, which steps every four ticks +of the system clock, 50 times per second: -One unit is one step of the LACP timers, taken every four system ticks, so 50 -per second whatever the pace of the main loop; 50 units came out as 1.00 s on -the wire in all 116 intervals of a two minute capture. +| Constant | Value | Time | +|----------------------|-------|------| +| `LACP_FAST_PERIODIC` | 50 | 1 s | +| `LACP_SLOW_PERIODIC` | 1500 | 30 s | +| `LACP_SHORT_TIMEOUT` | 200 | 4 s | -The two transmit periods are worth a word, because they are the one thing here -that is not ours to choose freely. Our own timeouts only decide how patient we -are with a partner that has gone quiet, and being slow there costs nobody -anything. The transmit period is the opposite: it decides when the *partner* -gives up on us. 802.3ad pairs a 1 s fast period with a 3 s short timeout, and a -30 s slow period with a 90 s long timeout, and the ratio is the whole point. +802.3ad specifies a fast periodic time of 1 s with a short timeout of 3 s, and a +slow periodic time of 30 s with a long timeout of 90 s. The switch transmits at +the rate the partner asks for with its TIMEOUT bit. Its own timeout is always +the short one, one second above the standard, so a partner that stops sending +while its link stays up leaves the trunk after 4 s. -Getting that wrong does not break the aggregate, which is what makes it easy to -miss. An earlier version of this code transmitted every 2 to 3 s while the -partner was asking for the fast rate. The links still aggregated and still -carried traffic, but every time a gap crossed 3 s the partner's receive machine -timed us out and recovered: 13 of its 93 LACPDUs came back carrying `EXPIRED` -and reporting our SYNC as cleared. Since a received actor state is recorded -verbatim as the partner state and sent back out, the flap was visible from both -ends, and the bond's churn machines never settled. With the periods above, the -same capture shows no expiry at all and both churn states read `none`. - -Our own timeouts sit one period above the standard's 3 s and 90 s, which -absorbs a single lost LACPDU plus the tick jitter. A partner whose LACP has -stopped while its link stayed up is out of the trunk after that; anything -longer keeps hashing traffic onto a member that no longer agrees to carry it. +The Partner block of a transmitted LACPDU repeats the partner's actor +information as it was last received: system priority, system, key, port +priority, port and state. A Linux partner sets our SYNC only when this block +matches what it sent. ## LACP API ``` -void lacp_init(void) __banked; /* boot init: clear per-LAG state */ +void lacp_init(void) __banked; void lacp_off(void) __banked; void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked; void lacp_in(void) __banked; @@ -193,35 +125,25 @@ On the switch, with those two ports as the group: lag 1 lacp 7 8 ``` -`lacp show` should then give both ports an actor state of `0x3f`, which is -ACTIVITY, TIMEOUT, AGGREGATION, SYNC, COLLECTING and DISTRIBUTING together, a -partner state with SYNC in it, and the partner's system MAC. `lag show` should -list both ports as trunk members, since that reads the hardware rather than the -protocol's opinion of itself. +`lacp show` should then give both ports an actor state of `0x3f` (ACTIVITY, +TIMEOUT, AGGREGATION, SYNC, COLLECTING and DISTRIBUTING), a partner state with +SYNC set, and the partner's system MAC. `lag show` lists the trunk members as +programmed in the hardware and should show both ports. -The host's view is the one worth trusting, in `/proc/net/bonding/bond0`. Both -slaves want the **same `Aggregator ID`**; two different numbers mean the bond -has not aggregated anything and each link is sitting in its own group, which -looks healthy at a glance and is not. Both churn states should read `none`. -`monitoring` that never settles is the signature of our transmit period being -too slow for the rate the partner asked for. +On the host, `/proc/net/bonding/bond0` should show the same `Aggregator ID` for +both slaves; different IDs mean that each link is in a group of its own. Both +churn states should read `none`. -Three things are worth checking past convergence, because each of them has been -broken here at some point: +Further checks: -* `tcpdump -i enp1s0 ether proto 0x8809` on each slave should show the switch's - LACPDUs and that slave's own, and never the other slave's. A sibling leak is - what the trap prevents: the PDU never enters the forwarding path at all. -* No `illegal loopback` in the host's log. -* In a hex dump of a frame from the switch, the three reserved bytes after the - actor state should be zero. They used to carry whatever the transmit buffer - held from the previous frame. +* `tcpdump -i enp1s0 ether proto 0x8809` on each slave shows the LACPDUs of the + switch and of that slave, never those of the other slave. +* The host log has no `illegal loopback`. +* The three reserved bytes after the actor state in a LACPDU from the switch + are zero. -Pulling one slave down and putting it back is the cheapest failure test. The -group narrows to the surviving member and comes back within tens of seconds, -and traffic across the transition loses a few percent at the edges rather than -half of everything, which is what it would lose if the ASIC kept hashing onto -the dead member. +When one slave goes down, the group narrows to the remaining member, and the +slave rejoins after it comes back. ## Known limits diff --git a/doc/link_aggregation.md b/doc/link_aggregation.md index b725e419..90fc7637 100644 --- a/doc/link_aggregation.md +++ b/doc/link_aggregation.md @@ -227,3 +227,7 @@ 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 + +Dynamic aggregation with LACP is described in [lacp.md](lacp.md). From 2481810906d135972c7a1935eee74bfd2c5e6b1c Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:55:11 +0200 Subject: [PATCH 15/20] lacp: tidy up after review Move port_bit() to rtl837x_common.h as PORT_BIT(), a uint16_t shift, and MAC_ADDR_LEN from rtl837x_lldp.h next to it, and use MAC_ADDR_LEN for every address in the LACP code instead of a literal 6. Define lacp_mux_update() before its first caller and drop the forward declaration, declare variables at the top of their blocks, put the operator at the end of a continued line, drop the field value comments of the LACPDU struct, and leave lacp_port_lag to lacp_init() instead of initialising it in the test environment. --- httpd/page_impl.c | 4 +- rtl837x_common.h | 2 + rtl837x_lacp.c | 121 +++++++++++++++++++++++----------------------- rtl837x_lacp.h | 5 +- rtl837x_lldp.h | 1 - test/env_tables.c | 9 ++-- test/test_lacp.c | 36 +++++++------- 7 files changed, 88 insertions(+), 90 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index e1f43f6e..183510d0 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -752,7 +752,7 @@ void send_lacp(void) slen += strtox(outbuf + slen, "\",\"aggValid\":"); bool_to_html(lacp_agg_valid[l]); slen += strtox(outbuf + slen, ",\"agg\":\""); - for (uint8_t j = 0; j < 6; j++) + for (uint8_t j = 0; j < MAC_ADDR_LEN; j++) byte_to_html(lacp_agg_sys[l][j]); slen += strtox(outbuf + slen, "\",\"members\":\""); byte_to_html(lacp_members_last[l] >> 8); @@ -776,7 +776,7 @@ void send_lacp(void) byte_to_html(lacp_rx_count[i] >> 8); byte_to_html(lacp_rx_count[i]); slen += strtox(outbuf + slen, "\",\"psys\":\""); - for (uint8_t j = 0; j < 6; j++) + for (uint8_t j = 0; j < MAC_ADDR_LEN; j++) byte_to_html(lacp_partner_sys[i][j]); slen += strtox(outbuf + slen, "\"},"); } diff --git a/rtl837x_common.h b/rtl837x_common.h index a36d41fc..a9747a18 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -16,6 +16,8 @@ #define TICKS_DUE(last, period) ((uint8_t)((uint8_t)ticks - (last)) >= (period)) #define CPU_PORT 9 +#define MAC_ADDR_LEN 6 +#define PORT_BIT(p) ((uint16_t)1 << (p)) #define NUL '\0' // Define Port-masks for 9-port devices and 6-port devices diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c index 5a14e3b4..c1bf4dc2 100644 --- a/rtl837x_lacp.c +++ b/rtl837x_lacp.c @@ -26,7 +26,7 @@ extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; /* 20-byte Actor/Partner information body (802.3ad 43.4.2.2), network byte order */ struct lacp_info { uint16_t sys_prio; - uint8_t sys[6]; + uint8_t sys[MAC_ADDR_LEN]; uint16_t key; uint16_t port_prio; uint16_t port; @@ -36,32 +36,32 @@ struct lacp_info { /* Outgoing LACPDU (no VLAN tag on TX, as in stp_pkt) */ struct lacpdu { - uint8_t dst[6]; - uint8_t src[6]; + uint8_t dst[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; struct rtl_tag rtl_tag; - uint16_t ethertype; /* 0x8809 */ - uint8_t subtype; /* 0x01 */ - uint8_t version; /* 0x01 */ - uint8_t tlv_actor; /* 0x01 */ - uint8_t actor_len; /* 0x14 */ + uint16_t ethertype; + uint8_t subtype; + uint8_t version; + uint8_t tlv_actor; + uint8_t actor_len; struct lacp_info actor; - uint8_t tlv_partner; /* 0x02 */ - uint8_t partner_len; /* 0x14 */ + uint8_t tlv_partner; + uint8_t partner_len; struct lacp_info partner; - uint8_t tlv_collector; /* 0x03 */ - uint8_t collector_len; /* 0x10 */ + uint8_t tlv_collector; + uint8_t collector_len; uint16_t collector_max_delay; uint8_t collector_reserved[12]; - uint8_t tlv_terminator; /* 0x00 */ - uint8_t terminator_len; /* 0x00 */ + uint8_t tlv_terminator; + uint8_t terminator_len; uint8_t terminator_reserved[50]; }; /* Incoming LACPDU, split because a VLAN tag sits between the two only when the * frame was forwarded: a trapped one carries none (cf. stp_pkt_in) */ struct lacpdu_in_hdr { - uint8_t dst[6]; - uint8_t src[6]; + uint8_t dst[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; struct rtl_tag rtl_tag; }; @@ -86,7 +86,7 @@ __xdata uint8_t lacp_rxbody; __xdata uint8_t lacp_actor_state[10]; __xdata uint8_t lacp_rx_state[10]; __xdata uint8_t lacp_partner_state[10]; -__xdata uint8_t lacp_partner_sys[10][6]; +__xdata uint8_t lacp_partner_sys[10][MAC_ADDR_LEN]; __xdata uint16_t lacp_partner_key[10]; __xdata uint16_t lacp_partner_port[10]; __xdata uint16_t lacp_partner_sys_prio[10]; @@ -102,7 +102,7 @@ extern volatile __xdata uint32_t ticks; __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; __xdata uint8_t lacp_port_lag[10]; -__xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +__xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][MAC_ADDR_LEN]; __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; __xdata uint16_t lacp_members_last[LACP_NUM_LAGS]; @@ -113,11 +113,9 @@ __xdata uint8_t lacp_scratch_lag; __xdata uint16_t lacp_rx_count[10]; -void lacp_mux_update(void) __banked; - static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b) { - for (uint8_t i = 0; i < 6; i++) { + for (uint8_t i = 0; i < MAC_ADDR_LEN; i++) { if (a[i] != b[i]) return 0; } @@ -161,21 +159,41 @@ static uint8_t lacp_mux_machine(uint8_t port) return 1; } -#define port_bit(p) (((uint8_t)1) << (p)) +void lacp_mux_update(void) __banked +{ + for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + if (!lacp_lag_ports[lag]) + continue; + lacp_scratch_mask = 0; + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] != lag) + continue; + if ((lacp_actor_state[i] & LACP_STATE_FULL) == LACP_STATE_FULL + && (lacp_partner_state[i] & LACP_STATE_SYNC)) + lacp_scratch_mask |= PORT_BIT(i); + } + if (lacp_scratch_mask != lacp_members_last[lag]) { + lacp_members_last[lag] = lacp_scratch_mask; + port_lag_members_set(lag, lacp_scratch_mask); + } + } +} void lacp_send(uint8_t port) __banked { + uint16_t saved_mgmt_vlan; + memset((__xdata uint8_t *)LACP_O, 0, sizeof(struct lacpdu)); LACP_O->dst[0] = 0x01; LACP_O->dst[1] = 0x80; LACP_O->dst[2] = 0xc2; LACP_O->dst[3] = 0x00; LACP_O->dst[4] = 0x00; LACP_O->dst[5] = LACP_DST5; - memcpy(LACP_O->src, uip_ethaddr.addr, 6); + memcpy(LACP_O->src, uip_ethaddr.addr, MAC_ADDR_LEN); LACP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); LACP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; LACP_O->rtl_tag.reason = 0x00; LACP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS | RTL_TAG_KEEP); - LACP_O->rtl_tag.pmask = HTONS(port_bit(port)); + LACP_O->rtl_tag.pmask = HTONS(PORT_BIT(port)); LACP_O->ethertype = HTONS(SLOW_PROTO_ETHERTYPE); LACP_O->subtype = SLOW_PROTO_SUBTYPE_LACP; @@ -184,7 +202,7 @@ void lacp_send(uint8_t port) __banked LACP_O->tlv_actor = LACP_TLV_ACTOR; LACP_O->actor_len = LACP_TLV_LEN_INFO; LACP_O->actor.sys_prio = HTONS(LACP_SYS_PRIO); - memcpy(LACP_O->actor.sys, uip_ethaddr.addr, 6); + memcpy(LACP_O->actor.sys, uip_ethaddr.addr, MAC_ADDR_LEN); LACP_O->actor.key = HTONS((uint16_t)(lacp_port_lag[port] + 1)); LACP_O->actor.port_prio = HTONS(LACP_DEF_PORT_PRIO); LACP_O->actor.port = HTONS((uint16_t)port + 1); @@ -193,7 +211,7 @@ void lacp_send(uint8_t port) __banked LACP_O->tlv_partner = LACP_TLV_PARTNER; LACP_O->partner_len = LACP_TLV_LEN_INFO; LACP_O->partner.sys_prio = HTONS(lacp_partner_sys_prio[port]); - memcpy(LACP_O->partner.sys, lacp_partner_sys[port], 6); + memcpy(LACP_O->partner.sys, lacp_partner_sys[port], MAC_ADDR_LEN); LACP_O->partner.key = HTONS(lacp_partner_key[port]); LACP_O->partner.port_prio = HTONS(lacp_partner_port_prio[port]); LACP_O->partner.port = HTONS(lacp_partner_port[port]); @@ -206,7 +224,7 @@ void lacp_send(uint8_t port) __banked LACP_O->terminator_len = 0x00; lacp_ntt[port] = 0; - uint16_t saved_mgmt_vlan = management_vlan; + saved_mgmt_vlan = management_vlan; management_vlan = 0; uip_len = sizeof(struct lacpdu); tcpip_output(); @@ -247,7 +265,7 @@ void lacp_in(void) __banked lacp_rx_count[port]++; - memcpy(lacp_partner_sys[port], LACP_I->actor.sys, 6); + memcpy(lacp_partner_sys[port], LACP_I->actor.sys, MAC_ADDR_LEN); lacp_scratch_mask = HTONS(LACP_I->actor.key); lacp_partner_key[port] = lacp_scratch_mask; lacp_scratch_mask = HTONS(LACP_I->actor.port); @@ -262,7 +280,7 @@ void lacp_in(void) __banked lacp_timeout[port] = LACP_SHORT_TIMEOUT; if (!lacp_agg_valid[lag]) { - memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, 6); + memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, MAC_ADDR_LEN); lacp_agg_valid[lag] = 1; } @@ -277,26 +295,6 @@ void lacp_in(void) __banked } -void lacp_mux_update(void) __banked -{ - for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { - if (!lacp_lag_ports[lag]) - continue; - lacp_scratch_mask = 0; - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - if (lacp_port_lag[i] != lag) - continue; - if ((lacp_actor_state[i] & LACP_STATE_FULL) == LACP_STATE_FULL - && (lacp_partner_state[i] & LACP_STATE_SYNC)) - lacp_scratch_mask |= port_bit(i); - } - if (lacp_scratch_mask != lacp_members_last[lag]) { - lacp_members_last[lag] = lacp_scratch_mask; - port_lag_members_set(lag, lacp_scratch_mask); - } - } -} - static void lacp_step(void) { @@ -307,8 +305,8 @@ static void lacp_step(void) if (lacp_periodic[i]) { lacp_periodic[i]--; } else { - lacp_periodic[i] = (lacp_partner_state[i] & LACP_STATE_TIMEOUT) - ? LACP_FAST_PERIODIC : LACP_SLOW_PERIODIC; + lacp_periodic[i] = (lacp_partner_state[i] & LACP_STATE_TIMEOUT) ? + LACP_FAST_PERIODIC : LACP_SLOW_PERIODIC; lacp_ntt[i] = 1; } @@ -326,9 +324,10 @@ static void lacp_step(void) } for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + uint8_t any_current = 0; + if (!lacp_agg_valid[lag]) continue; - uint8_t any_current = 0; for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { if (lacp_port_lag[i] == lag && lacp_rx_state[i] == LACP_RX_CURRENT && lacp_sys_eq(lacp_partner_sys[i], lacp_agg_sys[lag])) { @@ -356,11 +355,11 @@ void lacp_timers(void) __banked static void lacp_port_init(uint8_t port) { - lacp_actor_state[port] = LACP_STATE_ACTIVITY | LACP_STATE_AGGREGATION - | LACP_STATE_TIMEOUT; + lacp_actor_state[port] = LACP_STATE_ACTIVITY | LACP_STATE_AGGREGATION | + LACP_STATE_TIMEOUT; lacp_rx_state[port] = LACP_RX_INITIALIZE; lacp_partner_state[port] = LACP_STATE_DEFAULTED; - memset(lacp_partner_sys[port], 0, 6); + memset(lacp_partner_sys[port], 0, MAC_ADDR_LEN); lacp_partner_key[port] = 0; lacp_partner_port[port] = 0; lacp_periodic[port] = LACP_FAST_PERIODIC; @@ -376,7 +375,7 @@ static void lacp_port_release(uint8_t port) lacp_partner_state[port] = 0; lacp_rx_state[port] = LACP_RX_INITIALIZE; lacp_rx_count[port] = 0; - memset(lacp_partner_sys[port], 0, 6); + memset(lacp_partner_sys[port], 0, MAC_ADDR_LEN); } static uint8_t lacp_any_lag(void) @@ -424,7 +423,7 @@ void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked lacp_scratch_flag = lacp_any_lag(); for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - if (lacp_port_lag[i] == lag && !(ports & port_bit(i))) { + if (lacp_port_lag[i] == lag && !(ports & PORT_BIT(i))) { lacp_port_lag[i] = LACP_LAG_NONE; lacp_port_release(i); } @@ -440,12 +439,12 @@ void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked lacp_engine_on(); for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - if (!(ports & port_bit(i))) + if (!(ports & PORT_BIT(i))) continue; lacp_scratch_lag = lacp_port_lag[i]; lacp_port_lag[i] = lag; if (lacp_scratch_lag != LACP_LAG_NONE && lacp_scratch_lag != lag) { - lacp_scratch_mask = lacp_lag_ports[lacp_scratch_lag] & ~port_bit(i); + lacp_scratch_mask = lacp_lag_ports[lacp_scratch_lag] & ~PORT_BIT(i); lacp_lag_ports[lacp_scratch_lag] = lacp_scratch_mask; if (!lacp_scratch_mask) { lacp_agg_valid[lacp_scratch_lag] = 0; @@ -483,7 +482,7 @@ void lacp_show(void) __banked print_string(" ports "); print_short(lacp_lag_ports[l]); print_string(" aggregator "); if (lacp_agg_valid[l]) { - for (uint8_t j = 0; j < 6; j++) + for (uint8_t j = 0; j < MAC_ADDR_LEN; j++) print_byte(lacp_agg_sys[l][j]); } else { print_string("(none)"); @@ -498,7 +497,7 @@ void lacp_show(void) __banked print_string(" rxst "); print_byte(lacp_rx_state[i]); print_string(" rx "); print_short(lacp_rx_count[i]); print_string(" psys "); - for (uint8_t j = 0; j < 6; j++) + for (uint8_t j = 0; j < MAC_ADDR_LEN; j++) print_byte(lacp_partner_sys[i][j]); write_char('\n'); } diff --git a/rtl837x_lacp.h b/rtl837x_lacp.h index 44e3ad4b..a03c4c69 100644 --- a/rtl837x_lacp.h +++ b/rtl837x_lacp.h @@ -7,6 +7,7 @@ */ #include +#include "rtl837x_common.h" void lacp_init(void) __banked; void lacp_in(void) __banked; @@ -22,11 +23,11 @@ extern __xdata uint8_t lacpEnabled; extern __xdata uint8_t lacp_actor_state[10]; extern __xdata uint8_t lacp_partner_state[10]; extern __xdata uint8_t lacp_rx_state[10]; -extern __xdata uint8_t lacp_partner_sys[10][6]; +extern __xdata uint8_t lacp_partner_sys[10][MAC_ADDR_LEN]; extern __xdata uint16_t lacp_rx_count[10]; extern __xdata uint8_t lacp_port_lag[10]; extern __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; -extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][MAC_ADDR_LEN]; extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS]; diff --git a/rtl837x_lldp.h b/rtl837x_lldp.h index 298775c6..0e6ee674 100644 --- a/rtl837x_lldp.h +++ b/rtl837x_lldp.h @@ -7,7 +7,6 @@ #define LLDP_ETHERTYPE 0x88cc #define LLDP_ETHERTYPE_LENGTH 2 #define LLDP_MAX_FRAME 256 -#define MAC_ADDR_LEN 6 #define LLDP_TX_INTERVAL_TICKS 6000 #define LLDP_PORT_ID_TLV_TYPE 2 diff --git a/test/env_tables.c b/test/env_tables.c index 6a841a19..719e045a 100644 --- a/test/env_tables.c +++ b/test/env_tables.c @@ -130,13 +130,10 @@ 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]; +uint8_t lacp_partner_sys[10][MAC_ADDR_LEN]; 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, -}; +uint8_t lacp_port_lag[10]; uint16_t lacp_lag_ports[LACP_NUM_LAGS]; -uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +uint8_t lacp_agg_sys[LACP_NUM_LAGS][MAC_ADDR_LEN]; uint8_t lacp_agg_valid[LACP_NUM_LAGS]; uint16_t lacp_members_last[LACP_NUM_LAGS]; diff --git a/test/test_lacp.c b/test/test_lacp.c index f0ab5f3f..dbadea04 100644 --- a/test/test_lacp.c +++ b/test/test_lacp.c @@ -56,7 +56,7 @@ void trap_rma_set(uint16_t reg, uint8_t act) struct wire_info { uint16_t sys_prio; - uint8_t sys[6]; + uint8_t sys[MAC_ADDR_LEN]; uint16_t key; uint16_t port_prio; uint16_t port; @@ -65,8 +65,8 @@ struct wire_info { }; struct wire_pdu_out { /* what lacp_send() writes at uip_buf+12 */ - uint8_t dst[6]; - uint8_t src[6]; + uint8_t dst[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; struct rtl_tag rtl_tag; uint16_t ethertype; uint8_t subtype; @@ -83,8 +83,8 @@ struct wire_pdu_out { /* what lacp_send() writes at uip_buf+12 */ }; struct wire_pdu_in { /* forwarded form: the hardware inserts a VLAN tag */ - uint8_t dst[6]; - uint8_t src[6]; + uint8_t dst[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; struct rtl_tag rtl_tag; struct vlan_tag vlan_tag; uint16_t ethertype; @@ -123,14 +123,14 @@ void tcpip_output(void) /* 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, +static void partner_frame_full(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], 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); + memcpy(in.src, sys, MAC_ADDR_LEN); 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 */ @@ -140,7 +140,7 @@ static void partner_frame_full(uint8_t port, const uint8_t sys[6], uint8_t pstat in.tlv_actor = 0x01; in.actor_len = 0x14; in.actor.sys_prio = HTONS(0x8000); - memcpy(in.actor.sys, sys, 6); + memcpy(in.actor.sys, sys, MAC_ADDR_LEN); in.actor.key = HTONS(0x0011); in.actor.port_prio = HTONS(0x00ff); in.actor.port = HTONS((uint16_t)port + 101); @@ -149,7 +149,7 @@ static void partner_frame_full(uint8_t port, const uint8_t sys[6], uint8_t pstat 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); + memcpy(in.partner.sys, last_tx[port].actor.sys, MAC_ADDR_LEN); in.partner.key = last_tx[port].actor.key; in.partner.port = last_tx[port].actor.port; in.partner.state = last_tx[port].actor.state; @@ -166,18 +166,18 @@ static void partner_frame_full(uint8_t port, const uint8_t sys[6], uint8_t pstat lacp_in(); } -static void partner_frame_len(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo, uint16_t len) +static void partner_frame_len(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], 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) +static void partner_frame(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], 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) +static void partner_frame_trap(uint8_t port, const uint8_t sys[MAC_ADDR_LEN], uint8_t pstate, int echo) { partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 0); } @@ -201,8 +201,8 @@ static int failures; #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}; +static const uint8_t SYS_A[MAC_ADDR_LEN] = {0x02,0xaa,0xaa,0xaa,0xaa,0x01}; +static const uint8_t SYS_B[MAC_ADDR_LEN] = {0x02,0xbb,0xbb,0xbb,0xbb,0x02}; int main(int argc, char **argv) { @@ -342,11 +342,11 @@ int main(int argc, char **argv) * 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); + memset(lacp_partner_sys[0], 0, MAC_ADDR_LEN); 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); + && !memcmp(lacp_partner_sys[0], SYS_A, MAC_ADDR_LEN); 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"); @@ -376,7 +376,7 @@ int main(int argc, char **argv) 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), + CHECK(stayed_out && hw_members == 0x0003 && !memcmp(lacp_agg_sys[0], SYS_B, MAC_ADDR_LEN), "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 @@ -420,7 +420,7 @@ int main(int argc, char **argv) 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), + && lacp_rx_count[4] == 0 && !memcmp(lacp_partner_sys[4], "\0\0\0\0\0\0", MAC_ADDR_LEN), "T20 release: partner state, rx state, counter and system cleared"); /* T21: applying the same candidate set again (the LAG page resends it From e125ebd097ad91e849313584ffd93db643ad7545 Mon Sep 17 00:00:00 2001 From: Brunoz78 Date: Thu, 8 Oct 2026 11:03:45 +0200 Subject: [PATCH 16/20] i18n(de): translate the port isolation page Co-Authored-By: Claude Opus 5.5 --- html/app.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/html/app.js b/html/app.js index 34e9713e..aa90524e 100644 --- a/html/app.js +++ b/html/app.js @@ -588,7 +588,7 @@ fw_timeout:"Le commutateur n'est plus joignable depuis 150 s: verifiez l'aliment }, de:{ nav_dash:"Übersicht",nav_ports:"Ports",nav_stp:"Spanning Tree",nav_stats:"Statistik", -nav_vlan:"VLANs",nav_l2:"MAC-Tabelle",nav_mirror:"Spiegelung",nav_lag:"LAG",nav_eee:"EEE", +nav_vlan:"VLANs",nav_l2:"MAC-Tabelle",nav_mirror:"Spiegelung",nav_isolate:"Isolation",nav_lag:"LAG",nav_eee:"EEE", nav_bw:"Bandbreite",nav_system:"System",nav_fw:"Firmware", hdr_dirty:"ungespeicherte Änderungen",hdr_dirty_t:"Laufende Konfiguration weicht von der Startkonfiguration ab", hdr_save:"Im Flash speichern",hdr_save_t:"Laufende Konfiguration dauerhaft im Flash speichern", @@ -648,6 +648,9 @@ l2_learned:"gelernt",l2_loading:"lädt...",l2_failed:"Laden fehlgeschlagen",l2_e l2_del_t:"Eintrag löschen",l2_flush_q:"Alle gelernten MAC-Einträge löschen?", m_title:"Port-Spiegelung",m_active:"aktiv",m_monitor:"Monitor-Port",m_mirror:"Spiegeln",m_both:"Beide", m_note:"Beide = RX und TX des Ports auf den Monitor-Port spiegeln.",m_none:"Mindestens einen zu spiegelnden Port wählen", +iso_title:"Port-Isolation",iso_from:"Von Port",iso_to:"Zu Port",iso_all:"Alle erlauben", +iso_intro:"Ein Paket, das über einen Port hereinkommt, darf den Switch nur über die Ports verlassen, die in seiner Zeile angehakt sind. Die Isolation wirkt vor der Weiterleitung: Ein nicht angehakter Port ist direkt nicht erreichbar, egal was in der MAC-Tabelle steht.", +iso_legend:"Das Feld des Ports selbst ist immer erlaubt. Eine rote Portnummer markiert einen Port, der nicht ganz offen ist.", lag_hash:"Hash:",lag_note:"Eine LAG braucht mindestens ein Mitglied, um in der Startkonfiguration gespeichert zu werden; eine leere Gruppe zu übernehmen löscht sie.", lag_clear_q:"LAG {n} auflösen?",lag_clear_d:"Alle Mitglieds-Ports kehren in den Normalbetrieb zurück.", e_title:"Energy Efficient Ethernet",e_adv:"Angeboten",e_lp:"Gegenstelle",e_active:"Aktiv",e_enable:"Aktivieren", From 3a8d1d323f786669054084b7adcd1348be2a53eb Mon Sep 17 00:00:00 2001 From: Brunoz78 Date: Sun, 11 Oct 2026 16:43:18 +0200 Subject: [PATCH 17/20] i18n(de): translate the DNS server settings Co-Authored-By: Claude Opus 5.5 --- html/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/app.js b/html/app.js index aa90524e..129ce631 100644 --- a/html/app.js +++ b/html/app.js @@ -675,7 +675,7 @@ sy_ip_err:"Ungültige IP / Netzmaske / Gateway",sy_host_err:"Hostname: 1-23 druc sy_net_q:"Netzwerkeinstellungen übernehmen?",sy_net_d:"Die Management-IP ändert sich auf {ip}: Diese Seite muss dort neu geöffnet werden.", sy_ip_changed:"IP geändert, neu verbinden unter http://{ip}/",sy_dhcp_q:"Auf DHCP umstellen?", sy_dhcp_d:"Der Switch bezieht seine Adresse per DHCP. Du musst seine neue IP herausfinden, um dich wieder zu verbinden.", -sy_sysip_err:"Ungültige IP des Syslog-Servers",sy_sysport_err:"Syslog-Port muss zwischen 1 und 65535 liegen", +sy_sysip_err:"Ungültige IP des Syslog-Servers",sy_dns1:"DNS-Server",sy_dns2:"Zweiter DNS-Server",sy_dns_err:"Ungültige IP des DNS-Servers",sy_dns_dhcp:"von DHCP: {ip}",sy_sysport_err:"Syslog-Port muss zwischen 1 und 65535 liegen", sy_pw_len:"Passwort: 1-20 Zeichen",sy_pw_space:"Das Passwort darf keine Leerzeichen enthalten",sy_pw_match:"Die Passwörter stimmen nicht überein", sy_pw_q:"Admin-Passwort ändern?",sy_pw_d:"Gilt sofort für neue Anmeldungen. Im Flash speichern, damit es erhalten bleibt.", sy_reboot_q:"Switch neu starten?",sy_reboot_d:"Es gibt UNGESPEICHERTE Änderungen, sie gehen verloren. Zuerst im Flash speichern, wenn du sie behalten willst.", From ecc8356f4d0e983321cbc76b74ab4c8620d00166 Mon Sep 17 00:00:00 2001 From: Brunoz78 Date: Mon, 12 Oct 2026 21:47:21 +0200 Subject: [PATCH 18/20] i18n(de): translate the NTP settings Co-Authored-By: Claude Opus 5.5 --- html/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/html/app.js b/html/app.js index 129ce631..482b3773 100644 --- a/html/app.js +++ b/html/app.js @@ -676,6 +676,7 @@ sy_net_q:"Netzwerkeinstellungen übernehmen?",sy_net_d:"Die Management-IP änder sy_ip_changed:"IP geändert, neu verbinden unter http://{ip}/",sy_dhcp_q:"Auf DHCP umstellen?", sy_dhcp_d:"Der Switch bezieht seine Adresse per DHCP. Du musst seine neue IP herausfinden, um dich wieder zu verbinden.", sy_sysip_err:"Ungültige IP des Syslog-Servers",sy_dns1:"DNS-Server",sy_dns2:"Zweiter DNS-Server",sy_dns_err:"Ungültige IP des DNS-Servers",sy_dns_dhcp:"von DHCP: {ip}",sy_sysport_err:"Syslog-Port muss zwischen 1 und 65535 liegen", +sy_time:"Uhrzeit",sy_ntp:"NTP",sy_ntpsrv:"NTP-Server",sy_ntpsrv_p:"Hostname oder IPv4-Adresse",sy_ntpint:"Abfrageintervall [min]",sy_tz:"Zeitzone",sy_dst:"Sommerzeit",sy_dst_eu:"Europa",sy_dst_us:"USA",sy_now:"Lokale Zeit",sy_sync:"Letzte Synchronisation",sy_ntpaddr:"Server-Adresse",sy_nosync:"nicht synchronisiert",sy_waiting:"warte auf den Server",sy_ago:"vor {n} min",sy_ntp_err:"Hostname oder IPv4-Adresse des NTP-Servers eingeben",sy_ntpint_err:"Abfrageintervall muss zwischen 1 und 1440 liegen",sy_time_note:"Der Switch hat keine batteriegepufferte Uhr: Die Zeit geht bei jedem Neustart verloren und wird neu vom NTP-Server bezogen, sobald das Netzwerk bereit ist. Im Flash speichern, damit die Einstellungen erhalten bleiben.", sy_pw_len:"Passwort: 1-20 Zeichen",sy_pw_space:"Das Passwort darf keine Leerzeichen enthalten",sy_pw_match:"Die Passwörter stimmen nicht überein", sy_pw_q:"Admin-Passwort ändern?",sy_pw_d:"Gilt sofort für neue Anmeldungen. Im Flash speichern, damit es erhalten bleibt.", sy_reboot_q:"Switch neu starten?",sy_reboot_d:"Es gibt UNGESPEICHERTE Änderungen, sie gehen verloren. Zuerst im Flash speichern, wenn du sie behalten willst.", From 682ac462c42bca41c721e24f1d9526ac90b758f6 Mon Sep 17 00:00:00 2001 From: Brunoz78 Date: Sat, 10 Oct 2026 11:40:17 +0200 Subject: [PATCH 19/20] i18n(de): translate the LLDP page Co-Authored-By: Claude Opus 5.5 --- html/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/html/app.js b/html/app.js index 482b3773..c3458117 100644 --- a/html/app.js +++ b/html/app.js @@ -626,6 +626,7 @@ stp_en_q:"Spanning Tree aktivieren?", stp_en_d:"Die Ports starten blockiert und brauchen bis zum doppelten Forward Delay, bis sie weiterleiten; Edge-Ports sind sofort wieder aktiv.", stp_dis_q:"Spanning Tree deaktivieren?",stp_dis_d:"Alle Ports leiten sofort weiter; der Schutz vor Netzwerkschleifen entfällt.", stp_cost_err:"Pfadkosten müssen zwischen 0 und 200000000 liegen", +nav_lldp:"LLDP",lldp_enabled:"LLDP aktiv",lldp_title:"Dienststatus",lldp_portname:"Name",lldp_enable:"Pakete senden", st_title:"Port-Statistik",st_h:"Summen seit dem Start",st_txg:"TX gut",st_txb:"TX fehlerhaft",st_rxg:"RX gut", st_rxb:"RX fehlerhaft",st_details:"Details",st_counters:"MIB-Zähler",st_nonzero:"nur ungleich null", st_autoref:"automatisch aktualisieren",st_counter:"Zähler",st_value:"Wert",st_fail:"Zähler konnten nicht geladen werden", From d5f84cc2fba1328c6fa1b2310e8ebb47370ff822 Mon Sep 17 00:00:00 2001 From: Brunoz78 Date: Wed, 14 Oct 2026 20:52:53 +0200 Subject: [PATCH 20/20] i18n(de): translate the LACP settings of the LAG page Co-Authored-By: Claude Opus 5.5 --- html/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/html/app.js b/html/app.js index c3458117..626e522c 100644 --- a/html/app.js +++ b/html/app.js @@ -654,6 +654,7 @@ iso_intro:"Ein Paket, das über einen Port hereinkommt, darf den Switch nur übe iso_legend:"Das Feld des Ports selbst ist immer erlaubt. Eine rote Portnummer markiert einen Port, der nicht ganz offen ist.", lag_hash:"Hash:",lag_note:"Eine LAG braucht mindestens ein Mitglied, um in der Startkonfiguration gespeichert zu werden; eine leere Gruppe zu übernehmen löscht sie.", lag_clear_q:"LAG {n} auflösen?",lag_clear_d:"Alle Mitglieds-Ports kehren in den Normalbetrieb zurück.", +lacp_heading:"LACP (802.3ad)",lag_mode:"Modus",lag_static:"Statisch",lacp_neg:"wird ausgehandelt",lacp_agg:"Aggregator",lacp_members:"aktive Mitglieder",lacp_hint:"LACP: Ports auswählen und Übernehmen drücken, um die Aushandlung zu starten",lacp_actor:"Lokal",lacp_partner:"Partner",lacp_rxstate:"Empfangsstatus",lacp_rx:"Empfangene LACPDUs",lacp_psys:"Partner-System", e_title:"Energy Efficient Ethernet",e_adv:"Angeboten",e_lp:"Gegenstelle",e_active:"Aktiv",e_enable:"Aktivieren", e_note:"Angebotene und von der Gegenstelle unterstützte Stufen je Geschwindigkeit: 100M, 1G, 2.5G. SFP-Ports unterstützen kein EEE.", e_idle:"inaktiv",e_na:"k. A.",