Skip to content

fix(proxy): half-close WebSocket legs over TLS - #273

Closed
olesho wants to merge 1 commit into
paradigmxyz:mainfrom
olesho:ws-half-close
Closed

olesho wants to merge 1 commit into
paradigmxyz:mainfrom
olesho:ws-half-close

Conversation

@olesho

@olesho olesho commented Oct 4, 2026

Copy link
Copy Markdown

Fixes #272.

handleWebSocket passes a half-close on to the other leg only when the connection is a *net.TCPConn. In MITM mode both legs are *tls.Conn, so when the client goes away the upstream is never told: the client→upstream copy ends, but the upstream→client copy keeps waiting until the upstream's idle timeout. wg.Wait(), and with it the deferred EmitAudit, waits just as long, and the record is lost if the proxy stops first.

This change half-closes any connection that implements CloseWrite() error (*tls.Conn sends close_notify, *net.TCPConn shuts down its write side) and closes one that doesn't, so a session can no longer outlive both of its peers.

Test: TestCloseWrite in internal/proxy/proxy_test.go covers both cases: a TLS pair, where the peer must see EOF while the reply in the other direction still arrives, and a net.Pipe connection, which has no half-close and is closed. The TLS case fails with the previous *net.TCPConn check. go vet ./... and go test ./... pass.

End to end (tunnel listener, mitm, linux/arm64, v0.52.0 with this patch), against an upstream that keeps a WebSocket open until the client leaves, as most services do:

  • before: the upstream learned the client had left only when the proxy was killed (14 s later in our run), and no audit record was written for the session;
  • after: the upstream is told within 24 ms, and the session is audited with its real duration (status_code: 101).

Against wss://chatgpt.com/backend-api/codex/responses (Codex), sessions that were missing from the audit log now appear as GET /backend-api/codex/responses 101.

🤖 Generated with Claude Code

handleWebSocket propagated a half-close only for *net.TCPConn. In MITM
mode both legs are *tls.Conn, so when the client went away the upstream
was never told: the session, and the audit record emitted when it ends,
waited for the upstream's idle timeout, and was lost if the proxy
stopped first.

Half-close any connection that supports CloseWrite (*tls.Conn sends
close_notify); close one that does not.

Fixes paradigmxyz#272

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leoyvens

Copy link
Copy Markdown

#244 would also fix this.

@olesho olesho closed this Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebSocket over MITM: client disconnect never reaches upstream; session is not audited until upstream closes

2 participants