bella.network Blog

Dynamic routing using FRR over WireGuard

Connect multiple networks using WireGuard and provide dynamic routing between them using BGP
BGP used on routers interconnect the entire world, at smaller scale we can use it internally
BGP used on routers interconnect the entire world, at smaller scale we can use it internally (Photo by Thomas Jensen on Unsplash)

Public IPv4 addresses are becoming less and less available, IPv6 addresses will still not be widespread or dynamically assigned to end customers in 2024 and many applications still communicate with each other unencrypted today.

A wide variety of technologies and methods can be used to securely connect different networks. I would like to present one such approach here:

Dynamic site to site networking using WireGuard and BGP

Why should I do this?

There are many different reasons to use these technologies:

  • Security: WireGuard is a modern VPN protocol that is easy to set up and secure by default, directly integrated into the Linux kernel.
  • Performance: WireGuard is designed to be fast and efficient, with low latency and high throughput.
  • Flexibility: With dynamic routing, you can easily add new networks or change existing ones.
  • Redundancy: If a connection fails, the network can automatically switch to another path.
  • High granularity: You can control which networks are allowed to communicate with each other.
  • No NAT: You can avoid NAT and use your private IP addresses to communicate with other networks.

How does it work?

In this example, I will use a simple setup with three networks:

  • Network A: The primary site with a public IP address, data center and private networks.
  • Network B: A remote site with a static IP address, a few servers and clients.
  • Network C: A remote site with a dynamic non-public IP address and clients.

For the sites we assume the following networks:

  • Network A:
    • 10.10.0.0/16 – Data center network
    • 2001:db8:10::/48 – Data center network (IPv6)
    • 10.20.0.0/16 – Office network
    • 2001:db8:20::/48 – Office network (IPv6)
  • Network B:
    • 10.100.0.0/16 – Location Network B
    • 2001:db8:100::/48 – Location Network B (IPv6)
  • Network C:
    • 10.101.0.0/16 – Location Network C
    • 2001:db8:101::/48 – Location Network C (IPv6)
  • Transfer networks:
  • 172.31.10.0/24 – /32 subnets for direct connections between peers
  • 2001:db8::/32 – /112 subnets for direct connections between peers (use your own IPv6 prefix and /64 subnet if possible)

We will use WireGuard to create a secure tunnel between the sites and FRR (Free Range Routing) to provide dynamic routing between them using BGP (Border Gateway Protocol). This allows us to automatically exchange routing information between the sites and use the best path for each connection. We can also use BGP to filter which networks are allowed to communicate with each other.

With this setup, every network can communicate with each other directly, without the need for NAT or port forwarding. If one connection fails, the network can automatically switch to another path. We can also easily add new networks or change existing ones without reconfiguring all routers.

First, all dependencies need to be installed on the routers. For Ubuntu 24.04 LTS, you can use the following commands:

1
2
sudo apt update
sudo apt install wireguard-tools frr

WireGuard Setup

First, we need to set up WireGuard on all routers. We will use a simple configuration with a single interface and a pre-shared key for authentication. You can find more information about WireGuard in the official documentation.

In this setup, we will use Ubuntu 24.04 LTS as the operating system for all routers. You can use any other Linux distribution that supports WireGuard. (This setup also works in LXC containers or virtual machines like Proxmox.)

To generate a new private key for your local server, you can use the following command:

1
wg genkey > s2svp00.key

You can then use this private key to generate a public key for the remote server:

1
wg pubkey < s2svp00.key > s2svp00.pub

To generate a preshared key which is used on the local and remote server, you can use the following command:

1
wg genpsk > s2svp00.psk

Here is an example configuration for Network A (s2svp00) to which Network B (s2svp00) will connect, which is saved in the file /etc/wireguard/s2svp00.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Interface]
ListenPort = 51820
PrivateKey = <private key>

[Peer]
PublicKey = <public key>
PreSharedKey = <pre-shared key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <public ip Network B>:51820
PersistentKeepalive = 25

The following configuration is used on Network B (s2svp00) to connect to Network A (s2svp00), which is saved in the file /etc/wireguard/s2svp00.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Interface]
ListenPort = 51820
PrivateKey = <private key>

[Peer]
PublicKey = <public key>
PreSharedKey = <pre-shared key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <public ip Network A>:51820
PersistentKeepalive = 25

With the above configuration, both sites will try to directly connect to each other using the public IP address of the other site. If the public IP address changes, the connection will be lost. To avoid this, you can use a dynamic DNS service or a VPN server with a static IP address.

To start the WireGuard interface, a systemd service can be used which executes a script with the following content on the router of Network A:

1
2
3
4
5
6
7
8
9
#!/bin/bash

# Server 2 Server VPN Peering 1 - NETWORK A to NETWORK B
ip link add dev s2svp00 type wireguard
wg setconf s2svp00 /etc/wireguard/s2svp00.conf
ip addr add 172.31.10.0/32 peer 172.31.10.1/32 dev s2svp00
ip -6 addr add 2001:db8::0:0/112 peer 2001:db8::0:1/112 dev s2svp00
ip -6 addr add fe80:::0:0/64 dev s2svp00 scope link
ip link set s2svp00 up

On the router of Network B, the following script can be used:

1
2
3
4
5
6
7
8
9
#!/bin/bash

# Server 2 Server VPN Peering 1 - NETWORK B to NETWORK A
ip link add dev s2svp00 type wireguard
wg setconf s2svp00 /etc/wireguard/s2svp00.conf
ip addr add 172.31.10.1/32 peer 172.31.10.0/32 dev s2svp00
ip -6 addr add 2001:db8::0:1/112 peer 2001:db8::0:0/112 dev s2svp00
ip -6 addr add fe80:::0:1/64 dev s2svp00 scope link
ip link set s2svp00 up

Using the command wg show you can check if the WireGuard interface is up and running.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface: s2svp00
  public key: <public key>
  private key: (hidden)
  listening port: 51820

peer: <public key remote>
  preshared key: (hidden)
  endpoint: <public ip remote>:51820
  allowed ips: 0.0.0.0/0, ::/0
  latest handshake: 16 seconds ago
  transfer: 2.20 MiB received, 2.84 MiB sent
  persistent keepalive: every 25 seconds

The WireGuard interface should now be up and running on both routers. You can now ping the other router using the IP address of the WireGuard interface.

FRR Setup (BGP)

To provide dynamic routing between the sites, we will use FRR (Free Range Routing) with BGP (Border Gateway Protocol). FRR is a fork of Quagga and provides a modern routing stack with support for BGP, OSPF, RIP, and other routing protocols.

First, we need to configure FRR on all routers. You can find more information about FRR in the official documentation.

To enable BGP within FRR, the file /etc/frr/daemons needs to be edited and the following line needs to be modified:

1
bgpd=yes

After changing the configuration, the FRR service needs to be restarted:

1
sudo systemctl restart frr

Next, the BGP configuration needs to be added using vtysh on the router of Network A. On this site we will use the AS number 65001 and on the router of Network B the AS number 65002.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
hostname <network A unique hostname of router>
log stdout informational
log syslog
!
debug bgp neighbor-events
debug bgp updates in
debug bgp updates out
!
router bgp 65001
 bgp log-neighbor-changes
 no bgp ebgp-requires-policy
 no bgp suppress-duplicates
 no bgp hard-administrative-reset
 no bgp default ipv4-unicast
 no bgp graceful-restart notification
 bgp graceful-restart
 no bgp network import-check
 neighbor 172.31.10.1 remote-as 65002
 neighbor 172.31.10.1 description network-b
 neighbor 2001:db8::0:1 remote-as 65002
 neighbor 2001:db8::0:1 description network-b
 bgp fast-convergence
 !
 address-family ipv4 unicast
  network 10.10.0.0/16
  network 10.20.0.0/16
  network 172.31.10.0/31
  redistribute static
  neighbor 172.31.10.1 activate
  neighbor 172.31.10.1 addpath-tx-all-paths
 exit-address-family
 !
 address-family ipv6 unicast
  network 2001:db8:10::/48
  network 2001:db8:20::/48
  network 2001:db8::0:0/112
  redistribute static
  neighbor 2001:db8::0:1 activate
  neighbor 2001:db8::0:1 addpath-tx-all-paths
 exit-address-family
exit

do wr mem

On the router of Network B, the following configuration needs to be added using vtysh (basically only the AS number and local networks need to be changed):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
hostname <network A unique hostname of router>
log stdout informational
log syslog
!
debug bgp neighbor-events
debug bgp updates in
debug bgp updates out
!
router bgp 65002
 bgp log-neighbor-changes
 no bgp ebgp-requires-policy
 no bgp suppress-duplicates
 no bgp hard-administrative-reset
 no bgp default ipv4-unicast
 no bgp graceful-restart notification
 bgp graceful-restart
 no bgp network import-check
 neighbor 172.31.10.0 remote-as 65001
 neighbor 172.31.10.0 description network-a
 neighbor 2001:db8::0:1 remote-as 65001
 neighbor 2001:db8::0:1 description network-a
 bgp fast-convergence
 !
 address-family ipv4 unicast
  network 10.100.0.0/16
  network 172.31.10.0/31
  redistribute static
  neighbor 172.31.10.0 activate
  neighbor 172.31.10.0 addpath-tx-all-paths
 exit-address-family
 !
 address-family ipv6 unicast
  network 2001:db8:100::/48
  network 2001:db8::0:0/112
  redistribute static
  neighbor 2001:db8::0:0 activate
  neighbor 2001:db8::0:0 addpath-tx-all-paths
 exit-address-family
exit

do wr mem

With the above configuration, the routers will automatically exchange routing information using BGP. You can use the command show ip bgp summary within vtysh to check if the BGP session is up and running.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Routing# show ip bgp summary

IPv4 Unicast Summary:
BGP router identifier 127.0.0.3, local AS number 65001 VRF default vrf-id 0
BGP table version 7058
RIB entries 66, using 6336 bytes of memory
Peers 1, using 181 KiB of memory

Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
172.31.10.1     4      65002     36058     38613     7058    0    0      03m            2       93 network-b

If the field State/PfxRcd shows a number greater than 0 and Up/Down does not show never, the BGP session is up and running.

With this setup, you can now communicate between the networks using the private IP addresses of the routers. To add an additional site like Network C, you can simply repeat the steps above and add the new network to the BGP configuration.

The main difference to Network C is that the IP address of the site is dynamic and not public, or even behind a NAT. Network A and B have to remove the Endpoint line from the WireGuard configuration and rely on Network C to initiate the connection, as the public IP address of Network C is not known to the other sites.

Example of a configuration for Network A for a connection coming from Network C (s2svp01) is saved in the file /etc/wireguard/s2svp01.conf:

1
2
3
4
5
6
7
8
9
[Interface]
ListenPort = 51821
PrivateKey = <private key>

[Peer]
PublicKey = <public key>
PreSharedKey = <pre-shared key>
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

Example of a configuration for Network C for a connection to Network A (s2svp01) is saved in the file /etc/wireguard/s2svp01.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Interface]
ListenPort = 51821
PrivateKey = <private key>

[Peer]
PublicKey = <public key>
PreSharedKey = <pre-shared key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <public ip Network A>:51821
PersistentKeepalive = 25

The WireGuard script does only need to be adjusted to use the new interface name and IP addresses.

1
2
3
4
5
6
7
8
9
#!/bin/bash

# Server 2 Server VPN Peering 2 - NETWORK C to NETWORK A
ip link add dev s2svp01 type wireguard
wg setconf s2svp01 /etc/wireguard/s2svp01.conf
ip addr add 172.31.10.2/32 peer 172.31.10.3/32 dev s2svp01
ip -6 addr add 2001:db8::1:2/112 peer 2001:db8::1:3/112 dev s2svp01
ip -6 addr add fe80:::1:2/64 dev s2svp01 scope link
ip link set s2svp01 up

The BGP configuration on Network A needs to be adjusted to include the new network and the new peer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
router bgp 65002
 neighbor 172.31.10.3 remote-as 65003
 neighbor 172.31.10.3 description network-c
 neighbor 2001:db8::0:3 remote-as 65003
 neighbor 2001:db8::0:3 description network-c
 address-family ipv4 unicast
  network 172.31.10.2/31
  neighbor 172.31.10.3 activate
  neighbor 172.31.10.3 addpath-tx-all-paths
 exit-address-family
 !
 address-family ipv6 unicast
  network 2001:db8::1:0/112
  neighbor 2001:db8::1:3 activate
  neighbor 2001:db8::1:3 addpath-tx-all-paths
 exit-address-family
 !
exit

do wr mem

The configuration on Network C is similar to the configuration on Network B, with the AS number 65003 and the local networks of Network C.

With this setup, you can now communicate between all networks using the private IP addresses of the routers. Idially, you can add a communication between Network B and C by adding the necessary configuration to the routers of Network B and C. This way, one site like Network A can be down without affecting the communication between the other sites.

Advanced: Inbound and outbound route filtering

To control which networks are allowed to communicate with each other, you can use route filtering in BGP. This allows you to filter inbound and outbound routes based on the network prefix, AS path, or other criteria. You can also use route maps to modify the routes before they are advertised to other routers.

Here is an example of how to filter inbound and outbound routes in BGP using FRR on Network A with route maps:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
router bgp 65001
  address-family ipv4 unicast
  network 10.10.0.0/16 route-map ANNOUNCE-v4
  network 10.20.0.0/16 route-map ANNOUNCE-v4
  network 172.31.10.0/31 route-map ANNOUNCE-v4
  redistribute static
  neighbor 172.31.10.1 activate
  neighbor 172.31.10.1 soft-reconfiguration inbound
  neighbor 172.31.10.1 route-map FILTER-INBOUND in
  neighbor 172.31.10.1 route-map ANNOUNCE-v4 out
 exit-address-family
exit
!
ip prefix-list MY-NETWORKS seq 10 permit 10.10.0.0/16
ip prefix-list MY-NETWORKS seq 20 permit 10.20.0.0/16
ip prefix-list MY-NETWORKS seq 30 permit 172.31.10.0/24
ip prefix-list MY-NETWORKS seq 100 deny 0.0.0.0/0
!
ip prefix-list INBOUND-FILTER seq 87 permit 10.0.0.0/8 ge 9
ip prefix-list INBOUND-FILTER seq 88 permit 172.16.0.0/12 ge 13
ip prefix-list INBOUND-FILTER seq 89 permit 192.168.0.0/16 ge 17
ip prefix-list INBOUND-FILTER seq 100 deny 0.0.0.0/0
!
route-map ANNOUNCE-v4 permit 10
 match ip address prefix-list MY-NETWORKS
exit
!
route-map ANNOUNCE-v4 deny 20
exit
!
route-map FILTER-INBOUND permit 10
 match ip address prefix-list INBOUND-FILTER
exit
!
route-map FILTER-INBOUND deny 20

With the above configuration, only the networks defined in MY-NETWORKS are allowed to be announced to the BGP peer. The route map FILTER-INBOUND filters inbound routes based on the INBOUND-FILTER prefix list, which allows only private IP addresses to be accepted (Except too big RFC1918 address ranges like 10.0.0.0/8).

You can use the command show ip bgp within vtysh to check if the routes are filtered correctly.

Example Homelab

In my homelab environment, I use a similar setup to connect different networks using WireGuard and provide dynamic routing between them using BGP. I have a primary site with a public IP address and multiple IPv4 and IPv6 networks, as well as three remote sites with static and dynamic IP addresses. Additionally, some friends and family members have access to the network using WireGuard.

Homelab BGP network diagram

Conclusion

With this setup, you can easily connect multiple networks using WireGuard and provide dynamic routing between them using BGP. This allows you to automatically exchange routing information between the sites and use the best path for each connection. You can also use BGP to filter which networks are allowed to communicate with each other.

Why s2svp01?

s2svp01 is th short form of “Server 2 Server VPN Peering 01” and is used to identify the different VPN connections between the sites. This way, you can easily add new connections and using the following scheme it’s easy to identify the connection and IP addresses used:

  • s2svp00: Connection 00
    • 172.31.10.0/31 with 172.31.10.0 primary and 172.31.10.1 secondary
    • 2001:db8::0:0/112 with 2001:db8::0:0 primary and 2001:db8::0:1 secondary
  • s2svp01: Connection 01
    • 172.31.10.2/31 with 172.31.10.2 primary and 172.31.10.3 secondary
    • 2001:db8::1:0/112 with 2001:db8::1:2 primary and 2001:db8::1:3 secondary
  • s2svp02: Connection 02
    • 172.31.10.4/31 with 172.31.10.4 primary and 172.31.10.5 secondary
    • 2001:db8::2:0/112 with 2001:db8::2:4 primary and 2001:db8::2:5 secondary
  • s2svp03: Connection 03
    • 172.31.10.6/31 with 172.31.10.6 primary and 172.31.10.7 secondary
    • 2001:db8::3:0/112 with 2001:db8::3:6 primary and 2001:db8::3:7 secondary

This way, you can easily identify the connection and IP addresses used for each VPN connection. Using IPv6, you can also easily identify the connection interface.