Evomi

Blog / Proxy Fundamentals

How Proxy Traffic Gets Detected: The Network-Layer Signals

The ScraperThe Scraper8 min read
How Proxy Traffic Gets Detected: The Network-Layer Signals

Your exit IP geolocates to Chicago. Three IP-intelligence vendors all call it a residential connection on a consumer ISP's ASN, and it has no listing anywhere you can find. The site still hands you an interstitial on the second request, while the same page loads instantly in the browser on your laptop.

Nothing in a database explains that, because the decision did not come from a database. It came from the connection.

A commercial database answers "what is known about this address", the MaxMind and IPinfo post covers where those records come from and why they disagree. This post is the other half: what a server measures about a TCP connection while it is happening, without looking anything up. Those measurements are cheaper, fresher and much harder to argue with.

Latency Is the Signal You Cannot Argue With

The strongest single network-layer tell is round-trip time that contradicts the claimed geography, and it is strong because it is physics rather than heuristics.

Light travels through fibre at roughly two-thirds of its vacuum speed, about 200,000 km/s. That puts a hard floor on round-trip time of roughly 1 ms per 100 km of path, before routing, switching and queuing add another 30-100% on top. New York to London (about 5,600 km) cannot beat about 56 ms and in practice runs 70-80 ms. An address claiming Chicago that takes 160 ms to reach a Chicago edge node is claiming something the universe does not allow.

The server needs no cooperation to measure this: the TCP handshake alone yields an RTT sample before a byte of application data moves.

What makes it decisive at CDN scale is anycast. A large CDN routes you to a nearby point of presence, so it knows both which PoP answered and how far away you behaved. A Chicago connection should land on a Chicago-area PoP in the low tens of milliseconds. Landing on Frankfurt at 30 ms while your IP says Chicago says the traffic entered the network in Europe. Chaining hops does not hide this, every hop adds real distance.


The TCP/IP Stack Fingerprint

Before TLS, before HTTP, the SYN packet already carries implementation choices that differ between stacks. This is passive OS fingerprinting, the technique p0f popularised, and it costs the observer nothing.

TCP/IP stack fingerprint


The interesting part is not the fingerprint itself, it is the disagreement. A forward proxy terminates TCP and opens a fresh connection to the target, so the target sees the exit's kernel, not yours. If that kernel fingerprints as a Linux server while the User-Agent above it claims Windows Chrome, the two layers describe different machines. The same coherence reasoning at higher layers is covered in the JA3/JA4 TLS fingerprinting and HTTP/2 fingerprinting posts; TCP is where it starts.

MTU and Tunnel Signatures

MSS gets its own section because it leaks the shape of the path. A plain Ethernet path has a 1500-byte MTU and advertises MSS 1460. Encapsulation eats into that, and the common values are well known: PPPoE leaves 1492 (MSS 1452), GRE leaves 1476, WireGuard commonly runs 1420 (MSS 1380), and IPsec lands in the 1400s depending on mode and ciphers.

An MSS of 1380 is therefore a hint that traffic traversed a tunnel, and no more than a hint. Consumer DSL, mobile networks, satellite links and plenty of corporate networks clamp MSS legitimately, often at an intermediate router rather than the endpoint.

Active Checks and Their Limits

Everything above is passive. Some detection is not: an operator can connect back to the address that just connected to them and see what is listening, historically the ports associated with open proxies and VPN endpoints, such as 3128, 8080, 1080, 1194, 500/4500 and 51820.

In practice it is a weak and awkward tool.

  • It is slow. No scan fits inside a request budget, so it runs offline and feeds a list that is stale by the time it is read.
  • It usually finds nothing. A residential exit behind a home router with no port forwarding has nothing reachable, and neither does a well-configured cloud host.
  • It answers the wrong question. Under CGNAT the scanned address is shared by thousands of subscribers, so whatever answers is not the client in question.
  • It carries legal and policy weight. Unsolicited scanning is contested in several jurisdictions and violates most cloud providers' acceptable-use policies.

Which is why active probing belongs to specialist IP-intelligence vendors building lists at their own pace, not to an origin in the request path.

Where Resolution Happened, and Whether Anything Agrees

Two more signals sit at the boundary between network and application.

Resolver locality. A domain's authoritative DNS server sees which recursive resolver asked and roughly where it sits. A connection arriving from Chicago whose lookup came from a resolver in São Paulo has name resolution and traffic egress in different hemispheres, an ordinary consequence of a partially configured proxy, covered in the DNS leaks post.

Timezone and locale. Intl.DateTimeFormat().resolvedOptions().timeZonenavigator.languages and Accept-Language all state something about where a user lives. When those say Europe/Warsaw while the address geolocates to Ohio, nothing is proven, but the fields no longer corroborate each other. Detection is largely the business of counting how many independent things agree.

ASN and rDNS Shape

Networks look different from outside, and the differences are structural rather than reputational.

Hosting and transit ASNs announce large, contiguous, stable blocks. Consumer ISPs announce space with dynamic assignment patterns and characteristic reverse-DNS naming, cpe-dsl-dynamic-, a customer identifier, a regional POP name. Cloud providers stamp their own: ec2-…compute.amazonaws.com is not subtle. A residential address with no PTR at all, or a PTR that reads like a rack label, is an inconsistency. None of this needs a commercial dataset — registry data, routing tables and a reverse lookup are public.

Concurrency and Fan-Out

This is the signal you can never see about yourself, and arguably the most powerful one here.

A household address produces a handful of concurrent connections, from a few distinct stacks, to a modest set of destinations. An address opening sixty simultaneous sessions, presenting several TLS fingerprints in the same minute, or authenticating as a dozen unrelated accounts is describing something other than a household.

Extend that across a CDN's footprint and it becomes fan-out: how many different protected sites did this address touch in the last hour? A person browses a few. A shared exit touches hundreds of unrelated ones, invisible from the address itself, completely legible to anyone sitting in front of a large share of the web.

Inspecting Your Own Connection

You cannot see what a server sees of your exit, that asymmetry is the point. But you can see two useful halves. Cloudflare's /cdn-cgi/trace endpoint reports the address it observed, the country it assigned, and a colo field naming the PoP that answered; a local timing loop gives your own RTT.


Python
import socket
import statistics
import time
import httpx

TRACE = "https://www.cloudflare.com/cdn-cgi/trace"
PROXY = None  # or "http://user:pass@gateway.example.com:1000"

def trace(proxy=None):
    with httpx.Client(proxy=proxy, timeout=15) as c:
        body = c.get(TRACE).text
    return dict(line.split("=", 1) for line in body.strip().splitlines())

def tcp_rtt_ms(host, port=443, samples=7):
    seen = []
    for _ in range(samples):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        t0 = time.perf_counter()
        s.connect((host, port))
        seen.append((time.perf_counter() - t0) * 1000)
        s.close()
    return min(seen), statistics.median(seen)

direct = trace()
print(f"direct : ip={direct['ip']} country={direct['loc']} pop={direct['colo']}")

if PROXY:
    via = trace(PROXY)
    print(f"proxied: ip={via['ip']} country={via['loc']} pop={via['colo']}")

lo, med = tcp_rtt_ms("www.cloudflare.com")
print(f"local anycast RTT: min {lo:.1f} ms / median {med:.1f} ms")


Two things to read. The colo code is the network's opinion of where you are, independent of any database, if your exit claims one continent and the answering PoP is on another, that gap is the signal described above. And the RTT floor: divide your straight-line distance to that PoP's city by 100 for the millisecond minimum. Anything close to it means the path is honest.

For a rough view of the second leg, time a request through the proxy and subtract your RTT to the gateway. It overstates, the through-proxy figure includes a TLS handshake and an HTTP round trip, but the order of magnitude is informative.

Every Signal Has a Legitimate Population

None of this produces a verdict alone, because every signal above flags large groups of ordinary users.

  • CGNAT. Thousands of mobile subscribers behind one address, producing exactly the concurrency and fan-out profile described above, the CGNAT post covers what that does to reputation.
  • Corporate VPN and SASE. Employees egress through a security vendor's cloud: hosting ASN, tunnel-sized MSS, a server-shaped TCP stack, and a location unrelated to the person.
  • Satellite. Geostationary links carry a ~500-600 ms RTT floor set by orbital distance. Every latency heuristic reads that as impossible. It is physics from further away.
  • Mobile carriers. Traffic can egress hundreds of kilometres from the handset, middleboxes rewrite headers, and MTU is rarely 1500.
  • Privacy-conscious users. DNS-over-HTTPS moves resolution off the network path by design, and a browser that normalises timezone and language is breaking coherence deliberately.

Now the arithmetic, illustration, not measurement. Say you have eight independent network signals, each flagging 2% of your legitimate users. Combined with OR they flag 1 - 0.98^8 ≈ 15% of real traffic. Score them with a threshold instead, requiring three or four to agree, and the false-positive rate drops by orders of magnitude while clients that trip all eight are caught just the same. That is why mature systems score rather than block, and why a boolean "is this a proxy" answer is selling you the 15%.

What People Get Wrong

  • "A clean database record means a clean connection." Separate evidence. An address with a perfect reputation can still produce an RTT that contradicts its own geolocation.
  • "Detection happens at the application layer." The first RTT sample and the SYN fingerprint exist before your first HTTP header does.
  • "Slow means detected." Latency is a symptom of distance far more often than of scrutiny. Measure before concluding.
  • "Residential means undetectable." Residential describes the address's registration, not where the connection originated or how many sessions share it.
  • "One weird value gets you blocked." Sensible systems weight and combine, which is also why chasing individual signals is unproductive.
  • "This is diagnosis you can act on." Mostly it is not. If your traffic legitimately crosses an ocean, the RTT will say so, because it did.

Wrapping Up

Proxy detection at the network layer is mostly arithmetic on things a server measures for free. RTT against the speed of light is the signal with no counter-argument, and chaining hops only makes it worse. The TCP stack fingerprint, MSS, resolver locality, timezone and rDNS shape are coherence checks: individually weak, collectively meaningful, and interesting mainly when they disagree with each other. Concurrency and fan-out are invisible from your side and highly visible from a CDN's. And every one of them misjudges satellite users, corporate VPN users, mobile subscribers and anyone behind CGNAT, which is exactly why they feed a score instead of deciding anything. If you are being misjudged, measure your real RTT and PoP before assuming a reputation problem. And if a site has decided it does not want automated traffic, the network layer is not where that gets resolved, the API, an access request and an honest crawler identity are.