Closed general

Surface token-mismatch auth failures clearly (client + relay), not a silent clean close

Drew Owens · 3mo ago ·closed by John Lauer

The ask

When a desktop connection fails because its registered relay token does not match the relay's token, surface that clearly on both sides. Today it looks like a mysterious flap, and diagnosing it means reading the relay log.

What it looks like now

After a relay token rotation, a container's registered auth_token can go stale (still adom-dev-token-2025) while the relay runs a rotated token. Every reconnect then loops like this, forever:

  • Relay log: WS connected → type=auth → Auth FAILED → sent Close frame → WS disconnected (every 30s).
  • Client (AD): Connecting… → Connected, authenticating… → Authenticated → Connection closed cleanly after ~200ms → Reconnecting in 30s.
  • Manual Connect: Connect failed: Server 'X' is already connected or connecting.

Nothing on the client says auth failed or token mismatch. "Closed cleanly" and "Authenticated" actively mislead: it reads like a network blip, not a rejected credential. It took reading relay.log on the container to find the Auth FAILED line.

Requested changes

  1. Client-side state. When the relay closes a connection for auth reasons, show a distinct, honest state, for example: "Authentication failed: the server rejected this connection's token. Update this server's token." Do not report the connection as "Authenticated" or "closed cleanly" when the server rejected it.
  2. Relay-side close reason. Have the relay send a close code + reason on auth failure (e.g. a WS close code plus auth_failed: token_mismatch) so the client can render it. The relay already logs Auth FAILED; propagate that to the peer instead of a bare clean close.
  3. A pointer to the fix. Since the registration's auth_token is not auto-updated on rotation, the message should hint at the remedy (re-register / update this server's token).
  4. Disambiguate the manual-Connect error. already connected or connecting (client's own retry state) should read differently from a real server-side auth rejection, so a user clicking Connect during the loop is not told the wrong thing.

Why it matters

Token rotation is a normal operation and the registration token does not follow it automatically, so this exact failure mode will recur across containers. A clear message turns a log-diving investigation into a one-glance fix.

Filed after diagnosing an AdomPNP relay that looped for days on a stale token; the only signal was Auth FAILED buried in a 380 MB relay.log.

2 Replies

John Lauer · 2mo ago

Partly fixed in 2.0.84, and the root cause is smaller and dumber than the report assumed - which is good news for you.

The relay was never silent. It already sends, before closing:

{"type":"auth_result","success":false,"error":"Invalid token"}

ab was reading the wrong key. Its handler looked at payload["message"] - a field the relay does not send - so on every single rejection the real reason was discarded and replaced with the generic fallback string. The information you had to go read the relay log for was on the wire the whole time, and ab threw it away.

Fixed: ab now reads error (falling back to message), and the line is actionable rather than just accurate:

[galliaApril] AUTH FAILED: Invalid token. This relay rejected ab's token, so
reconnecting will NOT help - every retry sends the same token. Usually the relay's
token was rotated and this entry still holds the old one. Fix: update the token for
'galliaApril' (server_add with the current authToken, or Settings > the server's
entry), then Connect.

That goes to both the log and the activity log, so it is visible in ab without SSH.

What I did NOT do, and think you are right about. Your deeper point is that the connection should stop presenting as a normal reconnect cycle. It still cycles Reconnecting every 30 s, because the honest fix is a new ConnectionStatus::AuthFailed variant so the row reads "auth failed - token mismatch" and the UI can offer the fix inline. That enum is matched in the GUI, server_list, and both status builders, and a concurrent session is editing those files right now; I was not willing to half-apply it across three surfaces and leave the row inconsistent.

So: the diagnosis is no longer a mystery, but the presentation is still a flap. If you want the status variant, say so and I will do it as its own change - it is mechanical, just wide.

One thing I deliberately did not change: ab keeps retrying rather than giving up. A relay token can be rotated back, or the entry fixed remotely, and an ab that stopped trying would be an ab nobody can recover without physical access. Slow and loud beats silent and stopped.

John Lauer · 2mo ago

Finished in 2.1.50. On top of the 2.0.84 reason line, ab now treats an auth rejection as what it is: after the relay answers auth_result {success:false} the reconnect loop holds for 5 minutes instead of retrying the same token every 1..30 s, and says so in the activity log ("AUTH FAILED s ago: holding reconnects for 5 minutes. Update this server's token, then Connect to retry now"). A manual Connect (or fixing the token via server_add / Settings) retries immediately; a successful auth clears the hold. Both logs stop filling with the flap, and the operator sees one actionable line per hold instead of a stream of connects. Closing.

John Lauer closed this · 2mo ago

Log in to reply.