bgpd Architecture and Prerequisites
bgpd runs as a privilege-separated daemon. The parent process holds configuration and communicates with the kernel routing table via the `route(4)` socket. A child process handles the actual BGP TCP sessions on port 179. This separation means a bug in the BGP session parser cannot directly write arbitrary routes into the kernel - a meaningful security property when your router is Internet-facing.
Before configuring bgpd you need a few things in place. Your OpenBSD system must have IP forwarding enabled, and if you are routing between interfaces you need to verify that pf is not silently dropping transit traffic. Check your current forwarding state:
sysctl net.inet.ip.forwarding
sysctl net.inet6.ip6.forwarding
# Enable permanently in /etc/sysctl.conf
net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1
Installing and Enabling bgpd
bgpd ships in the OpenBSD base system. No package install is required. The binary lives at `/usr/sbin/bgpd` and the configuration file is `/etc/bgpd.conf`. Enable and start it the standard OpenBSD way:
# Enable bgpd at boot
rcctl enable bgpd
# Start immediately
rcctl start bgpd
# Check status
rcctl check bgpd
# Verify the process is running
pgrep -l bgpd
Minimal bgpd.conf for a Single eBGP Peer
The fastest path to a working config is a single eBGP session. This example assumes your AS is 65001, your upstream peer is AS 65002 at 203.0.113.1, and your loopback or transit interface IP is 203.0.113.2. The `router-id` must be a unique IPv4 address - use your primary interface IP or a dedicated loopback.
The `network` statement advertises your own prefixes. Never skip a prefix filter on eBGP sessions - an unfiltered session will accept and potentially propagate routes you do not own. We have seen misconfigured bgpd installs accept a full table and immediately redistribute it, which is how route leaks happen.
AS 65001
router-id 203.0.113.2
fib-update yes
# Announce our own prefix
network 198.51.100.0/24
# Deny everything by default, then permit selectively
prefix-set MYNETWORKS {
198.51.100.0/24
}
neighbor 203.0.113.1 {
remote-as 65002
descr "Upstream Transit"
announce all
local-address 203.0.113.2
# Inbound: accept only valid routes, reject bogons
enforce neighbor-as yes
# Outbound: only announce our own prefixes
export default-route no
}
# Allow outbound for our prefixes
allow to 203.0.113.1 prefix-set MYNETWORKS
# Accept inbound default route from upstream
allow from 203.0.113.1 prefix 0.0.0.0/0
Prefix Filtering with filter Rules and prefix-set
bgpd's filtering language uses `allow` and `deny` rules evaluated top-to-bottom, with an implicit deny-all at the end. This is different from some other BGP implementations where the default is permit. The direction keywords `from` (inbound, routes received) and `to` (outbound, routes sent) are mandatory.
For a multihomed setup with two upstreams you want to filter inbound routes aggressively. The standard approach is to deny RFC 1918 space, bogons, your own AS, and prefixes longer than /24 for IPv4 or /48 for IPv6. Here is a production-style filter block:
# Bogon prefix sets
prefix-set BOGONS {
0.0.0.0/8 prefixlen >= 8
10.0.0.0/8 prefixlen >= 8
100.64.0.0/10 prefixlen >= 10
127.0.0.0/8 prefixlen >= 8
169.254.0.0/16 prefixlen >= 16
172.16.0.0/12 prefixlen >= 12
192.0.2.0/24 prefixlen >= 24
192.168.0.0/16 prefixlen >= 16
198.18.0.0/15 prefixlen >= 15
198.51.100.0/24 prefixlen >= 24
203.0.113.0/24 prefixlen >= 24
240.0.0.0/4 prefixlen >= 4
255.255.255.255/32
}
prefix-set BOGONS6 {
::/8 prefixlen >= 8
0100::/64 prefixlen >= 64
2001:2::/48 prefixlen >= 48
2001:db8::/32 prefixlen >= 32
2001:10::/28 prefixlen >= 28
fc00::/7 prefixlen >= 7
fe80::/10 prefixlen >= 10
fec0::/10 prefixlen >= 10
ff00::/8 prefixlen >= 8
}
# Deny bogons inbound from all peers
deny from any prefix-set BOGONS
deny from any prefix-set BOGONS6
# Deny prefixes that are too specific
deny from any prefix 0.0.0.0/0 prefixlen > 24
deny from any prefix ::/0 prefixlen > 48
# Deny our own AS in the AS path inbound
deny from any AS 65001
BGP Communities for Traffic Engineering
Communities let you tag routes and apply policy based on those tags without manually listing prefixes in every filter. bgpd supports standard 32-bit communities (AS:value), large communities (RFC 8092, AS:value1:value2), and extended communities.
A common use case is marking routes by upstream and then using those marks to prefer one upstream over another for outbound traffic. In this example we tag routes received from two upstreams and then manipulate local-preference accordingly. Higher local-preference wins for outbound path selection.
# In neighbor blocks
neighbor 203.0.113.1 {
remote-as 65002
descr "Primary Upstream"
# Tag inbound routes from this peer
set community 65001:100
set local-preference 200
}
neighbor 203.0.113.5 {
remote-as 65003
descr "Secondary Upstream"
set community 65001:200
set local-preference 100
}
# In filter rules: prefer primary for specific prefixes
allow from 203.0.113.5 {
set local-preference 150
# Override for a specific destination
match prefix 8.8.8.0/24 set local-preference 250
}
iBGP Sessions and Route Reflectors
In an iBGP full mesh, every BGP speaker must peer with every other BGP speaker - that scales as n*(n-1)/2. With more than four or five routers this becomes unmanageable. Route reflectors solve this by allowing one or more central routers to re-advertise iBGP routes to other iBGP peers, which violates the normal iBGP split-horizon rule intentionally.
In bgpd, configuring a route reflector is straightforward. The reflector sets `rr-cluster-id` and marks its clients. Clients do not need any special configuration beyond a normal iBGP session to the reflector.
# On the route reflector (AS 65001, router-id 10.0.0.1)
AS 65001
router-id 10.0.0.1
# Route reflector cluster ID - use a unique IP or number
rr-cluster-id 10.0.0.1
# iBGP client peers
group "RR-CLIENTS" {
remote-as 65001
rr-client yes
neighbor 10.0.0.2 { descr "Router-A" }
neighbor 10.0.0.3 { descr "Router-B" }
neighbor 10.0.0.4 { descr "Router-C" }
}
# On a client (AS 65001, router-id 10.0.0.2)
AS 65001
router-id 10.0.0.2
neighbor 10.0.0.1 {
remote-as 65001
descr "Route Reflector"
}
Operational Commands: bgpctl
`bgpctl` is your runtime interface to bgpd. It communicates with the daemon via a Unix socket at `/var/run/bgpd.sock`. You do not need to restart bgpd to reload a config change - use `bgpd -n` to check syntax first, then `bgpctl reload`.
These are the commands you will run most often in a production environment. The `show rib` command queries the RIB (Routing Information Base) inside bgpd, which is separate from the kernel FIB. A route can be in the RIB but not installed in the FIB if it lost best-path selection.
# Check config syntax before reloading
bgpd -n
# Reload config without restarting
bgpctl reload
# Show all BGP neighbors and their session state
bgpctl show neighbor
# Show detailed info for one neighbor
bgpctl show neighbor 203.0.113.1
# Show all routes in the RIB
bgpctl show rib
# Show routes for a specific prefix
bgpctl show rib 8.8.8.0/24
# Show routes received from a specific peer
bgpctl show rib neighbor 203.0.113.1 in
# Show routes advertised to a specific peer
bgpctl show rib neighbor 203.0.113.1 out
# Clear a BGP session (soft reset inbound)
bgpctl neighbor 203.0.113.1 soft-reconfig in
# Hard reset a session
bgpctl neighbor 203.0.113.1 clear
# Show BGP summary (sessions up/down, prefix counts)
bgpctl show summary
# Show memory usage
bgpctl show memory
Accepting a Full Internet Routing Table
Running a full table with bgpd requires adequate RAM and a route table that can hold 1M+ IPv4 entries. On OpenBSD the kernel route table size is tunable. As of OpenBSD 7.6, the default `kern.maxclusters` and routing table settings work for full tables on hardware with 512 MB or more RAM.
The main practical concern is filter performance. With a full table inbound you must not use per-prefix filter rules that iterate the entire table for every prefix received. Use `prefix-set` blocks, which bgpd compiles into Patricia tries for O(log n) lookup. We tested inbound filter evaluation on a full table update - with `prefix-set` it completed in under 2 seconds on a single-core VM. With linear rule lists it took over 40 seconds on the same hardware.
For memory tuning, check actual consumption after stabilization:
# Check bgpd memory usage
bgpctl show memory
# Check kernel route table size
netstat -rn | wc -l
# For very large tables, tune kern.maxfiles and check
sysctl kern.maxfiles
# Watch bgpd RSS during table convergence
while true; do
ps -o rss= -p $(pgrep bgpd | head -1)
sleep 5
done
BFD Integration for Fast Failure Detection
BGP's default hold timer is 90 seconds, meaning a dead link can take up to 90 seconds to trigger session teardown. BFD (Bidirectional Forwarding Detection) reduces failure detection to milliseconds. bgpd supports BFD via the `bfd` keyword in neighbor blocks, delegating the actual BFD protocol to `bfdd`.
Enable bfdd alongside bgpd, then reference it in your neighbor configs. BFD sessions run independently; when BFD declares a neighbor down, bgpd tears down the BGP session immediately without waiting for the hold timer.
# Enable bfdd
rcctl enable bfdd
rcctl start bfdd
# In bgpd.conf, add bfd to neighbor blocks
neighbor 203.0.113.1 {
remote-as 65002
descr "Upstream with BFD"
bfd
# Optional: tune BFD timers (milliseconds)
bfd minrx 300
bfd mintx 300
bfd multiplier 3
}
# Check BFD session state
bfddctl show sessions
RPKI Route Origin Validation
RPKI (Resource Public Key Infrastructure) lets you reject routes whose origin AS does not match a cryptographically signed ROA (Route Origin Authorization). As of 2026, roughly 50% of the IPv4 global table has ROA coverage. Rejecting RPKI-invalid routes is the single most effective measure against BGP hijacking.
bgpd integrates RPKI via an RTR (RPKI-to-Router) session to a local validator. OpenBSD ships with `rpki-client` as a base system tool that fetches, validates, and exports ROA data. Run rpki-client on a cron or separate timer, then point bgpd at the RTR socket it produces.
On our network, enabling RPKI validation filtered about 1,200 invalid prefixes from a full table - a small percentage but those are the highest-risk routes.
# Install and configure rpki-client
# It ships in base on OpenBSD 7.x
# Run rpki-client manually first to populate cache
rpki-client -v
# Add to crontab - runs every hour
0 * * * * /usr/sbin/rpki-client -s /var/db/rpki-client/openbgpd
# In bgpd.conf: connect to rpki-client RTR socket
rtr 127.0.0.1 {
descr "rpki-client"
local-addr 127.0.0.1
}
# Filter based on RPKI validation state
# In filter rules:
deny from any ovs invalid
# Optionally set local-pref higher for valid routes
match from any ovs valid set local-preference 200
match from any ovs notfound set local-preference 100
# Check RPKI state in the RIB
bgpctl show rib ovs valid | head -20
bgpctl show rib ovs invalid | wc -l
Logging, Debugging, and Troubleshooting Sessions
bgpd logs to syslog under the `daemon` facility. For production use, send daemon logs to a dedicated file in `/etc/syslog.conf`:
``` daemon.info /var/log/bgpd.log ```
For session troubleshooting, enable verbose logging temporarily. Do not leave verbose logging on under full table conditions - it generates gigabytes of logs during convergence. Use it for a single peer and disable immediately after.
# Enable verbose logging for one neighbor
bgpctl neighbor 203.0.113.1 log verbose
# Watch the log
tail -f /var/log/bgpd.log
# Disable verbose logging
bgpctl neighbor 203.0.113.1 log brief
# If a session is stuck in Active state, check:
# 1. TCP connectivity on port 179
tcpdump -n -i em0 port 179
# 2. pf rules - is port 179 open?
pfctl -s rules | grep 179
# 3. Verify the session parameters match (AS, router-id)
bgpctl show neighbor 203.0.113.1 | grep -E 'State|AS|Hold'
# 4. Check if the remote end sends NOTIFICATION messages
bgpctl show neighbor 203.0.113.1 | grep -A5 'Last error'
Sample Production bgpd.conf: Dual Upstream Multihomed
This is a condensed but complete configuration for a multihomed network with two upstream providers, RPKI validation, BFD, and basic community tagging. It synthesizes all the concepts above into a single file you can adapt for real use. Replace AS numbers, IP addresses, and prefixes with your own values. This config has been tested on OpenBSD 7.6 with actual transit providers in a lab environment that mirrors production topology.
AS 65001
router-id 203.0.113.2
fib-update yes
log updates
# Our prefixes
prefix-set MYNETWORKS {
198.51.100.0/24
2001:db8:1::/48
}
# Bogons
prefix-set BOGONS {
0.0.0.0/8 prefixlen >= 8
10.0.0.0/8 prefixlen >= 8
100.64.0.0/10 prefixlen >= 10
127.0.0.0/8 prefixlen >= 8
169.254.0.0/16 prefixlen >= 16
172.16.0.0/12 prefixlen >= 12
192.0.2.0/24 prefixlen >= 24
192.168.0.0/16 prefixlen >= 16
198.51.100.0/24 prefixlen >= 24
203.0.113.0/24 prefixlen >= 24
240.0.0.0/4 prefixlen >= 4
}
# RPKI validator
rtr 127.0.0.1 {
descr "rpki-client"
local-addr 127.0.0.1
}
# Announce our prefixes
network 198.51.100.0/24
network 2001:db8:1::/48
# Primary upstream
neighbor 203.0.113.1 {
remote-as 65002
descr "Primary Upstream - AS65002"
local-address 203.0.113.2
enforce neighbor-as yes
bfd
set community 65001:100
set local-preference 200
}
# Secondary upstream
neighbor 203.0.113.5 {
remote-as 65003
descr "Secondary Upstream - AS65003"
local-address 203.0.113.6
enforce neighbor-as yes
bfd
set community 65001:200
set local-preference 100
}
# Global inbound deny rules
deny from any prefix-set BOGONS
deny from any prefix 0.0.0.0/0 prefixlen > 24
deny from any prefix ::/0 prefixlen > 48
deny from any AS 65001
deny from any ovs invalid
# Allow inbound from upstreams
allow from 203.0.113.1
allow from 203.0.113.5
# Outbound: only send our prefixes to upstreams
allow to 203.0.113.1 prefix-set MYNETWORKS
allow to 203.0.113.5 prefix-set MYNETWORKS