""" 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']), ] # --------------------------------------------------------------------------- # 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']), ] # --------------------------------------------------------------------------- # Scenario: te_community_steering # Routes tagged with TE communities representing different "colors" for # community-based TE policy steering. Shows how communities drive path # selection when routers apply route-policy based on community values. # --------------------------------------------------------------------------- _TE_COMMUNITY_ROUTES = [ # Red paths (community 65020:100) — high-priority, low-latency _r('10.210.0.0/24', [65100, 65020], communities=['65020:100'], med=10), _r('10.210.1.0/24', [65100, 65020], communities=['65020:100'], med=10), _r('10.210.2.0/24', [65100, 65020], communities=['65020:100'], med=10), _r('10.210.3.0/24', [65100, 65020], communities=['65020:100'], med=10), _r('10.210.4.0/24', [65100, 65020], communities=['65020:100'], med=10), # Blue paths (community 65020:200) — bulk transfer, cost-optimized _r('10.220.0.0/24', [65100, 65020, 3356], communities=['65020:200'], med=100), _r('10.220.1.0/24', [65100, 65020, 3356], communities=['65020:200'], med=100), _r('10.220.2.0/24', [65100, 65020, 3356], communities=['65020:200'], med=100), _r('10.220.3.0/24', [65100, 65020, 3356], communities=['65020:200'], med=100), _r('10.220.4.0/24', [65100, 65020, 3356], communities=['65020:200'], med=100), # Green paths (community 65020:300) — backup/diverse paths _r('10.230.0.0/24', [65100, 65020, 1299, 6762], communities=['65020:300'], med=200), _r('10.230.1.0/24', [65100, 65020, 1299, 6762], communities=['65020:300'], med=200), _r('10.230.2.0/24', [65100, 65020, 1299, 6762], communities=['65020:300'], med=200), _r('10.230.3.0/24', [65100, 65020, 1299, 6762], communities=['65020:300'], med=200), _r('10.230.4.0/24', [65100, 65020, 1299, 6762], communities=['65020:300'], med=200), ] # --------------------------------------------------------------------------- # Scenario: origin_shift # Simulates an origin AS change: prefixes initially associated with # well-known origin ASNs are re-announced with a different origin. # Use: load internet_sample first, then load origin_shift to see the # origin_as column change in ip_rib_log (visible on Anomaly dashboard). # --------------------------------------------------------------------------- _ORIGIN_SHIFT_ROUTES = [ # These prefixes overlap with internet_sample but have different origin ASNs _r('8.8.8.0/24', [65100, 64999], communities=['65100:origin-shift']), # was 15169 (Google) _r('1.1.1.0/24', [65100, 64998], communities=['65100:origin-shift']), # was 13335 (Cloudflare) _r('9.9.9.0/24', [65100, 64997], communities=['65100:origin-shift']), # was 19281 (Quad9) _r('208.67.222.0/24', [65100, 64996], communities=['65100:origin-shift']), # was 36692 (OpenDNS) _r('156.154.70.0/24', [65100, 64995], communities=['65100:origin-shift']), # was 19318 (Neustar) ] # --------------------------------------------------------------------------- # Scenario: path_diversity # Multiple announcements of the same prefix with different AS paths, # MEDs, and communities. Demonstrates best-path selection: # - Shorter AS path wins (unless local-pref overrides) # - Lower MED preferred among paths from same neighbor AS # - Communities tag paths for policy identification # --------------------------------------------------------------------------- _PATH_DIVERSITY_ROUTES = [ # Prefix 1: 3 paths with varying length and MED _r('10.250.0.0/24', [65100, 174], communities=['65100:path-a'], med=50), _r('10.250.0.0/24', [65100, 174, 3356], communities=['65100:path-b'], med=100), _r('10.250.0.0/24', [65100, 174, 3356, 15169], communities=['65100:path-c'], med=150), # Prefix 2: paths with same length but different MED _r('10.250.1.0/24', [65100, 1299, 15169], communities=['65100:low-med'], med=10), _r('10.250.1.0/24', [65100, 3356, 15169], communities=['65100:high-med'], med=500), # Prefix 3: local-pref override (higher local-pref wins over shorter path) _r('10.250.2.0/24', [65100, 2914], communities=['65100:low-lp'], local_pref=50), _r('10.250.2.0/24', [65100, 2914, 7018], communities=['65100:high-lp'], local_pref=200), # Prefix 4: transit diversity _r('10.250.3.0/24', [65100, 174, 32934], communities=['65100:via-cogent']), _r('10.250.3.0/24', [65100, 3356, 32934], communities=['65100:via-lumen']), _r('10.250.3.0/24', [65100, 2914, 32934], communities=['65100:via-ntt']), ] # --------------------------------------------------------------------------- # Registry # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Full Internet Table Generator # Generates realistic-looking IPv4 prefixes across the routable address space # with varied AS paths, prefix lengths, origins, and communities. # Configurable count: 10K (quick test) to 900K+ (full table stress test). # --------------------------------------------------------------------------- # Well-known transit ASNs for realistic path construction _TRANSIT_ASNS = [174, 701, 1299, 2914, 3257, 3356, 6461, 6762, 7018, 3491, 5400, 1239] # Realistic origin ASNs (mix of large providers and small networks) _ORIGIN_POOL = [ 13335, 15169, 16509, 8075, 20940, 32934, 714, 54113, 13414, 7922, 36459, 46489, 14618, 16276, 24940, 47541, 35916, 49981, 9808, 4134, 4837, 9121, 12322, 3320, 6830, 5511, 1273, 6939, 4766, 9318, 23693, 38001, 45102, 58453, 10026, 18881, 28573, 7738, 26599, 8151, 11888, 17676, 4713, 7545, 9299, 50304, 51167, 60068, 41095, 34984, ] # IANA-allocated first octets for routable IPv4 (subset for realism) _ROUTABLE_FIRST_OCTETS = list(range(1, 56)) + list(range(57, 127)) + list(range(128, 224)) def generate_full_internet(count=900000): """Generate a realistic full IPv4 routing table. Distributes prefixes across the IPv4 address space with realistic prefix lengths (/8 through /24) and varied AS paths. Args: count: Number of prefixes to generate (default 900K). Returns: List of route dicts. """ import random rng = random.Random(42) # deterministic for reproducibility routes = [] generated = set() # Prefix length distribution (approximates real DFZ): # /24: ~55%, /23: ~8%, /22: ~7%, /21: ~5%, /20: ~5%, # /19: ~4%, /18: ~3%, /17: ~2%, /16: ~5%, /15-/8: ~6% prefix_len_weights = { 24: 55, 23: 8, 22: 7, 21: 5, 20: 5, 19: 4, 18: 3, 17: 2, 16: 5, 15: 2, 14: 1, 13: 1, 12: 1, 11: 0.5, 10: 0.3, 9: 0.1, 8: 0.1, } plen_choices = list(prefix_len_weights.keys()) plen_weights = list(prefix_len_weights.values()) # AS path length distribution: 1-hop: 5%, 2-hop: 30%, 3-hop: 40%, 4-hop: 20%, 5-hop: 5% path_len_weights = [5, 30, 40, 20, 5] while len(routes) < count: # Pick a routable first octet weighted by allocation density first = rng.choice(_ROUTABLE_FIRST_OCTETS) plen = rng.choices(plen_choices, weights=plen_weights, k=1)[0] # Generate random prefix within this /8 if plen <= 8: prefix = f'{first}.0.0.0/{plen}' elif plen <= 16: second = rng.randint(0, 255) & (0xFF << (16 - plen)) prefix = f'{first}.{second}.0.0/{plen}' elif plen <= 24: second = rng.randint(0, 255) third = rng.randint(0, 255) & (0xFF << (24 - plen)) prefix = f'{first}.{second}.{third}.0/{plen}' else: continue if prefix in generated: continue generated.add(prefix) # Build realistic AS path path_len = rng.choices([1, 2, 3, 4, 5], weights=path_len_weights, k=1)[0] origin = rng.choice(_ORIGIN_POOL) if rng.random() < 0.3 else (64512 + rng.randint(0, 65535 - 64512)) transits = rng.sample(_TRANSIT_ASNS, min(path_len - 1, len(_TRANSIT_ASNS))) as_path = [65100] + transits[:path_len - 1] + [origin] # Occasionally add communities (~20% of routes) communities = [] if rng.random() < 0.2: communities.append(f'65100:{rng.choice([100, 200, 300, 400, 500])}') routes.append(_r(prefix, as_path, communities=communities or None)) return routes 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, }, '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, }, 'te_community_steering': { 'description': 'Routes tagged with TE communities for color-based steering (65020:100=red, 65020:200=blue, 65020:300=green)', 'routes': _TE_COMMUNITY_ROUTES, }, 'origin_shift': { 'description': '5 prefixes with changed origin AS — simulates origin migration/hijack for anomaly detection', 'routes': _ORIGIN_SHIFT_ROUTES, }, 'path_diversity': { 'description': 'Same prefixes with different AS paths and MEDs — demonstrates best-path selection and path diversity', 'routes': _PATH_DIVERSITY_ROUTES, }, }