PHPackages                             binarylogic/tor-exit-nodes - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. binarylogic/tor-exit-nodes

ActiveLibrary

binarylogic/tor-exit-nodes
==========================

Download the Tor exit nodes list and check if a given IP address is a Tor exit node.

00

Since Jul 19Compare

[ Source](https://github.com/BinaryLogicRo/Tor-exit-nodes)[ Packagist](https://packagist.org/packages/binarylogic/tor-exit-nodes)[ RSS](/packages/binarylogic-tor-exit-nodes/feed)WikiDiscussions Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

Tor Exit Nodes
==============

[](#tor-exit-nodes)

This package provides a simple way to download the Tor exit node list from the [Onionoo API](https://metrics.torproject.org/onionoo.html) and check whether an IP address belongs to a Tor exit node. See the [examples](#examples) section for usage.

Features
--------

[](#features)

Main features:

- **Downloader** — retrieves the exit node list from the Onionoo API, in memory, with no filesystem involved.
- **Filesystem writer** — saves a downloaded (or hand-built) list to a local JSON file.
- **Filesystem reader** — loads a previously saved list back from that file.
- **Checker** — answers whether an IP address is a known exit node, with no network or filesystem access.

Code quality features:

- **Modular by design** — each piece above works standalone; use any of them without the others.
- **IPv4 and IPv6 support** — addresses are compared in canonical form, so notation differences don't cause false negatives.
- **Typed exceptions** — every failure mode has a dedicated exception, all catchable through one common interface.
- **Swappable HTTP client** — ships with a Guzzle implementation, but the transport is an interface you can replace.

Requirements
------------

[](#requirements)

- PHP 8.2 or newer
- Composer

Installation
------------

[](#installation)

```
composer require binarylogic/tor-exit-nodes
```

Examples
--------

[](#examples)

### Keep a local cache fresh without downloading on every request

[](#keep-a-local-cache-fresh-without-downloading-on-every-request)

The file's modification time is the only bookkeeping involved — no cache driver, no scheduled job, no extra metadata to keep in sync. Saving goes through an atomic write, so a request that reads the file while another one refreshes it still sees a complete list.

```
use Binarylogic\TorExitNodes\Downloader\ExitNodeDownloader;
use Binarylogic\TorExitNodes\Http\GuzzleHttpClient;
use Binarylogic\TorExitNodes\Storage\ExitNodeFileReader;
use Binarylogic\TorExitNodes\Storage\ExitNodeFileWriter;

$path = __DIR__.'/storage/tor-exit-nodes.json';
$maxAgeInSeconds = 3600;

$isStale = ! is_file($path) || filemtime($path) < time() - $maxAgeInSeconds;

if ($isStale) {
    $exitNodes = (new ExitNodeDownloader(new GuzzleHttpClient()))->downloadExitNodes();

    (new ExitNodeFileWriter($path))->saveExitNodes($exitNodes);
} else {
    $exitNodes = (new ExitNodeFileReader($path))->loadExitNodes();
}
```

### Keep serving the last good list if a refresh fails

[](#keep-serving-the-last-good-list-if-a-refresh-fails)

Catching both exceptions covers an unreachable Onionoo and a response that arrived but made no sense, and the saved file is only ever replaced once a payload has parsed cleanly. Note that the fallback needs a file to fall back to: on a first run with nothing saved yet, the reader throws `LoadFailedException`.

```
use Binarylogic\TorExitNodes\Downloader\ExitNodeDownloader;
use Binarylogic\TorExitNodes\Exception\DownloadFailedException;
use Binarylogic\TorExitNodes\Exception\MalformedExitNodeListException;
use Binarylogic\TorExitNodes\Http\GuzzleHttpClient;
use Binarylogic\TorExitNodes\Storage\ExitNodeFileReader;
use Binarylogic\TorExitNodes\Storage\ExitNodeFileWriter;

$path = __DIR__.'/storage/tor-exit-nodes.json';

try {
    $exitNodes = (new ExitNodeDownloader(new GuzzleHttpClient()))->downloadExitNodes();

    (new ExitNodeFileWriter($path))->saveExitNodes($exitNodes);
} catch (DownloadFailedException|MalformedExitNodeListException) {
    // Onionoo is unreachable or returned something unexpected; keep using the last saved list.
    $exitNodes = (new ExitNodeFileReader($path))->loadExitNodes();
}
```

### Reject requests coming from a Tor exit node

[](#reject-requests-coming-from-a-tor-exit-node)

Drop this in a front controller or middleware. Behind a proxy or load balancer, `REMOTE_ADDR` is the proxy's address rather than the visitor's, so read the client address from your framework's trusted-proxy handling instead. Each request pays one file read here; in a long-lived process, load the list once and reuse the checker.

```
use Binarylogic\TorExitNodes\Checker\ExitNodeChecker;
use Binarylogic\TorExitNodes\Storage\ExitNodeFileReader;

$reader = new ExitNodeFileReader(__DIR__.'/storage/tor-exit-nodes.json');
$checker = new ExitNodeChecker($reader->loadExitNodes());

if ($checker->isExitNode($_SERVER['REMOTE_ADDR'])) {
    http_response_code(403);
    exit;
}
```

### Check many IP addresses at once

[](#check-many-ip-addresses-at-once)

Useful for sifting Tor traffic out of log entries, an export, or a report. Lookups hit an in-memory hash map, so the size of the list barely affects the cost of a pass. Unparseable entries are treated as non-Tor here; rethrow instead if malformed input is something you would rather hear about.

```
use Binarylogic\TorExitNodes\Checker\ExitNodeChecker;
use Binarylogic\TorExitNodes\Exception\InvalidIpAddressException;

$checker = new ExitNodeChecker($exitNodes);

$ipAddressesFromLogFile = ['185.220.101.1', '8.8.8.8', '2001:db8::1', 'not-an-ip'];

$torIpAddresses = array_filter($ipAddressesFromLogFile, function (string $ipAddress) use ($checker): bool {
    try {
        return $checker->isExitNode($ipAddress);
    } catch (InvalidIpAddressException) {
        return false;
    }
});
```

Public API
----------

[](#public-api)

ClassResponsibility`Binarylogic\TorExitNodes\Downloader\ExitNodeDownloader`Retrieves the exit node list from Onionoo`Binarylogic\TorExitNodes\Http\HttpClient`HTTP abstraction the downloader depends on`Binarylogic\TorExitNodes\Http\GuzzleHttpClient`Guzzle implementation of `HttpClient``Binarylogic\TorExitNodes\Parser\OnionooExitNodeParser`Turns an Onionoo JSON response into an `ExitNodeList``Binarylogic\TorExitNodes\Storage\ExitNodeFileWriter`Saves an `ExitNodeList` to a JSON file`Binarylogic\TorExitNodes\Storage\ExitNodeFileReader`Loads an `ExitNodeList` from a JSON file`Binarylogic\TorExitNodes\Storage\ExitNodeListJsonSerializer`Encodes and decodes the stored JSON format`Binarylogic\TorExitNodes\Checker\ExitNodeChecker`Answers whether an IP address is an exit node`Binarylogic\TorExitNodes\ExitNode`Immutable single exit node address`Binarylogic\TorExitNodes\ExitNodeList`Immutable collection of `ExitNode` values### Download

[](#download)

```
use Binarylogic\TorExitNodes\Downloader\ExitNodeDownloader;
use Binarylogic\TorExitNodes\Http\GuzzleHttpClient;

$downloader = new ExitNodeDownloader(new GuzzleHttpClient());

$exitNodes = $downloader->downloadExitNodes();

echo count($exitNodes);
echo implode(PHP_EOL, $exitNodes->allIpAddresses());
```

Nothing is written to disk. The result is an in-memory `ExitNodeList`.

To keep the raw Onionoo response instead of a parsed list:

```
$json = $downloader->fetchRawList();
```

The endpoint, HTTP client and parser are all injectable:

```
$downloader = new ExitNodeDownloader(
    httpClient: new GuzzleHttpClient(timeoutSeconds: 10.0, userAgent: 'my-app/1.0'),
    endpoint: 'https://onionoo.torproject.org/details?type=relay&running=true&flag=Exit&fields=or_addresses,exit_addresses',
);
```

To use an HTTP client other than Guzzle, implement `HttpClient`:

```
use Binarylogic\TorExitNodes\Http\HttpClient;

final class MyHttpClient implements HttpClient
{
    public function fetchBody(string $url): string
    {
        // Return the response body, or throw a DownloadFailedException.
    }
}

$downloader = new ExitNodeDownloader(new MyHttpClient());
```

### Save

[](#save)

```
use Binarylogic\TorExitNodes\Storage\ExitNodeFileWriter;

$writer = new ExitNodeFileWriter(__DIR__.'/storage/tor-exit-nodes.json');

$writer->saveExitNodes($exitNodes);
```

The writer accepts any `ExitNodeList`, whether it came from a download, a file, or was built by hand.

### Load

[](#load)

```
use Binarylogic\TorExitNodes\Storage\ExitNodeFileReader;

$reader = new ExitNodeFileReader(__DIR__.'/storage/tor-exit-nodes.json');

$exitNodes = $reader->loadExitNodes();
```

### Check

[](#check)

```
use Binarylogic\TorExitNodes\Checker\ExitNodeChecker;

$checker = new ExitNodeChecker($exitNodes);

if ($checker->isExitNode('185.220.101.1')) {
    // The request came through the Tor network.
}
```

`ExitNodeChecker` never touches the network or the filesystem. It works with a list from any origin:

```
use Binarylogic\TorExitNodes\ExitNodeList;

$checker = new ExitNodeChecker(ExitNodeList::fromIpAddresses(['185.220.101.1', '2001:db8::1']));
```

IPv4 and IPv6 addresses are both supported. Addresses are compared in canonical form, so `2001:DB8::1` and `2001:db8:0:0:0:0:0:1` are treated as the same node.

### Parse an already retrieved response

[](#parse-an-already-retrieved-response)

```
use Binarylogic\TorExitNodes\Parser\OnionooExitNodeParser;

$exitNodes = (new OnionooExitNodeParser())->parseExitNodeList($json);
```

Stored file format
------------------

[](#stored-file-format)

```
{"exit_nodes":["185.220.101.1","2001:db8::1"]}
```

Exceptions
----------

[](#exceptions)

Every exception thrown by the package implements `Binarylogic\TorExitNodes\Exception\TorExitNodeException`, so all of them can be caught at once.

ExceptionThrown when`DownloadFailedException`The list could not be retrieved over HTTP`MalformedExitNodeListException`A JSON payload is not a valid exit node list`SaveFailedException`The list could not be written to disk`LoadFailedException`The list could not be read from disk`InvalidIpAddressException`An IP address given to the package is not valid```
use Binarylogic\TorExitNodes\Exception\TorExitNodeException;

try {
    $exitNodes = $downloader->downloadExitNodes();
} catch (TorExitNodeException $exception) {
    // Every failure mode of the package lands here.
}
```

License
-------

[](#license)

MIT

###  Health Score

9

—

LowBetter than 0% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/6fb6f8420eff117fab8f88ef0b3a761e420129d71bd03c2811a926f7fde117c1?d=identicon)[binarylogic](/maintainers/binarylogic)

### Embed Badge

![Health badge](/badges/binarylogic-tor-exit-nodes/health.svg)

```
[![Health](https://phpackages.com/badges/binarylogic-tor-exit-nodes/health.svg)](https://phpackages.com/packages/binarylogic-tor-exit-nodes)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
