diff --git a/html/config.js b/html/config.js index e7fb055..9b37f81 100644 --- a/html/config.js +++ b/html/config.js @@ -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/, diff --git a/html/stp.html b/html/stp.html index 1e5a5c2..15815be 100644 --- a/html/stp.html +++ b/html/stp.html @@ -16,7 +16,7 @@

Bridge settings

- + @@ -25,6 +25,7 @@ +
PriorityVersionHello [s]Max age [s]Fwd delay [s]Tx holdPriorityVersionHello [s]Max age [s]Fwd delay [s]Tx holdMgmt failsafe [s]

Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.

diff --git a/html/stp.js b/html/stp.js index c85712a..c6cd124 100644 --- a/html/stp.js +++ b/html/stp.js @@ -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; }); diff --git a/httpd/httpd.c b/httpd/httpd.c index d2b523d..565d6d9 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -20,6 +20,9 @@ #pragma constseg BANK1 extern volatile __xdata uint8_t sfr_data[4]; +extern volatile __xdata uint32_t ticks; +/* 200 Hz free-running tick, owned by rtlplayground.c */ +volatile __xdata uint8_t mgmt_alive; /* consumed by the STP management failsafe */ extern __code uint8_t * __code hex; extern __code struct f_data f_data[]; extern __code char * __code mime_strings[]; @@ -547,6 +550,8 @@ void httpd_appcall(void) __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); dbg_char('P'); + if (uip_newdata()) + mgmt_alive = 1; /* any HTTP activity proves management still works (STP failsafe) */ #ifdef DEBUG if (uip_newdata()) write_char('N'); diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 7b7db6b..c5fad8f 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -556,6 +556,10 @@ void send_stp(void) itoa_html(stp_fwddelay_s); slen += strtox(outbuf + slen, ",\"txhold\":"); itoa_html(stp_txhold); + slen += strtox(outbuf + slen, ",\"fs\":"); + itoa_html(stp_failsafe_s); + slen += strtox(outbuf + slen, ",\"fsT\":"); + itoa_html(stp_failsafe_tripped); slen += strtox(outbuf + slen, ",\"rootPrio\":\""); byte_to_html(root_bridge.prio); byte_to_html(root_bridge.ext); diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 6604ffd..a07e435 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -61,6 +61,16 @@ __xdata uint8_t stp_fwddelay_s; __xdata uint8_t stp_rstp; __xdata uint8_t stp_txhold; +/* Management failsafe: if any port is held out of Forwarding while no HTTP + * request has been seen for stp_failsafe_s seconds, assume STP just cut off + * in-band management (mgmt VLAN rides a blockable front port!) and disable + * itself, restoring forwarding. Commit-confirm pattern; hardware lockout of + * 2026-07-20 is the motivating incident. 0 disables the watchdog. */ +__xdata uint8_t stp_failsafe_s; +__xdata uint8_t stp_failsafe_cnt; /* seconds left before the trip */ +__xdata uint8_t stp_failsafe_tripped; +extern volatile __xdata uint8_t mgmt_alive; /* set by httpd on any request */ + __xdata uint8_t stp_pflags[10]; __xdata uint32_t stp_pcost[10]; __xdata uint8_t stp_pprio[10]; @@ -346,6 +356,26 @@ void stp_timers(void) __banked stp_sec_tick = 0; for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) stp_tx_budget[stp_i] = stp_txhold; + + /* Management failsafe: plain commit-confirm. While STP is on, ANY + * HTTP request re-arms the countdown (the web UI polls /stp.json + * every 2 s, so an open browser keeps it alive); stp_failsafe_s + * seconds of management silence disable STP and restore the + * pre-STP state. Deliberately NOT conditioned on our own MSTP + * states: hardware incident 2026-07-21 showed a NEIGHBOR (TP-Link + * Easy Smart loop prevention) cutting our uplink in reaction to + * our BPDUs while our ASIC was all-forwarding - only going fully + * quiet (no BPDU TX) lets such a neighbor recover. */ + if (mgmt_alive) { + mgmt_alive = 0; + stp_failsafe_cnt = stp_failsafe_s; + } else if (stp_failsafe_s && stp_failsafe_cnt && --stp_failsafe_cnt == 0) { + print_string("STP failsafe: no management activity - disabling STP\n"); + stp_off(); + stpEnabled = 0; + stp_failsafe_tripped = 1; + return; + } } for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { @@ -405,6 +435,8 @@ void stp_defaults(void) __banked stp_fwddelay_s = 15; stp_rstp = 1; stp_txhold = 6; + stp_failsafe_s = 180; + stp_failsafe_tripped = 0; for (stp_i = 0; stp_i < 10; stp_i++) { /* enabled, auto-edge on: host-facing ports go forwarding after * 3 s of BPDU silence instead of the full forward delay */ @@ -521,6 +553,8 @@ void stp_parse(void) __banked __reentrant { if (cmd_compare(1, "on")) { print_string("STP enabled\n"); + stp_failsafe_tripped = 0; + stp_failsafe_cnt = stp_failsafe_s; stpEnabled = 1; stp_setup(); return; @@ -629,6 +663,10 @@ void stp_parse(void) __banked __reentrant if (stp_scratch < 1 || stp_scratch > 10) goto err; stp_txhold = stp_scratch; + } else if (cmd_compare(1, "failsafe")) { + /* 0 disables the management watchdog; otherwise seconds to trip */ + stp_failsafe_s = stp_scratch; + stp_failsafe_cnt = stp_scratch; } else { goto err; } diff --git a/rtl837x_stp.h b/rtl837x_stp.h index cf9e9e9..bad0d03 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -23,7 +23,9 @@ extern __xdata uint8_t stp_hello_s; /* hello time, 1-10 s (default 2) */ extern __xdata uint8_t stp_maxage_s; /* max age, 6-40 s (default 20) */ extern __xdata uint8_t stp_fwddelay_s; /* forward delay, 4-30 s (default 15); our listen period */ extern __xdata uint8_t stp_rstp; /* 1 = RSTP BPDUs (v2), 0 = STP-compatible Config BPDUs (v0) */ -extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) */ +extern __xdata uint8_t stp_txhold; +extern __xdata uint8_t stp_failsafe_s; /* mgmt watchdog, seconds (0 = off) */ +extern __xdata uint8_t stp_failsafe_tripped; /* max BPDUs per port per second (default 6) */ /* Per-port config/status flags (stp_pflags[]) */ #define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ diff --git a/rtlplayground.c b/rtlplayground.c index aa12761..08057ef 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -621,7 +621,11 @@ void nic_rx_header(uint16_t ring_ptr) SFR_NIC_DATA_U16LE = buffer; SFR_NIC_RING_U16LE = ring_ptr; SFR_NIC_CTRL = 1; - do { } while (SFR_NIC_CTRL != 0); + /* Bounded, cf. nic_tx_packet: a stuck NIC DMA must not freeze the loop */ + { + uint16_t rx_guard = 0; + do { } while (SFR_NIC_CTRL != 0 && ++rx_guard != 0); + } } @@ -644,7 +648,11 @@ void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr) print_short(len); #endif SFR_NIC_CTRL = len; - do { } while (SFR_NIC_CTRL != 0); + /* Bounded, cf. nic_tx_packet: a stuck NIC DMA must not freeze the loop */ + { + uint16_t rx_guard = 0; + do { } while (SFR_NIC_CTRL != 0 && ++rx_guard != 0); + } } @@ -688,7 +696,16 @@ void nic_tx_packet(uint16_t ring_ptr) len += 0xf; len >>= 3; SFR_NIC_CTRL = len; - do { } while (SFR_NIC_CTRL != 0); + /* Bounded wait: normally the NIC consumes the frame in microseconds, but + * when the egress port is held in an MSTP non-forwarding state the ASIC + * has been observed to never complete the TX - an unbounded spin here + * then freezes the entire main loop (no STP/LACP timers, no HTTP, no + * ARP) until a power cycle. Give up after ~65k polls and drop the frame: + * losing one packet is recoverable, a frozen switch is not. */ + { + uint16_t tx_guard = 0; + do { } while (SFR_NIC_CTRL != 0 && ++tx_guard != 0); + } }