Toying with OSPF over Wireguard on FreeBSD
2026-04-10 - Some lessons learned
Tags: Bird FreeBSD OSPF Wireguard
Introduction
For almost a decade, I have been operating my own mesh overlay network using a combination of point to point OpenVPN tunnels with OSPF on top. It was well automated with Ansible and served me well all these years.
I decided it was time I tried to move this to Wireguard instead and spent a bit of time making it all work. This article does not explain everything I did, but will present some lessons I learned in the process.
Routing OSPF over Wireguard
By default, Wireguard encourages you to only allow the minimum set of IP prefixes over a Wireguard tunnel. It filters only that traffic, and conveniently populates a host’s routing table with static routes to the tunnel for each allowed prefix.
Wireguard also supports configuring multiple peers for a single Wireguard interface which is very convenient, but sadly cannot work with OSPF. Wireguard uses the allowed IP prefixes to decide which peer will receive a packet, and this is incompatible with a protocol that relies on multicast traffic in order to work. I therefore need one Wireguard interface per peer over which to run OSPF.
Also since I need to route traffic for dynamically learned routes, the allowed
IP filters pretty much need to be 0.0.0.0/0, ::/0 and cover everything. In
practice I could also use the more restrictive 10/8, 172.16/12, 192.168/16, 224.0.0.5, 224.0.0.6, fd00::/8, fe80::/10, ff02::5, ff02::6.
Since I cannot have Wireguard insert multiple identical routes for all my
Wireguard interfaces, I also need to disable this functionality. When using the
popular wg-quick method, one needs to add Table = off to the [Interface]
configuration. The native implementations do not need this, it is really only
for wg-quick.
My FreeBSD /etc/start_if.wg0 config therefore looks like:
ifconfig wg0 create up description "laptop"
ifconfig wg0 inet 172.31.254.0/31
ifconfig wg0 inet6 fd00:172:31:254::/127
ifconfig wg0 inet6 fe80::/64
wg syncconf wg0 /etc/wg0.conf
And the corresponding /etc/wg0.conf looks like:
[Interface]
PrivateKey = gFAoJwJSfA0A8g+FhfxHLWXj6+CZdmPH4EW5pghqv30=
ListenPort = 342
[Peer]
PublicKey = bAg8bmMQxPFoNSG+Qq2VteCW4VUsi//VleBQBNa5Mno=
AllowedIPs = 0.0.0.0/0,::/0
FreeBSD vs Linux bridge and wireguard interfaces
On FreeBSD, I had a little trouble making OSPF v3 (for IPv6) work on a detached bridge interface (that I use for VNET jails), and on a wireguard interface.
The problem was that FreeBSD does not generate link-local IPv6 addresses for these kinds of interfaces, because they are not backed by a MAC address. And it turns out that the Bird routing daemon needs these link-local IPv6 addresses in order to work in point-to-point mode!
Luckily these are in effect point-to-point interfaces, so it matters very little
which link-local address I set manually. I just used fe80::/64 everywhere and
it worked.
Linux does not have this issue and will assign random link-local addresses to all its interfaces which really made this a confusing debugging session.
Some PF configuration rules
While not something I learned this time around, I will record here that one can allow OSPF traffic via PF with:
table <myself> const { self }
table <private> const { 10/8, 172.16/12, 192.168/16, fd00::/8, fe80::/10 }
pass out from <myself> to any
pass on { wg0, wg1 } from <private> to <private>
pass on { wg0, wg1 } proto ospf allow-opts
Some Bird configuration
Here is a basic Bird3 configuration to reproduce my setup:
log syslog all;
router id 172.31.253.1;
protocol device {
}
protocol direct {
disabled;
ipv4;
ipv6;
}
protocol kernel {
ipv4 { export all; };
persist;
}
protocol kernel {
ipv6 { export all; };
persist;
}
protocol static { ipv4; }
protocol ospf v2 ospf4 {
area 0 {
interface "bridge0" { stub; };
interface "wg0" { bfd yes; cost 100; type ptp; };
interface "wg1" { bfd yes; cost 100; type ptp; };
};
}
protocol ospf v3 ospf6 {
area 0 {
interface "bridge0" { stub; };
interface "wg0" { bfd yes; cost 100; type ptp; };
interface "wg1" { bfd yes; cost 100; type ptp; };
};
}
protocol bfd {
interface "wg*" {};
}
Conclusion
I cannot say yet if I will rewrite my Ansible automation for OSPF over Wireguard, but I certainly had fun figuring these out. I will probably toy with BGP again next, just because it is also “fun” to redistribute routes this way.