High Availability WireGuard Site to Site

WireGuard makes it easy to set up a private connection between two networks, whether they’re simply different subnets in the same physical office or data center, or far-flung sites separated by continents or oceans. This article will show you how to set up multiple WireGuard routers at each connected site for redundancy — so that if one router goes down (or the link it’s using goes down) traffic will automatically failover to a router that’s still up and operational.

In the example we’ll cover, we’ll have two sites: Site A and Site B. We’ll set up a vanilla Linux box with WireGuard running on it as Router 1 in Site A, and connect it with WireGuard to a similar Router 3 in Site B. Then we’ll set up a parallel connection between a second WireGuard router in Site A, Router 2, and a second WireGuard router in Site B, Router 4:

High Availability WireGuard

We’ll use OSPF (Open Shorted Path First, an Internal Gateway Protocol, or IGP) to track the state of the links between the two parallel routes (Router 1 ←→ Router 3, and Router 2 ←→ Router 4), which we can propagate to the sites via OSPF or via iBGP (Interior Border Gateway Protocol). That way, if the link used by one route goes down, the site’s own LAN (Local Area Network) routers will detect it and replace it with the other route within a few seconds.

These are the steps we’ll cover:

Set Up WireGuard Routers

We’ll set up each of the four WireGuard routers all the same way: install Linux on it (any modern Linux distro will be fine and work basically the same), then:

This will build two parallel WireGuard VPNs (Virtual Private Networks): one between Router 1 and Router 3, and the other between Router 2 and Router 4:

WireGuard Network

Router 1 will have a private WireGuard IP address of 10.99.13.1, but a public IP address of 198.51.100.10 (and an IP address of 192.168.1.101 within Site A’s LAN). Router 3 will have a WireGuard address of 10.99.13.3, but a public IP address of 203.0.113.33 (and an IP address of 192.168.200.3 within Site B’s LAN). Router 1 will connect to Router 3’s public IP address over UDP port 51820, and vice versa.

Similarly, Router 2 will have a WireGuard address of 10.99.24.2, but a public IP address of 198.51.100.222 (with an IP address of 192.168.1.202 within Site A’s LAN). And Router 4 will have a WireGuard address of 10.99.24.4, with a public IP address of 203.0.113.244 (and an IP address of 192.168.200.224 within Site B’s LAN). Router 2 will also connect to Router 4’s public IP address over UDP port 51820, and vice versa.

Set Up WireGuard

On each router, install WireGuard. On Debian or Ubuntu, it’s as simple as sudo apt install wireguard. For other distros, refer to the WireGuard Install page.

Then generate a WireGuard public-key pair for each router. You can do it with the following one-liner:

$ wg genkey | tee /dev/stderr | wg pubkey
0F11111111111111111111111111111111111111110=
hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=

The first line you see will be the private key (which should be much more random than the above contrived example); the second line will be the public key. The private key goes in the router’s own configuration file; the public key goes in the configuration file of the corresponding router to which it connects.

Next, create a WireGuard configuration file at /etc/wireguard/wg0.conf. On Router 1, put the following settings in it:

# /etc/wireguard/wg0.conf on Router 1
# local settings for Router 1
[Interface]
PrivateKey = 0F11111111111111111111111111111111111111110=
Address = 10.99.13.1/24
ListenPort = 51820
Table = off

# remote settings for Router 3
[Peer]
PublicKey = IZ2rER/G68c1LCHeXIkpHqKYT7zB5bLkNULSSJ5HI1U=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.33:51820

Replace the dummy PrivateKey value above with the actual private key you generated for Router 1, and replace the PublicKey value with the actual public key you generated for Router 3. Replace the Endpoint value with the actual public IP address and UDP port at which Router 1 can connect to Router 3.

You can use a different UDP port than 51820 for any or all of the routers — just make sure that the port you specify for the ListenPort in Router 1’s config file matches the Endpoint port you specify in Router 3`s config file, and vice versa (or that you translate the destination port from the port specified in the Endpoint setting in one config file to the ListenPort specified in the other at some hop between Router 1 and Router 3).

You can also use a different Address value. This address will just be used within the private network between Router 1 and Router 3, so you can set it to anything convenient — just make sure that the addresses you choose for Router 1 and Router 3 use the same subnet (in this example, they’re both in 10.99.13.0/24), and that their subnet doesn’t contain any addresses from Site A or Site B that they may have to route.

Do not change the Table = off setting, which directs wg-quick not to add any routes based on AllowedIPs when it sets up the interface. Also do not change the AllowedIPs = 0.0.0.0/0, ::/0 setting, which directs WireGuard to pass all packets sent through the interface to Router 3, irrespective of the packet’s destination address.

On Router 3, put the following settings in /etc/wireguard/wg0.conf:

# /etc/wireguard/wg0.conf on Router 3
# local settings for Router 3
[Interface]
PrivateKey = 2H33333333333333333333333333333333333333330=
Address = 10.99.13.3/24
ListenPort = 51820
Table = off

# remote settings for Router 1
[Peer]
PublicKey = hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 198.51.100.10:51820

Set up /etc/wireguard/wg0.conf similarly on Router 2 and Router 4, with corresponding settings that allow them to connect to each other. On Router 2, like this:

# /etc/wireguard/wg0.conf on Router 2
# local settings for Router 2
[Interface]
PrivateKey = 2G22222222222222222222222222222222222222220=
Address = 10.99.24.2/24
ListenPort = 51820
Table = off

# remote settings for Router 4
[Peer]
PublicKey = 4QbAocAtphAuEXj5txnxL7BIYAmAmy9+Qyql0i34hiI=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.244:51820

And on Router 4, like this:

# /etc/wireguard/wg0.conf on Router 4
# local settings for Router 4
[Interface]
PrivateKey = 4I44444444444444444444444444444444444444404=
Address = 10.99.24.4/24
ListenPort = 51820
Table = off

# remote settings for Router 2
[Peer]
PublicKey = 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 198.51.100.222:51820

Set up WireGuard to start on system boot, and start it running now, with the following systemd command:

$ sudo systemctl enable --now wg-quick@wg0.service

Once WireGuard is up and running on each router, you should be able to ping Router 3 from Router 1 (and vice versa):

$ ping -nc1 10.99.13.3
PING 10.99.13.3 (10.99.13.3) 56(84) bytes of data.
64 bytes from 10.99.13.3: icmp_seq=1 ttl=64 time=20.3 ms

--- 10.99.13.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 20.325/20.325/20.325/0.000 ms

You should also be able to ping Router 4 from Router 2 (and vice versa). See the WireGuard Site to Site Configuration guide for more details about the above WireGurad settings, and for troubleshooting tips.

Configure Packet Forwarding

Next, to enabling packet forwarding, create a /etc/sysctl.d/local.conf file, and set it to this content:

net.ipv4.conf.all.forwarding=1

Then run the following command to apply it (along with all other sysctl conf files):

sudo sysctl --system

Install BIRD

Finally, install the BIRD 2.x routing daemon. On most Linux distros, it’s available via the bird package; on Debian and Ubuntu, it’s the bird2 package (install it with sudo apt install bird2).

BIRD 1.x will also work, but uses slightly different configuration settings — it doesn’t support both IPv4 and IPv6 at the same time (you have to configure it to use one or the other), so it doesn’t support the ip4 {} and ipv6 {} channel options that the BIRD 2.x configuration requires. (Other routing daemons, like FRR or Quagga, will also work fine — just with a different configuration syntax.)

Configure OSPF Between WireGuard Routers

At this point, we have WireGuard up and running on all four WireGuard routers, with Router 1 connected to Router 3, and Router 2 connected to Router 4. But because we put Table = off in their WireGuard configuration, none are set up yet to actually route packets beyond their own WireGuard subnet (10.99.13.0/24 for the connection between Router 1 and Router 3, and 10.99.24.0/24 for the connection between Router 2 and Router 4).

So we’ll configure the BIRD routing daemon with OSPF on each router in order to:

  1. Check whether the link between each router and its pair is up

  2. Exchange routes between the paired routers

  3. Add the exchanged routes to the router’s main routing table

We’ll assign Router 1 a router ID of 10.99.13.1, Router 3 a router ID of 10.99.13.3, and use the 10.99.13.0 OSPF area for their connection to each other; and we’ll assign Router 2 a router ID of 10.99.24.2, Router 4 a router ID of 10.99.24.4, and use the 10.99.24.0 OSPF area for their connection to each other:

OSPF Between WireGuard Routers
Note

Even though router and area IDs look like IP addresses, they’re actually just arbitrary 32-bit numbers that are by convention written in dotted-quad notation. Although it’s often a good idea to make them match the IP addresses of the routers and subnets they represent, it’s not required — for example, you could assign 1.1.1.1 as the ID for Router 1 and 3.0.0.0 as the ID for Router 3, and 0.0.0.123 to the area they share.

On Router 1, edit its /etc/bird/bird.conf file to make it look like the following:

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.13.0/24;
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}
Tip

White-space is not significant in BIRD configuration — for example, the above multi-line protocol device block could be written on a single line, like protocol device { } (or protocol device{}). Top-level protocol blocks do not end with a trailing semicolon (;), but all other blocks must. When empty, the squigly-brackets ({ }) of non-protocol blocks can be omitted — for example, they’re omitted from the interface line above, which could also be written interface "wg0" { };.

The router id line sets the global ID for this router (used by both OSPF and BGP). The protocol device block configures BIRD to periodically sync its interface list from the OS, so as to detect when interfaces are added, removed, or updated.

The protocol kernel block syncs the OS’s main IPv4 routing table with BIRD’s internal master4 routing table. By default all routes are imported from the OS’s table into BIRD’s table, but none are exported from BIRD’s table to the OS’s table. The export where proto = "wg"; line configures BIRD to export to the OS’s main table all the routes it added to its master4 table from the named wg protocol instance — in other words, to copy all the routes it learns from OSPF about the other site into its own active routing table.

Note

This guide assumes that the WireGuard routers themselves will be pre-configured with all the routes they need to forward packets within their own site, either statically or via DHCP (ie they can just forward packets that need to be routed to other subnets within their own site to the their own default gateway, and the default gateway will be able to forward those packets to the next hop).

If the WireGuard routers themselves need to make use of the routes they receive from OSPF or iBGP about their own site, then change the export where proto = "wg"; line to just export all; (or use an alternative filter that will allow the export of routes that the WireGuard routers will need themselves).

The protocol ospf v2 wg block sets up the OSPFv2 instance we’ll use for the WireGuard connection. Note that wg is just an arbitrary internal ID we’ve given to this instance — BIRD would have otherwise given it an ID of ospf1 by default. And by default it will import all the routes it receives over OSPF into its master4 routing table; but export none of the routes from its master4 table to OSPF. The export all; line in this block overrides this export default to export all of the routes from its master4 table to OSPF.

The import where net !~ 10.99.13.0/24; line configures BIRD to avoid pulling in any routes to the 10.99.13.0/24 subnet from OSPF — this is the subnet we’re using for the WireGuard connection between Router 1 and Router 3. The route for 10.99.13.0/24 is already set up in the OS’s main table by wg-quick when it starts up the WireGuard connection, so the OS doesn’t need to learn this route from BIRD; and since this route is only used behind the scenes by the private connection between the WireGuard routers, there’s no need to propagate it to other routers.

The area 10.99.13.0 block configures BIRD to use the area with an ID of 10.99.13.0 for OSPF on the specified interfaces (and as mentioned above, 10.99.13.0 is just an arbitrary 32-bit number, not necessarily related to an IP address or subnet). The interface "wg0"; line specifies that the wg0 WireGuard interface we set up in the section above is included in the area definition.

Other than the custom import and export filters, this OSPF configuration just uses BIRD’s default settings for everything — which is perfect for WireGuard (as long as all WireGuard routers in the same area use the same subnet — 10.99.13.0/24 in the case of Router 1 and Router 3). It will listen for and send out OSPF broadcasts on the wg0 interface, importing the routes learned from OSPF into its master4 table, and exporting the other routes in its master4 table to share over OSPF.

Warning

This first few sections of this guide show how to use OSPFv2, and will not work with OSPFv3. While you can use OSPFv3 to exchange IPv4 routes, OSPFv3 exclusively uses IPv6 link-local connections to do so — so you first need to have IPv6 link-local connections set up and running between your routers before you can use OSPFv3. See the Alternatively Use IPv6 section later for OSPFv3 instructions.

On Router 3, edit its /etc/bird/bird.conf file to match the settings of Router 1, with the exception of using Router 3’s own unique router ID:

router id 10.99.13.3;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.13.0/24;
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

On Router 2, edit its /etc/bird/bird.conf file to set it up similarly to Router 1 and Router 3, but skipping the import of its own WireGuard subnet (10.99.24.0/24), and using a different area ID of 10.99.24.0 for its shared area with Router 4:

router id 10.99.24.2;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.24.0/24;
        export all;
    };
    area 10.99.24.0 {
        interface "wg0";
    };
}

And on Router 4, edit its /etc/bird/bird.conf file to match the settings of Router 2, except with Router 4’s own router ID:

router id 10.99.24.4;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.24.0/24;
        export all;
    };
    area 10.99.24.0 {
        interface "wg0";
    };
}

Run the following command on each router to start up an interactive command-line client to BIRD:

$ sudo birdc
BIRD 2.0.7 ready.

Then run the following command in the BIRD client to reload your changes to /etc/bird/bird.conf:

bird> configure
Reading configuration from /etc/bird/bird.conf
Reconfigured

After reloading your changes on both Router 1 and Router 3, if you run the following command from Router 1, you should see that it’s communicating with Router 3:

bird> show ospf neighbors wg
wg:
Router ID       Pri          State      DTime   Interface  Router IP
10.99.13.3        1     Full/PtP        35.956  wg0        10.99.13.3

But it won’t have imported any routes into BIRD’s default IPv4 routing table yet:

bird> show route

Troubleshooting

If you don’t see any neighbors, first double-check that Router 1 can ping Router 3:

$ ping -nc1 10.99.13.3
PING 10.99.13.3 (10.99.13.3) 56(84) bytes of data.
64 bytes from 10.99.13.3: icmp_seq=1 ttl=64 time=20.3 ms

--- 10.99.13.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 20.325/20.325/20.325/0.000 ms

If that doesn’t work, your WireGuard connection isn’t working, and you need to go back and fix that first.

Otherwise, add a debug protocols all; line to the top of the /etc/bird/bird.conf file:

router id 10.99.13.1;
debug protocols all;
...

And reload your configuration:

bird> configure
Reading configuration from /etc/bird/bird.conf
Reconfigured

And then check BIRD’s syslog output. You should see periodic messages like:

$ journalctl -u bird.service
Oct 12 17:06:02 router1 bird[8106]: wg: HELLO packet sent via wg0
Oct 12 17:06:02 router1 bird[8106]: wg: HELLO packet received from nbr 10.99.13.3 on wg0
Oct 12 17:06:12 router1 bird[8106]: wg: HELLO packet sent via wg0
Oct 12 17:06:12 router1 bird[8106]: wg: HELLO packet received from nbr 10.99.13.3 on wg0
Oct 12 17:06:22 router1 bird[8106]: wg: HELLO packet sent via wg0
Oct 12 17:06:22 router1 bird[8106]: wg: HELLO packet received from nbr 10.99.13.3 on wg0

Plus occasional link-state updates.

Tip

You can “tail” BIRD’s syslog output with the following journald command:

$ journalctl -u bird.service -f

Configure OSPF to LAN Routers

Now that the WireGuard links between Site A and Site B are up and exchanging routes, the next step is to propagate those routes to the sites. If you use OSPF at your sites, follow the instructions in this section to do so; if you use iBGP, follow the instructions from the Alternatively Configure BGP to LAN Routers section.

For this example, we’ll imagine that Router 1 is on the LAN subnet 192.168.1.64/26, and will exchange routes with its LAN router 192.168.1.65 in OSPF area 0; Router 2 is on the LAN subnet 192.168.1.192/26, and will exchange routes with its LAN router 192.168.1.193, also in area 0; Router 3 is on the LAN subnet 192.168.200.0/26, and will exchange routes with its LAN router 192.168.200.1 in OSPF area 0; and Router 4 is on the LAN subnet 192.168.200.192/26, and will exchange routes with its LAN router 192.168.200.193, also in area 0:

OSPF Network

Site A also has a few other subnets, 192.168.2.0/24 and 192.168.3.0/24, that can be accessed through both the 192.168.1.65 and 192.168.1.193 gateways; while Site B has an additional subnet 192.168.200.128.0/26.

On each WireGuard router, edit its /etc/bird/bird.conf file to add a second protocol ospf block:

protocol ospf v2 lan {
    ipv4 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

Replace eth0 with the actual name of the router’s LAN interface (if it’s not eth0), and replace the area ID 0 with the actual OSPF area ID you’re using for the LAN subnet the router is on (if not 0). Also notice that we’ve assigned this protocol block an arbitrary internal ID of lan (otherwise BIRD would give it an autogenerated ID like ospf2).

The export all; line configures BIRD to export all the routes from its master4 table to share with OSPF. By default, BIRD will also import all the IPv4 routes it receives over OSPF into its master4 table. If you don’t want to propagate all of the site’s routes to the other site, add an import filter here to the ipv4 channel to limit the routes that BIRD will pull in from this OSPF instance.

For example, to propagate only routes in the 192.168.1.0/24 subnet, add an import where net ~ 192.168.1.0/24; line to the ipv4 block; or to import all routes except the 10.0.0.0/16 and 10.100.0.0/16 blocks, add import where net !~ 10.0.0.0/16 && net !~ 10.100.0.0/16;; etc. BIRD includes a complete programming language for filtering routes, if you need something more sophisticated.

The full /etc/bird/bird.conf file on Router 1 should now look like this:

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.13.0/24;
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol ospf v2 lan {
    ipv4 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

Now run the following command in the BIRD client to reload your changes to /etc/bird/bird.conf:

bird> configure
Reading configuration from /etc/bird/bird.conf
Reconfigured

After reloading, you should see at least one neighbor for the new lan OSPF instance:

bird> show ospf neighbors lan
lan:
Router ID       Pri          State      DTime   Interface  Router IP
192.168.1.65     10     Full/DR         39.184  eth0       192.168.1.65

If you don’t, check the OSPF configuration on the LAN router, and adjust the WireGuard router’s lan OSPF configuration to match. In particular, make sure the area ID and hello interval match (BIRD’s default hello interval is 10 seconds).

If you’ve only added the new lan OSPF instance to one WireGuard router (like only to Router 1), BIRD will pick up the routes from the router’s own site only:

bird> show route
Table master4:
192.168.1.64/26      unicast [lan 18:25:47.947] I (150/10) [10.99.13.1]
        dev eth0
                     unicast [lan 18:25:47.947] E1 (150/20) [192.168.1.65]
        via 192.168.1.65 on eth0
192.168.1.192/26     unicast [lan 18:25:47.947] E1 (150/30) [192.168.1.193]
        via 192.168.1.65 on eth0
192.168.3.0/24       unicast [lan 18:25:47.947] E1 (150/30) [192.168.1.1]
        via 192.168.1.65 on eth0
192.168.4.0/24       unicast [lan 18:25:47.947] E1 (150/30) [192.168.1.1]
        via 192.168.1.65 on eth0

But once you’ve updated all the routers, each router should have at least one path to all the routes exposed by OSPF on any LAN subnet in BIRD’s master4 table:

bird> show route
Table master4:
192.168.1.64/26      unicast [lan 18:25:47.947] I (150/10) [10.99.13.1]
        dev eth0
                     unicast [lan 18:25:47.947] E1 (150/20) [192.168.1.65]
        via 192.168.1.65 on eth0
192.168.1.192/26     unicast [lan 18:25:47.947] E1 (150/30) [192.168.1.193]
        via 192.168.1.65 on eth0
192.168.3.0/24       unicast [lan 18:25:47.947] E1 (150/30) [192.168.1.1]
        via 192.168.1.65 on eth0
192.168.4.0/24       unicast [lan 18:25:47.947] E1 (150/30) [192.168.1.1]
        via 192.168.1.65 on eth0
192.168.200.0/26     unicast [wg 19:37:19.495] E1 (150/20) [10.99.13.3]
        via 10.99.13.3 on wg0
                     unicast [lan 20:14:51.882] E1 (150/60) [192.168.200.1]
        via 192.168.1.65 on eth0
192.168.200.128/26   unicast [wg 19:37:19.495] E1 (150/30) [192.168.200.1]
        via 10.99.13.3 on wg0
                     unicast [lan 20:14:51.882] E1 (150/50) [192.168.200.193]
        via 192.168.1.65 on eth0
192.168.200.192/26   unicast [wg 19:37:19.495] E1 (150/40) [192.168.200.193]
        via 10.99.13.3 on wg0
                     unicast [lan 20:14:51.882] E1 (150/40) [10.99.24.4]
        via 192.168.1.65 on eth0

And each router should have the best path for each subnet at the other site in its OS’s main table (notice that the routes exported by BIRD will be listed as proto bird):

$ ip route
default via 192.168.1.65 dev eth0 proto dhcp src 192.168.1.101 metric 100
10.99.13.0/24 dev wg0 proto kernel scope link src 10.99.13.1
192.168.1.64/26 dev eth0 proto kernel scope link src 192.168.1.101
192.168.1.65 dev eth0 proto dhcp scope link src 192.168.1.101 metric 100
192.168.200.0/26 via 10.99.13.3 dev wg0 proto bird metric 32
192.168.200.128/26 via 10.99.13.3 dev wg0 proto bird metric 32
192.168.200.192/26 via 10.99.13.3 dev wg0 proto bird metric 32

So you should now be able to ping Endpoint B (at 192.168.200.22) in Site B from Endpoint A (at 192.168.1.91) in Site A (provided you don’t have any firewalls blocking ICMP packets on some hop between the two hosts):

$ ping -nc1 192.168.200.22
PING 192.168.200.22 (192.168.200.22) 56(84) bytes of data.
64 bytes from 192.168.200.22: icmp_seq=1 ttl=59 time=23.3 ms

--- 192.168.200.22 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 23.279/23.279/23.279/0.000 ms

And if you kill one of the WireGuard links between Site A and Site B (like for testing purposes, run sudo wg-quick down wg0 on Router 1), you should still be able to ping Endpoint B from Endpoint A — Endpoint A’s LAN router should receive the link-state update via OSPF, and re-route the traffic between Endpoint A and Endpoint B through the other WireGuard link.

Alternatively Configure BGP to LAN Routers

If you use use iBGP to exchange internal routes at one site (or both sites), you can use BGP instead of OSPF to propagate the routes exchanged by the WireGuard routers to the rest of the site.

For example, let’s say that Site A uses iBGP. We can configure Router 1 and Router 2 to use BGP, and connect them to a dedicated Route Reflector (RR) used by all the LAN routers at the site. The WireGuard routers will propagate the routes from Site B to the RR (and vice versa), and the RR will propagate the routes to the rest of the site:

BGP Network

The RR will be at 192.168.3.1, and will use a private Autonomous System Number (ASN) of 65000.

On Router 1 and Router 2, edit their /etc/bird/bird.conf files to add a protocol direct and a protocol bgp block:

protocol direct {
    ipv4;
    interface "eth0";
}

protocol bgp {
    ipv4 {
        export all;
        next hop self;
    };
    local as 65000;
    neighbor 192.168.3.1 internal;
}

Replace eth0 in the protocol direct block with the actual name of the router’s LAN interface (or if the WireGuard router has multiple interfaces connected to different parts of its site, use an interface pattern that encompasses them all, like "eth*", "gre*", etc). The protocol direct block is needed to import the direct interface routes for the LAN interface into BIRD’s master4 routing table, so that it can add the correct next hop to the routes from the other site that it will share via BGP.

In the protocol bgp block, replace 65000 with the actual private ASN you use, and replace 192.168.3.1 with the actual IP address of the iBGP server with which the router should exchange routes. Add multiple protocol bgp blocks if you have multiple iBGP servers with which the WireGuard routers need to exchange routes (one block for each iBGP server).

The export all; line directs BIRD to share all the routes in its master4 table with the other iBGP server. The next hop self; line directs it to adjust those routes to make the WireGuard router itself the next hop in all those routes. This will share the routes the WireGuard router learned from the other site, using the WireGuard router itself as the link to the site.

By default, BIRD will also import all the IPv4 routes it receives over BGP into its master4 table (to be shared back to its paired WireGuard router at the other site via OSPF). If you don’t want to propagate all of this site’s routes to the other site, add an import filter to ipv4 channel to limit the routes that BIRD will pull in from BGP.

For example, to propagate only routes in the 192.168.1.0/24 subnet, add an import where net ~ 192.168.1.0/24; line to the ipv4 block; or to import all routes except the 10.0.0.0/16 and 10.100.0.0/16 blocks, add import where net !~ 10.0.0.0/16 && net !~ 10.100.0.0/16;; etc. See BIRD’s filter reference if you need something more complicated.

The full /etc/bird/bird.conf file on Router 1 should now look like this:

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.13.0/24;
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol direct {
    ipv4;
    interface "eth0";
}

protocol bgp {
    ipv4 {
        export all;
        next hop self;
    };
    local as 65000;
    neighbor 192.168.3.1 internal;
}

Make a similar change to Router 2, and run the following command in the BIRD client on each to reload your changes:

bird> configure
Reading configuration from /etc/bird/bird.conf
Reconfigured

Once you’ve updated the routers, each router should have at least one path to all the routes exposed by BGP in BIRD’s master4 table:

bird> show route
Table master4:
192.168.1.64/26      unicast [direct1 22:21:41.501] * (240)
        dev eth0
                     unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0
192.168.1.192/26     unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0
192.168.3.0/24       unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0
192.168.4.0/24       unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0
192.168.200.0/26     unicast [wg 19:37:19.495] E1 (150/20) [10.99.13.3]
        via 10.99.13.3 on wg0
                     unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0
192.168.200.128/26   unicast [wg 19:37:19.495] E1 (150/30) [192.168.200.1]
        via 10.99.13.3 on wg0
                     unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0
192.168.200.192/26   unicast [wg 19:37:19.495] E1 (150/40) [192.168.200.193]
        via 10.99.13.3 on wg0
                     unicast [bgp1 22:30:34.341] (100) [i]
        via 192.168.1.65 on eth0

And each router should have the best path for each subnet at the other site in its OS’s main table (with the routes exported by BIRD listed with proto bird):

$ ip route
default via 192.168.1.65 dev eth0 proto dhcp src 192.168.1.101 metric 100
10.99.13.0/24 dev wg0 proto kernel scope link src 10.99.13.1
192.168.1.64/26 dev eth0 proto kernel scope link src 192.168.1.101
192.168.1.65 dev eth0 proto dhcp scope link src 192.168.1.101 metric 100
192.168.200.0/26 via 10.99.13.3 dev wg0 proto bird metric 32
192.168.200.128/26 via 10.99.13.3 dev wg0 proto bird metric 32
192.168.200.192/26 via 10.99.13.3 dev wg0 proto bird metric 32

So you should now be able to successfully ping Endpoint B (at 192.168.200.22) in Site B from Endpoint A (at 192.168.1.91) in Site A (provided you don’t have any firewalls blocking ICMP packets on some hop between the two hosts):

$ ping -nc1 192.168.200.22
PING 192.168.200.22 (192.168.200.22) 56(84) bytes of data.
64 bytes from 192.168.200.22: icmp_seq=1 ttl=59 time=23.3 ms

--- 192.168.200.22 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 23.279/23.279/23.279/0.000 ms

And if you kill one of the WireGuard links between Site A and Site B (like for testing purposes, run sudo wg-quick down wg0 on Router 1), you should still be able to ping Endpoint B from Endpoint A — Endpoint A’s LAN router should receive the updated route to Endpoint B via BGP, and re-route the traffic between Endpoint A and Endpoint B using the other WireGuard link.

Alternatively Use IPv6

You can use the same basic setup outlined in the above sections to exchange IPv6 routes (and to exchange routes over IPv6 links). The main difference is that you need to use OSPFv3 instead of OSPFv2 to exchange IPv6 routes between the WireGuard routers — and since OSPFv3 is meant to operate only over direct IPv6 links, you have to assign IPv6 link-local addresses to your WireGuard interfaces.

So these are the steps to follow in order to add IPv6 to (or replace IPv4 with IPv6 in) the above examples:

Enable IPv6 Packet Forwarding

To enabling IPv6 packet forwarding, add the following to your /etc/sysctl.d/local.conf file (create it if hadn’t already):

net.ipv6.conf.all.forwarding=1

Then run the following command to apply it (along with all other sysctl conf files):

sudo sysctl --system

Add WireGuard IPv6 Addresses

You can add multiple IP addresses, including a mix of IPv4 and IPv6 addresses, to any WireGuard interface. So, for example, we can add the fe99:13::1 IPv6 address (on the link-local fe99:13::/64 subnet) to Router 1 simply by adding a second Address entry to its interface definition in its /etc/wireguard/wg0.conf file:

# local settings for Router 1
[Interface]
PrivateKey = 0F11111111111111111111111111111111111111110=
Address = 10.99.13.1/24
Address = fe99:13::1/64
ListenPort = 51820
Table = off

# remote settings for Router 3
[Peer]
PublicKey = IZ2rER/G68c1LCHeXIkpHqKYT7zB5bLkNULSSJ5HI1U=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.33:51820

Alternatively, you can combine multiple IP addresses in the same Address entry by concatenating the addresses with commas:

Address = 10.99.13.1/24, fe99:13::1/64

And if you don’t need to use IPv4 with the interface, you can omit the IPv4 address entirely, and just use the IPv6 address:

Address = fe99:13::1/64
Note

It doesn’t matter whether the Endpoint address of the interface’s peers uses IPv4 or IPv6. WireGuard can tunnel both IPv4 and IPv6 packets through the same connection to the peer, regardless of whether or not the connection itself uses IPv4 or IPv6.

So add these IPv6 addresses to each WireGuard interface:

  1. fe99:13::1/64 for Router 1

  2. fe99:24::2/64 for Router 2

  3. fe99:13::3/64 for Router 3

  4. fe99:24::4/64 for Router 4

These addresses put Router 1 and Router 3 on the same fe99:13::/64 link-local subnet, and Router 2 and Router 4 on the same fe99:24::/64 link-local subnet.

If the interfaces are already up, restart them after making those changes (otherwise, start them now).

Set Up OSPFv3 in BIRD

If you don’t need to exchange IPv4 routes, you can replace the /etc/bird/bird.conf configuration on Router 1 with the following:

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol ospf v3 lan6 {
    ipv6 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

The only differences between the IPv4 version of this config and the IPv6 version are:

  1. It uses v2 instead of v3 for the protocol ospf blocks.

  2. It gives the protocol instances different IDs: wg6 for the instance exchanging OSPFv3 routes on the wg0 interface, and lan6 for the instance exchanging OSPFv3 routes on the eth0 interface. These IDs are just arbitrary, however — you can change them to whatever you want.

  3. Because we’re using IPv6 link-local addresses for WireGuard, we don’t need an explicit filter in the protocol ospf v3 wg6 block to exclude our WireGuard addresses from being imported into BIRD’s master6 table — BIRD will filter them out automatically (since it doesn’t make sense to exchange routes to link-local addresses).

If you do need both IPv4 and IPv6, add the above IPv6 protocol kernel and protocol ospf blocks to the original IPv4 version of Router 1’s /etc/bird/bird.conf file:

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.13.0/24;
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol ospf v2 lan {
    ipv4 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol ospf v3 lan6 {
    ipv6 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

Make similar changes to the /etc/bird/bird.conf on all the WireGuard routers; the IPv6-only version on Router 2 would look like this:

router id 10.99.24.2;

protocol device {
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.24.0 {
        interface "wg0";
    };
}

protocol ospf v3 lan6 {
    ipv6 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

And Router 3 like this:

router id 10.99.13.3;

protocol device {
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol ospf v3 lan6 {
    ipv6 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

And Router 4 like this:

router id 10.99.24.4;

protocol device {
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.24.0 {
        interface "wg0";
    };
}

protocol ospf v3 lan6 {
    ipv6 {
        export all;
    };
    area 0 {
        interface "eth0";
    };
}

Reload your changes to each /etc/bird/bird.conf file:

bird> configure
Reading configuration from /etc/bird/bird.conf
Reconfigured

After reloading, on each WireGuard router you should see one neighbor for the wg6 OSPF instance:

bird> show ospf neighbors wg6
wg6:
Router ID       Pri          State      DTime   Interface  Router IP
10.99.13.3        1     Full/PtP        35.956  wg0        fe99:13::3

And at least one neighbor for the lan6 OSPF instance:

bird> show ospf neighbors lan6
lan6:
Router ID       Pri          State      DTime   Interface  Router IP
192.168.1.65     10     Full/DR         39.184  eth0       fe80::1921:68ff:fe01:65/64

And each router should have at least one path to all the routes exposed by OSPF on any LAN subnet in BIRD’s master6 table:

bird> show route
Table master6:
2001:db8:1:100::/56    unicast [lan6 18:25:47.947] I (150/10) [10.99.13.1]
        dev eth0
                     unicast [lan6 18:25:47.947] E1 (150/20) [192.168.1.65]
        via fe80::1921:68ff:fe01:65 on eth0
2001:db8:1:300::/56    unicast [lan6 18:25:47.947] E1 (150/30) [192.168.1.193]
        via fe80::1921:68ff:fe01:65 on eth0
2001:db8:3::/48      unicast [lan6 18:25:47.947] E1 (150/30) [192.168.1.1]
        via fe80::1921:68ff:fe01:65 on eth0
2001:db8:4::/48      unicast [lan6 18:25:47.947] E1 (150/30) [192.168.1.1]
        via fe80::1921:68ff:fe01:65 on eth0
2001:db8:200::/56    unicast [wg6 19:37:19.495] E1 (150/20) [10.99.13.3]
        via fe99:13::3 on wg0
                     unicast [lan6 20:14:51.882] E1 (150/60) [192.168.200.1]
        via fe80::1921:68ff:fe01:65 on eth0
2001:db8:200:200::/56  unicast [wg6 19:37:19.495] E1 (150/30) [192.168.200.1]
        via fe99:13::3 on wg0
                     unicast [lan6 20:14:51.882] E1 (150/50) [192.168.200.193]
        via fe80::1921:68ff:fe01:65 on eth0
2001:db8:200:300::/56  unicast [wg6 19:37:19.495] E1 (150/40) [192.168.200.193]
        via fe99:13::3 on wg0
                     unicast [lan6 20:14:51.882] E1 (150/40) [10.99.24.4]
        via fe80::1921:68ff:fe01:65 on eth0

And each router should have the best path for each subnet at the other site in its OS’s main IPv6 table (where the routes exported by BIRD will be listed with proto bird):

$ ip -6 route
::1 dev lo proto kernel metric 256 pref medium
2001:db8:1:100::/56 dev eth0 proto ra metric 100 pref medium
2001:db8:200::/56 via fe99:13::3 dev wg13 proto bird metric 32 pref medium
2001:db8:200:200::/56 via fe99:13::3 dev wg13 proto bird metric 32 pref medium
2001:db8:200:300::/56 via fe99:13::3 dev wg13 proto bird metric 32 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
fe99::/64 dev wg0 proto kernel metric 256 pref medium
default via fe80::1921:68ff:fe01:65 dev eth0 proto ra metric 100 expires 1792sec pref medium

With that in place, you should now be able to ping the IPv6 address for Endpoint B (at 2001:db8:200:22::1) in Site B from Endpoint A (at 2001:db8:1:91::1) in Site A (provided you don’t have any firewalls blocking ICMPv6 packets on some hop between the two hosts):

$ ping -nc1 2001:db8:200:22::1
PING 2001:db8:200:22::1 (2001:db8:200:22::1) 56 data bytes
64 bytes from 2001:db8:200:22::1: icmp_seq=1 ttl=59 time=23.3 ms

--- 2001:db8:200:22::1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 23.279/23.279/23.279/0.000 ms

And if you kill one of the WireGuard links between Site A and Site B (like for testing purposes, run sudo wg-quick down wg0 on Router 1), you should still be able to ping Endpoint B from Endpoint A — Endpoint A’s LAN router should receive the link-state update via OSPF, and re-route the traffic between Endpoint A and Endpoint B through the other WireGuard link.

Alternatively Use BGP With IPv6

BGP can exchange IPv6 routes over IPv4, and IPv4 routes over IPv6, so you don’t need to change much with BIRD’s BGP configuration to use IPv6. You do need to configure the WireGuard routers to use OSPFv3 over IPv6 link-local addresses, however, as described above.

Once you do that, you can do IPv6-only exchanges of routes with a configuration like the following (for Router 1):

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol direct {
    ipv6;
    interface "eth0";
}

protocol bgp {
    ipv6 {
        export all;
        next hop self;
    };
    local as 65000;
    neighbor 2001:db8:3:1::1 internal;
}

To exchange both IPv4 and IPv6 routes, you’d instead configure the router’s /etc/bird/bird.conf like this (again for Router 1):

router id 10.99.13.1;

protocol device {
}

protocol kernel {
    ipv4 {
        export where proto = "wg";
    };
}

protocol ospf v2 wg {
    ipv4 {
        import where net !~ 10.99.13.0/24;
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol kernel {
    ipv6 {
        export where proto = "wg6";
    };
}

protocol ospf v3 wg6 {
    ipv6 {
        export all;
    };
    area 10.99.13.0 {
        interface "wg0";
    };
}

protocol direct {
    ipv4;
    ipv6;
    interface "eth0";
}

protocol bgp {
    ipv4 {
        export all;
        next hop self;
    };
    ipv6 {
        export all;
        next hop self;
        next hop address 2001:db8:1:111::1;
    };
    local as 65000;
    neighbor 192.168.3.1 internal;
}

Unlike the protocol kernel and protocol ospf instances, the same protocol direct and protocol bgp instances can be used for both IPv4 and IPv6. But in the protocol bgp block, you will need to declare the specific next hop address to use for the WireGuard router in the channel over which the routes are not exchanged (ie if routes are exchanged over IPv4, you’ll need to specify the next-hop address for the IPv6 channel, like the example above; if routes were exchanged over IPv6, however, you’d need to specify the next-hop address for the IPv4 channel instead).