Media Center Series Part 2: Wireguard with Docker on Raspberry Pi 4

Originally my media center was a docker containers with OpenVPN to handle VPN traffic. Since they I have updated my setup to use Wireguard instead and I’m documenting the resources I found useful in case anyone wants to do the same. Two main reasons drove me to move from OpenVPN to Wireguard:

The first reason is that my old provider Private Internet Access was bought out by Kape. There haven’t been any changes to PIA yet but the new owner is a bit worrying considering their track record. So I have changed to Mullvad as my new provider and they have very good Wireguard support.

The second reason is that Wireguard offers some compelling improvements over OpenVPN:

  • OpenVPN is very resource demanding on the RPi. The Model 4 Raspberry Pi has a new I/O architecture which can properly use Gigabit LAN. Unfortunately the CPU overhead for OpenVPN was high enough that it limited throughput. My Pi would regularly have a core pinned near 100% utilization from OpenVpn. In comparison Wireguard’s performance is considerably lighter leading to better throughput.
  • OpenVPN runs in user space where as Wireguard will be included in the kernel eventually. For now this complicates things a bit since it has to be installed separately but once the changes land it will make setup much easier.

Update 1/29/2020: Phoronix reports that Wireguard was just merged into the 5.6 kernel! However it will be a while before 5.6 is the stable kernel for many distrobutions so the installation instructions below will still be needed for a while.

Update 7/24/2020: While Debian 11 Bullseye isn’t releasing until 2021 its features are already being backported to Buster. Wireguard can now be pulled in from the buster-backports repo rather than having to build it!

In terms of researching how to set this up three articles were particularly useful:

Installing Wireguard on Debian

First I wanted general information about installing the Wireguard kernel modules and user space tools.

The Debian wiki generally outlines the process but leaves some details to the reader in terms of file permissions. Linode’s covered those in better details. I only care about the client configuration details since I am not running a server. Additionally instead of writing my own configuration file I used the Mullvad Wireguard configuration tools to generate one.

I also needed to install the following to follow the setup:

sudo apt install openresolv resolvconf

Using Wireguard with Docker

There’s a blog post that describes two solutions for using Wireguard with docker. The first involves creating a docker container that runs the Wireguard client. I went with the second approach instead which covers setting up a Wireguard interface on the host and using in a Docker network.

Raspberry Pi Specific Changes for Installation

Finally I want to do this on a Rapsberry Pi 4 which requires additional steps to install raspberrypi-kernel-headers and get Wireguard to run.

Putting It All Together With Ansible

Handling the Mullvad config file

The playbook assumes there is a file called “mullvad.conf” in the same directory as playbook which is the configuration file downloaded from the Mullvad configuration generation tool and the vars section uses the ini plugin for lookups to read the server Address and DNS information from config file:

---

  - name: Wireguard
    connection: ssh
    become_user: root
    become: yes
    hosts: rpi
    vars:
      address: "{{ lookup('ini', 'Address section=Interface file=mullvad.conf').split(',')[0] }}"
      dns: "{{ lookup('ini', 'DNS section=Interface file=mullvad.conf') }}"
      subnet_ip: "10.193.0.0/16"
    tasks:
      - name: Print Mullavd Address
        debug:
          msg: "Address is: {{ address }}"
      - name: Print Mullvad DNS
        debug:
          msg: "DNS is: {{ dns }}"

Note the we have to split the address on “,” since the INI entry contains both the ipv4 and ipv6 address and we only want the first half

Installing Wireguard

Update: Wireguard has been backported from Bullseye to buster and can be installed on a Pi:

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 04EE7237B7D453EC 648ACFD622F3D138

echo 'deb http://httpredir.debian.org/debian buster-backports main contrib non-free' | sudo tee -a /etc/apt/sources.list.d/debian-backports.list

sudo apt update
sudo apt install wireguard

We can achieve this in Ansible with the following:

      - name: Add first key
        apt_key:
          keyserver: keyserver.ubuntu.com
          id: 04EE7237B7D453EC
      - name: Add second key
        apt_key:
          keyserver: keyserver.ubuntu.com
          id: 648ACFD622F3D138
      - name: enable unstable
        lineinfile:
          path: /etc/apt/sources.list.d/debian-backports.list
          create: yes
          line: deb http://httpredir.debian.org/debian buster-backports main contrib non-free
      - name: Install deps
        apt:
          pkg:
            - wireguard
            - wireguard-tools
            - resolvconf
          update_cache: yes

Original Version: The installation process is basically a combination of the two posts on installing, but replacing command with ansible tasks to clean it up:

      - name: enable unstable
        lineinfile:
          path: /etc/apt/sources.list.d/unstable-wireguard.list
          create: yes
          line: deb http://deb.debian.org/debian/ unstable main
      - name: enable wireguard repo
        blockinfile:
          path: /etc/apt/preferences.d/limit-unstable
          create: yes
          block: |
            Package: *
            Pin: release a=unstable
            Pin-Priority: 150
      - name: Add first key
        apt_key:
          keyserver: keyserver.ubuntu.com
          id: 8B48AD6246925553
      - name: Add second key
        apt_key:
          keyserver: keyserver.ubuntu.com
          id: 7638D0442B90D010
      - name: Add third key
        apt_key:
          keyserver: keyserver.ubuntu.com
          id: 04EE7237B7D453EC
      - name: Install deps
        apt:
          pkg:
            - raspberrypi-kernel-headers
            - dirmngr
            - wireguard-dkms
            - wireguard-tools
            - resolvconf
          update_cache: yes

Configuring the Wireguard Interface

Finally we want to set up the wireguard interface that docker will use. We do this by first copying the Mullvad config over to the machine. Then as noted by the Wireguard on Docker article we remove the “Address” and “DNS” options from the config file since we have to manually configure the interface instead of using the wg-quick command. Then we are free to setup up the interface and configure it:

      - name: Copy config to server
        synchronize:
          src: mullvad.conf
          dest: /etc/wireguard/wg1.conf
      - name: remove Address entry
        ini_file:
          path: /etc/wireguard/wg1.conf
          section: Interface
          option: Address
          state: absent
      - name: remove DNS entry
        ini_file:
          path: /etc/wireguard/wg1.conf
          section: Interface
          option: DNS
          state: absent
      - name: add wireguard interface
        command: "ip link add dev wg1 type wireguard"
      - name: set wireguard config
        command: "wg setconf wg1 /etc/wireguard/wg1.conf"
      - name: set wireguard address
        command: "ip address add {{ address }} dev wg1"
      - name: put interface up
        command: "ip link set up dev wg1"
      - name: set nameserver
        command: "printf 'nameserver %s\n' '{{ dns }}' | resolvconf -a tun.wg1 -m 0 -x"
      - name: handle reverse path filtering
        command: "sysctl -w net.ipv4.conf.all.rp_filter=2"

Creating the Docker Network

Finally all that is left is to create the docker network and setup the firewall to route it through wireguard:

- name: create docker network
        docker_network:
          name: docker-vpn0
          ipam_config:
            - subnet: "{{ subnet_ip }}"
      - name: add firewall rule
        command: "ip rule add from {{ subnet_ip }} table 200"
      - name: add firewall route
        command: "ip route add default via {{ address.split('/')[0]}} table 200"

Updated:

Comments