BIRD2 BGP Configuration on a Linux VPS

14 min read·Matthieu·BGPBIRD2RoutingIPv6Firewall|

Install BIRD2 on Debian 12 or Ubuntu 24.04 and configure a BGP session to announce your own IP prefixes. Dual-stack, export filters, persistent dummy interfaces, nftables rules, and birdc verification.

BIRD is the routing daemon maintained by CZ.NIC. Version 2 unified IPv4 and IPv6 into a single daemon, replaced the split BIRD/BIRD6 architecture from 1.x, and added a powerful filter language. This guide covers installing BIRD2 on a Linux VPS, configuring a BGP session with your upstream provider, announcing your IP prefixes, and verifying everything works.

All examples are dual-stack (IPv4 + IPv6). Replace placeholder ASNs, IPs, and prefixes with your own values throughout.

What do you need before configuring BIRD2 for BGP?

Before touching bird.conf, you need an ASN, allocated IP space, and a provider that offers BGP sessions on your VPS. Without all three, BIRD2 has nothing to announce and no one to peer with.

Gather these details from your provider or RIR allocation:

Item Example value Where to get it
Your ASN AS65400 RIPE NCC, ARIN, or your LIR [-> register-asn-ripe-ncc]
Your IPv4 prefix 203.0.113.0/24 RIR allocation or PA assignment from provider
Your IPv6 prefix 2001:db8:1000::/48 RIR allocation or PA assignment from provider
Upstream ASN AS64496 Provider's BGP session details
Upstream peer IPv4 198.51.100.1 Provider's BGP session details
Upstream peer IPv6 2001:db8::1 Provider's BGP session details
Your peering IPv4 198.51.100.2 Provider's BGP session details
Your peering IPv6 2001:db8::2 Provider's BGP session details
MD5 password (shared secret) Agreed with provider

You also need ROA records published for your prefixes before your routes will pass RPKI validation at downstream networks. See for that setup.

Make sure your provider has added your prefixes to their IRR filters. Without that, your announcements will be filtered even if the BGP session establishes.

A VPS with at least 1 GB of RAM is sufficient for BIRD2 with a full table (over 1 million IPv4 routes). If you only accept a default route from your upstream, memory usage stays under 100 MB.

How do you install BIRD2 on Debian 12 and Ubuntu 24.04?

BIRD2 is available in the default repositories of both distributions. Debian 12 ships version 2.0.12, Ubuntu 24.04 ships 2.14. Both work for basic BGP, but the CZ.NIC official repository provides BIRD 2.18 (released January 2026) with BGP dynamic unnumbered support, performance improvements, and recent bug fixes. Use the CZ.NIC repo for production deployments.

Note: CZ.NIC publishes BIRD2 packages under the bird2 project name. Do not confuse it with the bird project (which only covers Debian) or bird3 (BIRD 3.x).

Add the official repository, then install:

sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates wget

Import the CZ.NIC GPG key:

sudo wget -O /usr/share/keyrings/cznic-labs-pkg.gpg https://pkg.labs.nic.cz/gpg

Add the repository. On Debian 12:

echo "deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/bird2 bookworm main" | sudo tee /etc/apt/sources.list.d/cznic-labs-bird.list

On Ubuntu 24.04:

echo "deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/bird2 noble main" | sudo tee /etc/apt/sources.list.d/cznic-labs-bird.list

Install BIRD2:

sudo apt-get update
sudo apt-get -y install bird2

Install from default repositories

If you prefer the distribution package:

sudo apt-get update
sudo apt-get -y install bird2

Verify the installation

bird --version

Expected output (CZ.NIC repo):

BIRD version 2.18

Check that the service is running:

sudo systemctl status bird

BIRD starts automatically after install. The default config at /etc/bird/bird.conf is a skeleton. You will replace it entirely in the next section.

How is bird.conf structured for BGP?

A BGP-ready bird.conf has five blocks: global settings, and four protocol sections (device, direct, kernel, bgp). Each protocol runs independently and communicates through routing tables.

Here is the complete bird.conf for a dual-stack BGP setup. Read through it first, then the sections below explain each block.

sudo cp /etc/bird/bird.conf /etc/bird/bird.conf.bak
sudo tee /etc/bird/bird.conf > /dev/null << 'BIRDCONF'
# /etc/bird/bird.conf - BIRD2 BGP configuration
# Replace all placeholder values with your own

log syslog all;

router id 198.51.100.2;

# Watch interface state changes
protocol device {
    scan time 10;
}

# Import connected routes (needed for next-hop resolution)
protocol direct {
    ipv4;
    ipv6;
    interface "dummy0";
    interface "eth0";
}

# Sync BIRD routes to kernel routing table
protocol kernel {
    ipv4 {
        export all;
        import all;
    };
    learn;
    scan time 15;
    merge paths on;
}

protocol kernel {
    ipv6 {
        export all;
        import all;
    };
    learn;
    scan time 15;
    merge paths on;
}

# Define your prefixes
define OWN_V4_PREFIX = 203.0.113.0/24;
define OWN_V6_PREFIX = 2001:db8:1000::/48;

# Export filter: only announce your own prefixes
filter export_bgp_v4 {
    if net = OWN_V4_PREFIX then accept;
    reject;
}

filter export_bgp_v6 {
    if net = OWN_V6_PREFIX then accept;
    reject;
}

# Static routes for prefix origination (point to dummy0)
protocol static static_v4 {
    ipv4;
    route 203.0.113.0/24 via "dummy0";
}

protocol static static_v6 {
    ipv6;
    route 2001:db8:1000::/48 via "dummy0";
}

# BGP session with upstream provider
protocol bgp upstream1 {
    description "Upstream Provider";
    local 198.51.100.2 as 65400;
    neighbor 198.51.100.1 as 64496;
    source address 198.51.100.2;
    hold time 90;
    keepalive time 30;
    password "your-md5-secret";

    ipv4 {
        import all;
        export filter export_bgp_v4;
        next hop self;
    };

    ipv6 {
        import all;
        export filter export_bgp_v6;
        next hop self;
    };
}
BIRDCONF

Set permissions on the config file. Only root and the bird user need access:

sudo chown root:bird /etc/bird/bird.conf
sudo chmod 640 /etc/bird/bird.conf

Verify permissions:

ls -la /etc/bird/bird.conf

Expected:

-rw-r----- 1 root bird 1247 Mar 19 12:00 /etc/bird/bird.conf

Global settings

log syslog all;
router id 198.51.100.2;

router id must be a globally unique IPv4 address. Use one of your assigned IPs. BIRD can auto-detect it from interfaces, but explicit is better.

log syslog all sends all log messages to the system journal. Read them with journalctl -u bird -f. If you need verbose output while debugging a specific protocol, add debug protocols all; temporarily. Remove it once the session is working. Debug-level logging generates high volume and can fill your journal storage.

Protocol device

protocol device {
    scan time 10;
}

Watches for interface state changes every 10 seconds. Required. Without it, BIRD does not know when interfaces come up or go down.

Protocol direct

protocol direct {
    ipv4;
    ipv6;
    interface "dummy0";
    interface "eth0";
}

Imports connected routes from the listed interfaces. BIRD needs these for next-hop resolution. List only the interfaces involved in your BGP setup.

Protocol kernel

Two kernel protocol blocks are needed: one for IPv4, one for IPv6. Each syncs its address family between BIRD's routing table and the Linux kernel table.

merge paths on enables ECMP if you have multiple upstreams. learn imports existing kernel routes into BIRD.

What does the protocol BGP block do?

The protocol bgp block defines a peering session with one neighbor. It specifies who you are (local ... as), who you peer with (neighbor ... as), authentication (password), and what routes to exchange (channel blocks).

Key fields:

Directive Purpose
local <ip> as <ASN> Your peering IP and ASN
neighbor <ip> as <ASN> Upstream's peering IP and ASN
source address <ip> Source IP for the TCP connection
hold time 90 Seconds before declaring peer dead (3x keepalive)
keepalive time 30 Interval between keepalive messages
password "..." MD5 authentication (RFC 2385)

MD5 authentication protects the BGP session against spoofed TCP resets and injection attacks. Both sides must configure the same password. Ask your provider for the shared secret.

How do you configure dual-stack IPv4 and IPv6 channels?

BIRD2 handles IPv4 and IPv6 as separate channels within the same protocol block. Each channel has its own import/export policy:

ipv4 {
    import all;
    export filter export_bgp_v4;
    next hop self;
};

ipv6 {
    import all;
    export filter export_bgp_v6;
    next hop self;
};

import all accepts all routes from the upstream. In production, you should filter imports too. See for import filter examples.

next hop self rewrites the next-hop attribute to your peering IP. Required when the upstream expects traffic to flow through your address, which is the standard setup on a VPS.

If your provider runs separate BGP sessions for IPv4 and IPv6 (different neighbor IPs per address family), split them into two protocol bgp blocks instead.

How do you write export filters to announce only your prefixes?

Export filters control which routes BIRD sends to the upstream. A misconfigured export filter can leak routes you do not own, which will get your session shut down and potentially earn you a call from your upstream's NOC.

The filter in the config above uses a simple prefix match:

define OWN_V4_PREFIX = 203.0.113.0/24;

filter export_bgp_v4 {
    if net = OWN_V4_PREFIX then accept;
    reject;
}

This accepts only the exact /24. Everything else is rejected. The reject at the end is the default deny.

For multiple prefixes, use a set:

define OWN_V4_PREFIXES = [ 203.0.113.0/24, 198.51.100.0/24 ];

filter export_bgp_v4 {
    if net ~ OWN_V4_PREFIXES then accept;
    reject;
}

The ~ operator matches against a prefix set. It also supports range notation:

define OWN_V4_PREFIXES = [ 203.0.113.0/24{24,24} ];

The {24,24} restricts the match to exactly /24. Use {24,28} to also allow deaggregation down to /28 if you need to announce more-specifics for traffic engineering.

For IPv6, the same pattern applies:

define OWN_V6_PREFIXES = [ 2001:db8:1000::/48{48,48} ];

filter export_bgp_v6 {
    if net ~ OWN_V6_PREFIXES then accept;
    reject;
}

Always keep export filters as tight as possible. Announce only what you own, at the exact prefix lengths you intend. A missing reject at the end of your filter will cause BIRD to use the default action for the channel, which may be accept. Always end with an explicit reject.

You can test your filter without applying it:

sudo birdc eval '203.0.113.0/24 ~ [ 203.0.113.0/24 ]'

This returns TRUE or FALSE and helps debug filter logic before a config reload.

How do you create a persistent dummy interface for prefix origination?

BGP announces prefixes that exist in the routing table. You need an interface with your prefix assigned so BIRD can originate the route. A dummy interface serves this purpose: it is always up, has no physical link, and survives reboots when configured through systemd-networkd.

Ephemeral ip link add commands are lost on reboot. Use systemd-networkd instead.

Create the .netdev file

sudo tee /etc/systemd/network/10-dummy0.netdev > /dev/null << 'EOF'
[NetDev]
Name=dummy0
Kind=dummy
EOF

Create the .network file

sudo tee /etc/systemd/network/10-dummy0.network > /dev/null << 'EOF'
[Match]
Name=dummy0

[Network]
Address=203.0.113.1/32
Address=2001:db8:1000::1/128
EOF

Assign a single /32 (or /128 for IPv6) from your allocated prefix to the dummy interface. This address does not need to be reachable from outside. It exists only so BIRD has a connected route for the prefix.

Apply and verify

sudo systemctl restart systemd-networkd

Wait a few seconds, then check:

ip addr show dummy0

Expected output:

3: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
    inet 203.0.113.1/32 scope global dummy0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:1000::1/128 scope global
       valid_lft forever preferred_lft forever

The state UNKNOWN for a dummy interface is normal. It means the link layer has no physical carrier to track, but the interface is UP and forwarding. The MAC address is randomly generated by systemd-networkd and will differ on your system.

If you are running ifupdown instead of systemd-networkd (check with networkctl status), you may need to enable systemd-networkd first:

sudo systemctl enable --now systemd-networkd

Verify the files are in place for next reboot:

ls -la /etc/systemd/network/10-dummy0.*
-rw-r--r-- 1 root root  42 Mar 19 12:00 /etc/systemd/network/10-dummy0.netdev
-rw-r--r-- 1 root root  89 Mar 19 12:00 /etc/systemd/network/10-dummy0.network

How do you secure BGP with nftables firewall rules?

BGP runs on TCP port 179. Without firewall rules, any host on the internet can attempt to open a BGP session with your router. Automated scanners find open port 179 within hours of a VPS going live. Restrict port 179 to your peer IPs only.

nftables may not be installed by default on Ubuntu 24.04. Install it first:

sudo apt-get -y install nftables

Create the nftables ruleset

sudo tee /etc/nftables.d/bgp.conf > /dev/null << 'EOF'
table inet bgp_filter {
    set bgp_peers_v4 {
        type ipv4_addr
        elements = { 198.51.100.1 }
    }

    set bgp_peers_v6 {
        type ipv6_addr
        elements = { 2001:db8::1 }
    }

    chain input {
        type filter hook input priority 0; policy accept;

        # Allow BGP from known peers only
        tcp dport 179 ip saddr @bgp_peers_v4 accept
        tcp dport 179 ip6 saddr @bgp_peers_v6 accept

        # Drop BGP from everyone else
        tcp dport 179 drop
    }
}
EOF

Add more peer IPs to the sets as you add upstreams. Named sets let you update peer lists without rewriting rules.

Integrate with your existing nftables config

If you already have /etc/nftables.conf, include the BGP rules:

echo 'include "/etc/nftables.d/bgp.conf"' | sudo tee -a /etc/nftables.conf

If you do not have nftables set up yet, create the directory and load the config:

sudo mkdir -p /etc/nftables.d
sudo systemctl enable --now nftables

enable --now starts nftables immediately and ensures it loads on every boot.

Apply the rules:

sudo nft -f /etc/nftables.d/bgp.conf

Verify the rules

sudo nft list table inet bgp_filter

Expected output:

table inet bgp_filter {
	set bgp_peers_v4 {
		type ipv4_addr
		elements = { 198.51.100.1 }
	}

	set bgp_peers_v6 {
		type ipv6_addr
		elements = { 2001:db8::1 }
	}

	chain input {
		type filter hook input priority filter; policy accept;
		tcp dport 179 ip saddr @bgp_peers_v4 accept
		tcp dport 179 ip6 saddr @bgp_peers_v6 accept
		tcp dport 179 drop
	}
}

nft list normalizes priority 0 to priority filter and strips comments. This is expected.

Test that the rule works by checking connection state. From the VPS:

sudo nft list ruleset | grep -c "179"

This should return a count matching the number of port 179 rules you created.

Also confirm that BGP traffic is not blocked between you and your peer. The rules above only affect inbound connections. BIRD initiates outbound connections to the peer, which are handled by the established/related conntrack entry. If you have a restrictive outbound policy, add:

sudo nft add rule inet bgp_filter input ct state established,related accept

This should already be in your base firewall ruleset. If it is not, add it before the BGP rules.

How do you apply the BIRD2 configuration?

Before loading a new config, validate it:

sudo birdc configure check

Expected output:

BIRD 2.18 ready.
Reading configuration from /etc/bird/bird.conf
Configuration OK

If there are syntax errors, BIRD reports the line number and the problem. Fix and re-check.

Apply the configuration:

sudo birdc configure
BIRD 2.18 ready.
Reading configuration from /etc/bird/bird.conf
Reconfigured

You may see Reconfiguration in progress instead of Reconfigured if BIRD is still processing protocol changes (like establishing a new BGP session). Both are normal.

BIRD applies the new config without restarting. Existing sessions perform a soft reconfiguration when possible. If you changed the neighbor IP or local AS, BIRD restarts the affected protocol automatically.

For config changes that should not disrupt running sessions (filter changes, static route additions), use:

sudo birdc reload upstream1

This re-applies import/export filters to existing routes without tearing down the TCP session.

How do you verify your BGP session and route announcements?

After applying the config, use birdc to check that the session is established and routes are being announced. These commands are your primary debugging tools.

Command What it shows When to use
show protocols Protocol state summary (Established, Active, etc.) First check after config change
show protocols all upstream1 Full session details, counters, timers Debugging a specific session
show route All routes in BIRD's table Verify received and static routes
show route export upstream1 Routes being sent to upstream Confirm your prefixes are announced
show route for 203.0.113.0/24 Best route for a specific prefix Trace path selection
show route protocol static_v4 Routes from a specific protocol Verify static routes are loaded

Check session state

sudo birdc show protocols

Expected output when working:

BIRD 2.18 ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     12:00:00.000
direct1    Direct     ---        up     12:00:00.000
kernel1    Kernel     master4    up     12:00:00.000
kernel2    Kernel     master6    up     12:00:00.000
static_v4  Static     master4    up     12:00:00.000
static_v6  Static     master6    up     12:00:00.000
upstream1  BGP        ---        up     12:00:05.000  Established

The Established state means the BGP session is up and routes can be exchanged. Anything else means something is wrong.

Check exported routes

sudo birdc show route export upstream1
BIRD 2.18 ready.
Table master4:
203.0.113.0/24       unicast [static_v4 12:00:00.000] * (200)
	dev dummy0
Table master6:
2001:db8:1000::/48   unicast [static_v6 12:00:00.000] * (200)
	dev dummy0

Both your IPv4 and IPv6 prefixes should appear here. If a prefix is missing, the export filter is rejecting it.

Check full session details

sudo birdc show protocols all upstream1

Look for the Routes: line:

  Routes:         2 imported, 0 filtered, 2 exported, 0 preferred

The exported count should match the number of prefixes you intend to announce.

Verify from outside

Use a public looking glass to confirm your prefix is visible on the internet. NLNOG Looking Glass or PeeringDB can help. Search for your prefix and verify:

  • The origin AS matches yours
  • The AS path goes through your upstream
  • No RPKI invalid status appears

Also check from the kernel routing table:

ip route show | grep 203.0.113
ip -6 route show | grep 2001:db8:1000

You should see routes pointing to the dummy0 interface.

How do you troubleshoot common BIRD2 BGP problems?

Most BGP issues fall into a few categories. Check these in order.

BGP session states reference

State Meaning What to check
Idle BIRD is not attempting to connect Config error, protocol disabled
Connect TCP connection in progress Firewall blocking port 179, wrong neighbor IP
Active TCP connection failed, retrying Peer unreachable, firewall, wrong source address
OpenSent TCP connected, waiting for OPEN reply MD5 mismatch, wrong ASN on remote side
OpenConfirm OPEN received, waiting for KEEPALIVE Rarely seen, usually transitions quickly
Established Session up, exchanging routes Working

Session stuck in Connect or Active

This means TCP cannot reach port 179 on the peer. Check:

# Can you reach the peer at all?
ping -c 3 198.51.100.1

# Is port 179 open on the peer?
nc -zv 198.51.100.1 179

# Are your nftables rules blocking outbound?
sudo nft list ruleset | grep 179

# Check BIRD logs
journalctl -u bird --since "5 minutes ago" --no-pager

Common causes: wrong source address in the BGP block, peer has not configured their side yet, or your firewall blocks outgoing connections.

Session stuck in OpenSent

The TCP connection works but the BGP OPEN message is rejected. This is almost always an MD5 password mismatch. Verify:

  1. Your password in bird.conf matches exactly what the provider gave you (case-sensitive, watch for trailing spaces)
  2. Both sides agree on the ASN

Check logs for details:

journalctl -u bird | grep -i "error\|md5\|password"

Prefix not showing in show route export

Your prefix exists in BIRD's table but the export filter is not accepting it. Debug the filter:

sudo birdc show route 203.0.113.0/24 all

Check the route source. If it says [device1] or [direct1] instead of [static_v4], the static route is not configured correctly. The export filter matches on the route, so it must come from the right protocol.

sudo birdc show route noexport upstream1

This shows routes that the filter explicitly rejected. If your prefix appears here, the filter logic has a bug.

Prefix announced but not visible on looking glass

Your export is working but the route is not propagating. Possible causes:

  • No ROA record: Your upstream or their peers filter RPKI-invalid routes. Publish ROA records first
  • IRR filtering: Your upstream filters based on IRR objects (RIPE DB, RADB). Create or update your route/route6 objects
  • Max-prefix limit: Your upstream may have a prefix limit configured. Contact them if you are hitting it
  • Propagation delay: BGP convergence takes minutes. Wait 5-10 minutes and check again

Read the logs

BIRD logs to the system journal. For real-time troubleshooting:

journalctl -u bird -f

For events in the last hour:

journalctl -u bird --since "1 hour ago" --no-pager

Look for lines containing Error, BGP, session, or Received. They tell you exactly what BIRD is doing and what went wrong.

BIRD2 vs BIRD 1.x: migration notes

If you are migrating from BIRD 1.x, the main differences are:

  • Single daemon: BIRD2 replaces both bird and bird6 with a single process
  • Channel syntax: IPv4 and IPv6 are configured as channels inside protocol blocks, not separate protocols
  • Filter syntax: Mostly compatible, but some functions changed names. Check the BIRD2 documentation for the full filter reference
  • Config file: Default location changed from /etc/bird/bird.conf and /etc/bird/bird6.conf to a single /etc/bird/bird.conf
  • birdc socket: Single socket at /run/bird/bird.ctl instead of separate IPv4/IPv6 sockets

Next steps

With your BGP session established and prefixes announced, there are several things to configure next:

  • RPKI validation: Set up RTR to validate incoming routes against ROA data
  • Import filters: Filter incoming routes to prevent route leaks and hijacks
  • Multiple upstreams: Add a second protocol bgp block for redundancy. Use preference to set primary/backup
  • BFD: Enable Bidirectional Forwarding Detection for sub-second failover between upstreams
  • Monitoring: Export BIRD metrics to Prometheus with bird_exporter

For the alternative routing daemon, see . For the full BYOIP journey, start from [-> bgp-bring-your-own-ip-vps].


Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.

Ready to try it yourself?

Deploy your own server in seconds. Linux, Windows, or FreeBSD.

See VPS Plans