VPN IPv6 Leak Prevention
The Problem
A VPN application that only sets 0.0.0.0/0 as the WireGuard AllowedIPs tunnels exclusively
IPv4 traffic. IPv6 traffic bypasses the VPN tunnel entirely and leaves the device unencrypted
over the physical interface (Wi-Fi, cellular). This is a critical privacy vulnerability:
the user's real location and activity are exposed over IPv6 even though they believe they are
fully protected.
This bug is particularly insidious because:
- It occurs silently on IPv4-only servers — no error is raised
- It affects all modern devices, since iOS and macOS prefer IPv6 by default
- It leaks even when the user is not aware they have an active IPv6 connection
Why This Happens on IPv4-only Servers
WireGuard defines its routing exclusively via AllowedIPs. Any packet whose destination
address is not covered by a peer's AllowedIPs is not sent into the tunnel — it is forwarded
through the device's regular routing table instead.
AllowedIPs = "0.0.0.0/0" → IPv4: tunnelled ✓
IPv6: leaked ✗
Many VPN servers do not support IPv6. Developers set only 0.0.0.0/0 and omit the IPv6 default
route. The result is an IPv6 leak.
The Fix
::/0 (the IPv6 default route) must always be added alongside 0.0.0.0/0 in AllowedIPs
— even when the server does not support IPv6.
AllowedIPs = ["0.0.0.0/0", "::/0"] → IPv4: tunnelled ✓
IPv6: tunnelled ✓ (dropped at server)
What happens to IPv6 packets at the server? The server has no IPv6 address and no IPv6 forwarding rules. Packets are received inside the tunnel and dropped — they do not exit the server onto the internet. This is the correct behaviour: no leak, no data loss, no connection error for the user.
In this configuration the WireGuard Go backend will log:
Missing IPv6 address
This message is not an error. It is an informational log emitted when the tunnel interface itself has no IPv6 address assigned, which is expected for IPv4-only servers. It can safely be ignored in the log viewer.
Implementation in This SDK (Apple / Swift)
In KapeWireGuardConfig (Sources/KapeVPN/Models/KapeWireGuardAuth.swift):
// WRONG — IPv6 leaks
var allowedIPs: String = "0.0.0.0/0"
// CORRECT — IPv6 is routed into the tunnel and dropped at the server
var allowedIPs: [String] = ["0.0.0.0/0", "::/0"]
In KapeWireGuardProvider.makePeerConfiguration (Sources/KapeVPN/Tunnel/KapeWireGuardProvider.swift),
all entries are parsed and passed as an array:
let parsedAllowedIPs = wgConfig.allowedIPs.compactMap { IPAddressRange(from: $0) }
peerConfig.allowedIPs = parsedAllowedIPs
WireGuardKit's PacketTunnelSettingsGenerator then produces:
NEIPv4Settingswith a default route → directs IPv4 into the tunnelNEIPv6Settingswith a default route → directs IPv6 into the tunnel
Chokepoint guarantee — all protocols, all modes
The AllowedIPs approach above only covers WireGuard. IPv6 leak prevention is now
enforced uniformly at the shared chokepoint
KapeSystemTunnel.setTunnelNetworkSettings (Sources/KapeVPN-PacketTunnel/Tunnel/KapeSystemTunnel.swift).
Every protocol's network settings pass through this method; before they are applied,
ensuringIPv6BlackholeRoute(in:) guarantees an IPv6 default route (::/0) is present:
- WireGuard and Lightway already carry an IPv6 default route → the helper is a no-op (idempotent).
- OpenVPN on IPv4-only servers previously produced
ipv6Settings == nil(TunnelKit returns no IPv6 when the server pushes none) → the helper adds the default route, closing the leak. This was the gap this change fixes.
Split tunnel does not support IPv6 yet. Because the route is forced unconditionally, a split tunnel also blackholes all IPv6 — IPv6 to bypassed destinations is captured by the tunnel and dropped at the server rather than escaping over the physical interface. This is intentional, not a regression. When selective IPv6 split-tunneling is required, it must be handled at the protocol's settings-build layer, not the chokepoint.
When no IPv6 interface address is present, the helper seeds the ULA address
fd00:cafe:face::2/128 (NEIPv6Settings requires at least one address). It mirrors
Lightway's IFACE_LOCAL_IPV6.
Verification
Check the connection log
After connecting, the log line must show both routes:
Building WireGuard config — host=1.2.3.4:51820 obfuscation=none allowedIPs='0.0.0.0/0, ::/0' ...
Manual leak test
- Connect the VPN.
- Open a browser and navigate to
https://ipv6leak.comorhttps://ipleak.net. - No IPv6 address should appear. If one does, the tunnel is misconfigured.
Technical verification (iOS / macOS)
# Check the routing table — the utun interface must be the default for both address families
netstat -rn | grep -E "^default|^::/0"
Both lines should reference the utun interface when the VPN is active.
Checklist for Other Projects
When implementing WireGuard in any application, verify the following:
-
AllowedIPscontains both0.0.0.0/0and::/0 -
PeerConfiguration.allowedIPsis set as an array with bothIPAddressRangeentries - The connection log shows both routes at tunnel start
- A manual leak test on
ipleak.netshows no IPv6 address while connected - The WireGuard log message
Missing IPv6 addressis treated as expected and harmless
Affected Projects
| Project | Status | File |
|---|---|---|
kp_platform_sdk (this repo) | ✅ fixed — all protocols (WireGuard, Lightway, OpenVPN) via chokepoint | Sources/KapeVPN-PacketTunnel/Tunnel/KapeSystemTunnel.swift |
kp_event_vpn | ⚠️ needs fix | VpnManager/Sources/VpnManager/Entities/WGEndpoint.swift — allowedIPs = "0.0.0.0/0" |