Question
I want to connect to a Tor hidden service from PHP using cURL.
Here is the code I am using:
<?php
$url = 'http://jhiwjjlqpyawmpjx.onion/';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:9050');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
$output = curl_exec($ch);
$curl_error = curl_error($ch);
curl_close($ch);
print_r($output);
print_r($curl_error);
This produces the error:
Couldn't resolve host name
But the equivalent command works from the Ubuntu terminal:
curl -v --socks5-hostname localhost:9050 http://jhiwjjlqpyawmpjx.onion
Why does the command-line version work while the PHP version fails, and how can I configure PHP cURL so the SOCKS5 proxy resolves the .onion hostname instead of PHP/libcurl trying to resolve it locally?
Short Answer
By the end of this page, you will understand why .onion addresses often fail with normal PHP cURL settings, how SOCKS5 proxy resolution works, and how to configure PHP cURL so Tor resolves the hostname correctly. You will also see common mistakes, practical examples, and a small project that fetches a page through Tor safely.
Concept
A Tor hidden service uses a .onion hostname, which is not a normal public DNS name. Your operating system's DNS resolver usually cannot resolve it.
When you send a request through a SOCKS5 proxy, there are two possible ways hostname resolution can happen:
- Local resolution: your machine tries to resolve the hostname before sending the connection to the proxy
- Proxy resolution: the proxy receives the hostname and resolves it itself
For .onion addresses, proxy resolution is required. Tor understands .onion names, but your normal DNS setup does not.
That is why this matters:
- If PHP/libcurl tries to resolve
example.onionlocally, it fails withCouldn't resolve host name - If Tor resolves it through SOCKS5, the request can succeed
In command-line curl, the option:
--socks5-hostname
means: use a SOCKS5 proxy and let the proxy resolve the hostname.
In PHP cURL, the matching idea is to use the proxy type that tells libcurl to pass the hostname to the SOCKS5 proxy instead of resolving it locally.
The important distinction is:
CURLPROXY_SOCKS5→ often resolves locally firstCURLPROXY_SOCKS5_HOSTNAME→ proxy resolves the hostname
Mental Model
Think of a SOCKS5 proxy like a courier service.
- With local resolution, you tell the courier: "Go to the building at this exact street address." But you first need to translate the name into an address yourself.
- With proxy resolution, you tell the courier: "Take this name and figure out where it goes." The courier handles the lookup.
A .onion address is like a destination that only a special courier understands. Tor is that special courier. If you try to look it up yourself using normal DNS, you get nowhere.
So the rule is simple:
- Normal domain? local or proxy resolution may both work
.oniondomain? let Tor resolve it through the proxy
Syntax and Examples
The key setting in PHP is the proxy type.
Correct syntax for Tor .onion services
<?php
$url = 'http://jhiwjjlqpyawmpjx.onion/';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:9050');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
$output = curl_exec($ch);
if ($output === false) {
echo 'cURL error: ' . curl_error($ch);
} else {
echo $output;
}
curl_close($ch);
Why this works
CURLPROXY_SOCKS5_HOSTNAME tells libcurl:
Step by Step Execution
Consider this code:
<?php
$ch = curl_init('http://exampleonionaddress.onion/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:9050');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
$response = curl_exec($ch);
if ($response === false) {
echo curl_error($ch);
}
curl_close($ch);
Here is what happens step by step:
curl_init()creates a cURL handle for the.onionURL.CURLOPT_RETURNTRANSFERtells cURL to return the response as a string instead of printing it directly.CURLOPT_PROXYsets Tor's local SOCKS proxy, usually127.0.0.1:9050.CURLOPT_PROXYTYPEis set to .
Real World Use Cases
This pattern appears in real software whenever an application must send traffic through Tor or another SOCKS5 proxy.
Common use cases
- Privacy-focused scripts that fetch data without direct outbound connections
- Monitoring hidden services to verify uptime or page availability
- Security research tools that access
.onionresources in controlled environments - Backend services that must route selected requests through Tor
- Scrapers or crawlers designed to work with SOCKS5-based hostname resolution
Example scenarios
- A PHP script checks whether an internal
.onionservice is online every 5 minutes - A web tool downloads content from a hidden service for later analysis
- A command-line PHP script sends requests through Tor to avoid exposing the server's direct IP
The underlying idea is always the same: if the target hostname is only meaningful to the proxy, the proxy must resolve it.
Real Codebase Usage
In real projects, developers usually wrap proxy-aware cURL setup in a helper function so they do not repeat the same options everywhere.
Common patterns
Guard clauses
Fail early if the cURL extension is missing or Tor is not available.
<?php
if (!function_exists('curl_init')) {
throw new RuntimeException('cURL extension is not enabled.');
}
Centralized configuration
Keep proxy settings in one place:
<?php
$torProxy = '127.0.0.1:9050';
$proxyType = CURLPROXY_SOCKS5_HOSTNAME;
This makes it easier to change environments later.
Timeouts and error handling
Developers rarely call curl_exec() without checking errors.
<?php
$result = curl_exec($ch);
if ($result === false) {
$error = curl_error();
}
Common Mistakes
1. Using CURLPROXY_SOCKS5 instead of CURLPROXY_SOCKS5_HOSTNAME
This is the main problem in the original question.
Broken
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
Correct
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
CURLPROXY_SOCKS5 can lead to local DNS resolution. .onion names need proxy-side resolution.
2. Including the proxy as a full URL when only host and port are needed
Some developers write:
curl_setopt($ch, CURLOPT_PROXY, 'http://127.0.0.1:9050/');
This may work in some cases, but the clearer and more typical form is:
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:9050');
3. Not checking whether curl_exec() returned
Comparisons
| Option / Behavior | What it does | Good for .onion? | Notes |
|---|---|---|---|
CURLPROXY_SOCKS5 | Uses SOCKS5 proxy, but hostname may be resolved locally | No, usually not | Can trigger Couldn't resolve host name |
CURLPROXY_SOCKS5_HOSTNAME | Uses SOCKS5 proxy and lets proxy resolve hostname | Yes | Best choice for Tor hidden services |
Command line --socks5 | SOCKS5 proxy, local resolution behavior | Usually no for .onion | Depends on target and resolver |
Command line --socks5-hostname | SOCKS5 proxy with proxy-side hostname resolution |
Cheat Sheet
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:9050');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
Quick rules
- Use
CURLPROXY_SOCKS5_HOSTNAMEfor.onionaddresses - Do not rely on local DNS for
.onion - Tor usually listens on
127.0.0.1:9050 - Check
curl_exec()forfalse - Use
curl_error()for debugging - Add timeouts for reliability
Minimal working example
<?php
$ch = curl_init('http://exampleonionaddress.onion/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => '127.0.0.1:9050',
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
]);
$response = curl_exec($ch);
if ( === ) {
();
}
();
FAQ
Why does PHP cURL say Couldn't resolve host name for a .onion URL?
Because local DNS does not understand .onion addresses. You must let the SOCKS5 proxy resolve the hostname.
What is the PHP equivalent of curl --socks5-hostname?
Use:
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
What is the difference between CURLPROXY_SOCKS5 and CURLPROXY_SOCKS5_HOSTNAME?
CURLPROXY_SOCKS5 may resolve the hostname locally. CURLPROXY_SOCKS5_HOSTNAME sends the hostname to the proxy for resolution.
Do I need Tor installed and running locally?
Yes, if you are connecting through 127.0.0.1:9050, Tor must be running and listening on that port.
Can I use http://127.0.0.1:9050/ as the proxy value?
It is better to use 127.0.0.1:9050. It is simpler and avoids confusion.
Does this only matter for .onion domains?
Mini Project
Description
Build a small PHP function that fetches a page through Tor using cURL. This project demonstrates the correct SOCKS5 hostname resolution setup for .onion addresses and shows how to handle network errors cleanly.
Goal
Create a reusable PHP function that requests a URL through Tor and returns the response body or a useful error.
Requirements
- Create a PHP function that accepts a URL as an argument.
- Configure cURL to use the local Tor SOCKS5 proxy.
- Ensure the proxy resolves the hostname, not the local machine.
- Return the response body when the request succeeds.
- Throw or display a clear error message when the request fails.
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.