#3753·paseo

bug: Desktop stores the relay hostname:443 as directTcp; probe times out and downloads hit the relay vhost

Author: zzy-suxiCreated Aug 23, 2026Updated Sep 18, 2026
Labelsbugp2triaged

What's broken

After pairing a daemon with paseo daemon pair --relay, Settings → Connections can show both:

  • Relay (relay.example:443) — correct, chat/control works
  • TCP (relay.example:443) — same hostname:port stored as directTcp

The TCP row is not a daemon. It is the relay HTTP/WebSocket vhost. Desktop then:

  1. Probes it as a daemon (GET /ws without relay query params / wrong protocol) → HTTP 400 or TCP timeout (TCP (relay.example:443) 超时 on Windows).
  2. Uses it as the first directTcp for file downloads (resolveDaemonDownloadTarget in packages/app/src/stores/download-store.ts), so even a working Relay (and even a working 127.0.0.1:6768 tunnel if it is not first) can fail or hit the wrong origin.

Official #offer= parsing only creates type: "relay" (upsertConnectionFromOfferupsertRelayConnection). connectionFromListen / Add Host TCP will happily parse host:443 as directTcp with no check that the endpoint is a relay.

Expected: either refuse to save a relay hostname as TCP, or never prefer a directTcp whose endpoint equals an existing relay.relayEndpoint. Download/control should use the active connection (see also #2436).

Steps to reproduce

  1. Run a daemon with daemon.relay.enabled and listen: 127.0.0.1:6767 (not public).
  2. paseo daemon pair --relay --json and add the host in Desktop via the #offer= URL. Confirm Connections shows Relay (…:443) only and chat works.
  3. Settings → host → Connections → Add a TCP connection using the same relay-host:443 (TLS on or off).
  4. Observe a second row: TCP (relay-host:443).
  5. Desktop marks that TCP row connecting/timeout. CLI: paseo ls --host relay-host:443Unexpected server response: 400. paseo ls --host tcp://relay-host:443?ssl=true → same 400.
  6. Files → Download on that host: either Download host is unavailable (if TCP is down and resolver requires directTcp) or an HTTP request to the relay origin /api/files/download instead of the daemon.

Also happens after Desktop upgrade (0.5.0-beta.5 → 0.5.0) when an existing host already had a relay endpoint: UI can surface the relay hostname as TCP.

Where did this happen

Desktop (Electron)

Paseo version

Desktop 0.5.0; daemons 0.5.0-beta.5

OS version

Windows 11 (client). Daemons: Ubuntu 24.04 and Windows Server.

Agent provider

Not relevant

Provider configuration

No response

Logs

CLI against the relay hostname as if it were a daemon (not the same as wss://…/ws?role=client):

Cannot connect to daemon at relay.example:443: Unexpected server response: 400
Cannot connect to daemon at tcp://relay.example:443?ssl=true: Unexpected server response: 400

Same host via a real daemon TCP tunnel works:

paseo ls --host 127.0.0.1:6768

returns agents.

download-store.ts still:

typescript
const connection = daemon?.connections.find((conn) => conn.type === "directTcp") ?? null;

host-runtime.ts upsertConnectionFromOffer only inserts type: "relay". connectionFromListen("relay.example:443") returns { type: "directTcp", endpoint: "relay.example:443" }.

Raw daemon log around a working relay control channel (not the failing TCP probe): relay_control_connected after pairing. The TCP probe never completes hello on the daemon.

Screenshots or video

UI rows look like:

TCP (relay.example:443)     ← timeout / 超时
Relay (relay.example:443)   ← online

versus a host that was given a real daemon TCP reverse-proxy hostname:

TCP (daemon-proxy.example:443)
TCP (daemon-proxy.example:443)

(duplicate TCP is a separate older issue, e.g. #1201).