mullvadvpn-app icon indicating copy to clipboard operation
mullvadvpn-app copied to clipboard

Linux IPv6 support / RFC 3484 compatibility

Open kubrickfr opened this issue 2 years ago • 5 comments

Although the Mullvad app is compatible with IPv6 on Linux, and can provide IPv6 inside the tunnels, it actually doesn't get used most of the time.

Mullvad provides IPv6 in the same way it provides IPv4: by providing a ULA in the range fc00::/7 which is then NATed to a public IP on the Mullvad server.

However, Linux implements RFC 3484 via the libc's getaddrinfo(), and is configured by default in /etc/gai.conf to not return an IPv6 address if only ULAs are configured. Quoting the configuration file:

#    This default differs from the tables given in RFC 3484 by handling
#    (now obsolete) site-local IPv6 addresses and Unique Local Addresses.
#    The reason for this difference is that these addresses are never
#    NATed while IPv4 site-local addresses most probably are.  Given
#    the precedence of IPv6 over IPv4 (see below) on machines having only
#    site-local IPv4 and IPv6 addresses a lookup for a global address would
#    see the IPv6 be preferred.  The result is a long delay because the
#    site-local IPv6 addresses cannot be used while the IPv4 address is
#    (at least for the foreseeable future) NATed.  We also treat Teredo
#    tunnels special

One way to make use of the connectivity provided by Mullvad is to change this default configuration. However it is not sufficient as it only helps with programs using getaddressinfo(), other software, in particular those not using DNS, check that the address is global unicast themselves, like transmission or WebRTC

Also, very few users are going to change this obscure piece of configuration...

I have implemented a way that, although hacky, should be fairly reliable:

  • I assign the exit IPv6 IP to the tunnel locally
  • SNAT outgoing connection to the ULA address
  • DNAT incoming connection to the global unicast address

This scrip is my little PoC:

#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0
#
# Copyright (C) 2022 François Guerraz <[email protected]>. All Rights Reserved.

die() {
	echo "[-] Error: $1" >&2
	exit 1
}

PROGRAM="${0##*/}"
ARGS=( "$@" )
SELF="${BASH_SOURCE[0]}"
[[ $UID == 0 ]] || exec sudo -p "[?] $PROGRAM must be run as root. Please enter the password for %u to continue: " -- "$BASH" -- "$SELF" "${ARGS[@]}"

# Collect data
LOCAL_IP=$(ip a show dev wg-mullvad | grep -o 'fc00[0-9a-f\:]*')
REMOTE_IP=$(curl -s https://ipv6.am.i.mullvad.net/)

# Clean-up previous run if any
ip addr del $REMOTE_IP dev wg-mullvad 2> /dev/null || true
nft delete chain ip6 nat postrouting 2> /dev/null || true
nft delete chain ip6 nat prerouting 2> /dev/null || true
nft flush table ip6 nat 2> /dev/null || true

# Insert new rules 
nft add table ip6 nat
nft add chain ip6 nat postrouting { type nat hook postrouting priority 0 \; }
nft add rule ip6 nat postrouting ip6 saddr $REMOTE_IP snat to $LOCAL_IP
nft add chain ip6 nat prerouting { type nat hook prerouting priority 0 \; }
nft add rule ip6 nat prerouting ip6 daddr $LOCAL_IP ct state new dnat to $REMOTE_IP
ip addr add $REMOTE_IP dev wg-mullvad

The only limitation that I am aware of, is that if a local client wanted to establish a connection to a service from another user using the same node this would fail. To fix it we would have to route traffic to $LOCAL_IP via the tunnel if it doesn't come from the tunnel.

I think this should be fairly easy to implement inside the daemon considering we're already using nftables and doing IP detection there.

kubrickfr avatar May 23 '22 13:05 kubrickfr

If anybody is interested, I have turned it into a NetworkManager dispatch script.

Also, it uses the existing mullvadmangle6 table managed by the Mullvad daemon.

#!/usr/bin/env bash
set -e

[[ "$CONNECTION_ID" == "wg-mullvad" ]] || exit 0


if [ "$2" == "up" ]; then 
	# Collect data
	LOCAL_IP=$(ip a show dev wg-mullvad | grep -o 'fc00[0-9a-f\:]*')
	REMOTE_IP=$(curl --retry-all-errors --retry-max-time 19 --retry 10 -s https://ipv6.am.i.mullvad.net/) || echo "Error getting remote IP" | systemd-cat -p error -t dispatch_script

	[[ "$REMOTE_IP" != "" ]] || exit 1

	echo "Adding masquerade rules from $LOCAL_IP to $REMOTE_IP" | systemd-cat -p info -t dispatch_script

	# Insert new rules 
	nft add rule ip6 mullvadmangle6 nat ip6 saddr $REMOTE_IP snat to $LOCAL_IP
	nft add chain ip6 mullvadmangle6 nat-in { type nat hook prerouting priority 0 \; }
	nft add rule ip6 mullvadmangle6 nat-in ip6 daddr $LOCAL_IP ct state new dnat to $REMOTE_IP
	ip addr add $REMOTE_IP dev wg-mullvad
fi

kubrickfr avatar Jun 22 '22 16:06 kubrickfr

If anybody is interested, I have turned it into a NetworkManager dispatch script.

~~Is there a way to trigger this dispatch script when connecting to a new mullvad endpoint through the app? It does work well when initially connecting but not when changing endpoints since the wg-mullvad link seems to stay up even when disconnected so NetworkManager-dispatch doesn't seem to get any kind of event.~~

ghost avatar Jul 10 '22 15:07 ghost

I'm also on arch, using the beta version of Mullvad though, and I don't have this issue.

kubrickfr avatar Jul 10 '22 18:07 kubrickfr

You are correct. There seems to a kernel netlink issue with 5.19.0-0.rc4 unrelated to this which prevents interfaces to be correctly removed on my device. Your script works great, thanks!

ghost avatar Jul 10 '22 19:07 ghost

I just thought I'd post an up to date version of the dispatch script I'm using, which is compatible with the latest versions

#!/usr/bin/env bash
set -e

[[ "$CONNECTION_ID" == "wg0-mullvad" ]] || exit 0


if [ "$2" == "up" ]; then 
	# Collect data
	LOCAL_IP=$(ip a show dev $CONNECTION_ID | grep -o 'fc00[0-9a-f\:]*')
	REMOTE_IP=$(curl --retry-all-errors --retry-max-time 19 --retry 10 -s https://ipv6.am.i.mullvad.net/) || echo "Error getting remote IP" | systemd-cat -p error -t dispatch_script

	[[ "$REMOTE_IP" != "" ]] || exit 1

	echo "Adding masquerade rules from $LOCAL_IP to $REMOTE_IP" | systemd-cat -p info -t dispatch_script

	# Insert new rules 
	nft add rule inet mullvad nat ip6 saddr $REMOTE_IP snat to $LOCAL_IP
	nft add chain inet mullvad nat-in { type nat hook prerouting priority 0 \; }
	nft add rule inet mullvad nat-in ip6 daddr $LOCAL_IP ct state new dnat to $REMOTE_IP
	ip addr add $REMOTE_IP dev $CONNECTION_ID
fi

kubrickfr avatar Dec 19 '23 16:12 kubrickfr