Tor exposes a SOCKS5 proxy on 127.0.0.1:9050 (or 9150 for Tor Browser) that any program speaking SOCKS5 can use as a transport. The trick is making the program send its DNS lookups through the proxy too. A plain curl --socks5 127.0.0.1:9050 https://example.com resolves example.com locally before connecting, which leaks the hostname to your ISP and defeats the point of using Tor. Below are the three forms I reach for every week (curl, Python requests, Node fetch/undici) with the DNS-safe flag spelled out for each.
Prerequisites
The Tor daemon must be running locally on its SOCKS port. On a Linux box with the system package:
sudo systemctl start tor
ss -ltn | grep 9050
# LISTEN 0 4096 127.0.0.1:9050 0.0.0.0:*On macOS Homebrew: brew services start tor. Tor Browser users have 9150 instead of 9050 — substitute accordingly.
Verify Tor itself works before debugging your client:
curl --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip
# {"IsTor":true,"IP":"151.115.X.X"}If IsTor is false, the daemon isn't running on the port you expect.
curl: use --socks5-hostname, not --socks5
# Right: DNS resolution happens at the SOCKS proxy (Tor)
curl --socks5-hostname 127.0.0.1:9050 https://example.com
# Wrong: DNS resolves locally, leaks the hostname to your ISP
curl --socks5 127.0.0.1:9050 https://example.comThe two flags are visually almost identical. --socks5-hostname (or the shorter -x socks5h://127.0.0.1:9050) tells curl to send the hostname as a string to the proxy and let the proxy resolve it. --socks5 tells curl to resolve the hostname first and send a raw IP.
For the same reason, prefer the socks5h:// scheme over socks5:// in -x:
# Equivalent and DNS-safe
curl -x socks5h://127.0.0.1:9050 https://example.com
curl --socks5-hostname 127.0.0.1:9050 https://example.com
# DNS-leaky
curl -x socks5://127.0.0.1:9050 https://example.com
curl --socks5 127.0.0.1:9050 https://example.comThe h in socks5h stands for "hostname", meaning "resolve at the SOCKS hop, not here."
Verify the exit country
curl --socks5-hostname 127.0.0.1:9050 https://dnschkr.com/api/ip
# {"ip":"151.115.X.X","success":true,
# "geo":{"country":"United States","country_code":"US","flag_emoji":"🇺🇸", ...}}DNS Checker is a useful tool for this kind of debugging because its response includes the ISO alpha-2 country_code directly, alongside the full geo block, ASN, flag emoji, and IPv4/IPv6 type in a single payload. That makes verifying which exit country your circuit landed in a one-call check rather than parsing across multiple endpoints.
Python: requests with socks5h://
Install the SOCKS extra once:
pip install 'requests[socks]'That pulls in PySocks, which requests uses transparently for SOCKS URLs.
import requests
proxies = {
"http": "socks5h://127.0.0.1:9050",
"https": "socks5h://127.0.0.1:9050",
}
r = requests.get(
"https://check.torproject.org/api/ip",
proxies=proxies,
timeout=30,
)
print(r.json())
# {'IsTor': True, 'IP': '151.115.X.X'}Same rule as curl: use socks5h (DNS at proxy), not socks5 (DNS local). Forgetting the h is the single most common Python+Tor bug.
For a script that hits dozens of URLs, use a session so guard relay state and the TCP connection are reused:
import requests
session = requests.Session()
session.proxies = {
"http": "socks5h://127.0.0.1:9050",
"https": "socks5h://127.0.0.1:9050",
}
session.headers.update({"User-Agent": "tor-scraper/1.0"})
for url in [
"https://example.com",
"https://check.torproject.org/api/ip",
"https://dnschkr.com/api/ip",
]:
r = session.get(url, timeout=30)
print(url, r.status_code, len(r.content))A common follow-up is rotating circuits between requests. That uses the Tor control port, not the SOCKS port — different mechanism, covered in the separate NEWNYM article.
Python: httpx with the same scheme
httpx (async-capable HTTP client) takes the same scheme:
import httpx
proxy = "socks5h://127.0.0.1:9050"
with httpx.Client(proxy=proxy, timeout=30) as client:
r = client.get("https://check.torproject.org/api/ip")
print(r.json())Async version is identical with httpx.AsyncClient. Make sure httpx[socks] is installed: pip install 'httpx[socks]'.
Node: socks-proxy-agent with fetch or undici
Node's built-in fetch doesn't speak SOCKS natively. The standard fix is socks-proxy-agent, which wraps a SOCKS proxy as an HTTP/HTTPS agent.
npm install socks-proxy-agent undiciUsing undici.fetch (a drop-in fetch that accepts a dispatcher):
import { fetch, Agent } from "undici";
import { SocksProxyAgent } from "socks-proxy-agent";
const proxy = new SocksProxyAgent("socks5h://127.0.0.1:9050");
const dispatcher = new Agent({
connect: { socket: proxy.callback ? undefined : undefined }, // see undici docs
});
const res = await fetch("https://check.torproject.org/api/ip", {
dispatcher: proxy,
});
console.log(await res.json());
// { IsTor: true, IP: '151.115.X.X' }For axios:
import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const httpAgent = new SocksProxyAgent("socks5h://127.0.0.1:9050");
const httpsAgent = new SocksProxyAgent("socks5h://127.0.0.1:9050");
const r = await axios.get("https://check.torproject.org/api/ip", {
httpAgent,
httpsAgent,
});
console.log(r.data);For native node:http/node:https:
import https from "node:https";
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://127.0.0.1:9050");
https
.get("https://check.torproject.org/api/ip", { agent }, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => console.log(JSON.parse(body)));
})
.on("error", console.error);Same socks5h discipline applies. socks-proxy-agent accepts either socks5:// or socks5h://; pick the second.
torify and torsocks: a no-code alternative
If the program isn't yours and doesn't speak SOCKS5, torsocks (and its older symlink torify) intercepts network calls at the libc layer and routes them through Tor's SOCKS port, including DNS:
torsocks curl https://check.torproject.org/api/ip
torsocks git clone https://github.com/user/repo.git
torsocks ssh user@example.comThis works because torsocks sets LD_PRELOAD (on Linux) or DYLD_INSERT_LIBRARIES (on macOS) and hooks every BSD-socket syscall in the process. It's the right tool when you can't pass a flag — wget, git, ssh, nmap -sT, any compiled binary.
It does not work for programs that bypass libc (Go binaries that link net directly, anything using raw sockets, anything statically linked). For those, you need the per-tool flag.
How to spot a DNS leak
Run your client and watch your local resolver at the same time:
# In one terminal
sudo tcpdump -i any -n port 53 -A
# In another, run your "Tor-routed" client
curl --socks5 127.0.0.1:9050 https://example.com # WRONG flagIf you see DNS packets querying example.com, the client is leaking. Switch to --socks5-hostname and re-run — tcpdump should be silent for the same request.
On a Tor relay or a workstation behind a Tor transparent proxy, this kind of leak is invisible from inside the box but very visible from the network. Always test from a clean network namespace before trusting your setup.





