# Detect VPN, proxy and Tor traffic from an IP | host.sh

> Spot VPN, proxy, Tor, relay and data center traffic from an IP address: what each flag means, where it misfires, and code that treats it as a risk signal.

Some addresses belong to networks that hide or stand in for the person behind them: VPN servers, open proxies, Tor exits, privacy relays and cloud hosts. Knowing that helps you weigh a sign-up, a payment or a burst of requests, and tells you when the IP's location says little about the visitor. This guide explains the flags of a host.sh lookup, the everyday cases where they are wrong, and how to use them to add a check rather than to shut people out. The JavaScript, Python and PHP examples set a timeout, check every value, and fall back to "unknown" when anything fails.

## The fields

A [lookup](https://www.host.sh/docs/lookup/index.md) has two layers. `connection.anonymity` holds a score per list, and `connection.security` sums them up as yes or no:

```json
{"connection":{"security":{"is_vpn":false,"is_proxy":false,"is_tor":false,"is_hosting":true,"is_relay":false,"threat":"medium"}}}
```

| Field | True when |
|---|---|
| `connection.security.is_vpn` | `anonymity.vpn.score` is above 0.5: a server of a commercial VPN service or a range known to be VPN exits |
| `connection.security.is_proxy` | `anonymity.proxy.score` is 0.75 or more: a listed public open proxy |
| `connection.security.is_tor` | `anonymity.tor.score` is above 0: a Tor exit node |
| `connection.security.is_hosting` | `anonymity.dch.score` is above 0: a hosting or data center range (cloud, VPS, dedicated servers) |
| `connection.security.is_relay` | `anonymity.relay.score` is above 0: a relay that keeps the user's rough location, such as iCloud Private Relay |
| `connection.security.threat` | `high` for Tor or a proxy, `medium` for a VPN or a hosting range, otherwise `low` |

Each `score` runs from 0 to 1 in steps of 1/16. 0 means the address is not on that list; a higher score means more, or more certain, evidence, and it is not a probability. The [response fields](https://www.host.sh/docs/fields/index.md#connection-security-a-yes-no-summary) page has every score, including the provider `name` and `url` of a hosting range. `low` means "not on these lists", which is not proof that a visitor is who they say.

For the visitor's own address a lookup needs no token, so a page can ask from the browser; the [VPN check](https://www.host.sh/vpn-check/index.md) tool shows the same flags for your own connection. To check an address on your server, such as the address of a sign-up, you need a [token](https://www.host.sh/docs/index.md#authentication).

## What each flag means, and where it misfires

- **VPN.** Many commercial VPN services publish their server addresses, and other ranges are known VPN exits. The thresholds are conservative on purpose: [Cloudflare WARP](https://developers.cloudflare.com/warp-client/) scores exactly 0.5, because the same network carries ordinary users of a privacy app and companies' traffic, so it is not `is_vpn`. A company's own VPN or a cloud-hosted company gateway often shows as hosting, or as nothing at all.
- **Relay.** [iCloud Private Relay](https://developer.apple.com/support/prepare-your-network-for-icloud-private-relay/) sends Safari browsing through relays whose exit ranges Apple publishes. These are ordinary users who turned on a privacy feature, and the relay keeps their rough location, so `is_relay` never raises `threat`. The address is the relay's, though, not the user's.
- **Proxy.** The open proxy lists are tested about every hour. Many open proxies are compromised home or mobile devices, so the same address can also carry the device owner's normal traffic; treat a hit as a signal, not proof.
- **Tor.** The Tor Project publishes the [list of exit addresses](https://check.torproject.org/torbulkexitlist), so Tor exits are known with confidence. The people behind them include those avoiding tracking or censorship as well as abusers; the Tor Project's [abuse FAQ](https://support.torproject.org/abuse/) discusses what blocking costs.
- **Hosting.** Traffic from a data center is often a script, a scraper or a server, but not always: cloud desktops, company proxies, AI assistants fetching a page, and browsers' cloud features all come from hosting ranges. Coverage of IPv6 hosting space is lower than of IPv4.
- **Shared addresses.** Mobile carriers and many home providers put many customers behind one public address with carrier-grade NAT ([RFC 6888](https://www.rfc-editor.org/rfc/rfc6888)), often with the shared address space of [RFC 6598](https://www.rfc-editor.org/rfc/rfc6598) inside their network. One infected phone can get a shared address onto a proxy list, and everyone behind it then looks the same.

So every flag has false positives that are ordinary people, and a missing flag proves nothing: a new VPN server or a residential proxy can show nothing at all.

## Use it as a risk signal, not a ban

- **Step up, don't shut out.** Ask for one more check when the score is high: an email or SMS code, a CAPTCHA, or 3-D Secure on a payment. Most real users pass it and carry on.
- **Rate limits.** Give flagged addresses a lower request or sign-up rate rather than none, and keep the limit per account where you can, since an address can be shared.
- **Review queues.** Send a high-risk order or sign-up to a person instead of declining it, and log why it was flagged.
- **Combine signals.** An IP flag is one input next to account age, payment checks and behaviour. A new account on Tor buying gift cards is different from a five-year customer on Private Relay.
- **Tell people what to do.** If a check blocks someone, say so and offer a way through, such as turning the VPN off for one step or contacting support.
- **Mind fairness.** Hard blocks fall hardest on people who use privacy tools for good reasons and on whole mobile networks behind shared addresses. Review your false positives now and then.

## In JavaScript, in the browser

Your visitor's own address, with no token, from the page (the API allows cross-origin requests). Useful for a hint such as "you seem to be on a VPN, so prices may be shown in another currency":

```js
const UNKNOWN = { threat: 'unknown', vpn: false, proxy: false, tor: false, hosting: false, relay: false };
const THREATS = ['low', 'medium', 'high'];

async function connectionRisk() {
  try {
    const res = await fetch('https://api.host.sh/v2/lookup/?fields=connection.security', { signal: AbortSignal.timeout(3000) });
    if (!res.ok) return UNKNOWN;
    const s = (await res.json())?.connection?.security;
    const flags = ['is_vpn', 'is_proxy', 'is_tor', 'is_hosting', 'is_relay'];
    if (!s || !THREATS.includes(s.threat) || flags.some((f) => typeof s[f] !== 'boolean')) return UNKNOWN;
    return { threat: s.threat, vpn: s.is_vpn, proxy: s.is_proxy, tor: s.is_tor, hosting: s.is_hosting, relay: s.is_relay };
  } catch {
    return UNKNOWN; // network error, timeout or a body that isn't JSON
  }
}

console.log(await connectionRisk());
```

"unknown" is the safe default here: when the lookup fails, show nothing and don't add friction. Tokenless lookups are limited to 10 a minute per address ([rate limits](https://www.host.sh/docs/rate-limits/index.md)), and people behind one shared address share that limit; over it the API answers `429` and the code falls back.

## On your server, in Python

On the server you check the address of the request, which needs a token. Take the client address as the [client IP guide](https://www.host.sh/guides/client-ip-behind-proxy/index.md) explains when you run behind a proxy or a CDN. The function returns a level for your own rules to act on; only the standard library:

```python
import json
import urllib.parse
import urllib.request


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args):
        return None  # a redirect would take the token along: fail instead


OPENER = urllib.request.build_opener(NoRedirect)

THREATS = ("low", "medium", "high")
FLAGS = ("is_vpn", "is_proxy", "is_tor", "is_hosting", "is_relay")


def ip_risk(ip, token):
    """low, medium or high from the address's lists, or unknown when the lookup fails."""
    try:
        query = urllib.parse.urlencode({"ip": ip, "fields": "connection.security"})
        req = urllib.request.Request("https://api.host.sh/v2/lookup/?" + query, headers={"Authorization": "Bearer " + token})
        with OPENER.open(req, timeout=3) as res:  # an HTTP error or a redirect raises
            security = json.load(res)["connection"]["security"]
    except Exception:
        return "unknown"
    if not isinstance(security, dict) or security.get("threat") not in THREATS:
        return "unknown"
    if not all(isinstance(security.get(flag), bool) for flag in FLAGS):
        return "unknown"
    return security["threat"]


# your rules: unknown and low go through, medium gets a code by email, high goes to review
print(ip_risk("8.8.8.8", "YOUR_TOKEN"))  # the request's client address in a real app
```

## On your server, in PHP

```php
<?php
const THREATS = ['low', 'medium', 'high'];
const FLAGS = ['is_vpn', 'is_proxy', 'is_tor', 'is_hosting', 'is_relay'];

// low, medium or high from the address's lists, or unknown when the lookup fails
function ip_risk(string $ip, string $token): string
{
    $url = 'https://api.host.sh/v2/lookup/?' . http_build_query(['ip' => $ip, 'fields' => 'connection.security']);
    $ctx = stream_context_create(['http' => ['timeout' => 3, 'header' => 'Authorization: Bearer ' . $token, 'follow_location' => 0]]);
    $fp = @fopen($url, 'r', false, $ctx); // false on a network or HTTP error
    if ($fp === false) {
        return 'unknown';
    }
    $body = stream_get_contents($fp);
    $meta = stream_get_meta_data($fp);
    fclose($fp);
    // only a 200 (redirects aren't followed: they would take the token along), and a timeout or a
    // connection that closed early leaves a short body
    $headers = $meta['wrapper_data'] ?? [];
    if (!preg_match('#^HTTP/\S+ 200\b#', $headers[0] ?? '')) {
        return 'unknown';
    }
    $length = null;
    foreach ($headers as $header) {
        if (preg_match('/^Content-Length:\s*(\d+)\s*\z/i', $header, $m)) {
            $length = (int) $m[1];
        }
    }
    if ($body === false || $meta['timed_out'] || ($length !== null && strlen($body) !== $length)) {
        return 'unknown';
    }
    $data = json_decode($body, true);
    $security = is_array($data) && is_array($data['connection'] ?? null) ? ($data['connection']['security'] ?? null) : null;
    if (!is_array($security) || !in_array($security['threat'] ?? null, THREATS, true)) {
        return 'unknown';
    }
    foreach (FLAGS as $flag) {
        if (!is_bool($security[$flag] ?? null)) {
            return 'unknown';
        }
    }
    return $security['threat'];
}

// REMOTE_ADDR is the client in a web request (behind a proxy, see the client IP guide)
echo ip_risk($_SERVER['REMOTE_ADDR'] ?? '8.8.8.8', 'YOUR_TOKEN'), "\n";
```

The open ports of an address are another clue about what it is; the [port check](https://www.host.sh/ports.md) shows them for your own connection.

## FAQ

### How do I detect if an IP address is a VPN?

Look the address up and read `connection.security.is_vpn`, which is true when the address is a known server of a commercial VPN service. Cloudflare WARP scores exactly 0.5 and is not counted, and iCloud Private Relay shows as `is_relay` instead ([response fields](https://www.host.sh/docs/fields/index.md)).

### Should I block VPN and Tor users?

Usually not outright. Many are ordinary people protecting their privacy, and shared addresses put whole mobile networks at risk of false positives. Ask for one more check, rate limit or review instead; the Tor Project's [abuse FAQ](https://support.torproject.org/abuse/) explains the cost of banning Tor.

### What does threat high, medium or low mean?

`high` means a Tor exit or a listed proxy, `medium` a VPN or a hosting range, and `low` that the address is on none of these lists. A relay such as iCloud Private Relay doesn't change it, and `low` is not proof that a visitor is safe ([response fields](https://www.host.sh/docs/fields/index.md#connection-security-a-yes-no-summary)).

### Why is my home connection flagged as a proxy?

Open proxy lists include compromised home and mobile devices, and carriers share one public address among many customers with carrier-grade NAT ([RFC 6888](https://www.rfc-editor.org/rfc/rfc6888)). If one device behind your address was used as a proxy, the address can be listed for a while.

### Does a data center address mean a bot?

Not always. Scripts and scrapers do run in the cloud, but so do cloud desktops, company proxies and AI assistants fetching a page for someone. Treat `is_hosting` as a reason to look closer, together with the User-Agent and the account's history ([VPN check](https://www.host.sh/vpn-check/index.md)).
