Architecture
Relay and networking
What it is
A server dials out to the relay, and a device meets it there over an end-to-end encrypted channel. This path always works. Around it sit the name directory (<name>.vyre.run), the built-in Wink network that pairs devices and tries a direct path, and vyre doctor, which says which path is in use.
What a person sees
- Devices screen (
apps/app/app/devices.tsxwithapps/app/screens/devices/DevicesScreen.tsx, headed "Devices"): each paired device with "This device" or "This browser", whether it is connected through the relay or direct, and its round trip. Adding a device is "Add a device". - Sign in by name: a new device types the person's name and asks to sign in. Devices already signed in are asked "Sign in on [device]? These words should match", the new device shows the same three words, and one Approve adds it. A request lasts 2 minutes.
- Address:
vyre nameprints the name the server serves and its phase;vyre name check alexsays whetheralex.vyre.runis free. vyre doctorshows four network lines: Path to your server ("direct", or "through the relay ... (no direct path yet)"), Relay ("answering"), Server door and Clock. Each failure names one thing to do next (core/cli/commands/doctor.js).- A device that types a code instead of scanning uses
apps/app/screens/devices/TypeCode.tsx. - Settings, Devices group: "Web links through the relay last (days)" (default 30) and "Public address for signing pages and shared links" (
relay.tunnel_url).
How it works
Relay channel. The box holds two keys in ~/.vyre/relay/keys.json (mode 0600): box.key, the Noise static key, and route.key, which proves the route to the relay (core/relay/keys.js). core/relay/link.js keeps one signed control socket and one data socket per device connection the relay announces; a text ping every 60 s, two missed pings end the socket, reconnect backoff runs from 1 s to 5 minutes. Each data socket runs Noise Noise_IK_25519_AESGCM_SHA256 (core/relay/noise.js, core/relay/channel.js): the device is the initiator, so the relay sees ciphertext only. Frames are head, data, end, reset, ping, pong over numbered streams. core/relay/bridge.js turns each stream into a real HTTP request to the daemon's own router, with the caller the relay established, so every route and check works as on a local socket.
Admission. admit decides who gets in. A device is named device:<id>. core/relay/chain-admit.js admits a device that a person's identity chain lists, for an identity that already has a role on this server, with a signed hello bound to the box id, a 2 minute window and a replay set. A device that is not an owner's or not on a chain cannot reach the owner's powers: a browser paired from the hosted web app is limited until trusted from another device and expires after relay.web_expiry_days unused. The classic pairing code is removed; relay.join and the old vyre relay pair are refused with words naming the next step (core/relay/classic-removed.test.js).
Pairing as grants (Wink). core/wink/ makes every way in a Wink: a QR or pasted long code (wink.phone.open, wink.phone.scan), a typed two-sided code (wink.code.open, wink.code.redeem), an invite (wink.invite), lending a computer (wink.share), and storage devices (wink.storage.*). Both sides show the same three words and a person says yes (wink.phone.pair.answer). Devices belong to the identity and are enrolled per Space (core/wink/pairing.js, registry wink_devices). One removal (wink.remove) undoes the grant. Events include wink.offered, wink.joined, wink.removed, wink.pair-done, wink.signin-asked, wink.network-changed; the relay emits relay.connected, relay.disconnected, device.paired, device.removed, pairing.requested, relay.signin-asked.
Built-in network. core/wink/netd.js starts, on a server home, one Headscale per Space on loopback, a gate in front of it, a Wink node and the node host's door, with states off, no-binary, starting, up, failed; on any failure the relay still carries everything. network.wink.status, network.wink.whois, network.wink.join, network.wink.leave (core/network/wink.js) are the public names; join and leave need the owner and presence. Whois answers from the identity list and sessions the door admitted, never from tags a peer could set. The gate carries only POST /hooks/<route> and GET or HEAD /s/<token> from the internet.
Another VPN is optional. Vyre never installs, signs in to or asks for another VPN product. core/network/other-vpn.js asks the other client for its status and, if one is running, Wink does not start a node and the link stays on the relay.
Direct paths vs relay. core/wink/reach.js tries, in the background and in order, IPv6, UPnP and NAT-PMP. A path is called direct only after verify reached it from outside (the relay dials the address back, POST /v1/reach/check in relay/node/server.js); otherwise the state stays "relay" and tried says why. A router that reports a private or carrier-grade external address is said not to be reachable. relay.devices.path lets a paired device report which way it reaches the box and its round trip.
Names. core/names holds <name>.vyre.run: names.status, names.check, names.domain.check (anyone), names.serve, names.unserve (person). A server never holds a name; it serves a Space's name and asks the hosted directory (signed with the relay route key through relay.route.id and relay.route.sign) to point it at the server and write the certificate challenge. Events: name.claimed, name.moved, name.released. Backup and sealing helpers are in core/names/backup.js and core/names/seal.js.
Relay server. relay/worker/index.js is the Cloudflare Worker with a Durable Object per route; relay/node/server.js is the same protocol in plain Node for self-hosting (relay/README.md, relay/compose.yml). Routes: GET /v1/box, GET /v1/device, POST /v1/wink/code, POST /v1/pair, POST /v1/setup/mbx, GET /health. The relay keeps nothing on disk. relay/app/ serves the web app at app.vyre.run from static assets; relay/wink/ is the camera page for scanning a ring; relay/client/ is the device side.
Where the code lives
core/relay/: box side (index.js,link.js,channel.js,noise.js,bridge.js,chain-admit.js,peers.js,setup.js,keys.js).core/wink/: pairing, grants, storage devices,netd.js,reach.js, andcontrol/andnode/for the built-in network.core/network/: publicnetwork.wink.*names andother-vpn.js.core/names/:<name>.vyre.runservice and directory client.relay/worker/,relay/node/,relay/app/,relay/wink/,relay/client/,relay/deploy/: relay servers, web app host, camera page, device client, deploy files.core/cli/commands/doctor.js: the checks;lib/publish/tunnel.js: the tunnel end for public sites.
How agents use it
- Open to models:
relay.status,relay.setup.status,names.status,names.check,names.domain.check,network.wink.status,network.wink.whois(reads, no secrets). - Person-only:
relay.enable,relay.disable,relay.devices.list,relay.devices.remove,relay.devices.trust,network.wink.join,network.wink.leave,names.serve,names.unserve, and nearly everywink.*act, which needs presence. Module-only tools (relay.ticket.mint,relay.devices.all,relay.route.sign) are not reachable by a model. - An agent adds, removes and pairs no device. The person approves on a device. See authority (agent doc "authority") and tools (agent doc "tools").
Extend it
- In 0.5 the relay carries every connection. Direct paths (IPv6, UPnP, NAT-PMP) were tried on servers with public addresses, not behind a home router, so a connection may stay on the relay.
vyre doctorshows whether the path is direct. - Pairing runs by signing in with the person's name and approving on a device already signed in. Continue with GitHub needs the relay Worker and the escrow Worker deployed, and every server updated first.
- If the person runs another VPN, Wink starts no node and the link stays on the relay. A team server does not accept the key of a Windows PC with no Hello (Identity and devices). The phone apps are not yet walked on real devices.
- To add another way in or another kind of path, write a custom module. How to extend Vyre covers modules and Integrations for outside apps. Open items: known gaps.