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 undiciSocksProxyAgent is an https.Agent, not an undici Dispatcher, so you cannot hand it straight to undici.fetch. For undici.fetch you need a real SOCKS dispatcher. The cleanest path is fetch-socks, which wraps the socks package as an undici-compatible dispatcher:
npm install undici fetch-socksimport { fetch } from "undici";
import { socksDispatcher } from "fetch-socks";
const dispatcher = socksDispatcher({
type: 5, // SOCKS5
host: "127.0.0.1",
port: 9050,
});
const res = await fetch("https://check.torproject.org/api/ip", {
dispatcher,
});
console.log(await res.json());
// { IsTor: true, IP: '151.115.X.X' }socksDispatcher resolves the destination hostname at the SOCKS hop (the socks5h behaviour), so there is no local DNS leak. The axios and native node:https snippets below use socks-proxy-agent instead, which is the right tool for the classic agent-based APIs.
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.
FAQ
The h stands for "hostname". With socks5h://, the client sends the hostname as a string to the SOCKS proxy and lets the proxy do the DNS lookup. With socks5://, the client resolves the hostname locally first, then sends a raw IP to the proxy.
For Tor specifically, you always want socks5h://, otherwise your ISP sees the DNS queries even though the TCP stream went through Tor.
So both can run on the same machine without colliding. The standalone Tor daemon installed by your package manager binds to 9050 by default. Tor Browser ships its own bundled daemon and uses 9150 so it doesn't fight with a system-wide tor.
Check which is running with ss -ltn | grep -E '9050|9150' on Linux or lsof -iTCP -sTCP:LISTEN -P | grep -E '9050|9150' on macOS.
SOCKS5 carries arbitrary TCP, so anything TCP-based works, SSH, IMAP, IRC, raw sockets. UDP support is more limited (only some clients and only with specific Tor builds). For most practical purposes, treat Tor as TCP-only.
For UDP, use a separate VPN-over-Tor setup or accept the limitation.
That uses Tor's control port (default 9051), not the SOCKS port. Send a NEWNYM signal via the control protocol, see the dedicated NEWNYM article for the script.
Rate-limited: Tor enforces a minimum interval (default 10 seconds) between NEWNYM signals to avoid abuse.
Not directly, Node's built-in fetch doesn't accept an agent or dispatcher. Use undici.fetch (which is what Node's fetch wraps internally) with the dispatcher option, or switch to node-fetch/axios which both accept an agent.
Newer Node releases (24+) are tightening this; check the release notes if you need built-in fetch + SOCKS.
Go's net/http ignores HTTP_PROXY for HTTPS unless you use HTTPS_PROXY too, and even then it does its own DNS resolution by default. For SOCKS5 in Go, use golang.org/x/net/proxy and build the http.Transport.Dial from the SOCKS5 dialer, that way Go sends the hostname to the proxy rather than resolving locally.
See also
- Tor exit nodes list by country: pin which country your SOCKS proxy exits through with ExitNodes and ExcludeNodes
- Tor Bridges Explained: obfs4, Snowflake, and meek: keep the SOCKS proxy reachable when the network blocks Tor
- Host a v3 .onion Hidden Service with Tor: the inverse use case where you offer a service over Tor rather than reaching one
- Force a New Tor Circuit on Demand with NEWNYM: rotate the exit IP between SOCKS requests
- torrc Cheat Sheet: SocksPort, IsolateDestAddr, and the rest of the SOCKS-relevant directives
Sources
Authoritative references this article was fact-checked against.
- curl man page (--socks5-hostname), curl.securl.se
- socks-proxy-agent, npmnpmjs.com
- fetch-socks (undici SOCKS dispatcher), npmnpmjs.com
- undici documentation, Node.jsundici.nodejs.org





![Convert a PSD to PNG From the Command Line (ImageMagick) Convert a PSD to PNG from the command line with ImageMagick using the [0] composite-layer trick, extract individual layers, batch a folder, and the Maximize Compatibility caveat.](https://images.techearl.com/convert-psd-to-png-command-line/convert-psd-to-png-command-line-1536.webp?v=2026-06-01T12%3A00%3A00Z)