Back to list
Skywire Development Update — October 3, 2026
The third of October was the day the browser visor became something a native visor can log into: pty exec against a tab now runs a real command and returns its output, pty fs mount can mount a tab’s filesystem from a native visor and a native visor’s filesystem from inside a tab, and the pty host now listens on skynet so those sessions stop going through the dmsg servers when a direct transport exists. Autoconnect stopped skipping visors that share this visor’s public IP and learned to give its scarce WebRTC-over-WebTransport slots to peers it already reaches, which is what lets a tab find the public visor running on its own machine. Both browser carriers gained the write bounds they had been missing, the deployment’s services config can now be followed over a CXO feed instead of an hourly poll, the module moved to Go 1.27, and the browser’s wasm module shrank by a fifth by leaving out commands a browser could never run.
Skywire: A Shell and a Filesystem on a Browser Visor
5354 changed the bridge behind pty start, pty exec and pty fs mount to try a skynet transport first, direct or through one relay, the same order DmsgHTTP already follows, and fall back to dmsg only when that fails. It also fixed pty exec against a browser visor coming back empty with exit code 255: a command that never starts now says why on stderr on every platform, and a browser visor runs skywire commands on the page’s own process layer while refusing anything else with a stated reason. Tested against a browser visor, pty exec returned the tab’s key, and a filesystem mount listed, read, wrote and copied 512 KB intact over the dmsg fallback.
That fallback was still the only path in practice, because the forwarding server on skynet had no pty port to dial. 5363 registered the pty host, sftp included, on that server, serving only direct streams, since a relay splices a relayed stream’s bytes in the clear and this is a shell. Mounting a browser visor from a native one over a direct transport, against dmsg in the same session, a directory listing went from 14.4 seconds to 0.19, a 10 KB read from 4.1 seconds to 0.03, and a 512 KB copy from 29.5 seconds to 1.2.
5359 made the other direction work. pty fs mount had been a Linux-only FUSE command; in a browser visor it now serves a directory of the page’s filesystem from the peer’s sftp subsystem through a new jsfs.mount call in the bottle fork, with every provider call answered from a goroutine so the page is never held up on the network. Tested against a native visor, a tab read a file from the native disk, got ENOENT for a missing one, and wrote a 10.5 KB generated config onto it that read back intact. 5362 then closed the gap that PR left open: every skywire command runs in the exec worker on that thread’s filesystem, while the desk shell’s own builtins read the page’s separate tree, so a mount under /mnt was invisible to ls and /opt/skywire showed stale seed files. The page now mounts the worker’s /mnt and /opt/skywire over the existing worker channel, which needs no SharedArrayBuffer or cross-origin isolation.
5367 made a mount survive its session dropping. A mount used to hold one sftp session for its whole life, so a peer restart turned every operation into EIO until it was remade. It now keeps a dial function, redials and retries once on a lost session, and reopens open files on the new one, which is safe because FUSE reads and writes carry their own offsets; reloading a browser visor under a native mount with a file held open left the mount and the file both working again.
5352 made config set pty.whitelist=[...] apply on the running visor, admitting added keys at once and removing trust from dropped ones unless they are the visor itself or a hypervisor. It also fixed config set being undone on package installs and in the browser, where skywire autoconfig rebuilds skywire.json from /etc/skywire.conf on every update, which in a browser means every page load. Only hypervisors had been mirrored back into skywire.conf; every field that has an autoconfig flag now is.
Skywire: Autoconnect Finds Peers on the Same Network
5353 removed autoconnect’s rule of skipping every visor that shares this visor’s public IP. The dialers already try LAN addresses first for such a peer, and the browser dialer tries IPv6 first, so the skip only cost links: a tab on the same machine as a public native visor reached it only over WebRTC, while adding a WebRTC-over-WebTransport transport to it by hand worked in about a second. The same change ordered that pass so peers already reached over WebRTC come first, since among roughly a thousand public visors and twenty slots any particular one was rarely chosen. 5355 made the ordering hold: reach verdicts were reshuffling the pass, and a peer reached only over dmsg was not counted at all, so peers already reached over WebRTC or dmsg now get a pass of their own before the rest of the public list.
Removing the same-NAT skip had a side effect in the Docker integration suite, where 5361 found autoconnect leaving SUDPH transports on both legs of the multihop test, which route setup could then pick even though SUDPH carries nothing in that environment; the test now removes every transport but STCPR on both legs. 5365 fixed a public visor logging “Pending hypervisor” for browser visors from around the network with no desk attached. The pairing scan treated every open WebSocket transport as a desk tab, and browser visors dial those to public visors through autoconnect, so now only a connection upgraded through the hypervisor’s own /tp/ws endpoint is marked as a pending desk.
Skywire: Writes With a Bound in the Browser
5356 found a browser WebTransport write waiting on a promise that resolves only once the peer drains, with SetWriteDeadline a no-op, so a peer that stopped draining parked the writing goroutine for good. The dmsg Noise handshake depends on such a bound to unblock a stuck read or write on timeout, so a stuck browser write left the handshake unable to return its own timeout. WebTransport has no per-write cancel, so the deadline now aborts the writer, which rejects the pending write, and four new tests under Node fail without the fix. The same change carried smaller fixes found alongside it: build tags on native-only tests so two packages vet under GOOS=js again, a ticker no longer leaked per VPN client reconnect, a timeout on the hypervisor’s transport-graph dmsg client, and an empty dmsg server config no longer advertising the discovery URL as a server address. 5357 gave the remaining native-only test files in the transport network package the same treatment, so the whole package now passes under js.
5358 turned to WebRTC, where neither end bounded what it queued: under load a tab leg measured 200 ms idle and 21 seconds under load, the mux resent into that queue, and a browser closes a channel whose queue overflows. The browser carrier now waits above 1 MiB queued until the channel drains below 256 KiB, reading the count directly or, in the exec worker, through the page, and native pion now uses blocking data channel writes so a write waits for SCTP to take the previous one. It adds Node-run tests for the browser half and a new make test-wasm target that runs selected js/wasm packages under Node. The PR itself notes that the native change alters write semantics for every native visor and that its throughput effect has not yet been measured with a clean single-transport bulk test.
Skywire: Desk and Dashboard
5351 made hv serve append each desk boot report as a JSON line to a file, since the journal on the host serving the public desk keeps only about an hour and reports were rotating away before anyone read them; the file sits under systemd’s state directory when one is set and rotates at 1 MiB. 5366 fixed the dashboard showing “Unexpected token ‘<’” when the hypervisor was not listening: the service worker’s network-first fallback answered every failed GET with the cached page shell, API calls included. API calls now bypass the worker entirely, and only page loads fall back to the shell.
5368 turned the mailbox from a fixed part of the visor into an in-process launcher app named skymail, run the same way pty and the resolving proxies are so it still works in a tab. It now shows in cli visor app ls and the dashboard’s app list, starts and stops like any other app, autostarts according to the mailbox’s own enable setting, and cli mail settings enable= starts or stops it.
Skywire: Following the Deployment, and Two-Hop Routes Everywhere
5376 made the conf service publish the services config it serves as a CXO feed on its own dmsg port, signed by its key and republished only on change, with visors holding one subscription to it and applying a changed config as it arrives. Applying now covers every deployment service address, not only the key sets: a field counts as the deployment’s when it holds the previously applied value, the embedded default, or nothing, and anything else is treated as the operator’s own and kept. Key sets take effect at once and changed addresses on restart. The PR also found that the existing hourly refresh, kept as the fallback, had never applied anything, because it expected an envelope the conf service does not actually serve; it now reads either shape, and the clearnet HTTP fallback is gone. No deployment keys change here.
5378 made the two-hop route path that uses the destination’s own transports always on and removed its config switch. Config generation had set it, but any config without it computed every route through transport discovery; a dial the oracle cannot serve still falls through to the route finder, and a flush drops the retired key from existing files. 5379 cleared a CodeQL allocation-size alert in the transport discovery routing publisher that had been failing the CodeQL check on every PR since it was introduced, by sizing the batch for the puts alone.
Skywire: Go 1.27, Dependencies, and a Smaller Browser Module
5364 moved every dependency to its latest version for the host, js/wasm, windows and darwin builds, twenty-one modules and none by a major version, with gvisor kept at its September pin because its newest published module still ships an assembly template the Go assembler cannot build. 5369 pinned the 0magnet modules to their new tags and the forks to the commits where their upstreams were merged in, which brought bash-compatible heredocs and real PIDs for $! through the shell fork and moved the go line to 1.26.6, which the u-root fork requires. 5370 followed by moving the Docker images and F-Droid recipe to Go 1.26.8, since they had been pinned below that and failed the e2e and Deploy runs. 5374 then moved the module, the images and the F-Droid recipe to Go 1.27.1, matching what most CI jobs were already testing with and bringing the goroutine leak profile that later changes use. 5360 dropped stale bottle hashes from go.sum that had left the module integrity check red on develop.
5377 shrank the browser module from 189.7 MB to 151.1 MB raw, and from 41.9 MB to 35.2 MB gzipped, by leaving out code a browser cannot run. The service and rewards commands, which need Redis, Postgres, systemd or a Telegram bot, now register only outside js, and the visor log server and dmsg http moved from Gin to net/http, dropping a set of codecs and a validator nothing used. That move also fixed dmsg http --wl, which had logged its whitelisted keys without ever checking them. Native builds keep every command; the PR notes it was not checked against a live visor.
Skywire: Tests on a Fake Clock
Several flaky tests were fixed. 5371 made the skysocks endgame test’s tail chunk wait for the duplicate it checks for, and 5375 then moved that test and the redial tests into testing/synctest bubbles, turning their sleeps into waits. 5372 made a dmsg mutual peer test take its snapshot only after each server has written its peer-session entry both times, so a normal second write no longer looks like churn. 5384 ran TestStreamBench in synctest bubbles over an in-memory network, cutting it from minutes to about three seconds, and the bubble surfaced real bugs fixed in the same change: the yamux fork held a stream’s receive lock while reading a frame body, so a read could not see its deadline, which had been the multi-second gap in the standby-cut case and is now under 400 ms; yamux also kept a process-wide timer pool, and skysocks client goroutines re-entered Close.
Skywire: How Skywire Compares
5380, 5381 and 5382 rewrote the README’s comparison with other overlay networks, first as two descriptive tables and then as a capability matrix covering dialing by key, needing no open port, multi-hop, multipath, carrying IP, serving by key, needing no central service, hole punching, internet exit, running in a browser, private networks and node rewards, with a second matrix of carriers from TCP and QUIC to WebRTC, raw Ethernet, serial and radio. iroh, Reticulum, Pollen, libp2p and ZeroTier joined the projects already compared, with cells checked against each project’s own documentation and one project dropped because its docs did not settle two of its cells.