# VPN DNS Leak Test Plan for App Teams: IPv6 and DoH

> A VPN DNS leak test plan for app teams: how DNS and IPv6 leaks happen, the platform settings involved, and nine tests with pass or fail criteria.

Source: https://digitaldtech.com/blog/vpn-dns-ipv6-leak-test-plan/
Published: 2026-11-14 | Updated: 2026-11-14
Author: ST Muntaha
Publisher: DigitalD.tech (https://digitaldtech.com/)


A VPN DNS leak test comes down to one question: once the tunnel is up, does any DNS or IPv6 packet leave the device through the physical network? Most leaks come from three places. The platform was never pointed at the tunnel's resolver. An IPv6 path was never covered. Or the packet escaped in a gap between states, such as a reconnect or a network switch. Loading a website once will not catch those. A capture based test plan will.

This guide explains how the leaks happen, what Android and Apple document about the settings involved, and gives nine tests with pass or fail criteria. It is written for teams that build or QA VPN apps.

## How Do DNS and IPv6 Leaks Happen in a VPN App?

**The resolver was never switched.** On Android, `addDnsServer()` is how a VPN sets its resolver, and the [platform source](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/net/VpnService.java) says that if you set none, the default network's DNS servers are used. On Apple platforms, the packet tunnel's `dnsSettings` carry the servers. Apple's [`matchDomains` documentation](https://developer.apple.com/documentation/networkextension/nednssettings/matchdomains) says that when the tunnel is the default route, those servers become the default resolver and `matchDomains` is ignored. In a split setup, `matchDomains` decides which hostnames use them, and an empty string makes the tunnel resolver the default domain.

**IPv6 was not covered.** Android blocks an address family when you add no address, route or DNS server of that family, and `allowFamily()` lets it through to the underlying network. On Apple platforms, IPv6 settings and a `::/0` included route carry IPv6 into the tunnel, and Apple does not say what happens to IPv6 if you leave them out. Dual stack clients lean on IPv6: [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305) has them send the AAAA query first, and it assumes a host address policy that favors IPv6, so an uncovered IPv6 path tends to get used.

**System Private DNS meets app resolvers.** On Android, system level Private DNS uses DNS over TLS. Google's [behavior changes](https://developer.android.com/about/versions/pie/android-9.0-changes-28) tell apps that target Android 9 to honor the private DNS APIs. If the system resolver uses DNS over TLS, an app's built in DNS client should use encrypted DNS to the same hostname or be disabled. The pages we read do not describe how Private DNS behaves with a VPN's own DNS servers, so make it a test case.

**DNS over HTTPS inside apps.** An app with its own DoH client picks its resolver from its own configuration, which [RFC 8484](https://datatracker.ietf.org/doc/html/rfc8484) describes as a URI template reached over HTTPS. Your resolver settings never apply to it. In a full tunnel the DoH connection still rides the tunnel. For an excluded app it does not. This is the app's resolver choice, not a network leak, but it matters if your VPN filters at the DNS layer, because plaintext DNS filtering stops working under DoH.

**Captive portals and network switches.** With `includeAllNetworks`, Apple's [routing guide](https://developer.apple.com/documentation/networkextension/routing-your-vpn-network-traffic) says captive portal negotiation traffic always stays outside the tunnel, so portal logins appear in a capture and are not leaks. The same guide says traffic is dropped during a Wi Fi to cellular move. Without that flag, only your own code covers the gap. On Windows, WireGuard for Windows documents that when a configuration lists DNS servers, its firewall kill switch permits port 53 packets only to those servers, to prevent Windows' multihomed DNS resolution behavior.

## What Do the Platform APIs Document?

| API | Documented behavior | Leak implication |
|---|---|---|
| Android `addDnsServer()` | Accepts IPv4 and IPv6 servers. With none set, the default network's servers are used | Forget it and lookups follow the underlying network's resolver |
| Android `addAddress()`, `addRoute()` | Both families supported. Adding one allows that family through the VPN | A family you never add stays blocked |
| Android `allowFamily()` | Unblocks a family without adding entries. Traffic typically falls through to the underlying network | Use it only on purpose |
| Apple `NEDNSSettings` | `servers`, plus `matchDomains` for split DNS | A wrong `matchDomains` sends queries to the wrong resolver |
| Apple `NEIPv6Settings` | `includedRoutes` with `::/0` sends unmatched IPv6 through the tunnel | Apple does not document the result of omitting it, so test |
| Apple `includeAllNetworks` | Drops traffic when the VPN is not connected. DHCP, captive portal and some cellular traffic stay outside | These exceptions appear in captures. Learn them |

Apple also expects IPv6 readiness. Its [IPv6 support page](https://developer.apple.com/support/ipv6/) says App Store apps must support IPv6 only networks, that hardcoded IPv4 addresses need changes, and that you should test on an IPv6 only network.

## How Do You Set Up a VPN DNS Leak Test?

Use two vantage points, because each sees what the other cannot.

**G, the gateway.** Put the device on a Wi Fi network provided by a Linux machine you control, with nothing else on it, and capture there:

```
tcpdump -ni wlan0 -w leak.pcap 'not host 203.0.113.10 and not host 2001:db8::10 and not arp and not port 67 and not port 68 and not icmp6'
```

Swap in your endpoint's IPv4 and IPv6 addresses. This records everything on the link except the endpoint, ARP, anything on ports 67 and 68 and all ICMPv6, which hides IPv6 neighbor discovery but also any other ICMPv6. What remains is a documented exception or a leak. Ignore multicast chatter such as mDNS. On desktop, Wireshark on the physical adapter gives the same view. G cannot see cellular.

**S, the server.** On a test server, run an authoritative DNS server for a zone you own, plus a web server. Give every test fresh random names, such as `k3f9a.canary.yourzone.example`, and capture:

```
tcpdump -ni eth0 'port 53 or port 80'
```

The source of each DNS query is the resolver that asked on the device's behalf. The source of each web request is the address the world sees. Your VPN's resolver exit is a pass. An ISP or carrier resolver is a fail. To look up what network an address belongs to, you can paste it into [IPLogs](https://digitaldtech.com/products/iplogs/). Its product page says it returns a verdict and a 0 to 100 threat score for any IPv4 or IPv6 address, with the network identity, geolocation, ASN and BGP prefix behind it.

On iOS, Apple's [packet trace guide](https://developer.apple.com/documentation/network/recording-a-packet-trace) shows how to capture from a Mac through a remote virtual interface with `rvictl`. That interface covers the device's whole networking stack, and the packet metadata records the interface for each packet. The guide says `tcpdump -k` displays that metadata and `-Q` filters on it, which is how you tell Wi Fi from cellular.

## What Are the Nine Tests?

| Test | How to run | Expected | Pass or fail |
|---|---|---|---|
| 1. Connect | Start G and S. Connect, then resolve fresh canary names over IPv4 and IPv6. On Android, repeat once with Private DNS set to a hostname | S sees the VPN resolver exit. G shows only the endpoint | Fail on any DNS or IPv6 packet at G, or an ISP resolver at S |
| 2. Steady state | Ten minutes idle, then ten of normal use with background apps | Same as test 1, no new destinations at G | Fail on any packet that is not the endpoint or a documented exception |
| 3. Reconnect | Block the endpoint at the gateway for 30 seconds, then release it | If you promise a kill switch, nothing at G during the gap. Lookups use the tunnel after | Fail on gap packets or a stale resolver afterward |
| 4. Wi Fi to cellular | Connect on Wi Fi, switch to cellular, then back. Use S, plus `rvictl` on iOS | Canary lookups keep arriving from the VPN exit | Fail if S sees a carrier or ISP address on either network |
| 5. IPv6 only network | Join an IPv6 only network with NAT64 and DNS64 | The tunnel connects by hostname. IPv6 stays inside the tunnel or is blocked on purpose | Fail on connect errors from IPv4 literals, or IPv6 at G |
| 6. Airplane mode | Toggle on, wait, toggle off | The first packets go to the endpoint | Fail if lookups precede the tunnel |
| 7. App kill | Force stop the app, then browse | Matches your documented promise | Fail if behavior differs from it |
| 8. Split tunnel apps | Include app A, exclude app B, resolve names in each | A reaches S from the VPN exit. B uses the system resolver by design | Fail if A shows at G |
| 9. DoH apps | Turn on secure DNS in a browser and resolve names | Resolver traffic is HTTPS on port 443 and rides the tunnel in a full tunnel | Fail if it bypasses the tunnel. Record the resolver |

The expected results are targets for a test plan, not documented platform behavior, and the platform pages we read are silent on several of these cases. Run the set on each OS version and device vendor you support. Behavior differs, and your results should say which.

## What Do You Do When a Test Fails?

Work backward from where the packet appeared. DNS at G means the resolver was never set for that path, so check `addDnsServer()` or `dnsSettings`, then check the IPv6 resolver separately. IPv6 at G means add the address and route, or block the family deliberately, and avoid `allowFamily()` unless you mean it. Packets only during gaps point to enforcement, not configuration, and our [kill switch guide](https://digitaldtech.com/blog/vpn-kill-switch-how-it-works/) covers that. Excluded apps are a split tunneling question, covered in our [split tunneling guide](https://digitaldtech.com/blog/split-tunneling-vpn-apps/). Retest with the same canary names after every fix.

## FAQ

### What is a VPN DNS leak?

A VPN DNS leak is a DNS query that leaves the device outside the tunnel, usually to the ISP's resolver, while the VPN appears connected. It exposes the names being looked up even when the traffic itself is tunneled.

### How do I test for an IPv6 leak without a website?

Capture at a gateway and look for IPv6 packets that are not going to your VPN endpoint. You can also give your canary server an IPv6 address and see whether the device reaches it from the VPN exit.

### Does Android Private DNS cause leaks with a VPN?

The Android pages we read do not say, so test it. Set a Private DNS hostname, connect, and watch both your lookups and connectivity. Apps with their own DNS client should follow the system's encrypted DNS settings.

### Is DNS over HTTPS in an app a leak?

Not a network leak, because the lookups travel over HTTPS and, in a full tunnel, through the tunnel. It is the app choosing its own resolver, so record it as known behavior.

### Does blocking IPv6 count as passing?

Yes, if you chose it deliberately and the app still works on IPv6 only networks. The pass condition is that no IPv6 leaves outside the tunnel, whether it is carried or blocked.

## The Takeaway

Run the VPN DNS leak test at every state change, not only after connect, because the gaps are where most leaks hide. Keep the captured names and results per OS version so the next release has a baseline.

Building or auditing a VPN app and unsure what to test? DigitalD.tech can help you plan the platform settings, the test matrix and the launch path. Tell us your platforms on the [contact page](https://digitaldtech.com/contact/).

*Last checked against Android's platform source, VPN guide and Android 9 behavior changes, Apple's Network Extension documentation, packet trace guide and IPv6 support page, WireGuard's Windows network notes, and RFC 8305 and RFC 8484 on 30 October 2026.*

