Protocol coverage
A frank table of what works, what doesn’t, and why.
TL;DR
| Protocol | Decrypt? | Why / why not |
|---|---|---|
| HTTP/1.1 cleartext | ✅ | We are the proxy. |
| HTTP/1.1 over TLS | ✅ | We 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-Web | ❌ | P04.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 TCP | ❌ | Not 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:
- Detect it via
Content-Type: application/grpc*. - Decode envelopes (1-byte flag + 4-byte length per message).
- Decompress per-message gzip if
grpc-encoding: gzip. - 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
| Browser | Status |
|---|---|
| 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
- Configure the proxy — how each client is wired up.
- Architecture overview — the engine internals.