NetLens is a data-driven analysis framework developed to support the empirical investigation of Resource Public Key Infrastructure (RPKI) adoption and its impact on routing security within the Swiss Internet ecosystem. The system is designed to systematically collect, aggregate, and analyze publicly available routing and RPKI data in order to assess the extent of Route Origin Authorization (ROA) deployment and its practical effectiveness in mitigating routing security threats.
The framework integrates multiple data sources, including BGP routing information from route collectors, RPKI validation data derived from local validator instances, and auxiliary metadata from publicly available registries. Based on these inputs, NetLens constructs a unified data model that links Autonomous Systems (AS), announced IP prefixes, and their corresponding RPKI validation states.
A key feature of NetLens is its ability to correlate routing data with organizational and sectoral information, enabling differentiated analysis across industry sectors and critical infrastructures. This allows for the identification of structural patterns in RPKI adoption and highlights disparities between different types of network operators.
Through the aggregation and analysis of these datasets, NetLens supports the quantitative evaluation of RPKI coverage as well as the qualitative assessment of its effectiveness in real-world routing scenarios. In particular, the framework enables the examination of whether invalid routing announcements are propagated within the observed network and to what extent RPKI contributes to reducing the exposure to origin hijacks.
By providing a reproducible and extensible data processing pipeline, NetLens forms the technical foundation for the empirical analysis conducted in this thesis and contributes to a better understanding of routing security practices in Switzerland.
RIPEstat
- AS
- Prefix
- Relationship
- Ownership
Routinator
- RPKI validation results
BGPKIT
- BGP routing information from route collectors
MANRS
- Organization is MANRS participant
RoVista
- AS Rank
- ROV Ratio
- Cone Size
Cloudflare
- Prefix Visibility
- Origin Hijacks
- Route Leaks
Build
docker build -t alexblaeuer/netlens:latest .Publish
docker push alexblaeuer/netlens:latest.env
# Django settings for netlens project.
DEBUG=False
SECRET_KEY=your_secret_key_here
ALLOWED_HOSTS="*"
TIME_ZONE=Europe/Zurich
# Database configuration
DB_NAME=netlens
DB_USER=netlens
DB_PASSWORD=your_db_password_here
DB_HOST=postgres
DB_PORT=5432
# NetLens
NETLENS_VERSION=latest
# Routinator RPKI validator configuration
ROUTINATOR_URL=http://localhost:8323
ROUTINATOR_API_KEY=your_api_key_here
# RoVista configuration
ROVISTA_USER=your_username_here
ROVISTA_PASSWORD=your_password_here
# MANRS configuration
MANRS_API_KEY=your_api_key_here
# Cloudflare configuration
CLOUDFLARE_API_KEY=your_api_key_here
# Gunicorn configuration
GUNICORN_WORKERS=4
docker-compose.yaml
---
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "${DB_PORT}:5432"
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
netlens:
image: alexblaeuer/netlens:${NETLENS_VERSION}
restart: unless-stopped
ports:
- "8000:8000"
env_file:
- ./.env
volumes:
- ./data/export:/app/export
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
Fetch data
docker compose exec netlens ./manage.py fetch_data
docker compose exec netlens ./manage.py fetch_data --force
Fetch related prefixes
docker compose exec netlens ./manage.py fetch_related_prefixes
docker compose exec netlens ./manage.py fetch_related_prefixes --force
Load data
docker compose exec netlens ./manage.py load_data
Fetch organization information
docker compose exec netlens ./manage.py fetch_orgs
docker compose exec netlens ./manage.py fetch_orgs --force
Fetch unannounced data
docker compose exec netlens ./manage.py fetch_unannounced
docker compose exec netlens ./manage.py fetch_unannounced --model asn
docker compose exec netlens ./manage.py fetch_unannounced --model prefix
Clean data
# Delete data
docker compose exec netlens ./manage.py clean_data
docker compose exec netlens ./manage.py clean_data --dry-run
docker compose exec netlens ./manage.py clean_data --target foreign-prefixes
docker compose exec netlens ./manage.py clean_data --target unannounced-prefixes
docker compose exec netlens ./manage.py clean_data --target prefixes-without-asn
docker compose exec netlens ./manage.py clean_data --target unannounced-asn
docker compose exec netlens ./manage.py clean_data --target asn-without-prefixes
docker compose exec netlens ./manage.py clean_data --target empty-organizations
# Show data
docker compose exec netlens ./manage.py clean_data --target contains-unannounced
docker compose exec netlens ./manage.py clean_data --target prefixes-without-organization
docker compose exec netlens ./manage.py clean_data --target prefixes-without-organization --json
Map Organizations
docker compose exec netlens ./manage.py map_orgs --model type
docker compose exec netlens ./manage.py map_orgs --model prefix
docker compose exec netlens ./manage.py map_orgs --model prefix --dry-run
Validate Prefix RPKI
docker compose exec netlens ./manage.py validate_prefix_rpki
docker compose exec netlens ./manage.py validate_prefix_rpki --dry-run
Extend Data
docker compose exec netlens ./manage.py extend_data --source rovista
docker compose exec netlens ./manage.py extend_data --source manrs
docker compose exec netlens ./manage.py extend_data --source cloudflare
Collect BGP Events
docker run --rm -d \
--network "netlens_default" \
--env-file "./.env" \
--entrypoint "" \
alexblaeuer/netlens:0.8.0 \
./manage.py collect_bgp_events \
--from-time "2026-05-01T00:00:00Z" \
--until-time "2026-05-01T13:59:59Z"
docker run --rm -d \
--network "netlens_default" \
--env-file "./.env" \
--entrypoint "" \
alexblaeuer/netlens:0.8.0 \
./manage.py collect_bgp_events \
--from-time "2026-05-01T00:00:00Z" \
--until-time "2026-05-01T13:59:59Z" \
--fill-gaps 15
collect_bgp_events.sh
#!/bin/bash
set -e
# Load .env file
source "/opt/netlens/.env"
# Use provided args or default to the past hour (UTC)
FROM_TIME="${1:-$(date -u -d "1 hour ago" '+%Y-%m-%dT%H:00:00Z')}"
UNTIL_TIME="${2:-$(date -u -d "1 hour ago" '+%Y-%m-%dT%H:59:59Z')}"
echo "Collecting BGP Events"
echo "From: $FROM_TIME"
echo "Until: $UNTIL_TIME"
docker run --rm \
--network "netlens_default" \
--env-file "/opt/netlens/.env" \
--entrypoint "" \
alexblaeuer/netlens:${NETLENS_VERSION} \
./manage.py collect_bgp_events \
--from-time "$FROM_TIME" \
--until-time "$UNTIL_TIME"crontab
5 * * * * /opt/netlens/collect_bgp_events.sh
Collect Cloudflare Incidents
Hijacks:
docker run --rm -d \
--network "netlens_default" \
--env-file "./.env" \
--entrypoint "" \
alexblaeuer/netlens:0.8.0 \
./manage.py fetch_bgp_incidents \
--type hijacks \
--from-time "2026-05-01T00:00:00Z" \
--until-time "2026-05-10T23:59:59Z"
# --asn 211452,51252
Leaks:
docker run --rm -d \
--network "netlens_default" \
--env-file "./.env" \
--entrypoint "" \
alexblaeuer/netlens:0.8.0 \
./manage.py fetch_bgp_incidents \
--type leaks \
--from-time "2026-05-01T00:00:00Z" \
--until-time "2026-05-10T23:59:59Z"
# --asn 211452,51252
BGP Analysis
docker compose exec netlens ./manage.py bgp_analysis --from-time "2026-05-01T00:00:00Z" --until-time "2026-05-10T23:59:59Z"
Backup
docker compose exec postgres bash
pg_dump -U netlens netlens > /var/lib/postgresql/data/netlens_0_5_1.sql
Restore
docker compose exec postgres bash
dropdb -U netlens netlens
createdb -U netlens netlens
psql -U netlens -d netlens < /var/lib/postgresql/data/netlens_0_5_1.sql