stp: management failsafe (commit-confirm) + bounded NIC waits

Enabling STP on a bridge whose management rides an in-band VLAN can cut
off that very management - and not only by our own blocking: on this
network the upstream TP-Link Easy Smart switch's "loop prevention"
reacted to our BPDU hellos by blocking ITS port towards us while our
ASIC was all-forwarding, isolating the whole segment until a power
cycle. Recoverable only by going quiet.

Add a commit-confirm watchdog: while STP is enabled, any HTTP request
re-arms a countdown ("stp failsafe <seconds>", default 180, 0 disables);
if management stays silent for the whole window, STP disables itself,
which also stops BPDU TX so a neighbour's loop protection can release
its block. The web UI polls /stp.json every 2 s, so an open browser
naturally keeps the watchdog re-armed. The trip is reported via
/stp.json (fs, fsT) and as a warning on the Spanning Tree page.

Deliberately not conditioned on our own MSTP port states - the incident
above proves the uplink can be dead while every local port forwards.

Also bound the NIC DMA busy-waits (nic_tx_packet, nic_rx_header,
nic_rx_packet): an unbounded spin on SFR_NIC_CTRL freezes the entire
main loop (timers, HTTP, ARP) if the ASIC ever fails to consume a
transfer; give up after ~65k polls and drop the frame instead.

Hardware-verified end to end: with priority 15 against a live RSTP
bridge the uplink died 6 s after "stp on" and the network recovered BY
ITSELF 66 s later (trip at 45 s + neighbour release), fsT=1, LACP and
LAN intact. Telemetry via syslog-to-edge-port host confirmed the full
chain: countdown 44->4, trip, hello TX stopping at the trip.

(cherry picked from commit 1fa9775156fd6d7ebfdda2382f73430b86601230)
This commit is contained in:
d00f
2026-08-04 03:26:10 +02:00
parent fbd19b2b4d
commit 6fcb8ef11f
8 changed files with 80 additions and 7 deletions
+2 -1
View File
@@ -23,6 +23,7 @@ const conf_cmds = [
/^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/,
/^stp\s+(on|off)$/,
/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/,
/^stp\s+failsafe\s+\d{1,3}$/,
/^stp\s+version\s+(rstp|stp)$/,
/^stp\s+port\s+\d{1,2}\s+(on|off)$/,
/^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/,
@@ -53,7 +54,7 @@ const conf_overwrite = [
/^lag\s+\d+\b/,
/^laghash\b/,
/^isolate\s+\d{1,2}\b/,
/^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/,
/^stp\s+(prio|hello|maxage|fwd|txhold|version|failsafe)\b/,
/^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/,
/^igmp\b/,
/^mtu\s+\d{1,2}\b/,
+2 -1
View File
@@ -16,7 +16,7 @@
<h2>Bridge settings</h2>
<table id="stpBridge">
<tr>
<th>Priority</th><th>Version</th><th>Hello [s]</th><th>Max age [s]</th><th>Fwd delay [s]</th><th>Tx hold</th>
<th>Priority</th><th>Version</th><th>Hello [s]</th><th>Max age [s]</th><th>Fwd delay [s]</th><th>Tx hold</th><th>Mgmt failsafe [s]</th>
</tr>
<tr>
<td><select id="bPrio"></select></td>
@@ -25,6 +25,7 @@
<td><input id="bMaxage" type="number" min="6" max="40" style="width:4em"></td>
<td><input id="bFwd" type="number" min="4" max="30" style="width:4em"></td>
<td><input id="bTxhold" type="number" min="1" max="10" style="width:4em"></td>
<td><input id="bFailsafe" type="number" min="0" max="255" style="width:4em" title="Auto-disable STP if ports stay blocked while management is silent; 0 = off"></td>
</tr>
</table>
<p style="font-size:small">Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.</p>
+6 -1
View File
@@ -83,7 +83,9 @@ function fetchStp() {
// (remote-controlled), never render it as HTML
if (!stpRows)
buildPortsTable(s.ports);
document.getElementById("stpStat").textContent = s.on
document.getElementById("stpStat").textContent = s.fsT
? "\u26a0 STP was disabled by the management failsafe (ports were blocked while management was unreachable). Review the topology before re-enabling."
: s.on
? (s.weRoot
? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16)
: "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac
@@ -107,6 +109,7 @@ function fetchStp() {
document.getElementById("bMaxage").value = s.maxage;
document.getElementById("bFwd").value = s.fwd;
document.getElementById("bTxhold").value = s.txhold;
document.getElementById("bFailsafe").value = s.fs;
for (const p of s.ports) {
document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off";
document.getElementById("edge_" + p.p).value =
@@ -147,6 +150,8 @@ window.addEventListener("load", function() {
.addEventListener("change", e => stpCmd("stp fwd " + e.target.value));
document.getElementById("bTxhold")
.addEventListener("change", e => stpCmd("stp txhold " + e.target.value));
document.getElementById("bFailsafe")
.addEventListener("change", e => stpCmd("stp failsafe " + e.target.value));
document.getElementById("stpMode")
.addEventListener("change", () => { stpDirty = true; });