Route-based VPN on Linux with WireGuard
Vincent Bernat
In a previous article, I described an implementation of redundant site-to-site VPNs using IPsec (with strongSwan as an IKE daemon) and BGP (with BIRD) to achieve this: 🦑
The two strengths of such a setup are:
- Routing daemons distribute routes to be protected by the VPNs. They provide high availability and decrease the administrative burden when many subnets are present on each side.
- Encapsulation and decapsulation are executed in a different network namespace. This enables a clean separation between a private routing instance (where VPN users are) and a public routing instance (where VPN endpoints are).
As an alternative to IPsec, WireGuard is an extremely simple (less than
5,000 lines of code) yet fast and modern VPN that utilizes state-of-the-art and
opinionated cryptography (Curve25519, ChaCha20, Poly1305) and whose
protocol, based on Noise, has been formally verified. It is
currently available as an out-of-tree module for Linux but is currently being
integrated into mainline. Compared to IPsec, its major weakness is its
lack of interoperability.
Update (2020-07)
WireGuard is part of Linux since the 5.6 release.
It can easily replace strongSwan in our site-to-site setup. On Linux, it already acts as a route-based VPN. As a first step, for each VPN, we create a private key and extract the associated public key:
$ wg genkey oM3PZ1Htc7FnACoIZGhCyrfeR+Y8Yh34WzDaulNEjGs= $ echo oM3PZ1Htc7FnACoIZGhCyrfeR+Y8Yh34WzDaulNEjGs= | wg pubkey hV1StKWfcC6Yx21xhFvoiXnWONjGHN1dFeibN737Wnc=
Then, for each remote VPN, we create a short configuration file:1
[Interface] PrivateKey = oM3PZ1Htc7FnACoIZGhCyrfeR+Y8Yh34WzDaulNEjGs= ListenPort = 5803 [Peer] PublicKey = Jixsag44W8CFkKCIvlLSZF86/Q/4BovkpqdB9Vps5Sk= EndPoint = [2001:db8:2::1]:5801 AllowedIPs = 0.0.0.0/0,::/0
A new ListenPort
value should be used for each remote VPN. WireGuard can
multiplex several peers over the same UDP port but this is not applicable here,
as the routing is dynamic. The AllowedIPs
directive tells to accept and send
any traffic.
The next step is to create and configure the tunnel interface for each remote VPN:
$ ip link add dev wg3 type wireguard $ wg setconf wg3 wg3.conf
WireGuard initiates a handshake to establish symmetric keys:
$ wg show wg3 interface: wg3 public key: hV1StKWfcC6Yx21xhFvoiXnWONjGHN1dFeibN737Wnc= private key: (hidden) listening port: 5803 peer: Jixsag44W8CFkKCIvlLSZF86/Q/4BovkpqdB9Vps5Sk= endpoint: [2001:db8:2::1]:5801 allowed ips: 0.0.0.0/0, ::/0 latest handshake: 55 seconds ago transfer: 49.84 KiB received, 49.89 KiB sent
Like VTI interfaces, WireGuard tunnel interfaces are namespace-aware: once
created, they can be moved into another network namespace where clear traffic is
encapsulated and decapsulated. Encrypted traffic is routed in its original
namespace. Let’s move each interface into the private
namespace and assign it
a point-to-point IP address:
$ ip link set netns private dev wg3 $ ip -n private addr add 2001:db8:ff::/127 dev wg3 $ ip -n private link set wg3 up
The remote end uses 2001:db8:ff::1/127
. Once everything is setup, from one
VPN, we should be able to ping each remote host:
$ ip netns exec private fping 2001:db8:ff::{1,3,5,7} 2001:db8:ff::1 is alive 2001:db8:ff::3 is alive 2001:db8:ff::5 is alive 2001:db8:ff::7 is alive
BIRD configuration is unmodified compared to our previous setup and the BGP sessions should establish quickly:
$ birdc6 -s /run/bird6.private.ctl show proto | grep IBGP_ IBGP_V2_1 BGP master up 20:16:31 Established IBGP_V2_2 BGP master up 20:16:31 Established IBGP_V3_1 BGP master up 20:16:31 Established IBGP_V3_2 BGP master up 20:16:29 Established
Remote routes are learnt over the different tunnel interfaces:
$ ip -6 -n private route show proto bird 2001:db8:a1::/64 via fe80::5254:33ff:fe00:13 dev eth2 metric 1024 pref medium 2001:db8:a2::/64 metric 1024 nexthop via 2001:db8:ff::1 dev wg3 weight 1 nexthop via 2001:db8:ff::3 dev wg4 weight 1 2001:db8:a3::/64 metric 1024 nexthop via 2001:db8:ff::5 dev wg5 weight 1 nexthop via 2001:db8:ff::7 dev wg6 weight 1
From one site, you can ping a host on the other site through the VPNs:
$ ping -c 2 2001:db8:a3::1 PING 2001:db8:a3::1(2001:db8:a3::1) 56 data bytes 64 bytes from 2001:db8:a3::1: icmp_seq=1 ttl=62 time=1.54 ms 64 bytes from 2001:db8:a3::1: icmp_seq=2 ttl=62 time=1.67 ms --- 2001:db8:a3::1 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 1.542/1.607/1.672/0.065 ms
As with the strongSwan setup, you can easily snoop unencrypted traffic with
tcpdump
:
$ ip netns exec private tcpdump -c3 -pni wg5 icmp6 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on wg5, link-type RAW (Raw IP), capture size 262144 bytes 08:34:34 IP6 2001:db8:a3::1 > 2001:db8:a1::1: ICMP6, echo reply, seq 40 08:34:35 IP6 2001:db8:a3::1 > 2001:db8:a1::1: ICMP6, echo reply, seq 41 08:34:36 IP6 2001:db8:a3::1 > 2001:db8:a1::1: ICMP6, echo reply, seq 42 3 packets captured 3 packets received by filter 0 packets dropped by kernel
You can find all the configuration files for this example on GitHub.
Update (2018-11)
It is also possible to transport IPv4 on top of IPv6 WireGuard tunnels. The lab has been updated to support such a scenario.
-
Compared to IPsec, the cryptography is not configurable and you have to use the strong provided defaults. ↩︎