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

LACP (802.3ad)

+
+ +
PortLAGActorPartnerRX stateLACPDUs receivedPartner system
+

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

diff --git a/httpd/httpd.c b/httpd/httpd.c index aec2eaf1..f2ca2d36 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -1036,6 +1036,8 @@ void httpd_appcall(void) send_stp(); } else if (is_word(q, "/lldp.json")) { send_lldp(); + } else if (is_word(q, "/lacp.json")) { + send_lacp(); } else if (is_word(q, "/vlanlist")) { send_vlanlist(); } else if (is_word(q, "/config")) { diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 2d58457e..44c9ddcb 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -14,6 +14,7 @@ #include "version.h" #include "machine.h" #include "rtl837x_stp.h" +#include "rtl837x_lacp.h" #include "page_impl.h" #include "syslog.h" @@ -408,14 +409,19 @@ void send_l2(uint16_t idx) */ __xdata uint16_t entry = idx & 0xfff; __xdata uint16_t first_entry = 0xffff; // An illegal entry index + __xdata uint16_t mbr; __bit first = true; char_to_html('['); while (1) { entries_left--; uint8_t port = 0; uint8_t lag; + uint8_t mc = 0; reg_read_m(RTL837x_TBL_DATA_0); - REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_L2UC << 6), sfr_data[3]); + // Iterate with "next address" (method 2) so static multicast entries - + // an IGMP group entry, say - are listed too, not only learned or + // static unicast ones. + REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_ADDRESS << 6), sfr_data[3]); REG_WRITE(RTL837X_TBL_CTRL, entry >> 8, entry, TBL_L2_UNICAST, TBL_EXECUTE); do { @@ -423,7 +429,7 @@ void send_l2(uint16_t idx) } while (sfr_data[3] & TBL_EXECUTE); reg_read_m(RTL837x_L2_DATA_OUT_B); - __bit valid = (sfr_data[0] & 0x20) != 0; + __bit valid = (sfr_data[0] & 0x20) && !(sfr_data[0] & 0x10); if (valid) { /* separator + 74-byte worst-case entry + closing "]" */ if (slen + 76 > TCP_OUTBUF_SIZE) @@ -432,6 +438,8 @@ void send_l2(uint16_t idx) char_to_html(','); first = false; + mc = sfr_data[2] & 0x01; // multicast MAC: member mask, no SPA/age + // VLAN, taken from the read above instead of reading the register twice slen += strtox(outbuf + slen, "{\"vlan\":\""); charhex_to_html(sfr_data[0] & 0x0f); @@ -450,12 +458,23 @@ void send_l2(uint16_t idx) // type reg_read_m(RTL837x_L2_DATA_OUT_C); - if (sfr_data[1] & 0x1) + if (mc || (sfr_data[1] & 0x1)) slen += strtox(outbuf + slen, "\",\"type\":\"s\",\"port\":"); else slen += strtox(outbuf + slen, "\",\"type\":\"l\",\"port\":"); - port |= (sfr_data[3] & 0x3) << 2; + if (mc) { + // Report the lowest member of the mask (an entry holding only + // the CPU port is rendered as "CPU" by the UI) + mbr = port | ((uint16_t)sfr_data[3] << 2); + port = 0; + while (mbr && !(mbr & 1)) { + mbr >>= 1; + port++; + } + } else { + port |= (sfr_data[3] & 0x3) << 2; + } itoa_html(port); slen += strtox(outbuf + slen, ",\"lag\":"); lag = port_lag_of(port); @@ -736,6 +755,60 @@ void send_stp(void) slen += strtox(outbuf + slen, "]}"); } +/* LACP protocol status for the LAG page ("/lacp.json"). State variables are + * exported read-only via rtl837x_lacp.h; hardware trunk membership itself is + * already visible through send_lag() (it reads the trunk registers). */ +void send_lacp(void) +{ + dbg_string("send_lacp called\n"); + slen = strtox(outbuf, HTTP_RESPONCE_JSON); + + slen += strtox(outbuf + slen, "{\"on\":"); + bool_to_html(lacpEnabled); + /* Per-LAG section: candidate ports, elected aggregator, trunk members. */ + slen += strtox(outbuf + slen, ",\"lags\":["); + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { + slen += strtox(outbuf + slen, "{\"cfg\":\""); + byte_to_html(lacp_lag_ports[l] >> 8); + byte_to_html(lacp_lag_ports[l]); + slen += strtox(outbuf + slen, "\",\"aggValid\":"); + bool_to_html(lacp_agg_valid[l]); + slen += strtox(outbuf + slen, ",\"agg\":\""); + for (uint8_t j = 0; j < 6; j++) + byte_to_html(lacp_agg_sys[l][j]); + slen += strtox(outbuf + slen, "\",\"members\":\""); + byte_to_html(lacp_members_last[l] >> 8); + byte_to_html(lacp_members_last[l]); + slen += strtox(outbuf + slen, "\"},"); + } + slen -= 1; // remove comma + slen += strtox(outbuf + slen, "],\"ports\":["); + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + slen += strtox(outbuf + slen, "{\"p\":"); + itoa_html(machine.log_to_phys_port[i]); + /* Which LACP LAG this port belongs to; 255 = none */ + slen += strtox(outbuf + slen, ",\"lag\":"); + itoa_html(lacp_port_lag[i]); + slen += strtox(outbuf + slen, ",\"a\":\""); + byte_to_html(lacp_actor_state[i]); + slen += strtox(outbuf + slen, "\",\"pt\":\""); + byte_to_html(lacp_partner_state[i]); + slen += strtox(outbuf + slen, "\",\"rs\":"); + itoa_html(lacp_rx_state[i]); + /* rx as 4-digit hex: itoa16_html() only renders values up to 9999 + * correctly (see its VLAN-ID comment), the counter goes to 65535 */ + slen += strtox(outbuf + slen, ",\"rx\":\""); + byte_to_html(lacp_rx_count[i] >> 8); + byte_to_html(lacp_rx_count[i]); + slen += strtox(outbuf + slen, "\",\"psys\":\""); + for (uint8_t j = 0; j < 6; j++) + byte_to_html(lacp_partner_sys[i][j]); + slen += strtox(outbuf + slen, "\"},"); + } + slen -= 1; // remove comma + slen += strtox(outbuf + slen, "]}"); +} + void send_lldp(void) { dbg_string("send_lldp called\n"); diff --git a/httpd/page_impl.h b/httpd/page_impl.h index fd51c8c6..5133d2eb 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -24,6 +24,7 @@ void send_cmd_log(void); void send_lag(void); void send_stp(void); void send_lldp(void); +void send_lacp(void); void send_vlanlist(void); /* Convert only the lower nibble to ascii HEX char. diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index 3ef2b2ec..39b53405 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -22,8 +22,8 @@ extern __xdata uint8_t igmpEnabled; #include "uip.h" #include "uip_arp.h" -#pragma codeseg BANK1 -#pragma constseg BANK1 +#pragma codeseg BANK3 +#pragma constseg BANK3 extern __xdata uint8_t cpuPort; extern __xdata uint8_t sfr_data[4]; diff --git a/rtl837x_lacp.c b/rtl837x_lacp.c new file mode 100644 index 00000000..b4874f16 --- /dev/null +++ b/rtl837x_lacp.c @@ -0,0 +1,607 @@ +/* + * LACP (IEEE 802.3ad Clause 43) driver for the RTL837x platform. + * This code is in the Public Domain. + */ + +// #define DEBUG + +#pragma codeseg BANK3 +#pragma constseg BANK3 + +#include +#include "rtl837x_common.h" +#include "rtl837x_regs.h" +#include "rtl837x_lacp.h" +#include "rtl837x_trap.h" +#include "rtl837x_port.h" +#include "uip.h" +#include "machine.h" + +extern __code const struct machine machine; +extern __xdata struct machine_runtime machine_detected; /* runtime chip detection, owned by rtl837x_port.c */ +extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for slow protocols */ +extern __xdata struct uip_eth_addr uip_ethaddr; +extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; +/* lacpEnabled: extern in rtl837x_lacp.h, owned by rtlplayground.c */ + +/* 20-byte Actor/Partner information body (802.3ad 43.4.2.2), network byte order */ +struct lacp_info { + uint16_t sys_prio; + uint8_t sys[6]; + uint16_t key; + uint16_t port_prio; + uint16_t port; + uint8_t state; + uint8_t reserved[3]; +}; + +/* Outgoing LACPDU (no VLAN tag on TX, as in stp_pkt) */ +struct lacpdu { + uint8_t dst[6]; + uint8_t src[6]; + struct rtl_tag rtl_tag; + uint16_t ethertype; /* 0x8809 */ + uint8_t subtype; /* 0x01 */ + uint8_t version; /* 0x01 */ + uint8_t tlv_actor; /* 0x01 */ + uint8_t actor_len; /* 0x14 */ + struct lacp_info actor; + uint8_t tlv_partner; /* 0x02 */ + uint8_t partner_len; /* 0x14 */ + struct lacp_info partner; + uint8_t tlv_collector; /* 0x03 */ + uint8_t collector_len; /* 0x10 */ + uint16_t collector_max_delay; + uint8_t collector_reserved[12]; + uint8_t tlv_terminator; /* 0x00 */ + uint8_t terminator_len; /* 0x00 */ + uint8_t terminator_reserved[50]; +}; + +/* Incoming LACPDU, split because a VLAN tag sits between the two only when the + * frame was forwarded: a trapped one carries none (cf. stp_pkt_in) */ +struct lacpdu_in_hdr { + uint8_t dst[6]; + uint8_t src[6]; + struct rtl_tag rtl_tag; +}; + +struct lacpdu_in { + uint16_t ethertype; + uint8_t subtype; + uint8_t version; + uint8_t tlv_actor; + uint8_t actor_len; + struct lacp_info actor; + uint8_t tlv_partner; + uint8_t partner_len; + struct lacp_info partner; +}; + +__xdata uint8_t lacp_rxbody; /* offset of the LACPDU body: the RTL tag, plus a VLAN tag when forwarded */ + +#define LACP_O ((__xdata struct lacpdu *)&uip_buf[RTL_FRAME_DESC_SIZE]) +#define LACP_H ((__xdata struct lacpdu_in_hdr *)&uip_buf[0]) +#define LACP_I ((__xdata struct lacpdu_in *)&uip_buf[lacp_rxbody]) + +/* --- Per-port participant state (802.3ad 43.4.7). Indexed by physical port. --- */ +__xdata uint8_t lacp_actor_state[10]; /* our Actor_State flags */ +__xdata uint8_t lacp_rx_state[10]; /* LACP_RX_* receive machine */ +__xdata uint8_t lacp_partner_state[10]; /* last Partner_State we saw */ +__xdata uint8_t lacp_partner_sys[10][6]; /* partner System ID */ +__xdata uint16_t lacp_partner_key[10]; /* partner Key */ +__xdata uint16_t lacp_partner_port[10]; /* partner Port number */ +/* The Partner block echoes the peer's actor identity verbatim, see + * doc/link_aggregation.md. */ +__xdata uint16_t lacp_partner_sys_prio[10]; /* partner System priority */ +__xdata uint16_t lacp_partner_port_prio[10]; /* partner Port priority */ +__xdata uint16_t lacp_periodic[10]; /* down-counter to next TX */ +__xdata uint16_t lacp_timeout[10]; /* down-counter to partner expiry*/ +__xdata uint8_t lacp_ntt[10]; /* Need-To-Transmit flag */ + +/* The timers step every four system ticks (50 Hz) whatever the pace of the + * main loop; a long pass is caught up on, a few steps at a time. */ +#define LACP_TICK_STEP (SYS_TICK_HZ / 50) +#define LACP_CATCH_UP 8 +__xdata uint8_t lacp_tick_last; +extern volatile __xdata uint32_t ticks; + +/* Per-LAG state: the hardware trunk groups run LACP independently. */ +__xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; +__xdata uint8_t lacp_port_lag[10]; +__xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +__xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; +__xdata uint16_t lacp_members_last[LACP_NUM_LAGS]; + +__xdata uint16_t lacp_scratch_mask; +__xdata uint8_t lacp_scratch_flag; +__xdata uint8_t lacp_scratch_lag; + +/* Per-port RX LACPDU count - primarily a hardware bring-up diagnostic to + * verify slow-protocol frames actually reach the CPU port (see "lacp show") */ +__xdata uint16_t lacp_rx_count[10]; + + +void lacp_mux_update(void) __banked; /* defined below, used from lacp_in */ + +static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b) +{ + for (uint8_t i = 0; i < 6; i++) { + if (a[i] != b[i]) + return 0; + } + return 1; +} + + +/* + * Selection logic (simplified 43.4.14): a port may join the aggregator when + * we have fresh partner info, the partner considers the link aggregatable, + * and the partner is the same system the aggregator was elected for. + */ +static uint8_t lacp_port_selected(uint8_t port) +{ + uint8_t lag = lacp_port_lag[port]; + if (lag == LACP_LAG_NONE) /* port not in any LACP LAG */ + return 0; + if (lacp_rx_state[port] != LACP_RX_CURRENT) + return 0; + if (!(lacp_partner_state[port] & LACP_STATE_AGGREGATION)) + return 0; + if (!lacp_agg_valid[lag]) + return 0; + return lacp_sys_eq(lacp_partner_sys[port], lacp_agg_sys[lag]); +} + + +/* + * Mux machine (simplified coupled control, 43.4.15): SYNC follows selection; + * COLLECTING+DISTRIBUTING additionally require the partner to be in sync. + * Returns 1 if the actor state changed (caller sets NTT). + */ +static uint8_t lacp_mux_machine(uint8_t port) +{ + uint8_t old = lacp_actor_state[port]; + uint8_t new = old; + + if (lacp_port_selected(port)) { + new |= LACP_STATE_SYNC; + if (lacp_partner_state[port] & LACP_STATE_SYNC) + new |= LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING; + else + new &= ~(LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING); + } else { + new &= ~(LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING); + } + + if (new == old) + return 0; + lacp_actor_state[port] = new; + return 1; +} + +#define port_bit(p) (((uint8_t)1) << (p)) + + +/* Build and emit one LACPDU out physical port `port` (802.3ad 43.4.2). */ +void lacp_send(uint8_t port) __banked +{ + memset((__xdata uint8_t *)LACP_O, 0, sizeof(struct lacpdu)); + LACP_O->dst[0] = 0x01; LACP_O->dst[1] = 0x80; LACP_O->dst[2] = 0xc2; + LACP_O->dst[3] = 0x00; LACP_O->dst[4] = 0x00; LACP_O->dst[5] = LACP_DST5; + memcpy(LACP_O->src, uip_ethaddr.addr, 6); + + LACP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); + LACP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; + LACP_O->rtl_tag.reason = 0x00; + /* Must go through HTONS like every other tag field: writing 0x0020 raw put + * the bits in the wrong byte (on the wire 0x2000 = EFID, not LEARN_DIS), so + * the ASIC could not parse the tag and forwarded the frame with the 0x8899 + * header still attached - the partner then saw ethertype 0x8899 instead of + * 0x8809 and ignored the LACPDU. KEEP additionally stops the switch from + * rewriting the 802.1Q tag format of a CPU-injected frame (as mainline). */ + LACP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS | RTL_TAG_KEEP); + /* ALLOW cleared => this word is the *forwarding* port mask, i.e. directed + * egress to exactly this port (what mainline does). Setting ALLOW instead + * makes it an allowance/permission mask on top of a normal lookup, which + * for a one-hot mask resolves to an empty egress set and the frame is + * silently dropped - verified on hardware. */ + LACP_O->rtl_tag.pmask = HTONS(port_bit(port)); /* egress this port only */ + + LACP_O->ethertype = HTONS(SLOW_PROTO_ETHERTYPE); + LACP_O->subtype = SLOW_PROTO_SUBTYPE_LACP; + LACP_O->version = LACP_VERSION; + + /* Actor TLV */ + LACP_O->tlv_actor = LACP_TLV_ACTOR; + LACP_O->actor_len = LACP_TLV_LEN_INFO; + LACP_O->actor.sys_prio = HTONS(LACP_SYS_PRIO); + memcpy(LACP_O->actor.sys, uip_ethaddr.addr, 6); + /* Per-LAG Actor Key so a partner never merges ports of our different LAGs + * into one aggregate (only ever called for ports in a LACP LAG). */ + LACP_O->actor.key = HTONS((uint16_t)(lacp_port_lag[port] + 1)); + LACP_O->actor.port_prio = HTONS(LACP_DEF_PORT_PRIO); + LACP_O->actor.port = HTONS((uint16_t)port + 1); /* 1-based port id */ + LACP_O->actor.state = lacp_actor_state[port]; + + /* Partner TLV: echo the last partner info we recorded, VERBATIM. A Linux + * 802.3ad partner (__record_pdu) only accepts the SYNC bit we set above if + * this block mirrors its own actor identity exactly - system+priority, key, + * port+priority. Priorities must be echoed, not hardcoded: Linux defaults to + * system priority 0xffff, so a hardcoded 0 here cleared its partner-SYNC + * (0x3f->0x37) and the aggregate never collected/distributed. */ + LACP_O->tlv_partner = LACP_TLV_PARTNER; + LACP_O->partner_len = LACP_TLV_LEN_INFO; + LACP_O->partner.sys_prio = HTONS(lacp_partner_sys_prio[port]); + memcpy(LACP_O->partner.sys, lacp_partner_sys[port], 6); + LACP_O->partner.key = HTONS(lacp_partner_key[port]); + LACP_O->partner.port_prio = HTONS(lacp_partner_port_prio[port]); + LACP_O->partner.port = HTONS(lacp_partner_port[port]); + LACP_O->partner.state = lacp_partner_state[port]; + + /* Collector TLV + Terminator */ + LACP_O->tlv_collector = LACP_TLV_COLLECTOR; + LACP_O->collector_len = LACP_TLV_LEN_COLLECTOR; + LACP_O->collector_max_delay = 0; + LACP_O->tlv_terminator = LACP_TLV_TERMINATOR; + LACP_O->terminator_len = 0x00; + + lacp_ntt[port] = 0; + /* Slow-protocol frames are link-local and must egress untagged. With a + * management VLAN set, tcpip_output() splices an 802.1Q tag after the SA, + * shifting our in-frame rtl_tag out of the position the ASIC expects - so it + * fails to parse/strip the CPU tag and the 0x8899 header leaks onto the wire + * (the partner then ignores the LACPDU). Suppress the VLAN insert for this + * frame only; management traffic keeps its tag. Verified on HW: with the + * insert suppressed the LACPDU egresses as clean 0x8809 and the bond + * partner converges (partner MAC becomes the switch, both links aggregate). */ + uint16_t saved_mgmt_vlan = management_vlan; + management_vlan = 0; + uip_len = sizeof(struct lacpdu); + tcpip_output(); + management_vlan = saved_mgmt_vlan; +} + + +/* + * Process an incoming LACPDU. Called from the rtlplayground.c dispatch when a + * frame with DA 01:80:C2:00:00:02 and ethertype 0x8809/subtype 0x01 arrives. + * The ingress port is the low nibble of rtl_tag.pmask on RX. + */ +void lacp_in(void) __banked +{ + lacp_rxbody = TRAP_RX_BODY(); + if (uip_len < lacp_rxbody + sizeof(struct lacpdu_in)) { + uip_len = 0; + return; + } + uip_len = 0; + + if (LACP_I->ethertype != HTONS(SLOW_PROTO_ETHERTYPE) + || LACP_I->subtype != SLOW_PROTO_SUBTYPE_LACP) + return; + + /* Per rtl837x_common.h, pmask carries a 4-bit port number on RX + * (hardware-verified on RTL8373: per-port rx counters track the actual + * ingress port through full LACP convergence). */ + uint8_t port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f; + if (port < machine.min_port || port > machine.max_port) + return; + + /* Only ports assigned to a LACP-mode LAG participate in the protocol. */ + uint8_t lag = lacp_port_lag[port]; + if (lag == LACP_LAG_NONE) + return; + + +#ifdef DEBUG + print_string("LACP in, port "); print_byte(port); + print_string(" partner_state "); print_byte(LACP_I->actor.state); write_char('\n'); +#endif + + lacp_rx_count[port]++; + + /* Record partner = the remote's Actor block (802.3ad 43.4.9 recordPDU) */ + memcpy(lacp_partner_sys[port], LACP_I->actor.sys, 6); + lacp_partner_key[port] = HTONS(LACP_I->actor.key); + lacp_partner_port[port] = HTONS(LACP_I->actor.port); + lacp_partner_sys_prio[port] = HTONS(LACP_I->actor.sys_prio); + lacp_partner_port_prio[port] = HTONS(LACP_I->actor.port_prio); + lacp_partner_state[port] = LACP_I->actor.state; + + /* Receive machine -> CURRENT, (re)arm partner timeout (43.4.12) */ + lacp_rx_state[port] = LACP_RX_CURRENT; + lacp_timeout[port] = (LACP_I->actor.state & LACP_STATE_TIMEOUT) + ? LACP_SHORT_TIMEOUT : LACP_LONG_TIMEOUT; + + /* Elect this LAG's aggregator partner system on first contact (43.4.14) */ + if (!lacp_agg_valid[lag]) { + memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, 6); + lacp_agg_valid[lag] = 1; + } + + /* Selection + Mux for this port; state change => Need-To-Transmit */ + if (lacp_mux_machine(port)) + lacp_ntt[port] = 1; + + /* update_NTT (43.4.12): if the partner's view of us is stale (their + * Partner block does not match our actor state/port), tell them again. */ + if (LACP_I->partner.state != lacp_actor_state[port] + || HTONS(LACP_I->partner.port) != (uint16_t)port + 1) + lacp_ntt[port] = 1; + + lacp_mux_update(); +} + + +/* Recompute each LACP LAG's hardware trunk membership: a port joins its LAG's + * trunk once it is fully in sync (SYNC+COLLECTING+DISTRIBUTING and the partner + * in SYNC). Each LACP-mode LAG is programmed independently (simplified 43.4.15). */ +void lacp_mux_update(void) __banked +{ + for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + if (!lacp_lag_ports[lag]) /* LAG not under LACP management */ + continue; + lacp_scratch_mask = 0; + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] != lag) + continue; + if ((lacp_actor_state[i] & LACP_STATE_FULL) == LACP_STATE_FULL + && (lacp_partner_state[i] & LACP_STATE_SYNC)) + lacp_scratch_mask |= port_bit(i); + } + /* Program the trunk only when this LAG's membership actually changed + * - this runs on every timer tick. */ + if (lacp_scratch_mask != lacp_members_last[lag]) { + lacp_members_last[lag] = lacp_scratch_mask; + port_lag_members_set(lag, lacp_scratch_mask); + } + } +} + + +static void lacp_step(void) +{ + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] == LACP_LAG_NONE) /* not in a LACP LAG */ + continue; + + /* Periodic transmit machine (802.3ad 43.4.13) */ + if (lacp_periodic[i]) { + lacp_periodic[i]--; + } else { + lacp_periodic[i] = (lacp_partner_state[i] & LACP_STATE_TIMEOUT) + ? LACP_FAST_PERIODIC : LACP_SLOW_PERIODIC; + lacp_ntt[i] = 1; + } + + /* Partner timeout -> DEFAULTED (802.3ad 43.4.12): the mux machine + * then drops SYNC/COLLECTING/DISTRIBUTING and the port leaves the + * trunk. TODO(43.4.12): churn detection is diagnostics-only, skipped. */ + if (lacp_timeout[i]) { + if (!--lacp_timeout[i]) { + lacp_rx_state[i] = LACP_RX_DEFAULTED; + lacp_partner_state[i] = LACP_STATE_DEFAULTED; + if (lacp_mux_machine(i)) + lacp_ntt[i] = 1; + } + } + + if (lacp_ntt[i]) + lacp_send(i); + } + + /* Release each LAG's aggregator identity once none of its ports still + * hears the elected partner, so a re-cabled setup can elect a new one + * (43.4.14). A port that is CURRENT with some other system must not keep + * the old identity alive: it would never be selected, and neither would + * the rest of the LAG once it moves to that system too. */ + for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) { + if (!lacp_agg_valid[lag]) + continue; + uint8_t any_current = 0; + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] == lag && lacp_rx_state[i] == LACP_RX_CURRENT + && lacp_sys_eq(lacp_partner_sys[i], lacp_agg_sys[lag])) { + any_current = 1; + break; + } + } + if (!any_current) + lacp_agg_valid[lag] = 0; + } + + lacp_mux_update(); +} + + +void lacp_timers(void) __banked +{ + uint8_t n = LACP_CATCH_UP; + while (n-- && TICKS_DUE(lacp_tick_last, LACP_TICK_STEP)) { + lacp_tick_last += LACP_TICK_STEP; + lacp_step(); + } +} + + +/* Bring one port up as an active-fast LACP participant (43.4.12 init). */ +static void lacp_port_init(uint8_t port) +{ + lacp_actor_state[port] = LACP_STATE_ACTIVITY | LACP_STATE_AGGREGATION + | LACP_STATE_TIMEOUT; /* active + fast */ + lacp_rx_state[port] = LACP_RX_INITIALIZE; + lacp_partner_state[port] = LACP_STATE_DEFAULTED; + memset(lacp_partner_sys[port], 0, 6); + lacp_partner_key[port] = 0; + lacp_partner_port[port] = 0; + lacp_periodic[port] = LACP_FAST_PERIODIC; + lacp_timeout[port] = 0; + lacp_ntt[port] = 1; /* announce ourselves immediately */ + lacp_rx_count[port] = 0; +} + +/* Return a port to non-LACP state. (Port isolation is no longer touched - a + * trapped LACPDU never enters the forwarding path, so a sibling cannot see + * it, which is what the isolation workaround was for.) */ +static void lacp_port_release(uint8_t port) +{ + lacp_actor_state[port] = 0; + lacp_ntt[port] = 0; + lacp_partner_state[port] = 0; + lacp_rx_state[port] = LACP_RX_INITIALIZE; + lacp_rx_count[port] = 0; + memset(lacp_partner_sys[port], 0, 6); +} + +/* True while any LAG is under LACP management (drives the global RMA trap). */ +static uint8_t lacp_any_lag(void) +{ + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) + if (lacp_lag_ports[l]) + return 1; + return 0; +} + +/* + * Turn the LACP engine on: trap the Slow-Protocols group to the CPU port. The + * trap acts on the address before the VLAN lookup and before the ingress + * filters, so delivery needs no L2 entry and no PVID of its own. + */ +static void lacp_engine_on(void) +{ + lacp_tick_last = (uint8_t)ticks; + lacpEnabled = 1; + trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_TRAP); +} + +/* Turn the LACP engine off: stop delivering slow-protocols to the CPU. */ +static void lacp_engine_off(void) +{ + trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_DROP); + lacpEnabled = 0; +} + +/* One-time boot init: no LAG runs LACP yet, so every port maps to LACP_LAG_NONE. + * MUST run before any "lag ... lacp" config replay - xdata is not zeroed to the + * 0xff sentinel, and a stray 0 would make a port look like it belongs to LAG 0. */ +void lacp_init(void) __banked +{ + for (uint8_t i = 0; i < 10; i++) { + lacp_port_lag[i] = LACP_LAG_NONE; + lacp_actor_state[i] = 0; + lacp_ntt[i] = 0; + } + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { + lacp_lag_ports[l] = 0; + lacp_agg_valid[l] = 0; + lacp_members_last[l] = 0; + } + lacpEnabled = 0; +} + +/* + * Assign a candidate-port mask to LACP-mode LAG `lag` (`lag lacp `). + * ports == 0 removes the LAG from LACP management. Ports leaving the LAG are + * returned to normal switching; ports joining start the protocol immediately. + * The engine's RMA trap is enabled on the first LACP LAG and torn down with the + * last. Never wipes a static "lag" the user configured on a different group. + */ +void lacp_lag_set(uint8_t lag, uint16_t ports) __banked +{ + if (lag >= LACP_NUM_LAGS) + return; + if (ports && ports == lacp_lag_ports[lag]) + return; + lacp_scratch_flag = lacp_any_lag(); + + /* Drop ports that were in this LAG but are not in the new mask. */ + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (lacp_port_lag[i] == lag && !(ports & port_bit(i))) { + lacp_port_lag[i] = LACP_LAG_NONE; + lacp_port_release(i); + } + } + /* Release this LAG's trunk: LACP's own, or the static one a group put + * under LACP still has. Taking the group out (ports == 0) leaves a static + * trunk alone. */ + if (lacp_members_last[lag] || ports) { + lacp_members_last[lag] = 0; + port_lag_members_set(lag, 0); + } + lacp_agg_valid[lag] = 0; + lacp_lag_ports[lag] = ports; + + if (ports && !lacp_scratch_flag) /* first LACP LAG: bring the engine up first */ + lacp_engine_on(); + + /* Adopt the new member ports (with the engine already up). A port that + * belonged to another LACP LAG leaves that LAG's candidate set first. */ + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + if (!(ports & port_bit(i))) + continue; + lacp_scratch_lag = lacp_port_lag[i]; + if (lacp_scratch_lag != LACP_LAG_NONE && lacp_scratch_lag != lag) { + lacp_lag_ports[lacp_scratch_lag] &= ~port_bit(i); + if (!lacp_lag_ports[lacp_scratch_lag]) { + lacp_agg_valid[lacp_scratch_lag] = 0; + if (lacp_members_last[lacp_scratch_lag]) { + lacp_members_last[lacp_scratch_lag] = 0; + port_lag_members_set(lacp_scratch_lag, 0); + } + } + } + lacp_port_lag[i] = lag; + lacp_port_init(i); + } + + if (!lacp_any_lag() && lacp_scratch_flag) /* removed the last LACP LAG */ + lacp_engine_off(); + + lacp_mux_update(); /* a LAG that lost a port to this one drops it from its trunk */ +} + + +/* "lacp off": remove every LACP LAG (releases their trunks, stops the engine). + * There is no "lacp on": the engine comes up with the first "lag lacp". */ +void lacp_off(void) __banked +{ + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) + if (lacp_lag_ports[l]) + lacp_lag_set(l, 0); +} + + +/* "lacp show": per-port protocol state - the primary bring-up diagnostic. + * A stuck rx=0 counter means slow-protocol frames never reach the CPU port + * (see the RMA note in lacp_engine_on()). */ +void lacp_show(void) __banked +{ + print_string("LACP "); print_string(lacpEnabled ? "on" : "off"); write_char('\n'); + for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) { + if (!lacp_lag_ports[l]) + continue; + print_string("lag "); print_byte(l + 1); + print_string(" ports "); print_short(lacp_lag_ports[l]); + print_string(" aggregator "); + if (lacp_agg_valid[l]) { + for (uint8_t j = 0; j < 6; j++) + print_byte(lacp_agg_sys[l][j]); + } else { + print_string("(none)"); + } + print_string(" members "); print_short(lacp_members_last[l]); write_char('\n'); + } + + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + print_string("port "); print_byte(machine.log_to_phys_port[i]); + print_string(" actor "); print_byte(lacp_actor_state[i]); + print_string(" partner "); print_byte(lacp_partner_state[i]); + print_string(" rxst "); print_byte(lacp_rx_state[i]); + print_string(" rx "); print_short(lacp_rx_count[i]); + print_string(" psys "); + for (uint8_t j = 0; j < 6; j++) + print_byte(lacp_partner_sys[i][j]); + write_char('\n'); + } +} diff --git a/rtl837x_lacp.h b/rtl837x_lacp.h new file mode 100644 index 00000000..5f03a16e --- /dev/null +++ b/rtl837x_lacp.h @@ -0,0 +1,101 @@ +#ifndef _RTL837X_LACP_H_ +#define _RTL837X_LACP_H_ + +/* + * LACP (IEEE 802.3ad Clause 43) for the RTL837x platform. + * This code is in the Public Domain. + */ + +#include + +void lacp_init(void) __banked; /* boot init: clear per-LAG state */ +void lacp_in(void) __banked; +void lacp_timers(void) __banked; +void lacp_off(void) __banked; +void lacp_show(void) __banked; /* "lacp show" - per-port state + RX counters */ +/* Assign a candidate-port mask to a LACP-mode LAG (`lag lacp `). + * ports == 0 removes the LAG from LACP management. Enables the engine on first + * LACP LAG and tears it down when the last one goes away. */ +void lacp_lag_set(uint8_t lag, uint16_t ports) __banked; + +/* + * Per-LAG LACP: each of the 4 hardware trunk groups (0-3) can independently run + * LACP on an admin-assigned set of candidate ports, forming its own aggregator + * (its own elected partner System). A port participates in LACP only if it has + * been assigned to a LACP-mode LAG (lacp_port_lag[port] != LACP_LAG_NONE). + * Ports of the same LAG that converge with a consistent partner join that LAG's + * hardware trunk. This is the port-channel model: `lag lacp `. + */ +#define LACP_NUM_LAGS 4 +#define LACP_LAG_NONE 0xff /* port not assigned to any LACP LAG */ + +/* Protocol state, exposed read-only for the web UI (page_impl.c send_lacp()) + * and the serial console. Owned by rtl837x_lacp.c / rtlplayground.c. */ +extern __xdata uint8_t lacpEnabled; +extern __xdata uint8_t lacp_actor_state[10]; +extern __xdata uint8_t lacp_partner_state[10]; +extern __xdata uint8_t lacp_rx_state[10]; +extern __xdata uint8_t lacp_partner_sys[10][6]; +extern __xdata uint16_t lacp_rx_count[10]; +extern __xdata uint8_t lacp_port_lag[10]; /* LAG (0-3) a port runs LACP on, or LACP_LAG_NONE */ +extern __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; /* admin candidate-port mask per LACP LAG (0 = LAG not LACP) */ +extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; /* elected partner System per LAG */ +extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; /* aggregator elected for this LAG */ +extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS];/* trunk members we last programmed per LAG */ + +/* Slow-Protocols / LACPDU identifiers (802.3ad 43.4) */ +#define SLOW_PROTO_ETHERTYPE 0x8809 +#define SLOW_PROTO_SUBTYPE_LACP 0x01 +#define LACP_VERSION 0x01 +#define LACP_DST5 0x02 /* 01:80:C2:00:00:02 last octet */ + +/* LACPDU TLV framing (802.3ad 43.4.2): type bytes + fixed TLV lengths. */ +#define LACP_TLV_ACTOR 0x01 +#define LACP_TLV_PARTNER 0x02 +#define LACP_TLV_COLLECTOR 0x03 +#define LACP_TLV_TERMINATOR 0x00 +#define LACP_TLV_LEN_INFO 0x14 /* Actor/Partner Information TLV length */ +#define LACP_TLV_LEN_COLLECTOR 0x10 /* Collector TLV length */ + +/* Our aggregation identity: System priority is a constant (Linux 802.3ad + * defaults to the same 0xffff); the default Actor port priority likewise. */ +#define LACP_SYS_PRIO 0xffff +#define LACP_DEF_PORT_PRIO 0x00ff + +/* Actor_State / Partner_State flag bits (802.3ad 43.4.2) */ +#define LACP_STATE_ACTIVITY 0x01 /* 1 = Active LACP (send periodically) */ +#define LACP_STATE_TIMEOUT 0x02 /* 1 = Short (fast) timeout */ +#define LACP_STATE_AGGREGATION 0x04 /* 1 = link is aggregatable */ +#define LACP_STATE_SYNC 0x08 /* 1 = in sync with partner */ +#define LACP_STATE_COLLECTING 0x10 +#define LACP_STATE_DISTRIBUTING 0x20 +#define LACP_STATE_DEFAULTED 0x40 /* partner info is administrative default*/ +#define LACP_STATE_EXPIRED 0x80 /* wire-format doc; unused by the simplified RX machine */ + +/* Fully-participating actor: in sync AND collecting AND distributing. */ +#define LACP_STATE_FULL (LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING) + +/* + * Timer units: one step of the LACP timers, taken every four system ticks, + * 50 per second. A long pass of the main loop is caught up on a few steps at + * a time, so a period is a range and the worst case is what matters. + * + * The transmit periods are not free to choose. Our own timeouts only decide + * how patient we are, but our TX period decides when the PARTNER gives up on + * us: 802.3ad pairs fast periodic 1 s with a 3 s short timeout, and slow + * periodic 30 s with a 90 s long timeout. A period whose worst case reaches + * the partner's threshold makes it expire us intermittently, which reads as + * an aggregate that works but never stops churning. */ +#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */ +#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */ +#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */ +#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */ + +/* Per-port LACP receive-machine state (802.3ad 43.4.12) */ +#define LACP_RX_INITIALIZE 0 +#define LACP_RX_PORT_DISABLED 1 /* numbering reserved (exported via /lacp.json "rs") */ +#define LACP_RX_EXPIRED 2 /* numbering reserved (exported via /lacp.json "rs") */ +#define LACP_RX_DEFAULTED 3 +#define LACP_RX_CURRENT 4 + +#endif diff --git a/rtl837x_regs.h b/rtl837x_regs.h index dc4b2dc1..1eb865a4 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -284,6 +284,10 @@ * Loop detection / STP */ #define RTL8373_RLDP_TIMER 0x1074 +/* Per reserved-multicast-address action. The registers are packed and + * consecutive by ENTRY rather than by the byte of the address: entry 0 is + * 01:80:C2:00:00:00 at 0x4ecc and entry 2 is ...:02 (Slow Protocols / LACP) + * at 0x4ed4, but entry 5 is ...:08, so the step of 4 counts entries. */ #define RTL837X_RMA0_CONF 0x4ecc #define RTL837X_RMA2_CONF 0x4ed4 #define RTL837X_RMA_ACT_MASK 0x30 diff --git a/rtlplayground.c b/rtlplayground.c index 3f7860dc..f61140a2 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -13,6 +13,7 @@ #include "rtl837x_port.h" #include "rtl837x_trap.h" #include "rtl837x_stp.h" +#include "rtl837x_lacp.h" #include "rtl837x_igmp.h" #include "rtl837x_leds.h" #include "rtl837x_bandwidth.h" @@ -133,6 +134,7 @@ __xdata bool stp_enabled; __xdata uint8_t igmpEnabled; __xdata bool lldp_enabled; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ +__xdata uint8_t lacpEnabled; __code const uint16_t bit_mask[16] = { 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, @@ -1059,12 +1061,18 @@ void handle_rx(void) print_byte(uip_buf[3]); print_byte(uip_buf[4]); print_byte(uip_buf[5]); write_char('\n'); print_string(" MGMT-VLAN: "); print_short(management_vlan); write_char('\n'); #endif - if (stp_enabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet? - && uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x00) { - stp_in(); - if (uip_len) { - print_string("STP TX\n"); - tcpip_output(); + if ((stp_enabled || lacpEnabled) && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 + && uip_buf[2] == 0xc2 && uip_buf[3] == 0x00 && uip_buf[4] == 0x00) { // 01:80:C2:00:00:0x + if (stp_enabled && uip_buf[5] == 0x00) { // STP/RSTP BPDU + stp_in(); + if (uip_len) { + print_string("STP TX\n"); + tcpip_output(); + } + } else if (lacpEnabled && uip_buf[5] == 0x02) { // LACPDU (slow protocols) + lacp_in(); + if (uip_len) + tcpip_output(); } } else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? && igmp_packet_handler()) { @@ -1290,6 +1298,9 @@ static void handle_tick(void) stp_timers(); } } + // If LACP enabled, drive its machines (own tick divider lives in the banked module) + if (lacpEnabled) + lacp_timers(); health_phase(HEALTH_PH_STP); lldp_tick(); health_phase(HEALTH_LLDP); @@ -1792,6 +1803,8 @@ void main(void) #endif stp_enabled = 0; stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ + lacpEnabled = 0; + lacp_init(); /* clear per-LAG state (port->LAG map = NONE) before any config replay */ nic_setup(); REG_SET(RTL837X_NIC_INT_MSK, NIC_INT_RXIE); trap_init(); diff --git a/test/Makefile b/test/Makefile index ae3884ac..e47cbf4e 100644 --- a/test/Makefile +++ b/test/Makefile @@ -59,15 +59,24 @@ $(BUILDDIR)/test_page_json: test_page_json.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR $(BUILDDIR)/test_httpd_tx: test_httpd_tx.c $(SRCROOT)/httpd/httpd.c $(SRCROOT)/uip/uip.c | $(BUILDDIR) $(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS) +# Sandboxes with a Makefile of their own; each fails the run when it fails. +SUBTESTS = lacp port_hash + run: $(TESTS) @fail=0; for t in $(TESTS); do \ echo "=== $$t ==="; \ $$t || fail=1; \ echo; \ done; \ + for d in $(SUBTESTS); do \ + echo "=== $$d ==="; \ + $(MAKE) -C $$d test || fail=1; \ + echo; \ + done; \ if [ $$fail -ne 0 ]; then echo "TESTS FAILED"; exit 1; else echo "ALL TESTS PASSED"; fi clean: rm -rf $(BUILDDIR) + @for d in $(SUBTESTS); do $(MAKE) -C $$d clean; done .PHONY: all run clean diff --git a/test/README.md b/test/README.md index 08ec0cef..4f040d5a 100644 --- a/test/README.md +++ b/test/README.md @@ -48,6 +48,8 @@ fires — so it drops straight into CI. | `test_port_tables` | `rtl837x_port.c` | VLAN entry layout and round trip, PVID register sharing, static multicast and management entries, per-port flush, trunk membership and hash seed | | `test_page_json` | `httpd/page_impl.c` + `rtl837x_port.c` | `/vlan.json`, `/vlanlist`, `/l2.json` (walk, wrap marker, paging inside `outbuf`), 64-bit counters in `/status.json` and `/counters.json` | | `test_httpd_tx` | `httpd/httpd.c` + `uip/uip.c` | a GET of a static file against a client that moves its receive window: ACK accounting, continuation chunks out of flash, retransmission | +| `lacp/` | `rtl837x_lacp.c` | state machines against a simulated 802.3ad partner: convergence, mis-cabling, expiry, re-election, two groups, LACPDU containment entries | +| `port_hash/` | `port_lag_members_set()` | a LAG seeds its own hash register | ## Adding a test for another module 1. Write `test_.c` with `main()` driving the module's entry points and diff --git a/test/env_tables.c b/test/env_tables.c index 41694b72..6a841a19 100644 --- a/test/env_tables.c +++ b/test/env_tables.c @@ -16,6 +16,7 @@ #include "syslog.h" #include "rtl837x_phy.h" #include "support.h" +#include "rtl837x_lacp.h" /* ---- console: everything lands in out_buf (support.c) ---- */ void print_byte(uint8_t v) @@ -124,3 +125,18 @@ uint8_t stp_prio = 0x80, stp_root_port = 0xff, stp_rstp = 1, stp_txhold = 6; uint8_t sfp_buf[16]; bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) { (void)slot; (void)reg; (void)len; return false; } void print_phys_port(uint8_t port) { itoa_short(machine.log_to_phys_port[port]); } + +uint8_t lacpEnabled; +uint8_t lacp_actor_state[10]; +uint8_t lacp_partner_state[10]; +uint8_t lacp_rx_state[10]; +uint8_t lacp_partner_sys[10][6]; +uint16_t lacp_rx_count[10]; +uint8_t lacp_port_lag[10] = { + LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, + LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, +}; +uint16_t lacp_lag_ports[LACP_NUM_LAGS]; +uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; +uint8_t lacp_agg_valid[LACP_NUM_LAGS]; +uint16_t lacp_members_last[LACP_NUM_LAGS]; diff --git a/test/lacp/.gitignore b/test/lacp/.gitignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/test/lacp/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/test/lacp/Makefile b/test/lacp/Makefile new file mode 100644 index 00000000..856f21e5 --- /dev/null +++ b/test/lacp/Makefile @@ -0,0 +1,31 @@ +# Host-side sandbox for the firmware LACP state machines. +# +# Compiles the UNMODIFIED ../../rtl837x_lacp.c against shim headers: SDCC +# storage-class keywords are blanked via -D, and the module copy lives in +# build/ so quote-includes resolve to shim/ (its own directory has no real +# platform headers) instead of the firmware tree. rtl837x_trap.h is copied +# alongside it for the same reason: TRAP_RX_BODY() is what delivery by trap +# rests on, so the test must use the real macro, not a shim restating it. +CC ?= cc +CFLAGS = -std=c11 -Wall -Wextra -Wno-unknown-pragmas \ + -D__xdata= -D__code= -D__banked= -D__reentrant= \ + -Ishim -I../.. + +all: test + +test: build/lacp_sim + ./build/lacp_sim + +verbose: build/lacp_sim + ./build/lacp_sim -v + +build/lacp_sim: lacp_sim.c ../../rtl837x_lacp.c ../../rtl837x_lacp.h ../../rtl837x_trap.h shim/*.h + mkdir -p build + cp ../../rtl837x_lacp.c build/lacp_under_test.c + cp ../../rtl837x_trap.h build/rtl837x_trap.h + $(CC) $(CFLAGS) -o $@ lacp_sim.c build/lacp_under_test.c + +clean: + rm -rf build + +.PHONY: all test verbose clean diff --git a/test/lacp/lacp_sim.c b/test/lacp/lacp_sim.c new file mode 100644 index 00000000..a833ac9f --- /dev/null +++ b/test/lacp/lacp_sim.c @@ -0,0 +1,464 @@ +/* + * Host-side sandbox for the RTL837x LACP implementation. + * + * Compiles the UNMODIFIED rtl837x_lacp.c against shim headers (see shim/) and + * drives it with synthetic LACPDUs from a simulated partner switch/bond. This + * exercises the real state machines that ship in the firmware image - only + * the platform below them (frame I/O, trunk registers, console) is mocked. + * + * Run: make -C test/lacp (exit code 0 = all scenarios pass) + */ +#include +#include +#include +#include + +#include "rtl837x_common.h" +#include "machine.h" +#include "uip.h" +#include "rtl837x_regs.h" +#include "rtl837x_lacp.h" + +/* ---------- platform mocks ---------- */ + +uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; +uint16_t uip_len; +struct uip_eth_addr uip_ethaddr = { .addr = {0x02,0x11,0x22,0x33,0x44,0x55} }; +const struct machine machine = { .min_port = 0, .max_port = 7, + .log_to_phys_port = {5, 1, 2, 3, 4, 6, 7, 8, 9} }; +struct machine_runtime machine_detected = { .isRTL8373 = 1 }; +uint8_t lacpEnabled; +uint16_t management_vlan; /* mocked; slow-protocol path zeroes it per-frame */ + +static int verbose = 0; +void print_string(char *s) { if (verbose) fputs(s, stdout); } +void print_byte(uint8_t b) { if (verbose) printf("%02x", b); } +void print_short(uint16_t v) { if (verbose) printf("%04x", v); } +void write_char(char c) { if (verbose) putchar(c); } + +/* Trunk programming mock: record last mask per LAG + call count. + * hw_members mirrors LAG 0 so the single-aggregator scenarios read naturally. */ +static uint16_t hw_members_lag[4]; +static int hw_set_calls; +#define hw_members hw_members_lag[0] +void port_lag_members_set(uint8_t lag, uint16_t members) +{ + if (lag < 4) + hw_members_lag[lag] = members; + hw_set_calls++; +} + +/* Reserved-multicast action mock: delivery of a LACPDU to the CPU rests on + * the action of the slow-protocols entry alone, so record what the engine + * sets. The real trap_rma_set() touches only the action field. */ +static uint16_t hw_rma_reg; +static uint8_t hw_rma_act; +static int hw_rma_calls; +void trap_rma_set(uint16_t reg, uint8_t act); +void trap_rma_set(uint16_t reg, uint8_t act) +{ + hw_rma_reg = reg; + hw_rma_act = act; + hw_rma_calls++; +} + +/* ---------- on-wire mirrors (independent re-statement of the layout) ---------- */ + +struct wire_info { + uint16_t sys_prio; + uint8_t sys[6]; + uint16_t key; + uint16_t port_prio; + uint16_t port; + uint8_t state; + uint8_t reserved[3]; +}; + +struct wire_pdu_out { /* what lacp_send() writes at uip_buf+12 */ + uint8_t dst[6]; + uint8_t src[6]; + struct rtl_tag rtl_tag; + uint16_t ethertype; + uint8_t subtype; + uint8_t version; + uint8_t tlv_actor, actor_len; + struct wire_info actor; + uint8_t tlv_partner, partner_len; + struct wire_info partner; + uint8_t tlv_collector, collector_len; + uint16_t collector_max_delay; + uint8_t collector_reserved[12]; + uint8_t tlv_terminator, terminator_len; + uint8_t terminator_reserved[50]; +}; + +struct wire_pdu_in { /* forwarded form: the hardware inserts a VLAN tag */ + uint8_t dst[6]; + uint8_t src[6]; + struct rtl_tag rtl_tag; + struct vlan_tag vlan_tag; + uint16_t ethertype; + uint8_t subtype; + uint8_t version; + uint8_t tlv_actor, actor_len; + struct wire_info actor; + uint8_t tlv_partner, partner_len; + struct wire_info partner; +}; + +_Static_assert(sizeof(struct wire_info) == 18, "info TLV body must be 18"); +_Static_assert(sizeof(struct wire_pdu_out) == 132, "TX pdu must be 132"); +_Static_assert(sizeof(struct rtl_tag) == 8, "rtl tag must be 8"); + +/* ---------- TX capture ---------- */ + +static struct wire_pdu_out last_tx[10]; +static int tx_count[10]; + +void tcpip_output(void) +{ + struct wire_pdu_out *p = (struct wire_pdu_out *)&uip_buf[RTL_FRAME_DESC_SIZE]; + uint16_t pmask = HTONS(p->rtl_tag.pmask); + for (int port = 0; port < 10; port++) { + if (pmask & (1u << port)) { + last_tx[port] = *p; + tx_count[port]++; + } + } + uip_len = 0; +} + +/* ---------- simulated partner ---------- */ + +/* Deliver a LACPDU into the switch on `port`, from partner system `sys`. + * pstate = partner's Actor_State flags. If `echo`, the partner echoes the + * switch's last-seen actor block back (fresh view) - required for quiescence. */ +static void partner_frame_full(uint8_t port, const uint8_t sys[6], uint8_t pstate, + int echo, uint16_t len, int tagged) +{ + struct wire_pdu_in in; + memset(&in, 0, sizeof(in)); + + in.dst[0]=0x01; in.dst[1]=0x80; in.dst[2]=0xc2; in.dst[5]=0x02; + memcpy(in.src, sys, 6); + in.rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); + in.rtl_tag.pmask = HTONS(port); /* RX: 4-bit ingress port */ + in.vlan_tag.svlan = HTONS(0x8100); /* the tag's TPID, which marks the body offset */ + in.ethertype = HTONS(0x8809); + in.subtype = 0x01; + in.version = 0x01; + + in.tlv_actor = 0x01; in.actor_len = 0x14; + in.actor.sys_prio = HTONS(0x8000); + memcpy(in.actor.sys, sys, 6); + in.actor.key = HTONS(0x0011); + in.actor.port_prio = HTONS(0x00ff); + in.actor.port = HTONS((uint16_t)port + 101); + in.actor.state = pstate; + + in.tlv_partner = 0x02; in.partner_len = 0x14; + if (echo) { /* fresh view of us, from our last TX */ + in.partner.sys_prio = last_tx[port].actor.sys_prio; + memcpy(in.partner.sys, last_tx[port].actor.sys, 6); + in.partner.key = last_tx[port].actor.key; + in.partner.port = last_tx[port].actor.port; + in.partner.state = last_tx[port].actor.state; + } /* else: zeros = stale view */ + + memcpy(uip_buf, &in, sizeof(in)); + if (!tagged) { /* trapped: no VLAN tag, so the body sits earlier */ + uint8_t *body = uip_buf + RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE; + memmove(body, body + VLAN_TAG_SIZE, + sizeof(in) - (RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE + VLAN_TAG_SIZE)); + len -= VLAN_TAG_SIZE; + } + uip_len = len; + lacp_in(); +} + +static void partner_frame_len(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo, uint16_t len) +{ + partner_frame_full(port, sys, pstate, echo, len, 1); +} + +static void partner_frame(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo) +{ + partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 1); +} + +/* The same LACPDU as the reserved-multicast trap delivers it. */ +static void partner_frame_trap(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo) +{ + partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 0); +} + +/* Advance time by n system ticks; the LACP timers step every fourth one */ +volatile uint32_t ticks; +static void step(int n) { while (n--) { ticks++; lacp_timers(); } } + +/* ---------- scenario runner ---------- */ + +static int failures; +#define CHECK(cond, name) do { \ + if (cond) printf("PASS %s\n", name); \ + else { printf("FAIL %s\n", name); failures++; } \ +} while (0) + +#define P_ACT LACP_STATE_ACTIVITY +#define P_AGG LACP_STATE_AGGREGATION +#define P_SYNC LACP_STATE_SYNC +#define P_COL LACP_STATE_COLLECTING +#define P_DIST LACP_STATE_DISTRIBUTING +#define P_TO LACP_STATE_TIMEOUT /* partner requests short (fast) timeout */ + +static const uint8_t SYS_A[6] = {0x02,0xaa,0xaa,0xaa,0xaa,0x01}; +static const uint8_t SYS_B[6] = {0x02,0xbb,0xbb,0xbb,0xbb,0x02}; + +int main(int argc, char **argv) +{ + verbose = (argc > 1 && !strcmp(argv[1], "-v")); + + /* Boot init first: on host, zeroed globals would otherwise read as "every + * port belongs to LAG 0" (the firmware calls this from main() the same way) */ + lacp_init(); + + /* T1: enable announces on every port with sane field contents */ + lacp_lag_set(0, 0x00ff); + step(8); + int all_tx = 1, sane = 1; + for (int i = machine.min_port; i <= machine.max_port; i++) { + if (!tx_count[i]) all_tx = 0; + if (last_tx[i].ethertype != HTONS(0x8809) || last_tx[i].subtype != 1 + || last_tx[i].dst[5] != 0x02 + || !(last_tx[i].actor.state & (P_ACT|P_AGG|P_TO))) + sane = 0; + } + CHECK(all_tx && sane, "T1 enable: LACPDU announced on all ports, well-formed"); + + /* T2: partner without SYNC -> actor SYNC only, no members yet */ + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO, 1); + partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO, 1); + step(8); + CHECK((lacp_actor_state[0] & P_SYNC) && !(lacp_actor_state[0] & (P_COL|P_DIST)) + && hw_members == 0, + "T2 partner not in sync: actor SYNC only, trunk empty"); + + /* T3: partner in sync on ports 0,1 -> full converge, members 0x0003 */ + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + CHECK(hw_members == 0x0003 + && (lacp_actor_state[0] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST) + && (lacp_actor_state[1] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST), + "T3 convergence: both ports collecting/distributing, trunk 0x0003"); + + /* T3b: partner-block echo must be VERBATIM. A Linux 802.3ad partner accepts + * our SYNC only if our Partner TLV mirrors its actor identity exactly; a + * hardcoded sys_prio/port_prio (instead of the recorded value) makes Linux + * clear partner-SYNC and the bond never distributes. Verify we echo the + * partner's own priorities back, not a constant. */ + CHECK(HTONS(last_tx[0].partner.sys_prio) == 0x8000 + && HTONS(last_tx[0].partner.port_prio) == 0x00ff + && HTONS(last_tx[0].partner.key) == 0x0011 + && HTONS(last_tx[0].partner.port) == (uint16_t)0 + 101, + "T3b partner echo: recorded partner priorities/key/port echoed verbatim"); + + /* T4: mis-cabling - port 2 sees a DIFFERENT system: must stay out */ + partner_frame(2, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + CHECK(hw_members == 0x0003 && !(lacp_actor_state[2] & P_SYNC), + "T4 mis-cabling: port with different partner system stays out"); + + /* T5: register-write economy: stable state must not rewrite the trunk */ + int calls_before = hw_set_calls; + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(64); + CHECK(hw_set_calls == calls_before, + "T5 stable state: no redundant trunk register writes"); + + /* T6: stale echo triggers retransmit (update_NTT) */ + int txc = tx_count[0]; + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 0 /* stale view of us */); + step(8); + CHECK(tx_count[0] > txc, "T6 update_NTT: stale partner view causes retransmit"); + + /* T7: expiry - partner silent past the short timeout: trunk drains, + * aggregator identity is released */ + step(4 * LACP_SHORT_TIMEOUT + 64); /* past the short timeout */ + CHECK(hw_members == 0 && lacp_agg_valid[0] == 0, + "T7 expiry: members drop to 0 and aggregator is released"); + + /* T8: re-convergence with a NEW partner system after release */ + /* (needs a fresh announce first so the echo below carries current state) */ + step(4 * 0x0100 + 8); /* let periodic TX refresh last_tx */ + partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + CHECK(hw_members == 0x0008 && (lacp_actor_state[3] & P_COL), + "T8 re-election: new partner system forms a fresh aggregate"); + + /* T9: lacp off clears the trunk */ + lacp_off(); + CHECK(hw_members == 0 && lacpEnabled == 0, "T9 disable: trunk cleared"); + + /* ---- per-LAG scenarios: two independent aggregators ---- */ + + /* T10: lag 1 lacp {0,1} + lag 2 lacp {2,3}, both to the SAME partner + * system (worst case for sibling scoping): each converges into its own + * hardware trunk, and the engine reports enabled. */ + lacp_lag_set(1, 0x0003); /* ports 0,1 */ + lacp_lag_set(2, 0x000c); /* ports 2,3 */ + step(8); /* announce */ + for (int r = 0; r < 2; r++) { /* two rounds so echoes carry fresh state */ + for (int p = 0; p < 4; p++) + partner_frame(p, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + } + CHECK(lacpEnabled == 1 + && hw_members_lag[1] == 0x0003 && hw_members_lag[2] == 0x000c, + "T10 two LAGs: independent aggregates converge on their own trunks"); + + /* T11: removing one LAG releases only its trunk; the other keeps running */ + lacp_lag_set(1, 0); + CHECK(hw_members_lag[1] == 0 && hw_members_lag[2] == 0x000c + && lacpEnabled == 1, + "T11 remove one LAG: its trunk drains, the other LAG unaffected"); + + /* T12: a port in no LACP LAG ignores LACPDUs entirely */ + int rx5_before = tx_count[5]; + partner_frame(5, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + CHECK(!(lacp_actor_state[5] & P_SYNC) && hw_members_lag[2] == 0x000c, + "T12 unassigned port: LACPDU ignored, no state change"); + (void)rx5_before; + + /* T13: removing the last LACP LAG shuts the engine down */ + lacp_lag_set(2, 0); + CHECK(lacpEnabled == 0 && hw_members_lag[2] == 0 + && hw_rma_reg == RTL837X_RMA2_CONF && hw_rma_act == RTL837X_RMA_ACT_DROP, + "T13 last LAG removed: engine off, trunk drained, address dropped"); + + /* T14: while the engine runs, the slow-protocols address is trapped to + * the CPU. The trap keys on the address alone, so delivery needs no L2 + * entry and nothing written per VLAN or per PVID. */ + hw_rma_calls = 0; + lacp_lag_set(0, 0x0003); + CHECK(hw_rma_calls == 1 && hw_rma_reg == RTL837X_RMA2_CONF + && hw_rma_act == RTL837X_RMA_ACT_TRAP, + "T14 engine on traps 01:80:c2:00:00:02 to the CPU"); + + /* T15: a trapped LACPDU arrives without the VLAN tag a forwarded one + * carries, so its body sits four bytes earlier. Both forms must parse. */ + uint16_t rx_before = lacp_rx_count[0]; + lacp_partner_state[0] = 0; /* all three are written past the ethertype gate */ + memset(lacp_partner_sys[0], 0, 6); + partner_frame_trap(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + int trap_ok = lacp_rx_count[0] == (uint16_t)(rx_before + 1) + && lacp_partner_state[0] == (P_ACT|P_AGG|P_TO|P_SYNC) + && !memcmp(lacp_partner_sys[0], SYS_A, 6); + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + CHECK(trap_ok && lacp_rx_count[0] == (uint16_t)(rx_before + 2), + "T15 trapped LACPDU parses like a forwarded one"); + + /* T16: the partner changes one cable at a time. Port 1 starts hearing B + * while port 0 still hears A, then A goes quiet and B appears on port 0 + * too. The aggregator must let go of A once no port hears it, even though + * port 1 was CURRENT the whole time, or the LAG never aggregates with B. */ + lacp_lag_set(0, 0); + lacp_lag_set(0, 0x0003); + step(8); + for (int r = 0; r < 3; r++) { + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + } + for (int t = 0; t < 40; t++) { /* port 1 moved to B, port 0 still on A */ + partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(4 * 40); + } + int stayed_out = hw_members == 0x0001 && !(lacp_actor_state[1] & P_SYNC); + for (int t = 0; t < 60; t++) /* port 0 unplugged from A, past the short timeout */ + { partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); step(4 * 40); } + for (int t = 0; t < 40; t++) { /* port 0 now on B as well */ + partner_frame(0, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(4 * 40); + } + CHECK(stayed_out && hw_members == 0x0003 && !memcmp(lacp_agg_sys[0], SYS_B, 6), + "T16 re-cabling one port at a time: aggregator moves from A to B"); + + /* T17: a group that already has a STATIC trunk is put under LACP with the + * same ports (the LAG page's Static -> LACP switch sends only "lag N lacp"). + * The static trunk must go: until the partner converges the ports are not + * an aggregate, and after a reboot the config replays only the LACP line. */ + hw_members_lag[1] = 0x0030; /* static lag 2: ports 4,5 */ + lacp_lag_set(1, 0x0030); + step(8); + CHECK(hw_members_lag[1] == 0 && lacp_members_last[1] == 0 && lacp_lag_ports[1] == 0x0030, + "T17 static trunk taken over: cleared while the group negotiates"); + lacp_lag_set(1, 0); + + /* T18: a port named in a second group moves there. The first group's + * candidate set drops it, so taking the second group out later leaves no + * orphan that group 1 still shows as a candidate but never runs LACP on. */ + lacp_lag_set(1, 0x0030); /* ports 4,5 */ + step(8); + for (int r = 0; r < 2; r++) { + partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + } + int converged = hw_members_lag[1] == 0x0030; + lacp_lag_set(2, 0x0060); /* ports 5,6: port 5 moves from group 2 to 3 */ + int moved = lacp_lag_ports[1] == 0x0010 && lacp_port_lag[5] == 2 + && hw_members_lag[1] == 0x0010; + lacp_lag_set(2, 0); + int no_orphan = lacp_port_lag[5] == LACP_LAG_NONE && lacp_lag_ports[1] == 0x0010; + lacp_lag_set(3, 0x0010); /* the last port leaves group 2 as well */ + CHECK(converged && moved && no_orphan && lacp_lag_ports[1] == 0 && lacp_agg_valid[1] == 0, + "T18 port moves between groups: old group drops it from candidates and trunk"); + lacp_lag_set(3, 0); + + /* T20: taking a group out of LACP leaves nothing of the partner behind + * for "lacp show" to report as if the port still heard it. */ + lacp_lag_set(1, 0x0030); + step(8); + partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + int had_partner = lacp_rx_state[4] == LACP_RX_CURRENT && lacp_rx_count[4] > 0; + lacp_lag_set(1, 0); + CHECK(had_partner && lacp_partner_state[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE + && lacp_rx_count[4] == 0 && !memcmp(lacp_partner_sys[4], "\0\0\0\0\0\0", 6), + "T20 release: partner state, rx state, counter and system cleared"); + + /* T21: applying the same candidate set again (the LAG page resends it + * with every Apply) must not tear the trunk down and renegotiate. */ + lacp_lag_set(1, 0x0030); + step(8); + for (int r = 0; r < 2; r++) { + partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1); + step(8); + } + int before = hw_set_calls; + lacp_lag_set(1, 0x0030); + CHECK(hw_members_lag[1] == 0x0030 && hw_set_calls == before + && (lacp_actor_state[4] & P_COL), + "T21 same ports applied again: trunk and state untouched"); + lacp_lag_set(1, 0); + + /* T22: a runt on the slow-protocols address must not be parsed as a + * LACPDU; the partner block would be read from stale buffer bytes. */ + lacp_lag_set(1, 0x0030); + step(8); + partner_frame_len(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1, 40); /* cut before the partner block */ + CHECK(lacp_rx_count[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE, + "T22 truncated LACPDU ignored: the port does not record it"); + lacp_lag_set(1, 0); + + printf("\n%s (%d failure%s)\n", failures ? "SANDBOX: FAILURES" : "SANDBOX: ALL PASS", + failures, failures == 1 ? "" : "s"); + return failures ? 1 : 0; +} diff --git a/test/lacp/shim/machine.h b/test/lacp/shim/machine.h new file mode 100644 index 00000000..7f6612fb --- /dev/null +++ b/test/lacp/shim/machine.h @@ -0,0 +1,12 @@ +/* Host-build shim: rtl837x_lacp.c reads min_port/max_port and, for the + * console dump, the front-panel numbering. The real map is not the identity + * on any machine, so the harness uses a shuffled one to keep a logical index + * from passing as a physical port by accident. */ +#ifndef _SHIM_MACHINE_H_ +#define _SHIM_MACHINE_H_ +#include +struct machine { uint8_t min_port; uint8_t max_port; uint8_t log_to_phys_port[9]; }; +extern const struct machine machine; +struct machine_runtime { uint8_t isRTL8373; }; +extern struct machine_runtime machine_detected; +#endif diff --git a/test/lacp/shim/rtl837x_common.h b/test/lacp/shim/rtl837x_common.h new file mode 100644 index 00000000..b21c7141 --- /dev/null +++ b/test/lacp/shim/rtl837x_common.h @@ -0,0 +1,63 @@ +/* + * Host-build shim for rtl837x_common.h - just enough for rtl837x_lacp.c. + * Layouts of rtl_tag/vlan_tag and the frame-desc size mirror the real header; + * the harness static-asserts the resulting on-wire struct sizes. + */ +#ifndef _SHIM_RTL837X_COMMON_H_ +#define _SHIM_RTL837X_COMMON_H_ + +#include +#include /* memcpy/memset - firmware provides its own */ + +struct rtl_tag { + uint16_t tag; + uint8_t version; + uint8_t reason; + uint16_t flags; + uint16_t pmask; /* TX: port bitmask; RX: 4-bit port number */ +}; + +struct vlan_tag { + uint16_t svlan; + uint16_t vlan; +}; + +#define RTL_TAG_SIZE (sizeof (struct rtl_tag)) +#define VLAN_TAG_SIZE (sizeof (struct vlan_tag)) +#define RTL_FRAME_TAG_ID 0x8899 +#define RTL_FRAME_TAG_VERSION 0x04 +#define RTL_FRAME_DESC_SIZE 12 +#define ETHER_HEADER_SIZE 14 + +#define HTONS(n) ((uint16_t)((((uint16_t)(n)) << 8) | (((uint16_t)(n)) >> 8))) + +/* Console output - provided by the harness */ +void print_string(char *s); +void print_byte(uint8_t b); +void print_short(uint16_t v); +void write_char(char c); + +#endif + +/* Host-build shim: port masks used by lacp_isolation_update() */ +#ifndef PMASK_CPU +#define CPU_PORT 9 +#define PMASK_9 0x1ff +#define PMASK_6 0x1f8 +#define PMASK_CPU 0x200 +#endif + +/* Host-build shim: directed-egress/strip-tag bit (bit15 of TX pmask word) */ +#ifndef RTL_TAG_ALLOW +#define RTL_TAG_ALLOW 0x8000 +#endif + +/* Host-build shim: tag flags word bits (tag_rtl8_4) */ +#ifndef RTL_TAG_LEARN_DIS +#define RTL_TAG_LEARN_DIS 0x0020 +#define RTL_TAG_KEEP 0x0080 +#define SYS_TICK_HZ 200 +extern volatile uint32_t ticks; +#define TICKS_DUE(last, period) ((uint8_t)((uint8_t)ticks - (last)) >= (period)) + +#endif diff --git a/test/lacp/shim/rtl837x_port.h b/test/lacp/shim/rtl837x_port.h new file mode 100644 index 00000000..646d5253 --- /dev/null +++ b/test/lacp/shim/rtl837x_port.h @@ -0,0 +1,7 @@ +/* Host-build shim: the harness mocks trunk programming and records calls */ +#ifndef _SHIM_RTL837X_PORT_H_ +#define _SHIM_RTL837X_PORT_H_ +#include +void port_lag_members_set(uint8_t lag, uint16_t members); +void port_isolate(uint8_t port, uint16_t pmask); +#endif diff --git a/test/lacp/shim/rtl837x_regs.h b/test/lacp/shim/rtl837x_regs.h new file mode 100644 index 00000000..2dc92d8d --- /dev/null +++ b/test/lacp/shim/rtl837x_regs.h @@ -0,0 +1,19 @@ +/* + * Host-build shim for rtl837x_regs.h - the register numbers rtl837x_lacp.c + * touches, with the values from the real header so a wrong constant shows up + * as a failing expectation rather than as a silent no-op. + */ +#ifndef _SHIM_RTL837X_REGS_H_ +#define _SHIM_RTL837X_REGS_H_ + +#include + +#define RTL837X_RMA2_CONF 0x4ed4 +#define RTL837X_RMA_ACT_TRAP 0x10 +#define RTL837X_RMA_ACT_DROP 0x20 + +/* Registers touched by lacp_send's CPU-port targeting */ +#define RTL837X_PORT_ISOLATION_BASE 0x50c0 +#define RTL837X_REG_NIC_TXCMD 0x7850 + +#endif diff --git a/test/lacp/shim/uip.h b/test/lacp/shim/uip.h new file mode 100644 index 00000000..0d5a51ec --- /dev/null +++ b/test/lacp/shim/uip.h @@ -0,0 +1,9 @@ +/* Host-build shim for uip.h - frame buffer + TX hook used by rtl837x_lacp.c */ +#ifndef _SHIM_UIP_H_ +#define _SHIM_UIP_H_ +#include +#define UIP_CONF_BUFFER_SIZE 600 +struct uip_eth_addr { uint8_t addr[6]; }; +extern uint16_t uip_len; +void tcpip_output(void); +#endif diff --git a/test/port_hash/.gitignore b/test/port_hash/.gitignore new file mode 100644 index 00000000..cf8f788c --- /dev/null +++ b/test/port_hash/.gitignore @@ -0,0 +1,2 @@ +test_lag_hash +port_lag_members_set.inc diff --git a/test/port_hash/Makefile b/test/port_hash/Makefile new file mode 100644 index 00000000..feeee940 --- /dev/null +++ b/test/port_hash/Makefile @@ -0,0 +1,6 @@ +test: port_lag_members_set.inc + gcc -Wall -Wextra -O1 -o test_lag_hash test_lag_hash.c && ./test_lag_hash +port_lag_members_set.inc: ../../rtl837x_port.c + awk '/^void port_lag_members_set/,/^}/' $< > $@ +clean: + rm -f test_lag_hash port_lag_members_set.inc diff --git a/test/port_hash/test_lag_hash.c b/test/port_hash/test_lag_hash.c new file mode 100644 index 00000000..205d3fc8 --- /dev/null +++ b/test/port_hash/test_lag_hash.c @@ -0,0 +1,68 @@ +/* Host test for port_lag_members_set(): the default load-balancing hash must be + * seeded into the CURRENT trunk's register (BASE + lag*4), never trunk 0's, and + * must not overwrite a hash that is already set. Compiles the REAL function + * (extracted verbatim from rtl837x_port.c) against register-recording shims. */ +#include +#include +#include + +#define __banked +#define __xdata +#define __code + +/* register defs (mirror rtl837x_regs.h) */ +#define RTL837X_TRK_MBR_CTRL_BASE 0x4f38 +#define RTL837X_TRK_HASH_CTRL_BASE 0x4f48 +#define LAG_HASH_SOURCE_PORT_NUMBER 0x01 +#define LAG_HASH_L2_SMAC 0x02 +#define LAG_HASH_L2_DMAC 0x04 +#define LAG_HASH_L3_SIP 0x08 +#define LAG_HASH_L3_DIP 0x10 +#define LAG_HASH_L4_SPORT 0x20 +#define LAG_HASH_L4_DPORT 0x40 +#define LAG_HASH_DEFAULT (LAG_HASH_L2_SMAC|LAG_HASH_L2_DMAC|LAG_HASH_L3_SIP|LAG_HASH_L3_DIP|LAG_HASH_L4_SPORT|LAG_HASH_L4_DPORT) +#define LAG_HASH_RESET (LAG_HASH_SOURCE_PORT_NUMBER|LAG_HASH_L2_SMAC|LAG_HASH_L2_DMAC|LAG_HASH_L3_SIP|LAG_HASH_L3_DIP|LAG_HASH_L4_SPORT) + +uint8_t SFR_DATA_24, SFR_DATA_16, SFR_DATA_8, SFR_DATA_0; +uint8_t sfr_data[4]; +static uint32_t reg_store[0x10000]; + +void reg_read_m(uint16_t r){ uint32_t v=reg_store[r]; + sfr_data[0]=(v>>24)&0xff; sfr_data[1]=(v>>16)&0xff; sfr_data[2]=(v>>8)&0xff; sfr_data[3]=v&0xff; } +void reg_write(uint16_t r){ reg_store[r]=((uint32_t)SFR_DATA_24<<24)|((uint32_t)SFR_DATA_16<<16)|((uint32_t)SFR_DATA_8<<8)|SFR_DATA_0; } +void print_string(char*s){(void)s;} void print_byte(uint8_t b){(void)b;} +void print_short(uint16_t s){(void)s;} void write_char(char c){(void)c;} + +#define REG_SET(r,v) do{ SFR_DATA_24=((uint32_t)(v)>>24)&0xff; SFR_DATA_16=((uint32_t)(v)>>16)&0xff; \ + SFR_DATA_8=((uint32_t)(v)>>8)&0xff; SFR_DATA_0=(v)&0xff; reg_write(r); }while(0) +#define REG_WRITE(r,a,b,c,d) do{ SFR_DATA_24=(a); SFR_DATA_16=(b); SFR_DATA_8=(c); SFR_DATA_0=(d); reg_write(r); }while(0) + +/* THE REAL FUNCTION (extracted from rtl837x_port.c at build time) */ +#include "port_lag_members_set.inc" + +static int fails; +#define CK(cond,msg,...) do{ if(cond){printf("PASS " msg "\n",##__VA_ARGS__);} else {printf("FAIL " msg "\n",##__VA_ARGS__); fails++;} }while(0) + +int main(void){ + for(int lag=0; lag<4; lag++){ + memset(reg_store,0,sizeof(reg_store)); + port_lag_members_set(lag, 0x0180); + uint16_t h = RTL837X_TRK_HASH_CTRL_BASE + (lag<<2); + uint16_t m = RTL837X_TRK_MBR_CTRL_BASE + (lag<<2); + CK(reg_store[h]==LAG_HASH_DEFAULT, "lag%d: default 0x%02x -> hash reg 0x%04x", lag, LAG_HASH_DEFAULT, h); + CK(reg_store[m]==0x0180, "lag%d: members 0x0180 -> mbr reg 0x%04x", lag, m); + if(lag) CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE]==0, "lag%d: trunk-0 hash NOT clobbered", lag); + } + /* guard: existing hash must survive a later member change */ + memset(reg_store,0,sizeof(reg_store)); + reg_store[RTL837X_TRK_HASH_CTRL_BASE+(1<<2)] = 0x06; /* lag1 already L2 */ + port_lag_members_set(1, 0x00c0); + CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE+(1<<2)]==0x06, "guard: preset lag1 hash 0x06 preserved"); + /* the chip's reset value counts as unset and is replaced by the default */ + memset(reg_store,0,sizeof(reg_store)); + reg_store[RTL837X_TRK_HASH_CTRL_BASE+(2<<2)] = LAG_HASH_RESET; + port_lag_members_set(2, 0x0003); + CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE+(2<<2)]==LAG_HASH_DEFAULT, "reset value 0x%02x replaced by default on lag2", LAG_HASH_RESET); + printf(fails? "\n=== FAIL (%d) ===\n" : "\n=== ALL PASS ===\n", fails); + return fails?1:0; +} diff --git a/test/test_httpd_tx.c b/test/test_httpd_tx.c index 4df05472..3f056030 100644 --- a/test/test_httpd_tx.c +++ b/test/test_httpd_tx.c @@ -148,6 +148,7 @@ void send_mirror(void) { } void send_mtu(void) { } void send_config(void) { } void send_cmd_log(void) { } +void send_lacp(void) { } void send_lag(void) { } void send_stp(void) { } void send_stp_counters(void) { }