| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source

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.

BGP is not just for enterprises. If you have two sites, two ISPs, or one cloud connection, you need BGP. Static routes break when links fail. BGP is how you automate the decision of which path to use — and how you make that decision based on policy, not just hop count. The investment in learning BGP pays off the moment you have more than one path between two points in your network.

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.

// Public ASNs: 1 – 64495 (assigned by ARIN/RIPE/etc.) // Private ASNs: 64512 – 65534 (use freely, like RFC1918)

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.

// You own 10.100.0.0/16 at site-a // BGP tells site-b and site-c how to reach it

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).

// AS_PATH: [64512, 64513, 64514] // Like a traceroute baked into the routing announcement

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.

// BGP session = persistent TCP connection // Keepalive every 60s, hold timer 90s (configurable)

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.

kldload's networking tutorial uses FRRouting because vtysh is familiar. This masterclass teaches BIRD because it is the better tool for automated, declarative, config-managed routing. Both work. Pick the one that fits your brain. If you came from Cisco, FRRouting will feel like home. If you prefer writing config files that Ansible or Salt can template and push, BIRD wins. For multi-site kldload deployments where the config is generated and distributed by automation, BIRD's file-based model is significantly easier to manage at scale.
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
Use 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.

// BIRD table → protocol kernel → kernel routing table // This is what makes ip route show work

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.

// Like running "ip link show" at startup // Tells BIRD: "eth0, wg0, lo exist"

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.

// eth0 has 10.0.1.1/24 → direct generates 10.0.1.0/24 // You can then redistribute this prefix via BGP

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.

// filter accept_rfc1918 { ... } // filter reject_default { ... }

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.

BIRD's config is declarative — you describe what you want, not step-by-step commands. This makes it ideal for config management tools like Salt, Ansible, and Terraform. A Salt state that templates /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.

eBGP is between different organizations or ASNs. iBGP is within your network. For a kldload multi-site setup, each site gets its own ASN from the private range 64512–65534 and you run eBGP between sites over WireGuard. This is simpler than iBGP for a small number of sites because you don't need route reflectors or full-mesh sessions. Use private ASNs freely — they are stripped before routes leave your network to the public internet.

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.

The Facebook outage, the Pakistan YouTube hijack, the Cloudflare leak — all caused by missing BGP filters. Filters are not optional. They are the difference between a working network and front-page news. Even on a private network with trusted peers, filters protect you from misconfiguration. If site-B accidentally announces a default route or a more-specific that overlaps your infrastructure, your filter on site-A is what prevents that from propagating.

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.

Communities are metadata-driven routing. "Please don't announce this prefix to your other customers" is expressed as a community tag — your provider's network reads the tag and applies a filter. "This route came from site-A, prefer it over the site-B path" is a community you set locally and your routers read to set LOCAL_PREF. You are encoding network policy as data attached to routes, not as configuration on every router in the path.

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
This is the same architecture as the networking tutorial's multi-cloud section, but with BIRD instead of FRRouting and with proper filters and communities baked in from the start. The WireGuard tunnel handles encryption and transport. BIRD handles route distribution and policy. Every site only needs to know the WireGuard IPs of its direct peers — the routing table fills in automatically. Adding a fourth site is two WireGuard tunnels and two BGP sessions. No manual routes anywhere.

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.

// AWS ASN: 7224 (all regions) // Your tunnel IPs: provided in the VPN config download // Announce: your LAN subnets. Accept: your VPC CIDR

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.

// GCP ASN: 16550 (Interconnect), varies (VPN) // Cloud Router manages BGP sessions on GCP side // Announce your RFC1918 prefixes, receive VPC routes

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.

// Azure private peering ASN: 65515 // Two sessions: primary (mandatory) + secondary (HA) // Announce: your on-prem. Receive: VNet address spaces

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.

// Wrong: announce 10.0.1.128/26 // Right: announce 10.0.1.0/24 or 10.0.0.0/16 // Always aggregate to the most summarized prefix you own

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;
    };
}
Every cloud interconnect speaks BGP. Your kldload node running BIRD can peer with AWS, GCP, and Azure using the same config pattern as peering with another kldload site. The protocol is identical. The only differences are the endpoint IP, the remote ASN, and the prefix rules imposed by the cloud provider. Once you understand the kldload-to-kldload peering model, cloud BGP is just a matter of looking up which ASN to use and which prefix lengths are allowed.

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.

// Cilium → LAN router (BGP) // Router learns: 192.168.100.10/32 (your LoadBalancer IP) // Traffic flows direct from LAN to pod

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.

// Cilium → BIRD (local iBGP) → WireGuard mesh // All sites learn: 192.168.100.10/32 // Traffic routes across WireGuard to the cluster

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.

Without BFD, if a WireGuard tunnel fails (the underlying link drops, the remote host reboots, a firewall blocks UDP 51820), BGP waits up to 90 seconds to notice. During those 90 seconds, traffic is blackholed. With BFD, detection is 300ms. For a kldload multi-site setup where you have primary and backup WireGuard paths, BFD is what makes failover fast enough to be invisible to applications. It is not optional for production.

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.

Even if you never peer at an IXP, understanding how the internet's routing fabric works makes you a better network engineer. Route servers are BIRD — the same software you are running on your kldload node. Most major IXPs publish their BIRD configs. Reading them shows you how production filters, community policies, and RPKI validation work at scale. Torix, LINX, AMS-IX, DE-CIX, and Equinix all use BIRD route servers.

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>).

// birdc show protocols all site_b // Last error: Connection refused → TCP 179 blocked // Last error: No route to host → WireGuard tunnel down

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.

// birdc show route protocol site_b all // Routes are listed but marked [rejected] with a filter name // Fix: adjust the import filter to match what the peer is actually sending

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.

// birdc show route → route is there // ip route show → route is missing // Fix: check protocol kernel { ipv4 { export all; }; }

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.

// Last error: Bad peer AS // Fix: verify neighbor ASN matches what the peer advertises // tcpdump -i wg0 tcp port 179 ← see the raw BGP OPEN message

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,...))
birdc is your diagnostic tool. "show route for 10.100.10.0/24" tells you exactly which protocol installed that route, which peer it came from, what AS path it transited, which community tags it carries, and what filters it passed through. If you are ever unsure why traffic is taking a particular path, or why a route is missing from the routing table, birdc has the answer. It is the most important tool in the BIRD ecosystem and it takes less than five minutes to learn the six commands you will use 90% of the time.

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