Skip to main content

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:

  • NEIPv4Settings with a default route → directs IPv4 into the tunnel
  • NEIPv6Settings with 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​

  1. Connect the VPN.
  2. Open a browser and navigate to https://ipv6leak.com or https://ipleak.net.
  3. 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:

  • AllowedIPs contains both 0.0.0.0/0 and ::/0
  • PeerConfiguration.allowedIPs is set as an array with both IPAddressRange entries
  • The connection log shows both routes at tunnel start
  • A manual leak test on ipleak.net shows no IPv6 address while connected
  • The WireGuard log message Missing IPv6 address is treated as expected and harmless

Affected Projects​

ProjectStatusFile
kp_platform_sdk (this repo)✅ fixed — all protocols (WireGuard, Lightway, OpenVPN) via chokepointSources/KapeVPN-PacketTunnel/Tunnel/KapeSystemTunnel.swift
kp_event_vpn⚠️ needs fixVpnManager/Sources/VpnManager/Entities/WGEndpoint.swift — allowedIPs = "0.0.0.0/0"

References​