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
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:
- Protocol can be set in configuration, to run a udp and tcp container simultaneously with the same image.
- 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:
- Add two remote directives in your ovpn profile file
- Enable udp traffic at 1194 in your firewall
- Expose post 1194 for udp traffic at your traefik docker container
- Define an openvpn entrypoint in traefik at port 1194 for udp traffic
- Spin up a second openvpn container with the udp protocol
- 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: