From f0de4864b6b336f5b42cbe6818d6bbf779ff46cf Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Mon, 11 May 2026 18:39:51 +0200 Subject: [PATCH] deprecate(service): mark active telnet round-trip probe for removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The swUpdate daemon caches its target URL at boot and ignores live `sys configuration` writes, so the active flip in RunTelnetRoundTripProbe never reaches the running daemon — confirmed empirically on a fully-migrated speaker (FW 27.0.6) where both the runtime and persistence layers were flipped and the device still dialed the previously-cached `/updates/soundtouch` URL plus DNS-intercepted `/streaming/software/update/account/*`. The probe URL was never observed. Marks DEPRECATED: - pkg/service/setup/telnet_probe.go: ProbeRegistrar, TelnetProbeResult, generateProbeToken, RunTelnetRoundTripProbe. - pkg/service/handlers/handlers_telnet_probe.go: HandleTelnetProbe, HandleProbeInbound, telnetProbeTimeout, telnetProbeResponse. - pkg/service/handlers/probe_registry.go: probeRegistry. - Server.probes field. - /probe/{token}[/*] and /setup/telnet-probe/{deviceId} routes. Adds §9.8 to docs/analysis/TELNET-MIGRATION-METHOD.md documenting the daemon-cache finding, the diagnostic that confirmed it, the passive observer replacement, the pre-flight branch on migration state, and the canonical telnet flow (Apply config → reboot → passive validation). All code symbols remain in place this commit; the follow-up commit performs the hard delete. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/soundtouch-service/main.go | 6 ++ docs/analysis/TELNET-MIGRATION-METHOD.md | 97 ++++++++++++++++++- pkg/service/handlers/handlers_telnet_probe.go | 11 +++ pkg/service/handlers/probe_registry.go | 7 ++ pkg/service/handlers/server.go | 8 +- pkg/service/setup/telnet_probe.go | 11 +++ 6 files changed, 137 insertions(+), 3 deletions(-) diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index c857538..b7e12a1 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -872,6 +872,11 @@ func setupRouter(server *handlers.Server) *chi.Mux { r.Get("/", server.HandleRoot) r.Get("/health", server.HandleHealth) + // DEPRECATED: /probe/{token}[/*] backs the deprecated round-trip + // probe (RunTelnetRoundTripProbe). Removed in a follow-up commit + // alongside the rest of the active-probe code path; see + // docs/analysis/TELNET-MIGRATION-METHOD.md §9.8. + // // Telnet round-trip probe inbound. The orchestrator temporarily // sets the speaker's swUpdateUrl to /probe/{token}; the speaker // then fans out a request that we observe here. Catch-all suffix @@ -1103,6 +1108,7 @@ func setupRouter(server *handlers.Server) *chi.Mux { r.Post("/test-connection/{deviceId}", server.HandleTestConnection) r.Post("/test-hosts/{deviceId}", server.HandleTestHostsRedirection) r.Post("/test-dns/{deviceId}", server.HandleTestDNSRedirection) + // DEPRECATED: see §9.8; removed in the follow-up commit. r.Post("/telnet-probe/{deviceId}", server.HandleTelnetProbe) r.Get("/ca.crt", server.HandleGetCACert) r.Get("/proxy-settings", server.HandleGetProxySettings) diff --git a/docs/analysis/TELNET-MIGRATION-METHOD.md b/docs/analysis/TELNET-MIGRATION-METHOD.md index 8e6dfef..4d686f8 100644 --- a/docs/analysis/TELNET-MIGRATION-METHOD.md +++ b/docs/analysis/TELNET-MIGRATION-METHOD.md @@ -625,4 +625,99 @@ configured `swUpdateUrl`. implemented. - Running the round-trip probe on SSH-capable speakers too (as additional validation alongside the curl-from-device HTTPS test), - not just as the SSH-less fallback it is today. + not just as the SSH-less fallback it is today. **Subsumed by §9.8 + — the round-trip probe is being removed; the passive observer is + transport-agnostic and replaces it for migrated speakers.** + +### 9.8 The swUpdate daemon-cache finding and removal of §9.5 + +The §9.5 round-trip probe was retired after empirical testing on a +fully-migrated speaker (FW 27.0.6) revealed that the `swUpdate` +daemon **caches its target URL at boot and ignores live config +writes**. The diagnostic sequence: + +1. Manual telnet flip of both layers — `sys configuration swUpdateUrl + ` (runtime) **and** `envswitch boseurls set + ` (persistence). `getpdo CurrentSystemConfiguration` + confirmed both writes stuck. +2. HTTP GET `:8090/swUpdateCheck` to trigger fan-out. +3. Service access log showed the device outbound landed on + `/updates/soundtouch` (the **previous** `swUpdateUrl` value, current + at the last daemon boot) and `/streaming/software/update/account/` + (a separate Bose URL the daemon hits, routed to this service by DNS + interception). The probe URL was never dialed. + +This falsifies the original NEXT.md hypothesis that the persistence +layer would override the runtime layer for the daemon's fan-out, and +points instead at daemon-level URL caching. Two consequences: + +- **The §9.5 probe cannot work on migrated speakers without a + reboot.** The cached URL is set when the daemon starts; flipping + config after that point has no effect on what the daemon dials. +- **The §9.5 probe likely cannot work on unmigrated speakers + either**, for the same reason — the daemon caches whatever URL it + read at startup, which on an unmigrated speaker is the Bose cloud + URL. We have no service running with the probe URL registered on + unmigrated speakers, so the original "it worked in testing" claim + has no empirical basis; it likely failed silently because nothing + was watching. + +The honest replacement is a **passive observer** (see +`pkg/service/setup/peer_probe.go`): + +1. Register the device IP with an in-process observer + (`handlers.peerObserver`, wired via `PeerObserverMiddleware`). +2. Nudge `:8090/swUpdateCheck` to make the daemon fan out *something* + sooner than its ~5min timer. +3. Wait up to 30s for any inbound from that IP. On a migrated + speaker, DNS interception means the daemon's outbounds (update + fan-out, marge polls, BMX registry calls) all funnel through this + service regardless of which URL the daemon resolved internally — + so reachability reduces to *"did the device dial us at all."* + +Endpoint: `POST /setup/peer-probe/{deviceId}`. No device-state +mutation; safe to re-run. Returns `{ok, result: {reached, +observed_path, elapsed_ms}, error}` with the same UI keying as the +old probe (`result.reached`). + +#### 9.8.1 The pre-flight panel branch + +The web UI's pre-flight orchestrator (`runApplyPreflight` in +`script.js`) branches on `summary.is_migrated`: + +| Migration state | Reachability row | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| Migrated (`is_migrated=true`) | "Reachability check (passive observer)" — calls `POST /setup/peer-probe/{deviceId}`. | +| Not migrated (incl. partial) | Skip row "Round-trip validation runs after Apply + reboot" with the rationale "daemon caches swUpdateUrl at boot". | + +Per-axis booleans (`xml_migrated`, `hosts_migrated`, `resolv_migrated`, +`telnet_migrated`) remain visible in the State card, so the user can +see which parts of the migration are already in place even when the +overall flag is false. The skip row does not attempt the active probe +on unmigrated speakers — the canonical telnet flow is: + +``` +Apply telnet config → user-initiated reboot → re-run pre-flight on +the now-migrated speaker → passive observer confirms fan-out. +``` + +#### 9.8.2 Removal trail + +Removed (or scheduled for removal in a follow-up commit) at the time +of §9.8 landing: + +- `pkg/service/setup/telnet_probe.go` — `RunTelnetRoundTripProbe`, + `ProbeRegistrar`, `TelnetProbeResult`, `generateProbeToken`. +- `pkg/service/handlers/handlers_telnet_probe.go` — `HandleTelnetProbe`, + `HandleProbeInbound`, `telnetProbeTimeout`, `telnetProbeResponse`. +- `pkg/service/handlers/probe_registry.go` — `probeRegistry` + tests. +- `Server.probes` field. +- Routes `/probe/{token}`, `/probe/{token}/*`, `/setup/telnet-probe/{deviceId}`. +- The `target_url` query-param plumbing on the deprecated endpoint. +- `script.js` — `checkTelnetRoundTrip` (orchestrator call site removed + in the commit that added the branch; function itself removed later). + +`isCommandNotFound` and `parseGetpdoConfig` stay — they are also used +by the migration writer (`telnet_migration.go`), preflight reader +(`telnet_preflight.go`), pairing path (`marge_pairing.go`), and +cross-check (`preflight_crosscheck.go`). diff --git a/pkg/service/handlers/handlers_telnet_probe.go b/pkg/service/handlers/handlers_telnet_probe.go index eb38042..8c20d25 100644 --- a/pkg/service/handlers/handlers_telnet_probe.go +++ b/pkg/service/handlers/handlers_telnet_probe.go @@ -8,12 +8,21 @@ import ( "github.com/go-chi/chi/v5" ) +// DEPRECATED: handlers_telnet_probe.go (HandleTelnetProbe, +// HandleProbeInbound, telnetProbeTimeout, telnetProbeResponse) is +// scheduled for removal. The swUpdate daemon caches its target URL +// at boot; the active flip in RunTelnetRoundTripProbe never reaches +// the running daemon. See docs/analysis/TELNET-MIGRATION-METHOD.md +// §9.8. Replaced by HandlePeerProbe (handlers_peer_probe.go). + // telnetProbeTimeout caps how long the orchestrator waits for the // device's outbound swUpdateCheck fan-out to land on /probe/{token}. // 6s lines up with the existing telnet preflight budgets and is well // above the median observed round-trip (<1s on FW 27.0.6). const telnetProbeTimeout = 6 * time.Second +// DEPRECATED: see the file-level note above. +// // HandleProbeInbound is the catch-all for /probe/{token}/* — the path // the round-trip orchestrator sets as the speaker's swUpdateUrl. Any // hit signals the registered channel; the response body is a minimal @@ -36,6 +45,8 @@ type telnetProbeResponse struct { Error string `json:"error,omitempty"` } +// DEPRECATED: see the file-level note above. +// // HandleTelnetProbe runs the SSH-less round-trip reachability check. // Generates a token, temporarily points the speaker's swUpdateUrl at // /probe/{token} via telnet, triggers :8090/swUpdateCheck, and reports diff --git a/pkg/service/handlers/probe_registry.go b/pkg/service/handlers/probe_registry.go index 30b2d73..697faff 100644 --- a/pkg/service/handlers/probe_registry.go +++ b/pkg/service/handlers/probe_registry.go @@ -2,6 +2,13 @@ package handlers import "sync" +// DEPRECATED: probeRegistry is scheduled for removal. The active +// round-trip probe it backs cannot work without a reboot (the swUpdate +// daemon caches its URL at boot). See +// docs/analysis/TELNET-MIGRATION-METHOD.md §9.8. Replaced by +// peerObserver (peer_observer.go) which keys on device IP and supports +// passive post-migration reachability checks. + // probeRegistry is the rendezvous between the telnet round-trip probe // orchestrator (which registers a one-shot token and waits for an // inbound) and the /probe/{token}/* HTTP handler (which closes the diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index 01ad994..5b9b02b 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -61,8 +61,12 @@ type Server struct { amazonClientSecret string amazonRedirectURI string amazonService *amazon.Service - probes *probeRegistry - peerObserver *peerObserver + // DEPRECATED: probes (probeRegistry) is removed in a follow-up + // commit alongside the round-trip probe handlers; see + // docs/analysis/TELNET-MIGRATION-METHOD.md §9.8. peerObserver is + // the replacement substrate. + probes *probeRegistry + peerObserver *peerObserver } // RequestSnapshot represents an immutable snapshot of an HTTP request. diff --git a/pkg/service/setup/telnet_probe.go b/pkg/service/setup/telnet_probe.go index 0bd749a..b54be18 100644 --- a/pkg/service/setup/telnet_probe.go +++ b/pkg/service/setup/telnet_probe.go @@ -10,6 +10,15 @@ import ( "time" ) +// DEPRECATED: ProbeRegistrar, TelnetProbeResult, generateProbeToken, +// and RunTelnetRoundTripProbe are scheduled for removal. The swUpdate +// daemon caches its target URL at boot and ignores live `sys +// configuration` writes, so the temporary flip never reaches the +// running daemon. See docs/analysis/TELNET-MIGRATION-METHOD.md §9.8. +// Replaced by Manager.RunPeerReachabilityProbe in peer_probe.go for +// the migrated case; pre-migration validation relies on the other +// pre-flight rows plus the Apply + reboot cycle. + // ProbeRegistrar is the rendezvous between the round-trip probe // orchestrator (which registers a token and waits) and an HTTP layer // (which signals the channel when the device's outbound lands on the @@ -48,6 +57,8 @@ func generateProbeToken() (string, error) { return hex.EncodeToString(b), nil } +// DEPRECATED: see the package-level note above. +// // RunTelnetRoundTripProbe is the SSH-less reachability check that // fills the gap the curl-from-device HTTPS test leaves on USB- // unlock-refusing speakers. The sequence: