diff --git a/Makefile b/Makefile index 46568abe..af14d987 100644 --- a/Makefile +++ b/Makefile @@ -150,10 +150,12 @@ SRCS += \ rtl837x_lldp.c \ rtl837x_storm.c \ rtl837x_lacp.c \ - rtl837x_trap.c + rtl837x_trap.c \ + rtl837x_stp_cli.c SRCS += \ httpd/httpd.c \ - httpd/page_impl.c + httpd/page_impl.c \ + httpd/page_stp.c SRCS += \ uip/timer.c \ uip/uip.c \ @@ -246,7 +248,7 @@ $(BUILDDIR)/%.rel: %.asm $(CCFLAGS_STAMP) | create_build_dir # mv -f $(addprefix $(basename $^), .lst .rel .sym) . $(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtbank.rel $(BUILDDIR)/crc16.rel - $(CC) $(CC_FLAGS) --xram-size 49151 -Wl-bHOME=0x00000 -Wl-bBANK1=0x14000 -Wl-bBANK2=0x24000 -Wl-bBANK3=0x34000 -Wl-r -o $@ $^ + $(CC) $(CC_FLAGS) --xram-size 49151 -Wl-bHOME=0x00000 -Wl-bBANK1=0x14000 -Wl-bBANK2=0x24000 -Wl-bBANK3=0x34000 -Wl-bBANK4=0x44000 -Wl-r -o $@ $^ $(BUILDDIR)/rtlplayground.img: $(BUILDDIR)/rtlplayground.ihx objcopy --input-target=ihex -O binary $< $@ diff --git a/doc/stp.md b/doc/stp.md index 96368535..543f8928 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -2,9 +2,11 @@ The switch can take part in a spanning tree (IEEE 802.1D / 802.1w) so that redundant links between bridges are blocked instead of forming a loop. The -implementation elects a root bridge from the BPDUs it receives, promotes ports -to forwarding once their listen period expires, ages the root out when it goes -silent, and blocks a port on which it sees its own BPDU. +implementation elects a root bridge from the BPDUs it receives, picks the +cheapest path to it as the root port, blocks ports on which a better bridge is +already designated, promotes ports to forwarding once their listen period +expires, drops information a neighbour stopped sending, and blocks a port on +which it sees its own BPDU. STP can be enabled and controlled via the web interface or the command line, as follows: @@ -64,32 +66,6 @@ LACPDUs, delivered by forward, stopped arriving on port 3, and BPDUs sent into the same port, delivered by trap, kept arriving. A port that admits tagged frames only therefore takes part in STP. -## Link aggregation - -A LAG (Link Aggregation Group) is handled like an additional port by the STP -protocol, with its own timers and states. The switch devices support up to 4 -LAGs, which are shown alongside the physical ports in the STP status. A group -carries its own path cost, priority, edge and guard settings, and gets its own -row on the Spanning Tree page. - -``` -stp lag 1 cost 10000 # the group decides, not its members -stp lag 1 edge off -``` - -LAGs are configured via the `lag` command. Once a port is a member of a LAG it -can no longer be configured individually for STP, so `stp port ` on a member -tells you which group to configure instead. Membership is re-read from the -aggregation registers once a second, so a group changing under LACP is picked -up without any coordination between the two. - -The switch hardware does not handle STP state for a LAG as a whole. State -changes for the members have to be done by the firmware, updating every member -port, which is possible using a single register write. BPDUs go out through the -lowest member and carry the group's own port id. Losing one member of a live -LAG is not a topology change; the logical port only goes down with its last -link. - Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port: `00` disabled, `01` blocking, `10` learning, `11` forwarding. A port in blocking forwards nothing between ports, but it still sends what the CPU @@ -109,19 +85,81 @@ encodes. All configured values are in seconds: | `stp fwd ` | 15 | 4–30 | | `stp txhold ` | 6 | 1–10 | -A port entering the tree spends `fwd` seconds in blocking before it forwards -(an edge port skips the wait). Root information is discarded after `maxage` -seconds without a BPDU, and the switch then reclaims the root role. +802.1D also requires `2 * (fwd - 1) >= maxage >= 2 * (hello + 1)`. A +command typed on the console or sent by the web page that would break it is +refused and the value stays as it was, so change the values in an order that +keeps the rule at every step (for a larger max age raise the forward delay +first). The web page shows the error, and a refused command is not recorded +in the command history, so saving the configuration does not pick it up. Lines of the saved configuration are taken one by one whatever their +order, and `stp on` checks the result: if the rule is broken it warns and +starts with 2/20/15 instead. + +The values above are what the switch uses while it is the root. Otherwise it +takes max age and forward delay from the BPDUs on its root port, as 802.1D +requires, and passes them on in its own BPDUs with the message age one second +higher; the hello time stays its own. + +A port entering the tree discards for one forward delay, then learns +addresses without forwarding for a second one, and only then forwards (an +edge port skips the wait). In RSTP mode on a point-to-point link +(`p2p auto` or `on`) it can go faster: a designated port that is still +discarding sets the proposal flag, and forwards as soon as the bridge on the +other side answers with an agreement. When the root port receives a proposal, +the switch first puts its other non-edge designated ports back to discarding +(they propose in turn), then forwards the root port and answers with an +agreement; this sync happens once per root port, later proposals are only +answered. An alternate or backup port that receives a proposal does the same +sync once for the information it holds and answers with an agreement in a +BPDU with the alternate role, so the bridge on the other side does not have +to wait out its timers; it stays discarding itself. An agreement is accepted +from a root or an alternate port on the other side. A new root port forwards at once, after every port that was root within +the last forward delay has been put back to discarding; such a recent root +port stays closed, even to an agreement, until that forward delay is over. +A new root port that was a backup port within the last two hello times does +not take the fast path and waits for its timers. + +A designated port that hears worse information from a bridge that still +claims to be designated and learning or forwarding on the same link has a +dispute: usually one direction of the link is lost and the other bridge does +not hear this switch's BPDUs. The port goes back to discarding, and every +further such BPDU restarts its listen period, so it does not forward while +the dispute lasts. + +With `stp version rstp` each port still talks 802.1D to a neighbour that +does: once a Config BPDU or TCN arrives and the migrate time (3 s since the +port came up or last switched) is over, the port sends Config BPDUs, uses +the 802.1D TC window and reports changes with TCN if it is the root port. An +RST BPDU after the migrate time switches it back, and so does a link that +comes up again or `stp port mcheck`. Any worse information heard on a designated port, disputed or +not, is answered with a BPDU at once instead of at the next hello, so the +other bridge learns sooner that it is not the designated one. Information heard on a port stays +valid for three of the sender's hello times; a BPDU whose message age has +reached its max age is not used at all. When the root port's information +expires the switch moves to the next best port, or reclaims the root role if +there is none. + +Among ports offering the same root, the lowest root path cost wins, then the +designated bridge, the designated port ID, and finally this switch's own port +ID. A worse BPDU from a different bridge does not replace what a port holds; +a worse one from the same designated bridge and port does. ## Topology changes -A change on a local non-edge port (the link coming or going, a port promoted -to forwarding) flushes the addresses learned on it and sets the TC flag in -our BPDUs for `maxage + fwd` seconds. A TC flag received in a BPDU is passed -on: the switch flushes the other non-edge ports once and keeps the flag in -its own BPDUs until one hello after the last flagged frame, so the -notification crosses the switch instead of dying at it. A legacy TCN is -acknowledged with TCA and then treated like a local change. +A topology change is detected when a non-edge port enters forwarding. That +port, and every other forwarding non-edge port, then carries the TC flag in +its BPDUs for hello time plus one second (max age plus forward delay with +`stp version stp`), and each of the other ports flushes the addresses it +learned. The root port takes part too: while its TC window runs it sends +BPDUs toward the root, so the change reaches the rest of the network. With +`stp version stp` the root port sends TCN instead, until a Config BPDU with +TCA comes back. + +A TC flag received on a forwarding root or designated port is passed on the +same way to the other ports, not back to the port it came in on; a port that +is still discarding or learning drops it. A legacy TCN is acknowledged with +TCA and then treated like a received TC. A port that turns +alternate, loses its link, or is blocked for a loop flushes its own addresses +without raising a topology change, and an edge port never raises one. ## Bridge settings @@ -129,6 +167,8 @@ acknowledged with TCA and then treated like a local change. stp prio <0-15> # bridge priority = n * 4096, default 8 (32768) stp version rstp|stp # RST BPDUs (default) or legacy Config BPDUs stp hello|maxage|fwd|txhold +stp pathcost long|short # automatic port costs, 802.1D-2004 (default) or 802.1D-1998 +stp bpdu flood|filter # BPDUs while STP is off: pass them on (default) or drop them ``` The bridge with the lowest priority wins the root election; ties are broken by @@ -136,6 +176,17 @@ the MAC address. If you do not want this switch to become the root of an existing network, give it a worse priority than the current root — `stp prio 15` (61440) is the usual "never me" value. +A port without a configured cost takes it from its link speed. The long +method uses the 802.1D-2004 values (20000 for 1G, 2000 for 10G); the short +one the 802.1D-1998 table for bridges that still use 16-bit costs (100 for +10M, 19 for 100M, 4 for 1G, 2 for 10G, and 3 for 2.5G and 5G, which that +table does not list). All bridges in a network should use the same method. + +`stp bpdu filter` keeps BPDUs from being passed between ports while STP is +off, instead of the default flooding that lets a surrounding spanning tree +run through the switch. It takes effect at once when STP is off, otherwise +when STP is turned off. + ## Per-port settings ``` @@ -146,6 +197,7 @@ stp port <1-9> prio <0-240> # port priority, steps of 16 stp port <1-9> guard none|bpdu|root stp port <1-9> filter on|off # neither send nor accept BPDUs stp port <1-9> p2p auto|on|off +stp port <1-9> mcheck # try RSTP again on a port that fell back to STP ``` **edge** — an edge port forwards immediately and does not trigger a @@ -162,26 +214,76 @@ claims a better priority. on the far side reacts badly to them (some unmanaged switches with loop prevention cut the link) but you still want STP on the rest of the ports. +## Link aggregation + +A LAG (Link Aggregation Group) is one port of the tree, with its own roles, +timers and states. The switch supports up to 4 LAGs; each one takes the place +of its member ports in the STP status and gets its own row on the Spanning +Tree page. A group carries its own path cost, priority, edge, guard and +point-to-point settings, and takes the same commands as a port: + +``` +stp lag <1-4> on|off|mcheck|edge|cost|prio|guard|filter|p2p ... +stp lag 1 cost 10000 # the group decides, not its members +``` + +LAGs are configured with the `lag` command. Once a port is a member of a LAG +it can no longer be configured on its own for STP, and `stp port ` on a +member names the group to configure instead. Membership is read from the +aggregation registers once a second, so a group changing under LACP is picked +up without any coordination between the two; a port that joins or leaves a +group, and the group itself, start over from discarding. The automatic path +cost of a group is the one of its lowest member that has a link. + +The hardware keeps no STP state for a LAG as a whole, so every state change is +written to all member ports, in a single register write. BPDUs go out through +the lowest member that has a link and carry the group's own port id, and a +BPDU received on any member belongs to the group. Losing one member of a live +LAG is not a topology change; the group only goes down with its last link. + ## Status The Spanning Tree page shows the elected root (priority and MAC), the path cost -to it, the root port, the topology-change counter and, per port, the live state -read from the ASIC together with the configured options. The same data is -available as JSON: +to it, the root port, the topology-change counter and the time since it last +moved, the max age and forward +delay in use (the root's, when this switch is not the root) and, per port, the +live state read from the ASIC, or "No link", together with the configured +options. For a port taking part in the tree it also shows the designated +bridge, port and cost, and whether it talks RSTP or 802.1D to its neighbour; +the Check button runs `stp port mcheck`. The same data is available as +JSON: ``` GET /stp.json ``` -The `stp status` command prints the same view on the serial console. +The Counters section of the same page (or `/stpcnt.json`) shows per port the +BPDUs received and sent, those of them that carried the topology change flag +(TCNs included), and the time since the last BPDU arrived. The counters start +from zero with `stp on`, and `stp clear` resets them; the topology change +counter is not reset. + +The `stp status` command prints the same view on the serial console. A port +is reported with one of the roles Root, Designated, Alternate, Backup or +Disabled; Disabled covers a port with STP turned off on it, one shut down by +BPDU guard and one without link. + +The console's `age[s]` column (previously labelled `bpdu`) is the elapsed +time since the last BPDU, capped at 255 seconds; it is not a receive count. +Use the web counters or `/stpcnt.json` for full receive counts. + +An own BPDU received back on another port can temporarily hold its sending +port in discarding, even if that sending port has received no BPDUs itself. +When the hold expires, auto-edge is restored if the sending port has remained +silent. A continuing loop keeps the hold active. + +BPDUs shorter than their type allows, judged by the 802.3 length field, are +dropped: 7 bytes for a TCN, 38 for a Config BPDU and 39 for an RST BPDU, +LLC header included. ## Limitations * One spanning-tree instance; no MSTP, no per-VLAN trees. -* No proposal/agreement handshake — an RST-capable neighbour will still - converge, but through the timers rather than the fast transition. -* Port roles are approximated: the root port and designated ports are - distinguished, alternate/backup are not. -* Topology changes propagate away from the root only: nothing is announced - on the root port (no TCN and no BPDUs at all), so bridges upstream rely on - their own detection. +* A backup port, one that hears the BPDUs of another port of this switch, + is blocked and reported as Backup, but keeps sending BPDUs: loop detection + on that segment relies on them. diff --git a/html/app.js b/html/app.js index 9e8432fa..7b4cba00 100644 --- a/html/app.js +++ b/html/app.js @@ -41,6 +41,11 @@ stp_en_d:"Ports start blocked and take up to twice the forward delay to reach fo stp_dis_q:"Disable spanning tree?",stp_dis_d:"All ports go straight to forwarding; loop protection is lost.", stp_cost_err:"Path cost must be 0-200000000", lldp_enabled: "LLDP Enabled",lldp_title:"Service Status",lldp_portname:"Name",lldp_enable:"Send Frames", +stp_pcs:"Path cost",stp_pcs_long:"Long (32 bit)",stp_pcs_short:"Short (16 bit)", +stp_bh:"BPDUs while disabled",stp_bh_flood:"Flood",stp_bh_filter:"Filter", +stp_r4:"Backup",stp_times:"max age / forward delay",stp_proto:"Protocol",stp_nolink:"no link",stp_lasttc:"last", +stp_mcheck:"Check",stp_mcheck_t:"Send RST BPDUs again to find out whether the neighbour still needs 802.1D", +stp_cnt:"Counters",stp_crx:"BPDUs received",stp_ctx:"BPDUs sent",stp_ctrx:"TC received",stp_cttx:"TC sent",stp_cage:"Last BPDU [s]", st_title:"Port statistics",st_h:"totals since boot",st_txg:"TX good",st_txb:"TX bad",st_rxg:"RX good", st_rxb:"RX bad",st_details:"Details",st_counters:"MIB counters",st_nonzero:"non-zero only", st_autoref:"auto-refresh",st_counter:"Counter",st_value:"Value",st_fail:"failed to load counters", @@ -834,6 +839,8 @@ var CONF_CMDS=[ /^isolate\s+\d+(\s+(off|\d+))+$/, /^stp\s+(on|off)$/,/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d+$/, /^stp\s+version\s+(rstp|stp)$/, + /^stp\s+pathcost\s+(long|short)$/, + /^stp\s+bpdu\s+(filter|flood)$/, /^stp\s+(port\s+\d+|lag\s+[1-4])\s+(on|off)$/, /^stp\s+(port\s+\d+|lag\s+[1-4])\s+edge\s+(on|off|auto)$/, /^stp\s+(port\s+\d+|lag\s+[1-4])\s+cost\s+\d+$/, @@ -1223,12 +1230,14 @@ function fmtBridge(hex){ if(!hex||hex.length<16)return"-"; return parseInt(hex.slice(0,4),16)+" / "+hex.slice(4).replace(/(..)(?=.)/g,"$1:"); } +function stpEnt(pt){pt.lag=pt.p>100?pt.p-100:0} function stpKey(pt){return pt.lag?"L"+pt.lag:String(pt.p)} -function stpName(pt){return pt.lag?"LAG"+pt.lag+" ("+maskToPorts(pt.mbr).join(",")+")":String(pt.p)} +function stpName(pt){return pt.lag?"LAG"+pt.lag:String(pt.p)} +function stpDhms(x){return Math.floor(x/86400)+"d "+Math.floor(x/3600)%24+"h "+Math.floor(x/60)%60+"m "+x%60+"s"} function stpPre(k){return k.charAt(0)==="L"?"stp lag "+k.slice(1)+" ":"stp port "+k+" "} function stpBuild(ports){ - var tb=$("stpcfg").tBodies[0],st=$("stpstat").tBodies[0]; - tb.innerHTML="";st.innerHTML=""; + var tb=$("stpcfg").tBodies[0],st=$("stpstat").tBodies[0],ct=$("stpcnt").tBodies[0]; + tb.innerHTML="";st.innerHTML="";ct.innerHTML=""; ports.forEach(function(pt){ var p=stpKey(pt); var tr=tb.insertRow(); @@ -1246,7 +1255,12 @@ function stpBuild(ports){ tr.insertCell().appendChild(h("button",{class:"ctl",text:t("c_apply"),onclick:function(){stpApplyPort(p)}})); var sr=st.insertRow(); sr.insertCell().textContent=stpName(pt); - ["ro","st","db","dp","dc","oe","op"].forEach(function(k){sr.insertCell().id="st"+k+p}); + ["ro","st","db","dp","dc","oe","op","pv"].forEach(function(k){sr.insertCell().id="st"+k+p}); + sr.insertCell().appendChild(h("button",{class:"ctl",id:"stmc"+p,text:t("stp_mcheck"),title:t("stp_mcheck_t"), + onclick:function(){postCmd(stpPre(p)+"mcheck").then(stpLoad).catch(function(){})}})); + var cr=ct.insertRow(); + cr.insertCell().textContent=stpName(pt); + ["rx","tx","trx","ttx","age"].forEach(function(k){cr.insertCell().id="sc"+k+p}); }); } function stpPortVals(pt){ @@ -1260,10 +1274,21 @@ function stpPortVals(pt){ p2p:["auto","on","off"][pt.p2]||"auto", }; } +function stpCnt(c){ + c.ports.forEach(function(pt){ + stpEnt(pt); + var p=stpKey(pt); + if(!$("scrx"+p))return; + var v=[0,1,2,3].map(function(k){return parseInt(pt.c.slice(8*k,8*k+8),16)}),age=parseInt(pt.c.slice(32,36),16); + ["rx","tx","trx","ttx"].forEach(function(k,i){$("sc"+k+p).textContent=v[i]}); + $("scage"+p).textContent=!v[0]?"-":(age===0xffff?">":"")+Math.floor(age/c.hz); + }); +} function stpLoad(){ return getJSON("/stp.json").then(function(s){ - s.ports.sort(function(a,b){return((a.lag||0)-(b.lag||0))||(a.p-b.p)}); - var sig=s.ports.map(function(pt){return stpKey(pt)+":"+(pt.mbr||0)}).join(); + s.ports.forEach(stpEnt); + s.ports.sort(function(a,b){return(a.lag-b.lag)||(a.p-b.p)}); + var sig=s.ports.map(stpKey).join(); if(sig!==stpSig){stpSig=sig;stpBuild(s.ports);} stpCur=s; var en=$("stpen"); @@ -1271,16 +1296,19 @@ function stpLoad(){ var msg; if(!s.on)msg=t("stp_off_msg"); else{ - var me=(s.prio*4096).toString(16).padStart(4,"0")+s.myMac; + var me=(s.prio*4096).toString(16).padStart(4,"0")+s.myMac,tc=parseInt(s.tc,16),rp="-"; + s.ports.forEach(function(pt){if(pt.role===1)rp=stpName(pt)}); msg=t("stp_bridge")+" "+fmtBridge(me)+(s.weRoot ?" ("+t("stp_root_self")+")" - :"; "+t("stp_root")+" "+fmtBridge(s.rootPrio+s.rootMac)+" "+t("stp_via")+" "+s.rootPort+", "+t("stp_cost")+" "+parseInt(s.cost,16)) - +"; "+t("stp_tc")+": "+parseInt(s.tc,16); + :"; "+t("stp_root")+" "+fmtBridge(s.rootPrio+s.rootMac)+" "+t("stp_via")+" "+rp+", "+t("stp_cost")+" "+parseInt(s.cost,16)) + +"; "+t("stp_tc")+": "+tc+(tc?" ("+t("stp_lasttc")+" "+stpDhms(parseInt(s.tcs,16))+")":"") + +"; "+t("stp_times")+" "+s.rMaxage+" / "+s.rFwd+" s"; } $("stpids").textContent=msg; var bc=$("stpbridge"); if(!bc.dataset.dirty){ $("stpver").value=s.rstp?"rstp":"stp"; + $("stppcs").value=s.pcs?"short":"long";$("stpbh").value=s.bh?"filter":"flood"; $("stpprio").value=String(s.prio); $("stphello").value=s.hello;$("stpmaxage").value=s.maxage; $("stpfwd").value=s.fwd;$("stptxhold").value=s.txhold; @@ -1289,18 +1317,21 @@ function stpLoad(){ var p=stpKey(pt); var trip=(pt.f&STP_PF.TRIP)?" "+badge(t("stp_trip"),"bad"):""; $("stro"+p).textContent=s.on&&pt.role?t("stp_r"+pt.role):"-"; - $("stst"+p).innerHTML=s.on?badge(t("stp_s"+pt.st),pt.st===3?"ok":"")+trip:"-"; + $("stst"+p).innerHTML=s.on?(pt.lk?badge(t("stp_s"+pt.st),pt.st===3?"ok":""):badge(t("stp_nolink")))+trip:"-"; $("stdb"+p).textContent=s.on?fmtBridge(pt.db):"-"; $("stdp"+p).textContent=s.on?parseInt(pt.dp.slice(0,2),16)+"."+parseInt(pt.dp.slice(2),16):"-"; $("stdc"+p).textContent=s.on?parseInt(pt.dc,16):"-"; $("stoe"+p).textContent=s.on?t((pt.f&STP_PF.OPEREDGE)?"c_yes":"c_no"):"-"; $("stop"+p).textContent=s.on?t(pt.p2===2?"c_no":"c_yes"):"-"; + $("stpv"+p).textContent=s.on&&pt.role?(s.rstp&&!pt.lg?"RSTP":"STP"):"-"; + $("stmc"+p).disabled=!(s.on&&pt.role&&s.rstp); var row=$("sten"+p).closest("tr"); if(row.dataset.dirty)return; var v=stpPortVals(pt); $("sten"+p).checked=v.en;$("sted"+p).value=v.edge;$("stco"+p).value=v.cost; $("stpr"+p).value=v.prio;$("stgu"+p).value=v.guard;$("stfi"+p).value=v.filter;$("stpp"+p).value=v.p2p; }); + if($("stpcntd").open)return getJSON("/stpcnt.json").then(stpCnt); }).catch(function(){}); } function stpApplyPort(p){ @@ -1328,6 +1359,8 @@ $("stpbapply").addEventListener("click",function(){ var ver=$("stpver").value,prio=$("stpprio").value; if(ver!==(s.rstp?"rstp":"stp"))cmds.push("stp version "+ver); if(prio!==String(s.prio))cmds.push("stp prio "+prio); + if($("stppcs").value!==(s.pcs?"short":"long"))cmds.push("stp pathcost "+$("stppcs").value); + if($("stpbh").value!==(s.bh?"filter":"flood"))cmds.push("stp bpdu "+$("stpbh").value); [["stphello","hello"],["stpmaxage","maxage"],["stpfwd","fwd"],["stptxhold","txhold"]].forEach(function(f){ var v=$(f[0]).value; if(String(v)!==String(s[f[1]]))cmds.push("stp "+f[1]+" "+v); @@ -1346,6 +1379,7 @@ $("stpen").addEventListener("change",function(){ var sel=$("stpprio"); for(var i=0;i<16;i++)sel.appendChild(h("option",{value:String(i),text:i+" ("+(i*4096)+")"})); })(); +$("stpcntd").addEventListener("toggle",function(){if(this.open)stpLoad()}); var stpPoller=new Poller(stpLoad,3000); tabHooks.stp={enter:function(){stpPoller.start()},leave:function(){stpPoller.stop()}}; diff --git a/html/index.html b/html/index.html index 5695f819..d866ed37 100644 --- a/html/index.html +++ b/html/index.html @@ -264,6 +264,8 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}

-

+ + @@ -281,9 +283,14 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}

Port status

- +
PortRoleStateDesignated bridgeDesignated portDesignated costOper. edgeOper. P2P
PortRoleStateDesignated bridgeDesignated portDesignated costOper. edgeOper. P2PProtocol
+

Counters

+
+ +
PortBPDUs receivedBPDUs sentTC receivedTC sentLast BPDU [s]
+
diff --git a/httpd/httpd.c b/httpd/httpd.c index 5128cabb..88034fab 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -1055,6 +1055,8 @@ void httpd_appcall(void) send_lldp(); } else if (is_word(q, "/lacp.json")) { send_lacp(); + } else if (is_word(q, "/stpcnt.json")) { + send_stp_counters(); } 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 7f107b7d..7b416fce 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -600,143 +600,6 @@ void send_lag(void) } -static __xdata uint32_t pi_u32; -static __xdata uint8_t pi_prio, pi_ext; -static __xdata uint8_t * __xdata pi_mac; - - -static void u32hex_html(void) -{ - __xdata uint8_t *b = (__xdata uint8_t *)&pi_u32; - byte_to_html(b[3]); - byte_to_html(b[2]); - byte_to_html(b[1]); - byte_to_html(b[0]); -} - - -static void bridge_to_html(void) -{ - byte_to_html(pi_prio); - byte_to_html(pi_ext); - for (uint8_t i = 0; i < 6; i++) - byte_to_html(pi_mac[i]); -} - -void send_stp(void) -{ - uint8_t i, j, st, dsg; - - dbg_string("send_stp called\n"); - slen = strtox(outbuf, HTTP_RESPONCE_JSON); - - slen += strtox(outbuf + slen, "{\"on\":"); - bool_to_html(stp_enabled); - slen += strtox(outbuf + slen, ",\"rstp\":"); - bool_to_html(stp_rstp); - slen += strtox(outbuf + slen, ",\"prio\":"); - itoa_html(stp_prio >> 4); - slen += strtox(outbuf + slen, ",\"hello\":"); - itoa_html(stp_hello_s); - slen += strtox(outbuf + slen, ",\"maxage\":"); - itoa_html(stp_maxage_s); - slen += strtox(outbuf + slen, ",\"fwd\":"); - itoa_html(stp_fwddelay_s); - slen += strtox(outbuf + slen, ",\"txhold\":"); - itoa_html(stp_txhold); - slen += strtox(outbuf + slen, ",\"rootPrio\":\""); - byte_to_html(root_bridge.prio); - byte_to_html(root_bridge.ext); - slen += strtox(outbuf + slen, "\",\"rootMac\":\""); - for (j = 0; j < 6; j++) - byte_to_html(root_bridge.mac[j]); - slen += strtox(outbuf + slen, "\",\"myMac\":\""); - for (j = 0; j < 6; j++) - byte_to_html(uip_ethaddr.addr[j]); - slen += strtox(outbuf + slen, "\",\"cost\":\""); - byte_to_html(root_bridge_cost >> 24); - byte_to_html(root_bridge_cost >> 16); - byte_to_html(root_bridge_cost >> 8); - byte_to_html(root_bridge_cost); - slen += strtox(outbuf + slen, "\",\"weRoot\":"); - bool_to_html(stp_root_port == 0xff ? 1 : 0); - slen += strtox(outbuf + slen, ",\"rootPort\":"); - j = stp_root_port; - if (j != 0xff && j >= STP_LAG_BASE) { - j = 0; - while (j < STP_LAG_BASE && !((stp_lag_mask[stp_root_port - STP_LAG_BASE] >> j) & 1)) - j++; - if (j >= STP_LAG_BASE) - j = 0; - } - itoa_html(j == 0xff ? 0 : machine.log_to_phys_port[j]); - slen += strtox(outbuf + slen, ",\"tc\":\""); - byte_to_html(stp_tc_count >> 8); - byte_to_html(stp_tc_count); - slen += strtox(outbuf + slen, "\",\"ports\":["); - reg_read_m(RTL837X_MSTP_STATES); - for (i = 0; i < STP_ENTITIES; i++) { - if (i < STP_LAG_BASE && !MACHINE_PORT_VALID(i)) - continue; - if (i < STP_LAG_BASE && stp_ent_of[i] != i) - continue; - if (i >= STP_LAG_BASE && !stp_lag_mask[i - STP_LAG_BASE]) - continue; - slen += strtox(outbuf + slen, "{\"p\":"); - itoa_html(i < STP_LAG_BASE ? machine.log_to_phys_port[i] : 0); - slen += strtox(outbuf + slen, ",\"lag\":"); - itoa_html(i < STP_LAG_BASE ? 0 : i - STP_LAG_BASE + 1); - slen += strtox(outbuf + slen, ",\"mbr\":"); - itoa16_html(i < STP_LAG_BASE ? 0 : stp_lag_mask[i - STP_LAG_BASE]); - slen += strtox(outbuf + slen, ",\"st\":"); - j = i; - if (i >= STP_LAG_BASE) { - j = 0; - while (j < STP_LAG_BASE && !((stp_lag_mask[i - STP_LAG_BASE] >> j) & 1)) - j++; - if (j >= STP_LAG_BASE) - j = 0; - } - st = (sfr_data[3 - (j >> 2)] >> ((j << 1) & 0x7)) & 0x3; - itoa_html(st); - slen += strtox(outbuf + slen, ",\"role\":"); - if (!(stp_pflags[i] & STP_PF_ENABLED) || (stp_pflags[i] & STP_PF_TRIPPED)) - itoa_html(0); - else if (i == stp_root_port) - itoa_html(1); - else if (st == 3) - itoa_html(2); - else - itoa_html(3); - slen += strtox(outbuf + slen, ",\"f\":"); - itoa_html(stp_pflags[i]); - slen += strtox(outbuf + slen, ",\"pc\":\""); - pi_u32 = stp_pcost[i]; u32hex_html(); - slen += strtox(outbuf + slen, "\",\"prio\":"); - itoa_html(stp_pprio[i]); - slen += strtox(outbuf + slen, ",\"p2\":"); - itoa_html(stp_pp2p[i]); - dsg = stp_dpid[i] && stp_bpdu_age[i] < (uint16_t)stp_maxage_s * STP_HZ; - slen += strtox(outbuf + slen, ",\"db\":\""); - if (dsg) { - pi_prio = stp_dbridge[i].prio; pi_ext = stp_dbridge[i].ext; - pi_mac = stp_dbridge[i].mac; - } else { - pi_prio = stp_prio; pi_ext = 0; - pi_mac = uip_ethaddr.addr; - } - bridge_to_html(); - slen += strtox(outbuf + slen, "\",\"dp\":\""); - byte_to_html(dsg ? (stp_dpid[i] >> 8) : stp_pprio[i]); - byte_to_html(dsg ? stp_dpid[i] : (i + 1)); - slen += strtox(outbuf + slen, "\",\"dc\":\""); - pi_u32 = dsg ? stp_dcost[i] : root_bridge_cost; u32hex_html(); - slen += strtox(outbuf + slen, "\"},"); - } - slen -= 1; // remove comma - slen += strtox(outbuf + slen, "]}"); -} - void send_lacp(void) { dbg_string("send_lacp called\n"); @@ -816,6 +679,7 @@ void send_lldp(void) char_to_html('}'); } + void send_eee(void) { dbg_string("send_eee called\nsending EEE status\n"); diff --git a/httpd/page_impl.h b/httpd/page_impl.h index 5133d2eb..366bfa8b 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -22,9 +22,10 @@ void send_isolation(void); void send_config(void); void send_cmd_log(void); void send_lag(void); -void send_stp(void); +void send_stp(void) __banked; void send_lldp(void); void send_lacp(void); +void send_stp_counters(void) __banked; void send_vlanlist(void); /* Convert only the lower nibble to ascii HEX char. diff --git a/httpd/page_stp.c b/httpd/page_stp.c new file mode 100644 index 00000000..fbc2ce50 --- /dev/null +++ b/httpd/page_stp.c @@ -0,0 +1,195 @@ +#include "rtl837x_sfr.h" +#include "rtl837x_common.h" +#include "rtl837x_regs.h" +#include "rtl837x_port.h" +#include "uip.h" +#include +#include "phy.h" +#include "machine.h" +#include "rtl837x_stp.h" +#include "page_impl.h" +#include "debug.h" + +#pragma codeseg BANK4 +#pragma constseg BANK4 + +extern __xdata uint8_t outbuf[TCP_OUTBUF_SIZE]; +extern __xdata uint16_t slen; + +static __code const uint8_t stp_json_hdr[] = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n"; + +static void stp_bool_html(char c) +{ + outbuf[slen++] = c ? '1' : '0'; +} + +static void stp_byte_html(uint8_t val) +{ + outbuf[slen++] = itohex(val >> 4); + outbuf[slen++] = itohex(val); +} + +static void stp_itoa_html(uint8_t v) +{ + uint8_t t = v / 100; + uint8_t print_zeros = t; + if (print_zeros) + outbuf[slen++] = '0' + t; + t = (v / 10) % 10; + print_zeros |= t; + if (print_zeros) + outbuf[slen++] = '0' + t; + outbuf[slen++] = '0' + (v % 10); +} + + +static __xdata uint32_t pi_u32; +static __xdata uint8_t pi_prio, pi_ext; +static __xdata uint8_t * __xdata pi_mac; + + +static void u32hex_html(void) +{ + __xdata uint8_t *b = (__xdata uint8_t *)&pi_u32; + stp_byte_html(b[3]); + stp_byte_html(b[2]); + stp_byte_html(b[1]); + stp_byte_html(b[0]); +} + + +static void bridge_to_html(void) +{ + stp_byte_html(pi_prio); + stp_byte_html(pi_ext); + for (uint8_t i = 0; i < 6; i++) + stp_byte_html(pi_mac[i]); +} + + +void send_stp(void) __banked +{ + static __xdata uint8_t i, j, st, dsg; + + dbg_string("send_stp called\n"); + slen = strtox(outbuf, stp_json_hdr); + + slen += strtox(outbuf + slen, "{\"on\":"); + stp_bool_html(stp_enabled); + slen += strtox(outbuf + slen, ",\"rstp\":"); + stp_bool_html(stp_rstp); + slen += strtox(outbuf + slen, ",\"prio\":"); + stp_itoa_html(stp_prio >> 4); + slen += strtox(outbuf + slen, ",\"hello\":"); + stp_itoa_html(stp_hello_s); + slen += strtox(outbuf + slen, ",\"maxage\":"); + stp_itoa_html(stp_maxage_s); + slen += strtox(outbuf + slen, ",\"fwd\":"); + stp_itoa_html(stp_fwddelay_s); + slen += strtox(outbuf + slen, ",\"txhold\":"); + stp_itoa_html(stp_txhold); + slen += strtox(outbuf + slen, ",\"pcs\":"); + stp_itoa_html(stp_pcost_short); + slen += strtox(outbuf + slen, ",\"bh\":"); + stp_itoa_html(stp_bpdu_filter); + slen += strtox(outbuf + slen, ",\"tcs\":\""); + pi_u32 = stp_tc_secs; u32hex_html(); + slen += strtox(outbuf + slen, "\",\"rMaxage\":"); + stp_itoa_html(stp_root_port == 0xff ? stp_maxage_s : stp_root_maxage); + slen += strtox(outbuf + slen, ",\"rFwd\":"); + stp_itoa_html(stp_root_port == 0xff ? stp_fwddelay_s : stp_root_fwd); + slen += strtox(outbuf + slen, ",\"rootPrio\":\""); + stp_byte_html(root_bridge.prio); + stp_byte_html(root_bridge.ext); + slen += strtox(outbuf + slen, "\",\"rootMac\":\""); + for (j = 0; j < 6; j++) + stp_byte_html(root_bridge.mac[j]); + slen += strtox(outbuf + slen, "\",\"myMac\":\""); + for (j = 0; j < 6; j++) + stp_byte_html(uip_ethaddr.addr[j]); + slen += strtox(outbuf + slen, "\",\"cost\":\""); + stp_byte_html(root_bridge_cost >> 24); + stp_byte_html(root_bridge_cost >> 16); + stp_byte_html(root_bridge_cost >> 8); + stp_byte_html(root_bridge_cost); + slen += strtox(outbuf + slen, "\",\"weRoot\":"); + stp_bool_html(stp_root_port == 0xff ? 1 : 0); + slen += strtox(outbuf + slen, ",\"tc\":\""); + stp_byte_html(stp_tc_count >> 8); + stp_byte_html(stp_tc_count); + slen += strtox(outbuf + slen, "\",\"ports\":["); + for (i = 0; i < STP_ENTITIES; i++) { + j = stp_ent_id(i); + if (!j) + continue; + slen += strtox(outbuf + slen, "{\"p\":"); + stp_itoa_html(j); + slen += strtox(outbuf + slen, ",\"st\":"); + st = stp_port_state(i); + stp_itoa_html(st); + slen += strtox(outbuf + slen, ",\"role\":"); + stp_itoa_html(stp_port_role(i)); + slen += strtox(outbuf + slen, ",\"f\":"); + stp_itoa_html(stp_pflags[i]); + slen += strtox(outbuf + slen, ",\"pc\":\""); + pi_u32 = stp_pcost[i]; u32hex_html(); + slen += strtox(outbuf + slen, "\",\"prio\":"); + stp_itoa_html(stp_pprio[i]); + slen += strtox(outbuf + slen, ",\"p2\":"); + stp_itoa_html(stp_pp2p[i]); + slen += strtox(outbuf + slen, ",\"lk\":"); + stp_itoa_html((stp_link_prev >> i) & 1); + slen += strtox(outbuf + slen, ",\"lg\":"); + stp_itoa_html((stp_legacy >> i) & 1); + dsg = stp_dpid[i] && stp_info_while[i]; + slen += strtox(outbuf + slen, ",\"db\":\""); + if (dsg) { + pi_prio = stp_dbridge[i].prio; pi_ext = stp_dbridge[i].ext; + pi_mac = stp_dbridge[i].mac; + } else { + pi_prio = stp_prio; pi_ext = 0; + pi_mac = uip_ethaddr.addr; + } + bridge_to_html(); + slen += strtox(outbuf + slen, "\",\"dp\":\""); + stp_byte_html(dsg ? (stp_dpid[i] >> 8) : stp_pprio[i]); + stp_byte_html(dsg ? stp_dpid[i] : (i + 1)); + slen += strtox(outbuf + slen, "\",\"dc\":\""); + pi_u32 = dsg ? stp_dcost[i] : root_bridge_cost; u32hex_html(); + slen += strtox(outbuf + slen, "\"},"); + } + slen -= 1; // remove comma + slen += strtox(outbuf + slen, "]}"); +} + + +void send_stp_counters(void) __banked +{ + uint8_t i, j; + + slen = strtox(outbuf, stp_json_hdr); + slen += strtox(outbuf + slen, "{\"on\":"); + stp_bool_html(stp_enabled); + slen += strtox(outbuf + slen, ",\"hz\":"); + stp_itoa_html(STP_HZ); + slen += strtox(outbuf + slen, ",\"tc\":\""); + stp_byte_html(stp_tc_count >> 8); + stp_byte_html(stp_tc_count); + slen += strtox(outbuf + slen, "\",\"ports\":["); + for (i = 0; i < STP_ENTITIES; i++) { + j = stp_ent_id(i); + if (!j) + continue; + slen += strtox(outbuf + slen, "{\"p\":"); + stp_itoa_html(j); + slen += strtox(outbuf + slen, ",\"c\":\""); + for (j = 0; j < STP_CNT_N; j++) { + pi_u32 = stp_cnt[j][i]; u32hex_html(); + } + stp_byte_html(stp_bpdu_age[i] >> 8); + stp_byte_html(stp_bpdu_age[i]); + slen += strtox(outbuf + slen, "\"},"); + } + slen -= 1; + slen += strtox(outbuf + slen, "]}"); +} diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 118e265c..1e41235f 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -20,8 +20,8 @@ extern __xdata uint8_t err_status; // All entry points are __banked and nothing here runs from an interrupt, // so the module does not need to stay in the resident bank -#pragma codeseg BANK2 -#pragma constseg BANK2 +#pragma codeseg BANK4 +#pragma constseg BANK4 extern __code const struct machine machine; extern __xdata uint8_t sfr_data[4]; @@ -32,14 +32,6 @@ extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_ALLOC_SIZE]; -extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; -extern __xdata uint8_t cmd_words_len; -extern __xdata uint8_t cmd_words_b[15]; -extern __xdata char save_cmd; /* 0 while execute_config() replays the saved config */ -uint8_t cmd_compare(uint8_t start, __code const uint8_t * cmd); -uint8_t atoi_byte(uint8_t idx); -uint8_t cmd_parse_port_separator(uint8_t idx); -extern __xdata uint8_t atoi_results_u8; /* ---- Configuration ---- */ __xdata uint8_t stp_prio; /* bridge priority high byte (0x80 = 32768) */ @@ -50,36 +42,54 @@ __xdata uint8_t stp_rstp; /* 1 = RST BPDUs, 0 = legacy Config BPDUs */ __xdata uint8_t stp_txhold; /* BPDUs per port per second */ -__xdata uint8_t stp_ent_of[STP_PORTS]; -__xdata uint16_t stp_lag_mask[STP_LAG_COUNT]; -__xdata uint8_t stp_ss_i; -__xdata uint8_t stp_map_dirty; __xdata uint8_t stp_pflags[STP_ENTITIES]; -__xdata uint32_t stp_pcost[STP_ENTITIES]; /* 0 = auto */ +__xdata uint32_t stp_pcost[STP_ENTITIES]; /* 0 = auto */ __xdata uint8_t stp_pprio[STP_ENTITIES]; -__xdata uint8_t stp_pp2p[STP_ENTITIES]; /* admin point-to-point: 0 auto, 1 on, 2 off */ +__xdata uint8_t stp_pspeed[STP_ENTITIES]; /* speed nibble last read from the ASIC */ +__xdata uint8_t stp_pp2p[STP_ENTITIES]; /* admin point-to-point: 0 auto, 1 on, 2 off */ -/* Designated bridge, port and cost last heard on the port; stp_bpdu_age tells - * whether they are still current. +/* Designated bridge, port and cost last heard on the port; stp_info_while + * tells whether they are still current. */ __xdata struct bridge stp_dbridge[STP_ENTITIES]; +__xdata struct bridge stp_droot[STP_ENTITIES]; __xdata uint16_t stp_dpid[STP_ENTITIES]; __xdata uint32_t stp_dcost[STP_ENTITIES]; +__xdata uint16_t stp_alt; /* bit per port: blocked, a better bridge owns the segment */ +__xdata struct bridge stp_self; +__xdata uint8_t stp_j; +__xdata uint8_t stp_best; +__xdata uint32_t stp_cost_cand; +__xdata uint32_t stp_cost_best; +__xdata int8_t stp_cmp; +__xdata uint8_t stp_rxage[STP_ENTITIES]; /* message age heard on the port, seconds */ +__xdata uint8_t stp_rxmaxage[STP_ENTITIES]; +__xdata uint8_t stp_rxhello[STP_ENTITIES]; +__xdata uint8_t stp_rxfwd[STP_ENTITIES]; +__xdata uint16_t stp_info_while[STP_ENTITIES]; /* ticks the heard information stays valid, 0 = none */ +__xdata uint8_t stp_root_maxage; /* max age and forward delay of the root */ +__xdata uint8_t stp_root_fwd; +__xdata uint8_t stp_reselect_due; +__xdata uint32_t stp_rpcost[STP_ENTITIES]; /* root path cost through the port: heard cost plus port cost */ /* ---- Status / runtime ---- */ __xdata struct bridge root_bridge; __xdata uint32_t root_bridge_cost; /* our cost to the root (rx cost + root port cost) */ __xdata uint8_t stp_root_port; /* 0xff = we are the root */ __xdata uint16_t stp_tc_count; -__xdata uint16_t stp_scratch16; /* scratch for status printing and the lag map */ -__xdata uint8_t stp_st_of; +__xdata uint16_t stp_tc_seen; +__xdata uint32_t stp_tc_secs; /* seconds since the topology change counter last moved */ +__xdata uint8_t stp_pcost_short; /* 1: automatic port costs from the 802.1D-1998 table */ +__xdata uint8_t stp_bpdu_filter; /* 1: keep BPDUs on the CPU while STP is off instead of flooding */ +__xdata uint16_t stp_scratch16; /* scratch for status printing only */ __xdata uint16_t port_timers[STP_ENTITIES]; /* listen-period countdown (0 = not listening) */ __xdata uint16_t port_hello[STP_ENTITIES]; /* hello TX countdown */ __xdata uint16_t stp_bpdu_age[STP_ENTITIES]; /* ticks since last BPDU seen on port (saturating) */ __xdata uint8_t stp_loop_held[STP_ENTITIES]; /* port is out of forwarding because a loop was seen on it */ +__xdata uint16_t stp_heard; /* bit per port: a BPDU arrived since the link last went down */ __xdata uint8_t stp_tx_budget[STP_ENTITIES]; /* tx hold: BPDUs left in the current second */ -__xdata uint8_t stp_tx_count[STP_ENTITIES]; /* BPDUs actually put on the wire, wraps at 256 */ +__xdata uint32_t stp_cnt[STP_CNT_N][STP_ENTITIES]; /* per-port counters, first index STP_CNT_* */ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ __xdata uint16_t stp_link_prev; /* carrier bitmap as of the last check */ __xdata uint16_t stp_link_now; @@ -89,15 +99,55 @@ __xdata uint8_t stp_tx_flags_extra; /* one-shot flags OR-ed into the next BPDU __xdata uint8_t stp_rxbody; __xdata uint16_t stp_rxlen; /* received frame length, saved before uip_len is consumed */ __xdata uint8_t stp_msg_age; /* message age of the root info we hold, seconds */ -__xdata uint16_t stp_tc_while; /* ticks left to set the TC flag in our BPDUs */ +__xdata uint16_t stp_tcwhile[STP_ENTITIES]; /* ticks left to send TC on the port, 0 = none */ +__xdata uint16_t stp_newinfo; /* bit per port: send a BPDU at the next tick */ +__xdata uint8_t stp_k; +__xdata uint8_t stp_tcn; +__xdata uint8_t stp_armed; +__xdata uint16_t stp_agree; /* bit per port: answer with an agreement in the next BPDU */ +__xdata uint16_t stp_rrwhile[STP_ENTITIES]; /* ticks the port still counts as a recent root port */ +__xdata uint16_t stp_rbwhile[STP_ENTITIES]; /* ticks the port still counts as a recent backup port */ +__xdata uint16_t stp_reroot; /* bit per port: recent root port held out of forwarding */ +__xdata uint8_t stp_synced_root; /* root port the last sync was done for, 0xff = none */ +__xdata uint16_t stp_backup; /* bit per port: blocked because another port of ours owns the segment */ +__xdata uint16_t stp_alt_agreed; /* bit per port: alternate or backup port that already agreed to the information it holds */ +__xdata uint16_t stp_legacy; /* bit per port: the neighbour speaks 802.1D, send Config BPDUs and TCN */ +__xdata uint16_t stp_seen_stp; /* bit per port: an 802.1D BPDU arrived during the migrate time */ +__xdata uint16_t stp_seen_rstp; /* bit per port: an RST BPDU arrived during the migrate time */ +__xdata uint8_t stp_mdelay[STP_ENTITIES]; /* ticks before the port may change protocol again */ __xdata uint8_t stp_i; __xdata uint32_t stp_cost_scratch; __xdata uint8_t stp_loop_peer; /* the other own port seen on a looped segment */ +__xdata uint8_t stp_ent_of[STP_PORTS]; /* entity a port answers to: itself, or STP_LAG_BASE + lag */ +__xdata uint16_t stp_lag_mask[STP_LAG_COUNT]; /* member ports of each lag, 0 = no such lag */ +__xdata uint16_t stp_map_changed; /* bit per entity: its membership changed, start it over */ +__xdata uint16_t stp_link_phys; /* carrier bitmap of the physical ports */ +__xdata uint16_t stp_ent_bit; +__xdata uint16_t stp_ss_mask; +__xdata uint8_t stp_ss_i; +__xdata uint8_t stp_lag; #define STP_EDGE_DELAY (3 * STP_HZ) /* auto-edge: forward after 3 s without BPDU */ -#define AUTO_COST 20000UL /* path cost used when stp_pcost == 0 (1G default) */ -#define PCOST(i) (stp_pcost[i] ? stp_pcost[i] : AUTO_COST) +#define MAXAGE_S (stp_root_port == 0xff ? stp_maxage_s : stp_root_maxage) +#define FWD_S (stp_root_port == 0xff ? stp_fwddelay_s : stp_root_fwd) +#define FWD_TICKS ((uint16_t)FWD_S * STP_HZ) +#define P2P(i) (stp_pp2p[i] != 2) +#define SEND_RSTP(i) (stp_rstp && !((stp_legacy >> (i)) & 1)) +#define STP_MIGRATE (3 * STP_HZ) + +#define AUTO_COST 20000UL /* path cost of a link whose speed we cannot read */ +#define PCOST(i) (stp_pcost[i] ? stp_pcost[i] : stp_speed_cost[(stp_pcost_short << 3) | (stp_pspeed[i] & 0x7)]) + +/* Path costs indexed by the speed nibble the ASIC reports: 0 10M, 1 100M, + * 2 1G, 4 10G, 5 2.5G, 6 5G; the rest are unknown to us. Entries 0-7 hold the + * 802.1Q recommended values, 8-15 the 802.1D-1998 ones for the same speeds. */ +static __code const uint32_t stp_speed_cost[16] = { + 2000000UL, 200000UL, 20000UL, AUTO_COST, + 2000UL, 8000UL, 4000UL, AUTO_COST, + 100UL, 19UL, 4UL, 4UL, + 2UL, 3UL, 3UL, 4UL +}; struct stp_pkt { uint8_t stp_addr[6]; @@ -165,31 +215,35 @@ struct stp_pkt_in { #define BPDU_LEN_RST 0x27 // LLC and a 36 byte body #define BPDU_LEN_MIN_BODY 9 #define BPDU_LEN_FULL_BODY 40 +#define BPDU_LEN_TCN 0x07 // LLC and a 4 byte body #define BPDU_FLAG_TC 0x01 +#define BPDU_FLAG_PROPOSAL 0x02 +#define BPDU_FLAG_AGREEMENT 0x40 #define BPDU_FLAG_LEARNING 0x10 #define BPDU_FLAG_FORWARDING 0x20 #define BPDU_FLAG_TCACK 0x80 #define BPDU_ROLE_ROOT (0b10 << 2) #define BPDU_ROLE_DESIGNATED (0b11 << 2) - -/* Port state as the MSTP register encodes it, two bits per port. */ -#define STP_ST_DISABLED 0b00 -#define STP_ST_BLOCKING 0b01 -#define STP_ST_LEARNING 0b10 -#define STP_ST_FORWARDING 0b11 -#define STP_ST_MASK 0b11 +#define BPDU_ROLE_MASK (0b11 << 2) +#define BPDU_ROLE_ALTBACK (0b01 << 2) /* Console messages name the port on the front panel, not the internal index. */ +static void print_ent(uint8_t e) __reentrant +{ + if (e >= STP_LAG_BASE) { + write_char('L'); + write_char('1' + e - STP_LAG_BASE); + } else { + print_byte(machine.log_to_phys_port[e]); + } +} + + static void print_port_nl(uint8_t port) __reentrant { - if (port >= STP_LAG_BASE) { - write_char('L'); - write_char('1' + port - STP_LAG_BASE); - } else { - print_byte(machine.log_to_phys_port[port]); - } + print_ent(port); write_char('\n'); } @@ -206,9 +260,10 @@ static void print_bridge_id(uint8_t prio, uint8_t ext, __xdata uint8_t *mac) __r * formatter. The state indices are the ASIC's own two bits, in the order * stp_state_set() writes them. */ static __code const char stp_state_txt[] = "off blocklearnfwd "; -static __code const char stp_role_txt[] = "desgroot"; +static __code const char stp_role_txt[] = "dis rootdesgaltnback"; static __code const char stp_edge_txt[] = "no yes "; +static uint8_t stp_state_get(uint8_t port) __reentrant; static uint8_t stp_ent_active(uint8_t e) __reentrant; static void print_field(__code const char *txt, uint8_t idx, uint8_t width) __reentrant @@ -219,7 +274,7 @@ static void print_field(__code const char *txt, uint8_t idx, uint8_t width) __re } -static void stp_status(void) +void stp_status(void) __banked { if (!stp_enabled) { print_string("STP off\n"); @@ -234,12 +289,7 @@ static void stp_status(void) print_string(" (this switch)\n"); } else { print_string(" port "); - if (stp_root_port >= STP_LAG_BASE) { - write_char('L'); - write_char('1' + stp_root_port - STP_LAG_BASE); - } else { - print_byte(machine.log_to_phys_port[stp_root_port]); - } + print_ent(stp_root_port); print_string(" cost "); print_long(root_bridge_cost); write_char('\n'); @@ -247,34 +297,20 @@ static void stp_status(void) print_string("changes "); print_short(stp_tc_count); write_char('\n'); - print_string("port state role edge tx bpdu\n"); - reg_read_m(RTL837X_MSTP_STATES); + print_string("port state role edge tx age[s]\n"); for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) { if (!stp_ent_active(stp_i)) continue; write_char(' '); - if (stp_i >= STP_LAG_BASE) { - write_char('L'); - write_char('1' + stp_i - STP_LAG_BASE); - } else { - print_byte(machine.log_to_phys_port[stp_i]); - } + print_ent(stp_i); print_string(" "); - stp_st_of = stp_i; - if (stp_i >= STP_LAG_BASE) { - stp_st_of = 0; - while (stp_st_of < STP_PORTS && !((stp_lag_mask[stp_i - STP_LAG_BASE] >> stp_st_of) & 1)) - stp_st_of++; - if (stp_st_of >= STP_PORTS) - stp_st_of = 0; - } - print_field(stp_state_txt, (sfr_data[3 - (stp_st_of >> 2)] >> ((stp_st_of << 1) & 0x7)) & 0x3, 5); + print_field(stp_state_txt, stp_state_get(stp_i), 5); write_char(' '); - print_field(stp_role_txt, stp_i == stp_root_port ? 1 : 0, 4); + print_field(stp_role_txt, stp_port_role(stp_i), 4); write_char(' '); print_field(stp_edge_txt, stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0, 4); write_char(' '); - print_byte(stp_tx_count[stp_i]); + print_byte((uint8_t)stp_cnt[STP_CNT_TX][stp_i]); write_char(' '); stp_scratch16 = stp_bpdu_age[stp_i] / STP_HZ; itoa(stp_scratch16 > 255 ? 255 : (uint8_t)stp_scratch16); @@ -283,20 +319,6 @@ static void stp_status(void) } -static void stp_record_designated(uint8_t port) __reentrant -{ - stp_dbridge[port].prio = STP_I->bridge.prio; - stp_dbridge[port].ext = STP_I->bridge.ext; - memcpy(stp_dbridge[port].mac, STP_I->bridge.mac, 6); - stp_dpid[port] = ((uint16_t)STP_I->port_prio << 8) | STP_I->port_id; - stp_cost_scratch = STP_I->root_path_cost; - stp_dcost[port] = ((stp_cost_scratch & 0xff) << 24) - | ((stp_cost_scratch & 0xff00) << 8) - | ((stp_cost_scratch >> 8) & 0xff00) - | (stp_cost_scratch >> 24); -} - - /* Lexicographic compare of n bytes. A MAC is 6 of them; a Bridge Identifier * is 8, the two priority octets ahead of the MAC, compared as one unsigned * number per 802.1D. */ @@ -313,109 +335,216 @@ int8_t cmpBytes(__xdata uint8_t *m1, __xdata uint8_t *m2, uint8_t n) __reentrant } -static void stp_lag_map(void) +static int8_t stp_msg_cmp(uint8_t port) __reentrant { - for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) - stp_ent_of[stp_ss_i] = stp_ss_i; - for (stp_scratch = 0; stp_scratch < STP_LAG_COUNT; stp_scratch++) { - stp_scratch16 = port_lag_members_get(stp_scratch); - if (stp_lag_mask[stp_scratch] != stp_scratch16) - stp_map_dirty = 1; - stp_lag_mask[stp_scratch] = stp_scratch16; - for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) - if ((stp_lag_mask[stp_scratch] >> stp_ss_i) & 1) - stp_ent_of[stp_ss_i] = STP_LAG_BASE + stp_scratch; - } + stp_cmp = cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&stp_droot[port], 8); + if (stp_cmp) + return stp_cmp; + if (stp_cost_scratch != stp_dcost[port]) + return stp_cost_scratch < stp_dcost[port] ? -1 : 1; + stp_cmp = cmpBytes((__xdata uint8_t *)&STP_I->bridge, (__xdata uint8_t *)&stp_dbridge[port], 8); + if (stp_cmp) + return stp_cmp; + if (STP_I->port_prio != (uint8_t)(stp_dpid[port] >> 8)) + return STP_I->port_prio < (uint8_t)(stp_dpid[port] >> 8) ? -1 : 1; + if (STP_I->port_id != (uint8_t)stp_dpid[port]) + return STP_I->port_id < (uint8_t)stp_dpid[port] ? -1 : 1; + return 0; } +static uint8_t stp_clamp(uint8_t v, uint8_t lo, uint8_t hi, uint8_t dflt) __reentrant +{ + return (v < lo || v > hi) ? dflt : v; +} + + +static void stp_rcv_info(uint8_t port) __reentrant +{ + if (STP_I->bpdu_type == BPDU_TYPE_RST + && (STP_I->flags & BPDU_ROLE_MASK) != BPDU_ROLE_DESIGNATED) + return; + + ((__xdata uint8_t *)&stp_cost_scratch)[0] = ((__xdata uint8_t *)&STP_I->root_path_cost)[3]; + ((__xdata uint8_t *)&stp_cost_scratch)[1] = ((__xdata uint8_t *)&STP_I->root_path_cost)[2]; + ((__xdata uint8_t *)&stp_cost_scratch)[2] = ((__xdata uint8_t *)&STP_I->root_path_cost)[1]; + ((__xdata uint8_t *)&stp_cost_scratch)[3] = ((__xdata uint8_t *)&STP_I->root_path_cost)[0]; + + if (stp_info_while[port] + && (cmpBytes(STP_I->bridge.mac, stp_dbridge[port].mac, 6) + || STP_I->port_id != (uint8_t)stp_dpid[port]) + && stp_msg_cmp(port) > 0) + return; + + if (!stp_info_while[port] || stp_msg_cmp(port)) + stp_alt_agreed &= ~((uint16_t)1 << port); + + stp_dbridge[port].prio = STP_I->bridge.prio; + stp_dbridge[port].ext = STP_I->bridge.ext; + memcpy(stp_dbridge[port].mac, STP_I->bridge.mac, 6); + stp_droot[port].prio = STP_I->root.prio; + stp_droot[port].ext = STP_I->root.ext; + memcpy(stp_droot[port].mac, STP_I->root.mac, 6); + stp_dpid[port] = ((uint16_t)STP_I->port_prio << 8) | STP_I->port_id; + stp_dcost[port] = stp_cost_scratch; + + stp_rxage[port] = (uint8_t)STP_I->age; + stp_rxmaxage[port] = stp_clamp((uint8_t)STP_I->age_max, 6, 40, 20); + stp_rxhello[port] = stp_clamp((uint8_t)STP_I->hello, 1, 10, 2); + stp_rxfwd[port] = stp_clamp((uint8_t)STP_I->fwd_delay, 4, 30, 15); + stp_info_while[port] = (uint16_t)stp_rxage[port] + 1 <= stp_rxmaxage[port] + ? (uint16_t)3 * stp_rxhello[port] * STP_HZ : 0; +} + + + + +/* Write one port's 2-bit state into the ASIC's MSTP register. + * 00 disable, 01 blocking, 10 learning, 11 forwarding. */ static uint8_t stp_ent_active(uint8_t e) __reentrant { if (e >= STP_ENTITIES) return 0; if (e >= STP_LAG_BASE) return stp_lag_mask[e - STP_LAG_BASE] != 0; - return MACHINE_PORT_VALID(e) && stp_ent_of[e] == e; + if (!MACHINE_PORT_VALID(e)) + return 0; + return stp_ent_of[e] == e; } -static uint8_t stp_ent_has(uint8_t ent, uint8_t p) __reentrant +uint8_t stp_ent_id(uint8_t e) __banked { - if (ent < STP_LAG_BASE) - return ent == p; - return (stp_lag_mask[ent - STP_LAG_BASE] >> p) & 1; + if (!stp_ent_active(e)) + return 0; + if (e >= STP_LAG_BASE) + return e + 101 - STP_LAG_BASE; + return machine.log_to_phys_port[e]; } -static uint8_t stp_state_port(uint8_t e) __reentrant +static uint16_t stp_members(uint8_t e) __reentrant +{ + return e >= STP_LAG_BASE ? stp_lag_mask[e - STP_LAG_BASE] : (uint16_t)1 << e; +} + + +static uint8_t stp_first(uint16_t mask) __reentrant { - if (e < STP_LAG_BASE) - return e; for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) - if ((stp_lag_mask[e - STP_LAG_BASE] >> stp_ss_i) & 1) + if ((mask >> stp_ss_i) & 1) return stp_ss_i; - return 0; -} - - -static void stp_state_bits(uint8_t port, uint8_t state) __reentrant -{ - for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) - if (stp_ent_has(port, stp_ss_i)) - sfr_data[3 - (stp_ss_i >> 2)] |= (uint8_t)(state << ((stp_ss_i << 1) & 0x7)); + return 0xff; } static void stp_state_set(uint8_t port, uint8_t state) __reentrant { + stp_ss_mask = stp_members(port); reg_read_m(RTL837X_MSTP_STATES); for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) { - if (!stp_ent_has(port, stp_ss_i)) + if (!((stp_ss_mask >> stp_ss_i) & 1)) continue; stp_scratch = 3 - (stp_ss_i >> 2); - sfr_data[stp_scratch] &= ~(uint8_t)(STP_ST_MASK << ((stp_ss_i << 1) & 0x7)); + sfr_data[stp_scratch] &= ~(uint8_t)(0b11 << ((stp_ss_i << 1) & 0x7)); sfr_data[stp_scratch] |= (uint8_t)(state << ((stp_ss_i << 1) & 0x7)); } reg_write_m(RTL837X_MSTP_STATES); } -static void stp_ent_apply(uint8_t e) __reentrant +static uint8_t stp_state_get(uint8_t port) __reentrant { - if (!(stp_pflags[e] & STP_PF_ENABLED)) { - stp_state_set(e, STP_ST_FORWARDING); - return; - } - if (stp_pflags[e] & STP_PF_TRIPPED) { - stp_state_set(e, STP_ST_DISABLED); - return; - } - if (stp_pflags[e] & STP_PF_ADMEDGE) { - stp_pflags[e] |= STP_PF_OPEREDGE; - stp_state_set(e, STP_ST_FORWARDING); - return; - } - if (port_timers[e]) - stp_state_set(e, STP_ST_BLOCKING); - else - stp_state_set(e, ((stp_link_prev >> e) & 1) ? STP_ST_FORWARDING : STP_ST_BLOCKING); + if (stp_first(stp_members(port)) == 0xff) + return 0; + reg_read_m(RTL837X_MSTP_STATES); + return (sfr_data[3 - (stp_ss_i >> 2)] >> ((stp_ss_i << 1) & 0x7)) & 0b11; } -/* Signal a topology change. Edge ports are exempt. */ -static void stp_topology_change(uint8_t port) __reentrant +uint8_t stp_port_state(uint8_t port) __banked +{ + return stp_state_get(port); +} + + +static void stp_forget(uint8_t e) __reentrant +{ + stp_ss_mask = stp_members(e); + for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) + if ((stp_ss_mask >> stp_ss_i) & 1) + port_l2_forget_port(stp_ss_i); +} + + +static void stp_new_tc_while(uint8_t port) __reentrant +{ + if (stp_tcwhile[port]) + return; + stp_tcwhile[port] = SEND_RSTP(port) ? ((uint16_t)stp_hello_s + 1) * STP_HZ + : (uint16_t)MAXAGE_S * STP_HZ + FWD_TICKS; + stp_newinfo |= (uint16_t)1 << port; +} + + +static uint8_t stp_tc_prop(uint8_t from) __reentrant +{ + stp_armed = 0; + for (stp_k = 0; stp_k < STP_ENTITIES; stp_k++) { + if (stp_k == from || !stp_ent_active(stp_k) || stp_tcwhile[stp_k] || stp_state_get(stp_k) != 0b11) + continue; + if (!(stp_pflags[stp_k] & STP_PF_ENABLED) || (stp_pflags[stp_k] & STP_PF_OPEREDGE)) + continue; + stp_forget(stp_k); + stp_new_tc_while(stp_k); + stp_armed = 1; + } + return stp_armed; +} + + +static void stp_tc_detected(uint8_t port) __reentrant { if (stp_pflags[port] & STP_PF_OPEREDGE) return; stp_tc_count++; - stp_tc_while = ((uint16_t)stp_maxage_s + stp_fwddelay_s) * STP_HZ; - if (port >= STP_LAG_BASE) { - for (uint8_t i = MACHINE_MIN_PORT; i <= MACHINE_MAX_PORT; i++) - if (stp_ent_has(port, i)) - port_l2_forget_port(i); - } else { - port_l2_forget_port(port); + stp_new_tc_while(port); + stp_tc_prop(port); +} + + +static void stp_forward_now(uint8_t port) __reentrant +{ + port_timers[port] = 0; + stp_loop_held[port] = 0; + stp_backup &= ~((uint16_t)1 << port); + stp_state_set(port, 0b11); + print_string("STP: rapid transition, port forwarding "); + print_port_nl(port); + stp_tc_detected(port); +} + + +static void stp_sync(uint8_t from) __reentrant +{ + for (stp_k = 0; stp_k < STP_ENTITIES; stp_k++) { + if (!stp_ent_active(stp_k)) + continue; + if (stp_k == from || stp_k == stp_root_port || !(stp_pflags[stp_k] & STP_PF_ENABLED) + || (stp_pflags[stp_k] & STP_PF_OPEREDGE) || ((stp_alt >> stp_k) & 1) + || !((stp_link_prev >> stp_k) & 1)) + continue; + if (stp_state_get(stp_k) != 0b01) { + stp_state_set(stp_k, 0b01); + port_timers[stp_k] = FWD_TICKS; + } + stp_newinfo |= (uint16_t)1 << stp_k; } + if (from != stp_root_port) + return; + stp_synced_root = from; + if (port_timers[from]) + stp_forward_now(from); } @@ -426,7 +555,7 @@ static void stp_topology_change(uint8_t port) __reentrant */ static void stp_loop_hold_peer(uint8_t port) __reentrant { - if (port >= STP_ENTITIES || !stp_ent_active(port)) + if (!stp_ent_active(port)) return; if (!(stp_pflags[port] & STP_PF_ENABLED)) return; @@ -435,12 +564,14 @@ static void stp_loop_hold_peer(uint8_t port) __reentrant if (!port_timers[port]) { print_string("STP: loop detected, blocking port "); print_port_nl(port); - stp_state_set(port, STP_ST_BLOCKING); + stp_state_set(port, 0b01); stp_pflags[port] &= ~STP_PF_OPEREDGE; - stp_topology_change(port); + stp_forget(port); + stp_tcwhile[port] = 0; } stp_loop_held[port] = 1; - port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; + stp_backup |= (uint16_t)1 << port; + port_timers[port] = FWD_TICKS; } @@ -456,11 +587,155 @@ static void stp_claim_root(void) } +static uint8_t stp_info_fresh(uint8_t port) __reentrant +{ + return stp_info_while[port] != 0; +} + + +static int8_t stp_cmp_root_path(uint8_t a, uint8_t b) __reentrant +{ + stp_cmp = cmpBytes((__xdata uint8_t *)&stp_droot[a], (__xdata uint8_t *)&stp_droot[b], 8); + if (stp_cmp) + return stp_cmp; + if (stp_rpcost[a] != stp_rpcost[b]) + return stp_rpcost[a] < stp_rpcost[b] ? -1 : 1; + stp_cmp = cmpBytes((__xdata uint8_t *)&stp_dbridge[a], (__xdata uint8_t *)&stp_dbridge[b], 8); + if (stp_cmp) + return stp_cmp; + if (stp_dpid[a] != stp_dpid[b]) + return stp_dpid[a] < stp_dpid[b] ? -1 : 1; + if (stp_pprio[a] != stp_pprio[b]) + return stp_pprio[a] < stp_pprio[b] ? -1 : 1; + return a < b ? -1 : 1; +} + + +static int8_t stp_cmp_designated(uint8_t port) __reentrant +{ + stp_cmp = cmpBytes((__xdata uint8_t *)&stp_droot[port], (__xdata uint8_t *)&root_bridge, 8); + if (stp_cmp) + return stp_cmp; + if (stp_dcost[port] != root_bridge_cost) + return stp_dcost[port] < root_bridge_cost ? -1 : 1; + stp_self.prio = stp_prio; + stp_self.ext = 0x00; + memcpy(stp_self.mac, uip_ethaddr.addr, 6); + stp_cmp = cmpBytes((__xdata uint8_t *)&stp_dbridge[port], (__xdata uint8_t *)&stp_self, 8); + if (stp_cmp) + return stp_cmp; + if (stp_dpid[port] == (((uint16_t)stp_pprio[port] << 8) | (port + 1))) + return 0; + return stp_dpid[port] < (((uint16_t)stp_pprio[port] << 8) | (port + 1)) ? -1 : 1; +} + + +static void stp_reroot_tree(void) __reentrant +{ + for (stp_k = 0; stp_k < STP_ENTITIES; stp_k++) { + if (!stp_ent_active(stp_k)) + continue; + if (stp_k == stp_root_port || !stp_rrwhile[stp_k] + || !(stp_pflags[stp_k] & STP_PF_ENABLED) || (stp_pflags[stp_k] & STP_PF_OPEREDGE)) + continue; + stp_reroot |= (uint16_t)1 << stp_k; + if (stp_state_get(stp_k) != 0b01) { + stp_state_set(stp_k, 0b01); + port_timers[stp_k] = FWD_TICKS; + stp_newinfo |= (uint16_t)1 << stp_k; + } + } +} + + +static void stp_reselect(void) +{ + stp_self.prio = stp_prio; + stp_self.ext = 0x00; + memcpy(stp_self.mac, uip_ethaddr.addr, 6); + + stp_best = 0xff; + for (stp_j = 0; stp_j < STP_ENTITIES; stp_j++) { + if (!stp_ent_active(stp_j)) + continue; + if (!(stp_pflags[stp_j] & STP_PF_ENABLED) || (stp_pflags[stp_j] & STP_PF_ROOTGUARD)) + continue; + if (!((stp_link_now >> stp_j) & 1) || !stp_info_fresh(stp_j)) + continue; + if (!cmpBytes(stp_dbridge[stp_j].mac, uip_ethaddr.addr, 6)) + continue; + if (cmpBytes((__xdata uint8_t *)&stp_droot[stp_j], (__xdata uint8_t *)&stp_self, 8) >= 0) + continue; + stp_rpcost[stp_j] = stp_dcost[stp_j] + PCOST(stp_j); + if (stp_best == 0xff || stp_cmp_root_path(stp_j, stp_best) < 0) + stp_best = stp_j; + } + + if (stp_best != 0xff) { + if (stp_root_port != stp_best + || cmpBytes((__xdata uint8_t *)&root_bridge, (__xdata uint8_t *)&stp_droot[stp_best], 8)) { + if (cmpBytes((__xdata uint8_t *)&root_bridge, (__xdata uint8_t *)&stp_droot[stp_best], 8)) + print_string("Updating Root bridge\n"); + stp_synced_root = 0xff; + stp_root_port = stp_best; + root_bridge.prio = stp_droot[stp_best].prio; + root_bridge.ext = stp_droot[stp_best].ext; + memcpy(root_bridge.mac, stp_droot[stp_best].mac, 6); + stp_tc_count++; + } + root_bridge_cost = stp_rpcost[stp_best]; + stp_msg_age = stp_rxage[stp_best]; + stp_root_maxage = stp_rxmaxage[stp_best]; + stp_root_fwd = stp_rxfwd[stp_best]; + } else if (stp_root_port != 0xff) { + print_string("STP: root aged out, claiming root\n"); + stp_synced_root = 0xff; + stp_claim_root(); + stp_tc_count++; + } + + for (stp_j = 0; stp_j < STP_ENTITIES; stp_j++) { + if (!stp_ent_active(stp_j)) + continue; + if (!(stp_pflags[stp_j] & STP_PF_ENABLED)) + continue; + if (stp_j != stp_root_port && stp_info_fresh(stp_j) + && stp_cmp_designated(stp_j) < 0) { + stp_pflags[stp_j] &= ~STP_PF_OPEREDGE; + port_timers[stp_j] = 0; + stp_state_set(stp_j, 0b01); + if (!((stp_alt >> stp_j) & 1)) { + stp_alt |= (uint16_t)1 << stp_j; + stp_forget(stp_j); + stp_tcwhile[stp_j] = 0; + print_string("STP: better bridge on the segment, blocking port "); + print_port_nl(stp_j); + } + continue; + } + if (!((stp_alt >> stp_j) & 1)) + continue; + stp_alt &= ~((uint16_t)1 << stp_j); + stp_alt_agreed &= ~((uint16_t)1 << stp_j); + port_timers[stp_j] = FWD_TICKS; + print_string("STP: port released, listening "); + print_port_nl(stp_j); + } + + if (stp_rstp && stp_root_port != 0xff && port_timers[stp_root_port] + && !((stp_alt >> stp_root_port) & 1) && !stp_rbwhile[stp_root_port]) { + stp_reroot_tree(); + stp_forward_now(stp_root_port); + } +} + + void stp_cnf_send(uint8_t port) __reentrant { /* A one-shot flag (TCA) belongs to the BPDU we were asked to send: drop * it with the frame, or it would surface on an unrelated port later. */ - if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED))) { + if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED)) + || !((stp_link_prev >> port) & 1) || !(stp_members(port) & stp_link_phys)) { stp_tx_flags_extra = 0; return; } @@ -469,7 +744,8 @@ void stp_cnf_send(uint8_t port) __reentrant return; } stp_tx_budget[port]--; - stp_tx_count[port]++; + stp_cnt[STP_CNT_TX][port]++; + stp_tcn = !SEND_RSTP(port) && port == stp_root_port; STP_O->stp_addr[0] = 0x01; STP_O->stp_addr[1] = 0x80; STP_O->stp_addr[2] = 0xc2; STP_O->stp_addr[3] = STP_O->stp_addr[4] = STP_O->stp_addr[5] = 0x00; @@ -478,42 +754,47 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; STP_O->rtl_tag.reason = 0x00; STP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS); - if (port >= STP_LAG_BASE) { - stp_scratch = 0; - while (stp_scratch < STP_PORTS && !stp_ent_has(port, stp_scratch)) - stp_scratch++; - if (stp_scratch >= STP_PORTS) { - stp_tx_flags_extra = 0; - return; - } - STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << stp_scratch); - } else { - STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port); - } + STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << stp_first(stp_members(port) & stp_link_phys)); STP_O->dsap = 0x42; STP_O->ssap = 0x42; STP_O->ctrl = 0x03; STP_O->proto = 0x0000; - if (stp_rstp) { + if (stp_tcn) { + STP_O->msg_len = HTONS(BPDU_LEN_TCN); + STP_O->version = BPDU_VER_STP; + STP_O->bpdu_type = BPDU_TYPE_TCN; + STP_O->flags = 0x00; + } else if (SEND_RSTP(port)) { STP_O->msg_len = HTONS(BPDU_LEN_RST); STP_O->version = BPDU_VER_RSTP; STP_O->bpdu_type = BPDU_TYPE_RST; - reg_read_m(RTL837X_MSTP_STATES); - stp_scratch = stp_state_port(port); - STP_O->flags = port == stp_root_port ? BPDU_ROLE_ROOT : BPDU_ROLE_DESIGNATED; - if (((sfr_data[3 - (stp_scratch >> 2)] >> ((stp_scratch << 1) & 0x7)) & STP_ST_MASK) == STP_ST_FORWARDING) + STP_O->flags = port == stp_root_port ? BPDU_ROLE_ROOT + : (((stp_alt | stp_backup) >> port) & 1) ? BPDU_ROLE_ALTBACK : BPDU_ROLE_DESIGNATED; + stp_scratch = stp_state_get(port); + if (stp_scratch == 0b11) STP_O->flags |= BPDU_FLAG_LEARNING | BPDU_FLAG_FORWARDING; + else if (stp_scratch == 0b10) + STP_O->flags |= BPDU_FLAG_LEARNING; + if (port != stp_root_port && port_timers[port] && P2P(port) + && !(stp_pflags[port] & STP_PF_OPEREDGE) && !(((stp_alt | stp_backup) >> port) & 1)) + STP_O->flags |= BPDU_FLAG_PROPOSAL; + if ((stp_agree >> port) & 1) { + STP_O->flags |= BPDU_FLAG_AGREEMENT; + stp_agree &= ~((uint16_t)1 << port); + } } else { STP_O->msg_len = HTONS(BPDU_LEN_CONFIG); STP_O->version = BPDU_VER_STP; STP_O->bpdu_type = BPDU_TYPE_CONFIG; STP_O->flags = 0x00; } - if (stp_tc_while) + if (stp_tcwhile[port]) STP_O->flags |= BPDU_FLAG_TC; STP_O->flags |= stp_tx_flags_extra; stp_tx_flags_extra = 0; + if (stp_tcn || (STP_O->flags & BPDU_FLAG_TC)) + stp_cnt[STP_CNT_TCTX][port]++; memcpy(STP_O->src_addr, uip_ethaddr.addr, 6); STP_O->src_addr[0] |= 0x02; @@ -539,16 +820,143 @@ void stp_cnf_send(uint8_t port) __reentrant * little-endian, so assigning the plain second count lands the value in * the high (seconds) octet - see age_max/hello/fwd_delay below. */ STP_O->age = (stp_root_port == 0xff) ? 0 : (uint16_t)(stp_msg_age + 1); - STP_O->age_max = stp_maxage_s; + STP_O->age_max = MAXAGE_S; STP_O->hello = stp_hello_s; - STP_O->fwd_delay = stp_fwddelay_s; + STP_O->fwd_delay = FWD_S; STP_O->version1_length = 0; /* RST BPDU: no version-1 information */ - uip_len = stp_rstp ? sizeof(struct stp_pkt) : sizeof(struct stp_pkt) - 1; + uip_len = stp_tcn ? sizeof(struct stp_pkt) - 32 + : (SEND_RSTP(port) ? sizeof(struct stp_pkt) : sizeof(struct stp_pkt) - 1); tcpip_output(); } +static uint8_t stp_len_ok(void) __reentrant +{ + if (STP_I->bpdu_type == BPDU_TYPE_TCN) + return HTONS(STP_I->msg_len) >= BPDU_LEN_TCN; + if (STP_I->bpdu_type == BPDU_TYPE_RST) + return HTONS(STP_I->msg_len) >= BPDU_LEN_RST; + return HTONS(STP_I->msg_len) >= BPDU_LEN_CONFIG; +} + + +static void stp_rx_seen(uint8_t port) __reentrant +{ + stp_bpdu_age[port] = 0; + stp_cnt[STP_CNT_RX][port]++; + if (stp_len_ok() && (STP_I->bpdu_type == BPDU_TYPE_TCN || (STP_I->flags & BPDU_FLAG_TC))) + stp_cnt[STP_CNT_TCRX][port]++; + if (!stp_rstp) + return; + if (STP_I->version < BPDU_VER_RSTP) + stp_seen_stp |= (uint16_t)1 << port; + else + stp_seen_rstp |= (uint16_t)1 << port; +} + + +static void stp_migrate_check(uint8_t port) __reentrant +{ + if (stp_mdelay[port]) { + stp_mdelay[port]--; + return; + } + if (!((stp_legacy >> port) & 1) && ((stp_seen_stp >> port) & 1)) { + stp_legacy |= (uint16_t)1 << port; + print_string("STP: 802.1D neighbour, sending STP on port "); + } else if (((stp_legacy >> port) & 1) && ((stp_seen_rstp >> port) & 1)) { + stp_legacy &= ~((uint16_t)1 << port); + print_string("STP: RSTP neighbour, sending RSTP on port "); + } else { + return; + } + print_port_nl(port); + stp_seen_stp &= ~((uint16_t)1 << port); + stp_seen_rstp &= ~((uint16_t)1 << port); + stp_mdelay[port] = STP_MIGRATE; + stp_newinfo |= (uint16_t)1 << port; +} + + +static int8_t stp_msg_vs_ours(uint8_t port) __reentrant +{ + stp_cmp = cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&root_bridge, 8); + if (stp_cmp) + return stp_cmp; + ((__xdata uint8_t *)&stp_cost_scratch)[0] = ((__xdata uint8_t *)&STP_I->root_path_cost)[3]; + ((__xdata uint8_t *)&stp_cost_scratch)[1] = ((__xdata uint8_t *)&STP_I->root_path_cost)[2]; + ((__xdata uint8_t *)&stp_cost_scratch)[2] = ((__xdata uint8_t *)&STP_I->root_path_cost)[1]; + ((__xdata uint8_t *)&stp_cost_scratch)[3] = ((__xdata uint8_t *)&STP_I->root_path_cost)[0]; + if (stp_cost_scratch != root_bridge_cost) + return stp_cost_scratch < root_bridge_cost ? -1 : 1; + stp_self.prio = stp_prio; + stp_self.ext = 0x00; + memcpy(stp_self.mac, uip_ethaddr.addr, 6); + stp_cmp = cmpBytes((__xdata uint8_t *)&STP_I->bridge, (__xdata uint8_t *)&stp_self, 8); + if (stp_cmp) + return stp_cmp; + if (STP_I->port_prio != stp_pprio[port]) + return STP_I->port_prio < stp_pprio[port] ? -1 : 1; + if (STP_I->port_id != port + 1) + return STP_I->port_id < port + 1 ? -1 : 1; + return 0; +} + + +static void stp_dispute_rx(uint8_t port) __reentrant +{ + if (port == stp_root_port || ((stp_alt >> port) & 1)) + return; + if (STP_I->bpdu_type == BPDU_TYPE_RST + && (STP_I->flags & BPDU_ROLE_MASK) != BPDU_ROLE_DESIGNATED) + return; + if (stp_msg_vs_ours(port) <= 0) + return; + stp_newinfo |= (uint16_t)1 << port; + if (!stp_rstp || STP_I->bpdu_type != BPDU_TYPE_RST + || !(STP_I->flags & BPDU_FLAG_LEARNING)) + return; + if (stp_state_get(port) != 0b01) { + stp_state_set(port, 0b01); + print_string("STP: dispute, port discarding "); + print_port_nl(port); + } + port_timers[port] = FWD_TICKS; +} + + +static void stp_rapid_rx(uint8_t port) __reentrant +{ + if (stp_rstp && STP_I->bpdu_type == BPDU_TYPE_RST && P2P(port)) { + if (port == stp_root_port + && (STP_I->flags & BPDU_ROLE_MASK) == BPDU_ROLE_DESIGNATED + && (STP_I->flags & BPDU_FLAG_PROPOSAL)) { + if (stp_synced_root != port) + stp_sync(port); + stp_agree |= (uint16_t)1 << port; + stp_newinfo |= (uint16_t)1 << port; + } else if ((((stp_alt | stp_backup) >> port) & 1) + && (STP_I->flags & BPDU_ROLE_MASK) == BPDU_ROLE_DESIGNATED + && (STP_I->flags & BPDU_FLAG_PROPOSAL)) { + if (!((stp_alt_agreed >> port) & 1)) + stp_sync(port); + stp_alt_agreed |= (uint16_t)1 << port; + stp_agree |= (uint16_t)1 << port; + stp_newinfo |= (uint16_t)1 << port; + } else if (port != stp_root_port && port_timers[port] + && !((stp_alt >> port) & 1) + && !(((stp_reroot >> port) & 1) && stp_rrwhile[port]) + && ((STP_I->flags & BPDU_ROLE_MASK) == BPDU_ROLE_ROOT + || (STP_I->flags & BPDU_ROLE_MASK) == BPDU_ROLE_ALTBACK) + && (STP_I->flags & BPDU_FLAG_AGREEMENT) + && !cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&root_bridge, 8)) { + stp_forward_now(port); + } + } +} + + void stp_in(void) __banked { uint8_t port; @@ -588,28 +996,36 @@ void stp_in(void) __banked print_string("STP: BPDU guard tripped, disabling port "); print_port_nl(port); stp_pflags[port] |= STP_PF_TRIPPED; - stp_state_set(port, STP_ST_DISABLED); + stp_state_set(port, 0b00); stp_tc_count++; return; } - stp_bpdu_age[port] = 0; + stp_rx_seen(port); /* A port that hears a BPDU is not an edge port, whatever it decided * during the silence after the link came up. Only the flag is dropped: * the port keeps whatever forwarding state the rules below give it, * rather than being pushed back through the listen period, which would * black-hole a working link for a forward delay on the first BPDU. The - * flag matters beyond the status page, since stp_topology_change() - * exempts edge ports and so would go on skipping the counter and the - * L2 flush for a port that has a bridge behind it. */ + * flag matters beyond the status page, since topology changes skip + * edge ports and would go on skipping the counter and the L2 flush for + * a port that has a bridge behind it. */ stp_pflags[port] &= ~STP_PF_OPEREDGE; + stp_heard |= (uint16_t)1 << port; + + if (!stp_len_ok()) + return; if (STP_I->bpdu_type == BPDU_TYPE_TCN) { + if (stp_state_get(port) != 0b11) + return; stp_tx_flags_extra = BPDU_FLAG_TCACK; stp_cnf_send(port); uip_len = 0; - stp_topology_change(port); + stp_tc_count++; + stp_new_tc_while(port); + stp_tc_prop(port); return; } @@ -647,57 +1063,107 @@ void stp_in(void) __banked return; } - /* Topology Change in transit. The flag arms a short window that our - * own BPDUs copy downstream (the TX side already sends TC while - * stp_tc_while runs) and that each further flagged BPDU refreshes, so - * it expires one hello after the neighbour stops - without shortening - * the long window a local change may have armed. The flush runs once, - * on the arming edge: everything learned on the other non-edge ports - * may sit behind the moved link and must be relearned. */ - if (STP_I->flags & BPDU_FLAG_TC) { - if (!stp_tc_while) { - uint8_t i; - stp_tc_count++; - for (i = MACHINE_MIN_PORT; i <= MACHINE_MAX_PORT; i++) - if (!stp_ent_has(port, i) - && (stp_pflags[stp_ent_of[i]] & STP_PF_ENABLED) - && !(stp_pflags[stp_ent_of[i]] & STP_PF_OPEREDGE)) - port_l2_forget_port(i); - } - if (stp_tc_while < ((uint16_t)stp_hello_s + 1) * STP_HZ) - stp_tc_while = ((uint16_t)stp_hello_s + 1) * STP_HZ; - } - - stp_record_designated(port); - - /* Better root than the one we know? The identifier is priority, system - * ID extension and MAC in that order: comparing the priority byte and - * then jumping to the MAC skipped the twelve bits in between, so two - * bridges differing only in the extension were ranked by MAC. */ - if (cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&root_bridge, 8) < 0) { - /* Root guard: this port must never become our path to the root. */ - if (stp_pflags[port] & STP_PF_ROOTGUARD) { - print_string("STP: root guard blocking port "); - print_port_nl(port); - stp_state_set(port, STP_ST_BLOCKING); - port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; - stp_pflags[port] &= ~STP_PF_OPEREDGE; - return; - } - print_string("Updating Root bridge\n"); - root_bridge.prio = STP_I->root.prio; - root_bridge.ext = STP_I->root.ext; - memcpy(root_bridge.mac, STP_I->root.mac, 6); - stp_root_port = port; + if ((STP_I->flags & BPDU_FLAG_TCACK) && port == stp_root_port) + stp_tcwhile[port] = 0; + if ((STP_I->flags & BPDU_FLAG_TC) && stp_state_get(port) == 0b11 && stp_tc_prop(port)) stp_tc_count++; + + if ((stp_pflags[port] & STP_PF_ROOTGUARD) + && cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&root_bridge, 8) < 0) { + print_string("STP: root guard blocking port "); + print_port_nl(port); + stp_state_set(port, 0b01); + port_timers[port] = FWD_TICKS; + stp_pflags[port] &= ~STP_PF_OPEREDGE; + return; } - /* Refresh our cost to the root when the update comes in on the root port */ - if (port == stp_root_port) { - /* Age of the information we now hold (see the TX note on the wire - * format); saturate rather than wrap on absurd input. */ - stp_msg_age = (STP_I->age > 254) ? 254 : (uint8_t)STP_I->age; - root_bridge_cost = stp_dcost[port] + PCOST(port); + stp_rcv_info(port); + stp_reselect(); + + stp_rapid_rx(port); + stp_dispute_rx(port); +} + + +static void stp_tc_clock(void) __reentrant +{ + if (stp_tc_seen != stp_tc_count) { + stp_tc_seen = stp_tc_count; + stp_tc_secs = 0; + } else if (stp_tc_secs != 0xffffffffUL) { + stp_tc_secs++; + } +} + + +void stp_lag_map(void) __banked +{ + for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) + stp_ent_of[stp_ss_i] = stp_ss_i; + for (stp_lag = 0; stp_lag < STP_LAG_COUNT; stp_lag++) { + stp_ss_mask = port_lag_members_get(stp_lag); + if (stp_ss_mask != stp_lag_mask[stp_lag]) { + stp_map_changed |= stp_ss_mask | stp_lag_mask[stp_lag] + | (uint16_t)1 << (STP_LAG_BASE + stp_lag); + stp_lag_mask[stp_lag] = stp_ss_mask; + } + for (stp_ss_i = 0; stp_ss_i < STP_PORTS; stp_ss_i++) + if ((stp_ss_mask >> stp_ss_i) & 1) + stp_ent_of[stp_ss_i] = STP_LAG_BASE + stp_lag; + } +} + + +static void stp_links_read(void) __reentrant +{ + reg_read_m(RTL837X_REG_LINKS_STS); + stp_link_phys = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8); + stp_link_now = 0; + for (stp_k = 0; stp_k < STP_ENTITIES; stp_k++) { + if (!stp_ent_active(stp_k) || stp_first(stp_members(stp_k) & stp_link_phys) == 0xff) + continue; + stp_link_now |= (uint16_t)1 << stp_k; + if (stp_k >= STP_LAG_BASE) + stp_pspeed[stp_k] = stp_pspeed[stp_ss_i]; + } +} + + +static void stp_ent_reset(uint8_t e) __reentrant +{ + stp_ent_bit = (uint16_t)1 << e; + stp_alt &= ~stp_ent_bit; + stp_backup &= ~stp_ent_bit; + stp_alt_agreed &= ~stp_ent_bit; + stp_legacy &= ~stp_ent_bit; + stp_seen_stp &= ~stp_ent_bit; + stp_seen_rstp &= ~stp_ent_bit; + stp_agree &= ~stp_ent_bit; + stp_reroot &= ~stp_ent_bit; + stp_heard &= ~stp_ent_bit; + stp_newinfo &= ~stp_ent_bit; + stp_link_prev = (stp_link_prev & ~stp_ent_bit) | (stp_link_now & stp_ent_bit); + stp_pflags[e] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); + stp_loop_held[e] = 0; + stp_bpdu_age[e] = 0; + stp_tx_budget[e] = stp_txhold; + stp_info_while[e] = 0; + stp_tcwhile[e] = 0; + stp_mdelay[e] = STP_MIGRATE; + stp_rrwhile[e] = 0; + stp_rbwhile[e] = 0; + port_timers[e] = 0; + port_hello[e] = (uint16_t)stp_hello_s * STP_HZ; + if (!stp_ent_active(e)) + return; + if (!(stp_pflags[e] & STP_PF_ENABLED) || (stp_pflags[e] & STP_PF_ADMEDGE)) { + if (stp_pflags[e] & STP_PF_ADMEDGE) + stp_pflags[e] |= STP_PF_OPEREDGE; + stp_state_set(e, 0b11); + } else { + stp_state_set(e, 0b01); + port_timers[e] = FWD_TICKS; } } @@ -709,32 +1175,7 @@ void stp_timers(void) __banked stp_sec_tick = 0; for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) stp_tx_budget[stp_i] = stp_txhold; - - stp_lag_map(); - if (stp_map_dirty) { - stp_map_dirty = 0; - for (stp_i = 0; stp_i < STP_LAG_BASE; stp_i++) { - if (!MACHINE_PORT_VALID(stp_i)) - continue; - if (stp_ent_of[stp_i] != stp_i) { - stp_loop_held[stp_i] = 0; - port_timers[stp_i] = 0; - } else { - stp_ent_apply(stp_i); - } - } - for (stp_i = STP_LAG_BASE; stp_i < STP_ENTITIES; stp_i++) { - if (stp_lag_mask[stp_i - STP_LAG_BASE]) { - stp_ent_apply(stp_i); - } else { - stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); - stp_loop_held[stp_i] = 0; - port_timers[stp_i] = 0; - } - } - if (stp_root_port != 0xff && !stp_ent_active(stp_root_port)) - stp_claim_root(); - } + stp_tc_clock(); /* Link supervision. Without this the state machine never learns * that a port lost carrier: it keeps the port in forwarding, keeps @@ -742,44 +1183,64 @@ void stp_timers(void) __banked * yet losing a link is the most ordinary topology change there is. * Once per second is soon enough, and it keeps register reads out * of the 50 Hz tick. */ - reg_read_m(RTL837X_REG_LINKS_STS); - stp_link_now = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8); - { - __xdata static uint16_t eff; - eff = 0; - for (stp_i = MACHINE_MIN_PORT; stp_i <= MACHINE_MAX_PORT; stp_i++) - if ((stp_link_now >> stp_i) & 1) - eff |= (uint16_t)1 << stp_ent_of[stp_i]; - stp_link_now = eff; + reg_read_m(RTL837X_REG_LINKS); + for (stp_i = MACHINE_MIN_PORT; stp_i <= MACHINE_MAX_PORT; stp_i++) { + if (stp_i == 8) + reg_read_m(RTL837X_REG_LINKS_89); + stp_pspeed[stp_i] = (stp_i & 1) + ? (sfr_data[3 - ((stp_i & 7) >> 1)] >> 4) + : (sfr_data[3 - ((stp_i & 7) >> 1)] & 0xf); } + stp_lag_map(); + stp_links_read(); + if (stp_map_changed) { + for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) + if ((stp_map_changed >> stp_i) & 1) + stp_ent_reset(stp_i); + stp_map_changed = 0; + } + if (stp_link_now != stp_link_prev) { for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) { - if (stp_i < STP_PORTS && !MACHINE_PORT_VALID(stp_i)) - continue; - if (stp_i < STP_PORTS && stp_ent_of[stp_i] != stp_i) + if (!stp_ent_active(stp_i)) continue; if (!(stp_pflags[stp_i] & STP_PF_ENABLED)) continue; if (!((stp_link_now ^ stp_link_prev) >> stp_i & 1)) continue; /* Either way the port must stop forwarding first. */ - stp_state_set(stp_i, STP_ST_BLOCKING); + stp_state_set(stp_i, 0b01); if ((stp_link_now >> stp_i) & 1) { /* Carrier back: re-run the listen period rather than * forwarding straight away - the segment may have been * rewired while we were down. Auto edge still applies. */ - port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ; + port_timers[stp_i] = FWD_TICKS; stp_pflags[stp_i] &= ~STP_PF_OPEREDGE; + stp_alt &= ~((uint16_t)1 << stp_i); stp_bpdu_age[stp_i] = 0; + stp_newinfo |= (uint16_t)1 << stp_i; + stp_legacy &= ~((uint16_t)1 << stp_i); + stp_seen_stp &= ~((uint16_t)1 << stp_i); + stp_seen_rstp &= ~((uint16_t)1 << stp_i); + stp_mdelay[stp_i] = STP_MIGRATE; } else { port_timers[stp_i] = 0; + stp_info_while[stp_i] = 0; + stp_rrwhile[stp_i] = 0; + stp_reroot &= ~((uint16_t)1 << stp_i); + stp_backup &= ~((uint16_t)1 << stp_i); + stp_alt_agreed &= ~((uint16_t)1 << stp_i); + stp_heard &= ~((uint16_t)1 << stp_i); + stp_alt &= ~((uint16_t)1 << stp_i); print_string("STP: link down, port blocking "); print_port_nl(stp_i); - stp_topology_change(stp_i); + stp_forget(stp_i); + stp_tcwhile[stp_i] = 0; } } stp_link_prev = stp_link_now; } + stp_reselect(); } for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) { @@ -788,6 +1249,19 @@ void stp_timers(void) __banked if (stp_bpdu_age[stp_i] < 0xffff) stp_bpdu_age[stp_i]++; + if (stp_info_while[stp_i] && !--stp_info_while[stp_i]) + stp_reselect_due = 1; + if (stp_tcwhile[stp_i]) + stp_tcwhile[stp_i]--; + if (stp_i == stp_root_port) + stp_rrwhile[stp_i] = FWD_TICKS; + else if (stp_rrwhile[stp_i] && !--stp_rrwhile[stp_i]) + stp_reroot &= ~((uint16_t)1 << stp_i); + if ((stp_backup >> stp_i) & 1) + stp_rbwhile[stp_i] = (uint16_t)2 * stp_hello_s * STP_HZ; + else if (stp_rbwhile[stp_i]) + stp_rbwhile[stp_i]--; + stp_migrate_check(stp_i); /* Periodic hello */ if (port_hello[stp_i]) @@ -798,43 +1272,63 @@ void stp_timers(void) __banked * where our own root information comes FROM, and echoing it back * there just feeds the upstream bridge its own data (and looks * like a competing designated bridge on that segment). */ - if (stp_i != stp_root_port) + if (((stp_agree >> stp_i) & 1) || (!((stp_alt >> stp_i) & 1) && (stp_i != stp_root_port || stp_tcwhile[stp_i]))) + stp_cnf_send(stp_i); + } + if ((stp_newinfo >> stp_i) & 1) { + stp_newinfo &= ~((uint16_t)1 << stp_i); + if (((stp_agree >> stp_i) & 1) || (!((stp_alt >> stp_i) & 1) && (stp_i != stp_root_port || stp_tcwhile[stp_i]))) stp_cnf_send(stp_i); } /* Promote a port out of blocking once its listen period expires * with no reason to stay blocked (no better root heard: we are * the designated bridge on that port). */ + if (port_timers[stp_i] && ((stp_alt >> stp_i) & 1)) + port_timers[stp_i] = 0; + if (port_timers[stp_i]) { if (!--port_timers[stp_i]) { - stp_loop_held[stp_i] = 0; - stp_state_set(stp_i, STP_ST_FORWARDING); - print_string("STP: port forwarding "); - print_port_nl(stp_i); - stp_topology_change(stp_i); + if (((stp_reroot >> stp_i) & 1) && stp_rrwhile[stp_i]) { + port_timers[stp_i] = stp_rrwhile[stp_i] + 1; + } else if (stp_state_get(stp_i) == 0b01) { + stp_state_set(stp_i, 0b10); + port_timers[stp_i] = FWD_TICKS; + print_string("STP: port learning "); + print_port_nl(stp_i); + } else { + /* A returned BPDU can hold its sending port even though + * that port never received one. Once the hold expires, + * recover auto-edge before the forwarding timer stops. */ + if (stp_loop_held[stp_i] && (stp_pflags[stp_i] & STP_PF_AUTOEDGE) + && !((stp_heard >> stp_i) & 1) + && stp_bpdu_age[stp_i] > STP_EDGE_DELAY) + stp_pflags[stp_i] |= STP_PF_OPEREDGE; + stp_loop_held[stp_i] = 0; + stp_backup &= ~((uint16_t)1 << stp_i); + stp_state_set(stp_i, 0b11); + print_string("STP: port forwarding "); + print_port_nl(stp_i); + stp_tc_detected(stp_i); + } } else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE) && !stp_loop_held[stp_i] + && !((stp_heard >> stp_i) & 1) && stp_bpdu_age[stp_i] > STP_EDGE_DELAY) { /* Auto edge: nothing talks (R)STP on this port - it is * host-facing, go to forwarding without the full wait. */ port_timers[stp_i] = 0; stp_pflags[stp_i] |= STP_PF_OPEREDGE; - stp_state_set(stp_i, STP_ST_FORWARDING); + stp_state_set(stp_i, 0b11); print_string("STP: edge port forwarding "); print_port_nl(stp_i); } } } - if (stp_tc_while) - stp_tc_while--; - - /* Age out a root that went silent: reclaim the tree. */ - if (stp_root_port != 0xff - && stp_bpdu_age[stp_root_port] > (uint16_t)stp_maxage_s * STP_HZ) { - print_string("STP: root aged out, claiming root\n"); - stp_claim_root(); - stp_tc_count++; + if (stp_reselect_due) { + stp_reselect_due = 0; + stp_reselect(); } } @@ -849,11 +1343,13 @@ void stp_defaults(void) __banked stp_fwddelay_s = 15; stp_rstp = 1; stp_txhold = 6; + stp_pcost_short = 0; + stp_bpdu_filter = 0; for (stp_i = 0; stp_i < STP_PORTS; stp_i++) stp_ent_of[stp_i] = stp_i; for (stp_i = 0; stp_i < STP_LAG_COUNT; stp_i++) stp_lag_mask[stp_i] = 0; - stp_map_dirty = 0; + stp_map_changed = 0; for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) { /* enabled, auto-edge on: host-facing ports go forwarding after * 3 s of BPDU silence instead of the full forward delay */ @@ -864,9 +1360,24 @@ void stp_defaults(void) __banked port_timers[stp_i] = 0; port_hello[stp_i] = 0; stp_tx_budget[stp_i] = 6; + stp_info_while[stp_i] = 0; + stp_tcwhile[stp_i] = 0; + stp_mdelay[stp_i] = 0; + stp_rrwhile[stp_i] = 0; + stp_rbwhile[stp_i] = 0; } + stp_reroot = 0; + stp_newinfo = 0; + stp_legacy = 0; + stp_seen_stp = 0; + stp_seen_rstp = 0; + stp_agree = 0; + stp_synced_root = 0xff; + stp_backup = 0; + stp_alt_agreed = 0; + stp_heard = 0; + stp_alt = 0; stp_tc_count = 0; - stp_tc_while = 0; stp_claim_root(); } @@ -897,54 +1408,54 @@ static void stp_fdb_update(__xdata uint16_t pmask) } +static void stp_times_check(void) __reentrant +{ + if ((uint16_t)2 * (stp_fwddelay_s - 1) >= stp_maxage_s + && stp_maxage_s >= (uint16_t)2 * (stp_hello_s + 1)) + return; + print_string("STP: timers break 2*(fwd-1) >= maxage >= 2*(hello+1), using 2/20/15\n"); + stp_hello_s = 2; + stp_maxage_s = 20; + stp_fwddelay_s = 15; +} + + +void stp_counters_clear(void) __banked __reentrant +{ + for (uint8_t i = 0; i < STP_CNT_N * STP_ENTITIES; i++) + ((__xdata uint32_t *)stp_cnt)[i] = 0; +} + + void stp_setup(void) __banked { + stp_times_check(); + stp_counters_clear(); + stp_tc_seen = stp_tc_count; + stp_tc_secs = 0; print_string("Enabling STP: "); - stp_lag_map(); + stp_claim_root(); + stp_newinfo = 0; + stp_reroot = 0; + stp_legacy = 0; + stp_seen_stp = 0; + stp_seen_rstp = 0; + stp_agree = 0; + stp_synced_root = 0xff; + stp_backup = 0; + stp_alt_agreed = 0; sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; - for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) { - stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); - stp_loop_held[stp_i] = 0; - stp_bpdu_age[stp_i] = 0; - stp_tx_budget[stp_i] = stp_txhold; - stp_tx_count[stp_i] = 0; - port_timers[stp_i] = 0; - port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ; - if (stp_i < STP_PORTS && !MACHINE_PORT_VALID(stp_i)) - continue; - if (stp_i >= STP_LAG_BASE && !stp_lag_mask[stp_i - STP_LAG_BASE]) - continue; - if (stp_i < STP_PORTS && stp_ent_of[stp_i] != stp_i) - continue; - if (!(stp_pflags[stp_i] & STP_PF_ENABLED) - || (stp_pflags[stp_i] & STP_PF_ADMEDGE)) { - /* not participating, or admin edge: forwarding immediately */ - if (stp_pflags[stp_i] & STP_PF_ADMEDGE) - stp_pflags[stp_i] |= STP_PF_OPEREDGE; - stp_state_bits(stp_i, STP_ST_FORWARDING); - port_timers[stp_i] = 0; - } else { - /* listen first: blocking until the forward-delay expires */ - stp_state_bits(stp_i, STP_ST_BLOCKING); - port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ; - } - } sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) reg_write_m(RTL837X_MSTP_STATES); + stp_lag_map(); + stp_links_read(); + stp_link_prev = stp_link_now; + stp_map_changed = 0; + for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) + stp_ent_reset(stp_i); print_reg(RTL837X_MSTP_STATES); write_char('\n'); - /* Seed the carrier bitmap, so turning STP on does not report every - * port that was already down as a fresh topology change. */ - reg_read_m(RTL837X_REG_LINKS_STS); - stp_link_now = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8); - stp_link_prev = 0; - for (stp_i = MACHINE_MIN_PORT; stp_i <= MACHINE_MAX_PORT; stp_i++) - if ((stp_link_now >> stp_i) & 1) - stp_link_prev |= (uint16_t)1 << stp_ent_of[stp_i]; - - stp_claim_root(); - trap_rma_set(RTL837X_RMA0_CONF, RTL837X_RMA_ACT_TRAP); } @@ -952,11 +1463,8 @@ void stp_setup(void) __banked void stp_off(void) __banked { sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; - for (stp_i = MACHINE_MIN_PORT; stp_i <= MACHINE_MAX_PORT; stp_i++) { - // Set STP port state to forwarding - // States are: 00 disable, 01 blocking, 10 learning, 11 forwarding - sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(STP_ST_FORWARDING << ((stp_i << 1) & 0x7)); - } + for (stp_i = MACHINE_MIN_PORT; stp_i <= MACHINE_MAX_PORT; stp_i++) + sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7)); for (stp_i = 0; stp_i < STP_ENTITIES; stp_i++) { stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); stp_loop_held[stp_i] = 0; @@ -965,173 +1473,55 @@ void stp_off(void) __banked sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) reg_write_m(RTL837X_MSTP_STATES); - /* Restore BPDU transparency: flood them again like an unmanaged switch. */ - stp_fdb_update(PMASK_CPU | (machine_detected.isRTL8373 ? PMASK_9 : PMASK_6)); - trap_rma_set(RTL837X_RMA0_CONF, RTL837X_RMA_ACT_FORWARD); -} - - -void stp_parse(void) __banked __reentrant -{ - uint8_t ent; - - if (cmd_compare(1, "on")) { - print_string("STP enabled\n"); - stp_enabled = 1; - stp_setup(); - return; - } - if (cmd_compare(1, "off")) { - print_string("STP disabled\n"); - stp_off(); - stp_enabled = 0; - return; - } - if (cmd_compare(1, "status")) { - stp_status(); - return; - } - if (cmd_words_len < 3) - goto err; - - if (cmd_compare(1, "port") || cmd_compare(1, "lag")) { - stp_lag_map(); - if (cmd_words_len < 4) - goto err; - if (cmd_compare(1, "lag")) { - if (!atoi_byte(cmd_words_b[2])) - goto err; - if (!atoi_results_u8 || atoi_results_u8 > STP_LAG_COUNT) - goto err; - ent = STP_LAG_BASE + atoi_results_u8 - 1; - } else { - if (!cmd_parse_port_separator(cmd_words_b[2])) - goto err; - ent = atoi_results_u8; - if (stp_ent_of[ent] != ent) { - print_string("Port belongs to a LAG, configure it as lag "); - print_byte(stp_ent_of[ent] - STP_LAG_BASE + 1); - write_char('\n'); - return; - } - } - if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off")) - goto err; - if (cmd_compare(3, "on")) { - stp_pflags[ent] |= STP_PF_ENABLED; - stp_pflags[ent] &= ~STP_PF_TRIPPED; - if (stp_enabled) { /* (re)join: listen first */ - stp_state_set(ent, STP_ST_BLOCKING); - port_timers[ent] = (uint16_t)stp_fwddelay_s * STP_HZ; - } - } else if (cmd_compare(3, "off")) { - stp_pflags[ent] &= ~STP_PF_ENABLED; - if (stp_enabled) - stp_state_set(ent, STP_ST_FORWARDING); /* plain forwarding */ - } else if (cmd_compare(3, "edge")) { - /* Also drop the *operational* edge flag: it is what exempts the - * ent from topology changes and lets it skip the listen period, - * so leaving it set would keep the old behaviour until the next - * "stp off"/"stp on". An admin edge is operational immediately. */ - stp_pflags[ent] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE | STP_PF_OPEREDGE); - if (cmd_compare(4, "on")) - stp_pflags[ent] |= STP_PF_ADMEDGE | STP_PF_OPEREDGE; - else if (cmd_compare(4, "auto")) - stp_pflags[ent] |= STP_PF_AUTOEDGE; - else if (!cmd_compare(4, "off")) - goto err; - } else if (cmd_compare(3, "cost")) { - /* raw 802.1D value, 0..200000000; 0 = auto (speed-based) */ - stp_cost_scratch = 0; - { - __xdata uint8_t *cp = &cmd_buffer[cmd_words_b[4]]; - if (*cp < '0' || *cp > '9') - goto err; - while (*cp >= '0' && *cp <= '9') { - stp_cost_scratch = stp_cost_scratch * 10 + (*cp - '0'); - cp++; - } - } - if (stp_cost_scratch > 200000000UL) - goto err; - stp_pcost[ent] = stp_cost_scratch; - } else if (cmd_compare(3, "p2p")) { - if (cmd_compare(4, "auto")) - stp_pp2p[ent] = 0; - else if (cmd_compare(4, "on")) - stp_pp2p[ent] = 1; - else if (cmd_compare(4, "off")) - stp_pp2p[ent] = 2; - else - goto err; - } else if (cmd_compare(3, "prio")) { - if (!atoi_byte(cmd_words_b[4])) - goto err; - if (atoi_results_u8 > 240 || (atoi_results_u8 & 0x0f)) - goto err; - stp_pprio[ent] = atoi_results_u8; - } else if (cmd_compare(3, "guard")) { - stp_pflags[ent] &= ~(STP_PF_BPDUGUARD | STP_PF_ROOTGUARD); - if (cmd_compare(4, "bpdu")) - stp_pflags[ent] |= STP_PF_BPDUGUARD; - else if (cmd_compare(4, "root")) - stp_pflags[ent] |= STP_PF_ROOTGUARD; - else if (!cmd_compare(4, "none")) - goto err; - } else if (cmd_compare(3, "filter")) { - if (cmd_compare(4, "on")) - stp_pflags[ent] |= STP_PF_FILTER; - else if (cmd_compare(4, "off")) - stp_pflags[ent] &= ~STP_PF_FILTER; - else - goto err; - } else { - goto err; - } - return; - } - - if (!atoi_byte(cmd_words_b[2])) { - if (cmd_compare(1, "version")) { - if (cmd_compare(2, "rstp")) - stp_rstp = 1; - else if (cmd_compare(2, "stp")) - stp_rstp = 0; - else - goto err; - return; - } - goto err; - } - stp_scratch = atoi_results_u8; - - if (cmd_compare(1, "prio")) { - if (stp_scratch > 15) - goto err; - stp_prio = stp_scratch << 4; /* n * 4096, as the BPDU's high byte */ - if (stp_root_port == 0xff) - stp_claim_root(); /* re-announce with the new priority */ - } else if (cmd_compare(1, "hello")) { - if (stp_scratch < 1 || stp_scratch > 10) - goto err; - stp_hello_s = stp_scratch; - } else if (cmd_compare(1, "maxage")) { - if (stp_scratch < 6 || stp_scratch > 40) - goto err; - stp_maxage_s = stp_scratch; - } else if (cmd_compare(1, "fwd")) { - if (stp_scratch < 4 || stp_scratch > 30) - goto err; - stp_fwddelay_s = stp_scratch; - } else if (cmd_compare(1, "txhold")) { - if (stp_scratch < 1 || stp_scratch > 10) - goto err; - stp_txhold = stp_scratch; + if (stp_bpdu_filter) { + stp_fdb_update(PMASK_CPU); + trap_rma_set(RTL837X_RMA0_CONF, RTL837X_RMA_ACT_DROP); } else { - goto err; + /* Restore BPDU transparency: flood them again like an unmanaged switch. */ + stp_fdb_update(PMASK_CPU | (machine_detected.isRTL8373 ? PMASK_9 : PMASK_6)); + trap_rma_set(RTL837X_RMA0_CONF, RTL837X_RMA_ACT_FORWARD); } - return; -err: - err_status = ERR_INVALID_ARGUMENT; - print_string("Error: stp on|off|status | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | port <1-9>|lag <1-4> on|off|edge|cost|prio|guard|filter ...\n"); +} + + +void stp_port_admin(uint8_t port, uint8_t on) __banked +{ + if (on) { + stp_state_set(port, 0b01); + port_timers[port] = FWD_TICKS; + } else { + stp_state_set(port, 0b11); + } +} + + +uint8_t stp_port_role(uint8_t port) __banked +{ + if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & STP_PF_TRIPPED) + || !((stp_link_prev >> port) & 1)) + return 0; + if (port == stp_root_port) + return 1; + if ((stp_backup >> port) & 1) + return 4; + if ((stp_alt >> port) & 1) + return 3; + return 2; +} + + +void stp_port_mcheck(uint8_t port) __banked +{ + stp_legacy &= ~((uint16_t)1 << port); + stp_seen_stp &= ~((uint16_t)1 << port); + stp_seen_rstp &= ~((uint16_t)1 << port); + stp_mdelay[port] = STP_MIGRATE; + stp_newinfo |= (uint16_t)1 << port; +} + + +void stp_prio_apply(void) __banked +{ + if (stp_root_port == 0xff) + stp_claim_root(); } diff --git a/rtl837x_stp.h b/rtl837x_stp.h index 95bac8af..5b60c5ea 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -2,13 +2,21 @@ #define _RTL837X_STP_H_ #include -#include "rtl837x_common.h" void stp_in(void) __banked; void stp_setup(void) __banked; void stp_timers(void) __banked; void stp_off(void) __banked; void stp_parse(void) __banked __reentrant; void stp_defaults(void) __banked; +void stp_status(void) __banked; +void stp_port_admin(uint8_t port, uint8_t on) __banked; +void stp_prio_apply(void) __banked; +void stp_port_mcheck(uint8_t port) __banked; +uint8_t stp_port_role(uint8_t port) __banked; +uint8_t stp_port_state(uint8_t port) __banked; +uint8_t stp_ent_id(uint8_t e) __banked; +void stp_lag_map(void) __banked; +void stp_counters_clear(void) __banked __reentrant; /* Tick rate of stp_timers(), also used by the web UI. */ #define STP_HZ 50 @@ -43,22 +51,38 @@ extern __xdata uint8_t stp_txhold; #define STP_PF_TRIPPED 0x80 /* runtime: disabled by BPDU guard */ extern __xdata uint8_t stp_pflags[STP_ENTITIES]; -extern __xdata uint32_t stp_pcost[STP_ENTITIES]; /* path cost; 0 = auto (20000) */ +extern __xdata uint32_t stp_pcost[STP_ENTITIES]; extern __xdata uint8_t stp_pprio[STP_ENTITIES]; extern __xdata uint8_t stp_pp2p[STP_ENTITIES]; -extern __xdata uint8_t stp_ent_of[STP_PORTS]; /* the STP entity a port answers to: itself, or its lag */ -extern __xdata uint16_t stp_lag_mask[STP_LAG_COUNT]; /* member ports of each lag; 0 = not part of STP */ +extern __xdata uint8_t stp_ent_of[STP_PORTS]; +extern __xdata uint16_t stp_lag_mask[STP_LAG_COUNT]; -/* Last-heard designated info per port (from received BPDUs); consult - * stp_bpdu_age to decide whether it is still current. */ extern __xdata struct bridge stp_dbridge[STP_ENTITIES]; extern __xdata uint16_t stp_dpid[STP_ENTITIES]; extern __xdata uint32_t stp_dcost[STP_ENTITIES]; -extern __xdata uint16_t stp_bpdu_age[STP_ENTITIES]; /* ticks since a BPDU was heard */ +extern __xdata uint16_t stp_bpdu_age[STP_ENTITIES]; +#define STP_CNT_RX 0 /* BPDUs taken in on an STP port */ +#define STP_CNT_TX 1 /* BPDUs put on the wire */ +#define STP_CNT_TCRX 2 /* BPDUs received with the TC flag, TCNs */ +#define STP_CNT_TCTX 3 /* BPDUs sent with the TC flag, TCNs */ +#define STP_CNT_N 4 +extern __xdata uint32_t stp_cnt[STP_CNT_N][STP_ENTITIES]; +extern __xdata uint16_t stp_info_while[STP_ENTITIES]; extern __xdata struct bridge root_bridge; extern __xdata uint32_t root_bridge_cost; extern __xdata uint8_t stp_root_port; +extern __xdata uint8_t stp_root_maxage; +extern __xdata uint8_t stp_root_fwd; +extern __xdata uint16_t stp_link_prev; +extern __xdata uint16_t stp_alt; +extern __xdata uint16_t stp_backup; +extern __xdata uint16_t stp_legacy; +extern __xdata uint16_t stp_rrwhile[STP_ENTITIES]; +extern __xdata uint16_t stp_rbwhile[STP_ENTITIES]; extern __xdata uint16_t stp_tc_count; +extern __xdata uint32_t stp_tc_secs; +extern __xdata uint8_t stp_pcost_short; +extern __xdata uint8_t stp_bpdu_filter; #endif diff --git a/rtl837x_stp_cli.c b/rtl837x_stp_cli.c new file mode 100644 index 00000000..107dbc21 --- /dev/null +++ b/rtl837x_stp_cli.c @@ -0,0 +1,228 @@ +/* + * Command line interface of the Spanning Tree Protocol for the RTL837x platform + * This code is in the Public Domain + */ + +#include +#include "rtl837x_common.h" +#include "rtl837x_stp.h" +#include "machine.h" + +#pragma codeseg BANK2 +#pragma constseg BANK2 + +extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; +extern __xdata uint8_t cmd_words_len; +extern __xdata uint8_t cmd_words_b[15]; +uint8_t cmd_compare(uint8_t start, __code const uint8_t * cmd); +uint8_t atoi_byte(uint8_t idx); +uint8_t cmd_parse_port_separator(uint8_t idx); +extern __xdata uint8_t atoi_results_u8; +extern __xdata char save_cmd; +extern __xdata uint8_t err_status; + +#define TIMES_OK(h, m, f) ((uint16_t)2 * ((f) - 1) >= (m) && (m) >= (uint16_t)2 * ((h) + 1)) + +__xdata uint32_t stp_cli_cost; +__xdata uint8_t stp_cli_val; + + +void stp_parse(void) __banked __reentrant +{ + uint8_t port; + + if (cmd_compare(1, "on")) { + print_string("STP enabled\n"); + stp_enabled = 1; + stp_setup(); + return; + } + if (cmd_compare(1, "off")) { + print_string("STP disabled\n"); + stp_off(); + stp_enabled = 0; + return; + } + if (cmd_compare(1, "status")) { + stp_status(); + return; + } + if (cmd_compare(1, "clear")) { + stp_counters_clear(); + return; + } + if (cmd_words_len < 3) + goto err; + + if (cmd_compare(1, "port") || cmd_compare(1, "lag")) { + if (cmd_words_len < 4) + goto err; + stp_lag_map(); + if (cmd_compare(1, "lag")) { + if (!atoi_byte(cmd_words_b[2]) || !atoi_results_u8 || atoi_results_u8 > STP_LAG_COUNT) + goto err; + port = STP_LAG_BASE - 1 + atoi_results_u8; + } else { + if (!cmd_parse_port_separator(cmd_words_b[2])) + goto err; + port = atoi_results_u8; + if (stp_ent_of[port] != port) { + print_string("Error: the port is in lag "); + write_char('1' + stp_ent_of[port] - STP_LAG_BASE); + write_char('\n'); + err_status = ERR_INVALID_ARGUMENT; + return; + } + } + if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off") + && !cmd_compare(3, "mcheck")) + goto err; + if (cmd_compare(3, "on")) { + stp_pflags[port] |= STP_PF_ENABLED; + stp_pflags[port] &= ~STP_PF_TRIPPED; + if (stp_enabled) { /* (re)join: listen first */ + stp_port_admin(port, 1); + } + } else if (cmd_compare(3, "mcheck")) { + if (stp_enabled) + stp_port_mcheck(port); + } else if (cmd_compare(3, "off")) { + stp_pflags[port] &= ~STP_PF_ENABLED; + if (stp_enabled) + stp_port_admin(port, 0); /* plain forwarding */ + } else if (cmd_compare(3, "edge")) { + /* Also drop the *operational* edge flag: it is what exempts the + * port from topology changes and lets it skip the listen period, + * so leaving it set would keep the old behaviour until the next + * "stp off"/"stp on". An admin edge is operational immediately. */ + stp_pflags[port] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE | STP_PF_OPEREDGE); + if (cmd_compare(4, "on")) + stp_pflags[port] |= STP_PF_ADMEDGE | STP_PF_OPEREDGE; + else if (cmd_compare(4, "auto")) + stp_pflags[port] |= STP_PF_AUTOEDGE; + else if (!cmd_compare(4, "off")) + goto err; + } else if (cmd_compare(3, "cost")) { + /* raw 802.1D value, 0..200000000; 0 = auto (speed-based) */ + stp_cli_cost = 0; + { + __xdata uint8_t *cp = &cmd_buffer[cmd_words_b[4]]; + if (*cp < '0' || *cp > '9') + goto err; + while (*cp >= '0' && *cp <= '9') { + stp_cli_cost = stp_cli_cost * 10 + (*cp - '0'); + cp++; + } + } + if (stp_cli_cost > 200000000UL) + goto err; + stp_pcost[port] = stp_cli_cost; + } else if (cmd_compare(3, "p2p")) { + if (cmd_compare(4, "auto")) + stp_pp2p[port] = 0; + else if (cmd_compare(4, "on")) + stp_pp2p[port] = 1; + else if (cmd_compare(4, "off")) + stp_pp2p[port] = 2; + else + goto err; + } else if (cmd_compare(3, "prio")) { + if (!atoi_byte(cmd_words_b[4])) + goto err; + if (atoi_results_u8 > 240 || (atoi_results_u8 & 0x0f)) + goto err; + stp_pprio[port] = atoi_results_u8; + } else if (cmd_compare(3, "guard")) { + stp_pflags[port] &= ~(STP_PF_BPDUGUARD | STP_PF_ROOTGUARD); + if (cmd_compare(4, "bpdu")) + stp_pflags[port] |= STP_PF_BPDUGUARD; + else if (cmd_compare(4, "root")) + stp_pflags[port] |= STP_PF_ROOTGUARD; + else if (!cmd_compare(4, "none")) + goto err; + } else if (cmd_compare(3, "filter")) { + if (cmd_compare(4, "on")) + stp_pflags[port] |= STP_PF_FILTER; + else if (cmd_compare(4, "off")) + stp_pflags[port] &= ~STP_PF_FILTER; + else + goto err; + } else { + goto err; + } + return; + } + + if (!atoi_byte(cmd_words_b[2])) { + if (cmd_compare(1, "version")) { + if (cmd_compare(2, "rstp")) + stp_rstp = 1; + else if (cmd_compare(2, "stp")) + stp_rstp = 0; + else + goto err; + return; + } + if (cmd_compare(1, "pathcost")) { + if (cmd_compare(2, "long")) + stp_pcost_short = 0; + else if (cmd_compare(2, "short")) + stp_pcost_short = 1; + else + goto err; + return; + } + if (cmd_compare(1, "bpdu")) { + if (cmd_compare(2, "filter")) + stp_bpdu_filter = 1; + else if (cmd_compare(2, "flood")) + stp_bpdu_filter = 0; + else + goto err; + if (!stp_enabled) + stp_off(); + return; + } + goto err; + } + stp_cli_val = atoi_results_u8; + + if (cmd_compare(1, "prio")) { + if (stp_cli_val > 15) + goto err; + stp_prio = stp_cli_val << 4; /* n * 4096, as the BPDU's high byte */ + stp_prio_apply(); + } else if (cmd_compare(1, "hello")) { + if (stp_cli_val < 1 || stp_cli_val > 10) + goto err; + if (save_cmd && !TIMES_OK(stp_cli_val, stp_maxage_s, stp_fwddelay_s)) + goto times; + stp_hello_s = stp_cli_val; + } else if (cmd_compare(1, "maxage")) { + if (stp_cli_val < 6 || stp_cli_val > 40) + goto err; + if (save_cmd && !TIMES_OK(stp_hello_s, stp_cli_val, stp_fwddelay_s)) + goto times; + stp_maxage_s = stp_cli_val; + } else if (cmd_compare(1, "fwd")) { + if (stp_cli_val < 4 || stp_cli_val > 30) + goto err; + if (save_cmd && !TIMES_OK(stp_hello_s, stp_maxage_s, stp_cli_val)) + goto times; + stp_fwddelay_s = stp_cli_val; + } else if (cmd_compare(1, "txhold")) { + if (stp_cli_val < 1 || stp_cli_val > 10) + goto err; + stp_txhold = stp_cli_val; + } else { + goto err; + } + return; +times: + print_string("Error: needs 2*(fwd-1) >= maxage >= 2*(hello+1)\n"); + err_status = ERR_INVALID_ARGUMENT; + return; +err: + err_status = ERR_INVALID_ARGUMENT; + print_string("Error: stp on|off|status|clear | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | pathcost long|short | bpdu filter|flood | port <1-9>|lag <1-4> on|off|mcheck|edge|cost|prio|guard|filter ...\n"); +} diff --git a/test/env_tables.c b/test/env_tables.c index 494499ff..9fbb676b 100644 --- a/test/env_tables.c +++ b/test/env_tables.c @@ -121,6 +121,12 @@ uint8_t stp_pflags[STP_ENTITIES]; uint8_t stp_pp2p[STP_ENTITIES]; uint8_t stp_pprio[STP_ENTITIES]; uint8_t stp_prio = 0x80, stp_root_port = 0xff, stp_rstp = 1, stp_txhold = 6; +uint8_t stp_bpdu_filter, stp_pcost_short, stp_root_fwd = 15, stp_root_maxage = 20; +uint16_t stp_legacy, stp_link_prev = 0x3ff; +uint16_t stp_info_while[STP_ENTITIES]; +uint32_t stp_cnt[STP_CNT_N][STP_ENTITIES]; +uint32_t stp_tc_secs; +uint8_t stp_ent_id(uint8_t e) { return e >= STP_LAG_BASE ? e + 101 - STP_LAG_BASE : machine.log_to_phys_port[e]; } /* ---- SFP: no module present, reads fail ---- */ uint8_t sfp_buf[16]; diff --git a/test/stp/Makefile b/test/stp/Makefile new file mode 100644 index 00000000..a3fd7126 --- /dev/null +++ b/test/stp/Makefile @@ -0,0 +1,29 @@ +CC ?= cc +SRCROOT = ../.. +CFLAGS = -std=c11 -g -O1 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-unknown-pragmas \ + -Wno-pointer-sign \ + -fpack-struct=1 \ + -DRTLP_HOST_TEST \ + -fsanitize=address,undefined -fno-omit-frame-pointer \ + -include ../sdcc_shim.h -I../stub -I$(SRCROOT) -I$(SRCROOT)/httpd -I$(SRCROOT)/uip +ifeq ($(findstring clang,$(shell $(CC) --version 2>/dev/null)),) +CFLAGS += -Wno-builtin-declaration-mismatch +endif +LDFLAGS = -fsanitize=address,undefined + +SRCS = stp_sim.c $(SRCROOT)/rtl837x_stp.c $(SRCROOT)/rtl837x_stp_cli.c + +all: build/stp_sim + ./build/stp_sim + +verbose: build/stp_sim + ./build/stp_sim -v + +build/stp_sim: $(SRCS) $(SRCROOT)/rtl837x_stp.h + mkdir -p build + $(CC) $(CFLAGS) $(SRCS) -o $@ $(LDFLAGS) + +clean: + rm -rf build + +.PHONY: all verbose clean diff --git a/test/stp/stp_sim.c b/test/stp/stp_sim.c new file mode 100644 index 00000000..12f93853 --- /dev/null +++ b/test/stp/stp_sim.c @@ -0,0 +1,2362 @@ +#include +#include +#include +#include +#include +#include + +#include "rtl837x_common.h" +#include "machine.h" +#include "uip.h" +#include "rtl837x_regs.h" +#include "rtl837x_stp.h" + +#define NPORTS 9 /* indices 0..8, front-panel 1..9 */ + +uint8_t uip_buf[UIP_CONF_BUFFER_ALLOC_SIZE]; +uint16_t uip_len; +struct uip_eth_addr uip_ethaddr = { .addr = { 0x1c, 0x2a, 0xa3, 0x1a, 0x72, 0x4e } }; +const struct machine machine = { + .log_to_phys_port = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } +}; +struct machine_runtime machine_detected = { .isRTL8373 = 1 }; +uint8_t sfr_data[4]; +bool stp_enabled; +uint16_t management_vlan; +uint8_t cmd_buffer[CMD_BUF_SIZE]; +uint8_t cmd_words_len; +uint8_t cmd_words_b[15]; +char save_cmd; +uint8_t err_status; +uint8_t atoi_results_u8; + +static int verbose; + +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 print_long(uint32_t v) +{ + if (verbose) + printf("%08x", v); +} + +void write_char(char c) +{ + if (verbose) + putchar(c); +} + +void itoa(uint8_t v) +{ + if (verbose) + printf("%u", v); +} + +void print_reg(uint16_t reg) +{ + (void)reg; +} + +uint8_t cmd_compare(uint8_t start, const uint8_t *cmd) +{ + const char *w; + size_t n; + + if (start >= cmd_words_len) + return 0; + w = (const char *)&cmd_buffer[cmd_words_b[start]]; + n = strlen((const char *)cmd); + return strncmp(w, (const char *)cmd, n) == 0 && (w[n] == ' ' || w[n] == 0); +} + +uint8_t atoi_byte(uint8_t idx) +{ + unsigned v = 0, n = 0; + while (cmd_buffer[idx + n] >= '0' && cmd_buffer[idx + n] <= '9') + v = v * 10 + (cmd_buffer[idx + n++] - '0'); + atoi_results_u8 = (uint8_t)v; + return (uint8_t)n; +} + +uint8_t cmd_parse_port_separator(uint8_t idx) +{ + if (!atoi_byte(idx) || atoi_results_u8 < 1 || atoi_results_u8 > NPORTS) + return 0; + atoi_results_u8--; + return 1; +} + +static void sim_cmd(const char *line) +{ + memset(cmd_buffer, 0, sizeof(cmd_buffer)); + strncpy((char *)cmd_buffer, line, sizeof(cmd_buffer) - 1); + cmd_words_len = 0; + for (uint8_t i = 0; cmd_buffer[i]; i++) + if (cmd_buffer[i] != ' ' && (i == 0 || cmd_buffer[i - 1] == ' ')) + cmd_words_b[cmd_words_len++] = i; + err_status = ERR_OK; + stp_parse(); +} + +void execute_config(void) +{ +} + +int8_t vlan_get(uint16_t vlan) +{ + (void)vlan; + return -1; +} + +uint16_t port_pvid_get(uint8_t port) +{ + (void)port; + return 1; +} + +static int l2mc_calls; +static uint16_t l2mc_pmask; + +void port_l2mc_set(uint8_t mac_last, uint16_t vid, uint16_t pmask) +{ + (void)mac_last; + (void)vid; + l2mc_calls++; + l2mc_pmask = pmask; +} + +void trap_rma_set(uint16_t reg, uint8_t act) +{ + (void)reg; + (void)act; +} + + +uint8_t port_ingress_filter_get(uint8_t port) +{ + (void)port; + return 0; +} + +static int flush_count[NPORTS]; + +void port_l2_forget_port(uint8_t port) +{ + if (port < NPORTS) + flush_count[port]++; +} + +static uint8_t mstp_reg[4]; +static uint16_t sim_lag[4]; + +uint16_t port_lag_members_get(uint8_t lag) +{ + return sim_lag[lag]; +} + +static uint16_t sim_links; +static uint8_t sim_speed[NPORTS]; /* nibble as the ASIC reports it: 2 = 1G, 4 = 10G */ +static int tx_frames[NPORTS]; + +struct sim_pkt_out { + uint8_t stp_addr[6]; + uint8_t src_addr[6]; + struct rtl_tag rtl_tag; + uint16_t msg_len; + uint8_t dsap; + uint8_t ssap; + uint8_t ctrl; + uint16_t proto; + uint8_t version; + uint8_t bpdu_type; + uint8_t flags; + struct bridge root; + uint32_t root_path_cost; + struct bridge bridge; + uint8_t port_prio; + uint8_t port_id; + uint16_t age; + uint16_t age_max; + uint16_t hello; + uint16_t fwd_delay; + uint8_t version1_length; +}; +/* SDCC uses byte alignment; both firmware and fixtures must match wire + * offsets rather than sharing the host compiler's padding. */ +_Static_assert(sizeof(struct sim_pkt_out) == 61, "RST BPDU with CPU tag must be 61 bytes"); +_Static_assert(offsetof(struct sim_pkt_out, root) == 30, "root identifier wire offset"); +static struct sim_pkt_out last_tx[NPORTS]; +static int tc_frames[NPORTS]; +static int tcn_frames[NPORTS]; + +void reg_read_m(uint16_t addr) +{ + memset(sfr_data, 0, 4); + if (addr == RTL837X_MSTP_STATES) + memcpy(sfr_data, mstp_reg, 4); + else if (addr == RTL837X_REG_LINKS_STS) { + sfr_data[1] = (uint8_t)sim_links; + sfr_data[2] = (uint8_t)(sim_links >> 8); + } else if (addr == RTL837X_REG_LINKS || addr == RTL837X_REG_LINKS_89) { + uint8_t base = (addr == RTL837X_REG_LINKS) ? 0 : 8; + for (uint8_t k = 0; k < 8 && base + k < NPORTS; k++) + sfr_data[3 - (k >> 1)] |= (k & 1) ? (uint8_t)(sim_speed[base + k] << 4) : sim_speed[base + k]; + } +} + +void reg_write_m(uint16_t addr) +{ + if (addr == RTL837X_MSTP_STATES) { + memcpy(mstp_reg, sfr_data, 4); + if (verbose > 1) + printf(" [mstp <- %02x %02x %02x %02x]\n", mstp_reg[0], mstp_reg[1], mstp_reg[2], + mstp_reg[3]); + } +} + +void tcpip_output(void) +{ + struct sim_pkt_out *f = (struct sim_pkt_out *)&uip_buf[RTL_FRAME_DESC_SIZE]; + uint16_t pmask = HTONS(f->rtl_tag.pmask); + for (uint8_t p = 0; p < NPORTS; p++) + if (pmask & (1 << p)) { + tx_frames[p]++; + if (f->bpdu_type == 0x80) + tcn_frames[p]++; + else if (f->flags & 0x01) + tc_frames[p]++; + memcpy(&last_tx[p], f, sizeof(*f)); + } + uip_len = 0; +} + +static uint8_t port_state(uint8_t port) +{ + return (mstp_reg[3 - (port >> 2)] >> ((port << 1) & 0x7)) & 0x3; +} + +static const char *state_name(uint8_t s) +{ + static const char *n[] = { "off", "discarding", "learning", "forwarding" }; + return n[s & 3]; +} + +static void links_set(uint16_t mask) +{ + sim_links = mask; +} + +static void tick(unsigned n) +{ + while (n--) + stp_timers(); +} + +static void secs(unsigned s) +{ + tick(s * STP_HZ); +} + +struct sim_bpdu { + uint8_t port; /* ingress port index */ + uint8_t root_prio; /* high byte of the root priority */ + uint8_t root_mac[6]; + uint32_t root_cost; /* root path cost as advertised */ + uint8_t br_prio; /* sender's own bridge priority high byte */ + uint8_t br_mac[6]; + uint8_t port_id; /* sender's port number, 1-based */ + uint8_t port_prio; /* 0 = 0x80 */ + uint8_t flags; /* 0 = designated, learning + forwarding */ + uint8_t age; /* message age, seconds */ + uint8_t maxage; /* 0 = 20 s, and hello/fwd default with it */ + uint8_t hello; + uint8_t fwd; + uint8_t legacy; /* 1 = 802.1D Config BPDU, 2 = TCN */ + uint8_t msg_len; /* 0 = the right length for the type */ +}; + +struct sim_pkt_in { + uint8_t stp_addr[6]; + uint8_t src_addr[6]; + struct rtl_tag rtl_tag; + struct vlan_tag vlan_tag; + uint16_t msg_len; + uint8_t dsap; + uint8_t ssap; + uint8_t ctrl; + uint16_t proto; + uint8_t version; + uint8_t bpdu_type; + uint8_t flags; + struct bridge root; + uint32_t root_path_cost; + struct bridge bridge; + uint8_t port_prio; + uint8_t port_id; + uint16_t age; + uint16_t age_max; + uint16_t hello; + uint16_t fwd_delay; + uint8_t version1_length; +}; +_Static_assert(sizeof(struct sim_pkt_in) == 65, "VLAN-tagged RX BPDU must be 65 bytes"); + +static uint32_t swap32(uint32_t v) +{ + return (v << 24) | ((v & 0xff00) << 8) | ((v >> 8) & 0xff00) | (v >> 24); +} + +static void bpdu_in(const struct sim_bpdu *b) +{ + struct sim_pkt_in *f = (struct sim_pkt_in *)uip_buf; + + memset(uip_buf, 0, sizeof(uip_buf)); + f->vlan_tag.svlan = HTONS(0x8100); + memcpy(f->stp_addr, "\x01\x80\xc2\x00\x00\x00", 6); + memcpy(f->src_addr, b->br_mac, 6); + f->rtl_tag.pmask = HTONS(b->port); + f->msg_len = HTONS(b->msg_len ? b->msg_len : (b->legacy == 2 ? 7 : (b->legacy ? 38 : 39))); + f->dsap = 0x42; + f->ssap = 0x42; + f->ctrl = 0x03; + f->proto = 0; + f->version = b->legacy ? 0x00 : 0x02; + f->bpdu_type = b->legacy == 2 ? 0x80 : (b->legacy ? 0x00 : 0x02); + f->flags = b->flags ? b->flags : (b->legacy ? 0x00 : 0x3c); + f->root.prio = b->root_prio; + f->root.ext = 0; + memcpy(f->root.mac, b->root_mac, 6); + f->root_path_cost = swap32(b->root_cost); + f->bridge.prio = b->br_prio; + f->bridge.ext = 0; + memcpy(f->bridge.mac, b->br_mac, 6); + f->port_prio = b->port_prio ? b->port_prio : 0x80; + f->port_id = b->port_id; + f->age = b->age; + f->age_max = b->maxage ? b->maxage : 20; + f->hello = b->maxage ? b->hello : 2; + f->fwd_delay = b->maxage ? b->fwd : 15; + uip_len = sizeof(struct sim_pkt_in); + stp_in(); +} + +static int failures; + +static void check(int cond, const char *what) +{ + if (!cond) { + printf(" FAIL: %s\n", what); + failures++; + } else if (verbose) + printf(" ok: %s\n", what); +} + +static void check_state(uint8_t port, uint8_t want, const char *what) +{ + uint8_t got = port_state(port); + if (got != want) { + printf(" FAIL: %s: port %u is %s, expected %s\n", what, port + 1, state_name(got), state_name(want)); + failures++; + } else if (verbose) + printf(" ok: %s (port %u %s)\n", what, port + 1, state_name(got)); +} + +static void reset_all(void) +{ + memset(mstp_reg, 0, sizeof(mstp_reg)); + memset(sim_lag, 0, sizeof(sim_lag)); + memset(flush_count, 0, sizeof(flush_count)); + memset(tx_frames, 0, sizeof(tx_frames)); + memset(last_tx, 0, sizeof(last_tx)); + memset(tc_frames, 0, sizeof(tc_frames)); + memset(tcn_frames, 0, sizeof(tcn_frames)); + for (uint8_t p = 0; p < NPORTS; p++) + sim_speed[p] = 2; + stp_enabled = 1; + stp_defaults(); + links_set(0); + stp_setup(); + tick(2 * STP_HZ); +} + +static const uint8_t ROOT_MAC[6] = { 0x1c, 0x2a, 0xa3, 0x1e, 0xc2, 0x03 }; +static const uint8_t PEER_MAC[6] = { 0x00, 0x82, 0x44, 0x2a, 0x74, 0x82 }; + +static void scen_edge_ports(void) +{ + printf("1. a port nobody talks STP on becomes an edge port\n"); + reset_all(); + links_set(1 << 3); + secs(1); + check_state(3, 1, "port with fresh carrier starts discarding"); + secs(4); + check_state(3, 3, "silent port goes forwarding as an edge port"); +} + +static void scen_root_port(void) +{ + struct sim_bpdu root_bpdu = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + + printf("2. the bridge follows the root heard on port 9\n"); + reset_all(); + links_set(1 << 8); + secs(1); + memcpy(root_bpdu.root_mac, ROOT_MAC, 6); + memcpy(root_bpdu.br_mac, ROOT_MAC, 6); + bpdu_in(&root_bpdu); + check(stp_root_port == 8, "port 9 becomes the root port"); + secs(20); + bpdu_in(&root_bpdu); + check_state(8, 3, "root port forwards after the listen period"); +} + +static void ring(void) +{ + struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + struct sim_bpdu from_peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + + reset_all(); + links_set((1 << 0) | (1 << 8)); + secs(1); + + memcpy(from_root.root_mac, ROOT_MAC, 6); + memcpy(from_root.br_mac, ROOT_MAC, 6); + + memcpy(from_peer.root_mac, ROOT_MAC, 6); + memcpy(from_peer.br_mac, PEER_MAC, 6); + + for (int i = 0; i < 12; i++) { + bpdu_in(&from_root); + bpdu_in(&from_peer); + secs(2); + } + check(stp_root_port == 8, "the direct uplink stays the root port"); + check_state(8, 3, "root port forwards"); + check_state(0, 1, "port 1 is discarding (alternate)"); +} + +static void scen_ring(void) +{ + printf("3. a neighbour with a cheaper path to the root must block us\n"); + ring(); +} + +static void scen_ring_clears(void) +{ + struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + + printf("4. when the better neighbour goes quiet the port comes back\n"); + ring(); + memcpy(from_root.root_mac, ROOT_MAC, 6); + memcpy(from_root.br_mac, ROOT_MAC, 6); + bpdu_in(&from_root); + secs(2); + bpdu_in(&from_root); + secs(2); + check_state(0, 1, "port 1 still discards within three of the peer's hellos"); + for (int i = 0; i < 18; i++) { + bpdu_in(&from_root); + secs(2); + } + check_state(0, 3, "port 1 forwards again once the peer information aged out"); +} + +static void scen_cheaper_path(void) +{ + struct sim_bpdu from_root_p9 = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + struct sim_bpdu from_root_p1; + + printf("5. the cheaper of two paths to the root becomes the root port\n"); + reset_all(); + stp_pcost[0] = 2000; + links_set((1 << 0) | (1 << 8)); + secs(1); + memcpy(from_root_p9.root_mac, ROOT_MAC, 6); + memcpy(from_root_p9.br_mac, ROOT_MAC, 6); + from_root_p1 = from_root_p9; + from_root_p1.port = 0; + from_root_p1.port_id = 7; + for (int i = 0; i < 12; i++) { + bpdu_in(&from_root_p9); + bpdu_in(&from_root_p1); + secs(2); + } + check(stp_root_port == 0, "port 1 wins on cost and becomes the root port"); + check(root_bridge_cost == 2000, "the root path cost follows that port"); + check_state(0, 3, "the root port forwards"); + check_state(8, 1, "the dearer path to the same root is discarding"); + stp_pcost[0] = 0; +} + +static void scen_we_are_better(void) +{ + struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + struct sim_bpdu from_peer = { + .port = 0, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 1, .flags = 0x0c + }; + + printf("6. a neighbour with a worse path does not block us\n"); + reset_all(); + links_set((1 << 0) | (1 << 8)); + secs(1); + memcpy(from_root.root_mac, ROOT_MAC, 6); + memcpy(from_root.br_mac, ROOT_MAC, 6); + memcpy(from_peer.root_mac, ROOT_MAC, 6); + memcpy(from_peer.br_mac, PEER_MAC, 6); + for (int i = 0; i < 18; i++) { + bpdu_in(&from_root); + bpdu_in(&from_peer); + secs(2); + } + check(stp_root_port == 8, "the uplink stays the root port"); + check_state(0, 3, "port 1 keeps forwarding"); +} + +static void scen_alt_survives_link_bounce(void) +{ + struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + struct sim_bpdu from_peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + + printf("7. a blocked port stays blocked when the carrier returns\n"); + ring(); + memcpy(from_root.root_mac, ROOT_MAC, 6); + memcpy(from_root.br_mac, ROOT_MAC, 6); + memcpy(from_peer.root_mac, ROOT_MAC, 6); + memcpy(from_peer.br_mac, PEER_MAC, 6); + + links_set(1 << 8); + secs(2); + links_set((1 << 0) | (1 << 8)); + bpdu_in(&from_peer); + for (int i = 0; i < 15; i++) { + bpdu_in(&from_root); + bpdu_in(&from_peer); + secs(2); + } + check_state(0, 1, "port 1 never reaches forwarding while the peer is better"); + check(stp_root_port == 8, "the uplink is still the root port"); +} + +static void scen_speed_cost(void) +{ + struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + struct sim_bpdu slow; + + printf("8. the faster of two links to one root wins on cost\n"); + reset_all(); + sim_speed[8] = 4; + sim_speed[0] = 2; + links_set((1 << 0) | (1 << 8)); + secs(1); + memcpy(from_root.root_mac, ROOT_MAC, 6); + memcpy(from_root.br_mac, ROOT_MAC, 6); + slow = from_root; + slow.port = 0; + slow.port_id = 7; + for (int i = 0; i < 12; i++) { + bpdu_in(&from_root); + bpdu_in(&slow); + secs(2); + } + check(stp_root_port == 8, "the 10G port is the root port"); + check(root_bridge_cost == 2000, "its cost is the 10G value, not a flat default"); +} + +static struct sim_bpdu root_on(uint8_t port) +{ + struct sim_bpdu b = { .port = port, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 }; + memcpy(b.root_mac, ROOT_MAC, 6); + memcpy(b.br_mac, ROOT_MAC, 6); + return b; +} + +static const uint8_t THIRD_MAC[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 }; + +static void scen_root_times(void) +{ + struct sim_bpdu r; + + printf("9. the times of the root travel on and time the listen period\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + r.age = 3; + r.maxage = 12; + r.hello = 1; + r.fwd = 8; + bpdu_in(&r); + check(stp_root_port == 8, "port 9 is the root port"); + links_set((1 << 1) | (1 << 8)); + for (int i = 0; i < 2; i++) { + bpdu_in(&r); + secs(1); + } + check_state(1, 1, "a new designated port starts discarding"); + for (int i = 0; i < 5; i++) { + bpdu_in(&r); + secs(1); + } + check_state(1, 1, "it still discards short of the root's forward delay"); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(1); + } + check_state(1, 2, "it learns after the root's 8 s, not our own 15 s"); + for (int i = 0; i < 8; i++) { + bpdu_in(&r); + secs(1); + } + check_state(1, 3, "and forwards after a second 8 s"); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(1); + } + check(tx_frames[1] > 0, "the designated port sends BPDUs"); + check((uint8_t)last_tx[1].age == 4, "the message age is one more than received"); + check((uint8_t)last_tx[1].age_max == 12, "max age is the root's"); + check((uint8_t)last_tx[1].fwd_delay == 8, "forward delay is the root's"); + check((uint8_t)last_tx[1].hello == 2, "hello time stays our own"); +} + +static void scen_too_old(void) +{ + struct sim_bpdu r; + + printf("10. information as old as max age is not used\n"); + reset_all(); + links_set(1 << 8); + secs(1); + r = root_on(8); + r.age = 20; + r.maxage = 20; + r.hello = 2; + r.fwd = 15; + bpdu_in(&r); + check(stp_root_port == 0xff, "the switch stays its own root"); + r.age = 19; + bpdu_in(&r); + check(stp_root_port == 8, "one second younger it is accepted"); +} + +static void scen_info_expiry(void) +{ + struct sim_bpdu r; + + printf("11. received information lasts three of the sender's hellos\n"); + reset_all(); + links_set(1 << 8); + secs(1); + r = root_on(8); + r.maxage = 20; + r.hello = 1; + r.fwd = 15; + bpdu_in(&r); + check(stp_root_port == 8, "port 9 is the root port"); + tick(2 * STP_HZ + STP_HZ / 2); + check(stp_root_port == 8, "still the root port 2.5 s later"); + tick(STP_HZ); + check(stp_root_port == 0xff, "gone after 3 s with a 1 s hello"); +} + +static void scen_dport_tiebreak(void) +{ + struct sim_bpdu a; + struct sim_bpdu b; + + printf("12. equal cost to one bridge: the lower designated port ID wins\n"); + reset_all(); + links_set((1 << 1) | (1 << 2)); + secs(1); + a = root_on(1); + a.port_id = 5; + b = root_on(2); + b.port_id = 2; + for (int i = 0; i < 12; i++) { + bpdu_in(&a); + bpdu_in(&b); + secs(2); + } + check(stp_root_port == 2, "port 3, which hears port ID 8002, is the root port"); + check_state(1, 1, "port 2, which hears 8005, is discarding"); +} + +static void scen_rxport_tiebreak(void) +{ + struct sim_bpdu a; + struct sim_bpdu b; + + printf("13. same designated port on two of ours: the receiving port ID decides\n"); + reset_all(); + links_set((1 << 3) | (1 << 4)); + secs(1); + a = root_on(3); + b = root_on(4); + for (int i = 0; i < 3; i++) { + bpdu_in(&a); + bpdu_in(&b); + secs(2); + } + check(stp_root_port == 3, "equal port priority: the lower port number, port 4"); + stp_pprio[4] = 0x40; + for (int i = 0; i < 3; i++) { + bpdu_in(&a); + bpdu_in(&b); + secs(2); + } + check(stp_root_port == 4, "a better priority on port 5 wins"); + stp_pprio[4] = 0x80; +} + +static void scen_inferior_info(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + struct sim_bpdu third; + + printf("14. worse information replaces stored only from the same sender\n"); + reset_all(); + links_set((1 << 0) | (1 << 8)); + secs(1); + r = root_on(8); + memcpy(peer.root_mac, ROOT_MAC, 6); + memcpy(peer.br_mac, PEER_MAC, 6); + third = peer; + third.root_cost = 400000; + memcpy(third.br_mac, THIRD_MAC, 6); + for (int i = 0; i < 10; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check_state(0, 1, "port 1 is discarding behind the better peer"); + for (int i = 0; i < 2; i++) { + bpdu_in(&r); + bpdu_in(&peer); + bpdu_in(&third); + secs(1); + } + check(stp_dcost[0] == 2000, "a worse bridge on the segment does not overwrite the peer"); + check_state(0, 1, "and port 1 keeps discarding"); + peer.root_cost = 400000; + peer.flags = 0x0c; + bpdu_in(&peer); + check(stp_dcost[0] == 400000, "the peer's own worse information does replace it"); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check_state(0, 3, "port 1 is designated and forwards again"); +} + +static void scen_root_role_bpdu(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer = { + .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1, .flags = 0x08 | 0x30 + }; + + printf("15. an RST BPDU sent by a root port is not designated information\n"); + reset_all(); + links_set((1 << 0) | (1 << 8)); + secs(1); + r = root_on(8); + memcpy(peer.root_mac, ROOT_MAC, 6); + memcpy(peer.br_mac, PEER_MAC, 6); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check_state(0, 3, "port 1 keeps forwarding"); + check(stp_root_port == 8, "the uplink stays the root port"); +} + +static void scen_alternate_takes_over(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + + printf("16. when the root port's information expires an alternate path takes over\n"); + reset_all(); + stp_pcost[8] = 2000; + links_set((1 << 0) | (1 << 8)); + secs(1); + r = root_on(8); + memcpy(peer.root_mac, ROOT_MAC, 6); + memcpy(peer.br_mac, PEER_MAC, 6); + for (int i = 0; i < 12; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check(stp_root_port == 8, "the 10G uplink is the root port"); + for (int i = 0; i < 4; i++) { + bpdu_in(&peer); + secs(2); + } + check(stp_root_port == 0, "port 1 is the root port once port 9 went quiet"); + check(memcmp(root_bridge.mac, ROOT_MAC, 6) == 0, "the root is still the same bridge"); + check(root_bridge_cost == 22000, "the cost is the peer's plus port 1's"); + stp_pcost[8] = 0; +} + +static void scen_dbridge_tiebreak(void) +{ + struct sim_bpdu via_peer = { .port = 1, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + struct sim_bpdu via_third; + + printf("17. equal cost through two bridges: the lower bridge ID wins\n"); + reset_all(); + links_set((1 << 1) | (1 << 2)); + secs(1); + memcpy(via_peer.root_mac, ROOT_MAC, 6); + memcpy(via_peer.br_mac, PEER_MAC, 6); + via_third = via_peer; + via_third.port = 2; + memcpy(via_third.br_mac, THIRD_MAC, 6); + via_third.br_prio = 0x70; + for (int i = 0; i < 12; i++) { + bpdu_in(&via_peer); + bpdu_in(&via_third); + secs(2); + } + check(stp_root_port == 2, "port 3, behind the bridge with priority 7000, is the root port"); + check_state(1, 1, "port 2, behind 8000, is discarding"); +} + +static void scen_times_revert(void) +{ + struct sim_bpdu r; + + printf("18. back to our own times once we are the root again\n"); + reset_all(); + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + r.age = 3; + r.maxage = 12; + r.hello = 2; + r.fwd = 8; + for (int i = 0; i < 4; i++) { + bpdu_in(&r); + secs(2); + } + check(stp_root_port == 8, "port 9 is the root port"); + secs(8); + check(stp_root_port == 0xff, "the switch is its own root again"); + tx_frames[1] = 0; + secs(3); + check(tx_frames[1] > 0, "port 2 still sends"); + check((uint8_t)last_tx[1].age == 0, "the message age is 0 from the root"); + check((uint8_t)last_tx[1].age_max == 20, "max age is our own again"); + check((uint8_t)last_tx[1].fwd_delay == 15, "forward delay is our own again"); +} + +static void ring_with_uplink_on_9(struct sim_bpdu *r, struct sim_bpdu *peer) +{ + struct sim_bpdu p = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + + reset_all(); + stp_pcost[8] = 2000; + links_set((1 << 0) | (1 << 8)); + secs(1); + *r = root_on(8); + memcpy(p.root_mac, ROOT_MAC, 6); + memcpy(p.br_mac, PEER_MAC, 6); + *peer = p; + for (int i = 0; i < 12; i++) { + bpdu_in(r); + bpdu_in(peer); + secs(2); + } +} + +static void scen_tc_via_root_port(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer; + int waited = 0; + + printf("19. a port that takes over as root port tells the root about it\n"); + ring_with_uplink_on_9(&r, &peer); + check(stp_root_port == 8 && port_state(0) == 1, "port 1 starts as the blocked alternate"); + check(tc_frames[0] == 0, "no TC on port 1 so far"); + links_set(1 << 0); + while (port_state(0) != 3 && waited < 20 * STP_HZ) { + bpdu_in(&peer); + tick(STP_HZ / 2); + waited += STP_HZ / 2; + } + check(stp_root_port == 0, "port 1 is the root port after port 9 went down"); + check_state(0, 3, "and forwards"); + tick(2); + check(tc_frames[0] > 0, "port 1 sends a BPDU with TC toward the root right away"); + check((last_tx[0].flags & 0x0c) == 0x08, "that BPDU carries the root port role"); + stp_pcost[8] = 0; +} + +static void scen_tc_window_rstp(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer; + int waited = 0; + int seen; + + printf("20. in RSTP the TC window lasts hello time plus one second\n"); + ring_with_uplink_on_9(&r, &peer); + links_set(1 << 0); + while (port_state(0) != 3 && waited < 20 * STP_HZ) { + bpdu_in(&peer); + tick(STP_HZ / 2); + waited += STP_HZ / 2; + } + for (int i = 0; i < 4; i++) { + bpdu_in(&peer); + secs(1); + } + seen = tc_frames[0]; + for (int i = 0; i < 6; i++) { + bpdu_in(&peer); + secs(1); + } + check(seen > 0, "TC was sent"); + check(tc_frames[0] == seen, "and stops once hello plus one second is over"); + stp_pcost[8] = 0; +} + +static void scen_tc_propagation(void) +{ + struct sim_bpdu r; + int tc2; + int tc9; + struct sim_bpdu rtc; + + printf("21. a TC from the root side goes out on the other ports, not back\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "designated port 2 forwards"); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + memset(flush_count, 0, sizeof(flush_count)); + tc2 = tc_frames[1]; + tc9 = tc_frames[8]; + rtc = r; + rtc.flags = 0x3d; + bpdu_in(&rtc); + tick(2); + check(tc_frames[1] > tc2, "port 2 passes the TC on at once"); + check(flush_count[1] > 0, "port 2 forgets what it learned"); + check(flush_count[8] == 0, "the port the TC came in on keeps its addresses"); + check(tc_frames[8] == tc9, "no TC is echoed back to the root"); +} + +static void scen_tc_legacy_tcn(void) +{ + struct sim_bpdu r; + struct sim_bpdu ack; + int quiet; + int n; + + printf("22. with 802.1D neighbours the root port sends TCN until acknowledged\n"); + reset_all(); + stp_rstp = 0; + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + r.legacy = 1; + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check(stp_root_port == 8, "port 9 is the root port"); + check(tcn_frames[8] > 0, "port 9 entering forwarding is itself reported with TCN"); + ack = r; + ack.flags = 0x81; + bpdu_in(&ack); + quiet = tcn_frames[8]; + for (int i = 0; i < 5; i++) { + bpdu_in(&r); + secs(2); + } + check(tcn_frames[8] == quiet, "acknowledged: no more TCN while nothing changes"); + links_set((1 << 1) | (1 << 8)); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "port 2 came up and forwards"); + check(tcn_frames[8] > quiet, "port 9 sends TCN toward the root again"); + bpdu_in(&ack); + n = tcn_frames[8]; + for (int i = 0; i < 5; i++) { + bpdu_in(&r); + secs(2); + } + check(tcn_frames[8] == n, "and stops once the root acknowledged it"); +} + +static void scen_alt_forgets(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 }; + + printf("23. a port that turns alternate forgets what it learned\n"); + reset_all(); + links_set((1 << 0) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 10; i++) { + bpdu_in(&r); + secs(2); + } + check_state(0, 3, "port 1 forwards as designated"); + memset(flush_count, 0, sizeof(flush_count)); + memcpy(peer.root_mac, ROOT_MAC, 6); + memcpy(peer.br_mac, PEER_MAC, 6); + bpdu_in(&peer); + check_state(0, 1, "port 1 is blocked behind the better peer"); + check(flush_count[0] > 0, "and its addresses are flushed"); +} + +static void scen_edge_no_tc(void) +{ + struct sim_bpdu r; + uint16_t tc; + int tc9; + int tc2; + + printf("24. an edge port coming up raises no topology change\n"); + reset_all(); + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 10; i++) { + bpdu_in(&r); + secs(2); + } + for (int i = 0; i < 20; i++) { + bpdu_in(&r); + secs(2); + } + tc = stp_tc_count; + tc9 = tc_frames[8]; + tc2 = tc_frames[1]; + links_set((1 << 1) | (1 << 3) | (1 << 8)); + for (int i = 0; i < 4; i++) { + bpdu_in(&r); + secs(2); + } + check_state(3, 3, "the host port forwards as an edge port"); + check(stp_tc_count == tc, "the change counter does not move"); + check(tc_frames[8] == tc9 && tc_frames[1] == tc2, "no TC is sent"); +} + +static void scen_link_loss_no_tc(void) +{ + struct sim_bpdu r; + int tc3; + int tc9; + + printf("25. losing a designated link flushes it but sends no TC\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + stp_pflags[2] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 2) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 20; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "port 2 forwards"); + memset(flush_count, 0, sizeof(flush_count)); + tc3 = tc_frames[2]; + tc9 = tc_frames[8]; + links_set((1 << 2) | (1 << 8)); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(1); + } + check(flush_count[1] > 0, "port 2's addresses are flushed"); + check(tc_frames[2] == tc3 && tc_frames[8] == tc9, "no TC goes out"); +} + +static void scen_proposal_agreement(void) +{ + struct sim_bpdu r; + struct sim_bpdu agree = { + .port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78 + }; + + printf("26. a new designated port proposes and forwards as soon as the neighbour agrees\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + links_set((1 << 1) | (1 << 8)); + tick(STP_HZ + STP_HZ / 2); + check_state(1, 1, "port 2 starts discarding"); + check((last_tx[1].flags & 0x02) != 0, "and its BPDU carries the proposal flag"); + check((last_tx[1].flags & 0x0c) == 0x0c, "with the designated role"); + memcpy(agree.root_mac, ROOT_MAC, 6); + memcpy(agree.br_mac, PEER_MAC, 6); + bpdu_in(&agree); + tick(2); + check_state(1, 3, "port 2 forwards right after the agreement"); +} + +static void scen_no_agreement(void) +{ + struct sim_bpdu r; + + printf("27. without an agreement the port waits both forward delays\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + stp_pflags[2] &= ~STP_PF_AUTOEDGE; + stp_pp2p[2] = 2; + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + links_set((1 << 1) | (1 << 2) | (1 << 8)); + for (int i = 0; i < 5; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 1, "port 2 still discards after 10 s"); + check((last_tx[2].flags & 0x02) == 0, "a port set to p2p off does not propose"); + for (int i = 0; i < 4; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 2, "port 2 learns once the first forward delay is over"); + for (int i = 0; i < 8; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "and forwards after the second"); + stp_pp2p[2] = 0; +} + +static void scen_sync_on_proposal(void) +{ + struct sim_bpdu r; + struct sim_bpdu prop; + + printf("28. a proposal on the root port syncs the designated ports and is agreed\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 3) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "designated port 2 forwards"); + check_state(3, 3, "edge port 4 forwards"); + check_state(8, 3, "root port 9 forwards"); + prop = r; + prop.flags = 0x3e; + bpdu_in(&prop); + tick(2); + check_state(1, 1, "port 2 is put back to discarding for the sync"); + check((last_tx[1].flags & 0x02) != 0, "and proposes downstream"); + check_state(3, 3, "the edge port is left alone"); + check_state(8, 3, "the root port keeps forwarding"); + check((last_tx[8].flags & 0x40) != 0, "the root port answers with an agreement"); + check((last_tx[8].flags & 0x0c) == 0x08, "in a BPDU with the root role"); +} + +static void scen_rapid_failover(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer; + + printf("29. when the root port goes down the alternate forwards at once\n"); + ring_with_uplink_on_9(&r, &peer); + check(stp_root_port == 8 && port_state(0) == 1, "port 1 is the blocked alternate"); + links_set(1 << 0); + for (int i = 0; i < 4; i++) { + bpdu_in(&peer); + tick(STP_HZ / 2); + } + check(stp_root_port == 0, "port 1 is the root port within two seconds"); + check_state(0, 3, "and already forwards"); + stp_pcost[8] = 0; +} + +static void scen_rapid_reroot(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer; + int both = 0; + + printf("30. the uplink coming back takes over without both ports forwarding\n"); + ring_with_uplink_on_9(&r, &peer); + stp_pcost[8] = 2000; + links_set(1 << 0); + for (int i = 0; i < 6; i++) { + bpdu_in(&peer); + tick(STP_HZ / 2); + } + check(stp_root_port == 0 && port_state(0) == 3, "port 1 is the forwarding root port"); + links_set((1 << 0) | (1 << 8)); + for (int i = 0; i < 12; i++) { + struct sim_bpdu p = r; + p.flags = 0x3e; + bpdu_in(&p); + if (port_state(0) == 3 && port_state(8) == 3) + both++; + bpdu_in(&peer); + if (port_state(0) == 3 && port_state(8) == 3) + both++; + for (int k = 0; k < STP_HZ / 4; k++) { + tick(1); + if (port_state(0) == 3 && port_state(8) == 3) + both++; + } + } + check(stp_root_port == 8, "port 9 is the root port again"); + check_state(8, 3, "and forwards within three seconds"); + check_state(0, 1, "port 1 is back to discarding"); + check(both == 0, "ports 1 and 9 never forwarded at the same time"); + stp_pcost[8] = 0; +} + +static void scen_agreement_other_root(void) +{ + struct sim_bpdu r; + struct sim_bpdu agree = { + .port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78 + }; + + printf("31. an agreement for a different root does not open a port\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + links_set((1 << 1) | (1 << 8)); + tick(STP_HZ + STP_HZ / 2); + memcpy(agree.root_mac, THIRD_MAC, 6); + memcpy(agree.br_mac, PEER_MAC, 6); + bpdu_in(&agree); + tick(2); + check_state(1, 1, "port 2 keeps discarding"); +} + +static void scen_repeated_proposal(void) +{ + struct sim_bpdu r; + struct sim_bpdu prop; + struct sim_bpdu agree = { + .port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78 + }; + int agreements = 0; + + printf("32. a proposal repeated every hello syncs only once\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "designated port 2 forwards"); + prop = r; + prop.flags = 0x3e; + bpdu_in(&prop); + tick(2); + check_state(1, 1, "the first proposal syncs port 2"); + memcpy(agree.root_mac, ROOT_MAC, 6); + memcpy(agree.br_mac, PEER_MAC, 6); + bpdu_in(&agree); + tick(2); + check_state(1, 3, "port 2 forwards again after its own agreement"); + for (int i = 0; i < 5; i++) { + int before = tx_frames[8]; + bpdu_in(&prop); + tick(2); + if (tx_frames[8] > before && (last_tx[8].flags & 0x40)) + agreements++; + secs(2); + check_state(1, 3, "a repeated proposal does not block port 2 again"); + } + check(agreements == 5, "every repeated proposal is answered with an agreement"); +} + +static void scen_dispute(void) +{ + struct sim_bpdu r; + struct sim_bpdu quiet = { + .port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x0c + }; + struct sim_bpdu claim; + int opened = 0; + + printf("33. a worse neighbour that claims to forward as designated is disputed\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "designated port 2 forwards"); + memcpy(quiet.root_mac, ROOT_MAC, 6); + memcpy(quiet.br_mac, PEER_MAC, 6); + bpdu_in(&quiet); + tick(2); + check_state(1, 3, "worse designated info without the learning flag is no dispute"); + claim = quiet; + claim.flags = 0x3c; + bpdu_in(&claim); + tick(2); + check_state(1, 1, "with the learning flag set port 2 goes back to discarding"); + for (int i = 0; i < 12; i++) { + bpdu_in(&r); + bpdu_in(&claim); + for (int k = 0; k < 2 * STP_HZ; k++) { + tick(1); + if (port_state(1) == 3) + opened++; + } + } + check(opened == 0, "and never forwards while the dispute goes on"); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + bpdu_in(&quiet); + secs(2); + } + check_state(1, 3, "once the neighbour stops claiming, port 2 forwards again"); +} + +static void scen_learning_state(void) +{ + struct sim_bpdu r; + struct sim_bpdu claim = { + .port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x3c + }; + + printf("34. without an agreement a port learns for one forward delay before it forwards\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + links_set((1 << 1) | (1 << 8)); + for (int i = 0; i < 6; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 1, "port 2 discards for the first forward delay"); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 2, "then learns"); + check((last_tx[1].flags & 0x30) == 0x10, "its BPDUs carry learning without forwarding"); + memcpy(claim.root_mac, ROOT_MAC, 6); + memcpy(claim.br_mac, PEER_MAC, 6); + bpdu_in(&claim); + tick(2); + check_state(1, 1, "a dispute while learning sends it back to discarding"); + for (int i = 0; i < 9; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 2, "after a full forward delay it learns again"); + for (int i = 0; i < 8; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "and forwards after the second forward delay"); +} + +static void scen_reply_to_inferior(void) +{ + struct sim_bpdu r; + int n; + int guard = 0; + struct sim_bpdu worse = { + .port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x0c + }; + struct sim_bpdu same_root_better = { + .port = 1, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1, .flags = 0x0c + }; + + printf("35. a designated port answers worse information at once\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "designated port 2 forwards"); + n = tx_frames[1]; + while (tx_frames[1] == n && guard++ < 3 * STP_HZ) + tick(1); + check(tx_frames[1] > n, "port 2 sent its regular hello"); + memcpy(worse.root_mac, ROOT_MAC, 6); + memcpy(worse.br_mac, PEER_MAC, 6); + n = tx_frames[1]; + bpdu_in(&worse); + tick(2); + check(tx_frames[1] > n, "the worse BPDU is answered within two ticks, not at the next hello"); + tick(STP_HZ / 2); + n = tx_frames[1]; + memcpy(same_root_better.root_mac, ROOT_MAC, 6); + memcpy(same_root_better.br_mac, PEER_MAC, 6); + bpdu_in(&same_root_better); + tick(2); + check(tx_frames[1] == n, "better information is not answered"); +} + +static void scen_backup_role(void) +{ + struct sim_bpdu r; + struct sim_bpdu own = { .port = 2, .root_prio = 0x40, .root_cost = 20000, .br_prio = 0x80, .port_id = 4 }; + + printf("36. a port hearing another port of this bridge is a blocked backup\n"); + reset_all(); + stp_pflags[2] &= ~STP_PF_AUTOEDGE; + stp_pflags[3] &= ~STP_PF_AUTOEDGE; + links_set((1 << 2) | (1 << 3) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(2, 3, "port 3 forwards"); + check_state(3, 3, "port 4 forwards"); + memcpy(own.root_mac, ROOT_MAC, 6); + memcpy(own.br_mac, uip_ethaddr.addr, 6); + bpdu_in(&own); + tick(2); + check_state(3, 1, "port 4, the worse of the two, is blocked"); + check(((stp_backup >> 3) & 1) == 1, "and has the backup role"); + check(((stp_backup >> 2) & 1) == 0, "port 3 is not a backup"); + for (int i = 0; i < 20; i++) { + bpdu_in(&r); + secs(2); + } + check_state(3, 3, "once its own BPDUs stop coming back port 4 forwards again"); + check(((stp_backup >> 3) & 1) == 0, "and is no longer a backup"); + bpdu_in(&own); + tick(2); + check(((stp_backup >> 3) & 1) == 1, "a new loop makes it a backup again"); + links_set((1 << 2) | (1 << 8)); + secs(2); + check(((stp_backup >> 3) & 1) == 0, "losing the link clears the backup role"); +} + +static void migrate_to_stp(void) +{ + struct sim_bpdu r; + struct sim_bpdu old = { + .port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .legacy = 1 + }; + + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + check(last_tx[1].version == 2, "port 2 starts with RST BPDUs"); + memcpy(old.root_mac, ROOT_MAC, 6); + memcpy(old.br_mac, PEER_MAC, 6); + bpdu_in(&old); + tick(2); + check(last_tx[1].version == 0 && last_tx[1].bpdu_type == 0, "after the 802.1D BPDU port 2 sends Config BPDUs"); + check((last_tx[1].flags & 0x02) == 0, "without a proposal"); + check(((stp_legacy >> 1) & 1) == 1 && ((stp_legacy >> 8) & 1) == 0, + "only port 2 is switched, not the root port"); +} + +static void scen_migrate_to_stp(void) +{ + printf("37. a port that hears an 802.1D neighbour sends Config BPDUs\n"); + migrate_to_stp(); +} + +static void scen_migrate_back(void) +{ + struct sim_bpdu r; + struct sim_bpdu old = { + .port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .legacy = 1 + }; + struct sim_bpdu rst; + + printf("38. mcheck and an RST neighbour bring the port back to RSTP\n"); + migrate_to_stp(); + r = root_on(8); + secs(4); + stp_port_mcheck(1); + tick(2); + check(last_tx[1].version == 2, "mcheck makes port 2 send RST BPDUs again"); + memcpy(old.root_mac, ROOT_MAC, 6); + memcpy(old.br_mac, PEER_MAC, 6); + bpdu_in(&old); + tick(STP_HZ); + check(last_tx[1].version == 2, "an 802.1D BPDU within the migrate time does not switch it yet"); + for (int i = 0; i < 4; i++) { + bpdu_in(&r); + tick(STP_HZ / 2); + } + tick(2 * STP_HZ); + check(last_tx[1].version == 0, "once the migrate time is over it falls back to STP"); + secs(4); + rst = old; + rst.legacy = 0; + rst.flags = 0x0c; + bpdu_in(&rst); + tick(2); + check(last_tx[1].version == 2, "an RST BPDU after the migrate time switches it back to RSTP"); +} + +static void scen_legacy_root_tcn(void) +{ + struct sim_bpdu r; + struct sim_bpdu ack; + int tcn; + + printf("39. behind an 802.1D root port a topology change goes out as TCN\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + r.legacy = 1; + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check(stp_root_port == 8, "port 9 is the root port"); + ack = r; + ack.flags = 0x81; + bpdu_in(&ack); + for (int i = 0; i < 5; i++) { + bpdu_in(&r); + secs(2); + } + tcn = tcn_frames[8]; + links_set((1 << 1) | (1 << 8)); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + secs(2); + } + check_state(1, 3, "port 2 came up and forwards"); + check(tcn_frames[8] > tcn, "port 9 reports the change with TCN"); + check(last_tx[1].version == 2, "port 2, with no 802.1D neighbour, sends RST BPDUs"); +} + +static void scen_alternate_agrees(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer; + struct sim_bpdu prop; + int n; + + printf("40. an alternate port answers a proposal with an agreement\n"); + ring_with_uplink_on_9(&r, &peer); + stp_pflags[2] &= ~STP_PF_AUTOEDGE; + links_set((1 << 0) | (1 << 2) | (1 << 8)); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check(stp_root_port == 8 && port_state(0) == 1, "port 1 is the blocked alternate"); + check_state(2, 3, "designated port 3 forwards"); + prop = peer; + prop.flags = 0x3e; + n = tx_frames[0]; + bpdu_in(&prop); + tick(2); + check(tx_frames[0] > n, "port 1 answers at once"); + check((last_tx[0].flags & 0x40) != 0, "with the agreement flag"); + check((last_tx[0].flags & 0x0c) == 0x04, "in a BPDU with the alternate role"); + check((last_tx[0].flags & 0x02) == 0, "and without a proposal of its own"); + check_state(0, 1, "port 1 stays discarding"); + check_state(8, 3, "the root port is not touched by the sync"); + check_state(2, 1, "the first proposal syncs designated port 3"); + for (int i = 0; i < 18; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check_state(2, 3, "port 3 forwards again"); + n = tx_frames[0]; + bpdu_in(&prop); + tick(2); + check(tx_frames[0] > n && (last_tx[0].flags & 0x40), "a repeated proposal is answered again"); + check_state(2, 3, "but does not sync port 3 a second time"); + n = tx_frames[0]; + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + bpdu_in(&peer); + secs(2); + } + check(tx_frames[0] == n, "without a proposal the alternate port stays silent"); + stp_pcost[8] = 0; +} + +static void scen_agreement_from_alternate(void) +{ + struct sim_bpdu r; + struct sim_bpdu agree = { + .port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x44 + }; + + printf("41. an agreement from an alternate port opens a designated port\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + links_set((1 << 1) | (1 << 8)); + tick(STP_HZ + STP_HZ / 2); + check_state(1, 1, "port 2 proposes from discarding"); + memcpy(agree.root_mac, ROOT_MAC, 6); + memcpy(agree.br_mac, PEER_MAC, 6); + bpdu_in(&agree); + tick(2); + check_state(1, 3, "the agreement in an alternate-role BPDU opens it"); +} + +static void scen_timer_relation(void) +{ + printf("42. bridge times must keep 2*(fwd-1) >= maxage >= 2*(hello+1)\n"); + reset_all(); + save_cmd = 1; + sim_cmd("stp fwd 30"); + sim_cmd("stp maxage 40"); + check(stp_fwddelay_s == 30 && stp_maxage_s == 40, "fwd 30 then maxage 40 is accepted"); + check(err_status == ERR_OK, "an accepted value reports no error, so it is kept in the command history"); + sim_cmd("stp fwd 20"); + check(stp_fwddelay_s == 30, "fwd 20 under maxage 40 is refused"); + check(err_status != ERR_OK, "a refused value reports an error, so it stays out of the command history"); + sim_cmd("stp port 3 cost x"); + check(err_status != ERR_OK, "a malformed command reports an error too"); + sim_cmd("stp maxage 6"); + sim_cmd("stp hello 3"); + check(stp_maxage_s == 6 && stp_hello_s == 2, "hello 3 under maxage 6 is refused"); + sim_cmd("stp fwd 4"); + sim_cmd("stp maxage 7"); + check(stp_fwddelay_s == 4 && stp_maxage_s == 6, "maxage 7 over fwd 4 is refused"); + stp_defaults(); + save_cmd = 0; + sim_cmd("stp maxage 40"); + sim_cmd("stp fwd 30"); + check(stp_maxage_s == 40 && stp_fwddelay_s == 30, "a saved config may pass through a broken order"); + stp_setup(); + check(stp_maxage_s == 40 && stp_fwddelay_s == 30, "and keeps its values when the result is consistent"); + sim_cmd("stp fwd 10"); + stp_setup(); + check(stp_hello_s == 2 && stp_maxage_s == 20 && stp_fwddelay_s == 15, + "an inconsistent result falls back to 2/20/15 when STP starts"); + save_cmd = 1; +} + +static void scen_recent_root(void) +{ + struct sim_bpdu r9; + struct sim_bpdu r3; + struct sim_bpdu agree = { + .port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78 + }; + + printf("43. ports that were root a moment ago are held back when a new root port forwards\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + stp_pflags[2] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + secs(1); + r9 = root_on(8); + for (int i = 0; i < 18; i++) { + bpdu_in(&r9); + secs(2); + } + check(stp_root_port == 8, "port 9 is the root port"); + check_state(1, 3, "designated port 2 forwards"); + stp_rrwhile[1] = 10 * STP_HZ; + stp_pcost[2] = 2000; + links_set((1 << 1) | (1 << 2) | (1 << 8)); + secs(1); + r3 = root_on(2); + bpdu_in(&r3); + tick(2); + check(stp_root_port == 2, "port 3, the cheaper path, is the new root port"); + check_state(2, 3, "and forwards at once"); + check_state(8, 1, "the old root port 9 is discarding"); + check_state(1, 1, "port 2, a recent root port too, is put back to discarding"); + memcpy(agree.root_mac, ROOT_MAC, 6); + memcpy(agree.br_mac, PEER_MAC, 6); + bpdu_in(&agree); + tick(2); + check_state(1, 1, "an agreement does not open it while it is a recent root"); + for (int i = 0; i < 6; i++) { + bpdu_in(&r3); + secs(2); + } + bpdu_in(&agree); + tick(2); + check_state(1, 3, "once that time is over the agreement opens it"); + stp_pcost[2] = 0; +} + +static void scen_recent_backup(void) +{ + struct sim_bpdu r; + struct sim_bpdu r4; + + printf("44. a port that was a backup a moment ago does not rush into forwarding as root\n"); + reset_all(); + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + stp_pflags[3] &= ~STP_PF_AUTOEDGE; + stp_pcost[3] = 2000; + stp_rbwhile[3] = 3 * STP_HZ; + links_set((1 << 3) | (1 << 8)); + secs(1); + r4 = root_on(3); + bpdu_in(&r4); + tick(2); + check(stp_root_port == 3, "port 4 is the new root port"); + check_state(3, 1, "but stays discarding while it is a recent backup"); + for (int i = 0; i < 4; i++) { + bpdu_in(&r4); + secs(1); + } + check_state(3, 3, "and forwards once that time is over"); + stp_pcost[3] = 0; +} + +static void scen_short_bpdu(void) +{ + struct sim_bpdu r; + struct sim_bpdu c; + int n; + struct sim_bpdu tcn; + + printf("45. BPDUs shorter than their type allows are ignored\n"); + reset_all(); + links_set(1 << 8); + secs(1); + r = root_on(8); + r.msg_len = 38; + bpdu_in(&r); + check(stp_root_port == 0xff, "an RST BPDU of 38 bytes is ignored"); + r.msg_len = 0; + bpdu_in(&r); + check(stp_root_port == 8, "at 39 bytes it is taken"); + reset_all(); + links_set(1 << 8); + secs(1); + c = root_on(8); + c.legacy = 1; + c.msg_len = 37; + bpdu_in(&c); + check(stp_root_port == 0xff, "a Config BPDU of 37 bytes is ignored"); + c.msg_len = 0; + bpdu_in(&c); + check(stp_root_port == 8, "at 38 bytes it is taken"); + n = tx_frames[8]; + tcn = c; + tcn.legacy = 2; + tcn.msg_len = 6; + bpdu_in(&tcn); + check(tx_frames[8] == n, "a TCN of 6 bytes gets no TCA"); + tcn.msg_len = 0; + bpdu_in(&tcn); + check(tx_frames[8] > n, "a TCN of 7 bytes is acknowledged"); +} + +static void scen_port_roles(void) +{ + struct sim_bpdu r; + struct sim_bpdu peer; + + printf("46. the reported role covers every port, a port without link is disabled\n"); + ring_with_uplink_on_9(&r, &peer); + check(stp_port_role(8) == 1, "port 9 is reported as root"); + check(stp_port_role(0) == 3, "port 1 as alternate"); + check(stp_port_role(4) == 0, "port 5, which has no link, as disabled"); + links_set((1 << 0) | (1 << 4) | (1 << 8)); + secs(2); + check(stp_port_role(4) == 2, "port 5 is designated once its link is up"); + stp_pflags[4] &= ~STP_PF_ENABLED; + check(stp_port_role(4) == 0, "and disabled again when STP is off on it"); + stp_pflags[4] |= STP_PF_ENABLED; + stp_pcost[8] = 0; +} + +static void scen_no_tx_without_link(void) +{ + struct sim_bpdu r; + + printf("47. no BPDU goes out on a port without link\n"); + reset_all(); + links_set(1 << 8); + secs(1); + r = root_on(8); + for (int i = 0; i < 5; i++) { + bpdu_in(&r); + secs(2); + } + check(tx_frames[1] == 0, "port 2 without link sent nothing in ten seconds"); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + for (int i = 0; i < 2; i++) { + bpdu_in(&r); + secs(2); + } + check(tx_frames[1] > 0, "once its link is up it sends"); +} + +static void scen_counters(void) +{ + struct sim_bpdu r; + int sent = 1; + int same = 1; + int any = 0; + struct sim_bpdu tcn; + int zero; + + printf("48. per-port BPDU and TC counters, and stp clear\n"); + reset_all(); + stp_pflags[1] &= ~STP_PF_AUTOEDGE; + links_set((1 << 1) | (1 << 8)); + r = root_on(8); + for (int i = 0; i < 40; i++) { + bpdu_in(&r); + secs(1); + } + check(stp_cnt[STP_CNT_RX][8] == 40 && stp_cnt[STP_CNT_RX][1] == 0, + "each BPDU taken in is counted on its own port"); + check(stp_cnt[STP_CNT_TX][1] > 0 && stp_cnt[STP_CNT_TX][0] == 0, + "BPDUs sent are counted, none on a port without link"); + for (int p = 0; p < NPORTS; p++) + sent &= stp_cnt[STP_CNT_TX][p] == (uint32_t)tx_frames[p]; + check(sent, "BPDUs sent match the frames on the wire on every port"); + check(stp_cnt[STP_CNT_TCRX][8] == 0, "a BPDU without the TC flag is not counted as TC"); + for (int p = 0; p < NPORTS; p++) { + same &= stp_cnt[STP_CNT_TCTX][p] == (uint32_t)(tc_frames[p] + tcn_frames[p]); + any |= tc_frames[p] + tcn_frames[p]; + } + check(any && same, "TC sent matches the TC and TCN frames on the wire"); + r.flags = 0x3d; + bpdu_in(&r); + check(stp_cnt[STP_CNT_TCRX][8] == 1, "a BPDU with the TC flag is counted as TC"); + tcn = r; + tcn.legacy = 2; + bpdu_in(&tcn); + check(stp_cnt[STP_CNT_TCRX][8] == 2 && stp_cnt[STP_CNT_RX][8] == 42, "and so is a TCN"); + sim_cmd("stp clear"); + zero = err_status == ERR_OK; + for (int k = 0; k < STP_CNT_N; k++) + for (int p = 0; p < NPORTS; p++) + zero &= stp_cnt[k][p] == 0; + check(zero, "stp clear zeroes every counter on every port"); +} + +static void scen_path_cost_method(void) +{ + struct sim_bpdu r; + + printf("49. short path costs follow 802.1D-1998\n"); + reset_all(); + links_set(1 << 8); + r = root_on(8); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(1); + } + check(root_bridge_cost == 20000, "long method: a 1G root port costs 20000"); + sim_cmd("stp pathcost short"); + check(err_status == ERR_OK && stp_pcost_short == 1, "stp pathcost short is accepted"); + for (int i = 0; i < 2; i++) { + bpdu_in(&r); + secs(1); + } + check(root_bridge_cost == 4, "short method: the same port costs 4"); + sim_speed[8] = 4; + for (int i = 0; i < 2; i++) { + bpdu_in(&r); + secs(1); + } + check(root_bridge_cost == 2, "and a 10G port costs 2"); + sim_cmd("stp pathcost medium"); + check(err_status != ERR_OK && stp_pcost_short == 1, "an unknown method is refused"); + sim_cmd("stp pathcost long"); + sim_speed[8] = 2; +} + +static void scen_bpdu_handling(void) +{ + printf("50. BPDU handling while STP is off: flood or filter\n"); + reset_all(); + stp_enabled = 0; + l2mc_calls = 0; + stp_off(); + check(l2mc_calls && (l2mc_pmask & PMASK_9) == PMASK_9, "by default BPDUs are flooded when STP stops"); + l2mc_calls = 0; + sim_cmd("stp bpdu filter"); + check(err_status == ERR_OK && l2mc_calls && l2mc_pmask == PMASK_CPU, + "filter keeps them on the CPU, at once while STP is off"); + stp_enabled = 1; + stp_setup(); + l2mc_calls = 0; + sim_cmd("stp bpdu flood"); + check(l2mc_calls == 0, "while STP runs the setting only waits"); + stp_enabled = 0; + stp_off(); + check((l2mc_pmask & PMASK_9) == PMASK_9, "and applies when STP stops"); + sim_cmd("stp bpdu drop"); + check(err_status != ERR_OK, "an unknown handling is refused"); + stp_enabled = 1; +} + +static void scen_last_tc(void) +{ + struct sim_bpdu tcn; + uint16_t before; + + printf("51. time since the last topology change\n"); + reset_all(); + links_set(1 << 8); + tcn = root_on(8); + tcn.legacy = 2; + secs(4); + before = stp_tc_count; + bpdu_in(&tcn); + check(stp_tc_count != before, "a TCN moves the topology change counter"); + secs(5); + check(stp_tc_secs >= 4 && stp_tc_secs <= 5, "the clock counts the seconds since then"); + bpdu_in(&tcn); + secs(1); + check(stp_tc_secs <= 1, "and starts again at the next change"); +} + +static void scen_lag(void) +{ + struct sim_bpdu before; + struct sim_bpdu b; + struct sim_bpdu c; + struct sim_bpdu t; + + printf("52. a link aggregation group is one port of the tree\n"); + reset_all(); + links_set((1 << 1) | (1 << 2) | (1 << 8)); + secs(1); + before = root_on(2); + bpdu_in(&before); + check(stp_root_port == 2 && stp_info_while[2], "port 3 hears the root before it joins a group"); + sim_lag[0] = (1 << 1) | (1 << 2); + secs(1); + check(stp_info_while[2] == 0, "joining the group drops what the port heard on its own"); + check(stp_ent_id(STP_LAG_BASE) && !stp_ent_id(1) && !stp_ent_id(2), "the group replaces its member ports"); + check(port_state(1) == 1 && port_state(2) == 1, "both members start discarding together"); + + b = root_on(2); + bpdu_in(&b); + check(stp_root_port == STP_LAG_BASE, "a BPDU on either member reaches the group"); + c = root_on(1); + c.root_cost = 4; + bpdu_in(&c); + check(stp_root_port == STP_LAG_BASE, "and the other member feeds the same port"); + secs(31); + bpdu_in(&b); + check(port_state(1) == 3 && port_state(2) == 3, "both members forward as the root port"); + + sim_cmd("stp port 2 cost 5"); + check(err_status != ERR_OK, "a member port cannot be set on its own"); + sim_cmd("stp lag 1 cost 5"); + check(err_status == ERR_OK && stp_pcost[STP_LAG_BASE] == 5, "the group takes the setting"); + sim_cmd("stp lag 5 cost 5"); + check(err_status != ERR_OK, "a group number past the last one is refused"); + + memset(tx_frames, 0, sizeof(tx_frames)); + tcn_frames[1] = 0; + t = root_on(2); + t.legacy = 2; + bpdu_in(&t); + check(tx_frames[1] == 1 && tx_frames[2] == 0, "a reply leaves through the lowest member"); + check(last_tx[1].port_id == STP_LAG_BASE + 1, "with the group's own port id"); + + memset(flush_count, 0, sizeof(flush_count)); + links_set((1 << 2) | (1 << 8)); + secs(1); + check(port_state(2) == 3 && stp_root_port == STP_LAG_BASE, "losing one member keeps the group forwarding"); + check(flush_count[2] == 0, "and is no topology change"); + memset(tx_frames, 0, sizeof(tx_frames)); + bpdu_in(&t); + check(tx_frames[1] == 0 && tx_frames[2] == 1, "replies move to the member that still has a link"); + + sim_lag[0] = 0; + links_set((1 << 1) | (1 << 2) | (1 << 8)); + secs(1); + check(!stp_ent_id(STP_LAG_BASE) && stp_ent_id(1) && stp_ent_id(2), "dissolving the group gives the ports back"); + check(stp_root_port != STP_LAG_BASE, "and the group stops being the root port"); + check(port_state(1) == 1 && port_state(2) == 1, "the ports start over discarding"); +} + +static void scen_tc_needs_forwarding(void) +{ + struct sim_bpdu r; + struct sim_bpdu worse = { + .port = 2, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x3d + }; + struct sim_bpdu tcn; + int tc; + + printf("53. a topology change heard on a port that does not forward yet is not passed on\n"); + reset_all(); + stp_pflags[2] &= ~STP_PF_AUTOEDGE; + links_set((1 << 0) | (1 << 2)); + secs(1); + r = root_on(0); + for (int i = 0; i < 3; i++) { + bpdu_in(&r); + secs(2); + } + check_state(0, 3, "root port 1 forwards"); + check_state(2, 1, "port 3 is still discarding"); + memcpy(worse.root_mac, ROOT_MAC, 6); + memcpy(worse.br_mac, PEER_MAC, 6); + memset(flush_count, 0, sizeof(flush_count)); + tc = stp_tc_count; + bpdu_in(&worse); + tick(2); + check(stp_tc_count == tc, "a TC flag on the discarding port raises no topology change"); + check(flush_count[0] == 0, "and the root port keeps its addresses"); + tcn = worse; + tcn.legacy = 2; + memset(last_tx, 0, sizeof(last_tx)); + bpdu_in(&tcn); + tick(2); + check(stp_tc_count == tc, "a TCN on the discarding port raises none either"); + check((last_tx[2].flags & 0x80) == 0, "and is not acknowledged"); + for (int i = 0; i < 16; i++) { + bpdu_in(&r); + secs(2); + } + check_state(2, 3, "port 3 forwards after both forward delays"); + for (int i = 0; i < 2; i++) { + bpdu_in(&r); + secs(2); + } + tc = stp_tc_count; + bpdu_in(&worse); + tick(2); + check(stp_tc_count == tc + 1 && flush_count[0] > 0, "now the same TC is passed on"); +} + +static void scen_root_port_needs_link(void) +{ + struct sim_bpdu r; + + printf("54. a BPDU credited to a port without carrier does not pick the root port\n"); + reset_all(); + links_set(1 << 2); + secs(1); + r = root_on(0); + bpdu_in(&r); + tick(2); + check(stp_root_port == 0xff, "the bridge stays its own root"); + links_set((1 << 0) | (1 << 2)); + secs(1); + check(stp_root_port == 0, "once the carrier is seen the port becomes the root port"); + check_state(0, 3, "and forwards"); +} + +static void follow_root(unsigned seconds) +{ + struct sim_bpdu root = root_on(8); + while (seconds--) { + bpdu_in(&root); + secs(1); + } +} + +static struct sim_bpdu loop_from_port8(void) +{ + struct sim_bpdu own = { + .port = 0, .root_prio = 0x40, .root_cost = 20000, + .br_prio = 0x80, .port_id = 8 + }; + memcpy(own.root_mac, ROOT_MAC, 6); + memcpy(own.br_mac, uip_ethaddr.addr, 6); + return own; +} + +static void loop_ports_ready(void) +{ + reset_all(); + links_set((1 << 0) | (1 << 7) | (1 << 8)); + follow_root(5); +} + +static void scen_loop_peer_edge_recovery(void) +{ + struct sim_bpdu own = loop_from_port8(); + int held = 1; + + printf("55. a silent loop peer regains auto-edge when the loop hold expires\n"); + loop_ports_ready(); + check((stp_pflags[0] & STP_PF_OPEREDGE) && (stp_pflags[7] & STP_PF_OPEREDGE), + "both host-facing ports start as edge"); + bpdu_in(&own); + check_state(7, 1, "a frame from port 8 returning on port 1 blocks port 8"); + check(!(stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 0, + "the held peer is non-edge even though it received no BPDU itself"); + for (int i = 0; i < 20; i++) { + follow_root(2); + held &= port_state(7) == 1 && !(stp_pflags[7] & STP_PF_OPEREDGE); + bpdu_in(&own); + } + check(held, "repeated returned BPDUs keep the peer blocked and non-edge"); + follow_root(14); + check_state(7, 1, "the peer waits its full initial forward delay"); + check(!(stp_pflags[7] & STP_PF_OPEREDGE), "auto-edge cannot bypass the loop hold"); + follow_root(14); + check_state(7, 2, "the peer then spends another forward delay learning"); + check(!(stp_pflags[7] & STP_PF_OPEREDGE), "the learning peer is still non-edge"); + follow_root(3); + check_state(7, 3, "the peer forwards once both forward delays have elapsed"); + check((stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 0, + "the silent peer regains edge with its RX counter still zero"); + check(!(stp_pflags[0] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][0] == 21, + "only the port that actually received the returned BPDUs counts and remembers them"); + bpdu_in(&own); + check_state(7, 1, "a new returned BPDU blocks the recovered peer again"); + check(!(stp_pflags[7] & STP_PF_OPEREDGE), "the new loop revokes the recovered edge status"); +} + +static void scen_loop_peer_heard_bpdu(void) +{ + struct sim_bpdu own = loop_from_port8(); + struct sim_bpdu peer = { + .port = 7, .root_prio = 0x40, .root_cost = 200000, + .br_prio = 0x90, .port_id = 1, .flags = 0x0c + }; + + printf("56. a loop peer that received a real BPDU remains non-edge\n"); + loop_ports_ready(); + bpdu_in(&own); + memcpy(peer.root_mac, ROOT_MAC, 6); + memcpy(peer.br_mac, PEER_MAC, 6); + bpdu_in(&peer); + follow_root(31); + check_state(7, 3, "the loop hold still expires after the full forward delays"); + check(!(stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 1, + "a real received BPDU prevents the peer from becoming edge"); +} + +static void scen_loop_peer_autoedge_off(void) +{ + struct sim_bpdu own = loop_from_port8(); + + printf("57. loop recovery respects administratively disabled auto-edge\n"); + reset_all(); + stp_pflags[7] &= ~STP_PF_AUTOEDGE; + links_set((1 << 0) | (1 << 7) | (1 << 8)); + follow_root(31); + bpdu_in(&own); + follow_root(31); + check_state(7, 3, "the peer forwards again after loop recovery"); + check(!(stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 0, + "zero RX does not override the disabled auto-edge setting"); +} + +/* Actual captured RouterOS bytes; only the redacted MAC octets are replaced. */ +static const uint8_t mikrotik_wire[] = { + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x00, 0xd0, 0xea, + 0x11, 0x12, 0x34, 0x56, 0x00, 0x27, 0x42, 0x42, + 0x03, 0x00, 0x00, 0x02, 0x02, 0x3c, 0x10, 0x00, + 0xd0, 0xea, 0x11, 0x12, 0x34, 0x56, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x00, 0xd0, 0xea, 0x11, 0x12, + 0x34, 0x56, 0x80, 0x0a, 0x00, 0x00, 0x14, 0x00, + 0x02, 0x00, 0x0f, 0x00, 0x00 +}; + +static void bpdu_wire(uint8_t port, int tagged) +{ + size_t body = 20; + + memset(uip_buf, 0, sizeof(uip_buf)); + memcpy(uip_buf, mikrotik_wire, 12); + uip_buf[12] = 0x88; + uip_buf[13] = 0x99; + uip_buf[14] = 0x04; + uip_buf[19] = port; + if (tagged) { + uip_buf[20] = 0x81; + uip_buf[23] = 1; + body += 4; + } + memcpy(uip_buf + body, mikrotik_wire + 12, sizeof(mikrotik_wire) - 12); + uip_len = body + sizeof(mikrotik_wire) - 12; +} + +static void scen_mikrotik_wire_capture(void) +{ + int attributed = 1; + + printf("58. the captured RouterOS BPDU is parsed on every physical ingress port\n"); + for (int tagged = 0; tagged < 2; tagged++) { + for (uint8_t port = 0; port < NPORTS; port++) { + reset_all(); + links_set((1 << NPORTS) - 1); + secs(5); + bpdu_wire(port, tagged); + /* The upper source-word bits and trap reason are not port bits. */ + uip_buf[15] = 0x04; + uip_buf[18] = 0xfe; + uip_buf[19] |= 0xa0; + stp_in(); + attributed &= stp_root_port == port && root_bridge.prio == 0x10 + && stp_dpid[port] == 0x800a; + for (uint8_t p = 0; p < NPORTS; p++) + attributed &= stp_cnt[STP_CNT_RX][p] == (p == port) + && !!(stp_pflags[p] & STP_PF_OPEREDGE) == (p != port); + } + } + check(attributed, "both VLAN layouts select the captured root and change RX and edge status only on the ingress port"); +} + +int main(int argc, char **argv) +{ + verbose = argc > 1 && argv[1][0] == '-' ? (argv[1][1] == 'd' ? 2 : 1) : 0; + scen_edge_ports(); + scen_root_port(); + scen_ring(); + scen_ring_clears(); + scen_cheaper_path(); + scen_we_are_better(); + scen_alt_survives_link_bounce(); + scen_speed_cost(); + scen_root_times(); + scen_too_old(); + scen_info_expiry(); + scen_dport_tiebreak(); + scen_rxport_tiebreak(); + scen_inferior_info(); + scen_root_role_bpdu(); + scen_alternate_takes_over(); + scen_dbridge_tiebreak(); + scen_times_revert(); + scen_tc_via_root_port(); + scen_tc_window_rstp(); + scen_tc_propagation(); + scen_tc_legacy_tcn(); + scen_alt_forgets(); + scen_edge_no_tc(); + scen_link_loss_no_tc(); + scen_proposal_agreement(); + scen_no_agreement(); + scen_sync_on_proposal(); + scen_rapid_failover(); + scen_rapid_reroot(); + scen_agreement_other_root(); + scen_repeated_proposal(); + scen_dispute(); + scen_learning_state(); + scen_reply_to_inferior(); + scen_backup_role(); + scen_migrate_to_stp(); + scen_migrate_back(); + scen_legacy_root_tcn(); + scen_alternate_agrees(); + scen_agreement_from_alternate(); + scen_timer_relation(); + scen_recent_root(); + scen_recent_backup(); + scen_short_bpdu(); + scen_port_roles(); + scen_no_tx_without_link(); + scen_counters(); + scen_path_cost_method(); + scen_bpdu_handling(); + scen_last_tc(); + scen_lag(); + scen_tc_needs_forwarding(); + scen_root_port_needs_link(); + scen_loop_peer_edge_recovery(); + scen_loop_peer_heard_bpdu(); + scen_loop_peer_autoedge_off(); + scen_mikrotik_wire_capture(); + if (failures) { + printf("\n%d check(s) failed\n", failures); + return 1; + } + printf("\nall scenarios passed\n"); + return 0; +}