From 531bbbea5dad7f8fb486ba58b78f027b06f828e7 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 21 Jul 2026 09:10:47 +0200 Subject: [PATCH] stp: full RSTP configuration (bridge + per-port), CLI + GUI + persistence Implements the standard 802.1D-2004/802.1w configuration surface: Bridge: priority (0-15 x4096), hello time, max age, forward delay, force-version (RSTP v2 / STP-compatible v0 Config BPDUs), tx hold count (per-port per-second BPDU budget). Per port: enable, admin edge (forwarding immediately - no listen gap), auto edge (forwarding after 3 s of BPDU silence; DEFAULT, so host-facing ports no longer take the full forward delay), path cost (0=auto/20000), port priority, BPDU guard (port disabled on BPDU receipt), root guard (never accept a better root on the port), BPDU filter (no BPDUs in or out). Engine additions: root max-age expiry (reclaim the tree when the root goes silent), root path cost accounting (rx cost + root-port cost, advertised in our BPDUs), loop detection (our own BPDU coming back blocks the port for a listen period), topology-change counter, approximated per-port roles (Root/Designated/Alternate) for diagnostics. CLI: "stp prio|hello|maxage|fwd|txhold|version ..." and "stp port on|off|edge|cost|prio|guard|filter ..." (stp_parse, delegated from cmd_parser); all forms accepted by the startup-config validator so the whole configuration persists. /stp.json now reports config + status; the Spanning Tree page exposes everything with immediate-apply controls and live state/role columns (edit-in-flight guard against the 2 s refresh). 8051 memory: the module moves to code BANK2; internal-RAM pressure from cross-bank calls resolved by xdata loop iterators/scratch, __reentrant on the small helpers, and moving httpd's header-pointer globals to xdata. Verified on hardware (SWTGW218AS): defaults land per standard; priority and hello change live; admin-edge ports (the LACP bond uplinks) keep the LAN at 0% loss THROUGH "stp on"; auto-edge ports forward after 3 s; a port that heard real BPDUs (a VM bridge behind physical port 6) correctly declined auto-edge, sat out the full listen period and became Designated; tc counts promotions; we win the root election at priority 16384 vs 32768. (cherry picked from commit 09a34dc6acdc81ab9cab0727d2f4a59c68131a3e) --- cmd_parser.c | 10 +- html/config.js | 11 +- html/stp.html | 22 +- html/stp.js | 143 ++++++++++--- httpd/httpd.c | 6 +- httpd/page_impl.c | 68 +++++-- rtl837x_stp.c | 502 +++++++++++++++++++++++++++++++++++++--------- rtl837x_stp.h | 36 +++- rtlplayground.c | 1 + 9 files changed, 641 insertions(+), 158 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 21ff7a8..6178f9a 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1525,15 +1525,7 @@ void cmd_parser(void) __banked else igmp_setup(); // Reverts to default with IP-MC being flooded } else if (cmd_compare(0, "stp")) { - if (cmd_compare(1, "on")) { - print_string("STP enabled\n"); - stpEnabled = 1; - stp_setup(); - } else { - print_string("STP disabled\n"); - stp_off(); - stpEnabled = 0; - } + stp_parse(); } else if (cmd_compare(0, "pvid") && cmd_words_len == 3) { __xdata uint16_t pvid; uint8_t port; diff --git a/html/config.js b/html/config.js index 6efa283..e7fb055 100644 --- a/html/config.js +++ b/html/config.js @@ -22,6 +22,14 @@ const conf_cmds = [ /^laghash\s+\d(\s+\w+)+$/, /^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+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)$/, + /^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,3}$/, + /^stp\s+port\s+\d{1,2}\s+prio\s+\d{1,3}$/, + /^stp\s+port\s+\d{1,2}\s+guard\s+(none|bpdu|root)$/, + /^stp\s+port\s+\d{1,2}\s+filter\s+(on|off)$/, /^igmp\s+(on|off)$/, /^mtu\s+\d{1,2}\s+\d+$/, /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, @@ -45,7 +53,8 @@ const conf_overwrite = [ /^lag\s+\d+\b/, /^laghash\b/, /^isolate\s+\d{1,2}\b/, - /^stp\b/, + /^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/, + /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/, /^igmp\b/, /^mtu\s+\d{1,2}\b/, /^bw\s+(in|out)\s+\d{1,2}\b/, diff --git a/html/stp.html b/html/stp.html index af968f5..1e5a5c2 100644 --- a/html/stp.html +++ b/html/stp.html @@ -13,7 +13,27 @@

Mode:

-
+

Bridge settings

+ + + + + + + + + + + + +
PriorityVersionHello [s]Max age [s]Fwd delay [s]Tx hold
+

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

+

Ports

+ + + + +
PortStateRoleSTPEdgeCost [k]PriorityGuardFilter
diff --git a/html/stp.js b/html/stp.js index 3682207..c85712a 100644 --- a/html/stp.js +++ b/html/stp.js @@ -1,12 +1,78 @@ -/* ---- Spanning Tree (RSTP) section ---- */ +/* Spanning Tree page: full RSTP configuration + live status. + * + * Every control applies IMMEDIATELY on change (POST /cmd "stp ...") - there is + * no per-row Apply. The refresh (2 s) repopulates controls from /stp.json; + * a global dirty flag suppresses that between a change and its confirmation + * so the refresh never reverts an edit in flight (same lesson as the LAG page). + */ // STP port states as encoded in the ASIC's MSTP register (2 bits per port) const STP_STATES = ["Disabled", "Blocking", "Learning", "Forwarding"]; +const STP_ROLES = ["-", "Root", "Designated", "Alternate"]; + +// stp_pflags bits (keep in sync with rtl837x_stp.h) +const PF_ENABLED = 1, PF_ADMEDGE = 2, PF_AUTOEDGE = 4, PF_BPDUGUARD = 8, + PF_ROOTGUARD = 16, PF_FILTER = 32, PF_OPEREDGE = 64, PF_TRIPPED = 128; -// "user is editing" flag: while set, the periodic refresh must not overwrite -// the mode dropdown (same pattern as the LAG page - without it the 2 s refresh -// silently reverts the user's choice before Apply). var stpDirty = false; +var stpRows = 0; // ports table built? + +async function stpCmd(cmd) { + stpDirty = true; + try { + await fetch('/cmd', { method: 'POST', body: cmd }); + } catch(err) { + console.error(`Error: ${err}`); + } + stpDirty = false; + fetchStp(); +} + +function sel(id, opts, onch) { + const s = document.createElement("select"); + s.id = id; + for (const [v, label] of opts) { + const o = document.createElement("option"); + o.value = v; o.textContent = label; + s.appendChild(o); + } + s.addEventListener("change", onch); + return s; +} + +function num(id, min, max, onch) { + const n = document.createElement("input"); + n.type = "number"; n.id = id; n.min = min; n.max = max; n.style.width = "4em"; + n.addEventListener("change", onch); + return n; +} + +function buildPortsTable(ports) { + const tbl = document.getElementById("stpPortsTbl"); + for (const p of ports) { + const tr = tbl.insertRow(); + tr.insertCell().textContent = p.p; // Port + tr.insertCell().id = "st_" + p.p; // State + tr.insertCell().id = "role_" + p.p; // Role + tr.insertCell().appendChild(sel("en_" + p.p, + [["on","on"],["off","off"]], + e => stpCmd("stp port " + p.p + " " + e.target.value))); + tr.insertCell().appendChild(sel("edge_" + p.p, + [["auto","auto"],["on","edge"],["off","off"]], + e => stpCmd("stp port " + p.p + " edge " + e.target.value))); + tr.insertCell().appendChild(num("cost_" + p.p, 0, 255, + e => stpCmd("stp port " + p.p + " cost " + e.target.value))); + tr.insertCell().appendChild(num("prio_" + p.p, 0, 240, + e => stpCmd("stp port " + p.p + " prio " + e.target.value))); + tr.insertCell().appendChild(sel("guard_" + p.p, + [["none","none"],["bpdu","BPDU"],["root","Root"]], + e => stpCmd("stp port " + p.p + " guard " + e.target.value))); + tr.insertCell().appendChild(sel("filt_" + p.p, + [["off","off"],["on","on"]], + e => stpCmd("stp port " + p.p + " filter " + e.target.value))); + } + stpRows = ports.length; +} function fetchStp() { var xhttp = new XMLHttpRequest(); @@ -15,21 +81,42 @@ function fetchStp() { const s = JSON.parse(xhttp.responseText); // textContent throughout: rootMac comes from received BPDUs // (remote-controlled), never render it as HTML - if (!stpDirty) - document.getElementById("stpMode").value = s.on ? "on" : "off"; + if (!stpRows) + buildPortsTable(s.ports); document.getElementById("stpStat").textContent = s.on ? (s.weRoot - ? "This switch is the root bridge (priority 0x" + s.rootPrio + ")" + ? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16) : "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac - + " \u2014 path cost: 0x" + s.cost) + + " via port " + s.rootPort + " — path cost: 0x" + s.cost + + " — topology changes: " + parseInt(s.tc, 16)) : ""; - let t = ""; - if (s.on) { - t = "port state\n"; - for (const p of s.ports) - t += String(p.p).padEnd(6) + STP_STATES[p.st] + "\n"; + // live status columns always refresh + for (const p of s.ports) { + const trip = (p.f & PF_TRIPPED) ? " (guard!)" : ""; + document.getElementById("st_" + p.p).textContent = + s.on ? STP_STATES[p.st] + trip : "-"; + document.getElementById("role_" + p.p).textContent = + s.on ? STP_ROLES[p.role] + ((p.f & PF_OPEREDGE) ? " edge" : "") : "-"; + } + if (stpDirty) // an edit is in flight - do not revert controls + return; + document.getElementById("stpMode").value = s.on ? "on" : "off"; + document.getElementById("bPrio").value = s.prio; + document.getElementById("bVer").value = s.rstp ? "rstp" : "stp"; + document.getElementById("bHello").value = s.hello; + document.getElementById("bMaxage").value = s.maxage; + document.getElementById("bFwd").value = s.fwd; + document.getElementById("bTxhold").value = s.txhold; + for (const p of s.ports) { + document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off"; + document.getElementById("edge_" + p.p).value = + (p.f & PF_ADMEDGE) ? "on" : ((p.f & PF_AUTOEDGE) ? "auto" : "off"); + document.getElementById("cost_" + p.p).value = p.cost; + document.getElementById("prio_" + p.p).value = p.prio; + document.getElementById("guard_" + p.p).value = + (p.f & PF_BPDUGUARD) ? "bpdu" : ((p.f & PF_ROOTGUARD) ? "root" : "none"); + document.getElementById("filt_" + p.p).value = (p.f & PF_FILTER) ? "on" : "off"; } - document.getElementById("stpPorts").textContent = t; } }; xhttp.open("GET", `/stp.json`, true); @@ -38,21 +125,31 @@ function fetchStp() { async function stpSub() { const on = document.getElementById("stpMode").value === "on"; - try { - await fetch('/cmd', { method: 'POST', body: on ? "stp on" : "stp off" }); - } catch(err) { - console.error(`Error: ${err}`); - } - stpDirty = false; // editing done - let the refresh show the truth - fetchStp(); + await stpCmd(on ? "stp on" : "stp off"); } window.addEventListener("load", function() { + // bridge priority: 0-15 (x4096) + const bp = document.getElementById("bPrio"); + for (let i = 0; i < 16; i++) { + const o = document.createElement("option"); + o.value = i; o.textContent = (i * 4096) + (i === 8 ? " (default)" : ""); + bp.appendChild(o); + } + bp.addEventListener("change", e => stpCmd("stp prio " + e.target.value)); + document.getElementById("bVer") + .addEventListener("change", e => stpCmd("stp version " + e.target.value)); + document.getElementById("bHello") + .addEventListener("change", e => stpCmd("stp hello " + e.target.value)); + document.getElementById("bMaxage") + .addEventListener("change", e => stpCmd("stp maxage " + e.target.value)); + document.getElementById("bFwd") + .addEventListener("change", e => stpCmd("stp fwd " + e.target.value)); + document.getElementById("bTxhold") + .addEventListener("change", e => stpCmd("stp txhold " + e.target.value)); document.getElementById("stpMode") .addEventListener("change", () => { stpDirty = true; }); -}); -window.addEventListener("load", function() { update( () => { fetchStp(); const interval = setInterval(update, 2000); diff --git a/httpd/httpd.c b/httpd/httpd.c index 74e29ce..d2b523d 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -41,8 +41,8 @@ __xdata uint32_t cont_addr; // HTTP header properties __xdata uint8_t boundary[72]; -__xdata uint8_t *content_type = 0; -__xdata uint8_t *session = 0; +__xdata uint8_t * __xdata content_type = 0; +__xdata uint8_t * __xdata session = 0; // Global variables holding POST state __xdata uint16_t bindex; // Current index into the boundary @@ -54,7 +54,7 @@ __xdata char passwd[21]; __xdata char session_id[SESSION_ID_LENGTH + 1]; __xdata uint8_t authenticated; __xdata uint32_t now; -__xdata uint8_t *timeptr; +__xdata uint8_t * __xdata timeptr; __xdata uint32_t last_session_use; #define TSTATE_NONE 0 diff --git a/httpd/page_impl.c b/httpd/page_impl.c index a71d0ac..7b7db6b 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -528,14 +528,14 @@ void send_lag(void) } -/* STP status for the L2 page ("/stp.json"): enable state, the elected root - * bridge (priority+MAC) and our path cost to it, whether we are the root, and - * the live per-port STP state read from the ASIC's MSTP register (2 bits per - * port: 0 disable, 1 blocking, 2 learning, 3 forwarding - same encoding - * stp_setup() writes). Ports are reported by their physical number. */ -/* Scratch for send_stp(): a plain local would land in the near-full 8051 - * internal-RAM overlay (OSEG). */ +/* STP status + configuration for the Spanning Tree page ("/stp.json"). + * Bridge config (prio index 0-15, hello/maxage/fwd seconds, rstp flag, tx + * hold), elected root (priority byte + MAC), our path cost, root port, TC + * counter, and per port: physical number, live ASIC state (2-bit MSTP field: + * 0 Dis 1 Blk 2 Lrn 3 Fwd), an approximated role, and the per-port config + * (enabled, edge admin/auto/oper, cost/1000, prio, guard, filter, tripped). */ __xdata uint8_t stp_we_root; +__xdata uint8_t pi_i, pi_j; /* shared loop iterators (DSEG relief) */ void send_stp(void) { @@ -544,33 +544,61 @@ void send_stp(void) slen += strtox(outbuf + slen, "{\"on\":"); bool_to_html(stpEnabled); + slen += strtox(outbuf + slen, ",\"rstp\":"); + bool_to_html(stp_rstp); + slen += strtox(outbuf + slen, ",\"prio\":"); + itoa_html(stp_prio >> 4); + slen += strtox(outbuf + slen, ",\"hello\":"); + itoa_html(stp_hello_s); + slen += strtox(outbuf + slen, ",\"maxage\":"); + itoa_html(stp_maxage_s); + slen += strtox(outbuf + slen, ",\"fwd\":"); + itoa_html(stp_fwddelay_s); + slen += strtox(outbuf + slen, ",\"txhold\":"); + itoa_html(stp_txhold); slen += strtox(outbuf + slen, ",\"rootPrio\":\""); byte_to_html(root_bridge.prio); byte_to_html(root_bridge.ext); slen += strtox(outbuf + slen, "\",\"rootMac\":\""); - for (uint8_t j = 0; j < 6; j++) - byte_to_html(root_bridge.mac[j]); + for (pi_j = 0; pi_j < 6; pi_j++) + byte_to_html(root_bridge.mac[pi_j]); slen += strtox(outbuf + slen, "\",\"cost\":\""); byte_to_html(root_bridge_cost >> 24); byte_to_html(root_bridge_cost >> 16); byte_to_html(root_bridge_cost >> 8); byte_to_html(root_bridge_cost); - /* are we the elected root? (our MAC == root MAC) */ - stp_we_root = 1; - for (uint8_t j = 0; j < 6; j++) { - if (root_bridge.mac[j] != uip_ethaddr.addr[j]) - stp_we_root = 0; - } + stp_we_root = (stp_root_port == 0xff) ? 1 : 0; slen += strtox(outbuf + slen, "\",\"weRoot\":"); bool_to_html(stp_we_root); - slen += strtox(outbuf + slen, ",\"ports\":["); + slen += strtox(outbuf + slen, ",\"rootPort\":"); + itoa_html(stp_root_port == 0xff ? 0 : machine.log_to_phys_port[stp_root_port]); + slen += strtox(outbuf + slen, ",\"tc\":\""); + byte_to_html(stp_tc_count >> 8); + byte_to_html(stp_tc_count); + slen += strtox(outbuf + slen, "\",\"ports\":["); reg_read_m(RTL837X_MSTP_STATES); - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + for (pi_i = machine.min_port; pi_i <= machine.max_port; pi_i++) { slen += strtox(outbuf + slen, "{\"p\":"); - itoa_html(machine.log_to_phys_port[i]); + itoa_html(machine.log_to_phys_port[pi_i]); slen += strtox(outbuf + slen, ",\"st\":"); - /* 2-bit field per logical port, packed from byte 3 up (cf. stp_setup) */ - itoa_html((sfr_data[3 - (i >> 2)] >> ((i << 1) & 0x7)) & 0x3); + stp_we_root = (sfr_data[3 - (pi_i >> 2)] >> ((pi_i << 1) & 0x7)) & 0x3; + itoa_html(stp_we_root); + /* role (approximated): 0 none/disabled, 1 root, 2 designated, 3 alternate(blocked) */ + slen += strtox(outbuf + slen, ",\"role\":"); + if (!(stp_pflags[pi_i] & STP_PF_ENABLED) || (stp_pflags[pi_i] & STP_PF_TRIPPED)) + itoa_html(0); + else if (pi_i == stp_root_port) + itoa_html(1); + else if (stp_we_root == 3) + itoa_html(2); + else + itoa_html(3); + slen += strtox(outbuf + slen, ",\"f\":"); + itoa_html(stp_pflags[pi_i]); + slen += strtox(outbuf + slen, ",\"cost\":"); + itoa_html(stp_pcost[pi_i] / 1000); + slen += strtox(outbuf + slen, ",\"prio\":"); + itoa_html(stp_pprio[pi_i]); slen += strtox(outbuf + slen, "},"); } slen -= 1; // remove comma diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 8d2e3fd..176f672 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -1,6 +1,17 @@ /* * 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 @@ -34,14 +45,48 @@ extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; -/* struct bridge now lives in rtl837x_stp.h (shared with the web UI). */ +/* 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; + +__xdata uint8_t stp_pflags[10]; +__xdata uint32_t stp_pcost[10]; +__xdata uint8_t stp_pprio[10]; + +/* ---- Status / runtime ---- */ __xdata struct bridge root_bridge; -__xdata uint32_t root_bridge_cost; +__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 uint8_t port_types[10]; -__xdata uint16_t port_timers[10]; -__xdata uint16_t port_hello[10]; +__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]; @@ -93,11 +138,7 @@ struct stp_pkt_in { #define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE]) #define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0]) -#define FLAG_PROPOSAL 0x02 -#define P_DESIGNATED ((STP_I->flags & 0x0c) == 0x0c) -#define P_PROPOSAL (STP_I->flags & FLAG_PROPOSAL) - -signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) +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]) @@ -110,50 +151,37 @@ signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) } -void stp_in(void) __banked +/* 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 { - // By default we do not send anything out - uip_len = 0; - // MSTPSTP_I_STATES 0x5310 - // reg_read_m(RTL837X_MSTP_STATES); - - print_string("Check BPDU... \n"); - for (uint8_t i = 0; i < 80; i++) { - print_byte(uip_buf[i]); - write_char(' '); - } - write_char('\n'); - print_byte(STP_I->dsap); - print_byte(STP_I->ssap); - print_byte(STP_I->ctrl); - - write_char('\n'); - // Make sure this is the type of RSTP packet we are interested in: - if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03)) - return; - print_string("Checking RSTP\n"); - if (STP_I->proto) - return; -// write_char('A'); print_byte(STP_I->version); write_char('\n'); - if (STP_I->version != 2) - return; -// write_char('B'); print_byte(STP_I->bpdu_type); write_char('\n'); - if (STP_I->bpdu_type != 2) - return; -// write_char('\n'); -// print_string("Flags: "); print_byte(STP_I->flags); write_char('\n'); - print_string("Check new Root\n"); - if (STP_I->root.prio < root_bridge.prio - || ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 0)) { - print_string("Updating Root bridge\n"); - root_bridge.prio = STP_I->root.prio; - memcpy(root_bridge.mac, STP_I->root.mac, 6); - } + 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); } -void stp_cnf_send(uint8_t port) +/* 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; @@ -168,61 +196,211 @@ void stp_cnf_send(uint8_t port) STP_O->ssap = 0x42; STP_O->ctrl = 0x03; STP_O->proto = 0x0000; - STP_O->version = 0x02; // RSTP - STP_O->bpdu_type = 0x00; // Config - STP_O->flags = 0x81; + if (stp_rstp) { + 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 { + 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 = 0x00; - STP_O->root_path_cost = 0x00000000; + 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 = 0x80; + STP_O->bridge.prio = stp_prio; STP_O->bridge.ext = 0x00; - STP_O->port_prio = 0x80; - STP_O->port_id = port; + 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 = 20; - STP_O->hello = 2; - STP_O->fwd_delay = 0x0f; + STP_O->age_max = stp_maxage_s; + STP_O->hello = stp_hello_s; + STP_O->fwd_delay = stp_fwddelay_s; -// uip_len = 0x27 + sizeof(struct rtl_tag); uip_len = sizeof(struct stp_pkt); tcpip_output(); } +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 { - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - port_hello[i]--; - if (!port_hello[i]) { - port_hello[i] = TIME_HELLO; - print_string("STP_HELLO port "); - print_byte(i); write_char('\n'); - stp_cnf_send(i); + /* 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; + } + + 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 the initial blocking state once its listen - * period expires. stp_setup() puts every port into blocking with - * port_timers = 10 s, but nothing ever counted that down - so on a - * network with no other RSTP bridge (nobody sends us BPDUs) every - * port stayed blocking FOREVER and "stp on" killed the whole - * network. If no better root was heard during the listen period we - * are the designated bridge on that port: go to forwarding. */ - if (port_timers[i]) { - if (!--port_timers[i]) { - reg_read_m(RTL837X_MSTP_STATES); - sfr_data[3 - (i >> 2)] |= (uint8_t)(0b11 << ((i << 1) & 0x7)); - reg_write_m(RTL837X_MSTP_STATES); + + /* 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(i); write_char('\n'); + 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; + 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(); } @@ -263,22 +441,29 @@ void stp_setup(void) __banked { print_string("Enabling STP: "); sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - // Set STP port state to blocking - // States are: 00 disable, 01 blocking, 10 learning, 11 forwarding - uint8_t bit_mask = 0b01 << ( (i << 1) & 0x7); - sfr_data[3 - (i >> 2)] |= bit_mask; - port_hello[i] = TIME_HELLO; - port_timers[i] = 0x280; // 10 s in blocking state (at the ~64 Hz stp_timers rate) + 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'); - root_bridge.prio = 0x80; // This corresponds to 32768 - root_bridge.ext = 0x00; - memcpy(root_bridge.mac, uip_ethaddr.addr, 6); + stp_claim_root(); /* Take BPDUs to the CPU only - we are a participating bridge now. */ stp_fdb_update(PMASK_CPU); @@ -288,11 +473,12 @@ void stp_setup(void) __banked void stp_off(void) __banked { sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + 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 - uint8_t bit_mask = 0b11 << ( (i << 1) & 0x7); - sfr_data[3 - (i >> 2)] |= bit_mask; + 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); @@ -300,3 +486,131 @@ void stp_off(void) __banked /* 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"); + 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]; + 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")) { + if (atoi_byte(&stp_scratch, cmd_words_b[4])) + goto err; + stp_pcost[port] = (uint32_t)stp_scratch * 1000; + } 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 { + 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"); +} diff --git a/rtl837x_stp.h b/rtl837x_stp.h index 4aaf959..cf9e9e9 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -6,8 +6,8 @@ void stp_in(void) __banked; void stp_setup(void) __banked; void stp_timers(void) __banked; void stp_off(void) __banked; - -#define TIME_HELLO 0x80 // 2 sec (stp_timers runs at ~64 Hz: main loop ~256 Hz / STP_TICK_DIVIDER+1) +void stp_parse(void) __banked __reentrant; /* "stp ..." CLI handler (cmd_parser delegates here) */ +void stp_defaults(void) __banked; /* boot init: 802.1D/w default configuration */ /* Bridge identifier as carried in a BPDU (priority, extension, MAC). */ struct bridge { @@ -16,11 +16,33 @@ struct bridge { uint8_t mac[6]; }; -/* Protocol state, exposed read-only for the web UI (page_impl.c send_stp()) - * - the elected root bridge and our path cost to it. Owned by rtl837x_stp.c; - * stpEnabled is owned by rtlplayground.c. */ -extern __xdata uint8_t stpEnabled; +/* ---- Configuration (defaults per 802.1D-2004/802.1w, set in stp_defaults) --- */ +extern __xdata uint8_t stpEnabled; +extern __xdata uint8_t stp_prio; /* bridge priority, high byte: 0x80 = 32768; CLI takes 0-15 (steps of 4096) */ +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) */ + +/* Per-port config/status flags (stp_pflags[]) */ +#define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ +#define STP_PF_ADMEDGE 0x02 /* admin edge: forwarding immediately */ +#define STP_PF_AUTOEDGE 0x04 /* auto edge: forward after 3 s without BPDU */ +#define STP_PF_BPDUGUARD 0x08 /* disable port if a BPDU arrives */ +#define STP_PF_ROOTGUARD 0x10 /* never accept a better root on this port */ +#define STP_PF_FILTER 0x20 /* neither send nor accept BPDUs */ +#define STP_PF_OPEREDGE 0x40 /* runtime: port went forwarding as an edge */ +#define STP_PF_TRIPPED 0x80 /* runtime: disabled by BPDU guard */ + +extern __xdata uint8_t stp_pflags[10]; +extern __xdata uint32_t stp_pcost[10]; /* path cost; 0 = auto (20000) */ +extern __xdata uint8_t stp_pprio[10]; /* port priority (default 0x80) */ + +/* ---- Status, exposed read-only for the web UI (send_stp) ---- */ extern __xdata struct bridge root_bridge; -extern __xdata uint32_t root_bridge_cost; +extern __xdata uint32_t root_bridge_cost; /* our path cost to the root (0 if we are root) */ +extern __xdata uint8_t stp_root_port; /* logical port towards the root; 0xff = we are root */ +extern __xdata uint16_t stp_tc_count; /* topology change counter (diagnostics) */ #endif diff --git a/rtlplayground.c b/rtlplayground.c index 27a6e3f..aa12761 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -2141,6 +2141,7 @@ void main(void) print_reg(RTL837X_REG_SEC_COUNTER); #endif stpEnabled = 0; + stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ nic_setup(); vlan_setup(); port_l2_setup();