mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-10-29 17:54:14 +08:00
stp_loop_hold_peer() takes operEdge away from the port that sent the BPDU we got back, though that port heard nothing itself. When the hold ran out the port stayed non-edge until its next link change, and every later state change on it counted as a topology change. Restore edge when the hold ends with the port still silent and auto-edge on, before the transition is reported, so the recovery itself is not a change. Hellos go out on edge ports too, so a loop that persists is caught again. The console column that shows seconds since the last BPDU is now called age[s]; it never was a receive count.
290 lines
14 KiB
Markdown
290 lines
14 KiB
Markdown
# Spanning Tree (STP / RSTP)
|
||
|
||
The switch can take part in a spanning tree (IEEE 802.1D / 802.1w) so that
|
||
redundant links between bridges are blocked instead of forming a loop. The
|
||
implementation elects a root bridge from the BPDUs it receives, picks the
|
||
cheapest path to it as the root port, blocks ports on which a better bridge is
|
||
already designated, promotes ports to forwarding once their listen period
|
||
expires, drops information a neighbour stopped sending, and blocks a port on
|
||
which it sees its own BPDU.
|
||
|
||
STP can be enabled and controlled via the web interface or the command line,
|
||
as follows:
|
||
|
||
## Quick start
|
||
|
||
```
|
||
stp on # start participating
|
||
stp off # stop, all ports back to forwarding
|
||
```
|
||
|
||
Live status is on the Spanning Tree page of the web UI (or `/stp.json`),
|
||
and on the serial console via `stp status`.
|
||
|
||
With no other bridge around, the switch elects itself root and every port ends
|
||
up forwarding — you can leave it on safely. Put the settings in the startup
|
||
config to make them survive a reboot:
|
||
|
||
```
|
||
stp prio 15
|
||
stp port 1 edge on
|
||
stp on
|
||
```
|
||
|
||
## Hardware background
|
||
|
||
BPDUs are addressed to `01:80:C2:00:00:00`, a reserved link-local group. The
|
||
ASIC's Reserved-Multicast action for that address decides what happens to the
|
||
frame.
|
||
|
||
While STP runs, the action is *trap*. A trap delivers to the port named in
|
||
`EXT_CPU_CTRL` (0x6724), whose reset value 0xf names no port, and with that
|
||
value the trapped frame is dropped at ingress. `trap_init()` sets it to 9, the
|
||
port the 8051 sits behind, once at startup. The trap needs no per-VLAN L2
|
||
entries, so it does not depend on which VLANs exist or on the PVIDs, and turning
|
||
STP on no longer walks the whole VLAN table. BPDUs reach the CPU on a port that
|
||
STP holds in blocking with either action: measured on a SWTGW218AS by sending
|
||
BPDUs into a blocked port with forward and with trap, both ports stopped being
|
||
edge and waited out the forward delay. The chip exempts this address from the
|
||
ingress state check; the slow-protocols address used by LACP is not exempt.
|
||
|
||
A trapped frame reaches the CPU in the format it came in with: unlike a
|
||
forwarded one it has no VLAN tag inserted after the RTL tag, and the RTL tag
|
||
carries the trap reason. `stp_in()` reads the BPDU from the offset
|
||
`TRAP_RX_BODY()` returns, which works for both.
|
||
|
||
With STP off, the action goes back to *forward* and a static L2 multicast
|
||
entry per VLAN in use (`port_l2mc_set()`) targets all ports, restoring the
|
||
transparency an unmanaged switch is expected to have, so a surrounding
|
||
spanning tree can span *through* this device.
|
||
|
||
The port's acceptable-frame-type filter treats the two actions differently.
|
||
BPDUs are untagged, so a port set to admit tagged frames only
|
||
(`ingress <port>t`) drops a forwarded BPDU before it reaches the CPU, while a
|
||
trapped one passes. Both were measured on a SWTGW218AS with `ingress 3t`:
|
||
LACPDUs, delivered by forward, stopped arriving on port 3, and BPDUs sent into
|
||
the same port, delivered by trap, kept arriving. A port that admits tagged
|
||
frames only therefore takes part in STP.
|
||
|
||
Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port:
|
||
`00` disabled, `01` blocking, `10` learning, `11` forwarding. A port in
|
||
blocking forwards nothing between ports, but it still sends what the CPU
|
||
hands it and still passes a received BPDU up to the CPU, which is what lets
|
||
loop detection go on working on a port it has already blocked.
|
||
|
||
## Timers
|
||
|
||
`stp_timers()` runs at 50 Hz (the main loop idles on the 200 Hz system tick and
|
||
STP is called every fourth pass), which is what `STP_HZ` in `rtl837x_stp.h`
|
||
encodes. All configured values are in seconds:
|
||
|
||
| setting | default | range |
|
||
|---|---|---|
|
||
| `stp hello <n>` | 2 | 1–10 |
|
||
| `stp maxage <n>` | 20 | 6–40 |
|
||
| `stp fwd <n>` | 15 | 4–30 |
|
||
| `stp txhold <n>` | 6 | 1–10 |
|
||
|
||
802.1D also requires `2 * (fwd - 1) >= maxage >= 2 * (hello + 1)`. A
|
||
command typed on the console or sent by the web page that would break it is
|
||
refused and the value stays as it was, so change the values in an order that
|
||
keeps the rule at every step (for a larger max age raise the forward delay
|
||
first). The web page shows the error, and a refused command is not recorded
|
||
in the command history, so saving the configuration does not pick it up. Lines of the saved configuration are taken one by one whatever their
|
||
order, and `stp on` checks the result: if the rule is broken it warns and
|
||
starts with 2/20/15 instead.
|
||
|
||
The values above are what the switch uses while it is the root. Otherwise it
|
||
takes max age and forward delay from the BPDUs on its root port, as 802.1D
|
||
requires, and passes them on in its own BPDUs with the message age one second
|
||
higher; the hello time stays its own.
|
||
|
||
A port entering the tree discards for one forward delay, then learns
|
||
addresses without forwarding for a second one, and only then forwards (an
|
||
edge port skips the wait). In RSTP mode on a point-to-point link
|
||
(`p2p auto` or `on`) it can go faster: a designated port that is still
|
||
discarding sets the proposal flag, and forwards as soon as the bridge on the
|
||
other side answers with an agreement. When the root port receives a proposal,
|
||
the switch first puts its other non-edge designated ports back to discarding
|
||
(they propose in turn), then forwards the root port and answers with an
|
||
agreement; this sync happens once per root port, later proposals are only
|
||
answered. An alternate or backup port that receives a proposal does the same
|
||
sync once for the information it holds and answers with an agreement in a
|
||
BPDU with the alternate role, so the bridge on the other side does not have
|
||
to wait out its timers; it stays discarding itself. An agreement is accepted
|
||
from a root or an alternate port on the other side. A new root port forwards at once, after every port that was root within
|
||
the last forward delay has been put back to discarding; such a recent root
|
||
port stays closed, even to an agreement, until that forward delay is over.
|
||
A new root port that was a backup port within the last two hello times does
|
||
not take the fast path and waits for its timers.
|
||
|
||
A designated port that hears worse information from a bridge that still
|
||
claims to be designated and learning or forwarding on the same link has a
|
||
dispute: usually one direction of the link is lost and the other bridge does
|
||
not hear this switch's BPDUs. The port goes back to discarding, and every
|
||
further such BPDU restarts its listen period, so it does not forward while
|
||
the dispute lasts.
|
||
|
||
With `stp version rstp` each port still talks 802.1D to a neighbour that
|
||
does: once a Config BPDU or TCN arrives and the migrate time (3 s since the
|
||
port came up or last switched) is over, the port sends Config BPDUs, uses
|
||
the 802.1D TC window and reports changes with TCN if it is the root port. An
|
||
RST BPDU after the migrate time switches it back, and so does a link that
|
||
comes up again or `stp port <n> mcheck`. Any worse information heard on a designated port, disputed or
|
||
not, is answered with a BPDU at once instead of at the next hello, so the
|
||
other bridge learns sooner that it is not the designated one. Information heard on a port stays
|
||
valid for three of the sender's hello times; a BPDU whose message age has
|
||
reached its max age is not used at all. When the root port's information
|
||
expires the switch moves to the next best port, or reclaims the root role if
|
||
there is none.
|
||
|
||
Among ports offering the same root, the lowest root path cost wins, then the
|
||
designated bridge, the designated port ID, and finally this switch's own port
|
||
ID. A worse BPDU from a different bridge does not replace what a port holds;
|
||
a worse one from the same designated bridge and port does.
|
||
|
||
## Topology changes
|
||
|
||
A topology change is detected when a non-edge port enters forwarding. That
|
||
port, and every other forwarding non-edge port, then carries the TC flag in
|
||
its BPDUs for hello time plus one second (max age plus forward delay with
|
||
`stp version stp`), and each of the other ports flushes the addresses it
|
||
learned. The root port takes part too: while its TC window runs it sends
|
||
BPDUs toward the root, so the change reaches the rest of the network. With
|
||
`stp version stp` the root port sends TCN instead, until a Config BPDU with
|
||
TCA comes back.
|
||
|
||
A TC flag received on a forwarding root or designated port is passed on the
|
||
same way to the other ports, not back to the port it came in on; a port that
|
||
is still discarding or learning drops it. A legacy TCN is acknowledged with
|
||
TCA and then treated like a received TC. A port that turns
|
||
alternate, loses its link, or is blocked for a loop flushes its own addresses
|
||
without raising a topology change, and an edge port never raises one.
|
||
|
||
## Bridge settings
|
||
|
||
```
|
||
stp prio <0-15> # bridge priority = n * 4096, default 8 (32768)
|
||
stp version rstp|stp # RST BPDUs (default) or legacy Config BPDUs
|
||
stp hello|maxage|fwd|txhold <seconds>
|
||
stp pathcost long|short # automatic port costs, 802.1D-2004 (default) or 802.1D-1998
|
||
stp bpdu flood|filter # BPDUs while STP is off: pass them on (default) or drop them
|
||
```
|
||
|
||
The bridge with the lowest priority wins the root election; ties are broken by
|
||
the MAC address. If you do not want this switch to become the root of an
|
||
existing network, give it a worse priority than the current root — `stp prio 15`
|
||
(61440) is the usual "never me" value.
|
||
|
||
A port without a configured cost takes it from its link speed. The long
|
||
method uses the 802.1D-2004 values (20000 for 1G, 2000 for 10G); the short
|
||
one the 802.1D-1998 table for bridges that still use 16-bit costs (100 for
|
||
10M, 19 for 100M, 4 for 1G, 2 for 10G, and 3 for 2.5G and 5G, which that
|
||
table does not list). All bridges in a network should use the same method.
|
||
|
||
`stp bpdu filter` keeps BPDUs from being passed between ports while STP is
|
||
off, instead of the default flooding that lets a surrounding spanning tree
|
||
run through the switch. It takes effect at once when STP is off, otherwise
|
||
when STP is turned off.
|
||
|
||
## Per-port settings
|
||
|
||
```
|
||
stp port <1-9> on|off # take part in STP, or stay plain forwarding
|
||
stp port <1-9> edge on|off|auto # host-facing port handling (default: auto)
|
||
stp port <1-9> cost <0-200000000> # path cost, 0 = automatic (20000)
|
||
stp port <1-9> prio <0-240> # port priority, steps of 16
|
||
stp port <1-9> guard none|bpdu|root
|
||
stp port <1-9> filter on|off # neither send nor accept BPDUs
|
||
stp port <1-9> p2p auto|on|off
|
||
stp port <1-9> mcheck # try RSTP again on a port that fell back to STP
|
||
```
|
||
|
||
**edge** — an edge port forwards immediately and does not trigger a
|
||
topology change when its link comes and goes; `auto` promotes a port to edge
|
||
after three seconds without a BPDU, and demotes it as soon as one arrives. Use
|
||
`edge on` for ports where only hosts are attached.
|
||
|
||
**guard** — `bpdu` disables a port as soon as a BPDU arrives on it (a host port
|
||
should never see one); `root` keeps a port from ever becoming the path to the
|
||
root, which protects an existing topology from a newly attached bridge that
|
||
claims a better priority.
|
||
|
||
**filter** — the port neither sends nor accepts BPDUs. Useful when the device
|
||
on the far side reacts badly to them (some unmanaged switches with loop
|
||
prevention cut the link) but you still want STP on the rest of the ports.
|
||
|
||
## Link aggregation
|
||
|
||
A LAG (Link Aggregation Group) is one port of the tree, with its own roles,
|
||
timers and states. The switch supports up to 4 LAGs; each one takes the place
|
||
of its member ports in the STP status and gets its own row on the Spanning
|
||
Tree page. A group carries its own path cost, priority, edge, guard and
|
||
point-to-point settings, and takes the same commands as a port:
|
||
|
||
```
|
||
stp lag <1-4> on|off|mcheck|edge|cost|prio|guard|filter|p2p ...
|
||
stp lag 1 cost 10000 # the group decides, not its members
|
||
```
|
||
|
||
LAGs are configured with the `lag` command. Once a port is a member of a LAG
|
||
it can no longer be configured on its own for STP, and `stp port <n>` on a
|
||
member names the group to configure instead. Membership is read from the
|
||
aggregation registers once a second, so a group changing under LACP is picked
|
||
up without any coordination between the two; a port that joins or leaves a
|
||
group, and the group itself, start over from discarding. The automatic path
|
||
cost of a group is the one of its lowest member that has a link.
|
||
|
||
The hardware keeps no STP state for a LAG as a whole, so every state change is
|
||
written to all member ports, in a single register write. BPDUs go out through
|
||
the lowest member that has a link and carry the group's own port id, and a
|
||
BPDU received on any member belongs to the group. Losing one member of a live
|
||
LAG is not a topology change; the group only goes down with its last link.
|
||
|
||
## Status
|
||
|
||
The Spanning Tree page shows the elected root (priority and MAC), the path cost
|
||
to it, the root port, the topology-change counter and the time since it last
|
||
moved, the max age and forward
|
||
delay in use (the root's, when this switch is not the root) and, per port, the
|
||
live state read from the ASIC, or "No link", together with the configured
|
||
options. For a port taking part in the tree it also shows the designated
|
||
bridge, port and cost, and whether it talks RSTP or 802.1D to its neighbour;
|
||
the Check button runs `stp port <n> mcheck`. The same data is available as
|
||
JSON:
|
||
|
||
```
|
||
GET /stp.json
|
||
```
|
||
|
||
The Counters section of the same page (or `/stpcnt.json`) shows per port the
|
||
BPDUs received and sent, those of them that carried the topology change flag
|
||
(TCNs included), and the time since the last BPDU arrived. The counters start
|
||
from zero with `stp on`, and `stp clear` resets them; the topology change
|
||
counter is not reset.
|
||
|
||
The `stp status` command prints the same view on the serial console. A port
|
||
is reported with one of the roles Root, Designated, Alternate, Backup or
|
||
Disabled; Disabled covers a port with STP turned off on it, one shut down by
|
||
BPDU guard and one without link.
|
||
|
||
The console's `age[s]` column (previously labelled `bpdu`) is the elapsed
|
||
time since the last BPDU, capped at 255 seconds; it is not a receive count.
|
||
Use the web counters or `/stpcnt.json` for full receive counts.
|
||
|
||
An own BPDU received back on another port can temporarily hold its sending
|
||
port in discarding, even if that sending port has received no BPDUs itself.
|
||
When the hold expires, auto-edge is restored if the sending port has remained
|
||
silent. A continuing loop keeps the hold active.
|
||
|
||
BPDUs shorter than their type allows, judged by the 802.3 length field, are
|
||
dropped: 7 bytes for a TCN, 38 for a Config BPDU and 39 for an RST BPDU,
|
||
LLC header included.
|
||
|
||
## Limitations
|
||
|
||
* One spanning-tree instance; no MSTP, no per-VLAN trees.
|
||
* A backup port, one that hears the BPDUs of another port of this switch,
|
||
is blocked and reported as Backup, but keeps sending BPDUs: loop detection
|
||
on that segment relies on them.
|