Back to list
Skywire Development Update — September 18, 2026

Skywire Development Update — September 18, 2026

The eighteenth was the busiest single day of the mux campaign and arguably of the whole run: forward traffic was finally confined to the leg it is supposed to ride rather than drifting onto whichever leg a stale heuristic favored, a shared-bottleneck detector that had been parking healthy legs on correlated noise was switched off by default, a tunnel gone quiet without actually dying got its own detection and recovery path, and every router and proxy tuning constant that used to require a rebuild to change became a live, persisted knob. An emulated multipath testbed let several of these failures be reproduced in seconds instead of an hour of rig time apiece. Elsewhere, the router’s inbound packet path was finally cut loose from a single slow peer, status.skysocks learned to survive a proxy restart, a heap profile found a third of one exit visor’s memory was a wasm module nothing had asked to load, and the mobile app shipped six Telegram-reported bug fixes plus in-app updates and Spanish.

Skywire: Forward Traffic Learns to Stay Put

5010 found the forward direction of a two-leg route group with no direct leg sending down the slower of the two legs in seven of ten rows regardless of which build ran, because the fallback leg-selection code read “leg added first” as its pick rather than measuring anything; it now picks the lowest measured end-to-end latency when every candidate has a sample, falling back to first-hop RTT only when none do. 5035 found the confinement itself leaking under sustained load: a flip controller meant to track direction was instead read by role, so a long upload could flip the whole group into looking like a multihop download and spill across every leg that matched the class, collapsing one 50MB upload split to a fifth of the single-route reference. Selection now follows the sender’s actual role, moves only when a challenger measures consistently better, and a full send window on the confined leg waits rather than spilling.

5037 closed the last gap in the same area: a leg whose delay basis ran many times its sibling’s was still handed a proportional share of the traffic rather than being recognized as effectively unusable for now, so pairing a 7.4MB/s leg with one measuring 0.06MB/s on its own reference stretched a 10MB download from eight seconds to thirty-seven depending on how much landed on the slow leg. Such a leg is now fed a bounded trickle to keep its own measurement current rather than a full share, with the ruling lifting itself the moment the leg’s delay basis recovers.

5030 found an acceptor-side controller parking a leg and mirroring that park back to the initiator, when parking the active set is supposed to be the initiator’s decision alone — on one compose set every recorded park was the acceptor doing exactly this, including one that sat inside a fifteen-second blackout. 5032 found the retransmit threshold on a loaded leg clamped to its own mean delay rather than a true margin above it, so any frame merely in flight longer than average was re-sent as if lost, duplicating roughly half the wire on a striped upload across two otherwise healthy legs.

Skywire: The Shared-Bottleneck Detector Goes Off By Default

5012 found the detector meant to spot two legs secretly sharing one physical link parking healthy legs on correlated noise rather than real evidence: its delay samples were both taken from how long the same shared reorder frontier took to advance, so the two legs’ readings were never independent of each other no matter what the network actually did. Four rig runs put it at zero correct parks out of eight, with parking left on costing 14 percent of a two-leg download’s throughput against parking left off.

Demotion now defaults to off, judged instead only on frames a SACK newly and independently acknowledges per leg, and every ruling the detector would have acted on is still recorded as an event so an operator can review and re-enable it with evidence rather than a guess.

Skywire: A Tunnel That Goes Quiet Gets Snubbed

5013 borrowed a mechanism from bittorrent for a tunnel that stays up, pinged and picked while delivering nothing: a tunnel holding outstanding work with no byte or ack for a few seconds, floored against its own measured RTT, is snubbed — its in-flight chunks abort and refetch elsewhere immediately, free of any retry budget, and the tunnel is retried later with exactly one chunk to prove itself again. 5029 found the first version of that snub firing on tunnels that were merely blocked behind a reorder wedge rather than actually silent, since a wedged route group looks identical to a dead one from the client’s side; the threshold moved from three seconds to twenty, above the longest wedge the rig had actually measured clearing.

5022 found a standby tunnel’s one chance to prove its own capacity — an idle audition offering it the next lone stream — could only ever be satisfied by a download’s very first chunk, so an unproven standby under that chunk held up the entire transfer rather than one eighth of it; only a sibling chunk running concurrently with others may now consume that offer.

Skywire: A Spread Policy for Chunk Placement

5014 built the policy criterion 10 of the campaign’s own goal asks for directly: a per-object ledger that weighs each tunnel by measured capacity, refuses to let any one tunnel exceed a configured share of an object while another sits under its own cap, and can duplicate a straggling tail chunk onto whichever idle tunnel proves fastest. All four of its knobs default to today’s behavior, so nothing changes until an operator opts in.

5031 found the chunk placement a striped upload had been using credited every idle, stale or never-measured tunnel with the best capacity present rather than a conservative median, which could score the identical ten-megabyte object four different ways depending on which tunnels happened to be warm; placement is now scored on upload capacity specifically, with the last chunk of an object always reserved for whichever tunnel is actually fastest, since that chunk decides when the object finishes. 5047 replaced the standby-pool promoter’s reliance on round-trip time, which measures how long a path is and not how much it delivers, with a delivered-goodput average folded only from windows a tunnel actually carried traffic in — on the rig the RTT-based promoter had parked the tunnel that was carrying an object in favor of one that merely pinged better.

Skywire: Uploads That Explain Their Own Failures

5025 found a failed upload’s only error message living in a private logger that never reached either the app’s own log store or the visor’s log file, so the blackout capture meant to diagnose it grabbed an already-rotated, unrelated log by sort order and recorded a cause that had nothing to do with the actual failure. A failed upload’s HTTP response now carries its own cause directly, and an in-process app’s logger is registered the same way an external app’s already is.

5027 found the client’s own tunnel-cutting logic could cause the exact sink refusal it then treated as fatal: closing a stream locally still lets a yamux read return the sink’s refusal of the now-truncated body, and that refusal was never recognized as self-inflicted; it is now retried immediately and for free rather than failing the whole object. 5023 found a cut mid-download costing five extra seconds beyond the roughly two it takes to detect and promote a replacement, because the range splitter’s in-order writer held an entire chunk back until it was completely done rather than streaming the prefix that had already safely arrived; the frontier chunk now streams incrementally, and a refetch resumes from exactly the last byte actually written rather than from the chunk’s start.

Skywire: The Router’s Inbound Path Stops Blocking Itself

5018 found the visor’s single inbound packet-reading goroutine able to park for up to thirty seconds on one unresponsive route group, stalling all eight other route groups on eight different first hops at the same instant. Each route group now owns its own bounded queue and worker, so a stuck group can only stall itself, with a full queue dropping and counting packets that the mux layer’s own retransmission can recover. 5049 found the same class of problem on the write side: a transit peer — a stranger routing through this visor — that stopped draining its connection could park the identical shared loop inside a blocking write with no deadline that would ever fire, which measured live as every one of the visor’s nine transports going dark for thirty seconds at once; transit writes now queue per next-hop with their own timeout, and a peer that won’t drain simply loses frames rather than freezing the visor.

5061 found the router learning a leg was gone only by polling a closed-transport flag from services that specifically step around a closed transport rather than act on it, so a single-leg group whose own transport the visor had removed could sit registered indefinitely, with the session on top finding out only whenever its next write happened to land on the dead link — sometimes not for fourteen seconds. The router now registers for the transport manager’s own close notification directly, cutting that detection to under a hundred microseconds in the emulated testbed. 5060 traced an intermittently failing ordering test to the test harness itself rather than the router: an emulator was scheduling delivery timers that could all fire in the same instant with no ordering guarantee between them, reordering frames with no router code involved at all; delivery now drains through a proper per-direction heap.

Skywire: A Live Knob Catalog for the Whole Mux Dataplane

5043 turned every router and mux tuning constant into an entry in a live catalog, set and read at runtime through route settings, eighty-seven knobs where twenty-one had been live before, each one asserted to default to the exact constant the binary used to compile in so an untouched visor behaves identically. 5044 extended the same catalog to the dial-time half of routing — ranking priors, candidate counts, warm-plan cache sizing — and added an operator-settable preferred-peer list and a flag to dial an app’s tunnel together with its legs rather than growing them afterward. 5042 moved the proxy’s own mux width and standby-pool size off two process-global variables that had been silently shared across every app on a visor, including the paired reference instance the bench measures against, onto per-app, persisted settings, and added a way to close exactly one tunnel and let the pool replace it without taking its sibling routes down too.

5051 batched route setup itself: a standby-pool fill or a multi-tunnel dial used to cost one fully separate setup-node round trip per route even when every route shared most of its path, so the fix coalesces the signed transport query and the per-hop installation work across an entire batch of routes to one exit, negotiated so an older setup node that doesn’t understand the new request simply falls back to the one-at-a-time behavior.

5067 found a setup node’s own circuit breaker, meant to stop hammering an unreachable destination, instead locking a visor with thirty-two already-healthy route groups out of any further routes to the same exit because it had no visibility into what that visor already held; the breaker now ships off by default, with failures still counted and the four parameters that controlled it available as live knobs for an operator who wants the old behavior back. 5069 closed the gap that let a standby pool collapse onto copies of a single route in the first place: a first-hop diversity filter was counting duplicate exclusion entries as distinct held hops, so the very evidence of a collapse was what unlatched the filter that should have prevented more of it.

Skywire: Testing the Mux Without the Rig

5046 built an in-process network emulator standing in for real transports, able to reproduce a live finding in roughly six seconds rather than the hour of rig time each one had been costing — jitter, loss, reordering, rate limiting and a cut switch that black-holes a direction without closing the underlying socket, all seeded for exact reproducibility. Five of its six initial scenarios matched live findings; the sixth deliberately fails, reproducing the exact composition collapse the day’s own forward-leg fixes were chasing, and stands as the test that will confirm the eventual fix.

5048 fixed the harness failing to compile the moment the knob catalog in 5043 renamed the constants it had been referencing directly. 5055 found six unrelated unit-test failures on darwin and windows were measuring the precision of each platform’s own clock rather than any real bug — a test asking for a one-nanosecond sampling interval on a clock that only ticks every half millisecond will read every delta as zero — and separated genuine router behavior from test rigs that simply assumed a Linux-grade clock.

Skywire: Status Pages and Proxy Starts

5040 found status.skysocks unable to survive a visor or proxy restart in a browser tab, because the brief reconnect window during which no SOCKS listener exists at all got blind navigation attempts from two separate auto-retry mechanisms, each of which could permanently replace the page with the browser’s own connection-refused error; both now probe before navigating and simply reschedule on a failed probe rather than ever landing during the gap.

5045 fixed the same status page drawing a session of two active and six standby route groups as eight active tunnels, since it read every tunnel from its leg glyphs and a standby’s single leg is active inside its own group; the page now reads the tunnel’s actual role directly. 5057 fixed proxy start waiting for every configured tunnel to come up before reporting success at all, which meant a topology with only one actual route to an exit could never satisfy the two-tunnel default and timed out a session whose first tunnel was already up and serving traffic; the remaining tunnels now fill in behind the start instead of blocking it.

Skywire: Config, Memory, and the Browse Origin

5052 gave a running visor a way to have one config field edited without the whole in-memory struct being re-marshaled over the hand-edit within the hour, through a new RPC that resolves and type-checks a dotted field path before applying it and refuses anything touching a secret key outright. 5053 fixed cli config gen silently ignoring an OUTPUT path set in the environment file, because a later command registration in the same package was clobbering the default before the flag was ever read — invisible because the help text kept printing the correct value while the write went somewhere else entirely.

5058 found a heap profile of a live exit visor with thirty-seven megabytes, a fifth of its total memory, pinned by a wasm module copied fully onto the Go heap merely by checking whether one was present at startup; the three servers that actually serve the module now stream it from its read-only mapping instead. 5063 closed the loophole that let that same module ship marked dirty in the first place by refusing to stage it from an unclean working tree.

5062 let a browse domain serve more than one embedded app rather than exactly one, needed for a second site wanting to render its own pages natively on a shared domain; wildcard parents are documented as a deliberate reputation-sharing decision rather than a shortcut. 5066 fixed a browse page’s first visit fetching its own subresources before the service worker meant to relay them had actually taken control of the page, which is a different and later event than the worker merely becoming active.

5065 rolled three dependency bumps dependabot could not merge automatically into one hand-verified update.

Skywire: Mobile, and Measuring All of It

5033 shipped the Android app’s response to six separate Telegram bug reports — a stuck chat status across a network switch, a blocking JS dialog that froze the whole chat page, a wallet sync timeout that lost a balance along with a 19MB transaction list, a sidebar overflow on desktop widths, a notification that re-posted itself for the length of an entire call, and a full-screen call intent silently denied by Android 14 — alongside in-app update checks with signature verification and a full Spanish translation.

The remaining eighteen PRs of the day were the bench side of everything above: refined ceiling, blackout and goroutine-dump capture (5016, 5017, 5019), a knob-sweep runner and a statistical screening design that can find which of twelve live factors matter without a rebuild per value (5059), a Thompson-sampling tuner layered on top of it (5015), paired-reference and chaos-cut corrections the campaign’s own runs exposed (5038, 5041, 5064), per-app mux-width restoration between bench sets (5056), and the running results, READMEs and changelog entries tying each day’s forty-plus PRs back to the test plan’s own criteria (5020, 5024, 5028, 5034, 5036, 5039, 5068).