cmd_parser.c conflicted twice: main rewrote the pvid command around the
new port-separator and atoi helpers, next to the line where this branch
delegates "stp" to stp_parse(). Both belong, so the STP delegation keeps
main's pvid body.
The linker caught what the merge could not see: main changed atoi_byte()
to return the digit count and leave the value in atoi_results_u8, where
it used to return non-zero on failure and write through a pointer. Note
the sense is inverted, so the three calls in rtl837x_stp.c are adjusted
rather than just re-arranged.
The window could be armed from the serial console but only ever disarmed
by an HTTP request. save_cmd, which gates arming, is cleared only while
execute_config() replays the startup config, so every interactive command
armed it wherever it was typed, while mgmt_alive, which disarms it, was
written in exactly one place, on HTTP traffic. An operator working
entirely on the serial console therefore lost STP 180 seconds after
enabling it however much they typed, which is what makes the mechanism
impossible to test from a console.
The documentation described the behaviour that was intended rather than
the one that was built, and in both directions: it said a command on the
serial console also confirms, and it said a reboot with STP in the
startup config disables it again three minutes later. Neither held. The
replay path never armed the window at all.
Repairing the asymmetry would have kept a mechanism whose premise is
contested anyway. A watchdog that switches the protection off in response
to silence adds a second failure mode on top of the first: where the
network is misconfigured and STP is the thing holding a storm back,
restoring forwarding removes the last reason management still answers.
Gone with it: the stp failsafe command, the fs and fsT fields of
/stp.json, the input and the tripped banner on the Spanning Tree page,
the two persistence patterns in config.js, the documentation section, and
mgmt_alive itself, which had no other reader.
550 bytes back, 145 of BANK1 and 405 of BANK2, and five of xdata, which
is the four counters and mgmt_alive and nothing else. Built for
SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
Three things from the page feedback.
The rows came out in logical order while carrying the physical port number,
so on the six-port boards the first row is labelled 5. Sorting the rows by
that number in JS puts every board back into front-panel order. I walked all
25 machine definitions and each one now yields a clean 1..N.
The Designated Bridge column is hard to read without knowing this switch's
own bridge ID, so the status line shows it in the same priority and MAC shape
as the cells use. On the test switch that reads 61440-06:05:16:1E:F9:24 and
matches the Designated Bridge of every locally designated port, which is the
comparison that was missing. The root bridge and the path cost now use the
same formatting as the columns instead of raw hex.
Enabling STP printed nothing until the next poll, and because the ports start
blocked, management can stay quiet for the whole listening and learning
period, so the page had no chance to say anything later. It now writes what
is about to happen before the command goes out, and how long the ports need.
Page data only. Both banks, xdata and the common bank are unchanged.
Bring the Spanning Tree page in line with a typical managed switch's
per-port panel. Configuration gains the full-range path cost (raw
0..200000000, 0 = auto, replacing the old 1000x-scaled byte), a
point-to-point admin control (auto/on/off), and the priority is now a
0..240 step-16 dropdown. A new status table shows, per port, the Port
State, Role, Designated Bridge / Port ID / Cost (learned from received
BPDUs, kept per port and aged via the BPDU age), Operational Edge and
Operational Point-to-Point.
The designated fields fall back to presenting this switch as the
segment's designated bridge when no fresh BPDU has been heard (so a
quiet port shows our own bridge-id, as the vendor UIs do). /stp.json
carries the packed hex fields plus our own MAC for that fallback.
Space: reclaim BANK2 for the above by moving rtl837x_pins to HOME and
compacting leds_dump into a register-address table (~800B); bandwidth
returns to BANK1. No BANK3 - hardware-verified that PSBANK > 2 crashes
this SoC at boot (a bricked unit and an SPI-programmer recovery earlier
today); a warning to that effect is now in rtl837x_lldp.c.
Hardware-verified: cost 200000000 and p2p off round-trip through the CLI
and JSON, the status table populates correctly with STP enabled (all
ports Forwarding/Designated, oper-edge and oper-p2p True), LACP 3f/3f
and the LAN unaffected.
(cherry picked from commit 2ec62072f061dc9e78bc821ba1c297cb6819e206)
Enabling STP on a bridge whose management rides an in-band VLAN can cut
off that very management - and not only by our own blocking: on this
network the upstream TP-Link Easy Smart switch's "loop prevention"
reacted to our BPDU hellos by blocking ITS port towards us while our
ASIC was all-forwarding, isolating the whole segment until a power
cycle. Recoverable only by going quiet.
Add a commit-confirm watchdog: while STP is enabled, any HTTP request
re-arms a countdown ("stp failsafe <seconds>", default 180, 0 disables);
if management stays silent for the whole window, STP disables itself,
which also stops BPDU TX so a neighbour's loop protection can release
its block. The web UI polls /stp.json every 2 s, so an open browser
naturally keeps the watchdog re-armed. The trip is reported via
/stp.json (fs, fsT) and as a warning on the Spanning Tree page.
Deliberately not conditioned on our own MSTP port states - the incident
above proves the uplink can be dead while every local port forwards.
Also bound the NIC DMA busy-waits (nic_tx_packet, nic_rx_header,
nic_rx_packet): an unbounded spin on SFR_NIC_CTRL freezes the entire
main loop (timers, HTTP, ARP) if the ASIC ever fails to consume a
transfer; give up after ~65k polls and drop the frame instead.
Hardware-verified end to end: with priority 15 against a live RSTP
bridge the uplink died 6 s after "stp on" and the network recovered BY
ITSELF 66 s later (trip at 45 s + neighbour release), fsT=1, LACP and
LAN intact. Telemetry via syslog-to-edge-port host confirmed the full
chain: countdown 44->4, trip, hello TX stopping at the trip.
(cherry picked from commit 1fa9775156fd6d7ebfdda2382f73430b86601230)
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 <n> 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)
Add a Spanning Tree page: an on/off toggle driving the existing "stp"
command over /cmd, and a live status section fed by a new /stp.json
endpoint - the elected root bridge (priority + MAC), our path cost,
whether we are the root, and the per-port STP state read live from the
ASIC's MSTP register (same 2-bit encoding stp_setup() writes). Ports are
reported by their physical numbers.
Recovered-from: 3132319, 9365c86
Both the L2 page and the ports page in #335 need to page through /l2.json
and decode the same fields, and the second copy arrived carrying the two
bugs the first one had only just been fixed for. Rather than keep two
copies in step by hand, the transport and the decoding move to main.js,
which every page already loads, and each page keeps only what is its own.
walkL2(onDone) pages through the table once, parses idx and vlan out of
hex, maps the port to a physical number or to 'CPU', and calls
onDone(entries, ok). It stops on a wrapped index, an empty page or 4096
entries, all of which set ok. A page that comes back as anything other than
200, or with a body JSON.parse rejects, is asked for again at the same
index up to three times; only once those run out does the walk end with ok
clear, so a caller can tell a finished table from a partial one. l2.js
keeps the s and l to label mapping, since that needs the page's own
translations, redraws only when ok is set, and restarts the walk from its
callback either way.
Two things change while moving:
The next request goes out from the previous reply rather than from a
setInterval that fires whether or not the last one came back. The httpd
serves one connection at a time, so a timer that outruns the responses only
queues work it cannot use.
A walk that reaches 4096 entries hands over what it collected. Before it
threw the entries away and cleared its own interval, which left the page
unable to refresh again until it was reloaded.
The retry is not a new idea, it is the old behaviour written down. The
previous code ignored anything that was not a 200 and let the interval ask
for the same index again, so a blip never disturbed the table on screen.
Dropping that on the way to a chained walk would have made every timeout
redraw the page with a truncated table, which at one connection at a time
is not a rare event.
Driven with a scripted server in node, running the helper itself rather
than a copy of it: an empty table gives 0 entries in 1 request; three pages
ending in a repeated index give 61 entries in 3 requests, asking for 0, 30
and 60; an empty page ends the walk after 2; a 500 and a malformed body are
each retried at the same index and then complete normally, asking 0, 30, 30
and 31; three failures in a row end the walk with ok clear and the 30
entries already collected; 4096 entries in one page end it with ok set; the
CPU port decodes to 'CPU'; vlan and idx come back as numbers.
main.js grows by 1331 bytes and l2.js loses 1039, so 292 bytes of flash.
Worth stating where they land: main.js is loaded by every page, so pages
that never walk the table now carry the helper too. That is the cost of
having the decoding exist exactly once, which is the point of the move.
l2.js reads the last index of a reply to know where the next page starts.
With the firmware side of this branch an empty table answers [], and
s[s.length-1] then throws on undefined. It used to answer commas with
nothing between them and throw in JSON.parse instead, so this is the same
case reaching a different line rather than a new one. An empty reply now renders what
has been collected and starts the next pass from zero.
The 4096 entry cap compared the array against the number instead of its
length, so it never fired: an empty array and a 5000 element one both
compare false. Comparing the length restores what the check was for.
Review feedback on #306. The value used to ride in /information.json and
the picker fetched /vlanlist separately, so the page needed both requests
to mean anything. /vlanlist now answers {"mgmt":N,"vlan":[...]} and the
picker reads both from the one response. Both consumers in vlan.js were
taught the new shape, and /information.json no longer carries mgmt_vlan.
The truncation guard now reserves 141 bytes instead of 139: the closing
grew to two bytes with the wrapping object, and the comma in front of a
non-first entry was never counted, so the worst case could land one byte
past outbuf even before this change. char_to_html() does not check.
The comments added on this branch are gone as well, style.css and
system.js both, since these files are served byte for byte.
page_impl.rel stays at DSEG 5, OSEG 0, BSEG 3 and the image reports the
same 10183 bytes of XDATA before and after.
`vlan <id> mgmt` existed on the CLI, but nothing reported which VLAN
currently carries management, so the setting was invisible from the web UI -
the only way to find out was to read the startup config.
Report it in information.json and offer it where the other switch-wide
addressing settings live, between Gateway and Language, as a picker filled
from the configured VLANs. When management is untagged there is no VLAN to
select, so that state shows as a disabled entry rather than inventing an id
the switch would reject.
Confirm before applying, and say what will happen rather than echoing the
action back: the switch starts tagging its own frames, and if the port you
came in on does not carry that VLAN the page becomes unreachable and the way
back is the console. Cancelling puts the picker back where it was.
The System Settings rows all pair a label with a form control, except Model,
which is a bare span. The inputs carry padding and a left margin, so their
text starts about 32px further in than the model name did - close enough to
look like a mistake and far enough to see it.
Give read-only values a class with the same metrics instead of styling the
model specifically; any other value shown without a control gets the
alignment for free.
Review feedback on #307: these files are served from flash byte for byte,
so comments ride along on every page load. The three added in this branch
are gone, 357 bytes across l2.js and style.css. The column list existed in
three copies inside renderL2() and is now a single const.
The paint loop reuses existing rows and rewrites port, MAC, VLAN and the
delete button, but never the type cell - only the insert path set it. With a
fixed row order that stayed invisible, since a row usually landed back where
it was. Sorting moves rows, so every other column followed the data while
type kept the previous row's value, which made sorting by type look broken
when the sort itself was correct.
Clicking a heading sorted the table but nothing said so afterwards - the
only feedback was the rows moving, which is no help when the sort key is a
column you are not looking at.
Give every sortable heading a permanent marker: a neutral double arrow when
it is not the sort key, up or down when it is. The marker sits in its own
span so the i18n pass, which replaces the heading text, does not wipe it.
The table lists every learned and static entry in one flat block, which is
fine with a handful and unusable with a few hundred: finding out where one
MAC sits, or what a port has learned, meant reading the whole thing.
Make the column headings sort and give each one a filter box. Filters are
substring matches combined with AND, so "port 8 + static" is two keystrokes.
A counter above the table shows matched out of total, so a filter that hides
everything is obvious rather than looking like an empty table.
Sorting keeps the CPU entry from comparing as a number - it sorts last
instead of landing between ports 8 and 9, where a string-vs-number compare
would otherwise put it.
The filter inputs live inside the existing header cells rather than in a
second row: the paint loop addresses data rows as rows[i+1], and a second
header row would have shifted every one of them.
The port column was mapped from logical to physical numbering first and the
CPU label applied afterwards, testing for port 9. On an 8+1 board the SFP
port maps to physical 9 as well, so every entry learned on the SFP was shown
as CPU - on this switch that was 26 of 30 entries.
Label the CPU while the number is still logical, before the mapping, so the
two cannot collide.
The hostname and model fields were populated through element-existence
checks and empty-string fallbacks that the other fields on the page do
without. Assign them the same way.
Suggested-by: vDorst
machine.machine_name is already reported in /information.json as hw_ver;
surface it read-only on the System Settings page so the exact build
target is visible in the UI, without adding a redundant JSON field.
Add a device hostname settable from the CLI (`hostname <text>`) and the
System Settings page. The value is sanitized on ingest to JSON-safe
printable ASCII (<=23 chars), stored in a shared __xdata buffer, seeded
to "RTLPlayground" at boot, persisted through the startup-config, and
reported in /information.json. It lives in the common header so other
modules can advertise it (LLDP uses it as the System Name TLV).
Two hygiene fixes for the web UI, prompted by a login that appeared to fail
under privacy shields (Brave Shields / NoScript-family extensions):
- httpd: replace the partial "style-src 'self' 'unsafe-inline'" CSP with a
complete, first-party policy (default-src 'self'; script-src 'self'
'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
connect-src 'self'; form-action 'self'). Everything the UI needs is
same-origin; the explicit policy stops shields injecting their own
restrictive report-only probes (the noisy script-src-elem 'none' console
spam) and passes a strict-CSP audit. Verified: no CSP violations in-browser.
- login.html: add autocomplete="current-password" so password managers
recognise the field (they showed "unknown password" without it).
NOTE: these do NOT bypass a browser's LAN-device protection (NoScript "lan" /
Brave Shields), which strips the POST body of requests to a LAN address and is
why the login can fail in-browser while the same credentials work over curl.
That is a deliberate browser security feature; the user must allow the site in
their shields to log in. The backend password (default 1234) is unchanged and
correct.
Add a language selector to the System Settings page supporting English,
Japanese, and Chinese. The i18n dictionary already contained zh
translations but lacked the UI to switch languages.
Changes:
- html/i18n.js: add sys_language key to en/ja/zh
- html/system.html: add language selector dropdown
- html/system.js: add changeLang() and selector initialization
- tools/httpd_sim.c: fix cookie parsing for multi-cookie headers;
fix Set-Cookie response (missing Path=/, missing \r\n\r\n);
add SO_REUSEADDR for faster port reuse
A bare "vlan N" line never matches conf_cmds (a VLAN entry always carries
a member port or the mgmt/name keyword), so it is never stored. The
c !== "vlan N" comparison could therefore never match an entry; the
startsWith(prefix) check alone covers all stored vlan lines.
The firmware has a single management VLAN (one management_vlan variable),
so a stored config should never carry more than one "vlan N mgmt" line.
When parsing a mgmt command, drop any previously stored mgmt entry so
repeatedly changing the management VLAN no longer accumulates stale lines.
parseConf() only recognized "port N name" in conf_cmds, so "port N <speed>"
lines were treated as unknown commands and dropped on save - configured port
speeds never persisted.
Add the speed command (incl. optional half/full duplex suffix) to conf_cmds,
add a "port N" entry to conf_overwrite so re-saving a speed replaces the
previous value, and guard the per-port name entry so changing a port's speed
no longer wipes its configured name.
The vlan/mgmt lookahead split (commit dcc60c3) correctly separated the
patterns so that `vlan N mgmt` and `vlan N <members>` match different
conf_overwrite entries. But the filter that applies the overwrite still
used `item.startsWith(matchStr + " ")` with matchStr = "vlan N", which
also matches "vlan N mgmt" — so the management entry was removed whenever
the VLAN's membership definition was re-saved.
Example: with both `vlan 44 management 2t 4t 5 6t` and `vlan 44 mgmt`
in the config, changing the membership dropped `vlan 44 mgmt` entirely.
Added a `!item.endsWith(" mgmt")` guard so management entries survive the
filter. No reordering is needed: `vlan N mgmt` only sets the global
management_vlan variable (cmd_parser.c) and does not depend on the VLAN
table entry existing, so its position in the config is irrelevant.
The pattern /^vlan\s+\d{1,4}\b/ matched both VLAN membership entries
(vlan N <ports>) and management entries (vlan N mgmt), causing them
to overwrite each other in configuration[]. When saving, whichever
form came last in the cmd_log would dedupe the other out of the
final config — resulting in either lost membership or a stale mgmt
setting.
Split into two patterns using negative lookahead:
- /^vlan\s+\d{1,4}\s+mgmt$/ matches only mgmt entries
- /^vlan\s+\d{1,4}(?!\s+mgmt\b)/ matches everything else
Both dedup independently. Discovered via hardware test on 6XH-X
where 'vlan 44 mgmt' silently dropped 'vlan 44 management 2t 5u'
from saved config, locking out web UI on next boot.
Multiple related bugs in the Save-to-Flash path:
1. Multipart upload was missing the required filename argument,
causing the backend parser to fail. Added 'config.txt' to the
form.append call. This was likely the primary reason Save-to-Flash
was unreliable.
2. Web UI was not pausing its polling interval during flash write,
causing CPU contention and intermittent crashes. Added isSaving
lock and clearInterval before sendConfig.
3. conf_cmds whitelist was incomplete. Added: syslog, passwd, pvid,
ingress, port name, lag, laghash, isolate, stp, igmp, mtu, bw,
vlan N mgmt, vlan N d.
4. VLAN regex blocked named VLANs. New pattern allows optional name
(starts with letter, matching CLI parser semantics).
5. vlan N d (delete) was not persisted. parseConf now removes the
matching vlan N ... entry from configuration[] when seeing a
delete command, without storing the delete itself. Result: the
saved config describes the end state.
6. configuration[] was not cleared between flashSave invocations,
leading to stale entries from prior interactions.
7. conf_overwrite boundary fix: 'pvid 1' no longer matches 'pvid 10'
etc. Added trailing space in startsWith check.
8. All conf_cmds patterns now anchored with ^...$ for full-line
match. parseConf normalizes whitespace before testing.
9. Port range widened to \d{1,2} so ports 11+ are accepted.
Structural fixes (1, 2, 6, 7, 8) ported from mcaptur's closed PR #219;
remaining fixes (3, 4, 5, 9) and overall regex strategy are new.
The hardware bitmask format encodes both "untagged members" and
"non-members" in the upper 10 bits (per doc/vlan.md). The existing
fetchVLAN() correctly masks this with the membership bitmask before
display, but loadVlanTable() did not, causing non-member ports to
appear in the Untagged Ports column.
Tested on KeepLiNK KP-9000-6XH-X.
Below the configuration form, a new table lists all configured VLANs
with their port memberships: Member, Tagged, Untagged, and PVID
columns. Port ranges are formatted compactly (e.g. "1-2,5").
Port-to-bit mapping uses the existing physToLogPort[] array populated
by /status.json, so the table is consistent with the icon view in
fetchVLAN() and works on any supported machine.
Each row has a delete button that sends "vlan N d" via /cmd, with
a confirmation dialog. VLAN 1 cannot be deleted (no button shown).
Table and dropdown both refresh automatically after Update/Create
and after delete via a new refreshVlanViews() helper.
The N+1 request pattern (1x /vlanlist + Nx /vlan.json) keeps the
backend simple and avoids buffer overflow risks for systems with
many VLANs. For typical configurations (<50 VLANs), page load
remains under one second.
A new <select> element above the VLAN ID input lets users pick an
existing VLAN by name instead of typing the ID. Options are loaded
from /vlanlist on page load and re-loaded after successful
Update/Create operations.
Selection triggers the existing fetchVLAN() flow.
Falls back gracefully if /vlanlist returns an empty list or fails:
the dropdown is hidden and the existing ID input remains functional.