Finalize USB watchdog management format as MGR2

This commit is contained in:
mikecarper committed 2026-10-03 16:34:37 -07:00
1 parent eeb25a8b5f
commit 5ee6059564
13 files changed
+269 -129

No files matched your search

+9 -12
View File
@@ -7,12 +7,10 @@
// which Web Crypto exposes directly. AES-CBC with a zero IV supplies the
// required AES block primitive; only its first ciphertext block is used.
const HEADER = 83;
const MGR2_HEADER = 98;
const CURRENT_HEADER = 111;
const TAG = 16;
const ENTRY = 13;
const PER_PAGE = 6;
const MGR2_PER_PAGE = 5;
const CURRENT_PER_PAGE = 4;
const MAX_KEYS = 36;
const ZERO_BLOCK = new Uint8Array(16);
@@ -221,17 +219,16 @@
function layout(payload) {
const magic = bytesToAscii(payload, 0, 4);
if (magic === "MGR1") return { header: HEADER, perPage: PER_PAGE };
if (magic === "MGR2") return { header: MGR2_HEADER, perPage: MGR2_PER_PAGE };
if (magic === "MGR3") return { header: CURRENT_HEADER, perPage: CURRENT_PER_PAGE };
if (magic === "MGR2") return { header: CURRENT_HEADER, perPage: CURRENT_PER_PAGE };
return null;
}
function canonicalLength(payload) {
const format = layout(payload);
if (!format || payload.length < format.header + TAG) {
managementError("This data does not begin with a complete MGR1/MGR2/MGR3 management page.");
managementError("This data does not begin with a complete MGR1/MGR2 management page.");
}
if (format.header >= MGR2_HEADER && ((uint16LE(payload, 83) & 0xf800) || (payload[85] & 0xc0))) {
if (format.header === CURRENT_HEADER && ((uint16LE(payload, 83) & 0xf800) || (payload[85] & 0xc0))) {
managementError("Reserved USB status bits must be zero.");
}
if (format.header === CURRENT_HEADER) usbWatchdogEvent(payload);
@@ -306,7 +303,7 @@
function inputByteStreams(input) {
if (typeof input !== "string" || input.trim() === "") {
managementError("Paste one or more MGR1/MGR2/MGR3 payloads or GroupData packet hex values first.");
managementError("Paste one or more MGR1/MGR2 payloads or GroupData packet hex values first.");
}
if (input.length > 32768) managementError("The pasted value is too large to be management-report data.");
// A wrapped single page is one candidate, not an invalid first-line
@@ -359,7 +356,7 @@
}
}
if (!found.length) {
managementError("No complete management page was found. Paste canonical MGR1/MGR2/MGR3 payload hex or an entire GroupData packet.");
managementError("No complete management page was found. Paste canonical MGR1/MGR2 payload hex or an entire GroupData packet.");
}
return found;
}
@@ -393,7 +390,7 @@
}
function usbStatus(payload) {
if (!["MGR2", "MGR3"].includes(bytesToAscii(payload, 0, 4))) return null;
if (bytesToAscii(payload, 0, 4) !== "MGR2") return null;
const flags = uint16LE(payload, 83);
const names = ["supported", "loggingEnabled", "watchdogEnabled", "hostConnected",
"readerConnected", "stalled", "recovering", "recoveryDeferred", "persistenceReady", "watchdogAuto", "loggerActive"];
@@ -408,7 +405,7 @@
}
function usbWatchdogEvent(payload) {
if (bytesToAscii(payload, 0, 4) !== "MGR3") return null;
if (bytesToAscii(payload, 0, 4) !== "MGR2") return null;
const code = payload[98];
const sequence = uint32LE(payload, 107);
if (!sequence) {
@@ -626,8 +623,8 @@
: event.actionCode === 4 ? "Reboot request was cancelled; no physical reset is claimed."
: "Recovery attempt was recorded; this does not prove successful recovery."]);
} else {
values.push(["Last USB watchdog event", model.public.protocol === "MGR3"
? "none recorded" : "unavailable in MGR1/MGR2"]);
values.push(["Last USB watchdog event", model.public.protocol === "MGR2"
? "none recorded" : "unavailable in MGR1"]);
}
summary.replaceChildren();
values.forEach(([term, description]) => {
+13 -10
View File
@@ -1,6 +1,6 @@
# Management report decoder
Paste MGR1, MGR2, or MGR3 management-report payloads or complete GroupData packets from a
Paste MGR1 or MGR2 management-report payloads or complete GroupData packets from a
packet analyzer to read a radio's public management information. Everything,
including password authentication and ACL decryption, happens locally in this
browser. The report, password, and candidate public key are never uploaded.
@@ -13,13 +13,13 @@ by someone who does not know that password.
## Decode a management report
<div class="management-tool" data-management-decoder>
<label for="management-packet-input">MGR1/MGR2/MGR3 payload or complete GroupData packet hex</label>
<label for="management-packet-input">MGR1/MGR2 payload or complete GroupData packet hex</label>
<textarea
id="management-packet-input"
data-role="input"
spellcheck="false"
autocomplete="off"
placeholder="Paste analyzer Raw Data, canonical MGR1/MGR2/MGR3 payload hex, or one MQTT raw value per line"
placeholder="Paste analyzer Raw Data, canonical MGR1/MGR2 payload hex, or one MQTT raw value per line"
aria-describedby="management-packet-help"
></textarea>
<p class="management-help" id="management-packet-help">
@@ -69,8 +69,7 @@ by someone who does not know that password.
- Complete `PAYLOAD_TYPE_GRP_DATA` (`0x06`) analyzer/MQTT packet hex. It checks
the MeshCore route header, encoded path length, version-specific page bounds, and required
zero padding.
- A canonical payload beginning with `4D475231` (`MGR1`), `4D475232` (`MGR2`),
or `4D475233` (`MGR3`).
- A canonical payload beginning with `4D475231` (`MGR1`) or `4D475232` (`MGR2`).
- One raw packet or canonical payload per line; duplicate observations of an
identical page are deduplicated.
@@ -79,22 +78,26 @@ the report-specific fingerprints and the administrator and/or OTA-signer flags.
It cannot turn a fingerprint back into a full key. Use the optional candidate
field to test a specific full public key.
MGR2 and MGR3 also show USB logging, host/reader connection, logger-client activity, stalled/recovering/deferred
MGR2 also shows USB logging, host/reader connection, logger-client activity, stalled/recovering/deferred
state, recovery stage, saved backoff, retry interval/inactive seconds, Auto qualification
seconds, and persistence
readiness. Unsupported observation is shown explicitly; legacy MGR1 has no
USB block and shows it as unavailable. These values are the report-start
snapshot, not proof that a Pi or host application is healthy. Older decoder
copies require an update for MGR3.
copies require an update for MGR2.
MGR3 additionally shows the latest USB watchdog action, reason mask and names,
MGR2 also shows the latest USB watchdog action, reason mask and names,
advisory node-clock timestamp, recorded-boot uptime seconds, event sequence,
and whether the record was durably saved. Software recovery and re-enumeration
mean attempted actions, not successful repairs. A reboot request is not proof
that a reboot completed; a later cancellation can replace it. The node clock
can be wrong, even when it resembles a valid date. Event uptime and sequence
remain useful for ordering. MGR1/MGR2 do not carry this event field and show it
as unavailable. All 36 ACL entries still fit, using up to nine MGR3 pages.
remain useful for ordering. MGR1 does not carry this event field and shows it
as unavailable. All 36 ACL entries still fit, using up to nine MGR2 pages.
The earlier USB-only MGR2 development layout was never released. This decoder
accepts the finalized MGR2 format with its 111-byte public header and latest
watchdog event.
Reports contain a frozen latest-event snapshot, not immediate watchdog alerts
or a complete event history. See [USB logging watchdog](usb_logging_watchdog.md)
+12 -16
View File
@@ -1,4 +1,4 @@
# Management reports (MGR1/MGR2/MGR3)
# Management reports (MGR1/MGR2)
Available on repeater (including observer), room-server and sensor firmware.
Companions/terminal-chat nodes and KISS modems do not originate these reports.
@@ -6,10 +6,11 @@ Off by default. A management password and explicit enable are both required.
No radio settings, existing preferences layout, or OTA authorization policy is
changed by enabling reporting. This protocol does **not** authorize updates.
Current firmware emits `MGR3`, including a public USB logging/recovery snapshot
and its latest watchdog event. Updated decoders still accept legacy `MGR1` and
`MGR2`; older decoder copies need an update to read MGR3. Reporting USB status
does not enable logging or change retry policy.
Current firmware emits `MGR2`, including a public USB logging/recovery snapshot
and its latest watchdog event. Updated decoders still accept released `MGR1`;
older decoder copies need an update to read MGR2. The earlier USB-only MGR2
development layout was never released and was replaced before release.
Reporting USB status does not enable logging or change retry policy.
## CLI
@@ -97,7 +98,7 @@ is bounded to 1.5 KiB, plus a small configuration object and temporary stack use
## Routing and MQTT
Both direct/path and flood reports use **`PAYLOAD_TYPE_GRP_DATA` (`0x06`)**.
The route bits independently select direct or flood. `MGR1`/`MGR2`/`MGR3` are application
The route bits independently select direct or flood. `MGR1`/`MGR2` are application
extensions with **literally plaintext public fields**, not a call to the ordinary
encrypted `createGroupDatagram()` builder. A fixed public marker is not an owner
or password-derived channel ID. There is no outer channel encryption.
@@ -133,11 +134,11 @@ For a browser-local decoder that also decrypts and authenticates ACL entries,
use the [Management report decoder](management_decoder.md). It does not upload
the captured packet or password.
## Canonical MGR3 payload (little endian)
## Canonical MGR2 payload (little endian)
| Offset | Bytes | Field |
|---:|---:|---|
| 0 | 4 | `MGR3` |
| 0 | 4 | `MGR2` |
| 4 | 16 | First 16 bytes of reporter public key |
| 20 | 4 | Persisted report sequence (never wraps; exhaustion stops TX) |
| 24 | 4 | RTC Unix timestamp; advisory, may be wrong |
@@ -176,7 +177,7 @@ the captured packet or password.
| after ACL | 16 | Full AES-SIV authentication tag |
| after tag | 0-15 | Zero padding to group-compatible length; not part of canonical payload |
MGR3 retains all 36 ACL entries, using four per page and up to nine pages. The
MGR2 retains all 36 ACL entries, using four per page and up to nine pages. The
largest canonical page is 179 bytes and already group-compatible;
the radio payload budget is unchanged. The USB snapshot is frozen with the
other public fields at report start and repeated on each page. It is not a
@@ -230,11 +231,6 @@ normal reboot and mode changes, but is not a log of hardware hangs or sudden
power failures. See [USB logging watchdog](usb_logging_watchdog.md) for storage
failure, cancellation, and review limitations.
Legacy MGR2 retains the same offsets through byte 97, followed by encrypted ACL
entries at byte 98 and the authentication tag. It carries five entries per page
and up to eight pages. Its USB status is available, but watchdog event history
is unavailable rather than an empty modern record.
Legacy MGR1 retains the same offsets through byte 82, followed immediately
by encrypted ACL entries at byte 83 and the authentication tag. It carries
six entries per page and up to six pages, with no USB block. Its absent USB
@@ -273,8 +269,8 @@ needs a candidate administrator's full key to identify a fingerprint.
Encryption is [RFC 5297 AES-SIV-CMAC-256](https://www.rfc-editor.org/rfc/rfc5297),
using the existing rweather AES primitive. The single associated-data string is
the complete clear header: 111 bytes for MGR3, including USB status and its event,
98 bytes for MGR2, or 83 bytes for MGR1. All retain the existing derivation domains; the authenticated
the complete clear header: 111 bytes for MGR2, including USB status and its event,
or 83 bytes for MGR1. Both retain the existing derivation domains; the authenticated
magic and layout distinguish their wire versions. Ciphertext is exactly the ACL byte length.
Full 16-byte tag, no truncation. This deterministic misuse-resistant mode avoids
reliance on the firmware's noncryptographic general-purpose RNG or RTC nonces.
+15 -5
View File
@@ -155,10 +155,10 @@ an exact trigger time; this record covers the staged USB logging watchdog.
| `defer`, `fs` | Ownership/backend deferral and verified durable persistence readiness. |
| `rec`, `boot` | Attempted USB recoveries and durable reset authorizations. Attempts save the event and recovery count together; a failed save can leave the live recovery count unsaved. The reset count can include a later safety/health veto; it is not a count of completed physical reboots. |
The [MGR3 management report](management_reports.md) exports the USB status and
The [MGR2 management report](management_reports.md) exports the USB status and
the same latest-event record as a frozen, authenticated public snapshot. Updated
Python and browser decoders retain MGR1/MGR2 support; older decoder copies must
be updated to read MGR3. Events appear on the normal management schedule, not
Python and browser decoders retain released MGR1 support; older decoder copies
must be updated to read MGR2. Events appear on the normal management schedule, not
as immediate alerts. Reading status does not renew the logging-client lease.
## Verification
@@ -182,12 +182,16 @@ and ordinary RAK 3x72 STM32 USB Companion. The STM32 image retains all CLI
features and its 32 KiB filesystem, but has only 368 bytes of app flash margin;
its unsupported USB watchdog machinery is compiled out.
Latest-event/MGR3 verification on the same date passed 116 USB regression tests,
Latest-event verification on the same date passed 116 USB regression tests,
15 management tests with fatal AddressSanitizer/UndefinedBehaviorSanitizer
checks, and the browser decoder tests. Coverage includes timestamp callbacks,
clock corrections and rollover, legacy-state migration, commit failures,
reboot-intent cancellation, deferred cancellation saves, and authenticated
cross-language MGR1/MGR2/MGR3 decoding. The full native suite passed 1,713 cases
cross-language decoding of the development report formats. The extension was
called MGR3 during those pre-release checks, then assigned to MGR2 because the
earlier USB-only MGR2 layout had never been released. Current decoders support
released MGR1 and the finalized MGR2 layout; this reassignment was not a migration
of deployed MGR2 reports. The full native suite passed 1,713 cases
during implementation; the final packet validator also passed a fresh 116-case
routing suite and 5,855,168 previous/current validator comparisons under fatal
sanitizers.
@@ -217,3 +221,9 @@ latter uses 229,020 of 229,376 app flash bytes (356 bytes of margin), retaining
all features and the 32 KiB filesystem. The bare recipe is tighter still.
These checks do not qualify every board or physical USB recovery. No device
was flashed and existing `out` artifacts were not changed.
Finalizing the event-capable layout as MGR2 passed 1,719 native cases, 16
management tests with real C++/Python/browser cryptography, the standalone
browser decoder checks, and the bare STM32 Companion compile. Coverage retains
MGR1, rejects retired formats and old prototype ciphertext, and verifies that
invalid management sends release their packet without queueing it.