├── README.md ├── am-i-mullvad.sh └── mullvad-wg-netns.sh /README.md: -------------------------------------------------------------------------------- 1 | dxld's mullvad config 2 | ===================== 3 | 4 | Overview 5 | -------- 6 | 7 | I use libpam-net to segregate a dedicated system user into a network namespace 8 | which can only talk to the outside world through a wireguard interface. 9 | 10 | [`mullvad-wg-netns.sh`](mullvad-wg-netns.sh) implements the provisioning of the 11 | wireguard configs (generating privkey, uploading pubkey to mullvad API etc.). It 12 | also supports bringing up the wireguard interface at boot since `wg-quick` does 13 | not support netns or operating on a pre-existing wg interface. 14 | 15 | Setup on Debian bullseye or later 16 | --------------------------------- 17 | 18 | First we set up dependencies and libpam-net: 19 | 20 | $ apt-get install dateutils curl jq wireguard-tools libpam-net 21 | $ pam-auth-update --enable libpam-net-usernet 22 | $ addgroup --system usernet 23 | $ adduser usernet 24 | 25 | Note we need at least libpam-net 0.3, which is part of Debian bullseye 26 | now. Yey! 27 | 28 | Now whenever `` logs in, or a service is started as them, it will 29 | be placed in a netns (cf. ip-netns(8)) corresponding to their 30 | username. This netns is created if it doesn't already exist, but the 31 | intention is that you arrange for it to be setup during boot. 32 | 33 | Next we provision the wireguard configs: 34 | 35 | $ path/to/mullvad-wg-net.sh provision 36 | 37 | This will ask you for your mullvad account number, so keep that ready. What 38 | this does is associate your mullvad account with the wg private key it 39 | generates. 40 | 41 | Note: The account number is not stored on the system after provisioning. 42 | 43 | We're almost done, now we setup `resolv.conf` to prevent DNS leaks in the 44 | netns: 45 | 46 | $ mkdir -p /etc/netns/ 47 | $ printf '%s\n' '# Mullvad DNS' 'nameserver 10.64.0.1' > /etc/netns//resolv.conf 48 | $ chattr +i /etc/netns//resolv.conf 49 | 50 | I do `chattr +i` to prevent resolvconf from meddling with this config. I suppose 51 | it would be possible just to change the resolvconf configuration to get it 52 | seperated from the main system, but without changes it will just use the DNS of 53 | the rest of the system. 54 | 55 | Finally to start the mullvad wireguard interface you should use the following 56 | command: 57 | 58 | $ path/to/mullvad-wg-net.sh init mullvad-.conf 59 | 60 | Replace `` by whatever mullvad region you want to use, for example 61 | `mullvad-at1.conf`, you can find the full list in `/etc/wireguard/` after 62 | provisioning. 63 | 64 | To make this permanent you can simply put it in `/etc/rc.local` or create a 65 | systemd unit or something if you insist. 66 | 67 | 68 | Security 69 | -------- 70 | 71 | In order to make sure this whole setup works and to prevent leaks if 72 | something fails I like to check if connectivity is going through mullvad on 73 | login. The mullvad guys provide a convinient service for this: 74 | https://am.i.mullvad.net and I wrote a convinient shell wrapper for it: 75 | [am-i-mullvad.sh](am-i-mullvad.sh). 76 | 77 | To use it put it in your `.bash_profile` or simmilar shell startup script: 78 | 79 | $ cat >> .bash_profile << EOF 80 | sh path/to/am-i-mullvad.sh || exit 1 81 | EOF 82 | 83 | If we're not connected through mullvad it will print an error message and kill 84 | the shell after a short timeout so you can still get access by Ctrl-C'ing the 85 | script if needed. 86 | -------------------------------------------------------------------------------- /am-i-mullvad.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-or-later 3 | # 4 | # Copyright (C) 2018 Daniel Gröber 5 | 6 | not_on_mullvad() { 7 | echo>&2 8 | echo>&2 9 | echo "!!! NOT ON MULLVAD !!!" >&2 10 | echo "$1">&2 11 | echo>&2 12 | sleep 3 13 | exit 123 14 | } 15 | 16 | warning() { 17 | echo>&2 18 | echo "$1">&2 19 | } 20 | 21 | 22 | echo -n 'Checking Mullvad...'>&2 23 | 24 | # IP Leak check 25 | 26 | mullvad_ip4="$( curl -4 -s --max-time 3 https://am.i.mullvad.net/json | jq -r '.mullvad_exit_ip' )" 27 | mullvad_ip6="$( curl -6 -s --max-time 3 https://ipv6.am.i.mullvad.net/json | jq -r '.mullvad_exit_ip' )" 28 | 29 | ip_check () { 30 | local var msg 31 | var="$1"; shift 32 | msg="$1"; shift 33 | 34 | local mullvad_ip 35 | eval "mullvad_ip=\$$var" 36 | if [ "$mullvad_ip" = 'false' ]; then 37 | not_on_mullvad "- $msg Leaking" 38 | exit 123 #not reached 39 | elif [ "$mullvad_ip" = '' ]; then 40 | warning "- $msg check errored" 41 | return 1 42 | fi 43 | 44 | return 0 45 | } 46 | 47 | ip_check mullvad_ip4 "IPv4"; rv_ip4=$? 48 | ip_check mullvad_ip6 "IPv6"; rv_ip6=$? 49 | if [ $rv_ip4 -ne 0 ] && [ $rv_ip6 -ne 0 ]; then 50 | not_on_mullvad "- All IP checks errored" 51 | fi 52 | 53 | dnsleak_domain=$(curl -s --max-time 3 https://am.i.mullvad.net/config | jq -r '.dns_leak_domain' ) 54 | 55 | # DNS Leak check 56 | 57 | dnsids= 58 | 59 | for i in $(seq 0 3); do 60 | id=$(xxd -p -l16 < /dev/urandom) 61 | dnsids="$dnsids $id" 62 | (curl -s "https://$id.$dnsleak_domain/" > /dev/null 2>&1 || true)& 63 | done 64 | 65 | wait 66 | 67 | for i in $dnsids; do 68 | mullvad_dns="$(curl -s --max-time 10 https://am.i.mullvad.net/dnsleak/$id \ 69 | | jq '[ .[] | .mullvad_dns ] | all')" 70 | 71 | if [ "$mullvad_dns" = '' ]; then 72 | warning "- DNS check errored" 73 | elif [ "$mullvad_dns" = 'false' ]; then 74 | not_on_mullvad "- DNS Leaking" 75 | fi 76 | done 77 | 78 | echo 'OK'>&2 79 | 80 | 81 | if [ -r ~/.mullvad-expiry ]; then 82 | expiry="$(cat ~/.mullvad-expiry)" 83 | 84 | if which dateutils.ddiff > /dev/null 2>&1; then 85 | dateutils.ddiff now "$expiry" -f 'Expires in %ddays %Hhours.' >&2 86 | else 87 | printf 'Expires on %s\n' "$(date -d "$expiry")" >&2 88 | fi 89 | fi 90 | -------------------------------------------------------------------------------- /mullvad-wg-netns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0 3 | # 4 | # Copyright (C) 2018 Daniel Gröber 5 | # Copyright (C) 2016-2018 Jason A. Donenfeld . 6 | # All Rights Reserved. 7 | 8 | # Based on https://mullvad.net/media/files/mullvad-wg.sh but modified to be 9 | # POSIX sh compliant and easier to review. This version also supports using a 10 | # wireguard interface in a network namespace. 11 | 12 | die() { 13 | echo "[-] Error: $1" >&2 14 | exit 1 15 | } 16 | 17 | provision() { 18 | umask 077 19 | 20 | ACCOUNT= 21 | if [ -r "$HOME"/.mullvad-account ]; then 22 | ACCOUNT="$(cat "$HOME"/.mullvad-account)" 23 | fi 24 | if [ -z "$ACCOUNT" ]; then 25 | printf '[?] Please enter your Mullvad account number: ' 26 | read -r ACCOUNT 27 | ACCOUNT=$(printf '%s' "$ACCOUNT" | tr -d '[[:space:]]') 28 | fi 29 | 30 | key="$(cat /etc/wireguard/mullvad-*.conf \ 31 | | sed -rn 's/^PrivateKey *= *([a-zA-Z0-9+/]{43}=) *$/\1/ip;T;q')" 32 | 33 | if [ -n "$key" ]; then 34 | echo "[+] Using existing private key." 35 | sleep 2 36 | echo "[?] Are you absolutely sure? (Ctrl-C to cancel)" 37 | sleep 1.5 38 | echo "[?] If you changed account number this will allow linking them!" 39 | printf '[?] Press ENTER to confirm...' 40 | read _ 41 | sleep 1 42 | echo OK. 43 | else 44 | echo "[+] Generating new private key." 45 | key="$(wg genkey)" 46 | fi 47 | 48 | mypubkey="$(printf '%s\n' "$key" | wg pubkey)" 49 | 50 | echo "[+] Submitting wg public key to Mullvad API." 51 | res="$(curl -sSL https://api.mullvad.net/wg/ \ 52 | -d account="$ACCOUNT" \ 53 | --data-urlencode pubkey="$mypubkey")" 54 | if ! printf '%s\n' "$res" | grep -E '^[0-9a-f:/.,]+$' >/dev/null 55 | then 56 | die "$res" 57 | fi 58 | myipaddr=$res 59 | 60 | echo "[+] Removing old /etc/wireguard/mullvad-*.conf files." 61 | rm /etc/wireguard/mullvad-*.conf || true 62 | 63 | echo "[+] Contacting Mullvad API for server locations." 64 | 65 | curl -LsS https://api.mullvad.net/public/relays/wireguard/v1/ \ 66 | | jq -r \ 67 | '( .countries[] 68 | | (.name as $country | .cities[] 69 | | (.name as $city | .relays[] 70 | | [$country, $city, .hostname, .public_key, 71 | .ipv4_addr_in, .ipv6_addr_in]) 72 | ) 73 | ) 74 | | flatten 75 | | join("\t")' \ 76 | | while read -r country city hostname pubkey ip4addr ip6addr; do 77 | code="${hostname%-wireguard}" 78 | addr="$ip4addr:51820" # TODO: allow v4/v6 choice 79 | 80 | conf="/etc/wireguard/mullvad-${code}.conf" 81 | 82 | if [ -f "$conf" ]; then 83 | oldpubkey="$(sed -rn 's/^PublicKey *= *([a-zA-Z0-9+/]{43}=) *$/\1/ip' <"$conf")" 84 | if [ -n "$oldpubkey" ] && [ "$pubkey" != "$oldpubkey" ]; then 85 | echo "WARNING: $hostname changed pubkey from '$oldpubkey' to '$pubkey'" 86 | continue 87 | fi 88 | fi 89 | 90 | mkdir -p /etc/wireguard/ 91 | rm -f "${conf}.tmp" 92 | cat > "${conf}.tmp" <<-EOF 93 | [Interface] 94 | PrivateKey = $key 95 | Address = $myipaddr 96 | 97 | [Peer] # $country, $city 98 | PublicKey = $pubkey 99 | Endpoint = $addr 100 | AllowedIPs = 0.0.0.0/0, ::/0 101 | EOF 102 | mv "${conf}.tmp" "${conf}" 103 | done 104 | 105 | 106 | 107 | ACCOUNT_INFO=$(curl -s https://api.mullvad.net/www/accounts/"$ACCOUNT"/) 108 | TOKEN=$(printf '%s\n' "$ACCOUNT_INFO" | jq -r .auth_token) 109 | expiry=$(printf '%s\n' "$ACCOUNT_INFO" | jq -r .account.expires) 110 | 111 | #printf '%s\n' "$ACCOUNT_INFO" | jq . 112 | 113 | curl -s -X POST https://api.mullvad.net/www/expire-auth-token/ \ 114 | -H "Authorization: Token $TOKEN" 115 | 116 | printf '%s\n' "$expiry" > ~/.mullvad-expiry 117 | 118 | echo; echo 119 | if command -v dateutils.ddiff > /dev/null 2>&1; then 120 | dateutils.ddiff now "$expiry" -f 'Account expires in %ddays %Hhours.' >&2 121 | else 122 | printf 'Account expires on %s\n' "$(date -d "$expiry")" >&2 123 | fi 124 | 125 | echo; echo 126 | echo "Please wait up to 60 seconds for your public key to be added to the servers." 127 | } 128 | 129 | init () { 130 | nsname=$1; shift 131 | cfgname=$1; shift 132 | parentns=${parentns:-} 133 | wgifname="wg-$nsname" 134 | 135 | # [Note POSIX array trick] 136 | # Ok, this is a nasty POSIX shell trick, we use the _one_ array we have 137 | # access to, the args, aka "$@" to store the -netns option I optionally 138 | # want to pass to `ip` below. Since we're done with cmdline parsing at this 139 | # point that's totally fine, just a bit opaque. Hence this comment. 140 | # 141 | # You're welcome. 142 | if [ -z "$parentns" ]; then 143 | set -- 144 | else 145 | set -- -netns "$parentns" 146 | fi 147 | 148 | # Check for old wg interfaces in (1) current namespace, 149 | if [ -z "$parentns" ] && [ -e /sys/class/net/"$wgifname" ]; then 150 | ip link del dev "$wgifname" 151 | fi 152 | 153 | # (2) parent namespace and 154 | if [ -n "$parentns" ] && ip netns exec "$parentns" \ 155 | [ -e /sys/class/net/"$wgifname" ] 156 | then 157 | ip -netns "$parentns" link del dev "$wgifname" 158 | fi 159 | 160 | # (3) target namespace. 161 | if ip netns exec "$nsname" [ -e /sys/class/net/"$wgifname" ]; then 162 | ip -netns "$nsname" link del dev "$wgifname" 163 | fi 164 | 165 | # See [Note POSIX array trick] above. 166 | ip "$@" link add "$wgifname" type wireguard 167 | 168 | if ! [ -e /var/run/netns/"$nsname" ]; then 169 | ip netns add "$nsname" 170 | fi 171 | 172 | # Move the wireguard interface to the target namespace. See [Note POSIX 173 | # array trick] above. 174 | ip "$@" link set "$wgifname" netns "$nsname" 175 | 176 | # shellcheck disable=SC2002 # come on, < makes the pipeline read like shit 177 | cat /etc/wireguard/"$cfgname" \ 178 | | grep -vi '^Address\|^DNS' \ 179 | | ip netns exec "$nsname" wg setconf "$wgifname" /dev/stdin 180 | 181 | addrs="$(sed -rn 's/^Address *= *([0-9a-fA-F:/.,]+) *$/\1/ip' < /etc/wireguard/"$cfgname")" 182 | 183 | ip -netns "$nsname" link set dev lo up 184 | ip -netns "$nsname" link set dev "$wgifname" up 185 | 186 | ( 187 | IFS=',' 188 | for addr in $addrs; do 189 | ip -netns "$nsname" addr add dev "$wgifname" "$addr" 190 | done 191 | ) 192 | 193 | ip -netns "$nsname" route add default dev "$wgifname" 194 | ip -netns "$nsname" -6 route add default dev "$wgifname" 195 | 196 | } # end init() 197 | 198 | 199 | set -e 200 | 201 | cmd="${1:-provision}"; shift 202 | "$cmd" "$@" # run $cmd with rest of args 203 | --------------------------------------------------------------------------------