feat(web): run telnet round-trip probe on SSH-capable speakers too

Previously the SSH-capable branch and the telnet-only branch were
mutually exclusive — speakers with both transports reachable only
got the curl-from-device HTTPS check, never the round-trip probe.
That left a class of bugs invisible to pre-flight: an asymmetric
network path where the speaker's userspace can reach our service
(curl works) but the swUpdateUrl fan-out can't (or vice versa).

Each transport now gets its own check; both run when both are
reachable. The two exercise meaningfully different code paths in
the speaker:

  - SSH curl-from-device: speaker's normal userspace HTTP stack
    over an arbitrary inbound TCP to our HTTP/HTTPS port.
  - Telnet round-trip: speaker's firmware-internal swUpdateCheck
    fan-out, which writes to its own DNS resolver and outbound
    HTTP code path that the curl test doesn't go near.

A speaker that passes one and fails the other reveals a real
connectivity asymmetry worth surfacing before the migration
writes its target URLs.

Cost: ~1s extra on the success path (probe is fast on healthy FW
27.0.6), up to ~6s extra on the timeout path. The probe restores
the runtime swUpdateUrl unconditionally so there's no lingering
state regardless of outcome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-11 00:37:11 +02:00
co-authored by Claude Opus 4.7
parent ae9b02a42b
commit 9ad159d41d
+23 -9
View File
@@ -3066,14 +3066,24 @@ async function runApplyPreflight(deviceId, methods, opts, targetUrl) {
results.push({name: "Backend summary re-check", status: "ok"});
const summary = r.summary;
// Step 2: reachability from the device. SSH-capable speakers get
// the curl-from-device test against the URL the migration will
// actually write (HTTP or HTTPS, derived from the plan); SSH-less
// speakers fall through to the telnet round-trip probe that
// temporarily flips swUpdateUrl and observes the resulting
// outbound. If neither transport is reachable, the check is
// surfaced as a skip rather than silently dropped.
// Step 2: reachability from the device. Each transport gets its
// own check — they exercise different network paths:
//
// - SSH (curl from device) verifies inbound TCP from the
// speaker to our HTTP/HTTPS port using the speaker's normal
// userspace stack.
// - Telnet round-trip exercises the outbound from the
// speaker's `swUpdateUrl` fan-out, which uses a different
// code path in the firmware. A speaker that passes the SSH
// curl test but fails the round-trip probe (or vice versa)
// reveals a real connectivity asymmetry worth surfacing.
//
// Both checks run when both transports are reachable. If neither
// is reachable, the row is surfaced as a deliberate skip rather
// than silently dropped.
const connectionTestURL = preflightConnectionTestURL(summary, methods, targetUrl);
const ranAnyReachability = (summary.ssh_success && !!connectionTestURL) || summary.telnet_reachable;
if (summary.ssh_success && connectionTestURL) {
const scheme = connectionTestURL.startsWith("https:") ? "HTTPS" : "HTTP";
const label = `${scheme} connection from device`;
@@ -3082,13 +3092,17 @@ async function runApplyPreflight(deviceId, methods, opts, targetUrl) {
const cr = await checkConnectionFromDevice(deviceId, connectionTestURL);
setPreflightItemStatus(item, cr.status, cr.message);
results.push({name: label, ...cr});
} else if (summary.telnet_reachable) {
}
if (summary.telnet_reachable) {
const item = addPreflightItem("Telnet round-trip probe (swUpdateUrl)");
setPreflightItemStatus(item, "running");
const cr = await checkTelnetRoundTrip(deviceId, targetUrl);
setPreflightItemStatus(item, cr.status, cr.message);
results.push({name: "Telnet round-trip probe", ...cr});
} else {
}
if (!ranAnyReachability) {
const item = addPreflightItem("Reachability from device");
setPreflightItemStatus(item, "skip", "neither SSH nor Telnet:17000 is reachable");
results.push({name: "Reachability from device", status: "skip"});