POST /api/room checked the room password on every request with no failure
limit, and a browser may send a new identity token each time. Any host that
reached WebConfig could guess the room admin password, which is also the
radio and /api/login admin password, at main-loop speed.
One budget now covers all browser tokens, with /api/login's numbers: at most
five distinct wrong passwords in each thirty-second window. After that, a
request carrying a password is refused without checking it until the window
ends. Wrong passwords count in read-only rooms too. Empty passwords are never
counted or refused, so public read-only access keeps working. The last four
wrong passwords are remembered by 8-byte SHA-256 digest and not counted
again, so a tab still polling with an old or mistyped password cannot use up
the budget. Successful sign-ins do not refund failures. Radio login is
unchanged.
test_room_web_service.py gains PASSWORD_LIMIT, which runs the real
processRoomRequest: refusal after five failures from five tokens; identical
replies for wrong, guest and admin passwords while refused, with no password
check (calls to authorizeRoomLogin are counted); no lock extension from 300
refused requests; the 29999/30000 ms boundary and the first window after
boot; read-only and empty-password cases; no refund on success; repeated,
rotating and read-only stale passwords; slow distinct failures ageing out;
and a guard that the digest ring stays in bounds. It fails on the previous
processRoomRequest at the sixth guess.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a room server the guest password grants Read/Write, which the login
replay store treats as a role that needs a durable record. Every new
guest-password identity therefore took one of the 128 permanent slots,
and once they were used up the store failed closed for every new
identity, including a new administrator device, until the filesystem was
erased. Rooms often publish the guest password.
A guest-password Read/Write session (one without explicitly assigned
permissions) now uses the policy public readers already use: it must
beat its in-boot timestamp and any record the identity already has,
which is still advanced, but it never creates a new record.
Administrator logins and explicitly assigned roles keep the durable
reservation and still fail closed on a full store.
Tests: test_room_login_replay_capacity.py runs the real room login
handler against the real ClientACL replay store with ASan/UBSan: guest
churn cannot lock out a new admin or assigned writer, a former admin's
record still binds guest-password logins, and a store full of privileged
records still refuses a new admin. It fails 2 of 3 on the previous code.
Two expectations in test_room_login_integration.py and the login profile
contract follow the new policy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integrate upstream 1.18.0, delayed direct ACK forwarding, zero-scope routing, Waveshare LR1121 and dynamic WiFi variants. Preserve fork chemistry-aware power management and bounded multi-interface UI; retain retry/dedup safety while adding upstream ACK delay. Qualify LR1121 band power and HP-only Morse handling with native regressions.
Share one bounded WiFi outage and recovery schedule across MQTT, automatic NTP, Companion station ownership and WebConfig. Preserve enabled settings and foreground access while allowing normal sleep after confirmed physical shutdown.
Honor failed SDK client stops before handing off WiFi, protect clean teardown and reconfiguration, and retry transient ESP-NOW channel restoration without repeated disconnects. Add production-method, SDK failure and real sleep regressions to CI.
Validation: native MQTT/NTP/clock/reconnect suites; production power and wrapper failure tests; browser, room HTTP, OTA and combined bridge regressions; Full Heltec V4 repeater, room and companion builds with capability, flash and linked-memory checks.
Add opt-in bounded chat history, persist explicit room ACL roles, enforce full-key bans and per-user quotas, and provide versioned information-board reads plus an ESP32 local room page. Preserve stored evidence on faults and withhold room delivery acknowledgments when a Companion cannot retain the message.
Exercise production handlers and filesystem fault recovery, pinned official-app JavaScript, real Chromium interactions, and representative ESP32, nRF52, RP2040 and STM32 builds. Add regression suites and bounded browser-workspace RAM reservations to CI.
Allow unknown blank-password guests only when read-only access is enabled,
disable empty configured credentials, and preserve explicit ACL roles on
reconnect unless a valid admin password selects Admin.
Save room topics through verified, recoverable filesystem transactions. Deliver
topics after older unread posts with independent ACK state and bounded retries,
including clock correction and long-uptime timer handling.
Exercise production handlers and pinned unmodified app JavaScript in CI.
Document topic commands and login policy. Native tests (1829), fault-injection
and compatibility suites, Pico W, V4 Full, G2 Full, and RAK4631 Full/Reduced
build qualification passed. Hardware behavior remains untested for this change.
Reject truncated anonymous reply paths and advert metadata before copying,
while preserving timestamp-only empty-password logins. Limit room posting to
read-write/admin ACL roles, retain inbox frames until the requesting transport
accepts them, and lock raw LittleFS traversal against concurrent writes.
Separate ISR event generations from main-loop radio state. Confirm RX/TX IRQs,
detach concrete wrapper callbacks safely, and discard inconsistent LR2021 FIFO
snapshots before rearming. Preserve bridge RX frames through packet-pool
exhaustion and bound ESP-NOW retries and missing-callback recovery. Screen RTC
register layouts without losing DS1307 startup, and ignore poisoned future
contact timestamps during clock bootstrap.
Keep the nRF52 loop stack at 8 KiB while reducing the OTA listing call chain
from 7,832 to 6,056 static bytes. Retain all target names through lossless Full
Companion compression and the canonical size optimizer. Preserve constrained
STM32 filesystems and bind corrected ESP32 UART/OLED budgets to source size
assertions. Document the separately measured preexisting W12 package limits.
Adapted for this branch from meshcore-dev/MeshCore PRs #3521, #3340, #3362,
#2062, #3503, #3038, #3534, #3544 and #3512.
Validation: 1,829 native tests; sanitized parser, inbox, LittleFS, ACL, clock,
RTC, IRQ/FIFO and bridge fault tests; 332 additional Companion cases; real
RAK4631 Full and both repeater profiles, G2 Full/standard, V3 Full and both
constrained STM32 release recipes. Radio-family compile coverage continues
separately; W12 full/portable package guards remain enforced.
Enroll a private channel and publisher public key on repeaters and room servers. Local Companion fleet sends sign targeted or whole-fleet commands, with expiry, durable replay protection, and a restricted command allowlist. Preserve exact transmitted-reply barriers for secondary-radio changes, bound validation and replies, and qualify crypto, persistence, transport scope, role permissions, and compact-profile exclusions with regression tests.
Bound ACL replies for encrypted routes, authorize Sensor ACL access using the actual Admin role, and preserve request retries after failed queue admission. Validate stored paths for replies, while keeping Room keep-alives direct-only.
Add both G3 RF-slot Sensor profiles, suppress battery alarms on unavailable measurements, and remove the unsupported Sensor WebConfig capability. Add executable regression coverage and CI checks.
The hostname was built once in begin() from the node name, so `set name` left
the node advertising its old name to DHCP until the next reflash. It is now
rebuilt on rename through a new CommonCLICallbacks::onNodeNameChanged() hook,
and begin() shares the same helper.
What this cannot do is rename a live lease. IDF 4.4 states that a hostname
changed after the interface is up "would only be reflected once the interface
restarts/reconnects", and Arduino 2.x's WiFi.setHostname() does not touch a
running netif at all — it writes a static string that the netif reads at
bring-up. Forcing the issue would mean bouncing the link, which costs every
MQTT slot a reconnect; that is not a reasonable price for a rename, so the new
name lands at the next reconnect or boot instead.
Only automatic (Ethernet-preferred) links carry a hostname today, exactly as
before. Giving plain Wi-Fi observers one would change the DHCP identity of the
existing fleet and is a separate decision.
#54 lands first, so the network abstraction is reworked on top of the
per-slot client ownership model instead of alongside it.
Two resolutions are semantic, not textual:
- Link-transition teardown goes through the ownership API. It called
softDisconnect() on every started client, which bypassed client_state and
ignored the typed result. A slot that is still Starting now gets a real
stop: softDisconnect() returns immediately on a client that is not yet
connected, so its attempt would otherwise complete against the old route and
deliver a CONNECTED event indistinguishable from the new one's (F04). On a
medium switch the old route can still be briefly usable, so that is not
hypothetical. A connected client keeps the cheap bounded path, and a
quarantined one is left alone.
- NetworkLink::applyPowerPrefs() adopts WifiPowerSavePolicy. Its local
`2 ? MAX : NONE` mapping would have reintroduced F11 and read the new stored
value 3 (explicit `min`) as none. It also applies the setting when the link
starts already associated, which is the case the bridge used to cover.
The NTP probe from #54 keeps its validation and its one-attempt-per-server
bound, but resolves and gates on the selected link rather than on WiFi, so it
works on an Ethernet-preferred node.
- The room server checked canFlashAfterStop() only when the bridge was
running at OTA time. After a timed-out restart the bridge reads as
stopped while its unacknowledged task may still own TLS/client state,
so an OTA could erase and write flash under it. It now refuses while
the stop is unproven, after first reaping any late ack. The repeater
already gated unconditionally.
- MyMesh::loop() now reaps a late stop acknowledgement whenever it lands,
releasing the withheld queue and buffers, and restarts only when a
resume is pending and the bridge is enabled. Before, a bridge disabled
during StopUnproven kept those resources until re-enabled or rebooted.
pollLateStopAck() is public for this.
- The wrapper's destructor no longer stops an already-stopped client:
destroySlotClients() had just stopped it, so every shutdown logged five
spurious "esp_mqtt_client_stop failed: ESP_FAIL" errors.
Hardware (Heltec V4, 1 s test stop deadline): restart -> StopUnproven ->
`set bridge.enabled off`; the late ack was reaped ("releasing withheld
resources"), status read "not running", and `set bridge.enabled on`
started cleanly without a second release.
setBridgeState(false) returned early when the bridge was already down,
so `set bridge.enabled off` after a timed-out restart left the resume
pending and MQTT restarted against the operator's choice once the task
acknowledged. Disabling now clears the pending resume before that early
return, and the resume also requires bridge_enabled. The StopUnproven
log and `get mqtt.status` no longer say only "reboot to recover", since
a late ack now recovers on its own.
Hardware (Heltec V4, 5 live slots, 1 s test stop deadline): a restart
went StopUnproven, the task acknowledged 27 s later and the bridge
resumed with all slots reconnecting; with `set bridge.enabled off` in
between it stayed stopped until `set bridge.enabled on`.
- A stop that timed out into StopUnproven refused begin(), and only a
later begin() honoured the task's late acknowledgement. Nothing called
it, so restartBridge() (any `set mqtt...` restart) or an aborted OTA left
the observer offline for the rest of the boot. MyMesh now records that a
start was refused and restarts the bridge once stopAcknowledgedLate()
reports the ack. The OTA-abort alert no longer claims "bridge resumed"
when the restart was refused.
- refreshNTP() now runs the validated probe with one attempt per server.
With two attempts and a 1 s pause it blocked the MQTT task ~18 s every
hour on networks that drop UDP/123, where the old async SNTP cost
nothing. Loop comment and docs updated to match.