A Docker-based lab that loads a full live BGP table (IPv4 + IPv6) into a BIRD2 router for policy experimentation. Routes are sourced from RouteViews MRT RIB dumps, aggregated by origin AS, and injected via a native BGP implementation written in Rust. The original Python test was 3x slower to process the dump files and the ExaBGP speaker crawled, hence the Rust port.
The primary use case is traffic engineering by origin AS: assign higher local-preference to prefixes from a specific origin AS to steer outbound traffic toward a preferred interface without managing static routes.
# bird.conf — prefer Google (AS15169) out interface A
if bgp_path.last = 15169 then bgp_local_pref = 200;
┌─────────────────────────────────────────┐
│ bgp-feeder (172.28.0.3) │
│ │
│ feeder-rs │
│ ├── downloads RouteViews MRT RIB dump │
│ ├── aggregates prefixes by origin AS │
│ └── injects via native BGP session ───────────┐
└─────────────────────────────────────────┘ │ BGP (port 179)
▼
┌─────────────────────────────────────────┐
│ bgp-router (172.28.0.2) │
│ │
│ BIRD2 │
│ ├── master4 ~400k IPv4 prefixes │
│ └── master6 ~92k IPv6 prefixes │
└─────────────────────────────────────────┘
feeder-rs (Rust):
- Fetches the latest 2-hour MRT RIB slot from
route-views2(IPv4) androute-views6(IPv6) - Caches downloads to
./rib-cache/so container restarts skip the download - Aggregates ~1M raw prefixes down to ~493k by collapsing adjacent and covered prefixes per origin AS
- Connects directly to BIRD on port 179 and announces routes as BGP UPDATEs, sending keepalives inline every 5,000 routes
- After the initial table load the session is kept alive with periodic keepalives
BIRD2: holds the full table in memory (≈ 2 GB). Nothing is installed in the kernel routing table — it's a policy/lookup engine only.
docker compose up -d
docker exec bgp-router birdc 'show protocols all feeder'
# wait ~5 min for the initial download + announcement, then:
docker exec bgp-router birdc 'show route count'Expected output after loading:
master4: 400257 routes
master6: 92329 routes
Edit bird.conf and add rules inside filter set_local_pref:
filter set_local_pref {
bgp_local_pref = 100; # default
if bgp_path.last = 15169 then bgp_local_pref = 200; # Google → prefer ifA
if bgp_path.last = 32934 then bgp_local_pref = 200; # Meta → prefer ifA
if bgp_path.last = 16509 then bgp_local_pref = 50; # AWS → prefer ifB
accept;
}
Then reload BIRD without restarting:
docker exec bgp-router birdc configure- Docker + Docker Compose
- ~2 GB RAM for the BIRD container (full IPv4+IPv6 table)
- ~200 MB disk for cached MRT dumps (
./rib-cache/)
| Container | IPv4 | IPv6 | AS |
|---|---|---|---|
| bgp-router | 172.28.0.2 | fd00:dead:beef::2 | 64512 |
| bgp-feeder | 172.28.0.3 | fd00:dead:beef::3 | 65001 |
# Session state
docker exec bgp-router birdc 'show protocols all feeder'
# Route count
docker exec bgp-router birdc 'show route count'
# Routes from a specific origin AS (e.g. Google)
docker exec bgp-router birdc 'show route where bgp_path.last = 15169'
# Reload config (e.g. after editing local-pref rules)
docker exec bgp-router birdc configure- URL selection — computes the latest completed 2-hour RouteViews slot (e.g.
rib.20260402.0600.bz2), backing off one slot if the file is less than 30 minutes old to avoid partial uploads. - Caching — downloads to
./rib-cache/{collector}.{filename}and skips the download on subsequent runs if the file is present. - Parsing — uses bgpkit-parser to stream-parse the bz2 MRT file, deduplicating prefixes and grouping by origin AS.
- Aggregation — for each origin AS, sorts prefixes and iteratively merges adjacent sibling pairs (e.g.
10.0.0.0/25+10.0.0.128/25→10.0.0.0/24) and removes covered prefixes, reducing the table size by roughly 50%. - BGP injection — opens a TCP connection to BIRD on port 179, exchanges OPEN/KEEPALIVE, then streams BGP UPDATE messages. Keepalives are sent inline every 5,000 routes so the hold timer never expires during the initial flood.
- IPv4:
archive.routeviews.org/route-views2/bgpdata/ - IPv6:
archive.routeviews.org/route-views6/bgpdata/
RouteViews publishes new RIB snapshots every 2 hours. The feeder loads the most recent available snapshot at startup and holds it until the container is restarted.