Skip to content

Secure web proxy negotiates h2 ALPN with client, breaking HTTP/1.1 CONNECT #8192

@lironthethird

Description

@lironthethird

Problem Description

When a client connects to an explicit HTTP proxy listener over TLS (a.k.a. "secure web proxy", i.e. the client's proxy URL is https://proxy:port), mitmproxy is supposed to force http/1.1 ALPN on that outer TLS handshake because it does not support CONNECT over HTTP/2. That logic lives in mitmproxy/addons/tlsconfig.py:254-262:

# Force HTTP/1 for secure web proxies, we currently don't support CONNECT over HTTP/2.
# ...
if len(tls_start.context.layers) == 2 and isinstance(
    tls_start.context.layers[0], modes.HttpProxy
):
    client_alpn: bytes | None = b"http/1.1"
else:
    client_alpn = client.alpn

The guard no longer matches. next_layer._setup_explicit_http_proxy eagerly instantiates both ClientTLSLayer and HttpLayer in a single pass, and Layer.__init__ appends each to context.layers, so by the time tls_start_client fires the stack is [HttpProxy, ClientTLSLayer, HttpLayer] — length 3, not 2. The forced-ALPN branch silently never runs, and ALPN selection falls through to the default callback, which happily picks h2 if the client offered it.

Observable effect: any client whose outer-TLS ALPN offer contains h2 (Node.js's TLS default is ["h2", "http/1.1"], so this covers a large class of HTTP clients built on Node's https / https-proxy-agent) negotiates h2 on the handshake and then receives HTTP/2 SETTINGS frames on a socket where it's waiting for HTTP/1.1 200 Connection established, followed by a protocol error and a broken proxy connection. curl --proxy https://... is not affected because curl only offers http/1.1 ALPN on the outer TLS.

Steps to reproduce the behavior:

  1. Start mitmproxy as a regular HTTP(S) proxy on a port:
mitmdump -p 8080
  1. Connect to it with TLS + ALPN h2,http/1.1 and try to send an HTTP/1.1 CONNECT. For example:
import socket, ssl

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(["h2", "http/1.1"])
s = ctx.wrap_socket(
    socket.create_connection(("localhost", 8080)),
    server_hostname="localhost",
)
print("outer ALPN:", s.selected_alpn_protocol())   # 'h2'  (bug)
s.sendall(b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n")
print(s.recv(200))  # HTTP/2 SETTINGS frames, not an HTTP/1.1 200
  1. Observe that the negotiated ALPN is h2 and mitmproxy logs HTTP/2 protocol error: Invalid HTTP/2 preamble. Expected: ALPN http/1.1 (or no ALPN) and a HTTP/1.1 200 Connection established response.

Suggested fix

I'm not deeply familiar with the internals of the layer system, so please take this as a starting point rather than a prescription — you'll almost certainly know a cleaner shape for it. But something along these lines in mitmproxy/addons/tlsconfig.py would replace the length-based guard with an intent-based one that doesn't break if the layer stack grows:

if (
    tls_start.conn is tls_start.context.client
    and tls_start.context.layers
    and isinstance(
        tls_start.context.layers[0],
        (modes.HttpProxy, modes.HttpUpstreamProxy),
    )
    and tls_start.context.server.address is None
    and sum(
        1
        for layer in tls_start.context.layers
        if isinstance(layer, proxy_tls.ClientTLSLayer)
    ) == 1
):
    client_alpn: bytes | None = b"http/1.1"
else:
    client_alpn = client.alpn

The accompanying test test_no_h2_proxy might also be worth updating to build the layer stack via _setup_explicit_http_proxy (or at least to include a real ClientTLSLayer + HttpLayer) rather than [modes.HttpProxy(ctx), 123], so this class of regression is harder to miss in the future.

Happy to put up a PR if this direction looks right, or to adjust if you'd prefer a different approach - no strong opinions on my end.

System Information

Mitmproxy: 12.2.2                                                                                                                                                                                     Python:    3.13.3                                                                                                                                                                                     
OpenSSL:   OpenSSL 3.5.4 30 Sep 2025                                                                                                                                                                  
Platform:  macOS-15.2-arm64-arm-64bit-Mach-O

Checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions