Wireguard and Linux network namespaces - perfect fit

I'm using two browsers usually - Firefox (switched to IceCat recently) and Chromium.

Firefox/IceCat is my workhorse, which is configured to route all traffic through SOCKS5 proxy (Linode instance actually).

I would definitely stay solely with it but some sites don't like requests coming from VPS servers so I have to use Chromium for such cases (and it always run in private mode to keep it clean).

SOCKS5 proxy works pretty good for me but I was curious if it's possible to use wireguard for my usecase - to route Firefox/IceCat traffic through VPN keeping all other traffic as is.

For domain name resolution I'm using locally installed Unbound server which is bound to localhost. My /etc/resolv.conf file looks like this:

nameserver 127.0.0.1

It is configured to forward requests to Quad9 resolver (no Google, no Cloudflare, right) but I'm planning to turn forwarding off and just use Unbound on its own.

There are numerous posts in the Internet covering wireguard configuration so I'll just show my server/client configuration with some notes.

For some reason I've missed the fact that wg-quick helper uses advanced version of wg configuration file syntax which means wg-quick configuration file can't be used with wg. I've lost some time while battling this issue.

Summary of the settings:

  • 10.10.10.0/24 - VPN network
  • 10.10.10.1/32 - VPN server address
  • 10.10.10.10/32 - VPN client address
  • 55000 - VPN server port wireguard will be listening on
  • VPN server will route all traffic coming from client to the outside world

Server

My server configuration file looks like this:

# /etc/wireguard/wg0.conf on the server

[Interface]
PrivateKey = <private key of the server>
ListenPort = 55000

[Peer]
PublicKey = <public key of the client>
PresharedKey = <preshared key>
AllowedIPs = 10.10.10.10/32

I use Alpine Linux on the server for my experiments with wireguard and my wg0 interface is defined like this:

# excerpt from /etc/network/interfaces

auto wg0
iface wg0 inet static
  address 10.10.10.1
  netmask 255.255.255.0
  pre-up ip link add dev wg0 type wireguard
  pre-up wg setconf wg0 /etc/wireguard/wg0.conf
  pre-down ip link delete dev wg0

One more player here is firewall (iptables in my case). It must be configured to allow traffic coming from wg0 interface to be forwarded to outside world.

Something like this should help you to get started:

$ sudo iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
$ sudo iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
$ sudo iptables -t nat -A POSTROUTING -o eth0 -s 10.10.10.0/24 -j MASQUERADE

Don't forget to make these rules permanent and allow forwarding on the server using sysctl:

net.ipv4.ip_forward=1

That's it for the server.

Client

Client configuration file looks like this:

# /etc/wireguard/wg0.conf on the client

[Interface]
PrivateKey = <private key of the client>

[Peer]
PublicKey = <public key of the server>
PresharedKey = <preshared key>
Endpoint = <server public ip>:55000
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

I've faced another issue while configuring the client - something absolutely unexpected when pinging any host, even VPN server endpoint:

$ ping 10.10.10.1
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
From 10.10.10.10 icmp_seq=1 Destination Host Unreachable
ping: sendmsg: Required key not available
From 10.10.10.10 icmp_seq=2 Destination Host Unreachable
ping: sendmsg: Required key not available
From 10.10.10.10 icmp_seq=3 Destination Host Unreachable
ping: sendmsg: Required key not available
^C
--- 10.10.10.1 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2028ms

The same for pinging Google DNS:

$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
From 10.10.10.10 icmp_seq=1 Destination Host Unreachable
ping: sendmsg: Required key not available
From 10.10.10.10 icmp_seq=2 Destination Host Unreachable
ping: sendmsg: Required key not available
From 10.10.10.10 icmp_seq=3 Destination Host Unreachable
ping: sendmsg: Required key not available
^C
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 2037ms

The reason for this was AllowedIPs misconfiguration:

# This is wrong setting
AllowedIPs = 0.0.0.0

I've somehow lost CIDR part from the address which means address will look like 0.0.0.0/32 which is absolutely wrong.

Pay attention to wg output, to allowed ips value especially:

$ sudo wg
interface: wg0
  public key: <client public key>
  private key: (hidden)
  listening port: 52312

peer: <server public key>
  preshared key: (hidden)
  endpoint: <server ip address>:55000
  allowed ips: 0.0.0.0/32
  latest handshake: 1 minute, 12 seconds ago
  transfer: 644 B received, 2.23 KiB sent
  persistent keepalive: every 25 seconds

So, correct value for AllowedIPs should look like this:

# This is correct setting
AllowedIPs = 0.0.0.0/0

And now we've come to the most interesting part of the show - to client-side network configuration.

The solution for my usecase was here - simple script by (probably) wireguard author. Thank you, Jason!

After some experiments I've taken an opposite approach from the original - prepared wg0 interface is moved to vpn namespace and is configured to be the default route within this namespace.

We can check what interfaces our new vpn namespace has and how routing table looks like:

$ vpn up
...
$ vpn exec ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
10: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none
...
$ vpn exec ip route
default dev wg0 scope link
10.10.10.0/24 dev wg0 proto kernel scope link src 10.10.10.10

Pretty good, right?

One more minor issue was the lack of domain name resolution for programs running within vpn namespace. I was not expecting this so it took some time to understand the reason.

Citing ip-netns manual:

A network namespace is logically another copy of the network stack, with its own routes, firewall rules, and network devices.

This means nobody is listening on 127.0.0.1:53 within vpn namespace. We can easily solve this issue by starting another copy of unbound within vpn namespace (I've decided to use separate copy of unbound.conf for this named unbound.vpn.conf, YMMV).

Here is the script I'm using to manage VPN:

#!/usr/bin/env bash

set -ex

[[ $UID != 0 ]] && exec sudo -E "$(readlink -f "$0")" "$@"

NS="vpn"
WGIF="wg0"
WGCONF="/etc/wireguard/$WGIF.conf"
WGADDRIPV4="10.10.10.10/24"
UNBOUNDCONF="/etc/unbound/unbound.$NS.conf"
UNBOUNDPID="/run/unbound.$NS.pid"

up() {
    ip netns add $NS
    ip link add $WGIF type wireguard
    wg setconf $WGIF $WGCONF
    ip link set $WGIF netns $NS
    ip -n $NS addr add $WGADDRIPV4 dev $WGIF
    ip -n $NS link set lo up
    ip -n $NS link set $WGIF up
    ip -n $NS route add default dev $WGIF
    ip netns exec $NS unbound -c $UNBOUNDCONF
}

down() {
    UPID=`cat $UNBOUNDPID`
    kill $UPID || true
    ip -n $NS link set $WGIF down
    ip -n $NS link del $WGIF
    ip netns del $NS
}

status() {
    exec ip netns exec $NS wg
}

execi() {
    exec ip netns exec $NS sudo -E -u \#${SUDO_UID:-$(id -u)} -g \#${SUDO_GID:-$(id -g)} -- "$@"
}

command="$1"
shift

case "$command" in
    up) up "$@" ;;
    down) down "$@" ;;
    exec) execi "$@" ;;
    status) status "$@" ;;
    *) echo "Usage: $0 up|down|exec" >&2; exit 1 ;;
esac

Now I can start Firefox/IceCat within vpn namespace and be sure all traffic is routed through VPN:

$ vpn exec icecat

I'm using netctl to manage my network interfaces and the plan is to use netctl hooks to bring VPN interface up or down as needed.

wireguard looks quite good (one more time - thank you, Jason A. Donenfeld!) and I'm planning to use it for some time to decide if I can switch from SOCKS5 proxy to VPN.

Last modified: 2020-04-22 17:35:00 +00:00 UTC