How on-device network inspection works on iOS
By Ryan · Published 2026-07-28
An App Store app can read the decrypted HTTPS traffic of every other app on the same iPhone. It needs two things Apple ships and documents: a NetworkExtension provider that publishes a device-wide proxy setting, and a root certificate you install and trust yourself. No jailbreak, no tethered Mac, no server in the middle. What follows is the mechanism at the level you'd need to judge one — taken from Tracer's implementation, including the parts that are ugly — and the places it stops working. If you only want to run a capture rather than build one, how to see network requests on your iPhone is the practical version. This is also the methodology page for our teardowns.
Two iOS facilities, stacked
On-device network inspection is a local HTTP proxy plus a locally generated certificate authority. The proxy receives traffic because a NetworkExtension provider publishes NEProxySettings pointing at loopback; it can decrypt what arrives because the phone trusts a root whose signing key is on the phone. Neither half is a private API.
app (URLSession / CFNetwork / WebKit) -> system proxy setting published by the tunnel -> a listener on 127.0.0.1, inside the extension process -> TLS terminated with a leaf minted for the requested host -> a second TLS connection, opened by the extension, to the real origin
Everything below is a consequence of that shape. The most important one: the boundary of what an inspector can see is clients that honour proxy settings, not packets.
The VPN is a delivery vehicle for one setting
The VPN profile iOS asks you to allow does not tunnel your packets
anywhere. On a device with no MDM enrollment, publishing NEProxySettings
from a NetworkExtension provider is the only way to set an HTTP proxy
that applies device-wide, including on cellular — so the tunnel exists to
carry that setting and nothing else. Tracer's provider claims a single
route inside 198.18.0.0/15, the range
RFC 2544 reserves
for benchmarking, matches every hostname by declaring the empty string as a
match domain — the empty string being a suffix of every hostname — and
never reads packetFlow at all.
Interception starts at the CONNECT line, not at SNI
A proxy-aware client names the HTTPS host it wants in cleartext, before
any TLS happens, as a
CONNECT request: CONNECT api.example.com:443 HTTP/1.1. That request line
is where an on-device inspector learns the hostname. It never has to
parse the SNI extension of a ClientHello, which is the part people
expect to be the hard bit.
The proxy answers HTTP/1.1 200 Connection Established, and
then — instead of relaying bytes — takes the connection over itself: a
TLS server presenting a leaf certificate for that host, an HTTP/1.1
parser behind it, and a fresh TLS connection opened upstream to the real
origin. Three consequences show up in every exchange you capture:
-
ALPN advertises only
http/1.1, to the app and to the origin server. A host that would negotiate HTTP/2 is spoken to over HTTP/1.1 for the duration of the capture. -
Each forwarded request carries
Connection: close, andProxy-ConnectionandProxy-Authorizationare stripped. Timings therefore include a fresh TCP and TLS handshake every time; don't benchmark through a capture. -
With no CA installed yet, the connection degrades to encrypted
passthrough rather than failing. The connection is recorded
with hostname, IP and byte counts and a status of
passthrough; the app keeps working. The three statuses a connection can end with areintercepted,passthroughandfailed.
The certificate the phone is handed, in full
The inspector mints a fresh leaf certificate for each hostname at the
moment of the first CONNECT to that host, signs it with the on-device
root, and caches it for the rest of the session. Here is one, generated
on 2026-07-28 by compiling Tracer's certificate encoder outside the app
— with a software key standing in for the Secure Enclave so the bytes
could be dumped — and read back with
openssl x509 -text:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1785261337366 (0x19fa9de2b16)
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN=Tracer CA 4f2a91bc, O=Tracer, C=US
Validity
Not Before: Jul 28 17:50:37 2026 GMT
Not After : Aug 4 17:55:37 2026 GMT
Subject: CN=api.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Alternative Name:
DNS:api.example.com
That is 414 bytes on the wire. The root above it is 432. What's absent is more interesting than what's present: no Authority Key Identifier, no CRL distribution point, no OCSP responder, no Certificate Transparency SCTs. iOS builds the chain by matching the leaf's issuer name against the installed root byte for byte, there is nothing to revoke against, and Apple's CT policy applies to publicly trusted CAs — not to a root the user installed. A certificate a browser would reject instantly on the public web is accepted here because the trust decision was made by hand, once, in Settings.
The rest of the shape, per
RFC 5280: one SAN,
exactly the host from the CONNECT line, with no wildcards and one
certificate per hostname; seven-day validity backdated five minutes to
absorb clock skew; a serial number that's milliseconds since the epoch,
so it tells you when the leaf was minted. Every leaf on one install
shares a public key — only the subject and SAN differ. The root above
them is CN=Tracer CA <8 hex digits>, O=Tracer, C=US,
CA:TRUE, valid ten years, with a hex suffix that's random per install, so
no two users share a subject name or a key.
The signing key is in the Secure Enclave. The key the TLS server uses is not.
Tracer's CA private key is a P-256 key generated inside the Secure Enclave and marked non-exportable. The leaf certificates it signs use a different key: an ordinary CryptoKit P-256 key stored as PEM in a keychain item shared between the app and the extension.
That split isn't a shortcut, it's forced. The Secure Enclave signs digests on request; it does not hand a private key to anything, and a TLS server needs actual key bytes to complete a handshake. So the enclave key does the one job that has to be unforgeable — signing certificates — and a software key does the job that requires possession.
The security property survives the split intact. Someone who extracts the
leaf key from the keychain gets a key pair no phone trusts, because every
certificate presented during interception has to carry a signature the CA
key produced, and that key cannot be copied off the device. Not by an
attacker, not by us. It's stored
AfterFirstUnlockThisDeviceOnly, so it never appears in a
backup and never migrates to a new phone.
iOS won't tell an app whether its own root is trusted, so you ask sideways
There is no iOS API that lists the root certificates a user has installed, and none that reports whether a given root has full trust enabled. An app that needs to know has to find out by experiment: mint a throwaway leaf under its own CA and ask the system trust evaluator whether this device accepts it.
Tracer mints one for a synthetic hostname that never touches the
network, builds a SecPolicyCreateSSL policy for that same
name, calls
SecTrustSetNetworkFetchAllowed(trust, false) so a revocation
fetch can't block the check, and runs
SecTrustEvaluateWithError. The trust store is device-wide
and honours the Certificate Trust Settings toggle, so an app-side
evaluation faithfully predicts what the tunnel's TLS server will see.
Three states come back: notInstalled,
installedNotTrusted — which covers both "never installed"
and "installed but full trust is off", because iOS gives no way to tell
those apart — and trusted.
The same probe runs inside the tunnel, once per session, against the first real leaf it mints, and writes the verdict to the system log. That turns the useless bug report — "it doesn't work" — into one of two specific answers: the device trusts a certificate the app just rejected, so the app pins; or the device rejects it too, so the trust setup is broken.
Getting the root onto the phone has its own piece of iOS trivia. The
system only starts the "Profile Downloaded" flow when a
.mobileconfig arrives over HTTP in Safari, so the app runs a
loopback listener on an ephemeral port, serves the profile as
Content-Type: application/x-apple-aspen-config with a
com.apple.security.root payload, and opens that
http://127.0.0.1:… URL in an in-app Safari view. Installing
and trusting are then two separate decisions in two different places in
Settings — the step people skip, and the reason the probe above exists.
What ends up in the record, and where each field comes from
Every field an inspector shows you is derived from bytes that crossed the connection, not from anything the app declared about itself. In Tracer, per exchange:
- Hostname, method, path, status, headers and both bodies — the CONNECT line and the decoded HTTP/1.1 exchange. Headers are re-rendered verbatim, in wire order, including the ones the SDK didn't mean for you to read; bodies are stored as the bytes that crossed the wire and inflated at display time.
- Remote IP, cloud provider and region — the peer address of the socket the extension actually opened, not a DNS answer, run through a memory-mapped IP-to-datacenter lookup at connect time so a later re-resolution can't rewrite history.
-
Edge PoP — read from whichever of
cf-ray,x-amz-cf-pop,x-served-byorx-vercel-idthe response carries. Each of those CDNs leaks the airport code of the edge location that served you in a response header. -
SDK name and version — the first whitespace-delimited
token of
User-Agent, split at its last slash. Soposthog-ios/3.30.0 CFNetwork/1490 Darwin/24.0.0becomesposthog-ios, version3.30.0.
A capture isn't complete by default: Apple system hosts and Tracer's own traffic are hidden so they don't drown the session, and both are toggles. The second filter is mostly a hostname list, with one case it can't be — Tracer's subscription SDK talks to RevenueCat and so do plenty of apps you'd want to inspect, so those requests are told apart after decryption by a header the calling app sets to identify itself. Turn the first toggle off before drawing conclusions about iOS system behaviour.
Where certificate pinning actually stops you
A pinned app compares the certificate it was handed against a copy it
ships with, and rejects the inspector's leaf during the handshake. You
get the hostname, the IP, byte counts and a connection marked
failed, with zero exchanges under it — and the app usually
shows an error until you stop capturing. Pinning doesn't conceal
traffic; it declines to produce any.
Pinning is per-host, not per-app, and most apps that pin, pin their own API and leave their analytics and advertising SDKs on the system trust store — those still decrypt in the same session. There is no way around a pin from any proxy, on a phone or a desktop; SSL Kill Switch, Frida and a re-signed IPA all need a jailbreak.
Three things that never reach the proxy at all
Proxy-based inspection sees HTTP conversations from proxy-aware clients. That covers URLSession, CFNetwork and WebKit — most of what an iOS app does — and it structurally excludes the following.
- QUIC and HTTP/3. They run over UDP and NEProxySettings has no effect on them. A client that brings its own QUIC stack bypasses the proxy and never appears in the capture at all — not as a failed connection, not as anything.
- Anything that isn't HTTP. DNS lookups, push notifications over APNs, VoIP transports, raw sockets — and the packet layer itself: no handshake bytes, no retransmissions, no window sizes. If the question is why a connection failed before HTTP started, this is the wrong instrument.
-
Connection upgrades. Every forwarded request goes
upstream with
Connection: close, so an interceptedUpgrade: websocketrequest doesn't complete its upgrade.
The same trick as Charles, with the trust boundary moved
On-device inspection and desktop interception are the same operation performed at a different point: terminate TLS with a certificate the client trusts, read the plaintext, re-encrypt upstream. Charles, Proxyman, mitmproxy, Burp and Tracer all do exactly this. Moving the middle onto the phone changes three things.
- What has to be true of the network. Nothing, versus a Wi-Fi network you can configure with a manual proxy and a route between the two devices. On-device capture works on cellular, and anywhere you can't put a computer.
-
Who holds the CA key. A per-install key in a Secure
Enclave, versus a file — mitmproxy keeps its CA at
~/.mitmproxy/mitmproxy-ca.pem— reused for every device you ever point at that machine. The desktop arrangement is more portable and strictly weaker. -
How far down you can look. HTTP and no further, versus
a Mac running
rvictland a full packet trace of a tethered iPhone. Editing traffic — rewrite rules, breakpoints, Map Local — is a desk job too, and desktop tools own it.
The comparison of the five on-device inspectors covers which of them make which trade.
Four questions to ask of any inspector you install
Trusting a root certificate grants whoever holds the matching private key the ability to impersonate any website to your phone, for as long as the root stays trusted. That grant is the thing to reason about — not the VPN icon in the status bar — and four questions settle it. Tracer's answers are below each one, and the same four are worth putting to anything else you install.
- Where was the CA key generated? On the device, per install, is the only acceptable answer; a root baked into an app binary is the same key on every phone that runs it. Tracer's is a P-256 key minted in the Secure Enclave at setup, with a random name suffix per install.
- Do captured bodies leave the device? Tracer's stay in a SQLite database in the app group container. That's also why exporting one deserves care — see the HAR export guide for what a single exported request turned out to contain.
- Is the tunnel scoped to a capture, or always on? Tracer's starts when a session starts and is torn down when it stops, so nothing sits between you and the internet the rest of the day.
- Can you remove it completely? Two places, because the CA key lives in a shared keychain access group rather than the app container: Reset Certificate in the app's settings, then delete the profile under Settings → General → VPN & Device Management. Deleting the app alone leaves the authority in place, which is true of any inspector that stores its key outside its own container.
FAQ
How does an iPhone app decrypt HTTPS without a jailbreak?
It terminates TLS itself using a certificate authority whose root you installed and trusted. A NetworkExtension provider publishes a device-wide proxy setting pointing at a listener inside the extension; that listener answers each CONNECT, presents a leaf certificate it mints for the requested hostname, and opens its own TLS connection to the real server. Both halves — NEProxySettings and a user-installed root — are documented iOS features available to App Store apps.
Does on-device network inspection capture packets?
No. A proxy-based inspector sees HTTP requests and responses from
proxy-aware clients, not packets. DNS lookups, QUIC and HTTP/3, push
notifications over APNs, TLS handshake bytes and retransmissions never
reach it. Packet capture on iPhone still requires a Mac running
rvictl with Wireshark or tcpdump.
Can an iOS app see network traffic from other apps?
Yes, if those apps use proxy-aware networking. Proxy settings published by a NetworkExtension provider apply to the whole device, so URLSession, CFNetwork and WebKit traffic from every app is routed through the inspector while its tunnel is running. Apps that bring their own QUIC stack or open raw sockets bypass it.
How do you tell certificate pinning apart from a broken certificate install?
Ask the device to evaluate a certificate the inspector just minted. If this device trusts that leaf and an app still rejected it, the app is pinning; if the device rejects it too, the root is not installed or full trust was never enabled. iOS provides no API to enumerate user-installed roots, so a trust evaluation against a throwaway leaf is the only way to get the answer.
Is it safe to install a root certificate on your iPhone?
It is safe only to the extent that the private key is unreachable and the traffic stays on the device. A trusted root is the ability to impersonate any website to your phone for as long as it stays trusted, so the questions that matter are where the key was generated, whether the same key ships to every user, whether captured bodies leave the device, and how you remove the profile afterwards.
None of this is exotic — it's a proxy, a certificate, and one setting that iOS will only accept from a NetworkExtension. Judge any inspector you install on the three things that follow from that: where its CA key was generated, whether the bytes it captures leave the phone, and whether its tunnel is scoped to a capture. Tracer is the implementation described above — the enclave-held CA, the session-scoped tunnel, the trust probe — and it runs on the phone, which is the one thing a desktop proxy can't do. Once the requests are in front of you, the teardowns are what tends to be in them.