Skip to content

Protocol coverage

A frank table of what works, what doesn’t, and why.

TL;DR

ProtocolDecrypt?Why / why not
HTTP/1.1 cleartextWe are the proxy.
HTTP/1.1 over TLSWe terminate TLS with a leaf cert signed by our root.
HTTP/2 (h2 ALPN)Same as h1 but stdlib http2.Server.ServeConn handles the framer.
WebSocket (ws://)⚠️Handshake captured. Frames relayed with byte+frame counters, no per-message reassembly yet (P03.5).
WebSocket (wss://)⚠️Same as ws://, with TLS terminated.
gRPC (over h2)⚠️Detected via content-type. Heuristic protobuf decode without .proto. Reflection in P04.5.
gRPC-WebP04.5.
HTTP/3 (QUIC)Browsers pin QUIC cert trust to system roots only. We strip Alt-Svc to force h2 fallback.
MQTT, AMQP, Redis, custom TCPNot in scope.
TLS to a client-pinned server (banking apps, Apple Pay)The client refuses our root. We see the TLS handshake fail; the app retries or errors.

Why we can’t decrypt HTTP/3

Chrome and Safari ship QUIC stacks that pin certificate trust to the system root store at the OS level, even when SSLKEYLOGFILE-style escape hatches exist. Our user-installed root CA isn’t in that trust set, and there’s no public API to add one.

Proxyman doesn’t decrypt QUIC either. Charles doesn’t. mitmproxy experimentally supports QUIC via a custom build but it doesn’t work against pinned clients.

Our workaround: force-downgrade

Every response that comes back through the proxy has its Alt-Svc and Alt-Svc-Used headers stripped. Without Alt-Svc, browsers can’t discover the server’s QUIC endpoint and fall back to h2. We decrypt the h2 traffic fully.

There’s a 30-second window during which a browser may have a cached QUIC session from before you turned on the proxy. The browser ignores the strip until that cache expires (or you toggle chrome://flags/#enable-quic to Disabled).

Why pinned apps fail

Apps like Apple Pay, Bank of America, Spotify, and most native iOS apps implement certificate pinning — they ship the expected upstream public key inside the app binary and reject anything else. Our leaf cert, signed by a different root, fails the pinning check and the TLS handshake aborts.

There’s no workaround that doesn’t involve patching the app binary itself (Frida, Objection). That’s out of scope for ProxyPro.

You can usually tell when an app pins: the proxy shows a CONNECT to the host followed by a TLS handshake failure, and the app reports a generic “network error” or “couldn’t reach server.”

What we do with gRPC

gRPC is HTTP/2 with a protobuf payload format. We:

  1. Detect it via Content-Type: application/grpc*.
  2. Decode envelopes (1-byte flag + 4-byte length per message).
  3. Decompress per-message gzip if grpc-encoding: gzip.
  4. Walk the protobuf wire format heuristically to recover field number + wire type + likely interpretation.

We don’t yet implement gRPC server reflection — that’s P04.5. With reflection, we’d fetch descriptors from the upstream and decode messages with real field names instead of #1, #2, #3.

Performance ceiling

The engine has been measured at:

  • ~5,000 captured flows/sec on M-series mac (synthetic h1 traffic)
  • ~1,000 captured flows/sec sustained with real-world response body sizes
    • gzip decode

The 10k-event ring buffer + 100MB body LRU keep memory bounded. Beyond those limits, older events are dropped or evicted.

Browser-specific quirks

BrowserStatus
Chrome (macOS)✅ Works after System keychain install + relaunch
Safari✅ Works after Login keychain install
Firefox✅ Works after import into Firefox’s NSS store
Edge✅ Same as Chrome
Chromium with --proxy-server flag✅ Works without system proxy changes

See also