Remote access: direct, relayed, authorized once

Implementation notes·about 1202 words

Away from home

The usual way to watch what is on your home NAS while you are out: install a mesh-VPN app, or open a system VPN, or forward a port. A mesh app means another background process; a system VPN has a single slot and fights with the work VPN; port forwarding puts home services on the public internet and leaves the security to you.

Panvox puts the tunnel inside the player itself: an embedded user-space networking component that lives and dies with the process. Switch Remote access on in settings, authorize once in the browser on first use; after that the switch brings it online silently. At home it stays on the local network and does not go around the tunnel; the tunnel takes over only when you are away.

Two lists, from two different places

A home has more than one machine. Which one should serve a given source? There are two lists here, with different origins, and they should not be mixed.

The node list comes from the networking layer: which nodes exist, their names, addresses, whether they are online, and whether a given link is direct or relayed. That layer knows this by itself.

The source list is something the networking layer does not know — it has no idea where your media lives. Each node reports what it can serve over a plain HTTP call to its management port; the client aggregates those reports into “for this source, these nodes can serve it”.

A source is identified by its address, not by who typed it in. Configure a WebDAV source pointing at the NAS on a PC, and the node that actually serves it is the one that can reach that address. Ownership is ranked by credibility: the machine that is that address → nodes on the same subnet → the always-on NAS or resident node → nodes that claim to have configured it → the only online node as a last resort. That order came out of a real failure: the phone that had configured the source was also away from home, displaced the resident node that was sitting next to the NAS, and the connection hung; moving the resident node ahead of “claims to have it” fixed it, and a unit test pins that scenario.

Direct, relayed, unreachable

Whether a link is punched peer-to-peer or relayed is the networking layer's call; the app only shows the result — every source is marked direct, relayed, or unreachable.

The networking layer only connects machines; it does not know about media sources. A source is not a node, so each node runs a gateway port: it lands the incoming connection on the source's real address. On the engine side exactly one thing changes — the HTTP proxy points at that local port, and the URL stays byte-for-byte the same. Changing both at once would collide.

A local folder is a different case: it has no host:port at all. The node starts a read-only file service on the network, exposes the configured directory, and the client takes it as a WebDAV source — reusing the playback path that already exists.

Public sources stay out of the tunnel. IPTV, live streams and public URLs are already out there; routing them home is pointless, so they connect directly.

Authorize once, keep one long-lived identity

The state machine was rebuilt once, because the device list kept growing and the old entries kept turning grey.

The old logic: if this machine had been online and then stayed offline for a while, it decided the identity was probably invalid; the settings page rendered that as “tap to reauthorize”. Tapping it wiped the state directory — and in this network, the state directory is the identity. A brief network hiccup therefore registered a brand-new device; the new device collided with the old name, got a -1 or -2 suffix, and the old one stayed grey forever. A UI hint about connectivity turned into one more historical device per tap.

After the rebuild there are seven states: not bundled, off, unauthorized, authorized · connecting, connected, authorized · unreachable, identity expired. “Unreachable” offers exactly one action: retry, without touching the identity. Wiping the identity happens only when the control plane explicitly says it does not recognize this node, and only after a second confirmation. Authorize once, and restarts, upgrades and network changes all reuse that same identity.

Naming got a fix as well: every phone used to be called panvox-localhost, colliding on every reinstall. Now a phone generates a name like panvox-iphone-xxxx on first launch and keeps it; desktops keep the OS hostname, which was already readable and distinguishable.

Things that only show up on real networks

Almost none of the pitfalls in this feature live in unit tests.

One is proxy software: some proxies resolve the networking control-plane domain into a fake-address range, and the control-plane dial binds its socket to the default interface, where the route does not match. The connection never returns; the tunnel sits at “connecting” with no error shown. It is an environment problem, but the product owes the user a diagnosis: when the control plane cannot dial and local DNS answers inside the fake range, it should say “add the networking domain to your proxy's fake-ip bypass”.

Another is ordering: the gateway address only exists after the tunnel comes online. A cold start that opens a file immediately gives the engine an empty proxy, which dials the private address directly and times out. The open path now checks: private source, not on this machine's subnet, tunnel not online yet — wait for it to come online before handing the address to the engine. At home, on the same subnet, it does not wait.

The most expensive one was heat. Two independent root causes, both “holding a core while waiting”. One was in the engine: a wait loop that spun on yield, burning a full core whenever an open got stuck — the fix is in the open-source repo, see the avox commit. The other was in the tunnel's mDNS read loop: after the app was suspended and resumed, a failed read retried immediately with no backoff, and two loops ate exactly two cores. The fix is the same family as the engine one — back off on failure, stop retrying closed sockets.

Those two incidents produced a watchdog. Whole-process CPU cannot be the criterion: decoding 4K is legitimate work and looks the same as spinning. Two gates instead: CPU spent in the networking layer's own code, and whether the data plane is actually moving bytes. Only when both go wrong does it dump a goroutine trace. It runs while idle too, writing one line in its log to prove it is alive.

Limits

Supported on Windows, iOS and macOS; Android comes later. There is no wake-on-demand: a machine that is off stays off, and what you can reach depends on having one always-on node at home. The app does not delete stale devices either — that needs admin credentials for the whole network and the deletion is irreversible, so it stays in the networking backend.

One tunnel, one authorization. Away from home, opening the app is opening your library.

Written for version 1.1.0; remote access is available on Windows, iOS and macOS.