mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-08-30 14:52:51 +08:00
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:
+2
-1
@@ -23,6 +23,7 @@ const conf_cmds = [
|
|||||||
/^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/,
|
/^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/,
|
||||||
/^stp\s+(on|off)$/,
|
/^stp\s+(on|off)$/,
|
||||||
/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/,
|
/^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+version\s+(rstp|stp)$/,
|
||||||
/^stp\s+port\s+\d{1,2}\s+(on|off)$/,
|
/^stp\s+port\s+\d{1,2}\s+(on|off)$/,
|
||||||
/^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/,
|
/^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/,
|
||||||
@@ -53,7 +54,7 @@ const conf_overwrite = [
|
|||||||
/^lag\s+\d+\b/,
|
/^lag\s+\d+\b/,
|
||||||
/^laghash\b/,
|
/^laghash\b/,
|
||||||
/^isolate\s+\d{1,2}\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/,
|
/^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/,
|
||||||
/^igmp\b/,
|
/^igmp\b/,
|
||||||
/^mtu\s+\d{1,2}\b/,
|
/^mtu\s+\d{1,2}\b/,
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,7 @@
|
|||||||
<h2>Bridge settings</h2>
|
<h2>Bridge settings</h2>
|
||||||
<table id="stpBridge">
|
<table id="stpBridge">
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><select id="bPrio"></select></td>
|
<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="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="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="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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<p style="font-size:small">Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.</p>
|
<p style="font-size:small">Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.</p>
|
||||||
|
|||||||
+6
-1
@@ -83,7 +83,9 @@ function fetchStp() {
|
|||||||
// (remote-controlled), never render it as HTML
|
// (remote-controlled), never render it as HTML
|
||||||
if (!stpRows)
|
if (!stpRows)
|
||||||
buildPortsTable(s.ports);
|
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
|
? (s.weRoot
|
||||||
? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16)
|
? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16)
|
||||||
: "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac
|
: "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac
|
||||||
@@ -107,6 +109,7 @@ function fetchStp() {
|
|||||||
document.getElementById("bMaxage").value = s.maxage;
|
document.getElementById("bMaxage").value = s.maxage;
|
||||||
document.getElementById("bFwd").value = s.fwd;
|
document.getElementById("bFwd").value = s.fwd;
|
||||||
document.getElementById("bTxhold").value = s.txhold;
|
document.getElementById("bTxhold").value = s.txhold;
|
||||||
|
document.getElementById("bFailsafe").value = s.fs;
|
||||||
for (const p of s.ports) {
|
for (const p of s.ports) {
|
||||||
document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off";
|
document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off";
|
||||||
document.getElementById("edge_" + p.p).value =
|
document.getElementById("edge_" + p.p).value =
|
||||||
@@ -147,6 +150,8 @@ window.addEventListener("load", function() {
|
|||||||
.addEventListener("change", e => stpCmd("stp fwd " + e.target.value));
|
.addEventListener("change", e => stpCmd("stp fwd " + e.target.value));
|
||||||
document.getElementById("bTxhold")
|
document.getElementById("bTxhold")
|
||||||
.addEventListener("change", e => stpCmd("stp txhold " + e.target.value));
|
.addEventListener("change", e => stpCmd("stp txhold " + e.target.value));
|
||||||
|
document.getElementById("bFailsafe")
|
||||||
|
.addEventListener("change", e => stpCmd("stp failsafe " + e.target.value));
|
||||||
document.getElementById("stpMode")
|
document.getElementById("stpMode")
|
||||||
.addEventListener("change", () => { stpDirty = true; });
|
.addEventListener("change", () => { stpDirty = true; });
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
#pragma constseg BANK1
|
#pragma constseg BANK1
|
||||||
|
|
||||||
extern volatile __xdata uint8_t sfr_data[4];
|
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 uint8_t * __code hex;
|
||||||
extern __code struct f_data f_data[];
|
extern __code struct f_data f_data[];
|
||||||
extern __code char * __code mime_strings[];
|
extern __code char * __code mime_strings[];
|
||||||
@@ -547,6 +550,8 @@ void httpd_appcall(void)
|
|||||||
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
|
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
|
||||||
|
|
||||||
dbg_char('P');
|
dbg_char('P');
|
||||||
|
if (uip_newdata())
|
||||||
|
mgmt_alive = 1; /* any HTTP activity proves management still works (STP failsafe) */
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
if (uip_newdata())
|
if (uip_newdata())
|
||||||
write_char('N');
|
write_char('N');
|
||||||
|
|||||||
@@ -556,6 +556,10 @@ void send_stp(void)
|
|||||||
itoa_html(stp_fwddelay_s);
|
itoa_html(stp_fwddelay_s);
|
||||||
slen += strtox(outbuf + slen, ",\"txhold\":");
|
slen += strtox(outbuf + slen, ",\"txhold\":");
|
||||||
itoa_html(stp_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\":\"");
|
slen += strtox(outbuf + slen, ",\"rootPrio\":\"");
|
||||||
byte_to_html(root_bridge.prio);
|
byte_to_html(root_bridge.prio);
|
||||||
byte_to_html(root_bridge.ext);
|
byte_to_html(root_bridge.ext);
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ __xdata uint8_t stp_fwddelay_s;
|
|||||||
__xdata uint8_t stp_rstp;
|
__xdata uint8_t stp_rstp;
|
||||||
__xdata uint8_t stp_txhold;
|
__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 uint8_t stp_pflags[10];
|
||||||
__xdata uint32_t stp_pcost[10];
|
__xdata uint32_t stp_pcost[10];
|
||||||
__xdata uint8_t stp_pprio[10];
|
__xdata uint8_t stp_pprio[10];
|
||||||
@@ -346,6 +356,26 @@ void stp_timers(void) __banked
|
|||||||
stp_sec_tick = 0;
|
stp_sec_tick = 0;
|
||||||
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++)
|
for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++)
|
||||||
stp_tx_budget[stp_i] = stp_txhold;
|
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++) {
|
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_fwddelay_s = 15;
|
||||||
stp_rstp = 1;
|
stp_rstp = 1;
|
||||||
stp_txhold = 6;
|
stp_txhold = 6;
|
||||||
|
stp_failsafe_s = 180;
|
||||||
|
stp_failsafe_tripped = 0;
|
||||||
for (stp_i = 0; stp_i < 10; stp_i++) {
|
for (stp_i = 0; stp_i < 10; stp_i++) {
|
||||||
/* enabled, auto-edge on: host-facing ports go forwarding after
|
/* enabled, auto-edge on: host-facing ports go forwarding after
|
||||||
* 3 s of BPDU silence instead of the full forward delay */
|
* 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")) {
|
if (cmd_compare(1, "on")) {
|
||||||
print_string("STP enabled\n");
|
print_string("STP enabled\n");
|
||||||
|
stp_failsafe_tripped = 0;
|
||||||
|
stp_failsafe_cnt = stp_failsafe_s;
|
||||||
stpEnabled = 1;
|
stpEnabled = 1;
|
||||||
stp_setup();
|
stp_setup();
|
||||||
return;
|
return;
|
||||||
@@ -629,6 +663,10 @@ void stp_parse(void) __banked __reentrant
|
|||||||
if (stp_scratch < 1 || stp_scratch > 10)
|
if (stp_scratch < 1 || stp_scratch > 10)
|
||||||
goto err;
|
goto err;
|
||||||
stp_txhold = stp_scratch;
|
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 {
|
} else {
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -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_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_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_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[]) */
|
/* Per-port config/status flags (stp_pflags[]) */
|
||||||
#define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */
|
#define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */
|
||||||
|
|||||||
+20
-3
@@ -621,7 +621,11 @@ void nic_rx_header(uint16_t ring_ptr)
|
|||||||
SFR_NIC_DATA_U16LE = buffer;
|
SFR_NIC_DATA_U16LE = buffer;
|
||||||
SFR_NIC_RING_U16LE = ring_ptr;
|
SFR_NIC_RING_U16LE = ring_ptr;
|
||||||
SFR_NIC_CTRL = 1;
|
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);
|
print_short(len);
|
||||||
#endif
|
#endif
|
||||||
SFR_NIC_CTRL = len;
|
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 += 0xf;
|
||||||
len >>= 3;
|
len >>= 3;
|
||||||
SFR_NIC_CTRL = len;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user