Skip to content
SorGuayteawLord edited this page Aug 21, 2026 · 2 revisions

Relay mode reaches your harness through dsh-relay — a plugin that runs beside the harness and asks who you are before letting anything through. It is the answer to the problem LAN Mode does not solve: that page serves a coding agent to everyone on your network.

Requires DSH Mobile 0.8.0 or newer and dsh-relay 0.1.1 or newer.

Why it exists

The harness is explicit that it has no authentication layer, and refuses dsh web --host 0.0.0.0 because doing so "would expose remote code execution to the network". LAN mode works around that refusal with a config patch. The relay is the layer itself: the harness keeps its loopback bind, and a second listener in front of it terminates TLS, checks a credential, and forwards.

That ordering matters. A relay that fails to start leaves the harness unreachable from the network — never open to it. And it refuses to start at all if it finds the harness already bound to 0.0.0.0, so you cannot accidentally run both.

What you get

  • A real credential. This device holds a bearer token issued once, at pairing, and sends it on every /api call and both event streams. The relay stores only a keyed hash of it.
  • Encryption, with the key pinned. The app pins the relay's public key rather than trusting a certificate authority, so a self-signed relay is verified rather than merely accepted.
  • Revocation you control. Every paired device is listed at /relay/devices with its own revoke button, and "sign out everywhere" invalidates every token at once.
  • It works from outside your Wi-Fi. Forward the relay's port and it keeps working; the app's "not on this phone's network" check is skipped for an endpoint that holds a token.

What it does not do is reduce what an authenticated client can reach. Pairing a phone gives it the same power as a terminal on that computer, because the agent runs commands there. The question the relay answers is "is the remote user you", not "how much can the remote user do".

Set it up

On the computer running the harness:

dsh plugin --profile web add dsh-relay
dsh web

The terminal prints the relay's addresses. Open one on that machine and set a password — until one exists the sign-in page is loopback-only, so nobody on the network can claim the relay first.

Then open /relay/pair on that machine. It shows a QR code and an eight-digit code; either works, and both expire after a few minutes.

Pair the phone

In the app: Relay → Pair a relay.

  • Scan QR code — point the camera at the code on the pairing page. This is the better route, and not only for convenience: see below.
  • Or type the code — enter the relay's address and the eight digits yourself. Useful when the camera is unavailable or the QR will not focus.

Name the device something you will recognise in the relay's device list, and tap Pair. The app stores the token and connects.

The two routes do not prove the same thing

The app says which one happened, because the difference is real:

What it establishes
Scanned QR The payload carries the relay's public key, so the very first byte the app sends is checked against it.
Typed address Carries no key — the relay only reveals one in its answer — so the certificate is trusted on first contact.

Trust on first contact means an attacker able to answer at that address during pairing would end up holding the enrolment. Over your own Wi-Fi that is a narrow window; over an untrusted network it is not. Scan the QR when you can.

If the relay is running tls: off, the app says so plainly: the token and everything you send travel in the clear.

Finding relays

Find relays browses for the relay's mDNS advertisement (_dsh._tcp), which carries the port, whether the listener speaks TLS, and the key to pin — so it answers in a second or two rather than sweeping a subnet.

Nothing depends on it arriving. Plenty of networks filter multicast and the relay's mdns flag can be turned off, so a quiet browse falls back to knocking the two ports a relay actually uses. It also knocks 127.0.0.1 first, which is where a relay reached through adb reverse answers.

A relay that answers but refuses the address you reached it by is listed anyway, with the entry to add to its configuration. That is the most recoverable thing a scan can find — see below.

When something goes wrong

The relay answers HTTP 403 for every unauthorised case and never 401, so the app works out which one it is from what it already knows about the address.

"The relay refused this device." Your token expired, or it was revoked from the relay's device list. Pair again. The app stops retrying rather than backing off forever — there is nothing to wait for.

"Refuses this address" / "…it refuses requests addressed this way." The relay only answers to authorities it knows: loopback, its own addresses, and whatever its operator listed. An emulator's 10.0.2.2, or a hostname it was never told about, gets refused before the pairing code is even read. Add the address to publicHostnames in the relay's cordis.patch.yml and restart it — or reach the relay by an address it already knows. In the harness log this looks like:

[dsh-relay] refused GET /relay/health from 127.0.0.1: untrusted-host

"This relay's key changed." The relay mints a new key whenever the set of addresses its certificate covers changes, so a laptop that moved networks produces this — and so would something else answering at that address. The app will not guess between them. Pair again only if you were expecting it.

Turn off the compatibility bridge

dsh-relay ships with compat.addressGrants on, which accepts requests from whatever address a paired device was last seen on. It exists for DSH Mobile 0.5.0, which had no field that could carry a token.

A source address is not authentication — it is shared behind NAT, reassigned by DHCP, rotated by IPv6 privacy extensions, and spoofable by anything on the same Wi-Fi. Once every client you use is on 0.8.0 or newer and has paired, set compat.addressGrants: false. Nothing in this app needs it.

See also

  • Security — the trust model for both modes
  • LAN Mode — the unauthenticated alternative, and why to prefer this
  • Connecting — the connect screen itself
  • docs/PROTOCOL.md — the wire contract, for developers

Clone this wiki locally