Syllabus Lesson 140 of 156 · Anonymity networks and traffic analysis
Anonymity networks and traffic analysis

Onion routing

Why it matters When you use Tor, your messages pass through three strangers' computers. Knowing exactly what each of those computers can see tells you what Tor protects, and what it leaves exposed. In this lab you build the layers yourself, so the idea stops being magic.

Picture a letter inside three envelopes, one inside another. You hand the bundle to a first courier. They open the outer envelope, find only the name of a second courier, and pass the rest on. The second courier opens the next envelope and finds the name of a third. Only the third courier opens the last envelope and sees where the letter is really going.

No single courier knows both who sent the letter and where it ends up. That is the whole trick, and it has a name.

Onion routing: sending a message wrapped in several layers of encryption through a chain of computers, where each one removes exactly one layer. Like peeling an onion, hence the name.

Relay: one computer in an onion-routing network that passes traffic on. Tor's relays are run by volunteers around the world.

Circuit: the chain of relays your traffic takes, chosen by your own device. In Tor a circuit normally has three relays: a guard, a middle and an exit.

Who knows what

RelayKnows who you are (your IP address)?Knows where you are going?
Guard (first)YesNo, only the middle relay
MiddleNo, only the guardNo, only the exit
Exit (last)No, only the middle relayYes, and it can read the traffic unless the site uses HTTPS

Your device does all the wrapping before anything leaves. It shares a separate secret key with each relay (in real Tor these keys come from a key exchange with each relay in turn). Because the guard must peel first, its layer has to be the outermost one, so you lock the layers inside out: the exit's layer first, then the middle's, then the guard's.

The practice world's relays

The module watched.leg7b_onion gives you twelve fictional relays named after trees. Each has a name, a key (a whole number) and flags. A relay with is_guard set is trusted to be a first hop; one with is_exit set agrees to connect out to websites. Some relays have both flags, some neither.

import random
from watched.leg7b_onion import relays, lock, unlock

rs = relays(seed=1)
print([r.name for r in rs if r.is_exit])   # the relays allowed to be exits

secret = lock(42, "hello")      # one layer, locked with key 42
print(secret)                   # unreadable hex text
print(unlock(42, secret))       # hello

rng = random.Random(7)          # a seeded random generator
print(rng.choice(["a", "b", "c"]))   # picks one item at random, the same one every run

lock is a teaching toy that is not secure. It only exists so you can watch layers being added and removed. Real Tor uses vetted ciphers.

Each layer's text has the form next hop|the rest: the name of the next relay, a bar character, then the still-locked inner layers. The exit's layer holds the destination host instead of a relay name, and then the message itself. The helper relay_views(onion, circuit, your_ip) sends your onion through the circuit and reports, for each relay, where the traffic came_from, where it was passed_to, and what it could read.

A common misconception: "Tor encrypts my traffic all the way to the website." It does not. The last layer comes off at the exit, so the exit sees whatever you send unless the site itself uses HTTPS. Tor hides who is talking; HTTPS hides what is said. You want both.

In other languages

The wrapping loop looks the same in any language: walk the circuit backwards, adding one layer each time.

// JavaScript
function wrap(message, destination, circuit, lock) {
  const hops = circuit.slice(1).map(r => r.name).concat([destination]);
  let data = message;
  for (let i = circuit.length - 1; i >= 0; i--) {
    data = lock(circuit[i].key, hops[i] + "|" + data);
  }
  return data;
}
// Go
func wrap(message, destination string, circuit []Relay) string {
    hops := []string{}
    for _, r := range circuit[1:] { hops = append(hops, r.Name) }
    hops = append(hops, destination)
    data := message
    for i := len(circuit) - 1; i >= 0; i-- {
        data = lock(circuit[i].Key, hops[i]+"|"+data)
    }
    return data
}
// Rust
fn wrap(message: &str, destination: &str, circuit: &[Relay]) -> String {
    let mut hops: Vec<String> = circuit[1..].iter().map(|r| r.name.clone()).collect();
    hops.push(destination.to_string());
    let mut data = message.to_string();
    for i in (0..circuit.len()).rev() {
        data = lock(circuit[i].key, &format!("{}|{}", hops[i], data));
    }
    data
}

Recap

  • Onion routing wraps a message in one layer per relay; each relay peels exactly one.
  • The guard knows you but not your destination; the exit knows the destination but not you; the middle knows neither.
  • Lock layers inside out: the exit's first, the guard's last.
  • Tor hides who is talking. Only HTTPS hides what is said from the exit.
Your turn

Use the fictional relays from watched.leg7b_onion. Write two functions:

  1. build_circuit(relays, rng): return a list [guard, middle, exit]. Pick the guard with rng.choice from the relays whose is_guard is true; the exit from the relays whose is_exit is true and that are not the guard; the middle from the relays that are neither. Some relays carry both flags, so check that all three are different.
  2. wrap(message, destination, circuit): lock one layer per relay. The guard's layer text is middle name|..., the middle's is exit name|..., and the exit's is destination|message. Lock the exit's layer first and the guard's last, using lock(relay.key, text), and return the outermost locked text.
The last lines send your onion and print what each relay saw. Tests use new relays, seeds and messages.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output