Add ExaBGP route injector, Grafana dashboards, and full documentation
- Add exabgp/ container: ExaBGP 5.x + Flask REST API for on-demand BGP
route injection into CML IOS-XR lab (AS 65020 via eBGP from AS 65100)
- Add 6 injection scenarios: internet_sample, churn, blackhole, anycast,
full_table, lab_prefixes
- Add inject.py CLI wrapper for the ExaBGP API
- Add iosxr_bgp_config.md with IOS-XR neighbor config and NETCONF script
- Add obmp-grafana/ dashboards and provisioning (17 dashboards)
- Update docker-compose.yml: add exabgp service, fix Kafka external
listener IP, extend log retention from 90min to 720min
- Add DOCS.md: full project documentation including architecture, setup,
user guide, sanity checks, troubleshooting, and command reference
- Update .gitignore: exclude .env and .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 14:46:37 -07:00
|
|
|
"""
|
|
|
|
|
BGP Route Injection Scenarios
|
|
|
|
|
|
|
|
|
|
Each scenario is a dict with:
|
|
|
|
|
description: str
|
|
|
|
|
routes: list of route dicts
|
|
|
|
|
|
|
|
|
|
Route dict keys:
|
|
|
|
|
prefix (required) e.g. "1.1.1.0/24"
|
|
|
|
|
next_hop default "self"
|
|
|
|
|
as_path list of ASNs, e.g. [65100, 1299, 15169]
|
|
|
|
|
communities list of strings, e.g. ["65100:100"]
|
|
|
|
|
med int, optional
|
|
|
|
|
local_pref int, optional
|
|
|
|
|
|
|
|
|
|
Well-known transit ASNs used in AS paths:
|
|
|
|
|
174 Cogent
|
|
|
|
|
701 Verizon / MCI
|
|
|
|
|
1299 Telia
|
|
|
|
|
2914 NTT
|
|
|
|
|
3257 GTT
|
|
|
|
|
3356 Lumen / Level3
|
|
|
|
|
6461 Zayo
|
|
|
|
|
6762 Sparkle / Telecom Italia
|
|
|
|
|
7018 AT&T
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Helper to build route dicts quickly
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _r(prefix, as_path, communities=None, med=None, local_pref=None):
|
|
|
|
|
return {
|
|
|
|
|
'prefix': prefix,
|
|
|
|
|
'next_hop': 'self',
|
|
|
|
|
'as_path': as_path,
|
|
|
|
|
'communities': communities or [],
|
|
|
|
|
'med': med,
|
|
|
|
|
'local_pref': local_pref,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: internet_sample
|
|
|
|
|
# Partial internet table — realistic mix of prefix lengths and AS paths.
|
|
|
|
|
# Prefixes are real public ranges with synthetic (but plausible) AS paths.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_INTERNET_V4 = [
|
|
|
|
|
# Cloudflare
|
|
|
|
|
_r('1.1.1.0/24', [65100, 174, 13335]),
|
|
|
|
|
_r('1.0.0.1/32', [65100, 3356, 13335]),
|
|
|
|
|
_r('104.16.0.0/13', [65100, 1299, 13335]),
|
|
|
|
|
_r('104.24.0.0/14', [65100, 6461, 13335]),
|
|
|
|
|
_r('162.158.0.0/15', [65100, 7018, 13335]),
|
|
|
|
|
_r('172.64.0.0/13', [65100, 2914, 13335]),
|
|
|
|
|
_r('198.41.128.0/17', [65100, 3257, 13335]),
|
|
|
|
|
# Google / Alphabet
|
|
|
|
|
_r('8.8.8.0/24', [65100, 3356, 15169]),
|
|
|
|
|
_r('8.8.4.0/24', [65100, 1299, 15169]),
|
|
|
|
|
_r('8.34.208.0/20', [65100, 6762, 15169]),
|
|
|
|
|
_r('34.0.0.0/15', [65100, 701, 15169]),
|
|
|
|
|
_r('35.190.0.0/17', [65100, 2914, 15169]),
|
|
|
|
|
_r('64.233.160.0/19', [65100, 3356, 15169]),
|
|
|
|
|
_r('66.102.0.0/20', [65100, 7018, 15169]),
|
|
|
|
|
_r('74.125.0.0/16', [65100, 174, 15169]),
|
|
|
|
|
_r('142.250.0.0/15', [65100, 3257, 15169]),
|
|
|
|
|
_r('172.217.0.0/16', [65100, 6461, 15169]),
|
|
|
|
|
_r('216.58.192.0/19', [65100, 1299, 15169]),
|
|
|
|
|
# Amazon AWS
|
|
|
|
|
_r('3.0.0.0/15', [65100, 3356, 16509]),
|
|
|
|
|
_r('13.32.0.0/15', [65100, 1299, 16509]),
|
|
|
|
|
_r('52.0.0.0/14', [65100, 6461, 16509]),
|
|
|
|
|
_r('52.84.0.0/15', [65100, 2914, 16509]),
|
|
|
|
|
_r('54.64.0.0/13', [65100, 701, 16509]),
|
|
|
|
|
_r('54.144.0.0/12', [65100, 174, 16509]),
|
|
|
|
|
_r('54.160.0.0/13', [65100, 3257, 16509]),
|
|
|
|
|
_r('99.77.128.0/18', [65100, 7018, 16509]),
|
|
|
|
|
_r('205.251.192.0/18',[65100, 3356, 16509]),
|
|
|
|
|
# Microsoft Azure
|
|
|
|
|
_r('13.64.0.0/11', [65100, 1299, 8075]),
|
|
|
|
|
_r('20.0.0.0/14', [65100, 6762, 8075]),
|
|
|
|
|
_r('20.33.0.0/16', [65100, 3356, 8075]),
|
|
|
|
|
_r('40.64.0.0/10', [65100, 2914, 8075]),
|
|
|
|
|
_r('52.224.0.0/11', [65100, 701, 8075]),
|
|
|
|
|
_r('104.40.0.0/13', [65100, 174, 8075]),
|
|
|
|
|
_r('168.61.0.0/16', [65100, 7018, 8075]),
|
|
|
|
|
# Akamai
|
|
|
|
|
_r('23.0.0.0/12', [65100, 3356, 20940]),
|
|
|
|
|
_r('23.32.0.0/11', [65100, 1299, 20940]),
|
|
|
|
|
_r('23.192.0.0/11', [65100, 6461, 20940]),
|
|
|
|
|
_r('92.122.0.0/15', [65100, 2914, 20940]),
|
|
|
|
|
_r('95.100.0.0/15', [65100, 3257, 20940]),
|
|
|
|
|
_r('184.24.0.0/13', [65100, 7018, 20940]),
|
|
|
|
|
# Fastly CDN
|
|
|
|
|
_r('23.235.32.0/20', [65100, 174, 54113]),
|
|
|
|
|
_r('103.244.50.0/24', [65100, 3356, 54113]),
|
|
|
|
|
_r('151.101.0.0/16', [65100, 1299, 54113]),
|
|
|
|
|
_r('157.52.192.0/18', [65100, 6461, 54113]),
|
|
|
|
|
_r('185.31.16.0/22', [65100, 2914, 54113]),
|
|
|
|
|
_r('199.27.72.0/21', [65100, 701, 54113]),
|
|
|
|
|
# Twitter / X
|
|
|
|
|
_r('104.244.42.0/24', [65100, 3356, 13414]),
|
|
|
|
|
_r('192.133.76.0/22', [65100, 1299, 13414]),
|
|
|
|
|
# Meta / Facebook
|
|
|
|
|
_r('31.13.24.0/21', [65100, 174, 32934]),
|
|
|
|
|
_r('31.13.64.0/18', [65100, 6762, 32934]),
|
|
|
|
|
_r('66.220.144.0/20', [65100, 7018, 32934]),
|
|
|
|
|
_r('69.63.176.0/20', [65100, 2914, 32934]),
|
|
|
|
|
_r('69.171.224.0/19', [65100, 3257, 32934]),
|
|
|
|
|
_r('157.240.0.0/17', [65100, 3356, 32934]),
|
|
|
|
|
_r('185.89.218.0/23', [65100, 701, 32934]),
|
|
|
|
|
_r('204.15.20.0/22', [65100, 1299, 32934]),
|
|
|
|
|
# Apple
|
|
|
|
|
_r('17.0.0.0/8', [65100, 1299, 714]),
|
|
|
|
|
_r('17.172.224.0/19', [65100, 6461, 714]),
|
|
|
|
|
_r('17.178.96.0/19', [65100, 2914, 714]),
|
|
|
|
|
_r('192.35.50.0/24', [65100, 3356, 714]),
|
|
|
|
|
# Comcast
|
|
|
|
|
_r('50.18.0.0/16', [65100, 7018, 7922]),
|
|
|
|
|
_r('73.0.0.0/8', [65100, 174, 7922]),
|
|
|
|
|
_r('96.0.0.0/11', [65100, 3257, 7922]),
|
|
|
|
|
# Verizon
|
|
|
|
|
_r('70.0.0.0/11', [65100, 3356, 701]),
|
|
|
|
|
_r('98.0.0.0/10', [65100, 1299, 701]),
|
|
|
|
|
_r('174.0.0.0/12', [65100, 6461, 701]),
|
|
|
|
|
# Generic transit destinations for AS path variety
|
|
|
|
|
_r('5.0.0.0/16', [65100, 1299, 6762, 34984]),
|
|
|
|
|
_r('45.86.0.0/16', [65100, 3257, 9002, 51847]),
|
|
|
|
|
_r('80.64.0.0/18', [65100, 174, 1239, 34224]),
|
|
|
|
|
_r('82.112.0.0/15', [65100, 6461, 5400, 12322]),
|
|
|
|
|
_r('89.0.0.0/17', [65100, 2914, 3491, 8551]),
|
|
|
|
|
_r('91.108.4.0/22', [65100, 701, 9002, 42831]),
|
|
|
|
|
_r('141.0.0.0/16', [65100, 7018, 1239, 6830]),
|
|
|
|
|
_r('185.0.0.0/22', [65100, 3356, 5400, 44946]),
|
|
|
|
|
_r('195.0.0.0/21', [65100, 1299, 3491, 30781]),
|
|
|
|
|
_r('212.0.0.0/16', [65100, 6762, 9002, 3301]),
|
|
|
|
|
_r('217.0.0.0/20', [65100, 174, 1239, 25160]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
_INTERNET_V6 = [
|
|
|
|
|
_r('2001:4860::/32', [65100, 3356, 15169]), # Google
|
|
|
|
|
_r('2001:4860:4860::/48', [65100, 1299, 15169]),
|
|
|
|
|
_r('2606:4700::/32', [65100, 174, 13335]), # Cloudflare
|
|
|
|
|
_r('2606:4700:4700::/48', [65100, 3356, 13335]),
|
|
|
|
|
_r('2400:cb00::/32', [65100, 2914, 13335]),
|
|
|
|
|
_r('2620:0:2d0::/48', [65100, 701, 2906]), # Netflix
|
|
|
|
|
_r('2600::/23', [65100, 6461, 16509]), # Amazon
|
|
|
|
|
_r('2a00:1450::/32', [65100, 1299, 15169]), # Google EU
|
|
|
|
|
_r('2001:8d8::/32', [65100, 3257, 20940]), # Akamai
|
|
|
|
|
_r('2620:1ec::/36', [65100, 7018, 8075]), # Microsoft
|
|
|
|
|
_r('2a03:2880::/32', [65100, 3356, 32934]), # Meta
|
|
|
|
|
_r('2001:df0::/32', [65100, 2914, 4837]), # China Unicom
|
|
|
|
|
_r('2001:500::/30', [65100, 174, 3356]), # ARIN
|
|
|
|
|
_r('2001:db8::/32', [65100, 1299, 65001]), # Documentation (RFC 3849)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: churn
|
|
|
|
|
# 30 prefixes designed to be announced then withdrawn repeatedly.
|
|
|
|
|
# Load with /scenario/churn, withdraw with DELETE /scenario/churn.
|
|
|
|
|
# Run announce→withdraw→announce cycles to populate ip_rib_log.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_CHURN_PREFIXES = [
|
|
|
|
|
'198.51.100.0/24', # RFC 5737 documentation space
|
|
|
|
|
'198.51.101.0/24',
|
|
|
|
|
'198.51.102.0/24',
|
|
|
|
|
'198.51.103.0/24',
|
|
|
|
|
'198.51.104.0/24',
|
|
|
|
|
'198.51.105.0/24',
|
|
|
|
|
'198.51.106.0/24',
|
|
|
|
|
'198.51.107.0/24',
|
|
|
|
|
'198.51.108.0/24',
|
|
|
|
|
'198.51.109.0/24',
|
|
|
|
|
'203.0.113.0/24', # RFC 5737 documentation space
|
|
|
|
|
'203.0.113.1/32',
|
|
|
|
|
'203.0.113.2/32',
|
|
|
|
|
'203.0.113.3/32',
|
|
|
|
|
'203.0.113.4/32',
|
|
|
|
|
'100.64.0.0/24', # RFC 6598 shared address space
|
|
|
|
|
'100.64.1.0/24',
|
|
|
|
|
'100.64.2.0/24',
|
|
|
|
|
'100.64.3.0/24',
|
|
|
|
|
'100.64.4.0/24',
|
|
|
|
|
'192.0.2.0/24', # RFC 5737
|
|
|
|
|
'192.0.2.128/25',
|
|
|
|
|
'192.0.2.0/25',
|
|
|
|
|
'192.0.3.0/24',
|
|
|
|
|
'192.0.4.0/24',
|
|
|
|
|
'192.0.5.0/24',
|
|
|
|
|
'192.0.6.0/24',
|
|
|
|
|
'192.0.7.0/24',
|
|
|
|
|
'192.0.8.0/24',
|
|
|
|
|
'192.0.9.0/24',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
_CHURN_ROUTES = [
|
|
|
|
|
_r(p, [65100, 65200], communities=['65100:200'])
|
|
|
|
|
for p in _CHURN_PREFIXES
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: blackhole
|
|
|
|
|
# Prefixes with RTBH (Remotely Triggered Black Hole) community.
|
|
|
|
|
# Community 65100:666 signals black-hole intent.
|
|
|
|
|
# Also includes the well-known BLACKHOLE community (65535:666).
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_BLACKHOLE_ROUTES = [
|
|
|
|
|
_r('192.0.2.1/32', [65100], communities=['65100:666', '65535:666']),
|
|
|
|
|
_r('192.0.2.2/32', [65100], communities=['65100:666', '65535:666']),
|
|
|
|
|
_r('192.0.2.3/32', [65100], communities=['65100:666', '65535:666']),
|
|
|
|
|
_r('198.51.100.1/32',[65100], communities=['65100:666', '65535:666']),
|
|
|
|
|
_r('198.51.100.2/32',[65100], communities=['65100:666', '65535:666']),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: anycast
|
|
|
|
|
# Same three prefixes announced with different AS paths and MEDs —
|
|
|
|
|
# simulates anycast competition (best-path selection testing).
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_ANYCAST_ROUTES = [
|
|
|
|
|
# Anycast prefix 1 — two paths, different MED
|
|
|
|
|
_r('192.0.2.0/24', [65100, 65300], med=100),
|
|
|
|
|
# Anycast prefix 2 — longer AS path
|
|
|
|
|
_r('198.51.100.0/24', [65100, 65300, 65400], med=200),
|
|
|
|
|
# Anycast prefix 3 — shorter AS path, preferred
|
|
|
|
|
_r('203.0.113.0/24', [65100, 65200], med=50),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: full_table
|
|
|
|
|
# 500+ prefixes simulating a large partial internet table.
|
|
|
|
|
# Built by expanding internet_sample with synthetic /24s.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _gen_full_table():
|
|
|
|
|
routes = list(_INTERNET_V4) + list(_INTERNET_V6)
|
|
|
|
|
# Add synthetic /24 blocks from 100.x.x.0/24 space (RFC 6598)
|
|
|
|
|
transit_paths = [
|
|
|
|
|
[65100, 1299, 7922],
|
|
|
|
|
[65100, 3356, 16509],
|
|
|
|
|
[65100, 174, 15169],
|
|
|
|
|
[65100, 6461, 32934],
|
|
|
|
|
[65100, 2914, 8075],
|
|
|
|
|
[65100, 7018, 20940],
|
|
|
|
|
[65100, 1299, 54113],
|
|
|
|
|
[65100, 3356, 13335],
|
|
|
|
|
]
|
|
|
|
|
for i in range(100, 200):
|
|
|
|
|
for j in range(0, 256, 8):
|
|
|
|
|
path = transit_paths[((i - 100) + (j // 8)) % len(transit_paths)]
|
|
|
|
|
origin = 64512 + ((i * 32 + j // 8) % 1023)
|
|
|
|
|
routes.append(_r(f'100.{i}.{j}.0/24', path + [origin]))
|
|
|
|
|
return routes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: lab_prefixes
|
|
|
|
|
# Mimics realistic enterprise/SP routes your lab routers would see.
|
|
|
|
|
# Useful for testing policy: communities, local-pref, AS path filtering.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_LAB_ROUTES = [
|
|
|
|
|
# Customer routes (shorter AS path, higher local-pref via community)
|
|
|
|
|
_r('10.200.0.0/24', [65100, 65500], communities=['65100:100'], local_pref=200),
|
|
|
|
|
_r('10.200.1.0/24', [65100, 65500], communities=['65100:100'], local_pref=200),
|
|
|
|
|
_r('10.200.2.0/24', [65100, 65500], communities=['65100:100'], local_pref=200),
|
|
|
|
|
# Peer routes (medium preference)
|
|
|
|
|
_r('10.201.0.0/22', [65100, 65600], communities=['65100:200'], local_pref=150),
|
|
|
|
|
_r('10.201.4.0/22', [65100, 65600], communities=['65100:200'], local_pref=150),
|
|
|
|
|
# Transit routes (longer path, lower preference)
|
|
|
|
|
_r('10.202.0.0/20', [65100, 1299, 65700], communities=['65100:300'], local_pref=100),
|
|
|
|
|
_r('10.202.16.0/20', [65100, 3356, 65700], communities=['65100:300'], local_pref=100),
|
|
|
|
|
# Default route
|
|
|
|
|
_r('0.0.0.0/0', [65100, 3356], communities=['65100:400']),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
Add Phase 2: Vue 3 control panel, 6 learning dashboards, new BGP scenarios
- exabgp-ui/: Vue 3 + Vite SPA served by NGINX on :5001; proxies /api/ to
ExaBGP Flask on :5050; includes StatusBar, ScenarioPanel, RouteTable,
AnnounceForm, PeerStatus, ChurnControl components
- docker-compose.yml: add obmp-exabgp-ui service (host network, port 5001)
- exabgp/scenarios/__init__.py: add convergence_test, route_leak,
hijack_simulation scenarios for structured BGP learning exercises
- exabgp/inject.py: add 'peers' and 'monitor' subcommands; live-refresh
terminal status view with ANSI cursor repositioning
- obmp-grafana/dashboards/Learning/: 6 new OBMP-Learning dashboards
(update rate, peer health, AS path, RPKI, churn, attributes)
- obmp-grafana/provisioning/dashboards/openbmp-dashboards.yml: add
OpenBMP-Learning folder provider pointing to dashboards/Learning/
- DOCS.md: document Web UI, 3 new scenarios, 6 learning dashboards;
fix section numbering (10-14) and architecture diagram (23 dashboards)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:37:16 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Registry
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: convergence_test
|
|
|
|
|
# 10 prefixes for timing BGP convergence.
|
|
|
|
|
# Announce with inject.py, observe arrival in ip_rib_log, then withdraw.
|
|
|
|
|
# Convergence time = delta between first announcement and stable state.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_CONVERGENCE_ROUTES = [
|
|
|
|
|
_r('192.168.100.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.101.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.102.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.103.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.104.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.105.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.106.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.107.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.108.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
_r('192.168.109.0/24', [65100, 65200], communities=['65100:convergence']),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: route_leak
|
|
|
|
|
# Simulates a route leak: real internet prefixes re-announced with a short
|
|
|
|
|
# (direct) AS path, as if an intermediate AS leaked them without proper
|
|
|
|
|
# filtering. Community 65100:999 tags these as "leaked".
|
|
|
|
|
# Learning: shows how a shorter AS path wins best-path selection even when
|
|
|
|
|
# the origin is unexpected. Watch the Grafana AS Path dashboard.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_ROUTE_LEAK_ROUTES = [
|
|
|
|
|
# Real prefixes, but announced with a single-hop path (leak simulation)
|
|
|
|
|
_r('8.8.8.0/24', [65100, 15169], communities=['65100:999']), # Google DNS — legit origin
|
|
|
|
|
_r('1.1.1.0/24', [65100, 13335], communities=['65100:999']), # Cloudflare — legit origin
|
|
|
|
|
_r('208.67.222.0/24', [65100, 36692], communities=['65100:999']), # OpenDNS
|
|
|
|
|
_r('9.9.9.0/24', [65100, 19281], communities=['65100:999']), # Quad9
|
|
|
|
|
_r('4.2.2.0/24', [65100, 3356], communities=['65100:999']), # Level3 DNS (leaked from transit)
|
|
|
|
|
_r('64.6.64.0/24', [65100, 19262], communities=['65100:999']), # Verisign
|
|
|
|
|
_r('156.154.70.0/24', [65100, 19318], communities=['65100:999']), # Neustar
|
|
|
|
|
_r('195.46.39.0/24', [65100, 21414], communities=['65100:999']), # SafeDNS
|
|
|
|
|
_r('216.146.35.0/24', [65100, 36692], communities=['65100:999']), # Dyn/Oracle
|
|
|
|
|
_r('77.88.8.0/24', [65100, 13238], communities=['65100:999']), # Yandex DNS
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Scenario: hijack_simulation
|
|
|
|
|
# Simulates a BGP prefix hijack: ExaBGP (AS 65100) announces a subset of
|
|
|
|
|
# the internet_sample prefixes with a *shorter* AS path than the legitimate
|
|
|
|
|
# announcements, mimicking an attacker claiming ownership.
|
|
|
|
|
# Community 65100:hijack marks these entries.
|
|
|
|
|
# Learning: demonstrates why shorter AS paths win, how RPKI prevents this,
|
|
|
|
|
# and why origin AS validation matters.
|
|
|
|
|
# Watch ip_rib on the CORE routers: the hijack paths should become bestpaths
|
|
|
|
|
# if they have a shorter AS path length than the existing legitimate routes.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_HIJACK_ROUTES = [
|
|
|
|
|
# Announcing Google prefixes as if originated directly from AS 65100
|
|
|
|
|
# (shorter path = wins best-path selection over the legitimate 3-hop paths)
|
|
|
|
|
_r('8.8.8.0/24', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
_r('8.8.4.0/24', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
_r('1.1.1.0/24', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
_r('104.16.0.0/13', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
_r('172.217.0.0/16', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
# Announcing AWS prefixes
|
|
|
|
|
_r('52.0.0.0/14', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
_r('54.64.0.0/13', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
# Announcing Azure prefixes
|
|
|
|
|
_r('40.64.0.0/10', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
_r('13.64.0.0/11', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
# Announcing Cloudflare prefixes
|
|
|
|
|
_r('162.158.0.0/15', [65100], communities=['65100:hijack', '65100:999']),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
Add ExaBGP route injector, Grafana dashboards, and full documentation
- Add exabgp/ container: ExaBGP 5.x + Flask REST API for on-demand BGP
route injection into CML IOS-XR lab (AS 65020 via eBGP from AS 65100)
- Add 6 injection scenarios: internet_sample, churn, blackhole, anycast,
full_table, lab_prefixes
- Add inject.py CLI wrapper for the ExaBGP API
- Add iosxr_bgp_config.md with IOS-XR neighbor config and NETCONF script
- Add obmp-grafana/ dashboards and provisioning (17 dashboards)
- Update docker-compose.yml: add exabgp service, fix Kafka external
listener IP, extend log retention from 90min to 720min
- Add DOCS.md: full project documentation including architecture, setup,
user guide, sanity checks, troubleshooting, and command reference
- Update .gitignore: exclude .env and .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 14:46:37 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Registry
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
SCENARIOS = {
|
|
|
|
|
'internet_sample': {
|
|
|
|
|
'description': 'Partial internet table (~80 IPv4 + 14 IPv6 prefixes with realistic AS paths)',
|
|
|
|
|
'routes': _INTERNET_V4 + _INTERNET_V6,
|
|
|
|
|
},
|
|
|
|
|
'churn': {
|
|
|
|
|
'description': '30 RFC documentation prefixes for announce/withdraw churn testing',
|
|
|
|
|
'routes': _CHURN_ROUTES,
|
|
|
|
|
},
|
|
|
|
|
'blackhole': {
|
|
|
|
|
'description': '5 /32 prefixes with RTBH community (65100:666 + 65535:666)',
|
|
|
|
|
'routes': _BLACKHOLE_ROUTES,
|
|
|
|
|
},
|
|
|
|
|
'anycast': {
|
|
|
|
|
'description': '3 prefixes with varying AS paths and MEDs for best-path testing',
|
|
|
|
|
'routes': _ANYCAST_ROUTES,
|
|
|
|
|
},
|
|
|
|
|
'full_table': {
|
|
|
|
|
'description': '500+ prefixes simulating a large partial internet table',
|
|
|
|
|
'routes': _gen_full_table(),
|
|
|
|
|
},
|
|
|
|
|
'lab_prefixes': {
|
|
|
|
|
'description': 'Enterprise/SP-style routes with communities and local-pref for policy testing',
|
|
|
|
|
'routes': _LAB_ROUTES,
|
|
|
|
|
},
|
Add Phase 2: Vue 3 control panel, 6 learning dashboards, new BGP scenarios
- exabgp-ui/: Vue 3 + Vite SPA served by NGINX on :5001; proxies /api/ to
ExaBGP Flask on :5050; includes StatusBar, ScenarioPanel, RouteTable,
AnnounceForm, PeerStatus, ChurnControl components
- docker-compose.yml: add obmp-exabgp-ui service (host network, port 5001)
- exabgp/scenarios/__init__.py: add convergence_test, route_leak,
hijack_simulation scenarios for structured BGP learning exercises
- exabgp/inject.py: add 'peers' and 'monitor' subcommands; live-refresh
terminal status view with ANSI cursor repositioning
- obmp-grafana/dashboards/Learning/: 6 new OBMP-Learning dashboards
(update rate, peer health, AS path, RPKI, churn, attributes)
- obmp-grafana/provisioning/dashboards/openbmp-dashboards.yml: add
OpenBMP-Learning folder provider pointing to dashboards/Learning/
- DOCS.md: document Web UI, 3 new scenarios, 6 learning dashboards;
fix section numbering (10-14) and architecture diagram (23 dashboards)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:37:16 -07:00
|
|
|
'convergence_test': {
|
|
|
|
|
'description': '10 prefixes for BGP convergence timing — announce, observe ip_rib_log, withdraw',
|
|
|
|
|
'routes': _CONVERGENCE_ROUTES,
|
|
|
|
|
},
|
|
|
|
|
'route_leak': {
|
|
|
|
|
'description': '10 real prefixes re-announced with short AS paths — simulates a route leak (community 65100:999)',
|
|
|
|
|
'routes': _ROUTE_LEAK_ROUTES,
|
|
|
|
|
},
|
|
|
|
|
'hijack_simulation': {
|
|
|
|
|
'description': '10 prefixes announced as if directly originated by AS 65100 — simulates a prefix hijack (community 65100:hijack)',
|
|
|
|
|
'routes': _HIJACK_ROUTES,
|
|
|
|
|
},
|
Add ExaBGP route injector, Grafana dashboards, and full documentation
- Add exabgp/ container: ExaBGP 5.x + Flask REST API for on-demand BGP
route injection into CML IOS-XR lab (AS 65020 via eBGP from AS 65100)
- Add 6 injection scenarios: internet_sample, churn, blackhole, anycast,
full_table, lab_prefixes
- Add inject.py CLI wrapper for the ExaBGP API
- Add iosxr_bgp_config.md with IOS-XR neighbor config and NETCONF script
- Add obmp-grafana/ dashboards and provisioning (17 dashboards)
- Update docker-compose.yml: add exabgp service, fix Kafka external
listener IP, extend log retention from 90min to 720min
- Add DOCS.md: full project documentation including architecture, setup,
user guide, sanity checks, troubleshooting, and command reference
- Update .gitignore: exclude .env and .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 14:46:37 -07:00
|
|
|
}
|