Wireguard and docker: providing VPN access to arbitrary containers

Date

Tags
#vpn #docker #wireguard

Your container might benefit from VPN access

Some containers just aren't meant to be connected directly to the internet. After all, you wouldn't want your ISP knowing which Linux distribution you download and share.

If like me you have your BitTorrent client installed as a container on a homeserver to make sure it's always connected but you don't want to route your other containers through a VPN, you'll probably want to use a VPN-in-a-container and route your BitTorrent client through it.

I already had a similar solution using OpenVPN but it was time for an upgrade. Oh yes, it's Wireguard time.

As VPN provider, I use Mullvad.

The solution

Our situation is this: our homeserver (could be a Linux machine, a Raspberry Pi…) runs two docker containers, one which is fine to be directly connected to the internet and one which would benefit from VPN access.

One could install the Wireguard client straight on the machine and route both containers through the VPN, but for various reasons, that's now what we want here.

Our solution will be to add another container which connects to the VPN and route our sensitive container through the VPN container.

With some experimenting, I got it working 90%. The only issue was that while the BitTorrent client was perfectly shielded by the VPN, I could no longer access the client myself. Not great.

After two days of trying stuff out and searching the internet, I found the working solution on a blog post from 2021 which sadly already no longer exists. But thanks to the Web Archive, its wisdom is lost no more.

PostUp and PreDown

The reason I didn't get it working myself is because I knew the problem lay in the PostUp/PreDown commands of the Wireguard configuration. And I don't know how to read or write those :/ Mullvad provides their own but they do not work in this situation.

I must therefore warn you that I sadly do not fully understand the solution. I probably could fiddle with it and get it working on a different system, but I don't understand it. I simply took my 90%-functional implementation, copy-pasted the PostUp/PreDown commands from the linked blog post and voilà, success!

Not proud of it, and I hope I'll gain understanding of these commands in the near future, but that's the situation.

The implementation

You must have Wireguard installed on your system but it doesn't need to be running any connection.

docker-compose.yml

version: '2.3'

services:
  wireguard:
    image: linuxserver/wireguard
    hostname: wireguard
    container_name: wireguard
    cap_add:
      - net_admin
      - sys_module
    ports:
      - 8112:8112
      - 58846:58846
    volumes:
      - /lib/modules:/lib/modules
      - ./data/wireguard:/config
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1

  deluge:
    image: linuxserver/deluge
    container_name: deluge
    network_mode: service:wireguard
    volumes:
      - ./data/deluge:/config
      - ./data/downloads:/downloads

In this docker-compose setup, we use the linuxserver/wireguard and linuxserver/deluge container images. Please have a look at their respective documentation for more information on their configuration.

A few interesting notes:

    cap_add:
      - net_admin
      - sys_module
    []
    volumes:
      - /lib/modules:/lib/modules

The linuxserver/wireguard image uses the system's Wireguard module and this configuration allows the container to access it.

    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1

This is important but sadly, I do not know what it does.

    ports:
      - 8112:8112
      - 58846:58846

This is the interesting part. We assign those ports to the wireguard container, but they are the ports exposed by the deluge container! Indeed, since the deluge container's network flows through the wireguard container, we can only access the deluge container through the wireguard container's network.

By the way, port 8112 is used for the Deluge WebUI and port 58846 is used by Deluge Thin Clients. Your BitTorrent client of choice will most likely use different ports!

    network_mode: service:wireguard

The trick that makes it all work: make sure that the deluge container connects to the internet through the wireguard container.

wg0.conf

This Wireguard configuration file is based on the one provided by Mullvad, but with the PostUp/PreDown commands found in the blog post mentioned earlier.

[Interface]
PrivateKey = <private key>
Address = <ip address>
DNS = <ip address>

PostUp = DROUTE=$(ip route | grep default | awk '{print $3}'); HOMENET=192.168.0.0/16; HOMENET2=10.0.0.0/8; HOMENET3=172.16.0.0/12; ip route add $HOMENET3 via $DROUTE;ip route add $HOMENET2 via $DROUTE; ip route add $HOMENET via $DROUTE;iptables -I OUTPUT -d $HOMENET -j ACCEPT;iptables -A OUTPUT -d $HOMENET2 -j ACCEPT; iptables -A OUTPUT -d $HOMENET3 -j ACCEPT;  iptables -A OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = HOMENET=192.168.0.0/16; HOMENET2=10.0.0.0/8; HOMENET3=172.16.0.0/12; ip route del $HOMENET3 via $DROUTE;ip route del $HOMENET2 via $DROUTE; ip route del $HOMENET via $DROUTE; iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT; iptables -D OUTPUT -d $HOMENET -j ACCEPT; iptables -D OUTPUT -d $HOMENET2 -j ACCEPT; iptables -D OUTPUT -d $HOMENET3 -j ACCEPT

[Peer]
PublicKey = <public key>
AllowedIPs = 0.0.0.0/0
Endpoint = <ip address with port>

Verification

We need to make sure we are in fact connected safely to Mullvad! To do this, let's use Mullvad's https://am.i.mullvad.net/connected API endpoint.

docker exec -t wireguard curl https://am.i.mullvad.net/connected
# You are connected to Mullvad (server XXYY-wireguard). Your IP address is XYZ.XYZ.XYZ.XYZ

Success! But wait, that's the wireguard container, this just checks whether our config is working. What about the deluge container?

docker exec -t deluge curl https://am.i.mullvad.net/connected
# You are connected to Mullvad (server XXYY-wireguard). Your IP address is XYZ.XYZ.XYZ.XYZ

Victory! Have fun sharing Linux distributions!