fix(agent): hand-roll wss:// relay dial with pinned-CA TLS (VA-76) #81

Merged
David merged 1 commit from fix/VA-76-relay-wss-tls-dial into main 2026-06-29 00:07:25 +02:00
Owner

What

The Files tab (and every other relay-backed usage: terminal, desktop, vnc, rdp, mcp) failed with tunnel WS connect failed: URL error: TLS support not compiled in.

Root cause: host/tunnel.rs::open dialed the relay with tokio_tungstenite::connect_async, but the crate builds tokio-tungstenite with default-features = false, features = ["connect"] (no TLS backend). A wss:// target therefore returns tungstenite::error::UrlError::TlsFeatureNotEnabled. The control channel never hit this because net::run_connection builds the TLS stream by hand and passes it to client_async_with_config; the tunnel path just never adopted that pattern.

Trust anchor

Probing the relay shows its leaf is issued by the internal CN=VervainRoot CA (subject CN=api.vervain.a8n.systems, SAN DNS:api.vervain.a8n.systems), not a publicly-trusted CA. So enabling a public-roots TLS feature on connect_async would compile but still fail verification. The correct anchor is the same internal Vervain PKI the control channel already pins (the enrolled SERVER_CA_KEY); the relay leaf is signed by the same root.

Changes

  • net.rs: new pub(crate) fn relay_tls_config(server_ca_pem), mirroring build_steady_state_config but with_no_client_auth() (the relay authenticates the agent via the rauth token in the rendezvous URL, not a client cert). Reuses load_roots + PinnedServerVerifier (pins by trust anchor, skips server-name validation, consistent with the control channel).
  • host/tunnel.rs: replace connect_async with hand-rolled dial_relay (parse URI, TCP connect, set_nodelay; wss:// -> rustls via relay_tls_config + TlsConnector::connect + client_async_with_config; ws:// -> plain). New RelayStream { Tls, Plain } enum keeps the per-usage run loop generic over one stream type. Stale docstring corrected.
  • dispatch.rs: Dispatcher gains server_ca + set_server_ca; handle_msg_tunnel passes it through. Session::new (net.rs) sets it from the store alongside set_server_url.

No rustls-tls-* feature added; aws-lc-rs is not pulled in (crate stays ring-only). Dev ws:// loop keeps working. No tests call tunnel::spawn/open directly, so the signature changes are internal.

Testing

just pre-commit green (fmt, clippy --deny warnings, build, host + windows targets, lib tests, tpm tests).

Fixes VA-76.

🤖 Generated with Claude Code

## What The Files tab (and every other relay-backed usage: terminal, desktop, vnc, rdp, mcp) failed with `tunnel WS connect failed: URL error: TLS support not compiled in`. Root cause: `host/tunnel.rs::open` dialed the relay with `tokio_tungstenite::connect_async`, but the crate builds `tokio-tungstenite` with `default-features = false, features = ["connect"]` (no TLS backend). A `wss://` target therefore returns `tungstenite::error::UrlError::TlsFeatureNotEnabled`. The control channel never hit this because `net::run_connection` builds the TLS stream by hand and passes it to `client_async_with_config`; the tunnel path just never adopted that pattern. ## Trust anchor Probing the relay shows its leaf is issued by the internal `CN=VervainRoot` CA (subject `CN=api.vervain.a8n.systems`, SAN `DNS:api.vervain.a8n.systems`), not a publicly-trusted CA. So enabling a public-roots TLS feature on `connect_async` would compile but still fail verification. The correct anchor is the same internal Vervain PKI the control channel already pins (the enrolled `SERVER_CA_KEY`); the relay leaf is signed by the same root. ## Changes - `net.rs`: new `pub(crate) fn relay_tls_config(server_ca_pem)`, mirroring `build_steady_state_config` but `with_no_client_auth()` (the relay authenticates the agent via the `rauth` token in the rendezvous URL, not a client cert). Reuses `load_roots` + `PinnedServerVerifier` (pins by trust anchor, skips server-name validation, consistent with the control channel). - `host/tunnel.rs`: replace `connect_async` with hand-rolled `dial_relay` (parse URI, TCP connect, `set_nodelay`; `wss://` -> rustls via `relay_tls_config` + `TlsConnector::connect` + `client_async_with_config`; `ws://` -> plain). New `RelayStream { Tls, Plain }` enum keeps the per-usage run loop generic over one stream type. Stale docstring corrected. - `dispatch.rs`: `Dispatcher` gains `server_ca` + `set_server_ca`; `handle_msg_tunnel` passes it through. `Session::new` (net.rs) sets it from the store alongside `set_server_url`. No `rustls-tls-*` feature added; aws-lc-rs is not pulled in (crate stays ring-only). Dev `ws://` loop keeps working. No tests call `tunnel::spawn`/`open` directly, so the signature changes are internal. ## Testing `just pre-commit` green (fmt, clippy `--deny warnings`, build, host + windows targets, lib tests, tpm tests). Fixes VA-76. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(agent): hand-roll wss:// relay dial with pinned-CA TLS
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 11m55s
Create release / Create release from merged PR (pull_request) Has been skipped
ebc14c73a3
The relay tunnel dialed with `tokio_tungstenite::connect_async`, but the crate builds tokio-tungstenite with no TLS backend feature, so a `wss://` target returned `UrlError::TlsFeatureNotEnabled` ("TLS support not compiled in"). This broke every relay-backed usage (files, terminal, desktop, vnc, rdp, mcp), surfacing first on the Files tab.

Replace `connect_async` with a hand-rolled `dial_relay` that mirrors the control channel and `wsproxy`: TCP connect, then for `wss://` wrap in rustls and run `client_async_with_config`; for `ws://` (dev) use the plain stream. The relay leaf is issued by the internal `VervainRoot` CA (not a public CA), so verification reuses the pinned server CA the control channel already trusts via a new `net::relay_tls_config` (no client cert; the relay authenticates the agent via the rendezvous `rauth` token). A small `RelayStream { Tls, Plain }` enum keeps the per-usage run loop generic over one stream type.

Thread the pinned CA `Session::new` -> `Dispatcher::set_server_ca` -> `handle_msg_tunnel` -> `tunnel::spawn`/`open`. No `rustls-tls-*` feature is added and aws-lc-rs is not pulled in. Stale tunnel.rs docstring corrected.

#VA-76
David merged commit 52618a9a51 into main 2026-06-29 00:07:25 +02:00
David deleted branch fix/VA-76-relay-wss-tls-dial 2026-06-29 00:07:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-agent!81
No description provided.