Route-based VPN on Linux with WireGuard

Vincent Bernat

In a pre­vi­ous ar­ti­cle, I de­scribed an im­ple­men­ta­tion of re­dun­dant site-to-site VPNs us­ing IPsec (with strongSwan as an IKE dae­mon) and BGP (with BIRD) to achieve this: 🦑

Redundant VPNs between 3 sites
Three sites using redundant IPsec VPNs to protect some subnets.

The two strengths of such a setup are:

  1. Rout­ing dae­mons dis­trib­ute routes to be pro­tected by the VPNs. They pro­vide high avail­abil­ity and de­crease the ad­min­is­tra­tive bur­den when many sub­nets are present on each side.
  2. En­cap­su­la­tion and de­cap­su­la­tion are ex­e­cuted in a dif­fer­ent net­work names­pace. This en­ables a clean sep­a­ra­tion be­tween a pri­vate rout­ing in­stance (where VPN users are) and a pub­lic rout­ing in­stance (where VPN end­points are).

As an al­ter­na­tive to IPsec, Wire­Guard is an ex­tremely sim­ple (less than 5,000 lines of code) yet fast and mod­ern VPN that uti­lizes state-of-the-art and opin­ion­ated cryp­tog­ra­phy (Curve25519, ChaCha20, Poly1305) and whose pro­to­col, based on Noise, has been for­mally ver­i­fied. It is cur­rently avail­able as an out-of-tree mod­ule for Linux but is cur­rently be­ing in­te­grated into main­line. Com­pared to IPsec, its ma­jor weak­ness is its lack of in­ter­op­er­ability.

Up­date (2020-07)

Wire­Guard is part of Linux since the 5.6 release.

It can eas­ily re­place strongSwan in our site-to-site setup. On Linux, it al­ready acts as a route-based VPN. As a first step, for each VPN, we cre­ate a pri­vate key and ex­tract the as­so­ci­ated public key:

$ wg genkey
oM3PZ1Htc7FnACoIZGhCyrfeR+Y8Yh34WzDaulNEjGs=
$ echo oM3PZ1Htc7FnACoIZGhCyrfeR+Y8Yh34WzDaulNEjGs= | wg pubkey
hV1StKWfcC6Yx21xhFvoiXnWONjGHN1dFeibN737Wnc=

Then, for each re­mote VPN, we cre­ate a short con­fig­u­ra­tion 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 re­mote VPN. Wire­Guard can mul­ti­plex sev­eral peers over the same UDP port but this is not ap­pli­ca­ble here, as the rout­ing is dy­namic. The AllowedIPs di­rec­tive tells Wire­Guard to ac­cept and send any traffic.

The next step is to cre­ate and con­fig­ure the tun­nel in­ter­face for each re­mote VPN:

$ ip link add dev wg3 type wireguard
$ wg setconf wg3 wg3.conf

Wire­Guard ini­ti­ates a hand­shake to es­tab­lish sym­met­ric 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 in­ter­faces, Wire­Guard tun­nel in­ter­faces are names­pace-aware: once cre­ated, they can be moved into an­other net­work names­pace where clear traf­fic is en­cap­su­lated and de­cap­su­lated. En­crypted traf­fic is routed in its orig­i­nal names­pace. Let’s move each in­ter­face into the private names­pace and as­sign 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 re­mote end uses 2001:​db8:​ff::​1/127. Once ev­ery­thing is set up, from one VPN, we should be able to ping each re­mote 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 con­fig­u­ra­tion is un­mod­i­fied com­pared to our pre­vi­ous setup and the BGP ses­sions should es­tab­lish 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

Re­mote routes are learned over the dif­fer­ent tun­nel in­terfaces:

$ 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 eas­ily snoop un­en­crypted traf­fic 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 con­fig­u­ra­tion files for this ex­am­ple on GitHub.

Up­date (2018-11)

It is also pos­si­ble to trans­port IPv4 on top of IPv6 Wire­Guard tun­nels. The lab has been up­dated to sup­port such a scenario.