Back to list
Skywire Development Update — September 22, 2026
After the weekend’s quiet, the mux campaign returned with a new idea: the standby pool a proxy session already holds is not just stream-level insurance, it is a ready source of packet-level mux legs for whichever tunnel is actually under load, and most of the day went into building and then correcting that connection. A long-lived WebRTC leak that had been slowly starving two public visors of memory and CPU was tracked down and fixed. A Wisp server and client shipped, letting a browser-side Linux guest’s network traffic ride a skywire route to an exit instead of whatever central relay it shipped with. The hypervisor UI picked up three fixes for how it behaves when served through a virtual-loopback service worker rather than the plain network, and an in-process emulated version of the mux bench joined the CI lanes so a scheduler regression no longer needs a day on the live rig to be noticed.
Skywire: The Standby Pool Becomes a Source of Mux Legs
5082 gave an active tunnel wanting another packet-level leg a shortcut around the usual route-finder round trip: the app’s own standby pool is already holding dozens of ranked, measured, live tunnels to the same exit, so their routes are now seeded into the visor’s warm-route pool and a grow operation can dial a known path with no fresh lookup at all. 5085 went further and let an active group adopt a standby tunnel’s entire already-built route chain in place, as one more mux leg, by rewriting the single rule that binds the chain to a route group rather than tearing anything down — negotiated so a peer that doesn’t support it simply falls back to dialing fresh.
5089 then corrected an assumption underneath both: with the visor’s mux width set to two, the self-heal logic had been growing a second leg on every single pooled tunnel, leaving thirty-two tunnels holding sixty-four legs to one exit and visible damage in reorder wedges and truncated downloads. A standby tunnel is a stream reserve, not a mux group, so widening is now purely an active-tunnel power; a demoted tunnel sheds its extra legs, and the pool is instead put to work the other way — when an active tunnel’s load genuinely latches, an arbiter borrows the best-ranked pooled tunnel as a leg and returns it once the load clears.
5094 and 5096 closed two races that let a tunnel get widened before the router even knew it belonged to the pool: a tunnel’s standby-or-active role was being read before the app had actually labeled it, so a brief keepalive could latch the load signal and a role-less pooled tunnel could be widened before its label ever arrived; the role is now seeded at the moment the group is created rather than reported after the fact. 5100 closed the gap that let this regression ship unnoticed for three thirty-minute rig runs, by making the emulated bench assert a standby tunnel’s leg count directly rather than only its throughput.
Skywire: Fixing the Forward Direction Under Load
5086 found the mechanism meant to give a heavy upload the aggregated mux structurally unable to do so: a flip controller could only move which class of leg a direction prefers, but in a group with no direct leg every leg is the same class, so flipping changed nothing and every upload row rode entirely on one leg regardless. Fanning the forward direction over several legs is now triggered directly by load — a leg that keeps filling its send window for three hundred milliseconds opens a band of similarly-fast siblings to the existing scheduler, and releases them again once nothing has been written for two seconds. 5087 found a real defect exposed while diagnosing that fan-out: the fallback inside the new band scanned its legs round-robin rather than by capacity, so legs of unequal speed drew equal shares and a slower leg’s larger bandwidth-delay window made it look unsaturated the longest, pushing frames out of order behind the receiver’s reorder frontier. The band is now ordered fastest-first.
Skywire: A Capacity Prior Before Promotion, and the Spread Policy Ships
5083 fixed a tunnel that had never carried a byte being credited as the best capacity present rather than an unknown one, since a standby tunnel by definition has nothing for its meter to sample — which is exactly why the spread policy had collapsed when it was first measured as a default. Every transport already carries its own observed throughput, so that number now becomes a tunnel’s capacity prior ahead of any real measurement, read over the same channel an app already uses to see its own tunnels, and an unmeasured route with no prior at all gets exactly one probe chunk rather than being assumed fastest. With that fixed, 5054 shipped the spread policy on by default: a session now spreads an object over at least three routes with no single route carrying more than forty percent of it, merged only once the rig’s own measurement cleared the eighty-percent bar the criterion requires.
Skywire: Verifying What the Mux Actually Delivers
5104 closed a gap that per-frame encryption never covered: a sealed frame catches corruption on the wire, but nothing had ever checked that the bytes handed to the application after reassembly were the exact bytes the sender wrote, in order — a reorder or flush defect could stay invisible until an application’s own hash check failed somewhere downstream. A sequenced frame’s payload now carries a small checksum stamped before encryption, so a retransmit or an FEC reconstruction of the same sequence reproduces identical bytes and a mismatch at delivery time is caught, counted and logged rather than silently passed up. Older peers that never advertise the capability are unaffected.
Skywire: Watching the Pool and the Mux From Outside
5101 added a dedicated view of the standby pool to visor state — one row per tunnel with its first hop, hop count, capacity prior and audition age — built from the same data the mux section already computes, plus whole-router counters for promotions, leg re-homes and fan-out transitions. 5105 put each tunnel’s live stream count directly on its line in the status.skysocks route tree, and added a per-app knob letting one app’s mux width differ from the visor-wide default. 5103 registered the last handful of mux and router constants that had still been compiled in as live, settable knobs, a purely mechanical follow-up with no behavior change unless an operator actually sets one. 5110 fixed the new counters from 5101 printing as a second, separate JSON value after a command’s main output, which silently broke any script piping that output through jq.
Skywire: Testing the Mux Bench Without the Rig
5084 moved the live mux bench’s own cells into an in-process test, running a real skysocks client and server over emulated links with constant delay, rate and loss instead of the frozen rig — cutting a measurement that used to cost minutes and swing twofold within an hour down to about forty seconds and made fully deterministic, with every object hash still a hard assertion. 5098 gave that emulator and the stream bench their own CI lane, so a mux regression now shows up as a lane named after the specific cell that broke rather than a buried line inside the general linux suite. 5112 fixed both new lanes on the same day they shipped — a knob round-trip test that could not parse its own empty-list default back, and a timeout tuned for the live rig that the in-process runner could not actually meet.
Skywire: Wisp, a Browser-Linux Backend Carried Over Skywire
5088 shipped a server for the Wisp protocol, which a browser-side Linux emulator uses to ask a backend to open TCP and UDP connections by name on a guest kernel’s behalf; serving it from skywire cli wisp means that guest’s traffic can ride a route to a skywire exit instead of whatever central relay the page shipped with. 5097 made the package itself buildable under GOOS=js at all, since the browser’s own WebSocket implementation is missing fields the library expected and a server cannot even be constructed in a tab — the fix carries a session over any plain connection rather than requiring a WebSocket specifically, verified by actually running a full session inside a real js/wasm runtime rather than merely compiling for one. 5090 added the other half, a Wisp client that lets a visor consume someone else’s Wisp backend and present it locally as an ordinary SOCKS5 proxy.
UDP needed its own path. 5093 taught skysocks to relay a UDP datagram across an exit at all, something its existing SOCKS5 implementation had never supported, by splitting the RFC 1928 association into a framed yamux stream on the client side and an unconnected socket on the exit side, with the destination named per datagram exactly as a client’s own SOCKS5 encapsulation already does. 5095 gave the Wisp client’s own local proxy the same ability, since the SOCKS5 library it had been built on simply answered UDP ASSOCIATE as unsupported. 5099 put a Wisp server directly inside the wasm visor itself, bound to a virtual-loopback port a browser tab can reach with ordinary page JavaScript, since a WebSocket server cannot exist in a tab and a service worker cannot intercept one even if it did — fixing the js build of the standalone CLI command along the way, which had quietly broken two PRs earlier.
Skywire: The Hypervisor UI Behind a Virtual Loopback
5078 found the hypervisor UI served through the wasm visor’s own virtual-network service worker reloading itself repeatedly and flashing a spurious “lost connection to the Hypervisor” toast, because a forbidden header the UI relies on to tell a framed request from a top-level one cannot survive passage through a service worker at all; a forwarded equivalent header fixes the detection without waiting on a browser standard to change.
5079 found the same blind spot hiding in the wallet’s own top-level-navigation bounce, in a different file with a different build tag, so it needed its own small fix moved to a tag-free file both the mobile and browser builds could share. 5080 stopped that same connectivity toast firing for an entirely different, benign event — the virtual-network worker’s own eight-second timeout answering with a 504 while the one browser tab holding its port table was simply busy rebooting, not actually disconnected from anything. 5102 fixed a UI auto-reloader that had been looping every thirty seconds under the same virtual-loopback mount, because it fetched an absolute path that resolved to the outer page’s own 404 rather than the relative one that would have reached the booted version stamp.
Skywire: A Long-Lived WebRTC Leak, and Smaller Fixes
5077 tracked down why two public visors were sitting at ninety-six percent CPU with over thirty thousand goroutines apiece: closing a WebRTC connection after it had already failed returned early on a guard meant for an ordinary double-close, which meant the entire PeerConnection underneath it — its ICE agent, DTLS, SCTP and SRTP machinery, around thirteen goroutines each — was never released. Roughly ninety-one percent of the live PeerConnections on those two visors had no transport behind them at all, accumulating at five hundred to six hundred new ones a day; the fix separates “this connection has ended” from “its resources are freed” into two distinct events rather than one flag standing for both.
5076 made help a real, working command at every level of the roughly 550-command CLI tree rather than on six root commands only, and let it print the same help text without the ASCII-rain backdrop that --help draws, so a plain screen is now one word away instead of behind an environment variable. 5081 dropped two stale hashes a dependency bump had left behind in go.sum, which was failing the module-tidiness CI check even though the build itself was already correct.
Skywire: Closing the Day’s Campaign
5109 and 5111 closed out the day’s documentation: changelog entries for the two dozen PRs above, a new results section in the test plan covering the standby-pool ceiling raise, the pool-sourced legs, the delivery checksum and the new CI lane, and the final bench chains run against the day’s last commit, together with three new tickets naming what is still open — chunked upload still rides a single tunnel, ten-megabyte objects still trail the single-route reference under the pool, and the emulator still cannot exercise every widening path the live router can take.