Context & Why BIRD?
When I started building a more serious HomeLab — five VLANs, six VPS nodes spread across two locations, and three bare-metal machines at home — static routes became a maintenance nightmare almost immediately. Every time I added a subnet or moved a machine, I had to update routes on every node by hand. That gets old fast.
I had already worked with FRRouting (there’s a dedicated FRR guide on this blog), but I kept running into scenarios where BIRD was the better fit. BIRD specifically excels when you need:
- A BGP route reflector inside your network — BIRD’s template-based config makes this dramatically cleaner
- A lightweight daemon that won’t chew through RAM — BIRD2 runs comfortably under 30MB on a quiet network, compared to 200MB+ for some alternatives
- Fine-grained route filtering using BIRD’s own filter language — proper accept/reject/modify logic, not just prefix lists
- Routing skills that transfer to real ISP and IXP environments — BIRD is widely deployed at internet exchanges
BIRD (BIRD Internet Routing Daemon) came out of CZ.NIC, the Czech domain registry, and has been running serious production networks for decades. BIRD2 — the current major version — handles both IPv4 and IPv6 in a single process. I’ve been running this exact setup in production for over a year. Not one unexpected BGP session drop.
The biggest difference from FRR: BIRD uses a single declarative config file with its own syntax — no Cisco-style “configure terminal” workflow. Edit the file, run birdc configure, done. Config diffs are clean and version-controllable.
Installation
Ubuntu / Debian
BIRD2 is in the standard repos on Ubuntu 20.04+ and Debian 11+:
sudo apt update
sudo apt install bird2
The service starts automatically after install. Verify it’s running:
sudo systemctl status bird
bird --version
# BIRD version 2.0.x
RHEL / Rocky Linux / AlmaLinux
On RHEL-family systems, pull it from EPEL:
sudo dnf install epel-release
sudo dnf install bird
On Rocky 9 and AlmaLinux 9, the EPEL package installs cleanly without extra dependencies.
The main config file lives at /etc/bird/bird.conf. Back it up before touching it:
sudo cp /etc/bird/bird.conf /etc/bird/bird.conf.orig
Configuration
Understanding the config structure
A BIRD config file is organized into three logical pieces:
- Global options — router ID, log destination, socket location
- Protocol blocks — one block per routing protocol or system function (kernel, device, static, ospf, bgp…)
- Filter definitions — named route filters applied inside protocol blocks
Here’s the minimal skeleton every config needs:
# /etc/bird/bird.conf
log syslog all;
router id 10.0.0.1; # Use your loopback or primary IP
# Discovers directly connected interfaces
protocol device { }
# Exports direct routes into BIRD's routing table
protocol direct {
ipv4;
ipv6;
}
# Syncs BIRD routes into the Linux kernel
protocol kernel {
ipv4 {
export all;
};
}
The kernel protocol is the bridge between BIRD and the Linux kernel routing table. Without export all, BIRD calculates routes internally but never installs them — one of the most common first-time gotchas.
Configuring OSPF
OSPF is the right choice for a HomeLab where you control all the routers. This config sets up a single OSPF area across two interfaces:
protocol ospf v2 homelab_ospf {
area 0.0.0.0 {
interface "eth0" {
type broadcast;
hello 10;
dead 40;
cost 10;
};
interface "eth1" {
type broadcast;
cost 20; # Higher cost = less preferred path
};
};
ipv4 {
import all;
export where source = RTS_OSPF || source = RTS_STATIC;
};
}
The export line controls what BIRD advertises into OSPF. Here we’re redistributing OSPF-learned routes and static routes — adjust this filter based on what each node should share. For IPv6, replace ospf v2 with ospf v3 and swap ipv4 for ipv6.
Configuring BGP
This is where BIRD’s template system earns its reputation. Instead of duplicating peer config for every neighbor, define a template once and inherit from it:
# Template for all iBGP peers — define once, reuse everywhere
template bgp ibgp_peers {
local as 65001;
hold time 90;
keepalive time 30;
ipv4 {
next hop self;
import all;
export all;
};
}
# Each peer just references the template
protocol bgp homelab_router2 from ibgp_peers {
neighbor 10.0.0.2 as 65001;
description "HomeLab Router 2";
}
protocol bgp homelab_router3 from ibgp_peers {
neighbor 10.0.0.3 as 65001;
description "HomeLab Router 3";
}
If you’re running a route reflector — one central node that redistributes routes to all others, eliminating the need for a full mesh — add two lines to the template:
template bgp ibgp_rr_client {
local as 65001;
rr client; # Treat connecting peers as RR clients
rr cluster id 1;
next hop self;
ipv4 {
import all;
export all;
};
}
Each client connects only to the route reflector, not to every other peer. Worth doing the math: six routers in a full mesh require 15 separate BGP sessions. With a route reflector, that drops to 5. A serious simplification.
Route filters
The filter language is where BIRD really separates itself. This filter only accepts routes from RFC1918 10.x.x.x space and rejects everything else:
filter only_homelab_routes {
if net ~ [ 10.0.0.0/8+ ] then accept;
reject;
}
protocol bgp homelab_router2 from ibgp_peers {
neighbor 10.0.0.2 as 65001;
ipv4 {
import filter only_homelab_routes;
export all;
};
}
The 10.0.0.0/8+ notation means “10.0.0.0/8 and any more-specific prefix” — it covers every subnet under 10.0.0.0/8 without listing them individually.
Applying config changes
After editing the config file, reload without dropping sessions:
sudo birdc configure
BIRD validates the syntax before applying. If there’s an error, the existing config stays active and the error is reported — no service disruption from a typo.
Verification & Monitoring
The birdc interactive shell
birdc is BIRD’s control interface — the equivalent of a router’s CLI:
sudo birdc
Key commands inside the shell:
bird> show status
bird> show protocols
bird> show protocols all
bird> show route
bird> show route table master4
bird> show ospf topology
bird> show ospf neighbors
bird> show bgp sessions
For scripting and monitoring, birdc accepts commands directly without entering the interactive shell:
sudo birdc show protocols
sudo birdc show route count
Checking BGP session state
sudo birdc show protocols
A healthy BGP session looks like this:
Name Proto Table State Since Info
homelab_router2 BGP --- up 2026-07-01 Established
If you see Active or Connect instead of Established, the TCP connection isn’t completing. First thing to check: firewall rules on both ends. BGP runs on TCP port 179 — both sides must allow inbound connections from the neighbor IP.
Checking OSPF adjacency
sudo birdc show ospf neighbors
Router ID Pri State DTime Interface Router ID
10.0.0.2 1 Full/DR 00:32 eth0 10.0.0.2
Full/DR means the adjacency is fully established. Stuck at 2-Way or ExStart? Check that both ends are in the same OSPF area and that hello/dead intervals match exactly.
Verifying route installation in the kernel
Confirm that BIRD is actually pushing routes into the Linux routing table:
ip route show
# Routes learned via BIRD will appear with their correct metrics
# Check a specific prefix:
ip route show 10.0.2.0/24
Real-time log monitoring
sudo journalctl -u bird -f
For debugging a specific BGP session, add debug all to the protocol block temporarily. Fair warning: it can generate hundreds of megabytes of logs per hour on an active session. Remove it before you forget:
protocol bgp homelab_router2 from ibgp_peers {
neighbor 10.0.0.2 as 65001;
debug all; # Remove after debugging
}
Simple health check for monitoring
I keep this one-liner in my monitoring stack to catch BGP session drops:
sudo birdc show protocols | grep BGP | grep -v Established \
&& echo "WARNING: BGP SESSION DOWN" \
|| echo "OK: All BGP sessions established"
Drop it into a cron job or a Prometheus textfile collector and you’ll be paged within minutes of a session going down.
Getting your head around BIRD’s config model takes maybe a weekend. After that it pays back fast. A full route reflector with filter policies fits comfortably in under 100 lines of config — the same setup in Cisco IOS would be three times that, and impossible to diff cleanly. Six months from now, when you need to add a peer or adjust a filter, the config file will still make sense without re-learning it from scratch.

