Featured image of post wg-quick network namespace

wg-quick network namespace

Hacking wg-quick to create net namespace

TL;DR

1
2
3
PreUp = ip netns list | grep -qF -- %i && ip netns delete %i || true
PostUp = (ip netns list | grep -qF -- %i || ip netns add %i) && ip -n %i link set up lo && IPS="$(ip address save dev %i | base64)" && ip link set %i netns %i && echo "$IPS" | base64 -d | ip -n %i address restore dev %i && ip -n %i link set up %i && (ip -n %i route show default | grep -qF default || ip -n %i route add default dev %i) && echo 'IyEvdXNyL2Jpbi9lbnYgc2gKc2V0IC1lCgpuZXRucz0nJXMnCgpjYXQgPDxFT0YgfCB4YXJncyAtMCBzdWRvIC1FIG5zZW50ZXIgIi0tbmV0PS92YXIvcnVuL25ldG5zLyRuZXRucyIgdW5zaGFyZSAtLW1vdW50IHNoIC1jCmZvciBjb25mIGluICIkbmV0bnMiICJ0dW4uJG5ldG5zIjsgZG8KICBpZiBbIC1mICIvdmFyL3J1bi9yZXNvbHZjb25mL2ludGVyZmFjZXMvXCRjb25mIiBdOyB0aGVuCiAgICBtb3VudCAtLWJpbmQgIi92YXIvcnVuL3Jlc29sdmNvbmYvaW50ZXJmYWNlcy9cJGNvbmYiIC9ldGMvcmVzb2x2LmNvbmYKICBmaQpkb25lCmV4cG9ydCBEQlVTX1NFU1NJT05fQlVTX0FERFJFU1M9dW5peDpwYXRoPS9kZXYvbnVsbApzdWRvIC1FIC11ICcjJHtTVURPX1VJRDotJChpZCAtdSl9JyAtZyAnIyR7U1VET19HSUQ6LSQoaWQgLWcpfScgLS0gJEAKRU9GCg==' | base64 -d | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-exec && echo 'IyEvdXNyL2Jpbi9lbnYgc2gKc2V0IC1lCgppZiBbICIkKGlkIC11KSIgLW5lIDAgXTsgdGhlbgogIGVjaG8gImVycm9yOiBydW4gbWUgYXMgcm9vdCIgMT4mMgogIGV4aXQgMQpmaQoKbmV0bnM9JyVzJwppcCBuZXRucyBleGVjICIkbmV0bnMiIGlwIGxpbmsgc2V0ICIkbmV0bnMiIG5ldG5zIDEKaXAgbmV0bnMgZGVsZXRlICIkbmV0bnMiCnN5c3RlbWN0bCBzdG9wICJ3Zy1xdWlja0AkbmV0bnMuc2VydmljZSIK' | base64 -d | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-down && chmod 0755 /usr/local/bin/wg-%i-* && chown root:root /usr/local/bin/wg-%i-* && echo 'bmV0bnM9JyVzJwpjYXQgPDxFT0YKIyAg4pSQIOKUrG/ilKzilIDilJDilKzilIDilJDilIzilIDilJDilKwg4pSQ4pSs4pSA4pSQ4pSs4pSA4pSQ4pSs4pSA4pSQICDilIzilJDilJDilKzilIDilJDilIzilJDilJDilIzilJDilJDilJDilIDilJAKIyAg4pSC4pSC4pSC4pSC4pSC4pSs4pSY4pSc4pSAIOKUgiDilKzilIIg4pSC4pSC4pSA4pSk4pSC4pSs4pSY4pSCIOKUgiAg4pSC4pSC4pSC4pSc4pSAICDilIIg4pSC4pSC4pSC4pSU4pSA4pSQCiMgIOKUlOKUtOKUmOKUmOKUmOKUlOKUmOKUtOKUgOKUmOKUmOKUgOKUmOKUmOKUgOKUmOKUmCDilJjilJjilJTilJjilJjilIDilJggIOKUmOKUlOKUmOKUtOKUgOKUmCDilJgg4pSY4pSU4pSY4pSA4pSA4pSYClRoYW5rcyBmb3IgdXNpbmcgbmV0bnMgd2ctcXVpY2sgd3JhcHBlciEKCi91c3IvbG9jYWwvYmluL3dnLSRuZXRucy1leGVjIENPTU1BTkQgICAtIGV4ZWN1dGUgc2hlbGwgY29tbWFuZCBpbiAkbmV0bnMgbmV0d29yayBuYW1lc3BhY2UKL3Vzci9sb2NhbC9iaW4vd2ctJG5ldG5zLWRvd24gICAgICAgICAgIC0gdXNlIGl0IGluc3RlYWQgb2Ygd2ctcXVpY2sgZG93biAkbmV0bnMKRU9G' | base64 -d | xargs -0 -I{} printf {} %i | sh - 
PostDown = rm -f /usr/local/bin/wg-%i-* || true

Introduction

Wireguard is an awesome, simple VPN that is already included in the kernel. A very powerful and simple tool. It’s much easier to set up than OpenVPN. For its simple configuration, the wg-quick utility is used, which is part of wireguard tools. For better understanding, I’ll additionally clarify that from my experience, most solutions like WGDashboard and wireguard-ui generate client configs specifically for wg-quick.

Problem Statement

Move the wireguard interface to a separate network namespace so that applications can be run in it. Why do this? This eliminates the need to proxy traffic to blocked sites through Wireguard with some complex solutions. Need to access some restricted place - just launch another browser in the Wireguard namespace and that’s it! For example, I rewrote the Exec line in the Thunderbird shortcut, which has no equally functional alternative yet, and now it always starts through VPN and can peacefully collect mail from any SMTP. We need to use vanilla wg-quick without modifications so it works out of the box.

Solution Analysis

In the documentation, there are 4 ways for additional wg-quick interface configuration: PreUp, PostUp, PreDown, PostDown. So we need to write scripts that do everything needed. The ready solution is presented at the beginning of the article, and now let’s analyze it. I’ll transform this compressed code into human-readable form - it looks different but is identical in content. Note: the code contains %i, this is a wg-quick macro itself, wherever it encounters this macro, it substitutes the name of the interface being created.

PreUp

Everything is simple here.

1
2
3
4
5
# search for our namespace in the list of network namespaces
if ip netns list | grep -qF -- %i; then
  # if found - delete it
  ip netns delete %i
fi

PostUp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
if ! ip netns list | grep -qF -- %i; then
  # create network namespace if it doesn't exist yet (and it shouldn't)
  ip netns add %i
fi
# start the loopback interface there (the 127.0.0.1 one)
ip -n %i link set up lo
# save the list of network addresses that wg-quick assigned to the interface during creation
# and encode it in base64, because this is a binary stream in the ip utility format
IPS="$(ip address save dev %i | base64)" 
# move the interface to our special namespace
ip link set %i netns %i
# restore the addresses back, decoding them from the variable created earlier
echo "$IPS" | base64 -d | ip -n %i address restore dev %i 
# start the network interface in that namespace
ip -n %i link set up %i
# look for a route to 0.0.0.0/0 in our namespace's route list
if ! ip -n %i route show default | grep -qF default; then
  # add it if not found
  ip -n %i route add default dev %i
fi
# here was a script in base64, I decoded it directly for you in the text to make it clear what's there
# this script is decoded and the interface name is inserted into it using printf, which will always
# work with the script
cat <<EOT | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-exec
#!/usr/bin/env sh
set -e

# the interface name will land here in the variable
netns='%s'

# and I'll explain this mess separately
cat <<EOF | xargs -0 sudo -E nsenter "--net=/var/run/netns/$netns" unshare --mount sh -c
for conf in "$netns" "tun.$netns"; do
  if [ -f "/var/run/resolvconf/interfaces/\$conf" ]; then
    mount --bind "/var/run/resolvconf/interfaces/\$conf" /etc/resolv.conf
  fi
done
export DBUS_SESSION_BUS_ADDRESS=unix:path=/dev/null
sudo -E -u '#${SUDO_UID:-$(id -u)}' -g '#${SUDO_GID:-$(id -g)}' -- $@
EOF
EOT
# same kind of script, but we execute not a user command there, but wg-quick down
cat <<EOT | xargs -0 -I{} printf {} %i > /usr/local/bin/wg-%i-down
#!/usr/bin/env sh
set -e

# must be run as root
if [ "$(id -u)" -ne 0 ]; then
  echo "error: run me as root" 1>&2
  exit 1
fi

netns='%s'
# return the interface back to the root netns
ip netns exec "$netns" ip link set "$netns" netns 1
# delete our custom netns
ip netns delete "$netns"
# and stop via wg-quick, which will remove the interface itself
systemctl stop "wg-quick@$netns.service"
EOT
# change owner and permissions on files
chmod 0755 /usr/local/bin/wg-%i-*
chown root:root /usr/local/bin/wg-%i-*
# just output text to screen when bringing up the interface - user help,
# which tells about the two scripts created earlier. This text is decoded and directly
# thrown to sh, i.e., executed on the fly, and there's only one cat there
cat <<EOT | xargs -0 -I{} printf {} %i | sh - 
netns='%s'
cat <<EOF
#  ┐ ┬o┬─┐┬─┐┌─┐┬ ┐┬─┐┬─┐┬─┐  ┌┐┐┬─┐┌┐┐┌┐┐┐─┐
#  │││││┬┘├─ │ ┬│ ││─┤│┬┘│ │  │││├─  │ │││└─┐
#  └┴┘┘┘└┘┴─┘┘─┘┘─┘┘ ┘┘└┘┘─┘  ┘└┘┴─┘ ┘ ┘└┘──┘
Thanks for using netns wg-quick wrapper!

/usr/local/bin/wg-$netns-exec COMMAND   - execute shell command in $netns network namespace
/usr/local/bin/wg-$netns-down           - use it instead of wg-quick down $netns
EOF
EOT

And here’s the real madness. Let me explain the main points.

  1. When you move a network interface to another namespace, unfortunately, all IP addresses are lost from it. Therefore, we save them at the beginning and then restore them after the transfer.
  2. The loopback interface in the new namespace is disabled by default, so we enable it ourselves
  3. A route is added only to 0.0.0.0/0 because in principle our traffic has nowhere to go except to wireguard, so we send everything there regardless of what’s written in AllowedIPs on the interface. This makes the script simpler, works the same way, and creates no vulnerabilities.
  4. Two scripts are created: wg-%i-exec, wg-%i-down. The first is needed to run programs in our network namespace, and the second is needed to simply do wg-quick down, because just executing wg-quick down won’t stop the interface - you need to execute wg-quick inside our namespace

How wg-%i-exec works

There are several problems that the script solves:

  1. Inside the namespace there’s no resolv.conf, or there might not be. wg-quick always creates it for itself through resolvconf, even without all our tricks. That is, it’s created for the interface. This is needed for DNS name resolution.
  2. We need to make it so that even when running exec as a user, they don’t get root privileges as a bonus, but remain with their own privileges.

I’ll break down the script line by line into possibly even non-working shell code, but it will be more convenient to comment this way.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env sh
set -e

# the interface name will land here in the variable
netns='%s'

# and I'll explain this mess separately
cat <<EOF | xargs -0 sudo -E nsenter "--net=/var/run/netns/$netns" unshare --mount sh -c
for conf in "$netns" "tun.$netns"; do
  if [ -f "/var/run/resolvconf/interfaces/\$conf" ]; then
    mount --bind "/var/run/resolvconf/interfaces/\$conf" /etc/resolv.conf
  fi
done
export DBUS_SESSION_BUS_ADDRESS=unix:path=/dev/null
sudo -E -u '#${SUDO_UID:-$(id -u)}' -g '#${SUDO_GID:-$(id -g)}' -- $@
EOF


# execute on behalf of sudo, preserving the current user's env, this will still be with root privileges
sudo -E 
# nsenter will run us in this namespace
nsenter "--net=/var/run/netns/$NETNS"
# and then unmount everything that was mounted
unshare --mount 
# shell command that will execute the script
sh -c
'
# where we mount resolv.conf created by wg-quick, if such exists at all
# on Ubuntu and Fedora this is needed, on Manjaro - /etc/resolv.conf is already in place
for conf in "$netns" "tun.$netns"; do
  if [ -f "/var/run/resolvconf/interfaces/\$conf" ]; then
    mount --bind "/var/run/resolvconf/interfaces/\$conf" /etc/resolv.conf
  fi
done
# browsers won't start without this
export DBUS_SESSION_BUS_ADDRESS=unix:path=/dev/null
# and using sudo preserving the same current user's env
sudo -E 
# not on behalf of root, but already on behalf of the user who ran the exec script
-u '#${SUDO_UID:-$(id -u)}' 
-g '#${SUDO_GID:-$(id -g)}' 
# execute what they requested
-- $@
'

Conclusion

Just insert these 3 lines into the config and that’s it! No Ansible, dancing with tambourines, complex setup and maintenance.

All rights reserved
Built with Hugo
Theme Stack designed by Jimmy