OpenVPN with Traefik 2.2 using UDP

A neat trick from OpenVPN is you can have a client configuration with two remote servers. You can connect by default with udp at port 1194 but if firewalls block either udp traffic or port 1194, your client will automatically have a failover using tcp at port 443. In this post I will show you how, using the new udp capabilities in Traefik 2.2, released just last month.

In OpenVPN it is possible to use a failover configuration:

remote vpn.myserver.com 1194 udp
remote vpn.myserver.com 443 tcp

As said in the introduction, this is quite useful in situations you want preferably the highest throughput (with udp traffic) but you need a fallback if udp is blocked. You might have used two configurations, so when you’re udp connection failed, you started a new client with another configuration. With two remotes in the client config, you have the failover built in.

This setup requires Traefik version 2.2, released March 25. With the 2.2 version it introduced udp traffic and I have been experimenting with this dual setup since. It can be quite tricky to set this up correctly so I’d like to share my findings with you.

I use Ansible to provision my servers so all examples are yaml for Ansible configuration. In most cases they are easily transformed into any other method you prefer.

If you just want the summary, jump to the TL;DR

Concepts

Image courtesy of Traefik.io

In Traefik we will use the above concept with two instances of a docker vpn container. The containers are ephemeral and contain no configuration, certificates or whatsoever. One container is the udp instance while the other will take care of the tcp traffic. They share a single volume which holds the persistent data.

I use the image from kylemanna/openvpn but I think any image for your container as long as they provide two features:

  1. Protocol can be set in configuration, to run a udp and tcp container simultaneously with the same image.
  2. The persistent data is stored in a separate volume so both containers access this data.

Allow udp traffic through the firewall

On my Ubuntu image from DigitalOcean I use UFW to control the firewall settings:

---
- name: Setup UFW
  ufw: state=enabled default=reject

- name: UFW allows OpenSSH and apply rate-limiting
  ufw: rule=limit app=OpenSSH

- name: UFW allows HTTP and HTTPS traffic
  ufw: rule=allow port="{{ item.port }}" proto="{{ item.proto }}"
  with_items:
    - { port: 80,  proto: tcp }
    - { port: 443, proto: tcp }

- name: UFW allows UDP traffic
  ufw: rule=allow port="{{ item.port }}" proto="{{ item.proto }}"
  with_items:
    - { port: 1194,  proto: udp }

You probably already had 80 and 443 opened for Traefik use, so it is important you add port 1194 and specify its protocol as udp. You’ll get something like this:

$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    LIMIT       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
1194/udp                   ALLOW       Anywhere
OpenSSH (v6)               LIMIT       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)
1194/udp (v6)              ALLOW       Anywhere (v6)

Run the traefik container

To run a container with ansible, you can use the docker_container module. The total task to get it running looks like this:

---
- name: Run traefik container
  docker_container:
    name: traefik
    image: traefik:2.2
    restart_policy: unless-stopped
    networks_cli_compatible: yes
    networks:
      - name: "{{ traefik_docker_network }}"
      - name: bridge
    ports:
      - "80:80"
      - "443:443"
      - "1194:1194/udp"
    volumes:
      - "{{ traefik_install_dir }}/traefik.toml:/etc/traefik/traefik.toml"
      - "{{ traefik_install_dir }}/acme.json:/acme.json"
      - "{{ traefik_install_dir }}/traefik.log:/traefik.log"
      - /var/run/docker.sock:/var/run/docker.sock
    labels:
      traefik.http.routers.api.entrypoints: "websecure"
      traefik.http.routers.api.rule: "Host(`{{ ansible_fqdn }}`)"
      traefik.http.routers.api.service: "api@internal"
      traefik.http.routers.api.middlewares: "api-auth"
      traefik.http.routers.api.tls: "true"
      traefik.http.routers.api.tls.certresolver: "le"
      traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: "https"

This task requires some setup of directories, files and network first, which you can find in this file.

docker_container parameters

There are a few things going on in above task, but the basic concepts are as follows.

name: traefik
image: traefik:2.2

This runs the container named traefik (so you can easily identify it later) and uses the traefik image version 2.2.

networks_cli_compatible: yes
networks:
  - name: "{{ traefik_docker_network }}"
  - name: bridge

Here traefik is attached to the bridge network (to expose the ports to the host) and a docker internal bridge network to attach all containers to, to allow traffic between traefik and the container instances. networks_cli_compatible is a flag to make ansible compatible with the way docker works and is just to make sure you’re already on par with how ansible 2.12 will work.

ports:
  - "80:80"
  - "443:443"
  - "1194:1194/udp"

The first two ports are pretty obvious, as traefik is usually placed before any webserver to allow http (port 80) and https (port 443) traffic. The last rule is important as an addition to get the openvpn udp container work. Please note the /udp suffix to mark this port for traffic to allow udp.

volumes:
  - "{{ traefik_install_dir }}/traefik.toml:/etc/traefik/traefik.toml"
  - "{{ traefik_install_dir }}/acme.json:/acme.json"
  - "{{ traefik_install_dir }}/traefik.log:/traefik.log"
  - /var/run/docker.sock:/var/run/docker.sock

The volumes map configuration files (the traefik.toml and the acme.json) as well as the log file to the internal counterpart. Exposing the log file to the host makes it easy to ssh to the server and inspect the log file without accessing the container.

For for the config file traefik.toml you have to have at least 3 entrypoints defined. Again, please be careful to make sure traefik knows port 1194 is used for udp:

[entryPoints]
  [entryPoints.web]
    address = ":80"

  [entryPoints.websecure]
    address = ":443"

  [entryPoints.openvpn]
    address = ":1194/udp"

The last parameters for docker_container are the labels:

labels:
  traefik.http.routers.api.entrypoints: "websecure"
  traefik.http.routers.api.rule: "Host(`{{ ansible_fqdn }}`)"
  traefik.http.routers.api.service: "api@internal"
  traefik.http.routers.api.middlewares: "api-auth"
  traefik.http.routers.api.tls: "true"
  traefik.http.routers.api.tls.certresolver: "le"
  traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: "https"

The labels expose the traefik dashboard. This is not really required to get OpenVPN running, but it just makes it easy to inspect if the service is running and if there are any errors. Be aware no authentication is applied, so please be warned about any security risks here.

Time to spin up OpenVPN

The complete ansible code to start OpenVPN with all prerequisites is largely based on Dave Burke’s repository. My version can be found in this file.

- name: Ensure openvpn TCP container is started
  docker_container:
    name: "ovpn-tcp"
    image: "{{ openvpn_docker_image }}"
    restart_policy: always
    capabilities:
      - NET_ADMIN
    networks_cli_compatible: yes
    networks:
      - name: "{{ traefik_docker_network }}"
    dns_servers:
      # DNS replies must come back from the docker gateway or
      # they are rejected as spoofing attempts.
      - "{{ ansible_docker0.ipv4.address }}"
    volumes:
      - "{{ openvpn_data_volume }}:/etc/openvpn"
    command: ovpn_run --proto tcp
    labels:
      traefik.tcp.routers.openvpn.rule: "HostSNI(`*`)"
      traefik.tcp.routers.openvpn.entrypoints: "websecure"
      traefik.tcp.routers.openvpn.service: "openvpn"
      traefik.tcp.services.openvpn.loadBalancer.server.port: "1194"
  
- name: Ensure openvpn UDP container is started
  docker_container:
    name: "ovpn-udp"
    image: "{{ openvpn_docker_image }}"
    restart_policy: always
    capabilities:
      - NET_ADMIN
    networks_cli_compatible: yes
    networks:
      - name: "{{ traefik_docker_network }}"
    dns_servers:
      # DNS replies must come back from the docker gateway or
      # they are rejected as spoofing attempts.
      - "{{ ansible_docker0.ipv4.address }}"
    volumes:
      - "{{ openvpn_data_volume }}:/etc/openvpn"
    command: ovpn_run --proto udp
    labels:
      traefik.udp.routers.openvpn.entrypoints: "openvpn"
      traefik.udp.routers.openvpn.service: "openvpn"
      traefik.udp.services.openvpn.loadBalancer.server.port: "1194"

Dave uses a loop with_items to start both containers on udp & tcp protocol. However, with the introduction of traefik 2.2 the container labels differ quite a lot and I just wanted to make it explicit we have a udp and tcp container running.

Final result

The result looks great when you finally hit this point:

A complete green dashboard with a tcp and udp service running!

Summary (TL;DR)

I hope above will get you through the loops of starting an OpenVPN container behind traefik with both a udp and tcp remote server. If you already had OpenVPN and traefik toghether, the TL;DR of this complete post is:

  1. Add two remote directives in your ovpn profile file
  2. Enable udp traffic at 1194 in your firewall
  3. Expose post 1194 for udp traffic at your traefik docker container
  4. Define an openvpn entrypoint in traefik at port 1194 for udp traffic
  5. Spin up a second openvpn container with the udp protocol
  6. Use the proper docker labels to attach the openvpn container to the udp entrypoint

This should get you going! If you miss anything, please leave me a message.

Docker compose file

Since there have been some requests for a docker-compose file instead of the Ansible configuration, here’s my attempt to it: