<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Yet Another SysAdmin Website - Bird</title><link>/tags/bird/</link><description>Recent content in Bird on Yet Another SysAdmin Website</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>2026-04-10</lastBuildDate><atom:link href="/tags/bird/index.xml" rel="self" type="application/rss+xml"/><item><title>Toying with OSPF over Wireguard on FreeBSD</title><link>/blog/2026/04/10/toying-with-ospf-over-wireguard-on-freebsd/</link><pubDate>2026-04-10</pubDate><guid>/blog/2026/04/10/toying-with-ospf-over-wireguard-on-freebsd/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="routing-ospf-over-wireguard"&gt;Routing OSPF over Wireguard&lt;/h2&gt;
&lt;p&gt;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&amp;rsquo;s routing table with static routes to the tunnel for each
allowed prefix.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Also since I need to route traffic for dynamically learned routes, the allowed
IP filters pretty much need to be &lt;code&gt;0.0.0.0/0, ::/0&lt;/code&gt; and cover everything. In
practice I could also use the more restrictive &lt;code&gt;10/8, 172.16/12, 192.168/16, 224.0.0.5, 224.0.0.6, fd00::/8, fe80::/10, ff02::5, ff02::6&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;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 &lt;code&gt;wg-quick&lt;/code&gt; method, one needs to add &lt;code&gt;Table = off&lt;/code&gt; to the &lt;code&gt;[Interface]&lt;/code&gt;
configuration. The native implementations do not need this, it is really only
for &lt;code&gt;wg-quick&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My FreeBSD &lt;code&gt;/etc/start_if.wg0&lt;/code&gt; config therefore looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ifconfig wg0 create up description &lt;span class="s2"&gt;&amp;#34;laptop&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ifconfig wg0 inet 172.31.254.0/31
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ifconfig wg0 inet6 fd00:172:31:254::/127
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ifconfig wg0 inet6 fe80::/64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wg syncconf wg0 /etc/wg0.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the corresponding &lt;code&gt;/etc/wg0.conf&lt;/code&gt; looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Interface&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PrivateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; gFAoJwJSfA0A8g+FhfxHLWXj6+CZdmPH4EW5pghqv30&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ListenPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;342&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Peer&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PublicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; bAg8bmMQxPFoNSG+Qq2VteCW4VUsi//VleBQBNa5Mno&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;AllowedIPs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 0.0.0.0/0,::/0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="freebsd-vs-linux-bridge-and-wireguard-interfaces"&gt;FreeBSD vs Linux bridge and wireguard interfaces&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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!&lt;/p&gt;
&lt;p&gt;Luckily these are in effect point-to-point interfaces, so it matters very little
which link-local address I set manually. I just used &lt;code&gt;fe80::/64&lt;/code&gt; everywhere and
it worked.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="some-pf-configuration-rules"&gt;Some PF configuration rules&lt;/h2&gt;
&lt;p&gt;While not something I learned this time around, I will record here that one can
allow OSPF traffic via PF with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table &amp;lt;myself&amp;gt; const &lt;span class="o"&gt;{&lt;/span&gt; self &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table &amp;lt;private&amp;gt; const &lt;span class="o"&gt;{&lt;/span&gt; 10/8, 172.16/12, 192.168/16, fd00::/8, fe80::/10 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pass out from &amp;lt;myself&amp;gt; to any
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pass on &lt;span class="o"&gt;{&lt;/span&gt; wg0, wg1 &lt;span class="o"&gt;}&lt;/span&gt; from &amp;lt;private&amp;gt; to &amp;lt;private&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pass on &lt;span class="o"&gt;{&lt;/span&gt; wg0, wg1 &lt;span class="o"&gt;}&lt;/span&gt; proto ospf allow-opts
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="some-bird-configuration"&gt;Some Bird configuration&lt;/h2&gt;
&lt;p&gt;Here is a basic Bird3 configuration to reproduce my setup:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;log syslog all&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;router id 172.31.253.1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol device &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol direct &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; disabled&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ipv4&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ipv6&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol kernel &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ipv4 &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;export&lt;/span&gt; all&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; persist&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol kernel &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ipv6 &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;export&lt;/span&gt; all&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; persist&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol static &lt;span class="o"&gt;{&lt;/span&gt; ipv4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol ospf v2 ospf4 &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; area &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;bridge0&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; stub&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;wg0&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; bfd yes&lt;span class="p"&gt;;&lt;/span&gt; cost 100&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; ptp&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;wg1&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; bfd yes&lt;span class="p"&gt;;&lt;/span&gt; cost 100&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; ptp&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol ospf v3 ospf6 &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; area &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;bridge0&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; stub&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;wg0&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; bfd yes&lt;span class="p"&gt;;&lt;/span&gt; cost 100&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; ptp&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;wg1&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; bfd yes&lt;span class="p"&gt;;&lt;/span&gt; cost 100&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; ptp&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protocol bfd &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; interface &lt;span class="s2"&gt;&amp;#34;wg*&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;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 &amp;ldquo;fun&amp;rdquo; to redistribute routes this way.&lt;/p&gt;</description></item></channel></rss>