When you use Tor Browser or connect to the Tor network, your traffic travels through a carefully constructed pathway called a circuit. Understanding how these circuits work helps developers build privacy-focused applications and empowers power users to make informed security decisions. This guide visualizes the Tor circuit architecture and explains each component in technical detail.

The Three-Hop Tor Circuit

Every Tor connection passes through exactly three relays: the entry guard (or guard node), the middle node, and the exit node. This three-hop design provides a balance between anonymity and performance.

┌─────────────┐      ┌─────────────┐      ┌─────────────┐      ┌─────────────────┐
│   Client    │ ───► │   Guard     │ ───► │   Middle    │ ───► │     Exit        │
│  (Your IP)  │      │   Node      │      │   Node      │      │     Node        │
└─────────────┘      └─────────────┘      └─────────────┘      └─────────────────┘
       │                   │                   │                   │
       │ ◄─ Encrypted ─►   │ ◄─ Encrypted ─►   │ ◄─ Encrypted ─►   │
       │                   │                   │                   │
       │            Knows only             Knows only            Knows origin
       │            client IP               guard IP             (via TLS metadata)

Why Three Relays?

Using three hops rather than more provides several advantages:

Circuit Construction Process

The Tor protocol uses a sophisticated key exchange mechanism to build circuits. Here’s how it works:

1. Establishing the Circuit

When you connect to Tor, your client performs a Diffie-Hellman key exchange with each relay in sequence:

# Simplified circuit extension (pseudo-code)
def extend_circuit(circuit, relay_info):
    # Create handshake with new relay
    public_key = generate_keypair()
    
    # Diffie-Hellman with relay
    shared_secret = diffie_hellman(
        public_key,
        relay_info.identity_key
    )
    
    # Derive session keys
    keys = derive_keys(shared_secret)
    
    # Add hop to circuit
    circuit.add_hop(relay_info, keys)
    
    return circuit

2. Onion Encryption Layers

Each relay only decrypts one layer of encryption, revealing only where to forward the traffic next. This creates the “onion” effect:

┌─────────────────────────────────────────────────────────────┐
│  Layer 3 (Exit Node) ◄── Decrypts this layer               │
│  ─────────────────────────────────────────────────────────  │
│  Layer 2 (Middle Node) ◄── Decrypts this layer             │
│  ─────────────────────────────────────────────────────────  │
│  Layer 1 (Guard Node) ◄── Decrypts this layer              │
│  ─────────────────────────────────────────────────────────  │
│  Layer 0 (Your Client) ◄── Original encrypted payload     │
└─────────────────────────────────────────────────────────────┘

3. Cell Structure

Tor uses fixed-size cells (512 bytes) for all communications. Each cell contains:

// Tor cell structure (simplified)
struct tor_cell {
    uint16_t circuit_id;    // Circuit identifier
    uint8_t  command;       // RELAY_BEGIN, RELAY_DATA, etc.
    uint8_t  payload[509];  // Encrypted payload
};

Visualizing Circuit Paths

You can visualize your current Tor circuit using the Tor Browser or by querying the Tor control port.

Using Tor Browser

  1. Click the shield icon in the Tor Browser toolbar
  2. View the “Circuit” section to see your current path
  3. Each relay shows its nickname, IP (partially obscured), and country

Programmatic Access

Connect to the Tor control port to programmatically inspect circuits:

# Enable control port in torrc
# ControlPort 9051
# CookieAuthentication 1

# Connect via socat or netcat
echo -e "AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT" | nc localhost 9051
# Using Stem library (Python)
from stem import Controller

with Controller.from_port(port=9051) as controller:
    controller.authenticate()
    
    # Get all circuits
    for circuit in controller.get_circuits():
        print(f"Circuit {circuit.id}:")
        for i, hop in enumerate(circuit.path):
            print(f"  Hop {i+1}: {hop[0]} ({hop[1]})")

This outputs something like:

Circuit 12:
  Hop 1: Unnamed (86.123.45.67) [Germany]
  Hop 2: DCHubRelays03 (193.56.78.90) [Netherlands]  
  Hop 3: tor4you (45.67.89.123) [France]

Circuit Lifetime and Renewal

Tor circuits don’t last forever. The default circuit lifetime is 10 minutes, after which Tor builds a new circuit. This limits the amount of traffic correlation an attacker can perform.

Configuration Options

You can adjust circuit behavior in your torrc:

# Build new circuit every N seconds (default: 600)
MaxCircuitDirtime 600

# Number of concurrent circuits (default: 1)
MaxClientCircuitsPending 1

# Use only entry guards that have been stable for N days
NumEntryGuards 3

Understanding Guard Nodes

Guard nodes (also called entry guards) are a critical security feature. Instead of choosing random entry points each time, Tor clients select a small set of stable relays as permanent guards.

This prevents certain attacks:

Guard Selection Algorithm

# Simplified guard selection logic
def select_guards(relays, config):
    # Filter stable, fast relays
    candidates = [r for r in relays 
                  if r.is_stable and r.is_fast 
                  and r.bandwidth > config.min_bandwidth]
    
    # Select weighted by bandwidth
    guards = weighted_selection(candidates, config.num_guards)
    
    return guards

Exit Node Considerations

The exit node is where Tor traffic meets the regular internet. This position carries unique risks:

Common Exit Node Ports

Most Tor relays allow these common ports:

# Typical exit policy (most permissive)
accept *:80, *:443    # HTTP, HTTPS
accept *:22           # SSH
accept *:853          # DNS over TLS
reject *:*            # Reject everything else

Building Applications with Tor

For developers integrating Tor, the Stem library provides excellent Python bindings:

from stem import Signal
from stem.control import Controller

# Start a new Tor circuit for each request
def tor_request(url, session):
    with Controller.from_port(port=9051) as controller:
        controller.authenticate()
        
        # Signal Tor to create new identity
        controller.signal(Signal.NEWNYM)
        
        # Configure session to use Tor
        session.proxies = {
            'http': 'socks5h://127.0.0.1:9050',
            'https': 'socks5h://127.0.0.1:9050'
        }
        
        return session.get(url)

Security Considerations

Understanding Tor circuit limitations helps you use it effectively:

Conclusion

Tor circuits provide robust anonymity through layered encryption and distributed relay architecture. By understanding how guard nodes, middle nodes, and exit nodes work together, you can make better privacy decisions and build more secure applications. Remember that Tor is one tool in a broader privacy toolkit—combine it with good operational security practices for optimal protection.


Built by theluckystrike — More at zovo.one