Skip to content
nsin docs

Clients whose traffic resembles a proxy tunnel

GET
/analytics/tunnel-suspects
curl --request GET \
--url 'https://api.nsin.ir/analytics/tunnel-suspects?period=1h' \
--header 'Authorization: Bearer <token>'

Clients whose WebSocket/gRPC traffic looks like a VPN or proxy tunnel run behind the CDN: sustained volume over a single fixed path, with opaque payloads and no sign of ordinary browsing (no real assets fetched, no referer).

This is a heuristic for investigation, not proof of abuse. balance is informational — tunnels used for browsing are download-heavy, so symmetry is not a criterion.

period
string
default: 24h
Allowed values: 1h 3h 6h 12h 24h 48h 7d 30d

Time window, ending now. Time series are bucketed by minute for 1h, by hour up to 48h, and by day for 7d and 30d. Buckets are cut on Asia/Tehran (UTC+03:30) boundaries and emitted as UTC instants, so an hourly bucket starts at :30 UTC. An unrecognised value falls back to 24h.

Suspected tunnel clients.

Media type application/json
object
suspects
Array<object>
object
remote_addr
string
network

The client’s network operator (AS organisation).

string
country
string
hostname
string
transport

Detected transport. httpupgrade is Upgrade: websocket without a real WebSocket handshake.

string
Allowed values: httpupgrade ws grpc xhttp http
reqs

All requests from this client to this host.

integer
tunnel_reqs

Requests with opaque payloads.

integer
tunnel_paths

Distinct paths used — around 1 for a tunnel.

integer
up

Client-to-edge bytes.

integer
down

Edge-to-client bytes.

integer
balance

min/max of up and down. Informational only.

number
max_secs

Longest single connection, in seconds.

integer
sample_path

The heaviest single path.

string
ja4

TLS fingerprint.

string
ja4h

HTTP fingerprint.

string
ua

The client’s User-Agent.

string
score

Additive tunnel score from the matched signals; higher is more tunnel-like.

integer
severity

The score’s band. Empty below the lowest band.

string
Allowed values: high medium low ""
signals

The signals that contributed to score.

Array<object>
object
name

Signal name.

string
value

The measured value.

number
points

Points it added to the score.

integer
detail

Human-readable explanation, when there is one.

string
path_uniqueness

Distinct URIs per request. High is a tunnel signal — some tunnel software puts a random token in every URI.

number
cache_hit_ratio

The client’s cache hit ratio, 0–1. A tunnel is uncacheable, so this is about 0.

number
bytes_per_req

Average bytes per request.

number
ja4_cluster

How many distinct client IPs in this result share this client’s TLS fingerprint on the same host.

integer
httpupgrade_byte_share

Share of this client’s bytes carried over the httpupgrade transport, 0–1.

number
as_of

When the ranking was computed. It is served from a precomputed store, so a 7d/30d answer can be up to an hour old.

string format: date-time
refreshing

Present (true) while a newer ranking is being computed behind this one.

boolean
Example
{
"suspects": [
{
"transport": "httpupgrade",
"severity": "high"
}
]
}

Missing, malformed, revoked or expired API key. (A valid key whose account has been deactivated gets 403 account is inactive instead.)

Media type application/json

The single error shape used by every endpoint.

object
error
required

Human-readable description of what went wrong.

string
Examples
Example invalidKey
{
"error": "invalid API key"
}

The key exceeded its request budget (300 requests per minute by default).

Media type application/json

The single error shape used by every endpoint.

object
error
required

Human-readable description of what went wrong.

string
Examples
Example limited
{
"error": "rate limit exceeded"
}

Either the analytics backend is temporarily unreachable (retry; no data is lost), or the answer is still being computed. The per-domain sections and the cached account-wide views (domains-overview, global-summary, global-summary/peak, requests-compare, bandwidth-overview, nodes-overview) wait at most about 25 seconds for their query; past that they answer 503 with pending: true and a Retry-After header, while the query keeps running and caches its result, so the retry is served from that cache instead of starting the same scan again. Identical concurrent requests share one computation.

Media type application/json
object
error
required

Human-readable description of what went wrong.

string
pending

true when the query is still running rather than failing. Retry after Retry-After seconds.

boolean
Examples
{
"error": "analytics unavailable"
}
Retry-After
integer
Example
5

Seconds to wait before retrying. Sent only with pending.