Add full internet table injection with background worker and progress tracking

Generates realistic IPv4 routing tables (1K-900K prefixes) with DFZ-like
prefix length distribution, varied AS paths, and transit ASN diversity.
Background injection with progress API, CLI follow mode, and Vue UI
component with preset sizes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
sam 2026-05-15 14:22:51 -07:00
parent 1f0936763b
commit d691b512f9
5 changed files with 834 additions and 4 deletions

View File

@ -41,6 +41,7 @@
<AnnounceForm v-else-if="activeTab === 'inject'" @routes-changed="fetchRoutes" />
<PeerStatus v-else-if="activeTab === 'peers'" :peers="peers" />
<ChurnControl v-else-if="activeTab === 'churn'" />
<FullTable v-else-if="activeTab === 'full-table'" @routes-changed="fetchRoutes" />
</div>
</main>
</div>
@ -63,6 +64,7 @@ import RouteTable from './components/RouteTable.vue'
import AnnounceForm from './components/AnnounceForm.vue'
import PeerStatus from './components/PeerStatus.vue'
import ChurnControl from './components/ChurnControl.vue'
import FullTable from './components/FullTable.vue'
const health = ref(null)
const routes = ref([])
@ -75,6 +77,7 @@ const tabs = [
{ id: 'inject', label: 'Inject' },
{ id: 'peers', label: 'Peers' },
{ id: 'churn', label: 'Churn' },
{ id: 'full-table', label: 'Full Table' },
]
async function fetchHealth() {

View File

@ -0,0 +1,477 @@
<template>
<div class="full-table">
<h2 class="section-title">Full Table Injection</h2>
<p class="section-desc">
Inject a realistic IPv4 routing table into ExaBGP for stress testing.
Routes are generated with varied AS paths, prefix lengths, and communities matching real DFZ distribution.
</p>
<div class="config-card">
<!-- Level selector -->
<div class="form-group">
<label>Table Size</label>
<div class="level-grid">
<button
v-for="level in levels"
:key="level.count"
class="level-btn"
:class="{ selected: selectedCount === level.count }"
:disabled="injecting"
@click="selectedCount = level.count"
>
<span class="level-count">{{ level.label }}</span>
<span class="level-desc">{{ level.desc }}</span>
</button>
</div>
</div>
<!-- Custom count -->
<div class="form-group">
<label>Custom Count</label>
<input
v-model.number="selectedCount"
type="number"
min="100"
max="950000"
step="1000"
:disabled="injecting"
class="mono-input"
/>
</div>
<!-- Action buttons -->
<div class="action-row">
<button v-if="!injecting" class="btn-start" @click="startInjection" :disabled="!selectedCount">
<span>&#9654;</span> Inject {{ formatNum(selectedCount) }} Routes
</button>
<button v-else class="btn-stop" @click="stopInjection">
<span>&#9632;</span> Stop Injection
</button>
<button
v-if="!injecting && lastCompleted"
class="btn-withdraw"
@click="withdrawAll"
:disabled="withdrawing"
>
{{ withdrawing ? 'Withdrawing...' : 'Withdraw All' }}
</button>
</div>
</div>
<!-- Status display -->
<div v-if="injecting || statusMsg" class="status-card">
<div class="status-header">
<span class="status-dot" :class="injecting ? 'dot-active' : 'dot-idle'"></span>
<span class="status-text">{{ statusMsg || 'Idle' }}</span>
</div>
<!-- Progress bar -->
<div v-if="state.total > 0" class="progress-section">
<div class="progress-labels">
<span>{{ formatNum(state.injected) }} / {{ formatNum(state.total) }}</span>
<span>{{ state.progress_pct || 0 }}%</span>
</div>
<div class="progress-track">
<div class="progress-fill" :style="{ width: (state.progress_pct || 0) + '%' }"></div>
</div>
</div>
<!-- Stats row -->
<div v-if="state.total > 0" class="stats-row">
<div class="stat-item">
<span class="stat-label">Rate</span>
<span class="stat-val">{{ formatNum(state.rate_pps || 0) }}/s</span>
</div>
<div class="stat-item">
<span class="stat-label">Elapsed</span>
<span class="stat-val">{{ state.elapsed_sec || 0 }}s</span>
</div>
<div class="stat-item">
<span class="stat-label">Active Routes</span>
<span class="stat-val">{{ formatNum(state.active_routes || 0) }}</span>
</div>
</div>
<!-- Error -->
<div v-if="state.error" class="inject-error">{{ state.error }}</div>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import { api } from '../api.js'
const emit = defineEmits(['routes-changed'])
const levels = [
{ count: 1000, label: '1K', desc: 'Quick test' },
{ count: 10000, label: '10K', desc: 'Light load' },
{ count: 50000, label: '50K', desc: 'Medium load' },
{ count: 100000, label: '100K', desc: 'Stress test' },
{ count: 500000, label: '500K', desc: 'Heavy load' },
{ count: 900000, label: '900K', desc: 'Full DFZ' },
]
const selectedCount = ref(10000)
const injecting = ref(false)
const statusMsg = ref('')
const lastCompleted = ref(false)
const withdrawing = ref(false)
const state = ref({})
let pollTimer = null
function formatNum(n) {
if (n == null) return '0'
return Number(n).toLocaleString()
}
async function startInjection() {
try {
statusMsg.value = 'Starting injection...'
injecting.value = true
lastCompleted.value = false
state.value = {}
await api.fullTableStart(selectedCount.value, 1000)
startPolling()
} catch (e) {
statusMsg.value = `Start failed: ${e.message}`
injecting.value = false
}
}
async function stopInjection() {
try {
await api.fullTableStop()
statusMsg.value = 'Stop requested...'
} catch (e) {
statusMsg.value = `Stop failed: ${e.message}`
}
}
async function withdrawAll() {
withdrawing.value = true
try {
const data = await api.withdrawAll()
statusMsg.value = `Withdrew ${data.count} routes`
lastCompleted.value = false
state.value = {}
emit('routes-changed')
} catch (e) {
statusMsg.value = `Withdraw failed: ${e.message}`
} finally {
withdrawing.value = false
}
}
function startPolling() {
stopPolling()
pollStatus()
pollTimer = setInterval(pollStatus, 2000)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
async function pollStatus() {
try {
const data = await api.fullTableStatus()
state.value = data
if (data.active) {
statusMsg.value = `Injecting: ${formatNum(data.injected)} / ${formatNum(data.total)} (${data.rate_pps || 0}/s)`
} else if (data.error) {
statusMsg.value = `Error: ${data.error}`
injecting.value = false
stopPolling()
} else if (data.injected > 0) {
statusMsg.value = `Complete: ${formatNum(data.injected)} routes in ${data.elapsed_sec}s (${data.rate_pps}/s)`
injecting.value = false
lastCompleted.value = true
stopPolling()
emit('routes-changed')
}
} catch (e) {
// keep polling
}
}
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
.full-table {
display: flex;
flex-direction: column;
gap: 18px;
max-width: 680px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.section-desc {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
margin-top: -8px;
}
.config-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
display: flex;
flex-direction: column;
gap: 18px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
label {
font-size: 12px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.level-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.level-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 12px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
transition: all 0.15s;
}
.level-btn:hover:not(:disabled) {
border-color: var(--accent);
background: rgba(79, 156, 249, 0.08);
}
.level-btn.selected {
border-color: var(--accent);
background: rgba(79, 156, 249, 0.15);
box-shadow: 0 0 0 1px var(--accent);
}
.level-count {
font-size: 18px;
font-weight: 700;
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
color: var(--accent);
}
.level-desc {
font-size: 11px;
color: var(--muted);
}
.mono-input {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 7px 10px;
font-size: 13px;
outline: none;
max-width: 200px;
}
.mono-input:focus {
border-color: var(--accent);
}
.action-row {
display: flex;
gap: 10px;
padding-top: 4px;
border-top: 1px solid var(--border);
}
.btn-start {
padding: 9px 22px;
background: rgba(72, 187, 120, 0.15);
color: #48bb78;
border: 1px solid rgba(72, 187, 120, 0.3);
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
gap: 7px;
}
.btn-start:hover:not(:disabled) {
background: rgba(72, 187, 120, 0.25);
}
.btn-stop {
padding: 9px 22px;
background: rgba(252, 129, 129, 0.15);
color: #fc8181;
border: 1px solid rgba(252, 129, 129, 0.3);
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
gap: 7px;
animation: pulse-border 1.5s ease-in-out infinite;
}
.btn-stop:hover {
background: rgba(252, 129, 129, 0.25);
}
.btn-withdraw {
padding: 9px 18px;
background: rgba(246, 173, 85, 0.15);
color: #f6ad55;
border: 1px solid rgba(246, 173, 85, 0.3);
font-weight: 600;
font-size: 13px;
}
.btn-withdraw:hover:not(:disabled) {
background: rgba(246, 173, 85, 0.25);
}
.status-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 12px;
}
.status-header {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-active {
background: #48bb78;
box-shadow: 0 0 8px #48bb78;
animation: pulse-dot 1s ease-in-out infinite;
}
.dot-idle {
background: var(--muted);
}
.status-text {
font-size: 14px;
font-weight: 600;
color: var(--text);
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
}
.progress-section {
display: flex;
flex-direction: column;
gap: 5px;
}
.progress-labels {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--muted);
}
.progress-track {
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent);
border-radius: 3px;
transition: width 0.5s ease;
}
.stats-row {
display: flex;
gap: 24px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-label {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-val {
font-size: 15px;
font-weight: 700;
color: var(--text);
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
}
.inject-error {
font-size: 12px;
color: #fc8181;
padding: 6px 10px;
background: rgba(252, 129, 129, 0.08);
border-radius: 4px;
border: 1px solid rgba(252, 129, 129, 0.2);
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes pulse-border {
0%, 100% { border-color: rgba(252, 129, 129, 0.3); }
50% { border-color: rgba(252, 129, 129, 0.6); }
}
</style>

View File

@ -48,12 +48,16 @@ peer_states = {}
# ExaBGP command helpers
# ---------------------------------------------------------------------------
_quiet_mode = False
def _send(cmd: str):
"""Write a command to ExaBGP via stdout."""
with _stdout_lock:
sys.stdout.write(cmd + '\n')
sys.stdout.flush()
log.info('→ ExaBGP: %s', cmd)
if not _quiet_mode:
log.info('→ ExaBGP: %s', cmd)
def _build_announce(prefix, next_hop='self', as_path=None, communities=None, med=None, local_pref=None):
@ -162,7 +166,22 @@ def api_withdraw_all():
# ---------------------------------------------------------------------------
sys.path.insert(0, '/exabgp')
from scenarios import SCENARIOS
from scenarios import SCENARIOS, generate_full_internet
# ---------------------------------------------------------------------------
# Full-table background injection
# ---------------------------------------------------------------------------
_injection_state = {
'active': False,
'total': 0,
'injected': 0,
'elapsed_sec': 0,
'rate_pps': 0,
'error': None,
'stop_requested': False,
}
_injection_lock = threading.Lock()
@app.route('/scenarios', methods=['GET'])
@ -223,6 +242,131 @@ def get_peers():
return jsonify({'peers': peer_states})
# ---------------------------------------------------------------------------
# Full-table injection endpoints
# ---------------------------------------------------------------------------
def _injection_worker(count, batch_size):
"""Background thread: generate and inject full internet table."""
global _quiet_mode
try:
_quiet_mode = True # suppress per-route logging
log.info('Generating %d full-table prefixes...', count)
routes = generate_full_internet(count)
with _injection_lock:
_injection_state['total'] = len(routes)
log.info('Generated %d routes, starting injection at batch_size=%d', len(routes), batch_size)
start_time = time.time()
for i, route in enumerate(routes):
with _injection_lock:
if _injection_state['stop_requested']:
log.info('Injection stopped by user at %d/%d', i, len(routes))
break
prefix = route['prefix']
announce_route(
prefix,
next_hop=route.get('next_hop', 'self'),
as_path=route.get('as_path', []),
communities=route.get('communities', []),
med=route.get('med'),
local_pref=route.get('local_pref'),
)
# Update progress periodically (every batch_size routes)
if (i + 1) % batch_size == 0:
elapsed = time.time() - start_time
with _injection_lock:
_injection_state['injected'] = i + 1
_injection_state['elapsed_sec'] = round(elapsed, 1)
_injection_state['rate_pps'] = round((i + 1) / elapsed, 1) if elapsed > 0 else 0
log.info('Injection progress: %d/%d (%.0f/s)',
i + 1, len(routes), (i + 1) / elapsed if elapsed > 0 else 0)
elapsed = time.time() - start_time
with _injection_lock:
_injection_state['injected'] = min(i + 1, len(routes))
_injection_state['elapsed_sec'] = round(elapsed, 1)
_injection_state['rate_pps'] = round(_injection_state['injected'] / elapsed, 1) if elapsed > 0 else 0
_injection_state['active'] = False
log.info('Injection complete: %d routes in %.1fs (%.0f/s)',
_injection_state['injected'], elapsed,
_injection_state['injected'] / elapsed if elapsed > 0 else 0)
except Exception as e:
log.error('Injection error: %s', e)
with _injection_lock:
_injection_state['error'] = str(e)
_injection_state['active'] = False
finally:
_quiet_mode = False
@app.route('/full-table/start', methods=['POST'])
def start_full_table():
"""Start background injection of a full IPv4 routing table.
POST body (all optional):
count: Number of prefixes (default 900000)
batch_size: Progress update interval (default 1000)
"""
with _injection_lock:
if _injection_state['active']:
return jsonify({
'error': 'Injection already in progress',
'state': dict(_injection_state),
}), 409
data = request.get_json(force=True) if request.data else {}
count = int(data.get('count', 900000))
batch_size = int(data.get('batch_size', 1000))
with _injection_lock:
_injection_state.update({
'active': True,
'total': count,
'injected': 0,
'elapsed_sec': 0,
'rate_pps': 0,
'error': None,
'stop_requested': False,
})
t = threading.Thread(target=_injection_worker, args=(count, batch_size), daemon=True)
t.start()
log.info('Started full-table injection: %d prefixes', count)
return jsonify({
'status': 'started',
'count': count,
'message': f'Generating and injecting {count} prefixes in background. GET /full-table/status to track progress.',
})
@app.route('/full-table/status', methods=['GET'])
def full_table_status():
"""Get current full-table injection progress."""
with _injection_lock:
state = dict(_injection_state)
if state['total'] > 0:
state['progress_pct'] = round(state['injected'] / state['total'] * 100, 1)
else:
state['progress_pct'] = 0
state['active_routes'] = len(active_routes)
return jsonify(state)
@app.route('/full-table/stop', methods=['POST'])
def stop_full_table():
"""Stop an in-progress full-table injection."""
with _injection_lock:
if not _injection_state['active']:
return jsonify({'error': 'No injection in progress'}), 400
_injection_state['stop_requested'] = True
return jsonify({'status': 'stop_requested', 'injected_so_far': _injection_state['injected']})
# ---------------------------------------------------------------------------
# ExaBGP event loop (main thread)
# ---------------------------------------------------------------------------

View File

@ -12,6 +12,9 @@ Usage:
inject.py withdraw-all
inject.py scenario <name>
inject.py withdraw-scenario <name>
inject.py full-table [--count N] [--follow] # inject full IPv4 table (background)
inject.py full-table-status # show injection progress
inject.py full-table-stop # stop injection
inject.py churn [--count N] [--interval SEC] # cycle announce/withdraw for ip_rib_log population
inject.py monitor # live-refresh terminal view
@ -29,8 +32,8 @@ import requests
API = os.environ.get('EXABGP_API', 'http://localhost:5050')
def _post(path, data=None):
r = requests.post(f'{API}{path}', json=data or {}, timeout=10)
def _post(path, data=None, timeout=10):
r = requests.post(f'{API}{path}', json=data or {}, timeout=timeout)
r.raise_for_status()
return r.json()
@ -174,6 +177,101 @@ def cmd_withdraw_scenario(args):
print(f"Withdrew scenario '{args.name}': {data['count']} routes withdrawn")
def cmd_full_table(args):
"""Inject a full IPv4 routing table for stress testing."""
count = args.count
print(f"Starting full-table injection: {count} prefixes")
print("This generates routes in background. Use 'inject.py full-table-status' to track.\n")
data = _post('/full-table/start', {'count': count, 'batch_size': args.batch_size}, timeout=120)
print(data.get('message', 'Started'))
if args.follow:
print()
_follow_injection()
def cmd_full_table_status(args):
"""Show full-table injection progress."""
data = _get('/full-table/status')
active = data.get('active', False)
total = data.get('total', 0)
injected = data.get('injected', 0)
pct = data.get('progress_pct', 0)
rate = data.get('rate_pps', 0)
elapsed = data.get('elapsed_sec', 0)
error = data.get('error')
active_routes = data.get('active_routes', 0)
if error:
print(f"ERROR: {error}")
elif active:
bar_len = 40
filled = int(bar_len * pct / 100)
bar = '#' * filled + '-' * (bar_len - filled)
print(f"[{bar}] {pct:.1f}%")
print(f" Injected: {injected:,} / {total:,} ({rate:.0f} routes/s)")
print(f" Elapsed: {elapsed:.0f}s")
print(f" Active routes in ExaBGP: {active_routes:,}")
elif total > 0:
print(f"Injection complete: {injected:,} / {total:,} routes in {elapsed:.0f}s ({rate:.0f}/s)")
print(f"Active routes in ExaBGP: {active_routes:,}")
else:
print("No injection running or completed.")
print(f"Active routes: {active_routes:,}")
def cmd_full_table_stop(args):
"""Stop an in-progress full-table injection."""
try:
data = _post('/full-table/stop')
print(f"Stop requested. Injected so far: {data.get('injected_so_far', '?'):,}")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 400:
print("No injection in progress.")
else:
raise
def _follow_injection():
"""Poll injection status until complete."""
import shutil
lines_printed = 0
try:
while True:
data = _get('/full-table/status')
active = data.get('active', False)
total = data.get('total', 0)
injected = data.get('injected', 0)
pct = data.get('progress_pct', 0)
rate = data.get('rate_pps', 0)
elapsed = data.get('elapsed_sec', 0)
active_routes = data.get('active_routes', 0)
# Move cursor up to overwrite
if lines_printed > 0:
print(f"\033[{lines_printed}A", end='')
bar_len = 40
filled = int(bar_len * pct / 100)
bar = '#' * filled + '-' * (bar_len - filled)
output_lines = [
f" [{bar}] {pct:.1f}%",
f" Injected: {injected:,} / {total:,} ({rate:.0f} routes/s) elapsed: {elapsed:.0f}s",
f" Active routes: {active_routes:,}",
]
print('\n'.join(output_lines))
lines_printed = len(output_lines)
if not active:
print(f"\nDone! {injected:,} routes injected in {elapsed:.0f}s")
break
time.sleep(2)
except KeyboardInterrupt:
print("\n\nFollowing stopped (injection continues in background).")
def cmd_churn(args):
"""
Cycle announce/withdraw on the 'churn' scenario to generate ip_rib_log
@ -236,6 +334,17 @@ def main():
p = sub.add_parser('withdraw-scenario', help='Withdraw a named scenario')
p.add_argument('name')
p = sub.add_parser('full-table', help='Inject full IPv4 routing table (background)')
p.add_argument('--count', type=int, default=900000, metavar='N',
help='Number of prefixes to inject (default: 900000)')
p.add_argument('--batch-size', type=int, default=1000, metavar='N',
help='Progress update interval (default: 1000)')
p.add_argument('--follow', '-f', action='store_true',
help='Follow progress until complete')
sub.add_parser('full-table-status', help='Show full-table injection progress')
sub.add_parser('full-table-stop', help='Stop full-table injection')
p = sub.add_parser('churn', help='Cycle announce/withdraw to populate ip_rib_log')
p.add_argument('--count', type=int, default=0, metavar='N',
help='Number of cycles (0 = infinite)')
@ -255,6 +364,9 @@ def main():
'withdraw-all': cmd_withdraw_all,
'scenario': cmd_scenario,
'withdraw-scenario': cmd_withdraw_scenario,
'full-table': cmd_full_table,
'full-table-status': cmd_full_table_status,
'full-table-stop': cmd_full_table_stop,
'churn': cmd_churn,
}

View File

@ -441,6 +441,100 @@ _PATH_DIVERSITY_ROUTES = [
# 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)',