Back to list
Skywire Development Update — September 17, 2026
The seventeenth was the mux campaign’s second day on the live rig, and it moved from making legs behave inside one route group to making several whole tunnels cooperate. A lone stream stopped stacking onto whichever tunnel happened to answer fastest regardless of load, a send window learned to size itself on what a leg’s own acknowledgements prove rather than on its first hop’s ping, sibling tunnels started being ranked and held in a standby pool before anything went wrong rather than redialed cold after it did, and an upload finally gained the ability to survive losing the tunnel underneath it the way a download already could. A live-tunable settings surface cut what had been three hours of rebuild-deploy-restart cycles out of the rest of the day. Release CI was hardened against shipping a binary built from a dirty tree, and a standalone dmsg socks tool that had never actually carried traffic over dmsg was fixed to do so.
Skywire: Spreading Load Across Tunnels Without Starving Any of Them
4965 found the root cause of a two- and three-tunnel range-split download riding one tunnel entirely: the capacity meter recorded an idle tunnel’s keepalive trickle as its proven rate, so a starved tunnel’s bad estimate was never revisited, since it never got busy enough to re-measure. Only a busy window now updates the estimate, and an idle tunnel whose reading has gone stale is credited the best known capacity so it gets a chance to prove itself again. 4966 found each range chunk paying three serialized round trips — the SOCKS greeting, the CONNECT, the ranged GET — before its first byte, measured at 1.9MB/s against 5.5MB/s for the same file over a plain route on a 250ms path; chunks now pipeline all three writes and release their concurrency slot once their bytes are in memory rather than after an in-order write, so a later chunk no longer waits on an earlier one’s delivery to the browser.
4978 fixed a lone stream — a browser connection, not a range chunk — always picking whichever tunnel had the lowest RTT regardless of how many streams it already carried, which stacked two concurrent downloads of different objects onto the same tunnel in three of three trials; the pick now minimizes RTT times open-streams-plus-one, so a second stream moves off a busy tunnel unless the alternative is more than proportionally worse.
4979 pipelined the first stream’s own exit handshake the same way 4966 had pipelined a chunk’s, since the three serial round trips a split download pays before any parallel fetch starts had been read as three separate costs when they were the same bug in three places. 4982 cut the remaining fixed cost out of the bench sink itself, which had been re-hashing a 50MB object’s entire body on every ranged request — 600MB of hashing for one 12-chunk download — and had been blocking a browser’s GET on a full extra round trip to the exit before forwarding the CONNECT reply it already held.
Skywire: A Send Window That Learns What a Leg Actually Carries
4970 found the scheduler and the retransmit threshold reasoning over a leg’s first-hop transport RTT rather than the true round trip a frame actually takes end to end — measured as 10ms and 95ms over two pinned legs while the real send-to-ack delay on both was roughly 170ms, which meant the scheduler needed nearly ten frames queued before it would spill onto a second leg, and once it did, the threshold built on the same wrong number declared those frames lost outright. Both now read the larger of the transport RTT and the measured ack delay, and the latency band that decides whether a leg stays active judges on a windowed minimum of its live samples rather than its latest one, since queuing delay only ever adds to that number and a window strips it back out.
4968 found two park decisions disagreeing about the same leg within microseconds of each other in the same tick — one parking a leg for sharing a bottleneck with another, the other re-admitting it as perfectly in-band — which flapped one leg fourteen times in a minute, each flap triggering a retransmit flush; an adaptive park now holds for thirty seconds before either controller can reverse it, with an operator pin able to clear the hold outright.
4969 found the mirror-image mistake on the receive side: a leg carrying almost no bytes was indistinguishable from a black-holing one except by the reorder frontier, so a perfectly healthy but lightly used leg was parked as stalled, and the park was then faithfully mirrored back by the peer’s own resync every seven seconds, which is how five consecutive downloads ended up riding only one of two pinned legs. A park now requires an actually stuck reorder gap, and a park that originated with the peer is recorded as an event but never re-asserted by a resync meant only to repair signals this end dropped.
4972 separated two things that had been sharing one number: the send window still has to follow the live feedback delay or it starves a long path, but the baseline it falls back to when idle needs to be the leg’s own first-hop minimum, not the combined end-to-end value, because that value had no time decay and let one transfer’s own queuing become the next transfer’s floor — five consecutive uploads over the same leg fell from 3.90 to 0.62MB/s with zero actual loss as the delay reading ratcheted upward. The same PR moved lone-stream selection from “most bytes moved” to lowest measured RTT, since a lone stream is not competing for a pipe the way a range chunk’s siblings are.
4971 closed the gap where an exit gone silent produced a bare quarter-minute timeout with no log line and no counter; the timeout is now a named warning with a running count, and the tunnel it happened on sits out the next few picks rather than being immediately handed the next stream.
Skywire: Picking Better Routes Before Dialing, Not After
4981 found a multi-tunnel dial steering each extra tunnel off its siblings’ first hops correctly but then taking whichever unused candidate its selection happened to list first, landing a tunnel on a 470ms intermediate while a 39ms one sat free; every path that can win a diversify dial now goes through one ranking by measured latency, probing an unmeasured candidate on demand under a two-second ceiling rather than leaving it to sort arbitrarily. The same PR closed a sibling-exclusion gap that let a dial land on the squicr twin of a direct stcpr tunnel — same host, same link, same bottleneck — by excluding a sibling’s peer rather than only its specific transport.
4983 found the ranking itself still wrong in one case: scoring only a candidate’s first hop put an unmeasured one-millisecond LAN neighbour ahead of a 470ms intermediate the ranking was built to avoid, so a candidate is now scored on its full summed path latency, and a first hop on the visor’s own local network is refused outright as sharing the very uplink the second tunnel exists to escape.
4967 fixed proxy mux --rg matching a route group by its source port, which is identical across every tunnel a client dials to one exit — the destination port is what is actually unique per group, and what the mux event log already reports — so the selector could never name one specific tunnel out of several despite --tunnels existing to shape exactly that.
Skywire: A Standby Pool Instead of a Cold Redial
4986 started dialing sibling tunnels beyond the active set and holding them open rather than discarding the extra candidates a diversify dial finds, each one a fully noise-handshaked, yamux-pinged route group costing about ten bytes a second to keep current — a route already held can be switched in instantly where a fresh dial costs eight to nine seconds through the setup node. The fill is bounded at a default pool of eight and stops at a transport ceiling well short of exhausting the candidate list, with nothing yet deciding which pool member should actually carry traffic.
4991 supplied that decision for the failure case: when an active tunnel dies, the best live standby is promoted in the very same tick rather than waiting on a fresh redial, and a replacement for the pool itself is dialed afterward without disturbing the active set — measured against the previous behavior, which had let a brand-new, unranked redial join the active set directly and routed three uploads at a twentieth of their reference speed onto it. The same PR made every tunnel switch an observable event rather than a silent reassignment.
4992 supplied the decision for the case where nothing has failed at all: the promoter compares the worst active tunnel against the best eligible standby on a sliding minimum of their yamux ping — a statistic chosen specifically because it only ever reads load as added latency, never as improvement — and swaps a standby in once it proves better by a fixed margin held over fifteen seconds. Because a standby tunnel has a measured RTT but never a measured capacity, an idle one is occasionally offered a single real stream, at most once a minute, so its ranking can eventually rest on evidence rather than guesswork alone.
Skywire: Uploads Learn to Survive a Dead Tunnel
Downloads had survived a cut tunnel since the campaign’s early range-splitting work; uploads had not, losing an object outright because a plain POST has no way to say where it left off. 4988 gave the bench sink the receiving half of a fix: an upload opting into chunked mode is addressed by offset, acknowledged per chunk through a durable contiguous prefix, and resumable from a probe that reports exactly how far that prefix has reached.
4997 gave the client the sending half, striping an opted-in upload into four-megabyte chunks spread across the active tunnels by the same capacity-weighted pick a download chunk already gets, re-sending a chunk on a live tunnel the instant its own tunnel dies rather than losing the object — measured on the rig, three uploads that previously lost 30 to 45 percent of a 50MB object to a single tunnel cut now completed and hashed correctly, with the sink naming exactly which chunk offsets it had to evict so a sender does not have to wait out a five-second backstop to notice.
4999 closed four smaller but serious correctness gaps the new upload path exposed: a session with a chunk mid-read could be swept as idle and silently orphaned, a chunk held ahead of the frontier was answered with the same success code that means “durable” and so was never kept around for a resend, a sealed session kept consuming a concurrency slot it no longer needed, and a refused request’s unread body could desynchronize the connection entirely; all four are fixed, and the sink’s advertised window moved from 16 to 64MiB once the arithmetic showed the smaller value was the actual bottleneck rather than the client’s own memory cap. 5008 sized a striped upload’s chunks from the object itself rather than a fixed four-megabyte step, the same fix 4995 had already made for downloads, after measuring a ten-megabyte upload split 2+1 across two tunnels finish at one tunnel’s rate.
Skywire: Chunk Sizing, Router Correctness, and a Run of Lifecycle Leaks
4995 carried the object-aware chunk sizing into downloads properly, roughly two chunks per active tunnel rather than a fixed step, while walking back three more ambitious changes that had been built and measured on the same rig and simply lost — confining a burst to one leg, re-driving a leg’s send window after a lull, and a client-side exit-stream pool — each quantified against the reference and removed rather than kept on the strength of intuition alone. The same PR fixed two real defects found while measuring the walk-back: a chunk that failed partway through used to discard the bytes it had already received and restart from zero, and a standby promotion was waiting on the next scheduled liveness check rather than firing at the close it was meant to answer.
4998 fixed a receiver recording a packet as acknowledged before its own reorder buffer had decided whether to keep it, so a packet dropped at the reorder cap was SACKed anyway and the sender’s retransmit buffer purged the one copy that could have refilled the gap — a permanent zero-throughput wedge with no counter attached to it. The same PR found a route group’s close holding its lock across both a two-second broadcast and a two-second wait for the peer’s own close packets, during which the entire visor’s packet intake stalled behind it, and clamped the per-leg send window to a hard ceiling in every code path rather than only the one path that had been checked.
4996 found six separate leaks in how a skysocks tunnel’s lifecycle was managed: a dead session’s memory stayed pinned for the client’s whole life because only its meter was cleared, a dial landing after the client had already closed could attach a live route group nothing would ever reap, and an app’s mux-event RPC had no ownership check at all, letting any app on the visor re-stamp another app’s tunnel role.
4974 made a tunnel’s death its own distinct error on the chunk path, so a chunk whose tunnel died mid-fetch refetches on a live one immediately rather than sleeping through a backoff it had already effectively failed. 4975 made stopping an app actually close its route groups instead of leaving them registered and self-renewing forever, which had been silently blocking a subsequent proxy start --route from running at all. 5001 stopped the diversify ranking from sorting a candidate with one unmeasured hop all the way to the bottom alongside candidates with nothing measured at all, which had cost a dial a route that had in fact measured faster minutes earlier.
5004 gave the router a short memory for a route that died within its own handshake timeout without carrying a single byte, since without one a standby-pool fill kept re-picking the identical dead path three times in a row.
Skywire: A Live-Tunable Surface for the Bench, and Release Hardening
5002 answered the day’s own operational complaint: six of the previous two days’ rig cycles had changed nothing but a constant, and each still cost a rebuild, a binary push and a restart. A registry of atomics now backs every tunable the bench had been recompiling to change, pulled by the running app on its existing five-second keepalive tick rather than pushed through any new channel, exposed as skywire cli proxy settings and the router-side route settings; nothing is persisted, and a client that sets nothing behaves exactly as it did before the registry existed.
4990 found that v1.3.94’s released linux binary, and the wasm module embedded in it, both carried vcs.modified=true — the release job was dirtying its own tree by unpacking a toolchain into the workspace before building — and fixed the build to run from CI’s scratch directory, assert a clean tree before every in-tree build, and refuse a PR that adds a stray multi-megabyte blob outside the five paths that are supposed to carry one.
4973 fixed the standalone dmsg socks client, found to be serving a SOCKS5 listener locally and dialing every CONNECT with a plain net dialer from the client’s own host — meaning a tool built specifically to carry SOCKS traffic over dmsg had never actually put a byte on dmsg at all; it now opens one dmsg stream per accepted connection and pipes bytes both ways; the server side, it turned out, had been correct the whole time.
Skywire: Measuring All of It
The rest of the day’s fifteen PRs were the measurement half of the same campaign rather than new behavior: paired reference rows taken immediately alongside each measured one rather than in a separate time window where route quality could have drifted (4987), a standby-pool set with a mid-transfer cut and failover assertions (4985), the closing runs and scoreboard that justified shipping two tunnels as the default (4980, 4984), direction and exit-resource analysis scripts that read both ends of a transfer’s own counters (4993, 4994, 5009), sweep and tuning tooling that lets a knob be swept across values or searched with a bandit sampler without hand-editing a script for each run (5006, 5007, 5015), a corrected cut timing and a ceiling-aware verdict scheme once the criteria were revised (5003, 5011), and two changelog-and-results passes tying the day’s forty-odd PRs back to the test plan’s own scoreboard (4976, 5005).