Skip to content

fix(proxy): support HTTP/2 via ALPN, tunnel h2 connections transparently#71

Merged
jaytaylor merged 1 commit into
strongdm:mainfrom
safe-ai-factory:fix/http2-alpn-tunnel
Apr 6, 2026
Merged

fix(proxy): support HTTP/2 via ALPN, tunnel h2 connections transparently#71
jaytaylor merged 1 commit into
strongdm:mainfrom
safe-ai-factory:fix/http2-alpn-tunnel

Conversation

@JuroOravec
Copy link
Copy Markdown
Contributor

Context

I tried to use Cursor CLI inside the Leash coder container, but it was consistently failing to connect. The problem wasn't with authentication.

With the help of Claude Sonnet 4.6, I was able to triage the issue.

Problem

When a client negotiates HTTP/2 via ALPN during the TLS handshake, Leash's
MITM proxy fails with errors like:

Error reading response: malformed HTTP response "\x00\x00\x06\x04..." Error reading HTTPS request: malformed HTTP request "SM"

This happens because the proxy accepts the TLS connection but then tries to
parse HTTP/2 binary frames as HTTP/1.1 text. The \x00\x00\x06\x04 bytes
are an HTTP/2 SETTINGS frame; "SM" is the start of the HTTP/2 client
preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n).

Affected clients include the Cursor CLI, any gRPC-based tool, and anything
else that uses ALPN to negotiate HTTP/2 over TLS.

Fix

Two changes to handleTransparentHTTPS:

  1. Advertise ALPN protocols — add NextProtos: []string{"h2", "http/1.1"}
    to the server-side tls.Config. Without this, clients that require ALPN
    negotiation abort the handshake entirely; clients that proceed anyway send
    HTTP/2 frames into an HTTP/1.1 parser.

  2. Detect and tunnel h2 — after the handshake, check
    NegotiatedProtocol. If it is "h2", hand off to the new tunnelH2()
    function instead of the HTTP/1.1 MITM path.

tunnelH2() opens a fresh TLS connection to the origin (also with h2 in
NextProtos), then pipes the two sides together with io.Copy — a
transparent tunnel that preserves all HTTP/2 semantics.

Policy enforcement

Connect allow/deny policy is unchanged. It is evaluated in
handleTransparentHTTPS before the tunnel is established, so HTTP/2
traffic remains subject to Cedar policy decisions.

Limitations / future work

This PR does not implement full HTTP/2 MITM (header inspection, request
logging, rewriting). The transparent tunnel approach is intentional: full h2
MITM requires a complete HTTP/2 framing + HPACK implementation and is
significantly more complex. A transparent tunnel is sufficient to unblock h2
clients while keeping the change small and reviewable.

Testing

Verified by running the Cursor CLI agent inside a Leash-sandboxed Docker
container. Previously the agent entered an infinite reconnect loop
({"type":"connection","subtype":"reconnecting",...}); with this fix it
connects successfully and processes requests.

When a client (e.g. the Cursor CLI, gRPC-based tools) negotiates HTTP/2
via ALPN during the TLS handshake, the existing MITM proxy fails with
"malformed HTTP request/response" errors because it tries to parse HTTP/2
binary frames as HTTP/1.1 text.

Fix:
- Advertise both "h2" and "http/1.1" in NextProtos on the server-side
  TLS config so that ALPN negotiation completes successfully.
- After the handshake, check NegotiatedProtocol. If "h2", hand off to
  the new tunnelH2() function instead of the HTTP/1.1 MITM path.
- tunnelH2() opens a fresh TLS connection to the origin (also with h2 in
  NextProtos), then pipes the two sides together with io.Copy — a
  transparent tunnel that preserves all HTTP/2 semantics.

Policy enforcement (connect allow/deny) is unchanged: it still happens in
handleTransparentHTTPS before the tunnel is established, so h2 traffic
is still subject to Cedar policy decisions.

Full h2 MITM (header inspection, rewriting) is left as a future
improvement; transparent tunneling is sufficient to unblock h2 clients.

Made-with: Cursor
JuroOravec added a commit to safe-ai-factory/leash that referenced this pull request Apr 6, 2026
Builds a patched version of the upstream Leash Docker image that adds
transparent HTTP/2 tunnelling via ALPN.

This is a temporary workaround for the Cursor CLI (and any gRPC client)
failing inside Leash-sandboxed containers with 'malformed HTTP' errors.
The fix is submitted upstream at strongdm#71.

This file is intentionally on a separate branch from the upstream PR so it
does not pollute the files to be merged.

Made-with: Cursor
@jaytaylor jaytaylor merged commit 164015b into strongdm:main Apr 6, 2026
1 check passed
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.

2 participants