Open-proxy measurement is supposed to be a solved problem. That’s the received wisdom, and honestly it’s the wisdom I started with: you take a list, you sweep :1080, you send a SOCKS5 greeting, and you write down which servers complete a no-authentication handshake. ZMap, Masscan, ProxyBroker, every academic survey I’ve read — they all hinge on that first TCP exchange. If the box asks you for a password, it’s closed. Next.

I want to tell you about the blind spot in that gate, because it swallowed a whole population of proxies and I only noticed by accident.

A SOCKS5 proxy can demand a password on TCP CONNECT and still relay UDP datagrams for absolutely anybody — no auth, no UDP ASSOCIATE, no check that the datagram even came from a client who logged in. To a handshake scanner that box looks locked. On the UDP side it’s a wide-open window around the side of the house.

I found one of these by fluke, took it apart packet by packet over a weekend, and then went looking for siblings. There are a lot of them — not a number I’m going to quote, but a whole population — sitting on Alibaba and Huawei cloud, on the default SOCKS port, invisible to every scanner that asks the front door for a password first.

This is the teardown. Standing rule for this blog: I publish signals, not weapons. Everything reflective below was measured and then deliberately defanged, and the reality is almost backwards from the scary story: these relays are lousy at the attack you’d worry about, but easy to spot. The debunk is the interesting part, so keep reading.

The lie the password tells

A correct SOCKS5 proxy treats the TCP control connection as the root of trust. You authenticate, you issue UDP ASSOCIATE, the proxy hands back a relay endpoint bound to your source, and that relay lives and dies with the TCP connection that spawned it. RFC 1928 spells all of this out; there’s nothing exotic about doing it right.

The broken build just… skips it on the UDP path. The password is real on TCP and theatre on UDP. I confirmed the bypass with the cheapest possible test — a single SOCKS5-wrapped UDP datagram fired straight at the proxy’s port, no TCP session, no auth, no ASSOCIATE, and see if the thing forwards:

sending 30B wrapped STUN to <relay>:<port> with NO tcp / NO auth / NO associate
REPLY in 28ms  ->  exit 178.x.x.x:36874
=> OPEN UDP RELAY (auth bypass)

It forwarded. One shot, 28ms, and there’s my reflected STUN answer coming back through a proxy that would have slammed the door on the exact same client over TCP.

clientSOCKS5 proxy:1080targetTCP CONNECTone UDP datagramno TCP · no auth · no ASSOCIATE✗ password required✓ forwards for anyonescanner checks TCP → "closed"
Same box, two doors. The scanner knocks on the locked one and writes the house down as empty.

That asymmetry is the load-bearing fact for the entire post, so let me say why it matters and not just that it’s rude. TCP can’t be spoofed — the handshake needs return packets to land on the real source, so a relay that insists on ASSOCIATE over a live TCP session is simply not reflectable. A stateless one that forwards a lone, unsolicited UDP datagram is. The whole story lives in that difference.

What the relay actually is, measured to death

Before I generalized a single thing, I characterized one open relay exhaustively. Pure stdlib tooling, STUN as the truth oracle, no third party touched harder than a public STUN server answering a single query.

It egresses from its own host. The STUN-reflexive exit IP came back identical to the proxy front and identical to the UDP relay endpoint. One box does ingress, relay, and egress — there is no exit isolation at all. Contrast that with a paid commercial residential proxy I tested for a baseline, where I measured three distinct IPs: the SOCKS front, the relay, and a bound exit that honored the customer’s IP selection. The broken build is one machine wearing one hat.

Its mapping is endpoint-independent — a cone. Inside a single association I probed five STUN servers across five distinct networks (Google, Cloudflare, Nextcloud, sipgate, metered) and got back the same egress port every time. Different concurrent associations each draw their own random egress port, and there’s no cross-association prediction to be had. Textbook cone NAT behavior, which matters for how you’d reason about the reflection surface.

Its egress filtering is restricted, not full-cone — and this one embarrassed me. My first pass read as full-cone, and I got briefly excited about it, and I was wrong. What I’d actually caught was a hairpin artifact: a packet that reached the relay’s egress port from the relay’s own IP — an internal loopback short-circuit — not from the outside world. When I sent a genuinely external, unsolicited datagram at that egress port, it got dropped. So inbound to a relayed port is filtered down to peers the relay has already spoken to. The lesson I’m passing on so you don’t burn the afternoon I did: measure your reflection surface from the outside, never through a hairpin, or the box will happily lie to you about how open it is.

It cannot fragment outbound, and this is the most consequential limit of the whole thing. I pushed wrapped payloads of increasing size through the relay to a server I control:

through relay:   1470 ok   1471 ok   1472 ok  |  1473 ✗   2000 ✗  ...  60000 ✗
direct control:  2000 ok   4000 ok   8000 ok         (my server reassembles fine)

That’s a razor cliff at payload 1472 — exactly one MTU (1472 + 8 UDP + 20 IP = 1500). The relay cheerfully reassembles fragmented inbound traffic, because the kernel does that before the relay ever reads the bytes. But the instant it has to emit a datagram larger than the link MTU, the packet is gone. Almost certainly DF is set on the egress socket — Linux default PMTUD — so sendto() returns EMSGSIZE and the thing is dropped without a sound. Anything over ~1472 bytes that needs to reach a victim simply vanishes. Keep that in mind — it matters later when I get to the debunk.

It drops empty payloads. A 0-byte inner payload is silently discarded; the minimum forwardable unit is one byte. Put the three quirks together — reassembles ingress, DF-drops anything past 1472 on egress, drops empty — and you’ve got a tidy behavioral fingerprint that needs no auth and no abuse to collect.

One sighting unrolls a whole block

The single relay I’d been poking wasn’t a loner; it belonged to a block, and the block followed a rule I could read off the wire. Given one seed IP:port, the next host’s port is the previous port plus two:

2.27.114.2 :46587      2.27.114.8  :46599
2.27.114.3 :46589      ...
2.27.114.4 :46591      2.27.114.14 :46611      <- block ends
2.27.114.5 :46593      2.27.114.15 :  (dead)

It’s just arithmetic: port = 46583 + 2 × last_octet, holding across a contiguous run that fills the usable addresses of a /28 (.2.14, skipping network, gateway, and broadcast). The +2 step is nothing clever — sparse allocation, one port per host. But the consequence is that a single open endpoint hands you its entire /28. From one sighting you reconstruct the block; a feather-light sweep maps the pool. File that under “why this is an enumeration story,” because it comes back around when we talk detection.

So can you actually weaponize it? Mostly, no.

Here’s the part I care about most, and it’s where I take the scary framing apart with a ruler. The fear writes itself: “open UDP relay equals DDoS reflector.” The reality I measured is a 1:1 forwarder with a header tax and a one-packet ceiling. It launders traffic. It does not multiply it. Let me show my work.

The header tax. Every datagram the relay forwards carries a 10-byte SOCKS5 UDP header, and it’s paid on both legs. Ten bytes you have to send can only ever lower an amplification ratio — you can’t conjure gain out of overhead you’re adding yourself. I measured this against real DNS and real NTP:

amplifierrequestresponseraw factorvia SOCKS5 relay
DNS TXT39 B960 B24.6×19.8×
NTP monlist8 B48,200 B (100 pkts)6025×2733×

The smaller the request, the more brutal the tax — NTP’s 8-byte monlist request paying an extra 10 bytes nearly halves the effective factor. That’s the opposite of what a reflector is supposed to do for you.

The MTU ceiling makes it worse still. Because the relay can’t fragment on egress, the deliverable response is capped at roughly 1462 bytes — one MTU minus the SOCKS5, UDP, and IP headers. Picture a 4 KB DNSKEY answer, exactly the high-amplification record an attacker would reach for: it arrives at the relay, gets faithfully reassembled, and then gets dropped on the way out. Over the cliff you don’t get a truncated packet, you get nothing. So the relay quietly enforces a “stay single-packet” rule, capping any DNS reflection through it at somewhere around 30×, single packet, or zero. The relay is sabotaging the attack it’s supposedly enabling.

And the relay is the bottleneck. Whatever amplified traffic exists has to leave through the relay’s own cloud uplink, at 1:1 packet rate. A real attacker just skips the relay and hits the amplifier directly — same bytes, no chokepoint. The only thing the relay actually buys anyone is anonymity, and even that’s a trade rather than a win, for reasons the fingerprint section is about to make embarrassing.

Building the reflection where nobody can get hurt

I wanted to watch the full reflection chain end to end without touching a single third party, so I built the whole thing inside an isolated Docker network10.99.0.0/24, no uplink, nothing leaves the bridge. Everything below happened on my laptop and reached exactly no one:

attacker ──spoof src=victim──> badproxy ──> dnsmasq (fat TXT) ──> badproxy ──> victim
10.99.0.10                     10.99.0.20    10.99.0.40                       10.99.0.30

Four containers: an attacker that forges the victim’s source IP with scapy, the deliberately-broken stateless relay, a real dnsmasq serving a fat TXT record as the amplifier, and a victim that just prints whatever lands on it. Here’s a run:

dnsmasq   query[TXT] amp.lab from 10.99.0.20      <- sees the PROXY, not attacker, not victim
badproxy  got 25B from 10.99.0.30:7777 (src NOT checked) -> forwarding to 10.99.0.40:53
badproxy  REFLECTED 499B reply -> 10.99.0.30:7777 (the spoofed source!)
victim    <- 509B from 10.99.0.20:1080            <- amplified answer lands on the victim

Two things fell out of this, and they’re both visibility stories rather than firepower stories.

The relay launders the spoof away from the amplifier. Look at what dnsmasq logged: a query from 10.99.0.20 — the proxy — a completely legitimate-looking lookup. Not the spoofed victim, not the attacker. In direct reflection the resolver would see the spoofed victim’s address and its anti-spoofing machinery could kick in — DNS cookies, response-rate limiting, TCP fallback. With the relay in front, the resolver sees a clean, ordinary client and none of those defenses ever fire. The spoofing only ever exists on the attacker→relay hop — and on that hop the real defense, BCP38 egress filtering, still catches it. In my lab the spoof “works” only because an isolated bridge has no BCP38 to enforce. On the actual internet, that gate is what these relays keep running into.

The victim gets handed a confession. What actually lands on the victim is SOCKS5-framed UDP — the payload prefixed with the relay’s reconstructed source header:

00 00 00 01 | 0a 63 00 28 | 00 35 | <real DNS response: amp.lab TXT ...>
└RSV+FRAG┘ATYP└10.99.0.40─┘└ 53 ─┘

The DNS server’s own address, 10.99.0.40:53, is sitting right there in the header of every reflected packet. A defender parsing a single datagram can say “this is laundered DNS reflection, original source port 53,” even though the outer IP is just some proxy. So yes, the attacker’s gain is real — a residential-or-cloud-looking source instead of 8.8.8.8, which slips past IP-reputation checks and naive “drop source-port 53” filters. But it’s paid for with a brand-new, self-incriminating payload signature and an enumerable pool of source IPs to boot. Some anonymity.

The one mechanism that genuinely multiplies — loops

Everything above is a poor multiplier of bytes. There is exactly one exception, and it’s the genuinely dangerous idea in this whole post, because it multiplies in time instead.

Here’s the trick. A SOCKS5 UDP response header (RSV RSV FRAG ATYP ADDR PORT) is byte-for-byte a valid request header. They’re the same shape. So when one open relay wraps a reply and delivers it to another open relay’s listen port, the second relay doesn’t read it as “a reply” — it reads it as a fresh forward request and acts on it. Point two of these relays at each other and you get a perpetual ping-pong. This is the application-layer “Loop DoS” class (CISPA, 2024); what follows is the SOCKS5-UDP instance of it.

Same isolated style as before — proxy A, proxy B, an echo server, and an attacker that fires one spoofed packet (src = B:1080) and then walks away:

attacker  fired ONE spoofed bootstrap: src=B:1080 -> A:1080   ...then exited
[A] forward #1..#21:  from B:1080 -> echo  'hi'
[B] forward #1..#20:  from A:1080 -> echo  'hi'
[echo] hit #1..#41
proxy A forwards: 21   proxy B forwards: 20   echo hits: 41

One packet produced 41 echo hits and 41 inter-proxy datagrams, and — this is the unsettling bit — the loop kept running entirely after the attacker was gone. A’s forwards #4 through #21 all happened with nobody there driving them. It’s a clean listen-to-listen ping-pong (:1080↔:1080), and it only stopped because I had capped the echo server. Uncapped, it runs forever — until a single UDP packet somewhere happens to drop. The amplification here isn’t in bytes, it’s in persistence: one forged datagram turns into unbounded traffic that saturates both relays’ uplinks and hammers the responder on every hop. Compose that with the enumerable /28 pool and, in principle, you could seed a whole field of independent loops, one packet apiece.

This is the genuinely scary bit — except the internet has mostly killed it off already, kind of by accident. And I mean genuinely starved, not “mitigated on paper”:

  • The loop needs a responder that replies to its own reply, fed back to it. Only “answer literally anything” services do that — echo, chargen, QOTD, daytime — and those have been off by default for decades. The fuel is essentially extinct.
  • Modern protocols are silent on purpose. WireGuard answers only a cryptographically valid handshake (cookie-based anti-DoS) and drops everything else without a peep. QUIC and DTLS withhold any reply until they’ve verified you aren’t spoofing. Feed the relay’s forward into any of them and you get back nothing — no reflection, no loop. That silence is a stated design goal, and it turns out to be exactly the right defense.
  • DNS and NTP don’t sustain it in this topology. Feed a resolver its own response packet and it just drops it on the floor; the loop dies inside a single hop.

So here’s the honest verdict on the scariest mechanism I found: the loop genuinely multiplies, it’s trivially seeded, and it composes beautifully with the enumerable pool — and its real-world fuel is gone, and the protocols people actually run today refuse to play. It runs forever in my lab, but out on the real internet there’s nothing to feed it. Strip it away and the residual risk collapses right back to weak one-shot reflection, and the relays settle back into being what they always were: a laundering and detection-signal story, not a volumetric weapon.

Why every standard scanner reports these as secure

Having understood one relay cold, I went and looked at a large working list of SOCKS endpoints, and a whole population turned out to be sitting on port 1080, in netblocks that were not the least bit ambiguous:

  • 47.79/80/81/83/236/237, 8.218/8.219Alibaba Cloud (Aliyun), including their international regions
  • 121.36, 124.71Huawei Cloud; plus China Telecom / Unicom ranges

Not residential — Chinese cloud ECS, default SOCKS port, the same sequential /28 layout everywhere, all pointing at one widely-deployed proxy build gone wrong.

Now the part that made me think this was worth writing up. The vulnerability class is not a secret. There’s an IETF draft to deprecate SOCKS5 FRAG that flatly notes UDP ASSOCIATE is reachable “prior to, or during, the establishment of a fully authenticated session,” and it explicitly wants the proxy barred from being “an amplification vector.” TURN’s security literature describes the identical shape — auth on the control channel, no per-packet auth on the data path — and calls TURN servers “the modern equivalent of those misconfigured open proxies.” There are SOCKS auth-bypass advisories and malicious no-auth ASSOCIATE modules floating around already. None of this is new.

But look at how we measure open proxies. The big internet surveys, the ZMap and Masscan sweeps, the tooling everyone cites — they all gate on a no-authentication TCP handshake. And these proxies require auth on TCP. So every conventional scanner connects, gets asked for a password, dutifully marks the host “closed / auth-required,” and moves on. The open-UDP-relay behavior is only visible if you probe the UDP path regardless of TCP auth — send the wrapped datagram and watch for a forward, which is precisely the check nobody’s list makes.

The result is a population that’s systematically undercounted. It’s a known bug class hiding inside a measurement-methodology gap. Everyone’s looking at the locked front door and nobody’s checking the open UDP window around the side.

What I’d actually do with this: detect, don’t touch

I don’t sell verdicts, I sell signals, and this is a clean one. The relay’s behavior is a multi-part fingerprint you can collect without auth and without abusing anything:

  • forwards a wrapped UDP datagram with no ASSOCIATE (the bypass itself)
  • reassembles inbound fragments but DF-drops anything >1472 B on egress
  • drops empty payloads, minimum forwardable unit one byte
  • egress IP == ingress IP (single box, no exit isolation)
  • /28-block, +2-per-host port allocation → enumerable from one sighting

Any host matching that profile is infrastructure, not an eyeball. And a host that also shows up sourcing amplifier-shaped traffic is a high-confidence “abuse relay.” For a model consuming a feed, “this IP is a stateless open UDP relay on cloud” is exactly the sort of observable that should move a score — even though I’d never tell anyone to go fire packets at it.

The right end of this is detection and notification: catalogue the pool, feed the fingerprint into scoring, and quietly point the cloud abuse desks at the misconfiguration. Not use. These are other people’s broken machines, and the useful move for an operator on the receiving end is boring and effective — enforce ASSOCIATE and per-packet source binding on the UDP path so it inherits the same trust as the TCP session, and set BCP38 egress filtering upstream so a spoofed source never leaves the network in the first place. Fix either and the relay stops being reflectable.

Honest limits

I’d rather undersell this than get caught overclaiming, so:

  • The deep teardown is one box. The +2//28 pattern and the cloud attribution come from the list, but I have not exhaustively re-probed every one of them, and I’m not going to mass-fire UDP at third-party infrastructure to pad a number. The lab work stayed in the lab.
  • “Chinese cloud” here means Chinese cloud providers — Alibaba, Huawei, including their overseas regions. It is a statement about where the boxes physically sit, not a claim about who operates each individual proxy.
  • The amplification figures are measured against real resolvers and a real monlist mock; field factors will vary with the record and the path. Treat the table as a ceiling I actually observed, not a promise.
  • The reflection only functions where source spoofing is possible at all. On the real internet BCP38 is the gate, and it holds far more often than it fails — which is the entire reason these relays are a laundering curiosity and not a volumetric weapon.

The interesting thing was never “open relay can DDoS.” It can’t, much — I spent a good chunk of this post proving that with tables. The interesting thing is a few thousand misconfigured boxes that every standard scanner reports as secure, each one leaking an open UDP forwarder that’s trivially fingerprinted the moment you stop asking the TCP front door for a password. That gap — between what the handshake says and what the UDP path actually does — is the signal.