TechEarl

Use Tor as a SOCKS5 Proxy with curl, Python, and Node

Route a single command, script, or HTTP client through Tor's SOCKS5 proxy — curl with --socks5-hostname, Python requests with socks5h://, Node with socks-proxy-agent — and avoid the DNS leak that catches everyone first time.

Ishan Karunaratne⏱️ 7 min readUpdated
Share thisCopied
Route curl, Python requests, and Node fetch through Tor's SOCKS5 proxy. Avoid DNS leaks with socks5-hostname and socks5h://. Working examples and verification.

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:

bash
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:

bash
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

bash
# 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.com

The 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:

bash
# 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.com

The h in socks5h stands for "hostname", meaning "resolve at the SOCKS hop, not here."

Verify the exit country

bash
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:

bash
pip install 'requests[socks]'

That pulls in PySocks, which requests uses transparently for SOCKS URLs.

python
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:

python
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:

python
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.

bash
npm install socks-proxy-agent undici

Using undici.fetch (a drop-in fetch that accepts a dispatcher):

javascript
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:

javascript
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:

javascript
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:

bash
torsocks curl https://check.torproject.org/api/ip
torsocks git clone https://github.com/user/repo.git
torsocks ssh user@example.com

This 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:

bash
# 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 flag

If 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

See also

TagsTORSOCKS5curlPythonNodeNetworkingPrivacyAnonymity

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

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.

How to Convert a PSD to PNG From the Command Line

Convert a PSD to PNG from the command line with ImageMagick. The one trick that matters: design.psd[0] selects the flattened composite, so you get one PNG instead of a folder full of separate layers.