feat: WebSocket transport for VPN-free remote daemon access#579
feat: WebSocket transport for VPN-free remote daemon access#579hard2forgetme wants to merge 1 commit intoDimillian:mainfrom
Conversation
Add WebSocket as an alternative transport alongside TCP for the remote daemon protocol. The daemon auto-detects WebSocket (HTTP Upgrade) vs raw TCP on the same port by peeking at the first bytes of each connection. This unlocks remote access through HTTP reverse proxies like Cloudflare Tunnel, removing the need for VPN/Tailscale for cross-network connectivity. Changes: - Daemon: auto-detect WebSocket vs TCP on same listen port via byte peek - Client: new WebSocketTransport implementing RemoteTransport trait - Client: auto-detect ws:///wss:// URLs in resolve_transport_config - Types: add WebSocket variant to RemoteBackendProvider - Settings UI: add Cloudflare Tunnel helper section alongside Tailscale - Scripts: add codexmonitor-tunnel.sh for daemon + CF tunnel management The JSON-RPC protocol is unchanged — only the framing differs (WebSocket text messages vs newline-delimited TCP). Auth works identically.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0b502ad11
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!remoteHostDraft.startsWith("wss://") && !remoteHostDraft.startsWith("ws://")) { | ||
| onSetRemoteHostDraft(`wss://${remoteHostDraft}`); |
There was a problem hiding this comment.
Accept WebSocket URLs before prepending
wss:// here
This button turns a Cloudflare hostname into wss://…, but validateRemoteHost() in src/features/settings/hooks/useSettingsServerSection.ts:118-126 still only accepts bare host:port values. As soon as the field blurs, onCommitRemoteHost() rejects the new value, so the Settings UI cannot actually save the WebSocket endpoint that this feature needs (desktop remote mode and the mobile connect test are both blocked unless the user edits settings outside the UI).
Useful? React with 👍 / 👎.
| if [[ -n "$url" ]]; then | ||
| echo " 3. Host: ${url#https://}:443" |
There was a problem hiding this comment.
Print the tunnel host as a WebSocket URL, not raw
host:443
When a tunnel is active, these instructions tell users to enter <trycloudflare-host>:443. In src-tauri/src/remote_backend/mod.rs:243-266, that value is still resolved as the TCP transport because only ws:///wss:// hosts (or the explicit provider) select WebSocket mode. Since this script created the tunnel with cloudflared tunnel --url http://127.0.0.1:4732, following the printed steps will try raw TCP against an HTTP/WebSocket endpoint and fail to connect.
Useful? React with 👍 / 👎.
Summary
Adds WebSocket as an alternative transport alongside TCP for the remote daemon protocol. The daemon auto-detects WebSocket (HTTP Upgrade) vs raw TCP on the same listen port by peeking at the first bytes of each connection — no config changes, no extra ports.
This unlocks remote access through any HTTP reverse proxy (Cloudflare Tunnel, nginx, Caddy, etc.), removing the need for Tailscale/VPN for cross-network iOS connectivity.
What changed
Daemon (server-side):
ws_transport.rs— handles WebSocket connections usingtokio-tungstenite(already a dependency)GET→ WebSocket upgrade, otherwise → raw TCPClient (app-side):
ws_transport.rsimplementingRemoteTransporttraitresolve_transport_configauto-detectsws:///wss://URLsWebSocketvariant toRemoteBackendProviderandRemoteTransportConfigSettings UI:
cloudflared tunnelcommand and wss:// usage instructionsScripts:
scripts/codexmonitor-tunnel.sh— manages daemon lifecycle + Cloudflare Quick Tunnel with auto-generated auth tokensHow it works
The JSON-RPC protocol is completely unchanged — each WebSocket text message = one JSON-RPC line. Auth works identically. The only difference is framing: WebSocket messages instead of newline-delimited TCP.
No new dependencies
tokio-tungsteniteandfutures-utilwere already inCargo.toml. Zero new crates.Test plan
cargo checkpasses (verified)tsc --noEmitpasses (verified)cloudflared tunnel --url http://127.0.0.1:4732proxies WebSocket correctlywss://URL through Cloudflare Tunnel