Prakhar Pandey

backend & protocol engineer


← writes

Onion services and the rendezvous protocol

June 1, 2026

Series:

  1. How Tor actually routes your traffic
  2. Onion services and the rendezvous protocol (this post)
  3. Building hop6

Part 1 covered reaching the ordinary internet through an exit relay. Onion services are different: there is no exit, the destination is itself inside Tor, and both sides stay hidden. This is the machinery hop6 runs on, so it is worth understanding properly.

The address is the key

A v3 onion address looks like 56 base32 characters followed by .onion. It is not a name that points at a key through some registry. It is the key:

  .onion address = base32( ed25519_public_key || checksum || version )
                          \__________________ 56 chars __________________/

This single fact removes an entire layer of trust that the normal web needs. On the clearnet, example.com is just a label; a certificate authority has to vouch that the server you reached really owns it. With onion services the address is derived from the public key, so when you connect to a .onion, Tor can verify the service holds the matching private key with no CA, no DNS, and nothing to spoof. The address authenticates itself. (In hop6 this is the whole identity model: a peer is its key.)

The problem rendezvous solves

The service wants to be reachable without revealing its IP. The client wants to reach it without revealing its IP. Neither can just connect to the other, because connecting means showing up at an address. The solution is to never have them connect directly at all. Instead they each build their own Tor circuit to a neutral meeting relay and get spliced together there.

It takes three supporting roles to set that up: a place to publish how to start (the directory), a few relays that take introductions (introduction points), and the meeting relay itself (the rendezvous point).

Step 1: the service publishes a descriptor

The service chooses a handful of relays to act as introduction points, builds a Tor circuit to each, and then publishes a signed descriptor that lists those introduction points. The descriptor is stored on a subset of relays called the HSDirs (hidden service directories), which form a hash ring.

In v3 this publication is privacy-preserving. The descriptor is encrypted, and the location on the ring is derived from a blinded version of the service key that rotates daily. The practical consequence:

  someone who already knows the .onion   →  can find and decrypt the descriptor
  an HSDir just holding the descriptor    →  cannot learn the .onion or its contents

So you cannot enumerate onion services by scraping HSDirs, which was a real weakness in the older v2 design.

Step 2: client finds the service and picks a rendezvous point

The client knows the .onion, so it can compute the right HSDir, fetch the descriptor, and decrypt it. Now it knows the service’s introduction points.

The client then picks any relay to be the rendezvous point (RP), builds a circuit to it, and gives it a one-time secret, the rendezvous cookie. The RP’s only job is to wait for someone who presents the matching cookie and then to splice the two circuits.

Step 3: introduction and rendezvous

The client sends an INTRODUCE1 message through an introduction point to the service. This message contains the chosen RP, the cookie, and the client’s half of a fresh handshake, all encrypted to the service’s key so the introduction point learns nothing useful.

The service then builds its own circuit to the rendezvous point, presents the cookie, and completes the handshake. The RP now relays cells between the two circuits.

                      ┌───────────────┐
        intro circuit │ introduction  │   "meet me at RP, cookie=...,
       ┌─────────────▶│    point      │────────────┐ here's my handshake half"
       │              └───────────────┘            ▼
   ┌────────┐                                  ┌─────────┐
   │ client │                                  │ service │
   └────────┘                                  └─────────┘
       │  3-hop circuit          3-hop circuit  │
       └──────────────┐        ┌────────────────┘
                      ▼        ▼
                  ┌──────────────────┐
                  │ rendezvous point │   matches the cookie,
                  │       (RP)       │   splices the two circuits
                  └──────────────────┘
        end-to-end path is ~6 hops, client ⇄ RP ⇄ service

After this, the introduction point is out of the picture; all traffic flows over the joined circuit through the RP.

Who learns what

This is the part worth dwelling on, because it is where the security claims actually live:

  party                 learns client IP   learns service IP   learns content
  ───────────────────   ────────────────   ─────────────────   ──────────────
  introduction point    no                 no                  no
  rendezvous point      no                 no                  no (relays cells)
  the client            (its own)          no                  yes
  the service           no                 (its own)           yes
  HSDir                 no                 no                  no

The rendezvous point is the relay closest to “the middle,” and even it learns neither identity, nor the content, nor even that this is a hidden-service rendezvous as opposed to an ordinary circuit. Each side built its own three-hop path to the RP, so each side’s location is protected by the same onion routing from part 1.

Why it is secure, concretely

How this maps onto hop6

hop6 leans on every one of these properties:

What hop6 does have to do is drive Tor to publish that service, dial peers through Tor, and run a small protocol on top of the resulting byte stream. That is part 3: building hop6.