On this page

bgplay·vis

Live BGP route collector + visualizer. Point your routers at it, it keeps per-peer adj-rib-in tables, and you watch announces / withdrawals / path changes fly around an animated AS graph in your browser.

        ┌─────────┐   BGP :179    ┌──────────────┐   ws :8080   ┌──────────┐
        │ router1 │ ────────────▶ │              │ ───────────▶ │ browser  │
        │ router2 │ ────────────▶ │  bgplay-vis  │              │ (canvas) │
        │ router3 │ ────────────▶ │  AS-graph UI │ ◀─────────── │ search   │
        └─────────┘               └──────────────┘   /api/*     └──────────┘
  • Passive BGP speaker — receives everything, never originates routes.
  • IPv4 + IPv6 unicast, 4-byte ASNs, add-path receive (multiple paths per prefix from route servers), MP-BGP, extended message length (RFC 8654), graceful-restart tolerant.
  • Routes kept per-peer (adj-rib-in); session drops mark routes stale instead of dropping them, resync flushes what wasn't refreshed.
  • Live canvas: local AS centered, peers on ring 1, transit/origin ASes on outer rings; announces stream in along the AS path and settle as glowing dots orbiting their origin; withdrawals race back out in red.
  • Search by prefix (1.1.1.0/24), bare IP (8.8.8.8 → LPM chain), AS number (as13335 → every route through it), community (community:64512:100), or peer name. Results group by prefix and badge the best path per group.
  • Pause/resume the live stream; missed events are caught up via the event-history API.
  • Every event is retained in a 4096-event ring — replayable over /api/events or the snapshot.recent field — ready for a future bgpath archiver to tail.

Run

go build -o bgplay-vis ./cmd/bgplay-vis
./bgplay-vis -asn 65000 -rid 10.0.0.5 -bgp :179 -web :8080

Port 179 is privileged → run as root, or setcap 'cap_net_bind_service=+ep' bgplay-vis, or just run -bgp :1179 and point routers at port 1179.

flag default meaning
-asn 65000 local ASN presented in OPEN
-rid 192.0.2.1 BGP router-id
-bgp :179 listen addr (-bgp= disables)
-peer — outbound session, repeatable (-peer 192.0.2.1)
-hold 90 holdtime seconds (0 disables keepalives)
-web :8080 UI address
-demo off synthetic feed, no router needed
-md5 — BGP TCP-MD5SIG key ip=password, repeatable (Linux)
-allow — inbound BGP allowlist CIDR, repeatable (empty = all)
-max-routes 0 cap on stored routes, 0 = unlimited
-max-peer-routes 0 cap on routes per peer, 0 = unlimited
-max-inbound 512 max concurrent inbound BGP connections
-token — require token on /api/* and /ws (?token= or Authorization: Bearer)
-origin — allowed Origin host for /ws, repeatable (suffix match)
-cors off send Access-Control-Allow-Origin: * on the JSON API

Try it instantly: ./bgplay-vis -demo -bgp :1179 → http://localhost:8080

Access control

The UI and API are read-only but expose your full routing table, so lock them down on untrusted networks:

  • -token s3cret — every /api/* (except /api/health) and /ws requires ?token=s3cret or a Bearer header. In the browser, open https://host/?token=s3cret once; it's kept in sessionStorage.
  • /ws rejects cross-site browser connections by default: the Origin host must match the request Host. -origin example.com replaces that with an explicit allowlist (with .example.com subdomain matching) for deployments served under multiple names. Non-browser clients send no Origin and are unaffected.
  • -md5 192.0.2.1=sharedsecret — TCP-MD5SIG on the BGP session itself (Linux only; works with the file capability already used for :179).
  • -allow 10.0.0.0/8 — only accept BGP TCP from these prefixes.

/api/health is unauthenticated by design (container/load-balancer liveness probe, returns {"ok":true}).

Docker

docker build -t bgplay-vis .
docker run -d -p 179:179 -p 8080:8080 bgplay-vis -asn 65000

or docker compose up -d. Runs as a non-root user with all capabilities dropped except NET_BIND_SERVICE (file cap on the binary), read_only, mem_limit/pids_limit, json-file log rotation, and a built-in HEALTHCHECK against /api/health. Stateless — everything is in memory.

Install (Debian/Ubuntu, one command)

sudo deploy/install-debian.sh --domain bgplay.example.com --email [email protected]

Builds from source (installs a checksummed Go toolchain if needed), drops a systemd unit with AmbientCapabilities=CAP_NET_BIND_SERVICE and all flags baked into ExecStart, and front-ends the UI with nginx + certbot (443 with a 80→443 redirect). Without --domain it serves plain HTTP on :80.

Flags: --asn, --rid, --bgp, --peer, --demo, --md5 ip=pass, --allow CIDR, --max-routes, --max-peer-routes, --max-inbound, --token, --origin, --cors, --tls none|letsencrypt, --uninstall.

Router config

The collector is a passive iBGP-style neighbor. It only receives — give it a session that exports your table (full view, or a route-server feed).

FRR (router connects to collector):

router bgp 64512
 neighbor 10.0.0.5 remote-as 65000
 neighbor 10.0.0.5 description bgplay-vis
 address-family ipv4 unicast
  neighbor 10.0.0.5 activate
  neighbor 10.0.0.5 addpath-tx-all-paths   ! optional: all paths, not just best
 exit-address-family

BIRD:

protocol bgp bgplay {
  local as 64512;
  neighbor 10.0.0.5 port 179 as 65000;
  ipv4 { export all; };
  ipv6 { export all; };
  add paths tx;          # optional
}

pathvector: add a peer block — it renders the same BIRD config:

peers:
  bgplay-vis:
    asn: 65000
    neighbors: ["10.0.0.5"]
    type: upstream        # exports the full table toward it
    add-path: true        # if your pathvector version supports it

Or let the collector dial out instead: ./bgplay-vis -peer r1.example.net -peer r2.example.net (configure neighbor … passive on the routers).

API

endpoint returns
GET /ws websocket: snapshot then batched events + 1s stats. Optional filters: ?pfx=, ?asn=, ?peer= (repeatable/combinable, applied server-side before send). Origin-checked (same-origin by default)
GET /api/stats counters + peer table
GET /api/peers peer table only
GET /api/search?q= prefix / IP / asNNNN / community:x:y / peer-name → matching routes, best-path grouped client-side
GET /api/events?since=<seq>&limit=<n> retained event history (ring of 4096), oldest→newest
GET /api/metrics uptime, route/peer/event counters, ws client count
GET /api/health {"ok":true} liveness probe (unauthenticated)

/api/search is rate-limited per client IP (60/min). The JSON API sends no CORS headers unless -cors is given; the UI is same-origin and doesn't need them. Security headers (nosniff, Referrer-Policy, frame-ancestors, CSP default-src 'self') are set on every response.

Event shape on the wire:

{"seq":4102,"ts":1793403661,"type":"announce","peer":"10.0.0.9","pa":64512,
 "pfx":"203.0.113.0/24","path":[64512,174,64496],"nh":"10.0.0.1","fam":"ipv4-unicast"}

type ∈ announce | withdraw | change | peer_up | peer_down | peer_state | eor | flush.

Limits & failure modes

Everything is bounded so a hostile or buggy peer can't take the daemon down:

  • BGP messages capped at 4 KB, or 64 KB once the peer negotiates RFC 8654 extended message length.
  • -max-inbound caps concurrent inbound BGP sessions (semaphore, pre-auth); outbound sessions are per -peer flag.
  • -max-routes / -max-peer-routes cap stored adj-rib-in entries; overflow announces are dropped and counted in stats.dropped.
  • Per-peer route indexes make session teardown, EOR flush and peer search O(peer routes) instead of O(table).
  • Websocket clients capped at 256; slow consumers are disconnected rather than allowed to stall the event loop. Idle clients are pinged every 20 s and dropped after ~2 min of silence.
  • /api/search rate-limited; trie and result scans bounded.
  • HTTP server: ReadHeaderTimeout/ReadTimeout/WriteTimeout/ IdleTimeout all set; hijacked WS conns manage their own deadlines.
  • Inbound source allowlist (-allow) and per-peer TCP-MD5SIG (-md5).

Layout

cmd/bgplay-vis      flags + wiring
internal/bgp        wire format, OPEN caps, UPDATE parser, session FSM, speaker, TCP-MD5SIG
internal/rib        adj-rib-in, diff engine, prefix trie, per-peer index, search
internal/hub        event batching → fanout, replay ring, per-client filters, history
internal/web        embedded UI, REST, hand-rolled websocket, auth/origin/CORS
internal/demo       synthetic feed (-demo)

No external dependencies. go test ./... covers the UPDATE parser (MP-BGP, AS4 merge, add-path, EOR), the session FSM over net.Pipe (including malformed-message handling), RIB diff/caps/search/stale-flush, hub ring wraparound + history + filters, and web auth/security headers. go test -race ./... is clean.

Not (yet) included

BMP (RFC 7854) ingestion, RPKI origin-validation status, an ASN organization/country dataset, and a radix-trie rewrite of the prefix index. The per-bit trie is fine to ~hundreds of thousands of routes; -max-routes bounds worst-case memory until then.