proxy: dial the tunnel through an HTTP proxy (Docker Sandboxes support) - #965
Conversation
The tunnel client (`proxy run --url`) used the runtime's native WebSocket, which ignores HTTP(S)_PROXY and always dials direct. In a sandbox whose only egress is an explicit proxy gateway (e.g. Docker Sandboxes), the broker was unreachable without an external socat shim. When a proxy applies to the tunnel URL (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY, honoring NO_PROXY), the client now establishes an HTTP CONNECT tunnel through it, optional TLS for wss://, and the WS handshake itself, then speaks RFC 6455 over the socket. This hand-rolled path is runtime-agnostic (net/tls/crypto), so it behaves identically under Node and the compiled Bun binary; the direct dial is unchanged. Adds a Docker Sandboxes guide.
|
The changes in this PR will be included in the next version bump.
|
📦 Bundle size
dist/ only; native binaries are versioned separately and not counted here. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
varlock-website | 58fec14 | Commit Preview URL Branch Preview URL |
Jul 31 2026, 08:40 PM |
There was a problem hiding this comment.
Important
The plaintext proxy path works, but the accepted HTTPS proxy path is broken and pending handshakes cannot be cancelled cleanly.
Reviewed changes in e0953408, covering the proxy selection, custom CONNECT WebSocket transport, tunnel tests, and Docker Sandboxes documentation.
- Proxy-aware tunnel dialing: Selects proxy environment variables, honors
NO_PROXY, and adds a custom RFC 6455 client over HTTPCONNECT. - Transport coverage: Adds in-process CONNECT tests for bootstrap, authentication, bypass, and bidirectional data transfer under Node and Bun.
- Docker Sandboxes guide: Documents host and remote broker setups and links the new guide into the sandbox navigation.
azure/gpt-5.6-sol | 𝕏
commit: |
sbx runs locally (microVMs on your own machine), so it does not belong with the cloud providers. It is the one local tool reached over the tunnel through its own gateway rather than loopback, so the section framing notes that.
- proxyForTunnelUrl now rejects https:// proxies. The client opens a plaintext TCP socket to the proxy, so an https:// proxy (TLS to the proxy itself) would never handshake; advertising it as accepted was broken. Only http:// CONNECT proxies are selected now; https:// and socks* fall back to the direct native dial. A wss:// broker still works through an http proxy (tunnel TLS runs end to end inside the CONNECT). - Make connection setup cancellable. The in-flight proxy/TLS socket is now tracked on the instance before each handshake await, so close() (e.g. the bootstrap timeout) or a setup failure destroys it instead of leaving it open and keeping proxy run alive. close() destroys outright while connecting; readHttpHead also rejects on socket close so a stalled handshake unwinds. - Tests: https-proxy is not selected; a stalled CONNECT is torn down on timeout.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes in 58fec149, covering the fixes added since the previous incremental pass.
- Restricted proxy selection: Rejected unsupported
https://proxy hops while preservingwss://broker TLS through plaintext HTTPCONNECTproxies. - Made setup cancellable: Tracked proxy and TLS sockets before each handshake await, destroyed in-flight setup on close, and unwound HTTP-head waits when sockets close.
- Added regression coverage: Verified unsupported proxy schemes and peer-observed socket teardown when a CONNECT response stalls past the bootstrap timeout.
azure/gpt-5.6-sol | 𝕏



What
varlock proxy run --urlnow dials its WebSocket tunnel through an HTTPCONNECTproxy when the guest's egress requires one, and adds a Docker Sandboxes (sbx) guide.Why
The tunnel client used the runtime's native
WebSocket, which ignoresHTTP(S)_PROXYand always dials direct. In a sandbox whose only egress is an explicit proxy gateway (Docker Sandboxes routes everything throughgateway.docker.internal:3128), a host or remote broker was unreachable without an externalsocatCONNECT shim.How
When a proxy applies to the tunnel URL, the client establishes the
CONNECTtunnel to the proxy, optional TLS forwss://, and the WebSocket handshake itself, then speaks RFC 6455 over the socket:HTTPS_PROXYforwss://,HTTP_PROXYforws://, falling back toALL_PROXY, honoringNO_PROXY(upper/lower case). A loopback orNO_PROXY-matched broker still dials direct via the existing native path, which is unchanged.http://CONNECTproxy is selected; anhttps://proxy (TLS to the proxy itself) andsocks*are ignored and fall back to the direct native dial. Awss://broker still works through anhttpproxy, since the tunnel's own TLS runs end to end inside the CONNECT.await, soclose()(e.g. the bootstrap timeout) or any setup failure destroys it rather than leaving a socket open and keepingproxy runalive.net/tls/crypto, so it behaves identically under Node and the compiled Bun binary (no dependency on native WebSocket proxy support, which differs across runtimes). The tunnel still carries TLS end to end, so an intermediate proxy that terminates TLS only sees the encrypted tunnel.Verification
NO_PROXYbypass, bad-token error passthrough, and proxy selection. Full suite green under bothvitest(Node) andbun test.varlock proxy start --expose,sbx policy allow network localhost:PORT, thenvarlock proxy run --url ws://host.docker.internal:PORTwith no shim. Secret injected and response scrubbed; agent saw only the placeholder.Docs
sandboxes/docker-sandboxes.mdx(host broker and remote broker recipes, thelocalhostpolicy quirk, varlock-vs-sbx-secrets comparison, trust model).Notes for the Docker team (policy check/enforcement mismatch on
host.docker.internal, unremovable custom secrets, unscrubbed response bodies) are captured separately in the spike branch, not this PR.