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".
Moving the global on/off onto the LLDP page
Building a table of LLDP status from the S ports. Later on, if
we wish to implement an lldp receiver we can display the remote
details here
Adding support to the cmd parser for the lldp commands
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.
The server timeout is an inactivity timeout: scan_header() refreshes
last_session_use on every authenticated request. Max-Age counts from
login instead, and the cookie is only set there, so the browser deletes
it session_timeout seconds after logging in however busy the session
is. Leave expiry to the server, which already handles it.
The five-digit itoa16_html() stays: information.json must print
timeouts from 10000 to 65535 correctly.
edc469f made max_upload count, which bounds how many bytes a request may
carry. Nothing bounds where those bytes land. The two are not the same
check: the first watches the request, the second watches the flash address.
uptr walks forward one page at a time from FIRMWARE_UPLOAD_START and has no
ceiling. A body whose closing boundary never arrives keeps writing page after
page past the staging region, and on a flash exactly twice FIRMWARE_UPLOAD_START
the address wraps onto the running image at zero, so a bad upload takes the
working firmware with it. That commit's own message notes the related hazard:
flash_write_bytes() carries the address in three bytes and drops what does not
fit.
Bound it where uptr actually moves: refuse the next page once it would leave
the area check_and_flash_update_image() reads back, and answer 400. A partial
image left behind is harmless - boot rejects it on CRC and erases it.
This rebase drops the part of the original patch that removed max_upload;
the length limit and the address limit now sit side by side.
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.
max_upload was armed once when the upload began and never reduced, so
`max_upload - uip_len > 0` was an unsigned expression that only went
false when a segment arrived whose length matched the limit exactly. The
write pointer climbed from the staging address with nothing to stop it,
while flash_write_bytes() carries the address in three bytes and drops
whatever does not fit.
Subtract each segment as it is taken and compare before subtracting, which
is what the comment there described. The limit was only ever armed for
firmware uploads; configuration uploads keep the bound their own buffer
already imposes.
The multipart branch for /config tests authenticated before it touches
anything; the branch for /upload does not, and the 401 for it is only
raised further down at the dispatch. By then the request has already set
config_upload, uptr, verify_crc, max_upload and pre_acc, and announced a
firmware upload on the console.
Those five are globals while the transfer state they steer is per
connection, and the stack is built for ten connections, so a request that
is about to be rejected can still retarget an upload another client is in
the middle of: a configuration upload continues as a firmware one and its
remaining parts are written to the staging area instead of the
configuration sector.
Apply the same test the neighbouring branch uses, before the assignments.
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.
The replay takes a line of at most 126 bytes, so a longer one is skipped
on boot. Neither the upload path nor the editor looked at line length,
so the file the user saved was not the configuration the switch would
come up with.
Reject such a body in config_take(), before the sector is erased, which
the upload path already maps to 400. The editor points at the offending
line and its length, and measures it in bytes rather than characters, so
a name written in a non-ASCII script is counted the way the switch
counts it.
uip_mss() is the limit on the next segment, not a record of the last
one: uIP recomputes it from the window the peer advertises in every
incoming segment, before the application is called. The ACK path
subtracted it from slen and added it to o_idx, and the retransmit path
handed it to uip_send(), so whenever the window moved between a segment
going out and its ACK coming back, the file being served lost or
repeated the difference. A window shrinking from 1460 to 600 between the
two repeats 860 bytes under fresh sequence numbers, which a browser
writes into the file; a window opening the same way skips 860.
Remember the length in tx_send() and use it in both places. The
retransmit path then repeats exactly the segment uIP still counts as
outstanding, which is what uip_connr->len makes it put on the wire.
uip_appdata is void *, so p - uip_appdata subtracts pointers of
different types. SDCC accepts it, C does not, and no host compiler will
build the file for a unit test. Cast to the type the byte count is meant
in; the generated firmware code is unchanged.
The session cookie carried neither Expires nor Max-Age, so browsers that
clean up session cookies aggressively (LibreWolf, for one) may drop it
while the session is still valid. Send Max-Age=session_timeout so the
declared lifetime matches the timeout already enforced by the firmware.
itoa16_html() only converted four digits, so extend it to five to print
timeouts up to 65535. That also fixes information.json, which used the
same helper and reported wrong values from 10000 s up.
handle_post() takes the request path from byte 6 of the segment it is
given. For the first segment of a POST that is the path, but for a later
segment of a multipart upload it is whatever part of the file happens to
sit there. The cmd and login handlers were matched before the multipart
state was checked, so a segment of a config or firmware upload with
"cmd " at that offset ran the rest of the segment as CLI commands, and
one with "login " there aborted the upload with 400.
Check for an upload in progress, and for new upload and config requests,
before looking at cmd and login. A new POST cannot match both sets of
words, so nothing changes for requests that are not uploads.
session_id is cleared at boot and only a successful login fills it in,
while the uptime counter and last_session_use both start at 0. For the
first SESSION_TIMEOUT seconds after a start, a request carrying an empty
"Cookie: session=" therefore passes is_word_x(session, session_id) and
is treated as logged in, which is enough for /cmd, /config and /upload.
Only look at the cookie once a session has been handed out.
The session timeout was a compile-time constant (200 s), so changing it
meant rebuilding the firmware. Make it a variable that keeps the same
default and add a "session <seconds>" command, so it can be set from
the console or from the startup configuration like the other settings.
Measured on KP_9000_9XHML_X_V3_1: ROM +180 bytes (BANK1 +17, BANK2
+161) and xdata +2 bytes; the internal RAM map is unchanged.
The pages become one index.html holding every section, one app.js and a
self-contained login.html. The layout is a sidebar with light, dark and
Selenized themes (or follow the OS), a live dashboard with per-port
packet rates, and confirmation dialogs in front of the actions that can
lock the operator out.
Behaviour changes against the previous pages:
- every request goes through one queue that waits for the previous
response BODY rather than its headers: the firmware shares a single
output buffer across connections and delimits responses by closing,
so a second request in flight corrupts the first
- "Save to flash" merges the command log into the startup configuration
with the grammar the console accepts, shows the result before writing,
verifies the sector by reading it back and clears the command log only
after that; the header shows an "unsaved changes" badge while the
running configuration differs from flash
- the console section shows the output the firmware returns for a
command, including the parser's explanation on a rejected one
- the firmware update section checks size, bank header and CRC16 of the
image in the browser before uploading, shows upload progress and waits
for the switch to come back after the reset
- the ports table shows the learned MAC of a port when there is exactly
one and the count otherwise; the MAC table sorts per column, filters,
and names the LAG a learned address sits behind
- the spanning tree section sends only the settings that changed on
Apply instead of one command per control event
- link aggregation groups are numbered 1-4 like the command line, and
applying an empty group clears it
The translation table is inline in app.js with English, Japanese and
Chinese. The language selector sits in the page header and reloads the
page so that dynamically built tables are rebuilt in the new language.
Only login.html and the favicon are served before authentication; the
stylesheet, the port images and the old script bundle no longer exist.
The simulator's allowlist follows the firmware's.
The session timer was refreshed only when a page from the file table was
served, so a client that loads its pages once and then talks to the JSON
endpoints alone is logged out SESSION_TIMEOUT seconds after the load,
however busy it is. A firmware upload started after that point is answered
with 401 instead of a verdict. Refresh the timer wherever the session
cookie validates, which covers every authenticated request, and drop the
file-serving refresh that this makes redundant.
The lookup tables live in BANK1, so the routine switches the code bank to
reach them with movc and never switches it back. That is the contract the
__naked declaration already implied: same bank or common segment only.
The name now says it.
crc16_bank1 is the routine unchanged. crc16 becomes a __banked wrapper in
BANK1, so a caller from another bank has its own bank put back by
__sdcc_banked_ret. The two existing call sites take the direct path and
pay nothing.
A file larger than the output buffer is sent in two phases: the first
buffer goes out in MSS-sized pieces, with o_idx walking through it, and
what is left is then streamed from flash a chunk at a time into outbuf
from offset 0.
The continuation branch never reset o_idx, so it kept the offset the first
phase had left it at, while uip_rexmit() retransmits from outbuf + o_idx.
A lost continuation segment was therefore retransmitted as the bytes 1460
further into the buffer, which the client accepts as the missing data, and
with o_idx + slen past the 2500-byte buffer it also read whatever xdata
follows outbuf.
Reproduced on a SWTGW218AS by dropping 15% of the switch's TCP segments:
seven of eight fetches of a 21 kB static came back the right length and
the wrong content, each first differing at a fixed phase of the 1460-byte
chunk. With o_idx reset the retransmission repeats the chunk it sent.
The o_idx += slen right after slen = 0 went too: it added nothing.
(cherry picked from commit 789d29423288f89411a41673e26ce1854b296c3d)
uip_init() does not set uip_conn, and httpd_init() runs right after it, so
the assignment went through a pointer nothing had assigned yet.
It has stayed invisible because the startup code does clear xdata: uip_conn
is NULL rather than garbage, appstate sits 28 bytes into struct uip_conn, and
the byte therefore lands at xdata 0x1C. With the current map that is inside
the command line buffer, at an offset nothing reads while the line is empty,
and cmd_editor_init() runs later anyway.
Nothing is broken today, but it is a write to an address that has nothing to
do with the state being set, and it moves the moment the struct grows or the
map shifts. Address the connection directly.
* uip: time out an ESTABLISHED connection whose peer went silent
uIP has no keepalive and no idle timeout for an ESTABLISHED connection, so
a peer that vanishes without FIN or RST holds it open forever. A crashed
client does it, and so does a SYN caught while the switch is still
booting. With UIP_CONF_MAX_CONNECTIONS = 1 that wedges the single slot:
the web UI stops answering, port 80 no longer even SYN-ACKs, and only a
power cycle recovers it. Every other TCP state already has an exit path
(UIP_MAXRTX, UIP_TIME_WAIT_TIMEOUT); idle ESTABLISHED was the one hole.
Close it inside uIP rather than in httpd, so any future TCP service is
covered too. The retransmission timer is unused while nothing is in
flight, so the idle count lives in it: a 1 Hz strobe derived from the
uip_periodic() rate ages it, any arriving segment clears it, and past the
limit the connection is aborted through the same path as UIP_MAXRTX. Two
new options in uipopt.h, default off; this project enables 30 s.
httpd also joins uip_timedout() to its cleanup: it only handled closed
and aborted, so both this timeout and the existing UIP_MAXRTX one would
have left the transfer state stuck on a dead connection.
* uip: keep the tuning values out of the prose
The comment named the timeout and the tick rate, which go stale the
moment either constant is edited. It now says why an idle connection
is aged out and leaves the numbers to the defines under it.
The /cmd path carried its own copy of the plain-text response header,
which was the same string page_impl.c already keeps in
HTTP_RESPONCE_TXT, minus one difference: the shared one never said
Connection: close.
This httpd closes after every response, so a reply that does not
advertise it invites the browser to reuse the socket and lose the next
request. HTTP_RESPONCE_JSON has always said so; the text one was the
odd one out, and send_config() and send_cmd_log() were answering
without it.
The copy is gone and the shared string carries the header. The check
for a command that printed nothing now compares against the length
strtox() returned instead of sizeof() on the macro.
Three ways the answer could mislead the caller, all in the paths that
matter most when something is wrong.
The capture sat in write_char(), so anything printed through
print_string_no_syslog() escaped it. That is exactly the replies of the
syslog commands, which deliberately avoid the syslog path: "syslog on"
answered with the bare address and no sentence around it, "syslog off"
answered with nothing at all. Moving the capture one level down, into
write_char_no_syslog(), catches both paths and costs nothing, since
write_char() reaches it anyway.
On a parse error send_bad_request() rewrote the buffer from the start,
throwing away the very text that explains the error and replacing it
with the 400 HTML page. Keeping the captured text and restating only the
status keeps the parser's own message; "400 NO" is as long as "200 OK",
so the header does not have to be rebuilt.
An overlong listing was cut at the buffer end with nothing to show for
it, so a clipped answer read as a short one. It now ends in a marker,
and the reserved room is accounted for in the guard.
While here: a command that configures something prints nothing, and an
empty body reads the same as "nothing ran", so a successful silent
command now answers "OK".
No internal RAM and no xdata; 174 bytes of BANK1, which has 7356 left.
POST /cmd runs the command and answers with an empty 200, so a caller cannot
tell "done" from "nothing happened", and whatever the command prints only
reaches the serial console. Reading a register over the network needs a second
channel just to see the answer.
write_char() is the one funnel every console helper goes through, and it already
tees into the syslog buffer. A third sink, live only while the httpd runs a
command that arrived over the network, puts the same bytes into the response
behind the header. Serial and syslog keep getting their copy.
The capture stops at the end of the response buffer, so a long listing is
truncated rather than overrunning it.
BANK1 grows 39 bytes and the common area 52. One byte of xdata, BANK2 unchanged.
login.html asks for i18n.js, but the file is not on the short list a
client may have before it authenticates, so the request is answered with
a redirect back to the login page and the strings stay as the English
that is baked into the markup.
The file carries only the interface text, the same text the page already
ships untranslated, so serving it before login gives nothing away.
A POST /cmd whose body arrives in a separate TCP segment from the headers
executed nothing: handle_post() read the body from the segment that
carried the request line, found it empty, and answered 200 OK. Python's
urllib and requests both write headers and body separately, so every
scripted command from those clients became a silent no-op. POST /login had
the same hole. The multipart endpoints were fixed earlier; this covers the
plain ones.
When the first segment holds fewer body bytes than Content-Length
announces, the bytes accumulate in config_buf (idle outside multipart
uploads) and the connection waits in TSTATE_POSTBODY; once the announced
length is in, the endpoint runs against the buffer. The /cmd and /login
executions move into run_cmd_body() and run_login_body() so both paths
share them; the 200 OK builder they duplicated becomes send_ok(), and
run_login_body() checks the pwd= prefix instead of skipping four bytes
blindly.
The announced length is the contract on both paths. A body that arrives
with the headers is terminated at Content-Length before it runs, so
pipelined bytes after it are not executed as commands the way the old
code did. A request without a usable Content-Length answers 411: without
one there is no way to know when the body has arrived, and the old 200
OK for an empty body is the bug this fixes. Bodies announced at or above
the buffer size answer 400 before anything is buffered.
The wait state has a deadline. uIP is built with a single connection,
and an ESTABLISHED connection with nothing in flight never times out on
its own, so headers followed by silence (a script interrupted, a link
dropped mid-request, or someone holding the socket on purpose) would
otherwise keep the slot until reboot; POST /login reaches the wait
before authentication. The poll handler aborts the connection when the
body has made no progress for five seconds, measured on the 200 Hz
system tick rather than on poll count, which runs faster under interrupt
load.
Record the announced body length alongside Content-Type and the session
cookie. The value goes through parse_short(), so an announcement that does
not fit sixteen bits saturates at 0xffff rather than wrapping to something
the size checks would accept, and a missing or unparseable header reads as 0.
Nothing consumes it yet; the plain POST endpoints take it up next.
The scan stepped over the NUL that ends the received data before breaking,
so on a request whose headers were cut short it returned a pointer one past
the data. handle_post() then tested that byte for the end of the header,
reading whatever an earlier packet had left there instead of the NUL the
appcall wrote, and could go on to run an endpoint against the stale bytes.
Break before advancing so the returned pointer sits on the NUL and the
caller's check sees it.
The digit loop accumulated into a uint16_t without a bound, so a query
such as /vlan.json?vid=65540 read as VLAN 4 and every consumer saw a small,
valid-looking number for an out-of-range one. Clamp the result at 0xffff
once another digit would overflow (6552 * 10 + 9 is the last value that
fits). The consumers already reject or mask 0xffff: vlan_get() refuses
anything from 4095 up, send_l2() masks the index to the table size, and
l2_delete() masks the high byte.
scan_header() found Content-Type and Cookie with is_word(), which compares
bytes exactly and requires a separator after the pattern. Field names are
case-insensitive (RFC 7230 section 3.2), and the whitespace after the colon
is optional, so "content-type: multipart/..." and "Content-Type:multipart/..."
were both treated as absent. The value pointers were then fixed offsets that
assumed exactly one space.
Add header_value(), which matches a lower-case name anchored at the line
start, folds the request bytes to lower case as it compares, and returns
the start of the value past any blanks, so a call site no longer adds the
name length by hand. Cookie scanning reuses the returned pointer, and the
end-of-header test becomes the strstart() the file already has.
The tail of handle_post() interleaved the config and firmware upload
paths behind one config_upload conditional. Give each path its own
function, handle_config_fragment() and handle_firmware_fragment(), and
reduce handle_post() to routing.
Both handlers keep their own copy of the buffer-and-bounds-check
prologue: with SDCC a shared helper costs 2 bytes of direct RAM for
the fragment pointer (a spill slot when the parameter lives in xdata,
a DSEG home when it does not), and direct RAM is fully allocated. The
memory layout is unchanged from the pre-split code.
cfg_pos, cfg_hdr, cfg_body, cfg_end, cfg_last and cfg_bl are used only
by config_take(), so move them out of file scope and into the function.
The five cursors stay static: SDCC register-caches automatic xdata
locals and spills them through DSEG slots, which costs 6 bytes of direct
RAM this build has no room for; static keeps the access pattern of the
old globals and the memory layout is unchanged. cfg_bl produces no spill
slot at the current register pressure, so it is a plain local. The
mechanism is recorded in issue #386.
handle_post() reused cfg_pos and cfg_end as scratch for unrelated
values; those uses get their own named locals, frag_len and
payload_start.
Both conflicts are the register keyword removal meeting a change of ours,
so each takes main's signature and keeps what the branch was saying:
nic_rx_packet() still returns a bool, and port_ingress_filter_get() stays
declared.
main has since put its own copy of the STP module in BANK2, so the pragma
this branch carried is now duplicated. The one with main's comment stays.