WireGuard Site-to-Site VPN with OSPF Dynamic Routing via FRRouting: Automated Multi-Branch Failover on Linux

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

Last November, an upstream fiber cut severed our primary link between Austin and Chicago at 2:14 AM. Twenty engineers working on an overnight PostgreSQL migration suddenly lost access. Worse, our static IPsec tunnels sat idle while engineers scrambled across three gateways to manually rewrite routing tables. That four-hour outage cost us roughly $18,000 in delayed deployment downtime. It also convinced our infrastructure team to ditch static routes for good.

WireGuard pairs cleanly with FRRouting (FRR) running Open Shortest Path First (OSPF). Together, they discover adjacent links, monitor link health with sub-second heartbeats, and re-route traffic without manual intervention. We run this exact setup across our hybrid AWS and bare-metal colocation sites. Failover now happens cleanly in under four seconds with zero packet loss on active TCP sessions.

Quick Start: Minimal Two-Site Setup in Under 10 Minutes

This baseline setup connects two Ubuntu 24.04 gateways (Site A: Austin HQ and Site B: Chicago Branch) over an encrypted point-to-point tunnel with OSPF route exchange.

1. Network Topology and Addressing

  • Site A (HQ): Public WAN 203.0.113.10, Local LAN 10.10.0.0/24, Tunnel IP 10.255.255.1/30
  • Site B (Branch): Public WAN 198.51.100.20, Local LAN 10.20.0.0/24, Tunnel IP 10.255.255.2/30
  • Operating System: Ubuntu 22.04 or 24.04 LTS (kernel 5.15+)

2. Configure the WireGuard Tunnels

First, install WireGuard and generate cryptographic keys on both hosts:

sudo apt update && sudo apt install -y wireguard
umask 077
wg genkey | tee privatekey | wg pubkey > publickey

On Site A (HQ), save the following to /etc/wireguard/wg0.conf. Notice Table = off: this setting prevents wg-quick from overriding your system default gateway when handling catch-all peer definitions.

[Interface]
PrivateKey = <Site_A_PrivateKey>
Address = 10.255.255.1/30
ListenPort = 51820
Table = off
MTU = 1420

[Peer]
PublicKey = <Site_B_PublicKey>
Endpoint = 198.51.100.20:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 15

On Site B (Branch), mirror the configuration inside /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = <Site_B_PrivateKey>
Address = 10.255.255.2/30
ListenPort = 51820
Table = off
MTU = 1420

[Peer]
PublicKey = <Site_A_PublicKey>
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 15

Enable kernel packet forwarding and bring up the tunnels:

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-router.conf
sudo sysctl --system
sudo systemctl enable --now wg-quick@wg0

3. Spin Up FRRouting OSPF

Install the FRR suite and enable the OSPF routing daemon:

sudo apt install -y frr frr-pythontools
sudo sed -i 's/ospfd=no/ospfd=yes/' /etc/frr/daemons
sudo systemctl restart frr

Launch the FRR integrated shell on Site A using sudo vtysh, then enter these configuration directives:

configure terminal
interface wg0
 ip ospf network point-to-point
 ip ospf area 0
exit
router ospf
 ospf router-id 10.255.255.1
 network 10.10.0.0/24 area 0
exit
do write
exit

Now apply the corresponding configuration on Site B inside its own sudo vtysh prompt:

configure terminal
interface wg0
 ip ospf network point-to-point
 ip ospf area 0
exit
router ospf
 ospf router-id 10.255.255.2
 network 10.20.0.0/24 area 0
exit
do write
exit

Confirm neighbor establishment directly from the shell:

sudo vtysh -c "show ip ospf neighbor"

Your neighbor state will transition from Init to Full/ - in under three seconds. Zebra will immediately install the remote subnet (10.20.0.0/24) into the Linux kernel routing table.

Deep Dive: Why WireGuard Breaks Traditional Dynamic Routing

WireGuard does not act like a standard dummy TUN or GRE interface. It operates as a Cryptokey Routing engine. Outgoing packets must match an explicit subnet inside AllowedIPs before WireGuard encrypts them and delivers them to a peer.

The Multicast Problem

OSPF relies heavily on link-local multicast traffic sent to 224.0.0.5 (All OSPF Routers) and 224.0.0.6 (Designated Routers). Many engineers attempt to lock down AllowedIPs to explicit subnets like 10.255.255.2/32, 10.20.0.0/24. WireGuard silently drops all multicast packets in that setup.

The cryptographic handshake succeeds. Counters in wg show increment normally. Yet OSPF neighbors never form. Setting AllowedIPs = 0.0.0.0/0 alongside Table = off solves this hurdle. It allows multicast packets through the crypto-check while delegating actual forwarding decisions to FRR.

Why Point-to-Point Mode Is Non-Negotiable

OSPF defaults to broadcast network semantics on non-serial interfaces. In broadcast mode, OSPF elects a Designated Router (DR) and Backup Designated Router (BDR). Over VPN tunnels, these elections add 40 seconds of unnecessary latency. Any packet jitter can trigger election flapping.

Running ip ospf network point-to-point tells OSPF that exactly two nodes share the link. It skips DR/BDR elections completely. Adjacencies form instantly, and recovery times drop from 40 seconds down to sub-second thresholds.

Advanced Multi-Branch Mesh with Cost-Based Failover

Production environments demand route redundancy. Consider a triangular topology connecting Austin HQ, Chicago Branch 1, and Denver Branch 2. Austin handles heavy traffic across 1 Gbps fiber. A secondary 200 Mbps broadband tunnel directly bridges Chicago and Denver for emergency redundancy.

        +-----------------------+
        |     Austin HQ (HQ)    |
        |      10.10.0.0/24     |
        +-----------------------+
             /              \
   wg0 (Cost 10)          wg1 (Cost 10)
           /                  \
+-------------------+  wg2 (Cost 50)  +-------------------+
|  Chicago (Branch) |=================|  Denver (Branch)  |
|   10.20.0.0/24    |                 |   10.30.0.0/24    |
+-------------------+                 +-------------------+

Tuning Timers and Metric Costs

We assign lower metrics to primary fiber links and higher metrics to the cross-branch tunnel. We also lower hello and dead intervals to speed up failure detection.

On the Chicago gateway, configure interface parameters in vtysh:

configure terminal
interface wg0
 description Uplink-to-Austin-HQ
 ip ospf cost 10
 ip ospf dead-interval 4
 ip ospf hello-interval 1
exit
interface wg2
 description Cross-Branch-Backup-to-Denver
 ip ospf cost 50
 ip ospf dead-interval 4
 ip ospf hello-interval 1
exit
do write

With 1-second hello intervals and a 4-second dead timer, FRR detects link drops in four missed heartbeats. If Austin drops offline, FRR immediately recalculates Shortest Path First (SPF) routes. Traffic routes through wg2 into Denver in 4.2 seconds, avoiding broken TCP sessions on long-lived database queries.

Production Hardening and Troubleshooting

1. Prevent DBD Packet Fragmentation with Safe MTU Sizing

Encapsulating packets inside WireGuard adds 60 bytes of overhead over IPv4 (20-byte IP header + 8-byte UDP header + 32-byte WireGuard header) and 80 bytes over IPv6. When your WAN MTU is 1500, setting a 1500 MTU on wg0 forces packet fragmentation.

Large OSPF Database Description (DBD) packets will fragment during synchronization. Many cloud transit firewalls drop fragmented UDP packets outright. When this happens, OSPF neighbors hang indefinitely in the ExStart or Exchange state. Set your tunnel MTU explicitly to 1420 inside [Interface] to guarantee clean passage across any upstream carrier.

2. Keep NAT Sessions Alive

Gateways running behind AWS NAT Gateways or local enterprise firewalls drop idle UDP session mappings after 30 to 60 seconds. Add PersistentKeepalive = 15 to all remote peer definitions. This setting emits empty encrypted packets every 15 seconds, holding the stateful firewall port open around the clock.

3. Three-Step Incident Triage

When an automated route alert fires, work through your network stack in this exact sequence:

# 1. Verify cryptographic transport and handshake age
sudo wg show wg0 latest-handshakes

# 2. Check routing adjacency and state machines
sudo vtysh -c "show ip ospf neighbor"

# 3. Verify kernel routing table population
ip route show proto ospf

If the handshake is older than 120 seconds, troubleshoot UDP port 51820 reachability first. If the handshake is recent but neighbors stay in ExStart, verify that MTUs match on both sides. When neighbor status reads Full/ -, check ip route show proto ospf to verify zebra has properly populated your Linux kernel tables.

Share: