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
For Tor hidden services, you usually need CURLPROXY_SOCKS5_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:
- use a SOCKS5 proxy
- send the hostname to the proxy
- let the proxy resolve it
That matches this command-line option:
curl --socks5-hostname 127.0.0.1:9050 http://example.onion
Example with a timeout
Timeouts are useful because Tor connections can be slower than normal HTTP requests.
<?php
$ch = curl_init('http://jhiwjjlqpyawmpjx.onion/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => '127.0.0.1:9050',
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
if ($response === false) {
echo 'Error: ' . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
This version is cleaner and easier to maintain.
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 toCURLPROXY_SOCKS5_HOSTNAME.- When
curl_exec()runs, libcurl connects to the Tor SOCKS proxy. - Instead of trying to resolve
exampleonionaddress.onionwith local DNS, libcurl sends the hostname to the proxy. - Tor recognizes the
.onionhostname and routes the request through the Tor network. - If the hidden service is reachable, the response body is returned.
- If something fails,
curl_error()gives the error message. curl_close()frees the cURL handle.
If you changed the proxy type to CURLPROXY_SOCKS5, step 6 would often be different: libcurl might try local name resolution first, which causes the Couldn't resolve host name error.
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($ch);
// log or throw exception
}
Conditional routing
Some codebases only route .onion requests through Tor and send regular domains normally.
<?php
if (str_ends_with(parse_url($url, PHP_URL_HOST), '.onion')) {
curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:9050');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
}
Reusable helper
<?php
function fetchViaTor(string $url): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => '127.0.0.1:9050',
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException($error);
}
curl_close($ch);
return $response;
}
This is the kind of structure you often see in production code.
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 false
Broken
$response = curl_exec($ch);
echo $response;
If the request fails, you may print nothing and miss the real issue.
Better
$response = curl_exec($ch);
if ($response === false) {
echo curl_error($ch);
}
4. Forgetting that Tor must actually be running
If Tor is not listening on 127.0.0.1:9050, the request cannot work.
Check that your Tor service is running and that the SOCKS port matches your configuration.
5. Ignoring timeouts
Tor can be slower than direct connections. Without timeouts, your script may hang longer than expected.
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
6. Assuming all hostname problems are PHP bugs
Often the issue is not PHP itself. It is simply the difference between:
- local DNS resolution
- proxy-side hostname resolution
Understanding that difference makes the error much easier to debug.
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 | Yes | Command-line equivalent of CURLPROXY_SOCKS5_HOSTNAME |
CURLOPT_PROXY value styles
| Style | Example | Recommended? | Why |
|---|---|---|---|
| Host and port | 127.0.0.1:9050 | Yes | Clear and common |
| Full URL-like string | http://127.0.0.1:9050/ | Not ideal | Can be confusing when combined with proxy type |
Normal domain vs .onion
| Host type | Local DNS works? | Proxy hostname resolution needed? |
|---|---|---|
example.com | Usually yes | Sometimes optional |
something.onion | No | Yes |
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 ($response === false) {
echo curl_error($ch);
}
curl_close($ch);
Command-line equivalent
curl --socks5-hostname 127.0.0.1:9050 http://exampleonionaddress.onion
Common error
Couldn't resolve host name
Usually means the hostname was being resolved locally instead of by Tor.
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?
It matters most for .onion domains because normal DNS cannot resolve them. For regular domains, either local or proxy resolution may work.
Is this a PHP bug?
No. It is usually a configuration issue about where hostname resolution happens.
Should I add timeouts when using Tor with cURL?
Yes. Tor connections can be slower, so CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT are good practice.
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.