stp_reselect() took every port with fresh information as a candidate, so a
BPDU credited to a port whose carrier the once-a-second link poll had not
seen yet made that port the root port before the port was set up for it.
A candidate now needs its link bit as well; the poll that sees the carrier
a moment later runs the selection again. Scenario 54.
stp_in() propagated the TC flag of any BPDU that did not arrive on an
alternate port, and acknowledged and propagated a TCN from any port, so a
port still in its listen period or held discarding in a dispute could flush
the addresses of every other port and move the topology change counter.
802.1D-2004 17.31 keeps the Topology Change machine in LEARNING until the
port forwards and drops rcvdTc and rcvdTcn there. Both paths now ask for
the forwarding state first; an alternate port is discarding, so the old
check is covered by the new one.
Scenario 53 plays a TC flag and a TCN into a port that is still discarding,
and then into the same port once it forwards. Scenario 51 used a TCN on a
port one tick short of its edge transition as the trigger for the clock it
tests; it now waits for the transition.
The frame the simulator builds carries a vlan_tag between the RTL tag and the BPDU, but left its TPID zero. stp_in() now takes the body offset from TRAP_RX_BODY(), which reads those two bytes, so it took the untagged branch and parsed the body four bytes early. Writing 0x8100 makes the frame what its layout says it is.
Reception moved to the RMA trap in main, so rtl837x_stp.c calls trap_rma_set() from stp_setup() and stp_off(). The simulator defines neither, so it does not link. Linking rtl837x_trap.c instead would pull trap_init() and the register primitives it needs, which the simulator does not mock.
stp_sim.c declared variables in the middle of the scenarios and put
whole loops and conditions on one line. Each block now starts with its
declarations; an initializer that depends on what happens before it
stays where it was as an assignment. Bodies of if and for are on lines
of their own, functions are separated by an empty line, and lines stay
within 120 columns. The output of stp_sim -v is unchanged.
Scenarios 4 and 7 start from the state scenario 3 leaves behind, and 38
from the one 37 leaves, and they called those scenarios for it, so the
headings of 3 and 37 showed up again in the middle of the run. The setup
of 3 and 37 now lives in functions of its own that the scenarios call.
test/stp/shim held hand-written copies of the headers the STP code
includes, and they had drifted: the uIP buffer was 600 bytes instead of
1556, struct machine had three fields, port_ingress_filter_get()
returned a plain uint8_t. The simulator now compiles rtl837x_stp.c and
rtl837x_stp_cli.c the way test/ compiles firmware sources, with the real
headers and sdcc_shim.h forced in, and with the same warnings, including
-Wno-pointer-sign for the string literals passed to cmd_compare(). It
stays on -std=c11: in gnu11 mode glibc defines LITTLE_ENDIAN, which
uip-conf.h names as the byte order, and HTONS() stops swapping. Its own
print_string() and cmd_compare() take the firmware's argument types.
* stp: select the root port by cost and block redundant links
* stp: move the protocol to BANK4 and keep the command line in BANK2
* stp: implement the RSTP state machines of 802.1D-2004 clause 17
* stp: counters, more settings and a fuller Spanning Tree page
* stp: make a link aggregation group one port of the RSTP tree
* web: serve the Spanning Tree JSON from BANK4 as well
Kept in BANK1, the larger Spanning Tree page would leave under 1 kB
there for the rest of the web server and every page still to come, so
send_stp() and send_stp_counters() move to httpd/page_stp.c in BANK4
with the protocol, as banked calls, with their own copies of the few output
helpers and of the JSON header, so nothing in BANK4 calls into BANK1
directly or hands it a pointer into its code space. BANK4 takes the
free flash between BANK3 and the web files.
KP_9000_6XHML_X2: BANK1 44336, BANK2 36969, BANK3 11942, BANK4 20965
bytes of 49152.
Review feedback on #560 (DrDoof):
- A login that came with a cookie that is still valid took a second
slot. The first one looked fresh, so the next login could push out
someone who is using the switch. The slot of the cookie now gets the
new id.
- Changing the password leaves no old login valid: session_init() from
parse_passwd(). With one session the next login pushed a leftover one
out, with several it would stay valid until the next restart.
- The 32-bit comparison of the last use times moves into a leaf function,
so the compiler does not give it a 4-byte scratch in internal RAM.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The web server's bank had no room left for the four sessions once the
original firmware's more robust header handling came in. Picking a slot,
checking a cookie and starting a session move to sessions.c in bank 3;
httpd.c only calls session_init(), session_check() and session_new().
The random id is written straight into the reply, which drops
gen_random_hex_chars() from bank 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three more bench scenarios around the closing boundary. Every value
of the last image byte, uploaded whole and split right after the
payload, must end the part. A run of CR before the boundary of even
length passed before the fix and an odd one did not, so both are
checked. An image made of CR only, longer than a flash page, and one
made of repeated "\r\n-" exercise the broken matches back to back:
each one appends to the page buffer without a byte written, and the
page flush after both paths has to keep up.
stream_upload() matches the closing multipart boundary byte by byte.
When a byte breaks a partial match, the matched prefix goes to flash as
data, and so does the breaking byte, without being compared against the
start of the boundary.
The boundary starts with "\r\n--", so an image whose last byte is '\r'
never completes: that '\r' starts a match, the '\r' of the boundary
breaks it and is written as data, and the rest of the boundary no
longer matches. The browser waits for a reply that never comes.
Flush the matched prefix and look at the breaking byte again from the
start of the boundary. A boundary may not contain CR, so no longer
suffix of the matched prefix can start a new match.
Chrome on macOS labels .bin files application/macbinary. Searching for application/octet-stream mistakes the image bytes for more part headers and aborts when the preamble buffer fills.
Find the multipart header terminator instead, after the HTTP separator retained by handle_post. Keep authentication, CRC checks and staging limits unchanged. Cover MacBinary, an omitted part Content-Type, and a split header terminator with exact staged-byte checks.
With UIP_CONF_MAX_CONNECTIONS set to one, a verified upload can leave
reset intent set when its response is never acknowledged. A subsequent
TCP connection reuses that slot, and ACKing its plain GET response can
otherwise trigger reset_chip(). This is sequential slot reuse, not
concurrent HTTP connections.
Store reset intent in the connection appstate and initialize both tstate
and fw_reset_pending on every uip_connected() event, regardless of stale
appstate contents. Reset only after the verified upload response is fully
acknowledged.
Exercise the real HTTP callback for all stale tstate bytes, sequential
slot reuse and a later GET, partial/final ACKs, abort, timeout and CRC
failure. Keep the regression tests in the existing tab-indented style.
Move port_bit() to rtl837x_common.h as PORT_BIT(), a uint16_t shift,
and MAC_ADDR_LEN from rtl837x_lldp.h next to it, and use MAC_ADDR_LEN
for every address in the LACP code instead of a literal 6. Define
lacp_mux_update() before its first caller and drop the forward
declaration, declare variables at the top of their blocks, put the
operator at the end of a continued line, drop the field value comments
of the LACPDU struct, and leave lacp_port_lag to lacp_init() instead of
initialising it in the test environment.
The LACP test now compiles with the shared harness flags and
sdcc_shim.h against the firmware headers, so the copies under
test/lacp/shim are gone. test_port_tables already covers the trunk hash
seed that test/port_hash checked against its own register copies.
lacp_in() declares its locals at the top, which the stricter host build
needs.
The receive machine armed current_while from the partner's TIMEOUT bit,
so against a partner on the slow rate a port whose LACPDUs stopped with
the link still up stayed in the trunk for the long timeout, two minutes.
802.3ad 43.4.12 starts the timer from the actor's own LACP_Timeout, and
we always advertise the short one: the partner sends every second and is
let go after the short timeout.
Sandbox T23 plays a slow-rate partner that goes quiet and fails on the
code before this.
Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.
LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.
The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.
The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.
A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
timing with ticks
Fixing up the log_to_phys_port assignment
2000 is about every 10 seconds, so scaling up to once every 30ish
Removing the attempt to block lldp frames,
This does not work however the hard work has been done for us in
the #514 so we can borrow that work later on
boot_to_html outputs 1 and 0 not true and false, making the sim match
the real implementation.
lldp.json has a hanging , in the array; so overwriting it
The switch keeps a count of the dynamic L2 entries learned on every
port in L2_LRN_PORT_CONSTRT_CNT (0x53ac + 4 * port, 13 bits). `stat`
prints it in a new MACs column and /status.json carries it as "macs".
On a SWTGW218AS the count matched the L2 table port by port, static
entries are not included, and taking a port down drops it to 0.
A matrix of the ports, one row per ingress port, showing which ports a
frame entering there may leave through. The port number of a row turns
red when the port is not fully open, and the cell for the port itself is
marked since it is always allowed. Apply sends one "isolate" command per
changed row, and Allow all ticks the whole matrix.
The switch side is a new /isolation.json, which reports each port's
allowance mask read back from the hardware. The page is an Isolation
section of the single-page UI.
test_port_tables and test_page_json link httpd/page_impl.c unmodified,
which now references session_timeout. Define it in support.c next to
the other firmware globals so the tests link again on current main.
uip has no reassembly, so every fragment it receives is discarded and
logged. A neighbour that broadcasts fragmented traffic therefore produces a
continuous stream of "ip: fragment dropped." rather than an occasional one,
at roughly thirteen lines a second on a quiet segment, which buries every
other console message.
The message carries real information: a sudden burst of it says something
about the network. What carries none is repeating it per fragment. Report
the count once per five second window instead, which keeps the signal and
ends the flood.
The count is printed through the no_syslog primitives, as the original
message was, so the flood cannot move from the console into the log ring.
itoa() writes through write_char(), which also feeds the ring, so it gets
a no_syslog twin beside it rather than a private copy of its digits.
stp_timers() ran every fourth pass of the main loop and stp_clock counted
passes, which only measures time while a pass is one tick long. It never
quite was: a pass is as long as the packets it handles, and #482 now takes
up to four of them, so under an IPv4 stream a pass is about 16 ms and STP
time runs at a third of its rate, with hellos 6 s apart and a root aged out
after a minute. Since #483 the loop also stops sleeping while packets wait,
so under a flood of cheap frames passes come faster than ticks and STP time
runs fast instead, shortening the forward delay in exactly the situation a
loop produces. uip_periodic() had the same problem from the other side: it
ran once per pass, so the retransmit timer sped up with the loop.
Both now go by the tick counter the timer interrupt keeps. The STP block
catches up on ticks a long pass missed, at most eight steps at a time, and
handle_tx() runs its periodics once per tick at most. One byte of xdata
more, 48 bytes of code.
A host test drives the gate with one pass every 5 ms, every 16 ms and
every 0.6 ms for ten seconds each; the STP clock lands on 500 steps in
all three, where counting passes gave 156 and 4166. A pass of 1.3 s loses
time rather than firing 65 steps at once, and the arithmetic survives the
tick counter wrapping.
Building with SDCC 4.6.0 emits tons of warnings e.g.:
cmd_parser.c:31: warning 356: object in read-only code space should be const
cmd_parser.c:36: warning 357: pointer to object in read-only code space should be pointer to const
These of course become fatal when building with --Werror.
Comparing resultant binaries, it seems that a single string is shifted
(deduplication?) but the numbers emitted at the end of the build are
otherwise identical.
The TODO next to the disabled TCP checksum code asks which bit of the RX tag
carries the result. It is bit 2 of byte 1: the hardware sets it when the L4
checksum is bad.
That answers the question for UDP as well, which was not being checked either
because UIP_CONF_UDP_CHECKSUMS is 0. Both paths now read the flag instead of
running a checksum over the payload on the 8051.
A zero UDP checksum, which is optional in IPv4, is not flagged by the hardware,
so DHCP keeps working and no extra condition is needed.
rx_headers lives in rtlplayground.c, which the host tests do not link, so
test_httpd_tx carries the storage itself the way it already carries uip_buf.
A zeroed descriptor reads as a good checksum, so the existing scenarios are
unaffected.
A new scenario sets the flag on a request and expects it counted as a checksum
error and left unanswered, then sends the same request with the flag clear and
expects it served. Built against uip.c without this change, those first two
checks fail and the two control checks still pass, so the scenario tells the
two versions apart rather than merely exercising the code.
Costs 156 bytes of code on SWTGW218AS and no extra XDATA.
gcc flags the else whose only statement is dbg_string(), because that macro
expands to nothing unless DEBUG is on, leaving the branch with an empty body.
The line is older than this branch; the bench is simply the first thing that
compiles httpd.c with a host compiler and -Wall -Wextra.
Turn warnings into errors in the test Makefile too, so the next one is caught
where it appears instead of scrolling past. It is the only warning the four
test binaries produce, so nothing else has to change.
Drives the unmodified httpd.c and uip.c through a handshake and a GET
from a client that moves its receive window, and compares the bytes that
reach the wire with the file they are meant to carry. The served file
holds a position-dependent pattern, so a repeat or a gap is reported at
its offset rather than as content that differs somewhere.
The first scenario holds the window still and must pass: it is the check
that the bench itself is sound, before the two moving-window scenarios
are believed.
page_impl.c and rtl837x_port.c are linked together over the simulated
engine, so /vlan.json, /vlanlist and /l2.json are checked against what
vlan_create, pvid and the L2 table hold, through the same bytes the switch
would. The L2 walk is driven the way the web page drives it, page after
page until an index repeats, which is also where the repeat of the first
entry is pinned down as the wrap marker the page relies on. The counter
test fails on the code before the previous commit.