Skip to main content
A node’s own interface answers only on the computer running it. The remote console lets a paired browser drive that interface from anywhere:
  • The node keeps an outbound WebSocket to the relay.
  • A browser opens a second WebSocket to the relay.
  • The two run an end-to-end encrypted session through it. The relay forwards bytes it cannot read or forge.
This is the contract between the node (panofx-node/internal/remote), the relay (WsRemoteGateway) and the console page (panofx-web/src/features/remote). Test vectors are in docs/remote-console-vectors.json. The operator’s steps are under Run on a server.

Encodings

Node identity is a long-lived P-256 key N; its channel id is id(N_pub). Device identity is a long-lived, non-extractable P-256 key D per browser; its device id is id(D_pub).

Relay protocol

All relay messages are JSON text frames; data fields carry opaque bytes as b64u. The relay keeps no database: routing lives in memory, on a single replica.
The hello must arrive within 10 s. A newer node connection for the same channel closes the older one with 4009; a node that receives 4009 waits 60 s. When a node disconnects, every device socket on its channel is closed with 4410 node_gone.
Close codes. 4001 hello timeout, 4002 bad message, 4003 bad hello, 4009 replaced, 4400 closed by the node, 4403 origin not allowed, 4404 bad channel id, 4410 node gone, 4413 frame too large, 4429 too many connections or messages. Limits. 8 devices per channel, 20 device sockets per IP, 700,000 characters of data, 50 messages/s per device (burst 200), 500/s per node (burst 2,000). A socket whose bufferedAmount stays over 4 MiB for 10 s is closed.

Session handshake

M1 and M2 are UTF-8 JSON. M3, M4 and every later frame are AES-256-GCM ciphertext. The 12-byte nonce is 0x00000000 ‖ counter (u64 big-endian); each key has its own counter from 0, incremented once per frame. The receiver accepts only the next counter, so a replayed, reordered or dropped frame ends the session, as does any decryption failure or a counter reaching 2^32.
1

M1, device → node

pair is true only when the device is pairing with a secret.
2

M2, node → device

The node makes an ephemeral ECDH key e_N and computes:
and sends {"t":"auth","v":1,"e":"<b64u e_N>","pub":"<b64u N_pub>","sig":"<b64u sig_N>"}, or refuses in the clear with {"t":"refused","reason":"busy"|"version"}. The device checks id(pub) == channel, recomputes th, verifies sig_N, and derives the same keys.
3

M3, device → node (under hk_d2n, AAD 'panofx-remote-v1 m3')

sig_D = Sign(D, "panofx-remote-v1/device" ‖ th). pair is present only when pairing: HMAC-SHA256(key = SHA-256("panofx-remote-v1/pair-secret" ‖ secret), "panofx-remote-v1/pair" ‖ th ‖ D_pub). The node accepts when sig_D verifies and either id(D_pub) is in the device list or pair matches an unexpired pending pairing. A pairing is consumed atomically on success; a failed proof never consumes it.
4

M4, node → device (under hk_n2d, AAD 'panofx-remote-v1 m4')

or {"t":"denied","reason":"not_paired"|"pairing_expired"|"bad_proof"|"revoked"|"bad_signature"}.
5

Traffic keys

Application frames use these with AAD "panofx-remote-v1 app". Ephemeral keys and dh are discarded. A session lasts at most 12 hours; the node then sends bye and the browser runs a new handshake.

Application messages

Request, device → node: {"t":"req","id":7,"method":"POST","path":"/api/settings","headers":{"x-panofx-passphrase":"…"},"body":"…"}. Paths are /api/... or a UI asset. A path containing %, .., // or a query-less ? is refused. The only header passed through is x-panofx-passphrase. Response, node → device: {"t":"res","id":7,"status":200,"type":"application/json","enc":"text"|"b64","body":"…","part":0,"last":true}. A body over 256,000 characters is split into parts. A whole response is at most 8 MiB. At most 4 requests run at once per session; more get status 429. Notice, node → device: {"t":"bye","reason":"revoked"|"expired"|"remote_off"|"shutting_down"}. A device never retries a request that changes something after the socket drops; it reports the result as unknown.

Pairing

A pairing secret is 32 random bytes as b64u. It lasts 10 minutes, or 15 minutes for the link printed at a headless first boot. The link is <console URL>/remote#pair=v1.<channel>.<secret>.<network id>; the console page removes the fragment at once, shows the fingerprint, and waits for a click. PANOFX_REMOTE_PAIR_SECRET (at least 20 characters) is a pairing secret set in the platform’s secret store. It is valid only while no device is paired; the log then prints only the channel id and fingerprint.

What a paired browser may do

Every remote session starts locked and is unlocked with the console passphrase. A route with no rule is refused. A test keeps the route table complete.

Threat model

The relay, or anyone on the path, can see which channels are online, when devices connect, message sizes and timing, and device IPs; and drop, delay or reorder traffic, each of which ends a session. It cannot read or change traffic, pose as a node (the channel id is the hash of the node’s key and M2 is signed) or pose as a device (M3 is signed and bound to the transcript). A phishing link can pair a victim’s browser with an attacker’s node, whose interface might ask for a recovery phrase. The fingerprint is shown on both sides, pairing needs an explicit click, and the console page shows its own warning whenever the node’s interface sends a phrase or key. A stolen or compromised paired browser has no path to the wallet’s secrets once setup is done, needs the console passphrase to do anything, and raises an alert on large changes. Revoke it from the node, or with panofx-node remote revoke. The console’s web deployment and its npm dependencies are trusted. The console runs on its own host, uses its own layout with no wallet libraries, has a nonce-based CSP, and shows the node’s interface in a sandboxed, opaque-origin frame that cannot reach the device key. A later version will move the device credential to a passkey. Addresses set remotely must resolve to public addresses. DNS rebinding after the check is not covered.