A /32 block is close to useless against what IP scrutiny fires on: a datacenter/VPN/proxy customer has the rest of the range sitting right there, so blocking the single address we saw stops nothing the same actor does a minute later from the address next door. The width ceiling added 2026-09-13 made this the common case rather than the exception: its fallback for an over-wide ASN route was the bare address, so a flagged signup out of a /18 hosting allocation got a /32. Add IP_SCRUTINY_MIN_BLOCK_PREFIX_V4/_V6 (24/48) as a floor under that ceiling. Anything narrower than the floor -- a missing route, the /32 recorded when a route didn't contain the IP, a small hosting allocation, or the fallback from a route wider than the ceiling -- is widened to the network of that width containing the address. At the defaults the floor equals the ceiling, so every v4 block is now exactly the /24 containing the flagged address. State the collateral cost rather than waving at it: at the live settings (sign_up_block severity, ~30d expiry) one flagged signup now hard-refuses registration from 256 addresses for a month. Moving the severity to sign_up_requires_approval or shortening the expiry buys that back, and setting the floor to 32/128 restores the old behaviour entirely.
This commit is contained in:
1 parent
9e4456b7df
commit
233458d6b8
4 files changed
+189
-67
No files matched your search
+55
-21
@@ -444,38 +444,53 @@ def range_cache_tests():
|
||||
|
||||
|
||||
def block_width_cap_tests():
|
||||
"""capped_block_cidr: the ASN route is the right unit to block for hosting
|
||||
space and the wrong one for an ISP aggregate. ipapi.is reports routes as
|
||||
wide as /8, so without a ceiling one `abuser` flag on a residential or
|
||||
mobile address would wall off millions of consumer addresses."""
|
||||
"""capped_block_cidr: every registered block lands in the width window
|
||||
between the floor and the ceiling. The ceiling is there because ipapi.is
|
||||
reports routes as wide as /8, so one `abuser` flag on a residential or
|
||||
mobile address would otherwise wall off millions of consumer addresses.
|
||||
The floor is there because the opposite extreme is just as useless: a /32
|
||||
blocks the one address we saw while the next signup comes from its
|
||||
neighbour in the same datacenter range."""
|
||||
main.IP_SCRUTINY_MAX_BLOCK_PREFIX_V4 = 24
|
||||
main.IP_SCRUTINY_MAX_BLOCK_PREFIX_V6 = 48
|
||||
main.IP_SCRUTINY_MIN_BLOCK_PREFIX_V4 = 24
|
||||
main.IP_SCRUTINY_MIN_BLOCK_PREFIX_V6 = 48
|
||||
|
||||
# A. At or narrower than the ceiling: block the route, as before.
|
||||
# A. A route already inside the window is blocked as-is.
|
||||
assert main.capped_block_cidr("5.255.96.30", "5.255.96.0/24") == "5.255.96.0/24", \
|
||||
"the ceiling itself is allowed, not excluded"
|
||||
assert main.capped_block_cidr("5.255.96.30", "5.255.96.0/28") == "5.255.96.0/28"
|
||||
|
||||
# B. Wider than the ceiling: fall back to the single address. A /11 is a
|
||||
# consumer ISP aggregate — blocking it would take out 2m addresses.
|
||||
assert main.capped_block_cidr("98.45.12.7", "98.32.0.0/11") == "98.45.12.7/32"
|
||||
assert main.capped_block_cidr("197.211.45.9", "197.211.32.0/19") == "197.211.45.9/32"
|
||||
# B. Narrower than the floor: widened to the floor network containing the
|
||||
# address. A /28 hosting allocation is a fine cache answer but a poor
|
||||
# block — and a bare /32 is the case this floor exists for.
|
||||
assert main.capped_block_cidr("5.255.96.30", "5.255.96.0/28") == "5.255.96.0/24"
|
||||
assert main.capped_block_cidr("5.255.96.30", "5.255.96.30/32") == "5.255.96.0/24", \
|
||||
"a single-address route must widen to the /24, never block just the /32"
|
||||
|
||||
# C. Wider than the ceiling: back down to the floor network — NOT to the
|
||||
# single address, which is what this used to do. A /11 is a consumer
|
||||
# ISP aggregate; blocking it would take out 2m addresses.
|
||||
assert main.capped_block_cidr("98.45.12.7", "98.32.0.0/11") == "98.45.12.0/24"
|
||||
assert main.capped_block_cidr("197.211.45.9", "197.211.32.0/19") == "197.211.45.0/24"
|
||||
# ...including the VPS-sized ranges the bot used to block wholesale.
|
||||
assert main.capped_block_cidr("51.222.9.9", "51.222.0.0/16") == "51.222.9.9/32"
|
||||
assert main.capped_block_cidr("194.163.140.1", "194.163.128.0/18") == "194.163.140.1/32"
|
||||
assert main.capped_block_cidr("51.222.9.9", "51.222.0.0/16") == "51.222.9.0/24"
|
||||
assert main.capped_block_cidr("194.163.140.1", "194.163.128.0/18") == "194.163.140.0/24"
|
||||
|
||||
# C. Same rule for v6, on its own ceiling (/48 = the standard end-site
|
||||
# D. Same rules for v6, on its own window (/48 = the standard end-site
|
||||
# allocation, the rough v6 equivalent of a v4 /24).
|
||||
assert main.capped_block_cidr("2001:db8:1::5", "2001:db8:1::/48") == "2001:db8:1::/48"
|
||||
assert main.capped_block_cidr("2001:db8:1::5", "2001:db8::/40") == "2001:db8:1::5/128"
|
||||
assert main.capped_block_cidr("2001:db8:1::5", "2001:db8::/29") == "2001:db8:1::5/128"
|
||||
assert main.capped_block_cidr("2001:db8:1::5", "2001:db8::/40") == "2001:db8:1::/48"
|
||||
assert main.capped_block_cidr("2001:db8:1::5", "2001:db8::/29") == "2001:db8:1::/48"
|
||||
assert main.capped_block_cidr("2001:db8:1::5", "2001:db8:1::5/128") == "2001:db8:1::/48"
|
||||
|
||||
# D. Missing or unparseable input degrades to the single address rather
|
||||
# than to something wide.
|
||||
assert main.capped_block_cidr("203.0.113.5", "") == "203.0.113.5/32"
|
||||
assert main.capped_block_cidr("203.0.113.5", "not-a-cidr") == "203.0.113.5/32"
|
||||
# E. Missing or unparseable input degrades to the floor network, not to
|
||||
# something wide and not to the bare address.
|
||||
assert main.capped_block_cidr("203.0.113.5", "") == "203.0.113.0/24"
|
||||
assert main.capped_block_cidr("203.0.113.5", "not-a-cidr") == "203.0.113.0/24"
|
||||
# ...and an unparseable IP yields nothing at all, so callers can refuse.
|
||||
assert main.capped_block_cidr("not-an-ip", "") == ""
|
||||
|
||||
# E. The cap is applied to what gets BLOCKED, not to what gets CACHED:
|
||||
# F. The window is applied to what gets BLOCKED, not to what gets CACHED:
|
||||
# the wide route is still the right cache key, since the whole route
|
||||
# genuinely belongs to one operator and one lookup answers for all of
|
||||
# it. Narrowing the cache key instead would multiply provider lookups.
|
||||
@@ -485,7 +500,26 @@ def block_width_cap_tests():
|
||||
assert main.cached_ip_intel("100.64.200.200") is not None, \
|
||||
"a wide route must still answer from cache for its whole range"
|
||||
assert main.cached_ip_intel("100.64.200.200")["cidr"] == "100.64.0.0/10"
|
||||
assert main.capped_block_cidr("100.64.200.200", "100.64.0.0/10") == "100.64.200.200/32"
|
||||
assert main.capped_block_cidr("100.64.200.200", "100.64.0.0/10") == "100.64.200.0/24"
|
||||
|
||||
# G. The floor is a knob: setting it to /32 restores the old behaviour of
|
||||
# blocking exactly what the route says, down to the single address.
|
||||
main.IP_SCRUTINY_MIN_BLOCK_PREFIX_V4 = 32
|
||||
try:
|
||||
assert main.capped_block_cidr("98.45.12.7", "98.32.0.0/11") == "98.45.12.7/32"
|
||||
assert main.capped_block_cidr("5.255.96.30", "5.255.96.0/28") == "5.255.96.0/28"
|
||||
assert main.capped_block_cidr("203.0.113.5", "") == "203.0.113.5/32"
|
||||
finally:
|
||||
main.IP_SCRUTINY_MIN_BLOCK_PREFIX_V4 = 24
|
||||
|
||||
# H. A floor configured WIDER than the ceiling is nonsense; the ceiling
|
||||
# wins, since it is the one bounding collateral damage.
|
||||
main.IP_SCRUTINY_MIN_BLOCK_PREFIX_V4 = 16
|
||||
try:
|
||||
assert main.capped_block_cidr("98.45.12.7", "98.32.0.0/11") == "98.45.12.0/24"
|
||||
assert main.capped_block_cidr("98.45.12.7", "") == "98.45.12.0/24"
|
||||
finally:
|
||||
main.IP_SCRUTINY_MIN_BLOCK_PREFIX_V4 = 24
|
||||
|
||||
|
||||
def combined_signal_tests():
|
||||
|
||||
Reference in new issue
Block a user