Back to list
Skywire Development Update — September 9, 2026
Fifty-one merges, most of them pushing on one thread: making a dmsg session ride the mesh rather than the clearnet underneath it. Every visor now serves a relay acceptor on dmsg port 70 over skynet, a client reaches one through a session-dial hook the visor fills with an appnet dial, and the peers a visor already holds a pinned transport to are nominated as its relays with no configuration at all — which then took nine further fixes before a desk tab actually put its streams on the relay instead of on nine server sessions it did not need. By the end of the day a browser visor held exactly one dmsg session and had no discovery entry, because it no longer publishes one. Hypervisor pairing landed beside it, and the js/wasm command module the desk runs was folded into the native binary so a desk page loads one URL instead of chasing a file nothing shipped.
Skywire: A dmsg Session Carried Over Skynet
4704 is the substance of it, and the notable thing is how little of dmsg it touches. A client seeded with a server entry whose address reads skynet://<pk>:70 dials it through a new session-dial hook that the visor fills with an appnet skynet dial; the conn is wrapped in the usual Noise and yamux session and nothing above that layer changes. No discovery, no registration, and the relay is left out of the delegated servers the client publishes, so attaching to one advertises nothing.
Every visor serves the other side: an acceptor on dmsg port 70 over skynet, gated by the peer whitelist, answering with a server-role session on its own dmsg client and carrying each stream request over one of its own server sessions, ordered by the destination’s delegated servers and bounded at 256 relay slots. Peers attached to the same relay are bridged locally, and because the server accepts a forwarded request under the dialer’s key, the destination sees the dialer rather than the relay. A refused request is closed at once — a rejection carries no response the dialer could verify — so the dialer moves to its next phase instead of waiting out the handshake timeout. 4705 then admits one relationship that needs no configuration: a visor this node manages as its hypervisor, which already grants RPC and a shell, so relaying its dmsg streams is the hypervisor’s half of that bargain.
Skywire: Relays Nominated From Live Transports
4708 removes configuration from the picture. A visor nominates as dmsg relays the peers it already holds a live transport to, re-derived every ten seconds; the client dials nominees first, counts a relay session toward its floor, prefers it for every stream dial, never idle-reaps it, and backs one off for five minutes when it refuses, with configured servers staying the bootstrap and the fallback. 4709 narrowed the set the same day, after the host nominated a browser tab and a fleet visor as its own relays and attached to the fleet visor seconds after restarting: the hypervisor list includes the desk tabs a hypervisor serves, so nominees are persistent-transport peers only now — the operator pinned those deliberately, and a tab’s attach to the visor serving it is exactly one such pin.
Making a nomination take effect was three more bugs in the serve loop. 4710 found the loop parks inside its per-server walk once the session floor is met, so a nomination woke it mid-walk and it carried on down the list, dialing every remaining server and never the relay — which is why the attached desk tab sat at nine and ten server sessions. 4713 keys the pass on a nomination generation rather than the sentinel the loop hears only while parked. The tab’s first relay dial then raced the router’s own cold start into the ten-second dial timeout and was penalized for five minutes as though it had refused, while the same dial minutes later took about a second: 4715 bounds it at thirty seconds and retries a timeout after thirty, and 4716 wakes the loop when a lapsed backoff leaves a nominee pending.
With a relay attached the streams still avoided it. 4714 measured the tab putting 24 and 6 streams on its server sessions against 1 on the relay — the deployment services publish no client entry, so dials to them took the latency-sorted connected-servers fallback — and tries relay sessions first now, ahead of the discovery lookup, because the relay resolves the destination itself. 4718 fixed the mirror case, where the relay walked its own servers in latency order and every one that did not hold the service cost a handshake timeout: seven forward failures beside seven successes in the host log, the tab’s five-second health probes reading DOWN while the forward eventually succeeded behind them. 4722 covers the rollout window, since the two largest deployment dmsg servers, around 1,850 clients each, run builds predating the forwarded-request change and time out rather than answering or refusing.
Skywire: A Browser Visor With No Entry to Publish
Once a browser visor rides its host’s relay it is not dmsg-reachable, so the legacy route setup path — the setup node dialing the source back over dmsg to install its rules — cannot work for it. 4730 makes the source-driven cascade the default under js: the source installs its own rules locally and only asks the route setup node to sign over its own outbound dmsg. That is the prerequisite for 4731, which stops the browser visor registering at all — an entry would only advertise server sessions it does not hold — and drops the server-session floor for an unpublished client with a relay attached, so the idle reaper trims those sessions away entirely. The measured result is the stage’s acceptance criterion: exactly one dmsg session, and no entry in the discovery.
4740 qualifies that for the case which breaks the assumption. Off the LAN there is no same-origin transport to the host, so the tab’s visor has to be an ordinary dmsg client again; a relay-only mode replaces the flat no-register rule, holding a relay session as the only session while one is attached and otherwise publishing an entry and holding server sessions over wss like any client, so the host can still reach it as its hypervisor. The same change makes the hypervisor-served desk a progressive web app, with a service worker whose precache list comes from the serving context, so an installed desk opens off the LAN from cache.
Skywire: A Tab’s Transport Layer
Three fixes stopped a tab minting transports it could never use. 4707 compiles out, for js builds, the last resort in the best-transport walk that creates a dmsg-type transport when every direct type has failed: in a browser it fired on every on-demand dial and each one minted a transport nothing released, with one desk tab found holding 118 of them. 4712 found the route-setup hook reaching dmsg by its own preference walk whenever a peer advertises a direct type, and the tab back to four within minutes of a reload. 4721 skips a transport type the manager has no client for, which had been costing four seconds of futile stcpr retries per peer.
Two deeper defects sat under the stream layer. 4725 is a race with a twenty-second window: the dial path copied the route-ID-zero packet handlers onto a new managed transport before dialing and inserted it into the manager’s map only afterwards, while handler registration propagates by walking that map, so a handler registered during the dial was missed for the transport’s lifetime. A desk tab’s only transport is dialed at page load while initialization is still binding the skynet and app-direct muxes, so the tab dropped every app-direct ack from its host and its relay attaches failed.
4727 explains the strangest symptom of the day: stream writes put the whole payload in one packet and truncated the sixteen-bit size field, so a yamux frame or RPC reply over 64 KiB was written in full but declared short, the receiver parsed the excess as packet headers — payload bytes surfacing as packet type “Unknown(112)” — and the dmsg relay session riding that stream died complaining of an invalid protocol version. Writes are segmented at the packet limit now, streams are keyed by transport and id so a tab’s relay session and its host’s RPC dial back cannot collide, and a diag section in visor state makes the layer readable without grepping logs. Beside it, 4726 adds a goroutine dump over RPC, since a visor in a browser tab has no pprof listener, and 4728 moves three commands off a direct dial to localhost:3435, which fails on name resolution in a tab’s netstack.
Skywire: Every Working Exit Judged a Failure
The proxy auto-exit loop was rotating exits forever, and the reasons were found in a desk tab where a manual SOCKS fetch through the same port returned 200. 4720 is the blunt one: the verification probe sent a GET with no User-Agent and accepted only a 2xx or 3xx status, and neverssl.com answers a header-less request with 403 — curl -A "" http://neverssl.com/ returns 403, the same request with a user agent returns 200 — so every exit failed verification. The probe sends a user agent now and judges relay by whether a well-formed status line came back that the local client did not synthesize, which is all it can honestly prove.
4719 found the router tearing down the working session underneath it. The sole-leg black-hole reaper compared a route’s raw received-byte count against a 16 KiB floor, but that count includes the handshake, liveness pongs and SACKs, so a light session — a proxy client whose responses are a few hundred bytes — never cleared it: 366 bytes to 6 KB received per fifteen-second window in a desk tab, six reaps in two minutes, each followed within seconds by the proxy’s connection dying. The verdict rests on unique in-order payload bytes now. 4706 stops the loop stepping on the operator, which it had been doing by judging whatever sat behind the SOCKS port without checking the client was still its own candidate — so an explicit proxy start <pk> was rotated onto a random discovery exit, reproduced three times on a dev visor.
Skywire: Pairing a Hypervisor
4732 gives the hypervisor relationship a first-contact story instead of a configuration file. A peer holding a same-origin transport to this visor, or refused by the transport RPC for not being whitelisted, is recorded as a pending hypervisor; skywire cli visor hv pair lists pending keys by fingerprint, and approving one goes through the existing add path, which now also admits the key to the live peer whitelist so RPC and pty work immediately rather than after a restart. For a tab that cannot run the CLI, hv pair --code mints a one-time code redeemed at a pre-auth endpoint; codes expire, are single-use, and a few wrong attempts revoke every outstanding one. config gen also turns the UI password gate on for a freshly generated hypervisor config unless authentication is explicitly disabled. 4733 adds the desk half, a pair window and an identity window that exports or imports the tab’s key through the CLI, and makes removing a hypervisor drop its key from the live whitelist — the exact inverse of admission.
Three follow-ups make a pairing survive what used to erase it. 4735 carries the existing authentication setting through a config regeneration, which had been resetting an operator’s explicit choice on every autoconfig run, and 4746 mirrors a paired hypervisor into the package config file, since autoconfig rebuilds the visor config from it on every package update. 4748 fixes what that permanence exposed: a paired desk tab has no discovery entry once it is closed, and the visor kept dialing it every five seconds, one discovery lookup per attempt, forever, for every tab ever paired. That pause now doubles from one second up to one minute, with a direct transport from the peer cutting it short.
Skywire: One Module, One URL
The public wasm page had been serving the legacy boot page for a simple reason: the desk needs the full skywire command module built for GOOS=js, that module is too large to embed casually, and nothing published it anywhere. 4755 publishes it — the release build now produces a compressed js/wasm module beside the native binaries with the same toolchain and version stamping, and both hv serve and a visor-hosted desk default to the path the updater installs it at. 4756 goes further and embeds it in the native binary through a two-stage build: the module is built first, staged into an embed package, then the native binaries are built around it and serve it with gzip passthrough and an ETag from a content stamp. The cost is stated plainly — 161.6 MB raw, 35.6 MB gzipped, taking the native amd64 binary from 165.3 MB to 200.8 MB.
4757 collapses the last duplication: the desk host — shell, panels, nested browser, pairing windows, tab visor boot — moves into a library that the root js module runs as a hidden subcommand, so a desk page loads exactly one URL and spawns every command instance and the tab visor from the same module. Smoke-tested in Brave: only that module loaded, the desk ready 2.2 seconds after navigation, the tab visor’s hypervisor answering 39 seconds in, the module up from 161.6 MB to 168.4 MB with the desk host inside. 4724 fixes why none of this was visible during development — the served module is read from disk and only a reload runs a new one, the wasm-serve desk had no version stamp and no poller at all, and a desk tab was found sitting 22 hours on a blob rebuilt several times since; its size and modification time fold into both fingerprints now.
Skywire: The Desk in Use, and a Hypervisor That Knows Its Own Graph
Three changes came out of using the desk rather than building it. 4736 replaces the mode-and-exit panel with a single proxy field — the address of a SOCKS5 or HTTP proxy clearnet pages go through, or empty for the visor’s default egress. 4738 stops a hypervisor-served desk sending its own assets, down to a same-origin tab’s favicon, out to the host’s clearnet browse endpoint where the host then reached for a loopback of its own. 4742 fixes a deadlock that froze the whole page when the file browser opened: the pane listed its directory on the goroutine servicing the launch click, and on js/wasm the filesystem answers on a microtask that cannot run until that handler returns.
On the hypervisor side, 4751 turns data it already held into a routing input. It polls every attached visor’s summary every thirty seconds and each summary carries that visor’s transports with endpoints, type, label, latency and throughput, but nothing read them as a graph. Those entries are built into a local graph now — stale visors skipped, both ends of a shared edge deduplicated with merged metrics, the set versioned by a hash of its IDs — merged into every transport-discovery answer, so route calculation, the routing tools and the router’s own local search all see attached edges, and the router runs its local calculation before asking the route finder when the destination is in that graph; 4754 adds the counters that make the claim checkable. And 4747 folds a standalone dmsg server into the visor: the in-process server had been a bare instance on the visor key with no wss, no health endpoint and default session limits, while the deployment’s 1 GB hosts ran the visor and a separate server unit as two Go processes competing for memory, so pointing the config at the server’s own file now runs the full server service inside the visor.
The rest was upkeep and options: an opt-in legacy Angular dashboard at the hypervisor root for operators who do not want the desk, eight re-embeds of the committed wasm blob tracking the desk changes through the day, and a one-word spelling fix for a lint lane that only runs on darwin and windows.