From 9ad159d41dd1a260a3f9870e35e8729db9c811ca Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Mon, 11 May 2026 00:21:53 +0200 Subject: [PATCH] feat(web): run telnet round-trip probe on SSH-capable speakers too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pkg/service/handlers/web/js/script.js | 32 +++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index 602ae23..0193f52 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -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"});