AnonTestLab.
Research tool for anonymous communication networks

Anonymous Protocol
Emulator.

A real, circuit-based mix network for testing anonymous communication designs against 432 configurations of crypto, routing, traffic, and adversary models, genuine OS processes and real sockets standing in for the real network, not a discrete-event approximation of one.

432
testable configurations
6×3
AEAD ciphers × ECDHE curves
4
pluggable adversaries
254
addressable relay processes
Running tor-like... 10 relays ready session 1/10 complete (37/37 real delivered, build 34ms) session 2/10 complete (37/37 real delivered, build 29ms) ... Results for tor-like ──────────────────────────────────────── real_packets_sent 375 delivery_rate 1.0000 avg_latency_s 0.0006 <- real measured latency circuit_build_delay_s 0.0315 <- real measured handshake correlation_success_rate 1.0000 auc 1.0000 precision 1.0000 recall 1.0000
01 · why this exists

A gap between two things that already exist

Most anonymity-network research runs on a discrete-event simulator: fast, but every latency number and every crypto cost is a distribution someone had to assume first. The alternative is running the real Tor codebase, where every cost is real, but instrumenting one adversary experiment means patching production software built for a different job. AnonTestLab sits between them: real relay processes, real sockets, a real ECDHE handshake and AEAD encryption per hop, built from the ground up as a research harness where any single dial, cipher, curve, routing strategy, traffic shape, WAN condition, adversary, is one YAML field, and a full run takes seconds.

It's a low-latency, circuit-based mix network: Tor's telescoping onion-routing design, where each relay (a mix node) strips exactly one encryption layer and forwards, rather than the high-latency batch-and-shuffle mixnets built for anonymous email. That's the specific design point every measurement on this page is testing.

Discrete-event simulator

Fast, but assumed

Latency, handshake cost, and crypto overhead are numbers someone chose. Change the assumption, get a different answer, not a different measurement.

Running real Tor

Real, but heavyweight

Every cost is genuine, but a new adversary or a swapped cipher means patching production software that was never built to be an experiment harness.

AnonTestLab

Real, and a research harness

Real relays, real sockets, real crypto cost, and every axis worth studying is a config field, not a fork.

02 · how a session actually runs

Five real stages, no shortcuts

CLIENT n2 n5 n8 SERVER
active circuit (this session's 3 hops) other addressable relays, unused this session rings = onion layers, one sheds per hop
1

YAML becomes an ExperimentConfig

Every dimension (topology, routing, traffic shape, crypto, WAN conditions, adversary) is one validated, reproducible config object, seeded once.

2

spawn_relays() forks N real subprocesses

Each relay is its own OS process bound to its own loopback address, 127.0.0.1 through 127.0.0.254, not a coroutine pretending to be a host.

3

build_circuit() telescopes hop by hop

A real ephemeral ECDHE handshake and HKDF key derivation per hop, hop-local circuit IDs, independent forward and backward keys for the return path.

4

Real onion-wrapped cells over real sockets

Optionally padded to a fixed cell_size, optionally fragmented across cells, optionally slowed by per-node or per-edge WAN conditions.

5

MetricsCollector meets a pluggable Adversary

Measured latency, delivery, and bandwidth feed one or more adversary models; results land in results/<name>/ with a baseline diff if one's set.

03 · what this adds to the domain

Seven things you can't test on a discrete-event simulator

RUNTIME · anontestlab.emulator

Genuine emulation, not a timing model

Real subprocesses, real sockets, real crypto cost. Latency and delivery are measured outcomes, never sampled from a distribution someone had to assume first.

CRYPTO · crypto_layer.py

Bidirectional onion encryption

The return path is re-encrypted per hop with its own key, independent from the forward key but derived from the same handshake via HKDF, matching Tor's own directional key separation.

ADVERSARY · hop_depth.py

A leak, measured instead of assumed

Fixed-size cells still shrink by a fixed amount per hop position. The hop_depth adversary is a structural attacker that quantifies exactly how recoverable that position is, turning a disclosed simplification into a tested number.

CRYPTO · anontestlab.crypto

Crypto-agility as a first-class axis

Six AEAD ciphers across three ECDHE curves, selectable per experiment, for studies that actually compare algorithm choice rather than assuming one.

NETWORK · relay_process.py

WAN realism at two granularities

Per-node heterogeneous conditions layer with directional per-edge conditions, so a relay's link to one peer can differ from its link to another, not just from other relays.

ADVERSARY · anontestlab.adversary

A composable observer pipeline

Observation, Feature, and Decision are separately swappable stages, so a new correlation statistic or classifier drops in without touching how traffic is observed.

METHOD · README.md

Simplifications are disclosed, not discovered

Every scope trim is stated in the open and, where practical, given its own regression test, so "known limitation" means audited, not unexamined.

04 · configuration surface

Every axis is one YAML field away

6 AEAD ciphers × 3 ECDHE curves × 2 routing strategies × 3 traffic distributions × 4 adversary models: 432 combinations, each one a real config field, not a code change.

Traffic & topology

traffic.*

Traffic distribution

real_rate and cover_rate, each drawn from poisson (default), constant, or pareto for bursty, heavy-tailed arrivals. Real traffic is measured; cover traffic is decoy volume.

cover_behaviour.*

Cover traffic dropping

drop_probability lets an intermediate mix node silently discard a fraction of cover cells, modeling a relay that doesn't forward every decoy. Real cells are never dropped this way.

routing.*

Routing & path splitting

routing_strategy: random or bandwidth_weighted. path_length sets hops per path; num_paths and split_strategy (round_robin or random) spread one session across more than one path.

traffic_shaping.*

Cell shaping

cell_size pads every cell to one fixed wire size; mode is variable or fixed_rate, a scheduled send cadence. Oversized payloads fragment across multiple cells automatically.

link_conditions.*

WAN realism

latency_ms, jitter_ms, loss_probability, bandwidth_kbps, plus heterogeneous (per-node variation) and per_edge (per relay-to-peer variation, directional).

Crypto & adversaries

crypto.algorithm
CipherNote
noneplaintext passthrough, isolates transport cost
aes128gcm128-bit key
aes256gcm256-bit key, default
aes256gcmsivnonce-misuse resistant
aes256ocb3faster construction
chacha20poly1305stream-cipher based
crypto.keyexchange
CurveNote
x25519default, Curve25519
x448RFC 7748, larger keys
p256NIST secp256r1
adversary.types
AdversaryModelKey metric
global_observerObservation → Feature → Decision, AS-level partial visibilityauc, tpr_at_fpr
path_compromiseindependent-compromise Monte Carlo, no packets movefull_compromise_rate
watermarkactive delay-pattern injection at hop 1watermark_detection_rate
hop_depthstructural, quantifies the fixed-size-cell leakhop_position_accuracy
05 · methodology

Disclosed simplifications, not claims

A research harness earns trust by stating its scope trims in the open. These are the ones that still stand, each one deliberate:

Known limitations, v0.3

  • Fixed-size cells still leak hop position. A global observer watching multiple hops of one circuit could infer depth from the size sequence, which is exactly what hop_depth measures.
  • Per-edge link conditions are directional-only. Only the connection-initiating side scales by the edge factor; the receiving side's own upstream sends still use its plain per-node value.
  • No relay identity or directory system. Keys are ephemeral only: no TOFU question to answer, but also no persistent relay reputation.
  • bandwidth_weighted routing skips guard/exit-flag constraints. It weights by node capacity alone, deliberately not replicating Tor's position rules.
  • Timing varies run to run. The experiment design is reproducible from its seed; real measured latency isn't, because it's real.