The Tor cheat sheet covers onion routing internals, torrc configuration, bridges and pluggable transports, onion (hidden) services, stem/nyx control, torsocks, DNS over Tor, and the OPSEC pitfalls that deanonymize users.
A Tor circuit is built through 3 relays. Each hop peels one layer of encryption, so no single relay knows both who you are and where you are going.
| Hop | Relay | Knows | Never Knows |
|---|---|---|---|
| 1 | Your real IP | Destination | |
| 2 | Prev + next relay | Your IP or destination | |
| 3 | Destination + traffic | Your real IP |
You -> [Guard] -> [Middle] -> [Exit] -> Destination
layer 3 layer 2 layer 1 plaintext*
* only if the destination itself is not encrypted (HTTPS/onion)
MaxCircuitDirtiness)..onion end-to-end.# Debian/Ubuntu
sudo apt install tor torsocks nyx
# Arch / Gentoo
sudo pacman -S tor torsocks nyx
emerge net-vpn/tor
# Tor Browser Bundle (recommended for browsing)
# download from https://www.torproject.org/download/
# then verify the signature (see below)
# Start / enable the service
sudo systemctl enable --now tor
systemctl status tor
# Config lives here
/etc/tor/torrc # system daemon
~/.tor/torrc # per-user
# Import the Tor Browser signing key
gpg --auto-key-locate nodefault,wkd \
--locate-keys [email protected]
# Verify the detached signature
gpg --verify tor-browser-*.tar.xz.asc \
tor-browser-*.tar.xz
# expect: Good signature from "Tor Browser Developers"
# Confirm you are routed through Tor (JSON)
curl --socks5-hostname 127.0.0.1:9050 \
https://check.torproject.org/api/ip
# {"IsTor":true,"IP":"185.220.x.x"}
# torsocks wraps any TCP app
torsocks curl https://check.torproject.org/api/ip
## /etc/tor/torrc - the common, documented options
# SOCKS proxy Tor listens on (apps point here)
SocksPort 9050
SocksPort 127.0.0.1:9150 # extra listener (Tor Browser uses 9150)
# Control port for stem / nyx (pick ONE auth method)
ControlPort 9051
CookieAuthentication 1 # file-based auth (preferred)
# HashedControlPassword 16:... # from: tor --hash-password 'secret'
# Where Tor keeps state (keys, guards, consensus)
DataDirectory /var/lib/tor
# Logging
Log notice stdout
Log notice file /var/log/tor/notices.log
# Only used as a client (do NOT relay/exit)
ClientOnly 1
# Isolate streams so different apps do not share a circuit
SocksPort 9050 IsolateDestAddr IsolateDestPort
# Restrict exit to specific countries (ISO 3166 codes)
ExitNodes {us},{de},{nl}
StrictNodes 1 # never fall outside the set
# Force / avoid entry guards
EntryNodes {ch}
ExcludeNodes {ru},{cn},{ir},{by} # avoid these countries entirely
ExcludeExitNodes {us}
# Pin a specific relay by fingerprint
ExitNodes ABCD1234...FINGERPRINT
# Longer-lived circuits (less churn, more linkability - trade-off)
MaxCircuitDirtiness 600
StrictNodes 1makes the Exit/Entry sets hard constraints. Without it Tor treats them as a preference and may pick outside the set when no relay matches - which silently defeats geo-restrictions.
| Directive | Purpose |
|---|---|
SocksPort 9050 |
|
ControlPort 9051 |
|
ExitNodes {cc} |
Restrict exit country |
ExcludeNodes {cc} |
Blacklist relays by country |
StrictNodes 1 |
Enforce node sets strictly |
MaxCircuitDirtiness |
Circuit reuse window (s) |
NewCircuitPeriod |
How often new circuits are considered |
EnforceDistinctSubnets 1 |
Avoid same /16 in one circuit |
ClientOnly 1 |
Never act as a relay |
Bridges are unlisted relays; pluggable transports (PT) disguise the traffic so DPI cannot fingerprint Tor. Use them when Tor is blocked.
| Transport | Disguise | Best For |
|---|---|---|
obfs4 |
Most blocking; needs bridge address | |
snowflake |
Fast-changing IPs, no fixed bridge | |
meek-azure |
Heavy DPI where obfs4 IPs are blocked | |
webtunnel |
HTTPS website traffic | Looks like normal web browsing |
## torrc
UseBridges 1
ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy
# Get bridges: https://bridges.torproject.org
# or email [email protected] (from Gmail/Riseup)
Bridge obfs4 192.0.2.1:443 CERT=... iat-mode=0
Bridge obfs4 198.51.100.2:9443 CERT=... iat-mode=0
## Snowflake - no bridge line to distribute
UseBridges 1
ClientTransportPlugin snowflake exec /usr/bin/snowflake-client
Bridge snowflake 192.0.2.3:80 \
fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 \
url=https://snowflake-broker.torproject.net/ \
ice=stun:stun.l.google.com:19302
## meek-azure - domain fronting via a CDN
ClientTransportPlugin meek_lite exec /usr/bin/obfs4proxy
Bridge meek_lite 192.0.2.4:443 \
url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
## /etc/tor/torrc - map an .onion address to a local port
HiddenServiceDir /var/lib/tor/my_service/
HiddenServicePort 80 127.0.0.1:8080
HiddenServicePort 22 127.0.0.1:22 # multiple ports allowed
# Reload, then read the generated 56-char v3 address
sudo systemctl reload tor
sudo cat /var/lib/tor/my_service/hostname
# abcd...xyz.onion (base32, ed25519 pubkey)
# Files created in HiddenServiceDir:
# hostname -> your .onion address
# hs_ed25519_secret_key -> PRIVATE key, back up + guard it
# hs_ed25519_public_key
# Client authorization - only holders of a key can connect
HiddenServiceDir /var/lib/tor/my_service/
HiddenServiceVersion 3
# put authorized pubkeys in <HiddenServiceDir>/authorized_clients/
# <name>.auth: descriptor:x25519:<base32-pubkey>
# Defend against guard-discovery / DoS
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceMaxStreams 100
HiddenServiceMaxStreamsCloseCircuit 1
# Single-hop (faster, LESS anonymous - server location leaks)
# HiddenServiceSingleHopMode 1 # only if server anonymity is not needed
Bind the backend to
127.0.0.1only. If it also listens on a public interface, scanners can correlate the clearnet host with the onion service and deanonymize it. Strip server banners and version headers too.
# Generate a vanity .onion prefix (offline, brute-force ed25519)
mkp224o -d ./onions -n 1 myprefix
# then copy the produced keys into HiddenServiceDir
# Sanity-check reachability from a client
torsocks curl http://abcd...xyz.onion
# Terminal dashboard: bandwidth, circuits, connections, log
nyx
nyx -i 127.0.0.1:9051 # explicit control port
# In nyx:
# c -> connections/circuits view
# g -> circuit (graph) view
# right/left arrows switch pages
from stem import Signal
from stem.control import Controller
with Controller.from_port(port=9051) as c:
c.authenticate() # cookie auth
# Request a fresh identity (new circuits)
c.signal(Signal.NEWNYM)
# List active circuits and their path
for circ in c.get_circuits():
if circ.status == 'BUILT':
path = ' -> '.join(
f'{r.nickname}' for r in circ.path)
print(circ.id, circ.purpose, path)
# Read config live
print(c.get_conf('SocksPort'))
c.set_conf('ExitNodes', '{de}')
# Signal NEWNYM over the control port (cookie/none auth)
echo -e 'AUTHENTICATE ""\r\nSIGNAL NEWNYM\r\nQUIT' \
| nc 127.0.0.1 9051
# Inspect circuits
echo -e 'AUTHENTICATE ""\r\nGETINFO circuit-status\r\nQUIT' \
| nc 127.0.0.1 9051
# Route a single command's TCP through Tor
torsocks curl https://example.com
torsocks ssh user@host
torsocks git clone https://...
# Spawn a shell where everything is torified
torsocks --shell
# Point apps at the SOCKS proxy directly
export ALL_PROXY=socks5h://127.0.0.1:9050
curl --socks5-hostname 127.0.0.1:9050 https://example.com
## torrc - resolve DNS through the Tor network
DNSPort 5353
AutomapHostsOnResolve 1
AutomapHostsSuffixes .onion,.exit
# Query it
dig -p 5353 @127.0.0.1 example.com
tor-resolve example.com 127.0.0.1:9050
Use
socks5h://(the trailingh) or--socks5-hostnameso the resolver runs inside Tor. Plainsocks5://resolves DNS locally first - that leaks the hostname to your ISP even though the TCP goes through Tor.
| Level | JavaScript | Notes |
|---|---|---|
| Enabled everywhere | Most usable, largest attack surface | |
| Disabled on non-HTTPS | JIT off, some media click-to-play | |
| Disabled everywhere | No JS/SVG/JIT, hardest to exploit |
Ctrl+Shift+U) wipes state and builds fresh circuits.Tor Browser is a hardened Firefox ESR that also normalizes your fingerprint (fonts, canvas, timezone, resolution, user-agent). Pointing a normal browser at 127.0.0.1:9050 anonymizes the IP but leaves you trivially fingerprintable and prone to WebRTC/DNS leaks.
.onion when one exists.