Add GoBGP full-table feed container (roadmap E1)

New gobgp service: GoBGP peers eBGP-multihop with the AS57355 lab
route server (Bromirski) for the full real IPv4 + IPv6 Internet table
and BMP-exports it to the OpenBMP collector, landing in ip_rib as a
monitored peer.

Config follows the route server's published peering spec: local AS
65001, no password, keepalive 3600 / hold-time 7200, IPv4 feed on the
v4 session and IPv6 feed on the v6 session. gobgp/mrt-refresh.sh is a
cron-safe fallback that injects RouteViews MRT RIB dumps when the live
session is down. The live BGP session is not started here — bringing
gobgp up establishes the external session and loads ~1M routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sam 2026-05-19 07:39:12 -07:00
parent d60c582ff6
commit 88a5546e29
4 changed files with 289 additions and 0 deletions

View File

@ -419,6 +419,22 @@ services:
ports:
- "5053:5053"
# GoBGP -- pulls the full real Internet routing table (roadmap E1) from the
# AS57355 lab route server and BMP-exports it to the OpenBMP collector, where
# it lands in PostgreSQL ip_rib as a monitored peer. Config + MRT fallback
# script live in ./gobgp (see gobgp/README.md). Receive-only, local AS 65001.
gobgp:
restart: unless-stopped
container_name: obmp-gobgp
image: jauderho/gobgp:v4.5.0
depends_on:
- collector
# gobgpd reads /config/gobgpd.conf; the same mount carries mrt-refresh.sh
# and the cached MRT dumps it downloads.
volumes:
- ./gobgp:/config
command: ["gobgpd", "-f", "/config/gobgpd.conf", "-t", "toml"]
whois:
restart: unless-stopped
container_name: obmp-whois

99
gobgp/README.md Normal file
View File

@ -0,0 +1,99 @@
# GoBGP global Internet table feed (roadmap E1)
This service runs [GoBGP](https://github.com/osrg/gobgp) to pull the **full real
Internet routing table** (IPv4 ~1M + IPv6 ~200k routes) from Łukasz Bromirski's
lab route server (**AS57355**) and BMP-export every received route to the
OpenBMP collector. The table lands in PostgreSQL `ip_rib` as a monitored peer.
- Image: `jauderho/gobgp:v4.5.0` — community-maintained, multi-arch, tracks
upstream GoBGP releases (rebuilt within an hour of each release). Chosen
because the official `osrg/gobgp` image is published less consistently.
- Local AS: **65001** (private). Router-id: `10.40.40.250`.
- The session is **receive-only** — we announce nothing to the route server.
## Files
| File | Purpose |
|------------------|----------------------------------------------------------------|
| `gobgpd.conf` | GoBGP daemon config (global, neighbors, BMP export). TOML. |
| `mrt-refresh.sh` | MRT full-table fallback loader (cron-driven). |
| `mrt/` | Created at runtime; cached RouteViews RIB dumps. |
## Bring it up
The `gobgp` service is defined in the repo `docker-compose.yml`, on the same
default compose network as `collector`, and `depends_on` it.
```sh
docker compose config # validate compose is well-formed
docker compose up -d gobgp # start (collector must be running)
docker logs -f obmp-gobgp
```
> The live BGP cutover is performed by a human — bringing the container up is
> all that is needed; GoBGP initiates the eBGP-multihop sessions automatically.
## Confirm the session and route count
```sh
# session state — expect both neighbors in "Establ"
docker exec obmp-gobgp gobgp neighbor
# received route counts — expect ~1M IPv4, ~200k IPv6
docker exec obmp-gobgp gobgp global rib summary -a ipv4
docker exec obmp-gobgp gobgp global rib summary -a ipv6
```
## How the data appears in OpenBMP
GoBGP opens an outbound **BMP** session to `obmp-collector:5000` with
`route-monitoring-policy = "pre-policy"` (Adj-RIB-In, pre import-policy —
consistent with the rest of the OpenBMP fleet).
In OpenBMP / PostgreSQL the source is identified by the **BMP router**, which
GoBGP reports using its `router-id` (`10.40.40.250`) and `local-as` (`65001`):
- `routers` table — a row with `ip_address` / name derived from `10.40.40.250`.
- `bgp_peers` table — two peer rows for `85.232.240.179` and
`2001:1a68:2c:2::179`, both `peer_as = 57355`.
- `ip_rib` — every prefix from the global table, attributed to those peers.
To find it in Grafana/SQL, filter on `peer_as = 57355` or the router-id above.
## MRT fallback
AS57355 is a **single volunteer-run host with no SLA** — it can and does go
away. `mrt-refresh.sh` keeps the global table in `ip_rib` warm when the live
feed is down:
1. If any AS57355 session is `Established`, the script does nothing — the live
feed is authoritative and must not be overwritten with a stale dump.
2. Otherwise it downloads the latest full RIB dump from RouteViews
(`https://archive.routeviews.org/route-views/bgpdata/YYYY.MM/RIBS/rib.YYYYMMDD.HHMM.bz2`,
published every 2 hours UTC) and runs `gobgp mrt inject global <file>`,
which installs every prefix into the running daemon. BMP export to the
collector then happens automatically.
The script is idempotent (re-uses an already-downloaded dump), guarded by a
`flock` against overlapping runs, and prunes to the 4 most recent dumps.
### Schedule it (host crontab, 2-hour cadence)
```cron
0 */2 * * * docker exec obmp-gobgp /config/mrt-refresh.sh >> /var/log/gobgp-mrt.log 2>&1
```
Run it once manually to verify:
```sh
docker exec obmp-gobgp /config/mrt-refresh.sh
```
## Caveats
- **No SLA.** AS57355 is a volunteer lab route server; treat the live feed as
best-effort and rely on the MRT fallback for continuity.
- eBGP-multihop TTL is set to 64 — the route server is many hops away.
- A full table is ~1M+ prefixes; expect a noticeable load spike in the
collector and PostgreSQL when the session first establishes or an MRT dump
is injected.

70
gobgp/gobgpd.conf Normal file
View File

@ -0,0 +1,70 @@
# GoBGP daemon configuration -- OpenBMP "global Internet table" feed (roadmap E1)
#
# Pulls the full real Internet routing table (IPv4 ~1M + IPv6 ~200k routes)
# from Lukasz Bromirski's lab route server (AS57355) and BMP-exports every
# received route to the OpenBMP collector, where it lands in PostgreSQL ip_rib.
# Peering spec: https://lukasz.bromirski.net/post/bgp-w-labie-3/
#
# Receive-only: we announce NOTHING -- AS57355 explicitly asks peers not to
# send prefixes. Local AS is 65001 (the value the route server expects).
# Per the spec: eBGP multihop, no password, keepalive 3600 / hold-time 7200.
# TOML syntax targets GoBGP v3.x / v4.x.
[global]
[global.config]
as = 65001
router-id = "10.40.40.250"
# Listen for inbound BGP on the standard port. We only originate
# outbound sessions, but the daemon still needs a listen port.
port = 179
# --- Neighbor: route server, IPv4 feed --------------------------------------
# The IPv4 transport session carries the full IPv4 table only.
[[neighbors]]
[neighbors.config]
neighbor-address = "85.232.240.179"
peer-as = 57355
description = "AS57355 Bromirski lab route-server (IPv4 feed)"
[neighbors.timers.config]
keepalive-interval = 3600
hold-time = 7200
[neighbors.ebgp-multihop.config]
enabled = true
multihop-ttl = 64
[neighbors.transport.config]
# we initiate the session; no local-address pinning
passive-mode = false
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv4-unicast"
# --- Neighbor: route server, IPv6 feed --------------------------------------
# The IPv6 transport session carries the full IPv6 table only.
[[neighbors]]
[neighbors.config]
neighbor-address = "2001:1a68:2c:2::179"
peer-as = 57355
description = "AS57355 Bromirski lab route-server (IPv6 feed)"
[neighbors.timers.config]
keepalive-interval = 3600
hold-time = 7200
[neighbors.ebgp-multihop.config]
enabled = true
multihop-ttl = 64
[neighbors.transport.config]
passive-mode = false
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv6-unicast"
# --- BMP export to the OpenBMP collector ------------------------------------
# GoBGP connects OUT to the collector. "obmp-collector" resolves on the shared
# compose network; port 5000 is the collector's BMP listener.
# route-monitoring-policy = "pre-policy" exports the Adj-RIB-In (received
# routes, pre import-policy) -- consistent with the rest of the OpenBMP fleet.
[[bmp-servers]]
[bmp-servers.config]
address = "obmp-collector"
port = 5000
route-monitoring-policy = "pre-policy"
statistics-timeout = 3600

104
gobgp/mrt-refresh.sh Executable file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
#
# mrt-refresh.sh -- MRT full-table fallback loader for the OpenBMP GoBGP feed.
#
# Roadmap E1. The live route server (AS57355) is a single volunteer-run host
# with no SLA. When it is unreachable, the global table in PostgreSQL ip_rib
# would otherwise age out. This script downloads the latest RouteViews full
# MRT RIB dump and injects it into the running gobgpd so the table stays warm.
#
# Designed to be idempotent and cron-safe at a 2-hour cadence:
# - it only downloads a dump it does not already have,
# - it only injects when the live route server session is NOT established,
# - concurrent runs are guarded by a flock.
#
# Run it INSIDE the gobgp container (it shells out to the local `gobgp` CLI):
# docker exec obmp-gobgp /config/mrt-refresh.sh
#
# Example crontab entry on the docker host (every 2 hours):
# 0 */2 * * * docker exec obmp-gobgp /config/mrt-refresh.sh >> /var/log/gobgp-mrt.log 2>&1
set -euo pipefail
# --- tunables ---------------------------------------------------------------
MRT_DIR="${MRT_DIR:-/config/mrt}"
RV_BASE="${RV_BASE:-https://archive.routeviews.org/route-views/bgpdata}"
GOBGP="${GOBGP:-gobgp}"
LOCKFILE="${LOCKFILE:-/tmp/gobgp-mrt-refresh.lock}"
# RouteViews publishes a full RIB dump every 2 hours; dumps land a few minutes
# after the even hour, so we look back a safe margin.
LOOKBACK_HOURS="${LOOKBACK_HOURS:-4}"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
# --- single-instance guard --------------------------------------------------
exec 9>"${LOCKFILE}"
if ! flock -n 9; then
log "another mrt-refresh run is in progress; exiting"
exit 0
fi
mkdir -p "${MRT_DIR}"
# --- skip if the live route server is up ------------------------------------
# If any AS57355 neighbor is Established, the live feed is authoritative and
# we must NOT inject a stale MRT dump on top of it.
if ${GOBGP} neighbor 2>/dev/null | grep -qiE 'Establ'; then
log "a BGP session is Established; live feed is healthy, skipping MRT inject"
exit 0
fi
log "no Established BGP session; proceeding with MRT fallback"
# --- locate the most recent available RIB dump -----------------------------
# RouteViews RIB dumps:
# <RV_BASE>/YYYY.MM/RIBS/rib.YYYYMMDD.HHMM.bz2
# RIB dumps are taken at even hours (00,02,04,...,22) UTC.
found_url=""
found_file=""
now_epoch="$(date -u +%s)"
for ((h = 0; h <= LOOKBACK_HOURS; h++)); do
ts_epoch=$(( now_epoch - h * 3600 ))
hh="$(date -u -d "@${ts_epoch}" +%H)"
# only even hours carry RIB dumps
if (( 10#${hh} % 2 != 0 )); then
continue
fi
ym="$(date -u -d "@${ts_epoch}" +%Y.%m)"
ymd="$(date -u -d "@${ts_epoch}" +%Y%m%d)"
fname="rib.${ymd}.${hh}00.bz2"
url="${RV_BASE}/${ym}/RIBS/${fname}"
if curl -fsI --max-time 30 "${url}" >/dev/null 2>&1; then
found_url="${url}"
found_file="${fname}"
break
fi
done
if [[ -z "${found_url}" ]]; then
log "ERROR: no RouteViews RIB dump found within ${LOOKBACK_HOURS}h lookback"
exit 1
fi
dest="${MRT_DIR}/${found_file}"
# --- download (idempotent) --------------------------------------------------
if [[ -s "${dest}" ]]; then
log "already have ${found_file}; reusing cached copy"
else
log "downloading ${found_url}"
tmp="${dest}.partial.$$"
curl -fsSL --max-time 600 -o "${tmp}" "${found_url}"
mv -f "${tmp}" "${dest}"
log "downloaded $(du -h "${dest}" | cut -f1) -> ${dest}"
fi
# --- inject into the running gobgpd -----------------------------------------
# `gobgp mrt inject global` reads the bz2 dump directly and installs every
# prefix into the global RIB; BMP export to the collector follows automatically.
log "injecting ${found_file} into gobgpd global RIB"
${GOBGP} mrt inject global "${dest}"
log "MRT inject complete"
# --- housekeeping: keep only the 4 most recent dumps ------------------------
( cd "${MRT_DIR}" && ls -1t rib.*.bz2 2>/dev/null | tail -n +5 | xargs -r rm -f )
log "done"