mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-08-30 14:52:51 +08:00
An RST BPDU body is 36 bytes: the Config-BPDU fields plus a trailing version-1 length octet (zero - there is no version-1 information). Ours was 35 - strict 802.1w parsers treat such a BPDU as malformed and drop it. Add the field, keep legacy Config BPDUs at 35 bytes, and set the 802.3 length accordingly (0x27 with LLC for RST, 0x26 for Config).
710 lines
24 KiB
C
710 lines
24 KiB
C
/*
|
|
* This is a driver implementation for the Spanning Tree Protocol features for the RTL837x platform
|
|
* This code is in the Public Domain
|
|
*
|
|
* Configurable per 802.1D-2004/802.1w: bridge priority, hello time, max age,
|
|
* forward delay, force-version (RSTP/STP), tx hold count; per port: enable,
|
|
* admin/auto edge, path cost, port priority, BPDU guard, root guard, BPDU
|
|
* filter. CLI: "stp ..." (see stp_parse), status: /stp.json (send_stp).
|
|
*
|
|
* The engine itself stays deliberately simple (no proposal/agreement
|
|
* handshake, no full port-role machine): we elect a root from received BPDUs,
|
|
* promote ports to forwarding after the listen period (or immediately for
|
|
* edge ports), age the root out via max age, and block ports on which we see
|
|
* our own BPDU (loop!) or - with root guard - a better root.
|
|
*/
|
|
|
|
// #define REGDBG
|
|
// #define DEBUG
|
|
|
|
/* Place this module's code and constants in code bank 2 (cf. rtl837x_igmp.c):
|
|
* the always-mapped common area is nearly full, and the full state machine
|
|
* does not fit there. */
|
|
#pragma codeseg BANK2
|
|
#pragma constseg BANK2
|
|
|
|
#include <stdint.h>
|
|
#include "rtl837x_common.h"
|
|
#include "rtl837x_sfr.h"
|
|
#include "rtl837x_regs.h"
|
|
#include "rtl837x_stp.h"
|
|
#include "rtl837x_port.h" /* port_pvid_get(), port_l2mc_set() */
|
|
#include "uip.h"
|
|
#include "machine.h"
|
|
|
|
extern __code struct machine machine;
|
|
extern __xdata uint8_t sfr_data[4];
|
|
extern __xdata struct machine_runtime machine_detected; /* owned by rtl837x_port.c */
|
|
|
|
/* Scratch for stp_fdb_update(), in xdata: plain locals would overflow the
|
|
* near-full internal-RAM overlay (OSEG), cf. rtl837x_lacp.c. */
|
|
__xdata uint16_t stp_fdb_vid;
|
|
__xdata uint8_t stp_fdb_i;
|
|
|
|
extern __xdata struct uip_eth_addr uip_ethaddr;
|
|
|
|
extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2];
|
|
extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for BPDUs */
|
|
|
|
/* CLI tokenizer state + helpers (owned by cmd_parser.c, HOME bank) */
|
|
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 uint8_t * cmd);
|
|
uint8_t atoi_byte(__xdata uint8_t *out, uint8_t idx);
|
|
|
|
/* ---- Configuration ---- */
|
|
__xdata uint8_t stp_prio; /* bridge priority high byte (0x80 = 32768) */
|
|
__xdata uint8_t stp_hello_s;
|
|
__xdata uint8_t stp_maxage_s;
|
|
__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];
|
|
__xdata uint8_t stp_pp2p[10];
|
|
|
|
__xdata struct bridge stp_dbridge[10];
|
|
__xdata uint16_t stp_dpid[10];
|
|
__xdata uint32_t stp_dcost[10];
|
|
|
|
/* ---- 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 port_timers[10]; /* listen-period countdown (0 = not listening) */
|
|
__xdata uint16_t port_hello[10]; /* hello TX countdown */
|
|
__xdata uint16_t stp_bpdu_age[10]; /* ticks since last BPDU seen on port (saturating) */
|
|
__xdata uint8_t stp_tx_budget[10]; /* tx hold: BPDUs left in the current second */
|
|
__xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */
|
|
|
|
/* Scratch (8051: locals would overflow the internal-RAM overlay area) */
|
|
__xdata uint8_t stp_scratch;
|
|
__xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */
|
|
__xdata uint32_t stp_cost_scratch;
|
|
|
|
/* stp_timers() runs at ~64 Hz (main loop ~256 Hz / (STP_TICK_DIVIDER+1)) */
|
|
#define STP_HZ 64
|
|
#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)
|
|
|
|
struct stp_pkt {
|
|
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; /* RST BPDU only: length of the (empty) v1 part */
|
|
};
|
|
|
|
struct stp_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; /* RST BPDU only: length of the (empty) v1 part */
|
|
};
|
|
|
|
#define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE])
|
|
#define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0])
|
|
|
|
signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) __reentrant
|
|
{
|
|
for (uint8_t i = 0; i < 6; i++) {
|
|
if (m1[i] == m2[i])
|
|
continue;
|
|
if (m1[i] < m2[i])
|
|
return -1;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* Write one port's 2-bit state into the ASIC's MSTP register.
|
|
* 00 disable, 01 blocking, 10 learning, 11 forwarding. */
|
|
static void stp_state_set(uint8_t port, uint8_t state) __reentrant
|
|
{
|
|
reg_read_m(RTL837X_MSTP_STATES);
|
|
stp_scratch = 3 - (port >> 2);
|
|
sfr_data[stp_scratch] &= ~(uint8_t)(0b11 << ((port << 1) & 0x7));
|
|
sfr_data[stp_scratch] |= (uint8_t)(state << ((port << 1) & 0x7));
|
|
reg_write_m(RTL837X_MSTP_STATES);
|
|
}
|
|
|
|
|
|
/* Take the bridge back as root of its own tree (initial state / root aged out) */
|
|
static void stp_claim_root(void)
|
|
{
|
|
root_bridge.prio = stp_prio;
|
|
root_bridge.ext = 0x00;
|
|
memcpy(root_bridge.mac, uip_ethaddr.addr, 6);
|
|
root_bridge_cost = 0;
|
|
stp_root_port = 0xff;
|
|
}
|
|
|
|
|
|
void stp_cnf_send(uint8_t port) __reentrant
|
|
{
|
|
if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED)))
|
|
return;
|
|
if (!stp_tx_budget[port]) /* tx hold count exhausted for this second */
|
|
return;
|
|
stp_tx_budget[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;
|
|
|
|
STP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID);
|
|
STP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION;
|
|
STP_O->rtl_tag.reason = 0x00;
|
|
/* Through HTONS like every tag field: raw 0x0020 lands on the wire as
|
|
* 0x2000 (EFID), the ASIC fails to parse the tag and floods the frame
|
|
* with the 0x8899 header still on it (same bug class as LACP had).
|
|
* NOTE: no RTL_TAG_KEEP here - hardware-verified that KEEP on an
|
|
* LLC/802.3 (length-field) frame makes the ASIC drop it entirely,
|
|
* while the same flag works fine on ethertype frames (LACP). */
|
|
STP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS);
|
|
STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port);
|
|
|
|
STP_O->dsap = 0x42;
|
|
STP_O->ssap = 0x42;
|
|
STP_O->ctrl = 0x03;
|
|
STP_O->proto = 0x0000;
|
|
if (stp_rstp) {
|
|
/* 802.3 length = LLC (3) + RST BPDU body (36, incl. version1_length) */
|
|
STP_O->msg_len = HTONS(0x27);
|
|
STP_O->version = 0x02; /* RSTP */
|
|
STP_O->bpdu_type = 0x02; /* Rapid Spanning Tree BPDU */
|
|
/* flags: role designated (0b11 << 2) + learning + forwarding */
|
|
STP_O->flags = 0x3c;
|
|
} else {
|
|
/* 802.3 length = LLC (3) + Config BPDU body (35) */
|
|
STP_O->msg_len = HTONS(0x26);
|
|
STP_O->version = 0x00; /* legacy STP */
|
|
STP_O->bpdu_type = 0x00; /* Config BPDU */
|
|
STP_O->flags = 0x00;
|
|
}
|
|
|
|
memcpy(STP_O->src_addr, uip_ethaddr.addr, 6);
|
|
memcpy(STP_O->root.mac, root_bridge.mac, 6);
|
|
memcpy(STP_O->bridge.mac, uip_ethaddr.addr, 6);
|
|
|
|
STP_O->root.prio = root_bridge.prio;
|
|
STP_O->root.ext = root_bridge.ext;
|
|
/* Our root path cost, big-endian (0 while we are the root ourselves) */
|
|
STP_O->root_path_cost = ((root_bridge_cost & 0xff) << 24)
|
|
| ((root_bridge_cost & 0xff00) << 8)
|
|
| ((root_bridge_cost >> 8) & 0xff00)
|
|
| (root_bridge_cost >> 24);
|
|
|
|
STP_O->bridge.prio = stp_prio;
|
|
STP_O->bridge.ext = 0x00;
|
|
|
|
STP_O->port_prio = stp_pprio[port];
|
|
STP_O->port_id = port + 1;
|
|
STP_O->age = 0x00; // FIXME: This only works because we do not use HTONS and the values are in 1/256 seconds
|
|
STP_O->age_max = stp_maxage_s;
|
|
STP_O->hello = stp_hello_s;
|
|
STP_O->fwd_delay = stp_fwddelay_s;
|
|
STP_O->version1_length = 0; /* RST BPDU: no version-1 information */
|
|
|
|
/* BPDUs are link-local and must egress untagged: with a management VLAN
|
|
* set, tcpip_output() splices an 802.1Q tag after the SA, shifting the
|
|
* in-frame rtl_tag out of the position the ASIC parses - the CPU tag then
|
|
* leaks onto the wire as 0x8899 and the BPDU is flooded, not sent.
|
|
* Hardware-verified fix, same as lacp_send(). */
|
|
{
|
|
uint16_t saved_mgmt_vlan = management_vlan;
|
|
management_vlan = 0;
|
|
/* A legacy Config BPDU body is 35 bytes - without the trailing
|
|
* version-1 length byte that only the RST BPDU (36 bytes) carries. */
|
|
uip_len = stp_rstp ? sizeof(struct stp_pkt) : sizeof(struct stp_pkt) - 1;
|
|
tcpip_output();
|
|
management_vlan = saved_mgmt_vlan;
|
|
}
|
|
}
|
|
|
|
|
|
void stp_in(void) __banked
|
|
{
|
|
// By default we do not send anything out
|
|
uip_len = 0;
|
|
|
|
/* Ingress port: low nibble of the CPU tag's pmask on RX */
|
|
stp_scratch = ((uint8_t)HTONS(STP_I->rtl_tag.pmask)) & 0x0f;
|
|
if (stp_scratch < machine.min_port || stp_scratch > machine.max_port)
|
|
return;
|
|
{
|
|
__xdata static uint8_t port_l; /* NOT stp_scratch: stp_state_set() clobbers it */
|
|
uint8_t port = (port_l = stp_scratch);
|
|
(void)port_l;
|
|
|
|
// Make sure this is the type of (R)STP packet we are interested in:
|
|
if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03))
|
|
return;
|
|
if (STP_I->proto)
|
|
return;
|
|
/* Accept RSTP BPDUs (v2 type 2) and legacy Config BPDUs (v0 type 0) */
|
|
if (!((STP_I->version == 2 && STP_I->bpdu_type == 2)
|
|
|| (STP_I->version == 0 && STP_I->bpdu_type == 0)))
|
|
return;
|
|
|
|
if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & STP_PF_FILTER))
|
|
return;
|
|
|
|
/* BPDU guard: an edge-facing port must never see a BPDU - shut it down. */
|
|
if (stp_pflags[port] & STP_PF_BPDUGUARD) {
|
|
print_string("STP: BPDU guard tripped, disabling port ");
|
|
print_byte(port); write_char('\n');
|
|
stp_pflags[port] |= STP_PF_TRIPPED;
|
|
stp_state_set(port, 0b00);
|
|
stp_tc_count++;
|
|
return;
|
|
}
|
|
|
|
stp_bpdu_age[port] = 0;
|
|
|
|
/* Our own BPDU coming back at us = a loop in the network. Block the port
|
|
* for a listen period; if the loop persists the BPDUs keep arriving and
|
|
* the port stays blocked. */
|
|
if (cmpMAC(STP_I->bridge.mac, uip_ethaddr.addr) == 0) {
|
|
if (port_timers[port] == 0 && !(stp_pflags[port] & STP_PF_TRIPPED)) {
|
|
print_string("STP: loop detected on port ");
|
|
print_byte(port); write_char('\n');
|
|
stp_state_set(port, 0b01);
|
|
port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ;
|
|
stp_pflags[port] &= ~STP_PF_OPEREDGE;
|
|
stp_tc_count++;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Better root than the one we know? */
|
|
if (STP_I->root.prio < root_bridge.prio
|
|
|| ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 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_byte(port); write_char('\n');
|
|
stp_state_set(port, 0b01);
|
|
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;
|
|
stp_tc_count++;
|
|
}
|
|
|
|
/* Refresh our cost to the root when the update comes in on the root port */
|
|
if (port == stp_root_port) {
|
|
stp_cost_scratch = STP_I->root_path_cost;
|
|
/* big-endian on the wire */
|
|
root_bridge_cost = ((stp_cost_scratch & 0xff) << 24)
|
|
| ((stp_cost_scratch & 0xff00) << 8)
|
|
| ((stp_cost_scratch >> 8) & 0xff00)
|
|
| (stp_cost_scratch >> 24);
|
|
root_bridge_cost += PCOST(port);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void stp_timers(void) __banked
|
|
{
|
|
/* Refill the per-port tx budgets once per second (tx hold count) */
|
|
if (++stp_sec_tick >= STP_HZ) {
|
|
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++) {
|
|
if (!(stp_pflags[stp_i] & STP_PF_ENABLED))
|
|
continue;
|
|
|
|
if (stp_bpdu_age[stp_i] < 0xffff)
|
|
stp_bpdu_age[stp_i]++;
|
|
|
|
/* Periodic hello */
|
|
if (port_hello[stp_i])
|
|
port_hello[stp_i]--;
|
|
if (!port_hello[stp_i]) {
|
|
port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ;
|
|
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]) {
|
|
if (!--port_timers[stp_i]) {
|
|
stp_state_set(stp_i, 0b11);
|
|
print_string("STP: port forwarding ");
|
|
print_byte(stp_i); write_char('\n');
|
|
stp_tc_count++;
|
|
} else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE)
|
|
&& 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, 0b11);
|
|
print_string("STP: edge port forwarding ");
|
|
print_byte(stp_i); write_char('\n');
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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++;
|
|
}
|
|
}
|
|
|
|
|
|
/* Reset all configuration to the 802.1D/802.1w defaults. Called once at boot
|
|
* (before the startup config replays "stp ..." commands over it). */
|
|
void stp_defaults(void) __banked
|
|
{
|
|
stp_prio = 0x80; /* 32768 */
|
|
stp_hello_s = 2;
|
|
stp_maxage_s = 20;
|
|
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 */
|
|
stp_pflags[stp_i] = STP_PF_ENABLED | STP_PF_AUTOEDGE;
|
|
stp_pcost[stp_i] = 0; /* auto */
|
|
stp_pprio[stp_i] = 0x80;
|
|
stp_bpdu_age[stp_i] = 0;
|
|
port_timers[stp_i] = 0;
|
|
port_hello[stp_i] = 0;
|
|
stp_tx_budget[stp_i] = 6;
|
|
}
|
|
stp_tc_count = 0;
|
|
stp_claim_root();
|
|
}
|
|
|
|
|
|
/*
|
|
* Steer BPDUs (01:80:C2:00:00:00) while STP runs: one static CPU-only L2
|
|
* multicast entry per PVID in use, so BPDUs reach the CPU without being
|
|
* flooded to other ports (a bridge running STP must consume BPDUs, not
|
|
* relay them - relaying poisons the neighbours' view of the topology).
|
|
*
|
|
* With STP off the same entries are retargeted to all ports + CPU, which
|
|
* restores the previous flood behaviour ("BPDU transparency"): the
|
|
* surrounding spanning tree can keep spanning *through* this switch, which
|
|
* unmanaged setups rely on. Same per-PVID/IVL rules as the LACP steering -
|
|
* see port_l2mc_set() and rtl837x_lacp.c. NOTE: changing a port's PVID
|
|
* while STP runs needs `stp off`/`on` to refresh the entries.
|
|
*/
|
|
static void stp_fdb_update(__xdata uint16_t pmask)
|
|
{
|
|
/* Unlike LACPDUs (always untagged, so per-PVID entries suffice), BPDUs
|
|
* can arrive VLAN-tagged and then classify into the tag's VID - cover
|
|
* every VLAN that exists in the VLAN table, plus every port's PVID for
|
|
* the untagged case. A duplicate VID just overwrites the same slot. */
|
|
for (stp_fdb_vid = 1; stp_fdb_vid < 4095; stp_fdb_vid++) {
|
|
if (vlan_get(stp_fdb_vid) < 0)
|
|
continue;
|
|
if (!(sfr_data[0] & 0x02)) /* bit 1: VLAN table entry valid */
|
|
continue;
|
|
port_l2mc_set(0x00, stp_fdb_vid, pmask);
|
|
}
|
|
for (stp_fdb_i = machine.min_port; stp_fdb_i <= machine.max_port; stp_fdb_i++) {
|
|
stp_fdb_vid = port_pvid_get(stp_fdb_i);
|
|
port_l2mc_set(0x00, stp_fdb_vid, pmask);
|
|
}
|
|
}
|
|
|
|
|
|
void stp_setup(void) __banked
|
|
{
|
|
print_string("Enabling STP: ");
|
|
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++) {
|
|
stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED);
|
|
stp_bpdu_age[stp_i] = 0;
|
|
stp_tx_budget[stp_i] = stp_txhold;
|
|
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;
|
|
sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7));
|
|
port_timers[stp_i] = 0;
|
|
} else {
|
|
/* listen first: blocking until the forward-delay expires */
|
|
sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b01 << ((stp_i << 1) & 0x7));
|
|
port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ;
|
|
}
|
|
port_hello[stp_i] = (uint16_t)stp_hello_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);
|
|
|
|
print_reg(RTL837X_MSTP_STATES); write_char('\n');
|
|
|
|
stp_claim_root();
|
|
|
|
/* Take BPDUs to the CPU only - we are a participating bridge now. */
|
|
stp_fdb_update(PMASK_CPU);
|
|
}
|
|
|
|
|
|
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)(0b11 << ((stp_i << 1) & 0x7));
|
|
stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED);
|
|
port_timers[stp_i] = 0;
|
|
}
|
|
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));
|
|
}
|
|
|
|
|
|
/* ---- "stp ..." CLI ----
|
|
* stp on|off
|
|
* stp prio <0-15> (bridge priority = n * 4096)
|
|
* stp hello <1-10> | stp maxage <6-40> | stp fwd <4-30> | stp txhold <1-10>
|
|
* stp version rstp|stp
|
|
* stp port <1-9> on|off
|
|
* stp port <1-9> edge on|off|auto
|
|
* stp port <1-9> cost <0-255> (x1000; 0 = auto/20000)
|
|
* stp port <1-9> prio <0-240>
|
|
* stp port <1-9> guard none|bpdu|root
|
|
* stp port <1-9> filter on|off
|
|
*/
|
|
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;
|
|
}
|
|
if (cmd_compare(1, "off")) {
|
|
print_string("STP disabled\n");
|
|
stp_off();
|
|
stpEnabled = 0;
|
|
return;
|
|
}
|
|
if (cmd_words_len < 3)
|
|
goto err;
|
|
|
|
if (cmd_compare(1, "port")) {
|
|
if (cmd_words_len < 4)
|
|
goto err;
|
|
if (atoi_byte(&stp_scratch, cmd_words_b[2]) || stp_scratch < 1 || stp_scratch > 9)
|
|
goto err;
|
|
{
|
|
uint8_t port = machine.phys_to_log_port[stp_scratch - 1];
|
|
/* every sub-command except on/off carries one more argument; without
|
|
* this check cmd_compare(4,..) would read a stale word from the
|
|
* PREVIOUS command line (cmd_words_b is not cleared between commands) */
|
|
if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off"))
|
|
goto err;
|
|
if (cmd_compare(3, "on")) {
|
|
stp_pflags[port] |= STP_PF_ENABLED;
|
|
stp_pflags[port] &= ~STP_PF_TRIPPED;
|
|
if (stpEnabled) { /* (re)join: listen first */
|
|
stp_state_set(port, 0b01);
|
|
port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ;
|
|
}
|
|
} else if (cmd_compare(3, "off")) {
|
|
stp_pflags[port] &= ~STP_PF_ENABLED;
|
|
if (stpEnabled)
|
|
stp_state_set(port, 0b11); /* plain forwarding */
|
|
} else if (cmd_compare(3, "edge")) {
|
|
stp_pflags[port] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE);
|
|
if (cmd_compare(4, "on"))
|
|
stp_pflags[port] |= STP_PF_ADMEDGE;
|
|
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_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[port] = stp_cost_scratch;
|
|
} 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(&stp_scratch, cmd_words_b[4]))
|
|
goto err;
|
|
stp_pprio[port] = stp_scratch & 0xf0;
|
|
} 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(&stp_scratch, 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;
|
|
}
|
|
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;
|
|
} 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;
|
|
}
|
|
return;
|
|
err:
|
|
print_string("Error: stp on|off | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | port <1-9> on|off|edge|cost|prio|guard|filter ...\n");
|
|
}
|