Never block narrower than a /24
docker-build-push / build-push (push) Successful in 24s

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:
pmb committed 2026-09-21 22:33:00 -07:00
1 parent 9e4456b7df
commit 233458d6b8
4 files changed
+189 -67

No files matched your search

+11 -8
View File
@@ -80,8 +80,9 @@ Every `account.created` delivery already carries the signup IP for free
gate. If the signup is rejected instead, no welcome is ever sent.
- **Auto-registered IP block** (`IP_SCRUTINY_AUTO_IPBLOCK`) — the *network*
the signup IP belongs to (ipapi.is's `asn.route`, when it's a valid CIDR
that actually contains the IP; otherwise just that one `/32`/`/128`) is
added to Mastodon's native `Admin::IpBlock` at
that actually contains the IP and falls inside the allowed width window;
otherwise the `/24` — `/48` for v6 — containing the address, never the
bare `/32`/`/128`) is added to Mastodon's native `Admin::IpBlock` at
`IP_SCRUTINY_IPBLOCK_SEVERITY` (default `sign_up_block` — a hard reject
of future signups from the range at registration, no moderator queue,
no appeal via this bot; reversible from the admin UI). Blocking the
@@ -298,10 +299,11 @@ What the response deliberately does **not** contain:
their data rather than ours. An entry asserts only that the block exists.
Entries report the **network** that was blocked (ipapi.is's `asn.route` for
the signup IP, which is what `register_ip_block` registers), not the address
that signed up. A `/32` or `/128` entry means a single address really is what
was blocked — either the classification returned no usable route CIDR, or the
signup predates range-blocking (2026-07-15). The `block_cidr` column recording
the signup IP, clamped to the width window, which is what `register_ip_block`
registers), not the address that signed up. A `/32` or `/128` entry means a
single address really is what was blocked — a historical row now, since every
block registered from 2026-09-21 covers at least a `/24`: either the row
predates the width floor, or it predates range-blocking (2026-07-15). The `block_cidr` column recording
this arrived with the API itself, so existing rows were backfilled once from
the ipapi range cache by `bin/backfill-block-cidr.py`; ten rows with no cache
entry left are shown as their `/32`, since a narrower entry is a safer error
@@ -420,7 +422,8 @@ reactive webhook path alike:
| Setting | Default | Why |
|---|---|---|
| `IP_SCRUTINY_MAX_BLOCK_PREFIX_V4` / `_V6` | `24` / `48` | ipapi.is reports the flagged address's whole ASN route. Right for hosting space, wrong for an ISP aggregate — routes of `/8`–`/11` turn up routinely, and `197.211.32.0/19` was registered against a Benin **mobile carrier** off one `abuser` flag. Anything wider falls back to the single address. |
| `IP_SCRUTINY_MAX_BLOCK_PREFIX_V4` / `_V6` | `24` / `48` | **Ceiling** — the widest network one block may cover. ipapi.is reports the flagged address's whole ASN route. Right for hosting space, wrong for an ISP aggregate — routes of `/8`–`/11` turn up routinely, and `197.211.32.0/19` was registered against a Benin **mobile carrier** off one `abuser` flag. Anything wider falls back to the floor width below. |
| `IP_SCRUTINY_MIN_BLOCK_PREFIX_V4` / `_V6` | `24` / `48` | **Floor** — the narrowest network one block may cover. A `/32` blocks the address we saw and nothing the same actor moves to next door, which is exactly how these ranges get used; anything narrower than this (missing route, `/32` fallback, small hosting allocation) is widened to the network of this width containing the address. At the default this equals the ceiling, so every v4 block is exactly the `/24`. What that costs an innocent neighbour depends on the two policies around it — at `sign_up_block` severity with a month-long `IP_SCRUTINY_IPBLOCK_EXPIRES_IN` (what yttrx runs) one flagged signup hard-refuses registration from 256 addresses for a month; `sign_up_requires_approval` or a shorter expiry buys that back. Set to `32`/`128` to restore blocking exactly what the route says. |
| `IP_SCRUTINY_IPBLOCK_EXPIRES_IN` | `86400` (24h) | An IP block claims who is behind an address *now*, and VPS addresses are recycled constantly. A day breaks a signup run — which is the shape this abuse takes — without the blocklist accumulating forever. Email domain blocks have no expiry in Mastodon and are unaffected. |
The cap applies to what gets **blocked**, not what gets **cached**: the wide
@@ -515,7 +518,7 @@ Copy `.env.example` to `.env` and fill in:
| `IP_SCRUTINY_DRY_RUN` | `true` — classify + DM only, no held welcome, no ip_block write |
| `IP_SCRUTINY_HOLD_WELCOME` | `true` — hold the welcome for a flagged signup until `account.approved` |
| `IP_SCRUTINY_ABUSE_THRESHOLD` | Distinct-reporter threshold used (if lower) for accounts with a flagged signup IP |
| `IP_SCRUTINY_AUTO_IPBLOCK` | Auto-register a flagged signup's network (ipapi.is route, or its own `/32`/`/128` if no route) into Mastodon's `Admin::IpBlock` |
| `IP_SCRUTINY_AUTO_IPBLOCK` | Auto-register a flagged signup's network (ipapi.is route clamped to the width window, or the `/24`/`/48` containing the address if there's no usable route) into Mastodon's `Admin::IpBlock` |
| `IP_SCRUTINY_IPBLOCK_SEVERITY` | `sign_up_block` (default) — hard reject, no queue; other options: `sign_up_requires_approval` (soft, moderator queue), `no_access` |
| `IP_SCRUTINY_STRONG_SUSPEND_ENABLED` | Master switch — immediately suspend a signup whose IP is flagged `datacenter`/`proxy`/`tor`/`abuser` (not `vpn` alone), regardless of the email signal |
| `IP_SCRUTINY_STRONG_SUSPEND_ACTION` | `suspend` (default) or `silence`, applied immediately on a strong IP signal |