Building your own eyes¶
Borrowed eyes are quick, and they are someone else’s logs. A rig of your own is slower to stand up and answers to nobody but you. None of the pieces below need a budget or a peering to start: the first one needs a network connection and about ten lines of Python.
If you would rather not assemble it piece by piece, huginn-and-muninn is a small, local take on exactly this: a listening eye, a snapshot looking glass, an RPKI check and a historical recall, mostly standard library. The rest of this page is on how the pieces work, so you can build or extend your own.
A listening eye, in ten lines¶
RIS Live is RIPE’s public websocket of BGP updates seen across its collectors. Subscribe to a prefix, and it streams every announcement and withdrawal touching it, as JSON, from vantages worldwide. You announce nothing; you only listen.
pip install websocket-client
import json, websocket
PREFIX = "203.0.113.0/24"
ws = websocket.create_connection("wss://ris-live.ripe.net/v1/ws/?client=eyes-demo")
ws.send(json.dumps({"type": "ris_subscribe",
"data": {"prefix": PREFIX, "moreSpecific": True}}))
while True:
msg = json.loads(ws.recv())
if msg["type"] != "ris_message":
continue
d = msg["data"]
for ann in d.get("announcements", []):
for pfx in ann["prefixes"]:
print(d["timestamp"], "A", pfx, "path", d.get("path"), "seen by", d["peer"])
for pfx in d.get("withdrawals", []):
print(d["timestamp"], "W", pfx, "seen by", d["peer"])
Run it, announce a more-specific of the prefix from somewhere, and the line turns up here with your origin
at the end of the path. That is a working eye. If you would rather not write the loop, pybgpstream wraps
the same live and archived feeds behind one API:
from pybgpstream import BGPStream
stream = BGPStream(project="ris-live", filter="prefix more 203.0.113.0/24")
for e in stream:
print(e.time, e.type, e.fields.get("prefix"), e.fields.get("as-path"))
A collector of your own¶
When a table serves better than a stream, run a speaker that holds a RIB you can query. GoBGP is the least fuss. Point it at a neighbour you have (a route server, a tolerant peer, or your own lab fabric) in passive mode, so it only ever receives:
# gobgpd.conf
[global.config]
as = 65000
router-id = "192.0.2.1"
[[neighbors]]
[neighbors.config]
neighbor-address = "10.0.0.13"
peer-as = 65001
[neighbors.transport.config]
passive-mode = true
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv4-unicast"
gobgpd -f gobgpd.conf &
gobgp global rib -a ipv4 # the whole table you received
gobgp global rib 203.0.113.0/24 # one prefix, every path you saw
No neighbour to spare? Take the view straight off a router you already run. FRR (or any Cisco/Juniper box) will stream its tables over BMP to a collector, with no CLI scraping. Add a BMP listener to the same gobgpd:
[[bmp-servers]]
[bmp-servers.config]
address = "0.0.0.0"
port = 11019
! on the FRR router, under: router bgp 65001
bmp targets EYES
bmp connect 192.0.2.1 port 11019 min-retry 1000 max-retry 5000
bmp monitor ipv4 unicast pre-policy
Knowing what the table thinks of you¶
Propagation is half the question; validity the other half. Routinator turns the published ROAs into a verdict for a prefix and origin:
routinator init --accept-arin-rpa
routinator vrps # every validated payload
routinator validate --asn 65020 --prefix 203.0.113.0/25
# -> Valid | Invalid | NotFound
Run it before announcing to see whether an origin reads as invalid anywhere that enforces, and afterwards to see whether a more-specific slipped through a gap. It is the same validator a careful network runs to protect itself, which is the running joke of this section.
The record¶
For “what did it look like an hour ago”, the archives. RouteViews and RIS publish MRT dumps, both RIB snapshots and update streams, going back years:
wget https://routeviews.org/route-views2/bgpdata/2026.06/UPDATES/updates.20260613.0800.bz2
bgpdump -m updates.20260613.0800.bz2 | grep '203\.0\.113'
-m prints one machine-readable line per record: timestamp, peer, AS path, prefix. mrtparse and
pybgpstream read the same files if you would rather stay in Python.
Two things no command fixes¶
A single collector sees the table its neighbours offered, not the one everyone else sees. More sight comes from more feeds, never from inventing routes the collector did not receive: the moment the view is fabricated it stops being an eye and becomes a mirror. And the footprint moves rather than vanishes. A peering is a relationship someone can name; a RIS Live subscription is a connection a public service logs. Listening is quieter than announcing, but quiet is relative, and the view cuts both ways.