Merge pull request #299 from DrDoof/feat/lacp

lacp: implement IEEE 802.3ad (LACP)
This commit is contained in:
bloq authored and GitHub committed 2026-10-14 01:35:24 -05:00
commit 24e83d5b74
21 files changed
+1377 -15

No files matched your search

+1
View File
@@ -110,6 +110,7 @@ SRCS += \
rtl837x_stp.c \
rtl837x_lldp.c \
rtl837x_storm.c \
rtl837x_lacp.c \
rtl837x_trap.c
SRCS += \
httpd/httpd.c \
+1
View File
@@ -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)
+24 -1
View File
@@ -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;
}
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 | <port>...))\n");
cmd_error("lag (show | <1-4> (d | [lacp] <port>...))\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 <port>...\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)
+155
View File
@@ -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.
+4
View File
@@ -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).
+45 -8
View File
@@ -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 (.+)$/))){
+5
View File
@@ -356,6 +356,11 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}
<section class="tab" id="tab-lag">
<div id="lagwrap" class="grid2"></div>
<div class="card" style="margin-top:14px"><h2 data-i18n="lacp_heading">LACP (802.3ad)</h2>
<div class="scrollx"><table class="t" id="lacptbl">
<thead><tr><th data-i18n="c_port">Port</th><th>LAG</th><th data-i18n="lacp_actor">Actor</th><th data-i18n="lacp_partner">Partner</th><th data-i18n="lacp_rxstate">RX state</th><th data-i18n="lacp_rx">LACPDUs received</th><th data-i18n="lacp_psys">Partner system</th></tr></thead>
<tbody></tbody></table></div>
</div>
<p class="small mut" style="margin-top:10px" data-i18n="lag_note">A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.</p>
</section>
+2
View File
@@ -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")) {
+48
View File
@@ -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");
+1
View File
@@ -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.
+2
View File
@@ -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
+2 -2
View File
@@ -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];
+504
View File
@@ -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 <stdint.h>
#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');
}
}
+70
View File
@@ -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 <stdint.h>
#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
-1
View File
@@ -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
+12 -2
View File
@@ -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,13 +1061,17 @@ 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) {
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()) {
if (uip_len) {
@@ -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();
+6 -1
View File
@@ -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)
+1
View File
@@ -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_<module>.c` with `main()` driving the module's entry points and
+13
View File
@@ -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];
+1
View File
@@ -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) { }
+480
View File
@@ -0,0 +1,480 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include "rtl837x_common.h"
#include "machine.h"
#include "uip.h"
#include "rtl837x_regs.h"
#include "rtl837x_lacp.h"
/* ---------- platform mocks ---------- */
uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2];
uint16_t uip_len;
struct uip_eth_addr uip_ethaddr = { .addr = {0x02,0x11,0x22,0x33,0x44,0x55} };
const struct machine machine = { .min_port = 0, .max_port = 7,
.log_to_phys_port = {5, 1, 2, 3, 4, 6, 7, 8, 9} };
struct machine_runtime machine_detected = { .isRTL8373 = 1 };
uint8_t lacpEnabled;
uint16_t management_vlan; /* mocked; slow-protocol path zeroes it per-frame */
static int verbose = 0;
void print_string(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;
}