Skip to content
VPNDetection

Detect a VPN in an iOS app: the interface check vs the IP check

Interface names like utun0 can't tell you a phone is on a VPN. Check where the connection comes from in Swift instead, with no API key in the app.

Published 3 min read

On this page
  1. The interface check
  2. The IP check
  3. Decide on your server

Search for how to detect a VPN in an iOS app, and most answers that rank check the names of the phone's network interfaces. That asks whether this phone is running a VPN, and Apple's developer support says no documented meaning goes with those names. The question a streaming service, a game or a bank is asking is a different one: does this connection come from a VPN? The address the connection comes from answers that, and VPNDetection, our Swift package, looks it up. We ran its code against the live API on October 12, 2026, with VPNDetection 4.5.1 and Swift 6.3.3 on Linux, and every output below is from those runs. It's the same package an app adds for iOS 16 and later. The interface check only builds for Apple's platforms, so what it does on an iPhone is quoted from Apple's developer forums.

The interface check

The answers on Stack Overflow and in a gist read the __SCOPED__ dictionary that CFNetworkCopySystemProxySettings() returns, which is keyed by interface name, and call the connection a VPN when a name is or contains tap, tun, ppp or ipsec. One Stack Overflow answer adds utun1 and utun2. Talsec's article recommends an SDK that monitors for VPN interfaces.

Apple doesn't document what an interface name means. A developer whose check for utun0 and utun1 failed asked on Apple's forums, and Quinn of Apple's Developer Technical Support answered that "there's no documented mapping between iOS interface names and their user-level functionality". utun interfaces also exist with no VPN on. Asked whether a Mac can have more than one with no VPN running, another Apple DTS engineer answered "Yes, there can be."

The gist's comment says it leaves utun out for that reason. It matches with key.contains("tun"), though, and "utun0".contains("tun") is true.

Where the check does work, it only sees a tunnel on this phone. A VPN running on the Wi-Fi router the phone has joined never shows up in its interfaces.

The IP check

myIP() asks our API about the address your app's request came from. When the phone's traffic goes through a VPN, that's the VPN server's address. When it doesn't, it's the carrier's or the Wi-Fi network's. In Xcode, add the package under File > Add Package Dependencies, or in Package.swift:

swift
dependencies: [
    .package(
        url: "https://github.com/vpndetection-io/sdk-swift.git",
        from: "4.5.1"
    ),
],

and .product(name: "VPNDetection", package: "sdk-swift") in your target's dependencies. The check is one call:

swift
import VPNDetection

let vpnCheck = VPNDetectionClient()

/// Whether this connection comes from a VPN, or nil when the check can't run:
/// no network, or the day's free lookups are spent.
func connectionIsVPN() async -> Bool? {
    try? await vpnCheck.myIP().isVpn
}
swift
switch await connectionIsVPN() {
case true?: print("This connection comes from a VPN.")
case false?: print("No VPN on this connection.")
case nil: print("Couldn't check.")
}

From one of our servers, it printed No VPN on this connection.

No key goes in the app. Without one, the API answers ip and is_vpn, and allows 1,000 lookups a day per source address. A key built into an app can be read by anyone who downloads it, so keep yours on your server. On cellular, carriers often put many phones behind one shared IPv4 address, and those phones share its 1,000 lookups. Once they're spent, connectionIsVPN() returns nil.

myIP() gets the answer lookup() gives for that address. Here's what lookup() answers for a Mullvad server, 193.32.248.66 from Mullvad's own server list, and for an iCloud Private Relay address, 172.224.236.1 from the list Apple publishes:

swift
import Foundation
import VPNDetection

let env = ProcessInfo.processInfo.environment
let client = env["VPNDETECTION_API_KEY"].map { VPNDetectionClient(apiKey: $0) }
    ?? VPNDetectionClient()

for ip in ["193.32.248.66", "172.224.236.1"] {
    let result = try await client.lookup(ip)
    print(ip, "isVpn:", result.isVpn, "isRelay:", result.isRelay as Any)
}

Without a key:

193.32.248.66 isVpn: true isRelay: nil
172.224.236.1 isVpn: false isRelay: nil

With a key on the Max plan:

193.32.248.66 isVpn: true isRelay: Optional(false)
172.224.236.1 isVpn: false isRelay: Optional(true)

nil means the field isn't in your plan, not that the answer is no. is_vpn comes back without a key, a free key adds the Tor and relay flags, location and asn, and every paid plan answers every field (pricing).

Private Relay doesn't reach the app's check. Apple's guide for network operators says it protects "web browsing in Safari, DNS resolution queries, and insecure http app traffic", so myIP(), over HTTPS, sees the phone's own address. Your website sees the relay address when the same person visits in Safari, and Should you block iCloud Private Relay? covers what to do with it.

Decide on your server

Both checks run on a phone you don't control. On a jailbroken phone, or in a patched copy of your app, connectionIsVPN() returns whatever its owner likes, and so does an interface check. Use the app's answer for the experience, such as telling someone their VPN will stop a stream before they press play. Make the decisions that cost you money, a signup, a purchase or a region's catalog, on your server: look up the address the request to your server came from, with your key kept there.

Each framework post does that lookup: Express, Fastify, Next.js, Django, FastAPI, Flask, Rails, and PHP and WordPress.

Written by

  • Uman Shahzad

    Founder & CEO

    IP intelligence data expert for over 8 years, former lead engineer at IPinfo, Uman has pioneered many systems and practices used by IP data companies to operate at scale, provide high-quality data and offer the best developer experiences.

Get new posts by email

Or follow along in a feed reader.

RSS feed

Try it on your own traffic.

The detection these posts describe is one API call away, with SDKs for every major language.

00 · No key

Start without signing up.

Paste it into a terminal. No account needed. 1,000 lookups a day per address, answering ip and is_vpn.

curl "https://api.vpndetection.io/146.70.116.130"
{
"ip": "146.70.116.130",
"is_vpn": true
}
01 · Free key

Sign up and get 50,000 requests a month.

No card and no sales call: the key is issued on signup.
Every request flags VPN IPs.
Upgrade only when you outgrow the allowance.