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
+38
View File
@@ -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;
}