Onion services and the rendezvous protocol
June 1, 2026
Series:
- How Tor actually routes your traffic
- Onion services and the rendezvous protocol (this post)
- 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
- Self-authenticating endpoints. The address is the public key, so the client cryptographically verifies it reached the right service. There is no CA to compromise and no spoofable name.
- Mutual location hiding. Both client and service reach a neutral relay over their own circuits. No party in the path sees both ends.
- Forward secrecy. The client-service handshake (ntor) uses fresh ephemeral keys, so recorded traffic stays unreadable even if a long-term key leaks later.
- No open ports, no public IP. The service never accepts an inbound connection at its own address; it only ever makes outbound circuits to intro points and the RP. That also means it works behind NAT with nothing to forward.
- Harder discovery (v3). Blinded keys and encrypted descriptors stop HSDirs from enumerating or reading services.
How this maps onto hop6
hop6 leans on every one of these properties:
- Each instance generates an ed25519 key and publishes a v3 onion service. That
.onionis its identity; sharing it is sharing a public key, so there is nothing to register and nobody to trust. - Two peers reach each other purely as onion services, so neither needs a public IP, a port forward, or a server in the middle. NAT traversal comes for free.
- The connection is mutually authenticated and location-hidden by Tor itself, so hop6 does not have to implement any of its own transport crypto.
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.