Disabling IPv6 when an IPv4 only wireguard config is up

On June 01, 2023 by Sosthène Guédon

On linux, when using a wireguard VPN that only supports IPv4, the wg-quick scripts and NetworkManager will not do anything regarding IPv6, which will simply go through the default gateway and "leak".

It's not easy to reliably prevent this. Here is how to do it with NetworManager.

A standard wireguard IPv4 only config looks like this (xxx marks redacted data):

[Interface]
PrivateKey = xxx
Address = 10.0.0.2/32
DNS = 10.0.0.1

[Peer]
PublicKey = xxx
AllowedIPs = 0.0.0.0/0    
Endpoint = 101.121.131.141

Whereas an interface supporting IPv6 will have an additional IPv6 address configured:

Address = 10.0.0.2/32,fc00:0000:0000:0000:0000:0000:0002/128 

...

AllowedIPs = 0.0.0.0/0, ::0/0

In the first case, IPv6 traffic will not go through the VPN.

The best solution is to have a VPN server that supports IPv6, in which case a config similar to the second will work. However, this may not be a possibility. In that case, it is required to disable IPv6 when connecting to a VPN which does not support it.

The easiest way to solve the issue is to completely disable IPv6 on the machine by adding ipv6.disable=1 to your kernel boot parameters, but it is not an ideal solution as it makes you lose IPv6 support even when the VPN is disabled.

A more flexible solution is to use the NetworkManager dispatcher. This works by adding scripts to /etc/NetworManager/dispatcher.d/, which are run each time NetworkManager performs some actions. The following script will run every time the wg0 1 interface is up (or down), to deactivate (or reactivate) IPv6 for all other running interfaces.

#!/bin/sh

interface=$1 status=$2
WG_IPV4_INTERFACE="wg0"

if [ "$interface" != "$WG_IPV4_INTERFACE" ]
then
    exit
fi

case $status in
    up)
        # Disable IPV6.
        sysctl -w net.ipv6.conf.all.disable_ipv6=1
   	    sysctl -w net.ipv6.conf.default.disable_ipv6=1
        sysctl -w net.ipv6.conf.lo.disable_ipv6=1

    ;;
    down)
        # Enable IVP6.
        sysctl -w net.ipv6.conf.all.disable_ipv6=0
	      sysctl -w net.ipv6.conf.default.disable_ipv6=0
        sysctl -w net.ipv6.conf.lo.disable_ipv6=0
    ;;
esac

Save it to /etc/NetworkManager/dispatcher.d/99-wg-ipv6-leak-protection.sh and it will disable IPv6 when the VPN is up (don't forget to enable the dispatcher, with systemctl enable --now NetworkManager-dispatcher).

However this would not fully solve the problem. If you reconnect to your network, it will re-enable IPv6 for the connection, since the sysctl configurations are ephemeral.

The solution for this is to instead check every time an interface is up whether the VPN is running. If it is, disable IPv6 for the interface again:

#!/bin/sh

interface=$1 action=$2

WG_IPV4_INTERFACE="proton-wg"
wg_ipv4_interface_state=$(cat "/sys/class/net/$WG_IPV4_INTERFACE/operstate")

if [ "$interface" == "$WG_IPV4_INTERFACE" ]
then
    case "$action" in
        up)
            # Disable IPV6.
            sysctl -w net.ipv6.conf.all.disable_ipv6=1
       	    sysctl -w net.ipv6.conf.default.disable_ipv6=1
            sysctl -w net.ipv6.conf.lo.disable_ipv6=1

        ;;
        down)
            # Enable IVP6.
            sysctl -w net.ipv6.conf.all.disable_ipv6=0
    	      sysctl -w net.ipv6.conf.default.disable_ipv6=0
            sysctl -w net.ipv6.conf.lo.disable_ipv6=0
        ;;
    esac
elif [ "$wg_ipv4_interface_state" == "up" ] || [ "$wg_ipv4_interface_state" ==  "unknown" ] # It is normal for wireguard interfaces to be in the "unknown" state
then
    if [ "$action" == "up" ]
    then
        sysctl -w net.ipv6.conf."$interface".disable_ipv6=1
    fi
else
fi
1 you might need to rename it to the interface name you use ↩︎