BIRD & BGP Masterclass
This guide goes deep on BIRD — the routing daemon used by every major Internet Exchange Point on the planet — and on BGP, the protocol that holds the internet together. If you have read the Networking tutorial and understand how FRRouting works on kldload, this is the next step: stop treating BGP as something that only telecoms touch, and start running it as the backbone of your own multi-site, multi-cloud infrastructure.
BGP is not a carrier protocol. It is your protocol. Every ISP, cloud provider, CDN, and content network on the internet exchanges reachability information using BGP. AWS, GCP, Azure, and Cloudflare all speak BGP on their interconnects. Your kldload node running BIRD speaks the exact same protocol. The endpoint IP and ASN change. The wire protocol is identical.
Prerequisites: the Networking tutorial (FRRouting, VXLAN, eBPF dataplane basics) and a kldload node with WireGuard configured. Multi-site examples require three nodes, but you can follow along with a single node and a simulated peer.
1. BGP Is the Protocol That Runs the Internet
BGP (Border Gateway Protocol) is the routing protocol of the public internet. Roughly 80,000 autonomous systems — ISPs, enterprises, cloud providers, universities, CDNs — use BGP to tell each other which IP prefixes they can reach and how to get there. When you type a domain into a browser, DNS resolves it to an IP, and BGP is what tells every router between you and that IP which direction to forward the packet.
Your Linux box speaks this exact protocol. BIRD is a production-grade BGP daemon that runs on commodity hardware, inside containers, inside VMs, and on bare metal kldload nodes. The same software that IXPs use to run route servers for hundreds of peers can run on a Raspberry Pi.
You do not need to be a carrier to need BGP. Two sites connected by WireGuard? BGP handles route distribution. Two upstream ISPs for failover? BGP manages path selection. A Kubernetes cluster announcing LoadBalancer IPs to your LAN router? BGP does that too. The protocol scales from two nodes to the global routing table. Start small. The concepts are the same.
Autonomous System (AS)
A network under a single administrative policy, identified by an ASN (Autonomous System Number). AWS is AS16509. Cloudflare is AS13335. Your home lab can be AS64512. Every BGP peer belongs to an AS.
Prefix
A network block announced via BGP. AWS announces its EC2 ranges. Your site announces its LAN subnet. BGP routes are prefixes — not individual IPs, not host routes. Think /8 to /24 for IPv4.
Path attributes
Every BGP route carries metadata: AS_PATH (list of ASes the route transited), NEXT_HOP (where to forward packets), MED (metric for path selection), LOCAL_PREF (local preference within your network), and communities (arbitrary tags).
TCP 179
BGP runs over TCP port 179. Unlike OSPF or IS-IS (which use raw IP multicast), BGP is a reliable, connection-oriented protocol. Sessions survive brief packet loss. You can firewall everything except TCP 179 between peers.
2. BIRD vs FRRouting
kldload's networking tutorial uses FRRouting because the
vtysh CLI mirrors Cisco IOS and Juniper JunOS — immediately familiar to network engineers
coming from enterprise gear. BIRD takes a different approach: the entire daemon is configured
via a single text file with a clean, declarative syntax. No CLI session to connect to, no
running-config vs startup-config distinction. The file is the truth.
| Feature | BIRD | FRRouting |
|---|---|---|
| Architecture | Single binary, single config file | Suite of daemons (bgpd, ospfd, etc.) + management daemon |
| Config style | Declarative text file (/etc/bird/bird.conf) |
Interactive CLI (vtysh) or text config per daemon |
| Config management | Excellent — template the file, reload BIRD | Possible but awkward — CLI commands don't template cleanly |
| Memory footprint | ~10–30 MB for a full BGP table | ~50–150 MB depending on protocols enabled |
| BGP communities | Standard, extended, large — all supported | Standard, extended, large — all supported |
| MPLS / SR | Basic support (BIRD 2.x) | Mature MPLS, IS-IS, Segment Routing support |
| IXP / route server use | Dominant — most IXPs run BIRD | Common in service provider edge routers |
| Diagnostic CLI | birdc — socket-based, scriptable |
vtysh — Cisco IOS-like interactive shell |
| Best for | Automation, IXP peering, compact deployments | Engineers with Cisco/Juniper background, MPLS networks |
3. Install BIRD
BIRD 2.x is the current stable series. BIRD 1.x is end-of-life. Always install BIRD 2.
CentOS Stream 9 / RHEL 9 / Rocky Linux 9 / Fedora
# Enable EPEL (CentOS/RHEL/Rocky — Fedora has it in the main repo)
dnf install -y epel-release
# Install BIRD
dnf install -y bird2
# Start and enable
systemctl enable --now bird
# Verify
systemctl status bird
birdc show status
Debian 13 / Ubuntu 24.04
# Install BIRD
apt-get install -y bird2
# Start and enable
systemctl enable --now bird
# Verify
systemctl status bird
birdc show status
Config file location
/etc/bird/bird.conf # main config
/etc/bird/bird6.conf # IPv6 config (BIRD 1.x legacy — not needed in BIRD 2)
# Reload config without restarting the daemon (preserves BGP sessions)
birdc configure
# Full restart (drops all sessions — use sparingly)
systemctl restart bird
birdc configure whenever possible — it re-reads the config and applies changes without tearing down BGP sessions. A full restart will drop all your peering sessions and trigger route convergence at all your peers, which takes 10–90 seconds. On a production network, this is noticeable. birdc configure keeps sessions alive and applies changes gracefully.4. BIRD Config Fundamentals
BIRD's config is a single declarative file. You define your router ID, connect protocols to the kernel routing table, and write filters that decide which routes are accepted or announced. There is no running config separate from the file — the file is always authoritative.
The routing table model
BIRD maintains its own internal routing table (separate from the kernel's). Protocols export
routes into BIRD's table and import routes from it. Filters sit at both ends of that pipeline,
accepting or rejecting routes based on your policy. The protocol kernel syncs BIRD's table
to the kernel's routing table so the OS actually uses the routes.
protocol kernel
Syncs routes between BIRD's internal table and the kernel's routing table. Without this, BIRD knows about routes but the OS doesn't use them. The export all directive pushes all BIRD routes into the kernel.
protocol device
Reads the list of network interfaces from the kernel. Required for BIRD to know which interfaces exist. No routes are generated — this is purely discovery.
protocol direct
Generates routes for directly connected networks — the prefixes assigned to your interfaces. Without this, BIRD has no local routes to redistribute to peers.
Filters
Functions that accept or reject routes at protocol import/export time. A filter can check prefix, prefix length, ASN path, community tags, next-hop, and more. Filters are the policy layer.
Minimal working config
# /etc/bird/bird.conf — minimal config that exports connected routes
# Router ID — must be a unique IPv4 address (even on IPv6-only hosts)
router id 10.0.1.1;
# Log to syslog
log syslog all;
# Sync routes to/from kernel routing table
protocol kernel {
ipv4 {
export all; # push all BIRD routes into kernel
import all; # pull kernel routes into BIRD
};
}
# Learn interface names and addresses from the kernel
protocol device {
scan time 10; # re-scan interfaces every 10 seconds
}
# Generate routes for directly connected subnets
protocol direct {
ipv4;
}
After saving this and running birdc configure, run birdc show route — you will see your
directly connected subnets in BIRD's table, and they will be reflected in ip route show.
/etc/bird/bird.conf and runs birdc configure can manage routing policy across 50 sites as easily as one. That is not something you can do cleanly with FRRouting's interactive CLI model.5. BGP Peering — The Basics
eBGP vs iBGP
eBGP (external BGP) is a session between routers in different ASes. This is how your site peers with your ISP, how cloud providers peer with each other at IXPs, and how your kldload sites peer with each other over WireGuard. eBGP peers are typically (but not always) directly connected — one hop away.
iBGP (internal BGP) is a session between routers in the same AS. iBGP is how you propagate external routes through your network without re-announcing them to the outside world. iBGP peers do not modify the AS_PATH when relaying routes — the route looks the same as it did when it entered your AS.
Example: peer with an upstream router
# /etc/bird/bird.conf — eBGP session with upstream ISP/router
router id 10.0.1.1;
log syslog all;
protocol kernel {
ipv4 { export all; import all; };
}
protocol device { scan time 10; }
protocol direct { ipv4; }
# Simple filter: accept all routes (we will tighten this in section 6)
filter accept_all {
accept;
}
# eBGP session with upstream router at 10.0.1.254
protocol bgp upstream {
local 10.0.1.1 as 64512;
neighbor 10.0.1.254 as 64511;
ipv4 {
import filter accept_all; # accept routes from peer
export filter accept_all; # announce routes to peer
};
# Hold timer and keepalive (seconds)
hold time 90;
keepalive time 30;
}
Example: peer with another kldload node over WireGuard
# Site A: 10.0.1.1, ASN 64512, WireGuard interface wg0, peer IP 10.200.0.2
# Site B: 10.0.2.1, ASN 64513, WireGuard interface wg0, peer IP 10.200.0.1
# /etc/bird/bird.conf on Site A
router id 10.0.1.1;
log syslog all;
protocol kernel {
ipv4 { export all; import all; };
}
protocol device { scan time 10; }
protocol direct { ipv4; }
filter accept_all { accept; }
protocol bgp site_b {
local 10.200.0.1 as 64512; # our WireGuard IP, our ASN
neighbor 10.200.0.2 as 64513; # their WireGuard IP, their ASN
ipv4 {
import filter accept_all;
export filter accept_all;
};
}
# /etc/bird/bird.conf on Site B
router id 10.0.2.1;
log syslog all;
protocol kernel {
ipv4 { export all; import all; };
}
protocol device { scan time 10; }
protocol direct { ipv4; }
filter accept_all { accept; }
protocol bgp site_a {
local 10.200.0.2 as 64513;
neighbor 10.200.0.1 as 64512;
ipv4 {
import filter accept_all;
export filter accept_all;
};
}
# Verify session on Site A after both configs are applied
birdc show protocols
# Name Proto Table State Since Info
# site_b BGP --- up 2026-04-01 Established
birdc show route
# 10.0.2.0/24 via 10.200.0.2 on wg0 [site_b ...] * (100/0) [AS64513i]
# 10.200.0.0/24 dev wg0 [direct ...] * (240)
# 10.0.1.0/24 dev eth0 [direct ...] * (240)
Multihop eBGP
# When the peer is more than one hop away (common with loopback-based peering)
protocol bgp remote_site {
local 10.0.1.1 as 64512;
neighbor 192.168.100.1 as 64514;
multihop 3; # allow up to 3 hops between peers
...
}
6. Route Filters — The Most Important Part
Route filters are not optional. They are the single most important part of any BGP configuration. A BGP session with no filters is an open invitation for your peers to install arbitrary routes in your routing table, or for you to accidentally announce prefixes you should not be announcing.
The cardinal rule of BGP: always filter. Never accept or announce unfiltered.
The Facebook outage in October 2021 was caused by a BGP misconfiguration that withdrew Facebook's own prefixes from the global routing table — an unflitered announcement that made the entire network unreachable from the outside. The Pakistan Telecom YouTube hijack in 2008 was caused by a more-specific route (a /24 covering a YouTube /22) being accepted and propagated by upstream providers with no origin validation. The Cloudflare route leak in 2019 sent traffic for thousands of networks through a small ISP in Pennsylvania, causing outages across a large chunk of the internet. Every one of these events was preventable with proper BGP filters.
Filter syntax
# A BIRD filter is a function that terminates with accept or reject
# It has access to route attributes: net (prefix), bgp_path, bgp_community, etc.
filter accept_rfc1918 {
if net ~ [ 10.0.0.0/8+, 172.16.0.0/12+, 192.168.0.0/16+ ] then accept;
reject;
}
# The + after a prefix means "this prefix or any more-specific within it"
# 10.0.0.0/8+ matches 10.0.0.0/8, 10.1.0.0/16, 10.100.200.0/24, etc.
filter reject_default {
if net = 0.0.0.0/0 then reject;
accept;
}
# Accept only /24 or shorter (prevent host routes leaking in)
filter accept_reasonable_prefixes {
if net.len > 24 then reject; # reject /25, /26, /27, etc.
if net.len < 8 then reject; # reject implausibly large prefixes
accept;
}
# Reject routes with our own ASN in the path (loop detection)
filter reject_our_own_asn {
if bgp_path ~ [ * 64512 * ] then reject;
accept;
}
Combining filters for a production import policy
# A robust import filter for a site-to-site peer
filter import_from_peer {
# Reject default route
if net = 0.0.0.0/0 then reject;
# Only accept RFC1918 space (our private network)
if net !~ [ 10.0.0.0/8+, 172.16.0.0/12+, 192.168.0.0/16+ ] then reject;
# Reject routes that are too specific (potential route hijack)
if net.len > 24 then reject;
# Reject routes with our own ASN in the path
if bgp_path ~ [ * 64512 * ] then reject;
accept;
}
protocol bgp site_b {
local 10.200.0.1 as 64512;
neighbor 10.200.0.2 as 64513;
ipv4 {
import filter import_from_peer; # strict import
export where net ~ 10.0.1.0/24; # only announce our own subnet
};
}
Prefix lists
# Define a named prefix list for cleaner config
define MY_PREFIXES = [ 10.0.1.0/24, 10.0.2.0/24 ];
define PEER_PREFIXES = [ 10.0.3.0/24, 10.0.4.0/24 ];
filter export_my_prefixes {
if net ~ MY_PREFIXES then accept;
reject;
}
filter import_peer_prefixes {
if net ~ PEER_PREFIXES then accept;
reject;
}
7. BGP Communities
BGP communities are tags you attach to routes. They are how you encode policy decisions into the routing announcement itself — without modifying the prefix or the path. A route can carry multiple communities. Peers can read them and make forwarding decisions based on them. Communities are how you tell an upstream provider what to do with your routes without picking up the phone.
Standard communities (format: ASN:value)
# Standard community: 32-bit value, written as two 16-bit fields
# Format: :
#
# Well-known communities (RFC1997):
# 65535:0 NO_EXPORT — don't announce beyond your AS boundary
# 65535:1 NO_ADVERTISE — don't announce to any peer
# 65535:2 NO_EXPORT_SUBCONFED — don't export to sub-AS peers
# In BIRD, set a community on a route:
filter tag_site_a_routes {
bgp_community.add((64512, 100)); # tag as "from site-a"
accept;
}
# In BIRD, match on a community:
filter prefer_site_a {
if (64512, 100) ~ bgp_community then {
bgp_local_pref = 200; # prefer this path
}
accept;
}
Large communities (format: ASN:value1:value2)
# Large communities (RFC8092) use three 32-bit integers
# Format: ::
# Designed for 32-bit ASNs and richer semantics
# Example: tag a route with origin site and intended region
filter tag_with_large_community {
bgp_large_community.add((64512, 1, 1)); # (our-asn, site=1, region=1)
accept;
}
# Match on large community
filter handle_large_community {
if (64512, 1, 1) ~ bgp_large_community then {
bgp_local_pref = 150;
}
accept;
}
Full example: tag routes by origin site, use communities for path selection
# Three sites: site-a (primary), site-b (secondary), site-c (backup)
# ASNs: site-a=64512, site-b=64513, site-c=64514
# Community convention: (64512, 10) = primary path, (64512, 20) = secondary
# On site-a: tag outgoing routes as primary
filter export_primary {
bgp_community.add((64512, 10)); # mark as primary
if net ~ [ 10.0.1.0/24 ] then accept;
reject;
}
# On site-b: tag outgoing routes as secondary
filter export_secondary {
bgp_community.add((64512, 20)); # mark as secondary
if net ~ [ 10.0.1.0/24 ] then accept;
reject;
}
# On site-c: use communities to set LOCAL_PREF on imports
# Higher LOCAL_PREF = preferred path
filter import_with_community_policy {
if (64512, 10) ~ bgp_community then bgp_local_pref = 200;
if (64512, 20) ~ bgp_community then bgp_local_pref = 100;
accept;
}
8. Multi-Site BGP over WireGuard
This is the architecture used in production kldload multi-site deployments: three sites, each with its own private ASN and local subnet, connected by WireGuard tunnels, announcing their local subnets via eBGP. Every site learns every other site's routes automatically. No static routes. No manual route table entries.
Network topology
# Site A: AS64512, LAN 10.0.1.0/24, WireGuard IP 10.200.0.1
# Site B: AS64513, LAN 10.0.2.0/24, WireGuard IP 10.200.0.2
# Site C: AS64514, LAN 10.0.3.0/24, WireGuard IP 10.200.0.3
#
# WireGuard mesh: each site has tunnels to both other sites
# Site A peers: wg-b (10.200.0.2), wg-c (10.200.0.3)
# Site B peers: wg-a (10.200.0.1), wg-c (10.200.0.3)
# Site C peers: wg-a (10.200.0.1), wg-b (10.200.0.2)
#
# BGP sessions run over the WireGuard tunnel IPs
# Route propagation: A announces 10.0.1.0/24, B announces 10.0.2.0/24, etc.
Site A config
# /etc/bird/bird.conf — Site A (AS64512)
router id 10.0.1.1;
log syslog all;
protocol kernel {
ipv4 { export all; import none; }; # push BIRD routes to kernel, don't re-import
}
protocol device { scan time 10; }
protocol direct { ipv4; }
# Our local subnet
define MY_NET = 10.0.1.0/24;
# Import filter: accept RFC1918 from peers, no default, no too-specific
filter import_from_peer {
if net = 0.0.0.0/0 then reject;
if net !~ [ 10.0.0.0/8+ ] then reject;
if net.len > 24 then reject;
if bgp_path ~ [ * 64512 * ] then reject; # reject if our ASN is already in path
accept;
}
# Export filter: only announce our own subnet
filter export_to_peer {
bgp_community.add((64512, 10)); # tag as primary
if net ~ MY_NET then accept;
reject;
}
# Session to Site B
protocol bgp site_b {
local 10.200.0.1 as 64512;
neighbor 10.200.0.2 as 64513;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
# Session to Site C
protocol bgp site_c {
local 10.200.0.1 as 64512;
neighbor 10.200.0.3 as 64514;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
Site B config
# /etc/bird/bird.conf — Site B (AS64513)
router id 10.0.2.1;
log syslog all;
protocol kernel {
ipv4 { export all; import none; };
}
protocol device { scan time 10; }
protocol direct { ipv4; }
define MY_NET = 10.0.2.0/24;
filter import_from_peer {
if net = 0.0.0.0/0 then reject;
if net !~ [ 10.0.0.0/8+ ] then reject;
if net.len > 24 then reject;
if bgp_path ~ [ * 64513 * ] then reject;
accept;
}
filter export_to_peer {
bgp_community.add((64513, 10));
if net ~ MY_NET then accept;
reject;
}
protocol bgp site_a {
local 10.200.0.2 as 64513;
neighbor 10.200.0.1 as 64512;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
protocol bgp site_c {
local 10.200.0.2 as 64513;
neighbor 10.200.0.3 as 64514;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
Site C config
# /etc/bird/bird.conf — Site C (AS64514) — same pattern
router id 10.0.3.1;
log syslog all;
protocol kernel {
ipv4 { export all; import none; };
}
protocol device { scan time 10; }
protocol direct { ipv4; }
define MY_NET = 10.0.3.0/24;
filter import_from_peer {
if net = 0.0.0.0/0 then reject;
if net !~ [ 10.0.0.0/8+ ] then reject;
if net.len > 24 then reject;
if bgp_path ~ [ * 64514 * ] then reject;
accept;
}
filter export_to_peer {
bgp_community.add((64514, 10));
if net ~ MY_NET then accept;
reject;
}
protocol bgp site_a {
local 10.200.0.3 as 64514;
neighbor 10.200.0.1 as 64512;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
protocol bgp site_b {
local 10.200.0.3 as 64514;
neighbor 10.200.0.2 as 64513;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
Verify route propagation
# On Site A — confirm you see both remote subnets
birdc show protocols
# site_b BGP --- up Established
# site_c BGP --- up Established
birdc show route
# 10.0.1.0/24 dev eth0 [direct ...] * (240)
# 10.0.2.0/24 via 10.200.0.2 on wg0 [site_b ...] * (100/0) [AS64513i]
# 10.0.3.0/24 via 10.200.0.3 on wg0 [site_c ...] * (100/0) [AS64514i]
# Confirm kernel routing table matches
ip route show
# 10.0.1.0/24 dev eth0 proto bird ...
# 10.0.2.0/24 via 10.200.0.2 dev wg0 proto bird ...
# 10.0.3.0/24 via 10.200.0.3 dev wg0 proto bird ...
# Test end-to-end connectivity
ping -c3 10.0.2.1 # from Site A to Site B's LAN gateway
ping -c3 10.0.3.1 # from Site A to Site C's LAN gateway
9. BGP with Cloud Providers
Every cloud interconnect speaks BGP. AWS Direct Connect, GCP Cloud Router, and Azure ExpressRoute all peer with your on-premises router using the same protocol you have been configuring throughout this guide. The ASN and endpoint IP change. The BIRD config pattern is identical to peering with another kldload site.
AWS VPN + BGP
AWS Site-to-Site VPN supports BGP when you use dynamic routing. AWS assigns you a BGP tunnel IP and gives you their ASN (7224). Your kldload node peers with two AWS BGP endpoints (one per tunnel, for HA) and announces your on-prem prefixes.
GCP Cloud Router
GCP Cloud Interconnect and Cloud VPN (HA) both use Cloud Router for BGP. You specify your ASN in Cloud Router config, and GCP peers with your on-prem BGP speaker. GCP's ASN is 16550 for interconnects.
Azure ExpressRoute
ExpressRoute uses two BGP sessions (primary and secondary) over Microsoft's MSEE routers. Azure's ASN for public peering is 12076. Private peering uses AS65515. You need two /30s for peering IPs.
Prefix size rules
Cloud providers enforce prefix length limits. AWS and Azure reject prefixes longer than /24. GCP rejects longer than /24 on Cloud Interconnect. Announcing a /25 or /27 will be silently rejected. Aggregate your routes.
Example: announce on-prem subnet to AWS over Site-to-Site VPN
# AWS Site-to-Site VPN gives you:
# - Two tunnel public IPs (for the IPsec tunnels)
# - Two BGP peer IPs inside the tunnels (e.g., 169.254.100.2 and 169.254.101.2)
# - AWS BGP ASN: 7224
# - Your tunnel inside IPs (e.g., 169.254.100.1 and 169.254.101.1)
#
# On kldload: configure the IPsec tunnels using strongSwan or libreswan,
# then add BIRD sessions on the tunnel interfaces.
# /etc/bird/bird.conf — AWS BGP over Site-to-Site VPN
router id 10.0.1.1;
log syslog all;
protocol kernel { ipv4 { export all; import none; }; }
protocol device { scan time 10; }
protocol direct { ipv4; }
# Only announce our on-prem subnet
define ON_PREM = 10.0.1.0/24;
# Only accept our VPC CIDR from AWS
define AWS_VPC = 172.31.0.0/16;
filter import_from_aws {
if net ~ AWS_VPC then accept;
reject;
}
filter export_to_aws {
if net ~ ON_PREM then accept;
reject;
}
# BGP session over VPN tunnel 1 (primary)
protocol bgp aws_tunnel1 {
local 169.254.100.1 as 65000; # your ASN
neighbor 169.254.100.2 as 7224; # AWS BGP ASN
multihop 2;
ipv4 {
import filter import_from_aws;
export filter export_to_aws;
next hop self; # required for tunnel-based peering
};
}
# BGP session over VPN tunnel 2 (secondary — HA)
protocol bgp aws_tunnel2 {
local 169.254.101.1 as 65000;
neighbor 169.254.101.2 as 7224;
multihop 2;
ipv4 {
import filter import_from_aws;
export filter export_to_aws;
next hop self;
};
}
10. Announcing Kubernetes Services (Cilium + BIRD)
Cilium's BGP speaker can announce Kubernetes LoadBalancer service IPs directly to your LAN router — no MetalLB required, no NodePort hacks. The question is which BGP speaker is authoritative: Cilium's built-in speaker, or BIRD on the host. Both approaches are valid. The right one depends on your architecture.
Option 1: Cilium BGP speaker only
Cilium peers directly with your LAN router or upstream BIRD instance. Cilium announces LoadBalancer IPs when they are assigned. No BIRD on the host. Simplest setup for a single cluster.
Option 2: BIRD on host redistributes Cilium routes
Cilium peers with BIRD on the local host (loopback). BIRD then redistributes those service IPs to the rest of your BGP mesh. Use this when you have a multi-site BIRD mesh and want Kubernetes services visible across all sites.
Option 1: Cilium BGP policy to peer with BIRD on the host
# In your kldload cluster — Cilium BGP peering policy
# Cilium's BGP speaker peers with BIRD running on the same host (or LAN router)
# cilium-bgp-peer.yaml
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPPeeringPolicy
metadata:
name: bgp-policy
spec:
nodeSelector:
matchLabels:
kubernetes.io/os: linux
virtualRouters:
- localASN: 64520 # Kubernetes cluster ASN
exportPodCIDR: true
serviceSelector:
matchExpressions:
- key: somekey
operator: NotIn
values:
- never-select-me
neighbors:
- peerAddress: "10.0.1.1" # BIRD or LAN router IP
peerASN: 64512
# /etc/bird/bird.conf on the kldload host — accept Cilium BGP session
router id 10.0.1.1;
log syslog all;
protocol kernel { ipv4 { export all; import none; }; }
protocol device { scan time 10; }
protocol direct { ipv4; }
# Accept service IPs from Cilium (typically /32 LoadBalancer IPs and pod CIDRs)
filter import_from_cilium {
if net.len > 32 then reject;
if net.len < 24 then reject; # service IPs are /32, pod CIDRs /24 range
accept;
}
# Redistribute Kubernetes service IPs to the WireGuard BGP mesh
filter export_k8s_services {
# pass /32 LoadBalancer IPs and pod CIDRs to peers
if net.len >= 24 then accept;
reject;
}
# Session with Cilium BGP speaker (Kubernetes cluster ASN 64520)
protocol bgp cilium {
local 10.0.1.1 as 64512;
neighbor 10.0.1.2 as 64520; # Kubernetes node IP where Cilium runs
ipv4 {
import filter import_from_cilium;
export none; # don't send host routes back to Cilium
};
}
# Existing site-to-site sessions now redistribute Kubernetes routes
protocol bgp site_b {
local 10.200.0.1 as 64512;
neighbor 10.200.0.2 as 64513;
ipv4 {
import filter import_from_peer;
export filter export_k8s_services; # send K8s service IPs to remote sites
};
}
See the Cilium Masterclass for the full Cilium BGP configuration,
including CiliumLoadBalancerIPPool and service annotation details.
11. BFD (Bidirectional Forwarding Detection)
By default, a BGP session detects a failed link using the hold timer: if no keepalive is received for 90 seconds (the default hold time), the session is considered dead and routes are withdrawn. For a production WireGuard mesh where you need fast failover between paths, 90 seconds is unacceptable. BFD reduces detection to 300 milliseconds.
BFD config in BIRD
# /etc/bird/bird.conf — add BFD protocol and enable it per BGP session
router id 10.0.1.1;
log syslog all;
protocol kernel { ipv4 { export all; import none; }; }
protocol device { scan time 10; }
protocol direct { ipv4; }
# BFD protocol — provides fast failure detection
protocol bfd {
interface "wg*" { # run BFD on all WireGuard interfaces
min rx interval 100ms; # minimum receive interval
min tx interval 100ms; # minimum transmit interval
multiplier 3; # declare failure after 3 missed intervals (300ms)
};
}
filter import_from_peer {
if net = 0.0.0.0/0 then reject;
if net !~ [ 10.0.0.0/8+ ] then reject;
if net.len > 24 then reject;
if bgp_path ~ [ * 64512 * ] then reject;
accept;
}
filter export_to_peer {
if net ~ 10.0.1.0/24 then accept;
reject;
}
protocol bgp site_b {
local 10.200.0.1 as 64512;
neighbor 10.200.0.2 as 64513;
bfd on; # enable BFD for this session
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
protocol bgp site_c {
local 10.200.0.1 as 64512;
neighbor 10.200.0.3 as 64514;
bfd on;
ipv4 {
import filter import_from_peer;
export filter export_to_peer;
};
}
# Verify BFD sessions
birdc show bfd sessions
# IP address Interface State Since Interval Timeout
# 10.200.0.2 wg0 Up 2026-04-01 0.100 0.300
# 10.200.0.3 wg0 Up 2026-04-01 0.100 0.300
# With BFD enabled, a link failure is detected in ~300ms
# The BGP session drops immediately and routes are withdrawn
# Your routing table reconverges on the backup path in under 1 second
12. Route Servers and Looking Glasses
Internet Exchange Points (IXPs) are physical facilities where networks interconnect. Instead of each network maintaining bilateral BGP sessions with every other network at the exchange, the IXP runs a route server — a BIRD instance that accepts routes from all members and redistributes them. A network at an IXP connects to the route server once and receives routes from hundreds of other networks.
Looking glass tools
A looking glass is a public-facing interface to a network's BGP router. You can query it to see how a prefix is seen from that network's perspective — which AS path it came through, which community tags it carries, and how many routes exist to that prefix.
| Tool | URL | What it shows |
|---|---|---|
| bgp.tools | bgp.tools | Full BGP table lookup, AS info, peer relationships, community tags |
| RIPE Stat | stat.ripe.net | Historical BGP data, routing history, prefix visibility, RPKI status |
| PeeringDB | peeringdb.com | IXP membership, peering policies, NOC contacts for all public ASNs |
| Hurricane Electric BGP | bgp.he.net | AS graph, prefix announcements, peer relationships, IPv6 coverage |
| Cloudflare Radar | radar.cloudflare.com | Real-time internet routing changes, BGP hijack alerts, AS traffic data |
How to read a looking glass query
# Example: querying bgp.tools for 8.8.8.8 (Google DNS)
# You would see something like:
#
# Prefix: 8.8.8.0/24
# Origin AS: AS15169 (Google LLC)
# AS path: [your-AS] [transit-AS] 15169
# Next hop: 203.0.113.1 (your upstream peer)
# Communities: 15169:5050 (Google's internal community)
#
# This tells you:
# - Google announces 8.8.8.0/24 (a /24, the maximum granularity they will announce)
# - Your path to it goes through [transit-AS] before reaching Google
# - The community 15169:5050 is internal to Google — IXP members may see different tags
# To check your own announcements: look up your public IP
# bgp.tools shows whether your prefix is globally routable and from which ASNs
13. Troubleshooting
birdc is the BIRD control socket client. It connects to the running BIRD daemon and gives
you real-time visibility into sessions, routes, filters, and BFD state. All diagnostic work
starts here.
Essential birdc commands
# Show all protocol states — BGP sessions, direct, kernel, BFD
birdc show protocols
# Name Proto Table State Since Info
# direct1 Direct --- up 2026-04-01
# kernel1 Kernel master4 up 2026-04-01
# site_b BGP --- up 2026-04-01 Established
# site_c BGP --- down 2026-04-01 Active ← problem
# Show all routes in BIRD's table
birdc show route
# Show routes for a specific prefix
birdc show route for 10.0.2.0/24
# Show what BIRD would export to a specific protocol (tests your export filters)
birdc show route export site_b
# Show what BIRD received from a specific protocol (before import filters)
birdc show route protocol site_b
# Show detailed info for a specific protocol including last error
birdc show protocols all site_c
# Reload config without dropping sessions
birdc configure
# Restart a specific protocol (drops and re-establishes that BGP session only)
birdc disable site_c
birdc enable site_c
Common issues and fixes
Session stuck in Active
Active means BIRD is trying to connect but failing. Check: is TCP 179 open? ss -tlnp | grep 179 and telnet <peer-ip> 179. Check firewall rules. Verify the WireGuard tunnel is up (wg show). Verify the neighbor IP is reachable (ping <peer-ip>).
Session up but no routes
Session is Established but show route protocol site_b shows nothing. Your import filter is rejecting everything. Check the filter logic. Run birdc show route protocol site_b all to see rejected routes and why.
Routes not in kernel table
BIRD has the routes but ip route show doesn't. Check your protocol kernel export filter — it may be rejecting BGP-learned routes. Set export all to confirm, then narrow the filter if needed.
Wrong ASN / peer rejected
If the remote peer's ASN doesn't match what BIRD expects in the neighbor directive, the session is refused with an error. birdc show protocols all site_b shows the exact error message including the mismatched ASN.
Advanced diagnostics
# Watch BGP events in real time (increase log verbosity temporarily)
# Add to bird.conf: log "/var/log/bird.log" { debug, trace, info, remote, warning, error, auth, fatal, bug };
# Then tail the log:
tail -f /var/log/bird.log | grep -E "BGP|bgp|route"
# Capture BGP traffic on the WireGuard interface
tcpdump -i wg0 -n tcp port 179
# Check if a specific prefix is being filtered at export
birdc show route for 10.0.2.0/24 export site_b
# If the route exists in BIRD but doesn't appear here, export filter is rejecting it
# Trace a route end-to-end
birdc show route for 10.0.2.1
# Shows: which protocol installed it, which peer it came from, AS_PATH, communities
# Check RPKI validation state (if you have RPKI configured)
birdc show route where roa_check(roa_table, net, bgp_path.last) = ROA_VALID
TCP 179 firewall rules
# Allow BGP on WireGuard interface (firewalld — CentOS/RHEL/Fedora)
firewall-cmd --permanent --zone=trusted --add-interface=wg0
firewall-cmd --reload
# Or with nftables directly
nft add rule inet filter input iifname "wg0" tcp dport 179 accept
nft add rule inet filter input iifname "wg0" tcp sport 179 accept
# Verify with:
ss -tlnp | grep 179
# LISTEN 0 5 0.0.0.0:179 ... users:(("bird",pid=1234,...))
The complete picture: your kldload multi-site network has three layers working together. WireGuard provides encrypted, authenticated point-to-point transport between sites. BIRD distributes routing information between sites over those tunnels, using BGP with strict filters to ensure only legitimate prefixes are accepted. BFD detects link failures in 300ms and triggers automatic reconvergence to backup paths. Cilium (if you're running Kubernetes) peers with BIRD to advertise service IPs across the mesh. The result: a self-healing, policy-driven, encrypted network fabric that spans multiple sites and cloud providers — built from open-source tools running on commodity hardware.
BIRD is the same software that runs the route servers at AMS-IX, LINX, DE-CIX, and Torix. The protocol is the same one that AWS, Google, Cloudflare, and every ISP on earth uses to exchange routing information. You are not using a simplified subset of internet routing. You are using the real thing, on your own hardware, under your own policy.
Related pages
- Networking tutorial — FRRouting, VXLAN, eBPF dataplane foundations
- WireGuard Masterclass — the encrypted transport layer BGP runs over
- WireGuard Mesh & Multi-Site — the four-plane mesh architecture
- Cilium Masterclass — Kubernetes networking with BGP service announcement
- eBPF Reference — kernel-level network observability
- Firewall & Gateway — BGP in the kldload firewall/gateway recipe
- Multi-Site Cloud — production multi-site architecture