Question

Using PHP cURL with a SOCKS5 Proxy to Access Tor .onion Services

phpcurlproxytor

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.onion locally, it fails with Couldn'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 first
  • CURLPROXY_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
  • .onion domain? let Tor resolve it through the proxy
Take Quiz

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:

  1. curl_init() creates a cURL handle for the .onion URL.
  2. CURLOPT_RETURNTRANSFER tells cURL to return the response as a string instead of printing it directly.
  3. CURLOPT_PROXY sets Tor's local SOCKS proxy, usually 127.0.0.1:9050.
  4. CURLOPT_PROXYTYPE is set to CURLPROXY_SOCKS5_HOSTNAME.
  5. When curl_exec() runs, libcurl connects to the Tor SOCKS proxy.
  6. Instead of trying to resolve exampleonionaddress.onion with local DNS, libcurl sends the hostname to the proxy.
  7. Tor recognizes the .onion hostname and routes the request through the Tor network.
  8. If the hidden service is reachable, the response body is returned.
  9. If something fails, curl_error() gives the error message.
  10. 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 .onion resources 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 .onion service 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.

Take Quiz

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 / BehaviorWhat it doesGood for .onion?Notes
CURLPROXY_SOCKS5Uses SOCKS5 proxy, but hostname may be resolved locallyNo, usually notCan trigger Couldn't resolve host name
CURLPROXY_SOCKS5_HOSTNAMEUses SOCKS5 proxy and lets proxy resolve hostnameYesBest choice for Tor hidden services
Command line --socks5SOCKS5 proxy, local resolution behaviorUsually no for .onionDepends on target and resolver
Command line --socks5-hostnameSOCKS5 proxy with proxy-side hostname resolutionYesCommand-line equivalent of CURLPROXY_SOCKS5_HOSTNAME

CURLOPT_PROXY value styles

StyleExampleRecommended?Why
Host and port127.0.0.1:9050YesClear and common
Full URL-like stringhttp://127.0.0.1:9050/Not idealCan be confusing when combined with proxy type

Normal domain vs .onion

Host typeLocal DNS works?Proxy hostname resolution needed?
example.comUsually yesSometimes optional
something.onionNoYes
Take Quiz

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_HOSTNAME for .onion addresses
  • Do not rely on local DNS for .onion
  • Tor usually listens on 127.0.0.1:9050
  • Check curl_exec() for false
  • 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.

Take Quiz

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

  1. Create a PHP function that accepts a URL as an argument.
  2. Configure cURL to use the local Tor SOCKS5 proxy.
  3. Ensure the proxy resolves the hostname, not the local machine.
  4. Return the response body when the request succeeds.
  5. Throw or display a clear error message when the request fails.
Take Quiz

Keep learning

Related questions

View more