Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TorLab – Local Tor Testing & Traffic Correlation Lab

TorLab is a containerized Tor testing environment built with:

  • Docker
  • Containerlab
  • Alpine Linux
  • Single unified Tor image (role-based via ROLE env)

It is designed for:

  • Tor architecture testing
  • Directory Authority experiments
  • Exit relay behavior analysis
  • Traffic correlation research (client ↔ server observation)

Architecture Overview

                br_core (10.10.20.0/24)
 ┌─────────────────────────────────────────────┐
 │                                             │
 │  Clients   Relays   Exit Nodes   DA        │
 │                                             │
 └─────────────────────────────────────────────┘
                         │
                         │ (exit second NIC)
                         ▼
                br_servers (10.10.30.0/24)
                     │
                  Servers

Design Principles

  • Single Docker image: localhost/torlab
  • Role-based configuration (ROLE=client|relay|exit|da|server)
  • DA identity persisted via volume
  • Relay and exit node identity persisted via volumes to avoid fingerprint mismatch on redeploy
  • Tor configs rendered dynamically from templates
  • Use container hostname as tor Nickname for easy log tracking
  • No routing complexity
  • No BGP
  • No ISP simulation
  • Clean dual-vantage traffic correlation

TOR lab roles

ROLE Description Lab node hostname
da Directory Authority clab-torlab-da*
relay Middle relay clab-torlab-r*
exit Exit relay (dual-homed) clab-torlab-x*
client Tor client clab-torlab-c*
server HTTP test server clab-torlab-s*

Build Image

From repository root:

sudo docker build -t localhost/torlab ./images/tor

Prepare Persistent Data Directories

All nodes that act as Tor relays or authorities persist their /var/lib/tor via bind mounts. This ensures Tor identity keys survive redeploys and clients do not encounter fingerprint mismatches.

Create all required directories in one step:

for n in da1 r1 r2 r3 r4 r5 x1 x2 x3; do
    sudo mkdir -p containerlab/tor-data/$n
    sudo chown -R 100:100 containerlab/tor-data/$n
    sudo chmod -R u+rwX,g+rwX containerlab/tor-data/$n
done

Generate Directory Authority Keys (One-Time Step)

Run ephemeral key generator container:

sudo docker run --rm \
  --user root \
  --entrypoint /srv/generate-da-keys.sh \
  -v "$(realpath containerlab/tor-data/da1)":/var/lib/tor:Z \
  localhost/torlab \
  da1 10.10.20.40 9001 9030

This creates:

tor-data/da1/
 ├── keys/
 │    ├── authority_identity_key
 │    ├── authority_signing_key
 │    └── authority_certificate
 ├── fingerprint
 └── fingerprints.txt

Extract fingerprints:

sudo cat containerlab/tor-data/da1/fingerprints.txt

Copy:

  • DA_FP — RSA relay fingerprint
  • DA_V3IDENT — V3 authority identity

into containerlab/torlab.clab.yml.

Note: DA keys are generated once. Relay keys (r1-r5, x1-x3) are generated automatically on first start and persisted in their respective tor-data/ directories. Do not delete these directories between deploys or clients will fail with Unexpected identity in router certificate errors.


Create OS bridges

sudo ip link add br_core type bridge
sudo ip link set br_core up

sudo ip link add br_servers type bridge
sudo ip link set br_servers up

ip -br link show | grep -E 'br_core|br_servers'

Add lab bridges to firewalld (also contrainer lab public ports)

sudo firewall-cmd --zone=trusted --add-interface=br_core
sudo firewall-cmd --zone=trusted --add-interface=br_servers

sudo firewall-cmd --zone=public --add-port  9501/tcp
sudo firewall-cmd --zone=public --add-port  9502/tcp
sudo firewall-cmd --zone=public --add-port  9503/tcp

Deploy Lab

sudo containerlab deploy -t containerlab/torlab.clab.yml

Check status:

sudo containerlab inspect -t containerlab/torlab.clab.yml

Browser Access via SOCKS5

The three client nodes export their SOCKS5 proxy port to the host machine. You can point a browser or tool directly to one of these ports to route traffic through the lab Tor network:

Client Host SOCKS5 Port
c1 9501
c2 9502
c3 9503

Configure your browser to use SOCKS5 proxy:

  • Host: containerlab host ip address
  • Port: 9501 (or 9502 / 9503)
  • Enable "Proxy DNS when using SOCKS5" if available

Example with curl (loclahost):

curl --socks5-hostname 127.0.0.1:9501 http://10.10.30.11:8081
curl --socks5-hostname 127.0.0.1:9502 http://10.10.30.12:8082
curl --socks5-hostname 127.0.0.1:9503 http://10.10.30.13:8083

Note: Traffic routed this way exits through one of the lab exit nodes (x1/x2/x3) and reaches the lab servers (s1/s2/s3). No traffic leaves the lab environment.


Exit Policy

Exit node traffic is controlled via the exit node torrc template (torrc-exit.tmpl). Only the following ports are permitted by default:

# Allow only those ports in lab
ExitPolicy accept *:8080
ExitPolicy accept *:8081
ExitPolicy accept *:8082
ExitPolicy accept *:8083
ExitPolicy accept *:80
ExitPolicy accept *:443
ExitPolicy reject *:*

To allow additional ports, edit images/tor/torrc-exit.tmpl and rebuild the image:

sudo docker build -t localhost/torlab ./images/tor

Then redeploy the lab:

sudo containerlab destroy -t containerlab/torlab.clab.yml
sudo containerlab deploy -t containerlab/torlab.clab.yml

Validate Lab Deployment

Note: After deploy, wait 2-5 minutes for the DA to complete its first voting cycle and publish a consensus. Relays need additional time to register and appear in the consensus.


Check DA is running

Validate RSA fingerprint matches DA_FP in torlab.clab.yml:

sudo docker exec clab-torlab-da1 cat /var/lib/tor/fingerprint

Validate V3 identity matches DA_V3IDENT in torlab.clab.yml:

sudo docker exec clab-torlab-da1 grep "fingerprint" /var/lib/tor/keys/authority_certificate

Check DA logs for successful voting and consensus publishing:

sudo docker logs clab-torlab-da1 | grep -E "vote|consensus|Published" | tail -10

Expected output includes:

Time to vote.
Retrieved da1's vote from self.
Vote posted.
Published ns consensus
Published microdesc consensus

Check consensus is published

List all nodes in the current consensus:

sudo docker exec clab-torlab-da1 grep "^r " /var/lib/tor/cached-consensus

Expected: 9 nodes visible with their 10.10.20.x addresses (da1, r1-r5, x1-x3).

Check node flags — exit nodes must have the Exit flag:

sudo docker exec clab-torlab-da1 grep -A1 "^r " /var/lib/tor/cached-consensus

Expected for exit nodes: s Exit Fast Guard HSDir Running Stable V2Dir Valid


Check relay is registered in consensus

Verify relay torrc has correct Address:

sudo docker exec clab-torlab-r1 cat /tmp/torrc-relay.conf

Address must be 10.10.20.31.

Check relay published its descriptor:

sudo docker logs clab-torlab-r1 | grep -i "publishing\|reachable" | tail -5

Expected:

Self-testing indicates your ORPort 10.10.20.31:9001 is reachable from the outside. Excellent. Publishing server descriptor.

Check exit node

Verify exit torrc has correct Address, ExitRelay and ExitPolicy:

sudo docker exec clab-torlab-x1 cat /tmp/torrc-exit.conf

Check exit published its descriptor:

sudo docker logs clab-torlab-x1 | grep -i "publishing\|reachable" | tail -5

Check client bootstrap

sudo docker logs clab-torlab-c1 | grep -i "bootstrap" | tail -10

Expected last line:

Bootstrapped 100% (done): Done

Check client has full consensus with all nodes:

sudo docker exec clab-torlab-c1 grep "^r " /var/lib/tor/cached-microdesc-consensus

Inspect active circuits via control port

All nodes have ControlPort 9051 enabled with no authentication (CookieAuthentication 0).

Check active circuits on a client:

sudo docker exec clab-torlab-c1 sh -c 'echo -e "AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT" | nc 127.0.0.1 9051'

Trigger a request first to build a full circuit, then inspect:

sudo docker exec clab-torlab-c1 curl --socks5-hostname 127.0.0.1:9050 http://10.10.30.11:8081 &
sleep 10
sudo docker exec clab-torlab-c1 sh -c 'echo -e "AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT" | nc 127.0.0.1 9051'

Expected output shows a BUILT circuit with 3 hops (guard → middle → exit):

250-circuit-status=5 BUILT $FINGERPRINT1~node1,$FINGERPRINT2~node2,$FINGERPRINT3~node3 PURPOSE=GENERAL

Check streams (active connections) on a client:

sudo docker exec clab-torlab-c1 sh -c 'echo -e "AUTHENTICATE\r\nGETINFO stream-status\r\nQUIT" | nc 127.0.0.1 9051'

Check active circuits on a relay:

sudo docker exec clab-torlab-r1 sh -c 'echo -e "AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT" | nc 127.0.0.1 9051'

Trace a circuit path

After building a circuit, map the fingerprints to node IPs.

Get active circuits:

sudo docker exec clab-torlab-c1 sh -c 'echo -e "AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT" | nc 127.0.0.1 9051'

Each BUILT circuit shows 3 hops in order: guard → middle → exit:

BUILT $FINGERPRINT1~Unnamed,$FINGERPRINT2~Unnamed,$FINGERPRINT3~Unnamed PURPOSE=GENERAL

Map fingerprints to IPs using the consensus:

sudo docker exec clab-torlab-da1 grep "^r " /var/lib/tor/cached-consensus

The consensus format is:

r <nickname> <identity_b64> <digest_b64> <date> <IP> <ORPort> <DirPort>

The fingerprint in circuit-status is hex. To convert the base64 identity from consensus to hex for matching:

echo "<base64_identity>" | base64 -d | xxd -p | tr -d '\n' | tr '[:lower:]' '[:upper:]'

Example full trace:

# 1. Get circuit
sudo docker exec clab-torlab-c1 sh -c 'echo -e "AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT" | nc 127.0.0.1 9051' | grep BUILT | head -1

# 2. Get consensus node list
sudo docker exec clab-torlab-da1 grep "^r " /var/lib/tor/cached-consensus

# 3. Convert identity to hex for each hop and match to IP
echo "<base64_from_consensus>" | base64 -d | xxd -p | tr -d '\n' | tr '[:lower:]' '[:upper:]'

Note: Tor 0.4.9 uses Conflux — a multipath feature that builds multiple parallel circuits sharing the same CONFLUX_ID. This is normal and expected. Each group of circuits with the same CONFLUX_ID represents one logical connection split across multiple paths for performance.


End-to-end connectivity test

Verify exit node can reach server directly:

sudo docker exec clab-torlab-x1 curl http://10.10.30.11:8081

Test full Tor circuit from client to server:

sudo docker exec clab-torlab-c1 sh -c   'curl --socks5-hostname 127.0.0.1:9050 -H "X-Client-ID: ${HOSTNAME}" http://10.10.30.11:8081'
sudo docker exec clab-torlab-c1 sh -c   'curl --socks5-hostname 127.0.0.1:9050 -H "X-Client-ID: ${HOSTNAME}" http://10.10.30.12:8082'
sudo docker exec clab-torlab-c1 sh -c   'curl --socks5-hostname 127.0.0.1:9050 -H "X-Client-ID: ${HOSTNAME}" http://10.10.30.13:8083'

Expected: HTTP response OK from the test server.

Check server logs

docker exec clab-torlab-s1 cat /tmp/server.log

Quick status overview

sudo containerlab inspect -t containerlab/torlab.clab.yml

All nodes must show running state. da1 must not be in restarting.


Traffic Correlation Model

Observation points:

Client-side capture

sudo docker exec -it clab-torlab-c1 tcpdump -i eth1 -w client.pcap

Server-side capture

sudo docker exec -it clab-torlab-s1 tcpdump -i eth1 -w server.pcap

Goal:

Determine which client communicates with which server based on:

  • burst timing
  • packet direction
  • TLS record size
  • throughput patterns

Cleaning Up

Destroy lab:

sudo containerlab destroy -t containerlab/torlab.clab.yml --cleanup

Clean up Docker networks (this removes all currently unused networks):

sudo docker network prune

Remove DA keys (optional, destroys all node identities):

rm -rf containerlab/tor-data/

Remove only relay/exit keys (preserves DA keys):

for n in r1 r2 r3 r4 r5 x1 x2 x3; do
    rm -rf containerlab/tor-data/$n
done

Manual remove clab links:

for iface in $(ip link show | grep clab | awk -F': ' '{print $2}' | awk -F'@' '{print $1}'); do
    sudo ip link delete "$iface" 2>/dev/null || true
done

Notes

  • This environment is for testing only.
  • TestingTorNetwork 1 is enabled.
  • Not connected to the public Tor network.
  • No IPv6.
  • No NAT required.
  • Exit nodes allow HTTP (port 80), HTTPS (port 443), and lab HTTP (port 8080,8081,8082,8083).
  • All nodes expose ControlPort 9051 with no authentication for inspection.
  • Relay and exit node keys are persisted in containerlab/tor-data/ — do not delete between deploys unless you want to regenerate all identities.

License

Source code in this repository is licensed under the Apache License 2.0.

Documentation, diagrams, screenshots, examples and article text are licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0), unless otherwise stated.

Copyright (c) 2026 Nikolay Dachev.

About

Containerized Tor Laboratory for Educational Network Security Analysis

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages