Back to list
Skywire Development Update — September 24, 2026
The twenty-fourth split between the mux campaign’s continuing correctness work and a quiet but substantial refactor of the visor’s own internals, which had grown enough shared package state over the years that two visors built in the same process could silently overwrite each other’s module graph. The standby pool learned to size itself from how many genuinely disjoint routes actually exist to an exit rather than a flat guess, a reserved leg gained a path back to being a real stream-carrying tunnel, and three separate data-plane stalls were cleared with their own emulated regression tests. A recovered panic, previously visible only if someone happened to be reading the right log at the right moment, now leaves a trace visor state can show on any platform including the browser. The mobile app answered its first two rounds of F-Droid review feedback and shipped fixes for four more user-reported bugs.
Skywire: The Standby Pool Learns to Scale Itself
5168 replaced the standby pool’s flat cap of thirty-two tunnels with a bound computed from the actual topology: the number of genuinely disjoint one-intermediate routes to an exit, plus any live direct transports, reported back to the app as disjoint_routes so the pool can follow it directly rather than guess. On the rig that let the pool grow to 128 tunnels against a measured bound of 130, which turned out to be too generous in practice — 5176 capped the auto-sized pool back down to the old default of thirty-two after the larger pool added roughly ninety extra route groups and seventy-five megabytes to the exit’s memory while churning uploads during the fill, with the ceiling still a live knob for anyone who wants it higher.
5161 found a two-hop tunnel that had taken a direct transport as its second leg, striping frames across two paths of different shapes that reorder on every single frame and fail over onto routes of different lengths; a new knob now requires every leg a tunnel adds on its own to match its first leg’s hop count, falling through to the right-shaped route rather than the first one offered.
Skywire: Leg Reserves, Adoption, and Promotion Rules
5165 found the shape converger capable of promoting a group with no application session and no end-to-end handshake into a counted tunnel, simply because a chain the converger had split back out of a tunnel looked enough like a standby group to pass — such a group is now a distinct “leg reserve” that can be composed back in as a leg but can never be promoted as if it were carrying anything. 5181 then gave a leg reserve an actual path back to full tunnel status: a new control flag lets both ends re-point a reserved chain at a fresh tunnel descriptor and run the ordinary handshake over it, with the exit handling the adoption off its serial accept loop specifically so a handshake that never completes cannot stall ordinary route setup behind it — a failure mode the very first rig run of this change caught directly.
Skywire: Clearing Three Stalls in the Data Plane
5177 fixed three separate things an emulated test could reproduce but the live rig had only shown as symptoms: a snubbed range-split chunk’s read stayed blocked for its full twenty-second timeout because a local close didn’t actually wake a pending read, the spread policy’s per-route cap could be exceeded mid-object once a route died partway through an upload, and acknowledgments stayed pinned to a primary leg that had gone silent rather than moving to whichever leg was still actually receiving — that last fix cut recovery after a both-direction cut of the primary leg from twenty-eight seconds to just over two. 5179 found that an exit on an older build which never sends a proper end-of-stream signal could leak a stream indefinitely, since the client just waited forever for a FIN that was never coming; every read on that path now carries its own rolling idle deadline, and the exit-open timeout itself now scales with a tunnel’s measured round trip instead of a flat fifteen seconds that was too short for a slow multi-hop path.
5167 traced a wasm-desk burst where six concurrent SOCKS dials produced three silent failures and an empty-looking stream table, tracing it to a yamux stream open running inline on the accept loop itself — opening a stream can block on writing to the underlying route group’s connection for as long as that route takes, so one slow open stalled every other pending connection behind it. Opening now happens in each connection’s own goroutine, and the accept path gained its own counters so a connection that dies before it is ever fully accepted leaves a trace rather than nothing at all.
Skywire: Making a Recovered Panic Visible, and Relabeling the Status Page
5164 closed a gap specific to recovered panics, which unlike an unrecovered crash leave no trace anywhere the process keeps running fine — and on the wasm visor there is often nowhere to even look, since stderr doesn’t exist, the log server is off by default, and the log file isn’t reachable from the page at all. A bounded ring of recent recovered panics, each with its site, value and stack, now surfaces in visor state --select diag, with a running count that keeps climbing after the ring itself wraps so the two numbers together say whether what’s shown is the whole story. 5162 fixed the status page labeling every single route group “stream N” regardless of role, so a pool of thirty held standby tunnels read as thirty idle open streams; active groups now read “tunnel N” and pooled ones “standby N”, leaving “stream” for the actual SOCKS streams a tunnel carries.
Skywire: Two Correctness Fixes Away From the Mux
5178 fixed two unrelated but both serious defects found in the same sweep: an HTTP auth helper had been reading any Redis error, including a plain outage, as if the nonce were legitimately zero, silently resetting authentication state during exactly the kind of failure that should have refused the request instead; and a truncated or unexpected-type routing rule arriving from a setup node, a cascade install or a setup reply could panic the visor outright on a bare RPC goroutine, so every rule is now validated at the point it actually enters the system rather than wherever it happens to be read next.
Skywire: A Visor API That Isn’t the Whole Visor
Five PRs pulled the visor’s RPC surface and its command-line entry point out of the implementation they had been welded to for years. 5171 moved the entire 322-method API interface, its request and response types, and the RPC client into its own package that does not import the visor implementation at all, cutting the number of packages that needed the whole visor down from about forty to six. 5175 then split that one sprawling interface into eleven smaller role interfaces — apps, transports, routing, diagnostics and so on — so a caller like skychat that only needs chat methods can depend on exactly that slice rather than the whole contract.
5172 moved the actual command-line entry point itself out of the visor package into its own command package, passing an explicit options struct through rather than reaching into roughly thirty package-level variables, which incidentally fixed a reload path that used to call a fatal log line mid-reload instead of returning an error. 5173 untangled two stray imports running the wrong direction — the visor package reaching into a command package for its pairing manager, and a geoip lookup duplicated instead of shared — and 5174 fixed the root cause the whole refactor was chasing: the roughly sixty module definitions that wire a visor together at startup were package-level variables reassigned on every call, so two visors built back to back in the same process, which both the test suite and a live reload routinely do, could silently overwrite each other’s module graph.
Skywire: A Config File That Actually Exists, and a Faster Rig Deploy
5166 found that autoconfig could run against a config file that had simply never been created, because the step that writes one from the annotated template only ever runs from a packaging hook, leaving a plain go install, a hand-unpacked release binary, or the browser visor with nothing to read or write to at all — every knob documented in that template was invisible for exactly those installs. Autoconfig now creates the file from the same template on the same generate-once terms a package manager already follows, never touching one that already exists, and the browser visor’s own three-line placeholder is dropped in favor of the real template so a setting like enabling the hypervisor can actually survive a page reload. 5180 sped up the mux campaign’s own deploy tooling, which had been spending most of a rig deployment simply copying an unstripped binary over a dmsg connection; gzipping the upload and pushing to several visors in parallel brought a deploy of the exit down to about sixty seconds.
Skywire: Mobile Answers F-Droid Review, and Four More Bugs
5159 and 5163 answered the first two rounds of F-Droid’s review of the Android app’s submission: the store listing had been sitting in a location F-Droid’s own scanner doesn’t recognize as valid, so the app had no summary or description as far as the review was concerned, and the review separately asked that the embedded wasm routing-policy bundle be built from source during the F-Droid build rather than shipped as a committed binary, and that a release be pinned by full commit hash rather than a tag. 5169 answered F-Droid’s request to build TinyGo from source rather than installing a prebuilt package, and went a step further and built Go itself from source too, so a later review round wouldn’t ask the same question about the other toolchain; getting the two toolchains to agree needed Go’s own bootstrap chain, since TinyGo refuses to compile under a newer Go than the one it was itself built with.
5160 shipped fixes for four separate user reports against the Android app, each traced to a cause away from where the report pointed. A deleted chat came back because two different things rebuilt it on the next page load — a history store that still listed the peer and a replay buffer that handed the reloaded page the peer’s own recent messages as if they were new. VPN and SOCKS server lists loaded slowly or failed outright because a ten-second response deadline was measured from when the request was first read, while the discovery lookup behind it could legitimately take longer than that on its own; the fix adds a cache with a stale fallback and a proper per-response deadline. A call that arrived while the phone’s screen was off rang but never lit the screen, because the code setting the lock-screen-bypass flags ran on a UI clock that pauses exactly when the Activity is stopped. And a phone kept a call alive after the far end hung up, because call resumption had started treating an ordinary hangup’s connection close as if it were a dropped transport worth reconnecting over; a short zero-length frame now distinguishes a real goodbye from a lost connection, compatible with older builds that simply ignore it.