How the pipe works
Apps hit a local VPN. The proxy checks whether it can decrypt HTTPS as HTTP/1.1. If yes — MITM: open the bytes, inspect, send them on. If not — a sealed byte tunnel; you still see the connection in the UI, but not the HTTP payload.
Traffic flows from apps through a local VPN into the Lenswire proxy. The proxy either decrypts HTTPS with MITM for inspection, or relays a sealed byte tunnel when decryption is not possible. Results appear in the Lenswire UI.
iOS Packet Tunnel (utun → hev → SOCKS) · Android VpnService (TUN → tun2socks → SOCKS)
The fork
One gate. Two exits.
Before any decrypt, the proxy decides canMitm. Decrypt off, missing CA, no SNI, session bypass, or ALPN without HTTP/1.1 → runPassthrough. Otherwise → runMitm (Kotlin) / runMITM (Swift).
if (!canMitm) {
runPassthrough(/* … */, reasonCode = …) // captureMode: tunnel
return
}
runMitm(/* … */) // decrypt → inspect → upstream → encrypt backSame pipe. Two exits.
Open the payload — or keep it sealed
MITM calls show as HTTP/1.1 with full headers and body. Pure h2/h3 tunnels get version flags only. A successful downgrade is labeled HTTP/1.1 — the inspectable payload you actually got.
MITM path
We MITM
Terminate TLS with the app → read HTTP/1.1 → talk to the server → encrypt back.
When the client offers http/1.1 (often with h2), we force that ALPN, decrypt request and response, and show full payload in the UI. Overrides land here.
WebSocket
websocket_relayAfter MITM TLS, Upgrade is relayed end-to-end. Frames are not inspected; the host is not added to session bypass.
Passthrough path
We tunnel
runPassthrough relays encrypted bytes as-is. captureMode: tunnel — payload unavailable in the UI.
ClientHello only offers h2 or h3. We never start MITM — straight tunnel. UI shows HTTP/2 or HTTP/3, not a decrypted call.
QUIC
quicUDP/QUIC is not terminated for MITM. Those flows stay outside the HTTP decrypt path — connection may appear, payload does not.
Session bypass
mitm_bypassedTrust fail, pinning-style TLS reject, or non-HTTP/1.1 after handshake → that connect closes; the host stays tunnel-only until you stop VPN. UI shows the bypass cause.
No request after handshake
mitm_no_requestTimeout with no HTTP → close and bypass so retries tunnel. Empty EOF → close without bypass, so speculative CDN connects can still MITM next time.
MITM impossible up front. Still captured as a tunnel via runPassthrough — not a decrypted call.
Boundary in one line: decryptable payload is HTTP/1.1 after CA trust. Pure HTTP/2 and QUIC tunnel. WebSocket is relay-only (no frame inspect). Certificate pinning is not bypassed. Failures stay fail-open with a reason code.