Commit Graph
2 Commits
Author SHA1 Message Date
2GT-RichandClaude Opus 5 63b5677522 fix(firmware,ssh,logs): five reported bugs (v0.24.1-beta)
Closes #136. Also fixes #133, #137, #138, #139 (GitHub closes only the first).

None of these came from multi-site; two came from me.

#136 — waves failed on the second device. A regression from v0.23.31, the
firmware rewrite that replaced fire-and-forget `install` with `download` plus
polling. `/system/package/update/download` blocks for a minute or more, and
the client's read timeout is 30 seconds. The timeout path was the real damage:
it nulled the pending read but left the socket open, so when the reply finally
arrived processBuffer() queued it and the *next* command took it as its own
answer. Every later call on that connection returned the previous one's data,
which is why the status polls could never see "Downloaded".

  - a timed-out read now abandons the connection instead of silently
    desynchronising it, so the failure is loud rather than wrong
  - long-running commands can state their own budget; check-for-updates,
    download and /log/print now do
  - a download whose command times out is re-checked on a fresh connection
    rather than assumed failed, because the image may well have landed

#133 — the SSH terminal could not use a deployed key. It read only
ssh_password_encrypted and refused to connect without one, so retiring the
password after deploying a key -- the workflow the feature exists for -- broke
the console. It now uses the same resolver as every other SSH consumer.
preferredAuth(), written to make exactly this decision, had never run outside
its own tests, and disagreed with the resolver that did: it accepted a
'deployed' key where the live path required 'verified'. Unified on the
stricter rule and wired in.

#139 — the rollout was written before start() was attempted, so a rejected
start left the rows behind: "already running", press again, another rollout
banked each time. Devices already in an unfinished rollout are now refused by
name, before anything is persisted.

#138 — cancel worked but the API's explanation that the in-flight device is
never interrupted was discarded, so the button looked dead. It is shown.

#137 — log overfetching. /log/print returns the whole ring buffer, and the
count was incremented per attempted insert rather than per accepted row, so a
poll that stored nothing still reported "Collected 1000 new log entries" after
a thousand round trips. Inserts are batched and counted by RETURNING, and the
id watermark is the maximum over recent rows rather than the last one
inserted, which could move backwards and make the whole buffer look new.

One self-inflicted wound worth recording. The first cut of the desync guard
discarded sentences whenever `connected` was false -- but `connected` is only
set *after* login succeeds, so it threw away every login reply and took the
whole fleet offline until it was caught. "Not yet connected" and "abandoned"
are different states and now have different flags. Every existing test pre-set
connected = true, so none of them could see it; the test added for it fails
against the bad version.

Verified on hardware: devices back online, check-update 5.2s (was a 30s
failure), full sync completes, and log collection now reports 298 / 22 / 12 /
12 across successive polls -- real decreasing counts rather than a constant
1000 -- with no failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:17:52 -07:00
2GT-RichandClaude Opus 5 1ab2e6547e feat(ssh): per-device SSH keys, deployed and proved (v0.23.24-beta)
Closes #110. Also the prerequisite for #118 — arbitrary console commands need
SSH on every device, and stored passwords do not scale to sixty of them.

Each device gets its own ed25519 keypair, generated on the manager, pushed over
the existing password session, imported, and then *proved* by opening a fresh
connection and authenticating with it. Nothing trusts a key until that last step
succeeds. Backups and config export use a verified key where one exists.

Verified end to end on hardware: deploy, rotate twice leaving exactly one
authorised key each time, verify, and a 4.6 KB config backup pulled over
key-only SSH with no password fallback.

**The design note on the issue was wrong about the central safety property**, and
testing on a switch is what proved it. I claimed the stored password remained as
a fallback. It does not: RouterOS binds a key to a user and then refuses password
authentication for that user entirely. Removing the key restored password login
immediately, which settles the causation.

That mistake compounded with a second one. Deployment wrote the new private key
to the database before proving it, so a rotation that failed halfway left the
database holding a key the device had never authorised while the device demanded
one we no longer had — SSH lost on the test switch, recoverable only through the
binary API. Three changes follow:

  * Rotation authenticates with the *existing* key, since the password is no
    longer an option once a key is installed.
  * Nothing is written until a fresh connection has authenticated with the new
    key. A deployment that fails verification removes the key it just installed
    rather than leaving the device demanding a credential nobody holds.
  * The binary API, unaffected by any of this, is the removal path — and is
    therefore the real recovery route, not the password.

Two further faults came out of live testing rather than reasoning. Every key for
a device shared one comment, so removing "the old key" during a rotation removed
the new one too; comments are now unique per key. And RouterOS stores that
comment in `info`, not `key-owner` — matching the wrong field silently matched
nothing and left every rotation's old key behind, which is why the switch
accumulated keys that later blocked it.

Revoke now clears *every* key this manager installed for a device, not just the
one on record. Orphans from a half-failed rotation otherwise keep password SSH
disabled with no way back through the product, which is exactly the state the
test switch got stuck in. When deployment fails with an authentication error and
orphans are present, the message says so instead of leaving someone guessing.

The UI states the trade before it is made: installing a key disables password SSH
for that user, the API and WinBox are unaffected, and revoking restores it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:39:38 -07:00