Tor Deep Dive Part 3: Running a Tor Client on Debian 13
Part 1 covered what went wrong when hundreds of exit relays were turned against their users, and Part 2 explained the model. This part sets up a Tor client on a clean Debian 13 (trixie) virtual machine, so every later part has the same known starting point. Everything below was run on a fresh VM with 2 GB of RAM, and the output is real.
Which package to install
Debian ships its own tor package, and the Tor Project publishes one from its own apt repository. The Tor Project’s documentation says to use their repository, and warns against the copy in Ubuntu’s universe repository because it has not reliably been kept up to date. I followed their method.
On the day I tested, the two were the same upstream release:
| Source | Version |
|---|---|
| Debian trixie-security | 0.4.9.14-0+deb13u1 |
| Tor Project repository | 0.4.9.14-1~d13.trixie+1 |
So today the Debian package is a defensible choice too. The reason to prefer the Tor Project repository is update speed in the future, since a Tor release that fixes a network-level problem matters more than for most software.
Adding the Tor Project repository
First check your architecture. The repository only carries amd64 and arm64, and I also confirmed that it has a trixie suite before going further:
sudo dpkg --print-architecture
curl -sS -m 10 -o /dev/null -w "dists/trixie/Release: %{http_code}\n" https://deb.torproject.org/torproject.org/dists/trixie/Release
amd64
dists/trixie/Release: 200
Then install the prerequisites, add the signing key, and write the sources file in the deb822 format the Tor Project documents:
sudo apt install -y apt-transport-https gnupg wget
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null
sudo tee /etc/apt/sources.list.d/tor.sources >/dev/null <<'EOF'
Types: deb deb-src
URIs: https://deb.torproject.org/torproject.org/
Suites: trixie
Components: main
Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpg
EOF
Do not trust a key because a command downloaded it. Check that the fingerprint matches the one in the filename of the key URL:
gpg --show-keys --with-fingerprint /usr/share/keyrings/deb.torproject.org-keyring.gpg
pub rsa2048 2009-09-04 [SC] [expires: 2028-08-29]
A3C4 F0F9 79CA A22C DBA8 F512 EE8C BC9E 886D DD89
That matches. Now update, confirm apt prefers the new repository, and install Tor with the keyring package, which keeps the signing key current:
sudo apt update
apt-cache policy tor
sudo apt install -y tor deb.torproject.org-keyring
tor:
Installed: (none)
Candidate: 0.4.9.14-1~d13.trixie+1
Version table:
0.4.9.14-1~d13.trixie+1 500
500 https://deb.torproject.org/torproject.org trixie/main amd64 Packages
0.4.9.14-0+deb13u1 500
500 mirror+file:/etc/apt/mirrors/debian-security.list trixie-security/main amd64 Packages
The install also pulled in tor-geoipdb, torsocks and libtorsocks.
What the package sets up by default
The package starts Tor straight away, and the defaults are a client only. There are two units:
tor.service loaded active exited Anonymizing overlay network for TCP (multi-instance-master)
tor@default.service loaded active running Anonymizing overlay network for TCP
tor.service is a master unit that does no work itself. The process runs as tor@default. This is the detail that matters for the incident in Part 1: on Debian, systemctl disable tor does not stop a relay, because the real service is the instance unit. Part 8 shows that on a running service.
Tor listens on loopback only:
LISTEN 0 4096 127.0.0.1:9050 0.0.0.0:* users:(("tor",pid=14642,fd=6))
/etc/tor/torrc is entirely comments. The behaviour comes from the packaged defaults file, /usr/share/tor/tor-service-defaults-torrc:
DataDirectory /var/lib/tor
PidFile /run/tor/tor.pid
RunAsDaemon 1
User debian-tor
ControlSocket /run/tor/control GroupWritable RelaxDirModeCheck
ControlSocketsGroupWritable 1
SocksPort unix:/run/tor/socks WorldWritable
SocksPort 9050
CookieAuthentication 1
CookieAuthFileGroupReadable 1
CookieAuthFile /run/tor/control.authcookie
Log notice syslog
That means a SOCKS proxy on 127.0.0.1:9050 and a Unix socket at /run/tor/socks, cookie-authenticated control access, a dedicated debian-tor user, and no ExitRelay line, so this machine carries nobody else’s traffic. Because it is a client, it is not an exit.
Bootstrapping
Tor needs to fetch the network consensus and build its first circuits. The journal shows how long that took:
sudo journalctl -u tor@default --no-pager | grep Bootstrapped
The service started at 12:07:30 and reported Bootstrapped 100% (done) at 12:07:36. Six seconds on a fresh VM.
Proving traffic goes through Tor
The Tor Project runs an API that tells you whether the connection you are making arrives from a Tor exit. First without Tor, then through the SOCKS port:
curl -sS https://check.torproject.org/api/ip
curl -sS --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip
torsocks curl -sS https://check.torproject.org/api/ip
{"IsTor":false,"IP":"203.0.113.50"}
{"IsTor":true,"IP":"192.42.116.97"}
{"IsTor":true,"IP":"192.42.116.97"}
The first address is replaced with a documentation address here, since it is the lab’s real public IP. The other two are an exit relay. The repeated address is worth noting: both requests reused one circuit, because plain curl offers no SOCKS credentials, and Tor isolates streams by credentials, not by process.
socks5 or socks5-hostname
This is the mistake that catches people. With --socks5, curl resolves the hostname itself and hands Tor an IP address. With --socks5-hostname, curl hands Tor the name and Tor resolves it at the exit. Curl’s documentation describes the first form as doing the lookup locally. I tested it against an onion address, which makes the difference visible:
curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/
curl --socks5-hostname 127.0.0.1:9050 -o /dev/null -w "http=%{http_code}\n" http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/
curl: (97) Not resolving .onion address (RFC 7686)
http=301
Current curl refuses to look up an .onion name locally, which is a useful safety check. For normal domains there is no such refusal, so use the -hostname form, or torsocks, by habit. Part 9 measures what the local lookup sends over the wire with tcpdump.
Looking at the circuits
Tor’s control socket will list what it has built. This uses the cookie file from the defaults above and needs root to read it:
sudo python3 - <<'PY'
import socket, binascii, time
c = binascii.hexlify(open('/run/tor/control.authcookie','rb').read()).decode()
s = socket.socket(socket.AF_UNIX); s.connect('/run/tor/control')
s.sendall(f'AUTHENTICATE {c}\r\nGETINFO circuit-status\r\nQUIT\r\n'.encode())
time.sleep(1)
for line in s.recv(65536).decode().splitlines():
if 'BUILT' in line: print(line[:160])
PY
The output is a list of BUILT circuits: short internal one-hop circuits for directory work, and general-purpose circuits with three relays each. Reduced to relay nicknames, three of the general-purpose circuits from my VM looked like this (guard, middle, exit):
ForPrivacyNET -> FreeExit -> NTH17R7
magic848relay -> prsv -> NTH17R7
magic848relay -> ShinobiMidori -> Quintex400
Two guards are in use, ForPrivacyNET and magic848relay, which is the guard behaviour from Part 2 at work: the first hop comes from a small fixed set. The exits vary, and one exit, NTH17R7, appears in two circuits. The third relay in each circuit is the one that sees your traffic leave the network.
Practical limits
- Tor carries TCP only. UDP does not go through the SOCKS port, and
torsocksblocks it. - An application only uses Tor if it is told to. Pointing a browser at the SOCKS port is not a substitute for Tor Browser, which also deals with fingerprinting.
- Do not set
ExitNodesorStrictNodesto choose your own exit. The Tor Project advises against it, and it narrows the set of relays you blend in with.
The next part uses this client to reach a server over SSH, and compares sending a login across an exit with using an onion service.