BIRD2 BGP Configuration on a Linux VPS
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).
Install from CZ.NIC repository (recommended)
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:
- Your
passwordin bird.conf matches exactly what the provider gave you (case-sensitive, watch for trailing spaces) - 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
birdandbird6with 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.confand/etc/bird/bird6.confto a single/etc/bird/bird.conf - birdc socket: Single socket at
/run/bird/bird.ctlinstead 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 bgpblock for redundancy. Usepreferenceto 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