diff --git a/docker-compose.yml b/docker-compose.yml index 8129489..eabffc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/gobgp/README.md b/gobgp/README.md new file mode 100644 index 0000000..88b1b54 --- /dev/null +++ b/gobgp/README.md @@ -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 `, + 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. diff --git a/gobgp/gobgpd.conf b/gobgp/gobgpd.conf new file mode 100644 index 0000000..0041ac3 --- /dev/null +++ b/gobgp/gobgpd.conf @@ -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 diff --git a/gobgp/mrt-refresh.sh b/gobgp/mrt-refresh.sh new file mode 100755 index 0000000..ec857f5 --- /dev/null +++ b/gobgp/mrt-refresh.sh @@ -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: +# /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"