diff --git a/Makefile b/Makefile index 11cee779..67f3fa87 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,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 1aa2a5ad..9e226dbf 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" @@ -534,11 +535,21 @@ void parse_lag(void) goto err; if (cmd_compare(2, "d")) { + lacp_lag_set(group, 0); port_lag_members_set(group, 0); return; } 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) @@ -546,10 +557,15 @@ 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); + } else { + lacp_lag_set(group, 0); + 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"); } @@ -1974,6 +1990,13 @@ 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) diff --git a/doc/lacp.md b/doc/lacp.md new file mode 100644 index 00000000..b116ef9a --- /dev/null +++ b/doc/lacp.md @@ -0,0 +1,155 @@ +# LACP (Link Aggregation Control Protocol, IEEE 802.3ad) + +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. 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 + +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 + +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 + +The timer values are in units of the LACP timer, which steps every four ticks +of the system clock, 50 times per second: + +| Constant | Value | Time | +|----------------------|-------|------| +| `LACP_FAST_PERIODIC` | 50 | 1 s | +| `LACP_SLOW_PERIODIC` | 1500 | 30 s | +| `LACP_SHORT_TIMEOUT` | 200 | 4 s | + +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. + +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; +void lacp_off(void) __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; +``` + +`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 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. 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 leaves LACP, 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` (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. + +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`. + +Further checks: + +* `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. + +When one slave goes down, the group narrows to the remaining member, and the +slave rejoins after it comes back. + +## Known limits + +* 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/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). diff --git a/html/app.js b/html/app.js index d737810b..34e9713e 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,34 @@ 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 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("/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")+" "+maskPorts(parseInt(lg.members,16)):""; + }); + 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 +1874,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 +2281,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..183510d0 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" @@ -736,6 +737,53 @@ void send_stp(void) slen += strtox(outbuf + slen, "]}"); } +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); + 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 < 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); + 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]); + 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]); + 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 < MAC_ADDR_LEN; 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_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_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..c1bf4dc2 --- /dev/null +++ b/rtl837x_lacp.c @@ -0,0 +1,504 @@ +/* + * 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; +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]; + +/* 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[MAC_ADDR_LEN]; + 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[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; + struct rtl_tag rtl_tag; + 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; + uint8_t tlv_collector; + uint8_t collector_len; + uint16_t collector_max_delay; + uint8_t collector_reserved[12]; + 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[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; + 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; + +#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]) + +__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][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]; +__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]; + +#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; + +__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][MAC_ADDR_LEN]; +__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; + +__xdata uint16_t lacp_rx_count[10]; + + +static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b) +{ + for (uint8_t i = 0; i < MAC_ADDR_LEN; i++) { + if (a[i] != b[i]) + return 0; + } + return 1; +} + + +static uint8_t lacp_port_selected(uint8_t port) +{ + uint8_t lag = lacp_port_lag[port]; + if (lag == LACP_LAG_NONE) + 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]); +} + + +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; +} + +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, 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->ethertype = HTONS(SLOW_PROTO_ETHERTYPE); + LACP_O->subtype = SLOW_PROTO_SUBTYPE_LACP; + LACP_O->version = LACP_VERSION; + + 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, 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); + LACP_O->actor.state = lacp_actor_state[port]; + + 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], 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]); + LACP_O->partner.state = lacp_partner_state[port]; + + 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; + saved_mgmt_vlan = management_vlan; + management_vlan = 0; + uip_len = sizeof(struct lacpdu); + tcpip_output(); + management_vlan = saved_mgmt_vlan; +} + + +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; + return; + } + uip_len = 0; + + if (LACP_I->ethertype != HTONS(SLOW_PROTO_ETHERTYPE) + || LACP_I->subtype != SLOW_PROTO_SUBTYPE_LACP) + return; + + port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; + if (port < machine.min_port || port > machine.max_port) + return; + + 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 + 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]++; + + 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); + 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; + + lacp_rx_state[port] = LACP_RX_CURRENT; + lacp_timeout[port] = LACP_SHORT_TIMEOUT; + + if (!lacp_agg_valid[lag]) { + memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, MAC_ADDR_LEN); + lacp_agg_valid[lag] = 1; + } + + if (lacp_mux_machine(port)) + lacp_ntt[port] = 1; + + 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(); +} + + + +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) + continue; + + 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; + } + + 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); + } + + for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + uint8_t any_current = 0; + + if (!lacp_agg_valid[lag]) + continue; + 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(); + } +} + + +static void lacp_port_init(uint8_t port) +{ + 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, MAC_ADDR_LEN); + lacp_partner_key[port] = 0; + lacp_partner_port[port] = 0; + lacp_periodic[port] = LACP_FAST_PERIODIC; + lacp_timeout[port] = 0; + lacp_ntt[port] = 1; + lacp_rx_count[port] = 0; +} + +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, MAC_ADDR_LEN); +} + +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; +} + +static void lacp_engine_on(void) +{ + lacp_tick_last = (uint8_t)ticks; + lacpEnabled = 1; + trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_TRAP); +} + +static void lacp_engine_off(void) +{ + trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_DROP); + lacpEnabled = 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; +} + +void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked +{ + if (lag >= LACP_NUM_LAGS) + return; + if (ports && ports == lacp_lag_ports[lag]) + return; + 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))) { + lacp_port_lag[i] = LACP_LAG_NONE; + lacp_port_release(i); + } + } + 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) + lacp_engine_on(); + + 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]; + 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_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; + port_lag_members_set(lacp_scratch_lag, 0); + } + } + } + lacp_port_init(i); + } + + if (!lacp_any_lag() && lacp_scratch_flag) + lacp_engine_off(); + + lacp_mux_update(); +} + + +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); +} + + +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 < MAC_ADDR_LEN; 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 < MAC_ADDR_LEN; 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..a03c4c69 --- /dev/null +++ b/rtl837x_lacp.h @@ -0,0 +1,70 @@ +#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 +#include "rtl837x_common.h" + +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; +void lacp_lag_set(uint8_t lag, __xdata uint16_t ports) __banked; + +#define LACP_NUM_LAGS 4 +#define LACP_LAG_NONE 0xff + +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][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][MAC_ADDR_LEN]; +extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; +extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS]; + +#define SLOW_PROTO_ETHERTYPE 0x8809 +#define SLOW_PROTO_SUBTYPE_LACP 0x01 +#define LACP_VERSION 0x01 +#define LACP_DST5 0x02 + +#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 +#define LACP_TLV_LEN_COLLECTOR 0x10 + +#define LACP_SYS_PRIO 0xffff +#define LACP_DEF_PORT_PRIO 0x00ff + +#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 information is the default */ +#define LACP_STATE_EXPIRED 0x80 + +#define LACP_STATE_FULL (LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING) + +#define LACP_FAST_PERIODIC 50 +#define LACP_SLOW_PERIODIC 1500 +#define LACP_SHORT_TIMEOUT 200 + +#define LACP_RX_INITIALIZE 0 +#define LACP_RX_PORT_DISABLED 1 +#define LACP_RX_EXPIRED 2 +#define LACP_RX_DEFAULTED 3 +#define LACP_RX_CURRENT 4 + +#endif 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/rtlplayground.c b/rtlplayground.c index 3f7860dc..4a8b4ec7 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,16 @@ 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) { + 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) { + lacp_in(); } } else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? && igmp_packet_handler()) { @@ -1290,6 +1296,8 @@ static void handle_tick(void) stp_timers(); } } + if (lacpEnabled) + lacp_timers(); health_phase(HEALTH_PH_STP); lldp_tick(); health_phase(HEALTH_LLDP); @@ -1792,6 +1800,8 @@ void main(void) #endif stp_enabled = 0; stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ + lacpEnabled = 0; + lacp_init(); nic_setup(); REG_SET(RTL837X_NIC_INT_MSK, NIC_INT_RXIE); trap_init(); diff --git a/test/Makefile b/test/Makefile index ae3884ac..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,6 +55,10 @@ $(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) diff --git a/test/README.md b/test/README.md index 08ec0cef..a79b3242 100644 --- a/test/README.md +++ b/test/README.md @@ -48,6 +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 | +| `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/env_tables.c b/test/env_tables.c index 41694b72..719e045a 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,15 @@ 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][MAC_ADDR_LEN]; +uint16_t lacp_rx_count[10]; +uint8_t lacp_port_lag[10]; +uint16_t lacp_lag_ports[LACP_NUM_LAGS]; +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_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) { } diff --git a/test/test_lacp.c b/test/test_lacp.c new file mode 100644 index 00000000..dbadea04 --- /dev/null +++ b/test/test_lacp.c @@ -0,0 +1,480 @@ +#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(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); } + +/* 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[MAC_ADDR_LEN]; + 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[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; + 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[MAC_ADDR_LEN]; + uint8_t src[MAC_ADDR_LEN]; + 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[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, 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 */ + 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, MAC_ADDR_LEN); + 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, 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; + } /* 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[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[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[MAC_ADDR_LEN], 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[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) +{ + 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, 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, 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"); + + /* 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, 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 + * 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", 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 + * 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); + + /* 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); + + /* 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; +}