DEV Community

Cover image for Understanding IP Reputation: What Actually Gets a Proxy Flagged
Nick
Nick

Posted on

Understanding IP Reputation: What Actually Gets a Proxy Flagged

Two clients, same residential IP, same User-Agent header. This is what the server actually sees:

Chrome 58        1:65536;3:1000;4:6291456|15663105|0
Go-http-client   2:0;4:4194304;6:10485760|1073741824|0
Enter fullscreen mode Exit fullscreen mode

Those are HTTP/2 fingerprints in the notation Akamai proposed at Black Hat EU 2017: SETTINGS parameters, WINDOW_UPDATE increment, PRIORITY frames. Your library picks all of it before your code touches the request.

One of those gets a 200. The other gets a challenge page. The address is identical in both cases, so whatever is happening, the address is not the variable.

The score is not computed from the address

There is no registry where an IP's reputation lives. There are commercial scoring APIs, and their output is what people mean by "clean" and "burned." IPQualityScore documents theirs:

fraud_score      "The overall fraud score of the user based on the IP,
                  user agent, language, and any other optionally passed
                  variables."
recent_abuse     verified abuse across their network, window given only
                  as "the past few days"
abuse_velocity   high | medium | low | none
connection_type  Residential | Corporate | Education | Mobile | Data Center
Enter fullscreen mode Exit fullscreen mode

The vendor's own definition of an IP reputation score takes your User-Agent and your Accept-Language as inputs. It was never a measurement of the address alone and never claimed to be.

IPQS suggests 75 as suspicious and 90 as high risk. Every site using it picks its own threshold anyway, so a block is a private cutoff applied to a proprietary score built partly from headers you chose. Three separate decisions, one of which is yours.

The clearest case is one where nothing happened

Spamhaus runs the XBL for addresses observed doing something, and the PBL for something else entirely:

a dataset containing end-user IP address ranges from which email should never be sent directly to the final destination

Networks add and maintain many of those ranges themselves. Your home broadband address is very likely listed right now, submitted by your own ISP, with no incident behind it and nothing to clear. Category membership is the whole listing.

Proxy classification works the same way more often than people assume. An address is flagged for belonging to a range, and the range was classified months before your traffic existed.

The HTTP/2 layer

Implementations disagree about which SETTINGS parameters to send, in what order, with what values, about WINDOW_UPDATE increments, and about whether to send PRIORITY frames for streams that do not exist yet. Almost none of it is configurable.

Chrome 58        1:65536;3:1000;4:6291456|15663105|0
Edge 14          3:1024;4:10485760|10420225|0
Go-http-client   2:0;4:4194304;6:10485760|1073741824|0
curl 7.54        3:100;4:1073741824;2:0|1073676289|0
Enter fullscreen mode Exit fullscreen mode

Pseudo-header order splits the browsers on its own:

Chrome    :method :authority :scheme :path
Firefox   :method :path :authority :scheme
Safari    :method :scheme :path :authority
Enter fullscreen mode Exit fullscreen mode

A Go program claiming to be Chrome sends 2:0;4:4194304;6:10485760. The header is a string you set. The SETTINGS frame is your library's opinion, transmitted before request headers exist.

The TLS layer

JA3 hashed the Client Hello into one opaque value, which meant flipping a single cipher produced an entirely different hash. JA4 made it modular instead:

JA4 = a _ b _ c

a   TLS version, client library, connection type
b   ciphers
c   extensions and ALPN
Enter fullscreen mode Exit fullscreen mode

FoxIO's writeup gives the case that matters. A GreyNoise-tracked actor rotated one cipher at a time and generated a flood of distinct JA3 hashes. Under JA4 only b moved. The a and c segments held, and the actor stayed trackable on a partial fingerprint.

Detection does not need your whole fingerprint to match something known. It needs one stable substring.

Check what you are presenting

# pip install "httpx[http2]"
import httpx

PROXY = "http://user:pass@gateway.example.net:8000"

with httpx.Client(proxy=PROXY, http2=True, timeout=30) as client:
    data = client.get("https://tls.peet.ws/api/all").json()

print("Exit IP:  ", data["ip"])
print("JA4:      ", data["tls"]["ja4"])

# the http2 object is only present if the connection negotiated h2
h2 = data.get("http2")
print("HTTP/2 fp:", h2["akamai_fingerprint"] if h2 else "fell back to HTTP/1.1")
Enter fullscreen mode Exit fullscreen mode

If that prints the fallback, stop there. A client advertising Chrome while negotiating HTTP/1.1 against a host offering h2 is not a subtle inconsistency.

Otherwise, run the same request through curl_cffi with impersonate="chrome124" and diff the two. Same proxy, same exit IP, two unrelated identities on the wire. If the site you are fighting treats them differently, the pool was never the problem.

Agreement across layers is the actual signal

The most useful line in the Akamai paper is nearly an aside. A TCP fingerprint indicating Windows alongside an HTTP/2 fingerprint indicating Chrome on macOS implies an intermediary, because those parameters are not independently tunable in most implementations.

JA4+ is built on the same premise, publishing separate fingerprints for TLS, HTTP behavior, and network latency and TTL so a defender can correlate them and notice disagreement.

Nobody is hunting for a bad IP. They are looking for a stack that fails to agree with itself. Residential address in Ohio, Python TLS build, Go HTTP/2 frames, Chrome/131 in the headers, TTL suggesting a datacenter three hops out. That contradiction survives any improvement to the address.

What the address still decides

ASN classification is real and unspoofable. If a site drops known hosting ranges, a flawless Chrome fingerprint out of AWS still gets nothing. Subnet neighbors matter, since scoring works at range granularity and a bad /24 drags everything in it down. Concurrency per address is its own tell, because eight simultaneous sessions from one residential line is not a shape residential traffic takes.

The address sets a floor and the client stack sets the ceiling. Most teams spend on the floor, leave the ceiling at library defaults, and conclude the proxies are bad.

Where to start

Fingerprint yourself. It costs one request and it is frequently the entire answer. If JA4 says Python and the HTTP/2 frames say Go while your headers say Chrome, fix that before touching anything else.

Pacing and concurrency per address come second, and that is the one most often misdiagnosed as a reputation problem.

Pool quality comes third. When you do get there, pool size is the wrong question. Ask how addresses are acquired and expect a specific answer about consent rather than the phrase "ethically sourced," ask what the ASN spread looks like, and ask whether sessions hold long enough not to migrate mid-flow, since a migrating session manufactures exactly the cross-layer contradiction described above. Providers built on residential and mobile pools, 2Extract among them, can answer those directly, and deflection on sourcing is itself information. None of them can fix your client stack.

"IP reputation" is a comfortable phrase because it locates the problem outside your code. Usually it is inside it.

Top comments (0)