Since LLM training exploded, the forum gets hammered by non-human traffic: scrapers vacuuming up member-written content, bots probing endpoints for vulnerabilities, distributed IPs pulling attachments in parallel to dodge rate-limits. Stats is the extension I built to observe that traffic, classify it, and ban it automatically via fail2ban. Here’s what it actually does.
The context: why this extension exists
A vanilla phpBB has no built-in way to know who’s really visiting. Apache logs show hits but not intent. A legitimate Chrome 124 and a scraper claiming to be Chrome 124 are indistinguishable from the User-Agent alone. Yet the difference is huge: one reads a few pages, the other drains the entire forum in hours, often spreading requests across hundreds of IPv6 addresses from a single /48 to defeat per-IP rate limits.
The extension sits between Apache and fail2ban. It observes every visit on the PHP side — where the real signals live (cookies, JS, behavioral telemetry) — and emits structured events into a dedicated log. That log is then read by fail2ban filters which decide on bans. The extension never bans on its own: it just produces an actionable signal.

What you see in the ACP
The ACP is not a passive reporting tool: it’s the control center. Each tab answers an operational question.
- Overview — how many visits today? What share are presumed humans, grey-zone, confirmed bots? The probabilistic donut is computed by
session_probability_model.phpwhich combines observed signals into a per-sessionP(bot). Each session shows its badge. - Sessions — the timeline of every visitor: pages viewed, ordering, time between clicks, attachment downloads, signals fired, cookie state, AJAX telemetry result. This is where you spot a scraper instantly: 200 pages in 4 seconds, zero mouse movement, no screen resolution.
- Pages — top URLs visited with full referers. Great for spotting a third-party site running its own crawler.
- Map — geographic distribution (jVectorMap). The country-level waves jump out as soon as a new actor enters the hunt.
- Behavior — the game-changer tab. Statistical profiles learned from logged-in members (human reference), comparison with guests, outlier detection, SVG cursor traces when captured, and a history of marginal cases.



How it detects: five layers
The extension stacks five signal families. None is sufficient on its own — their combination is what produces robust decisions.
1. HTTP/UA signals — instant, server-side
Emitted on the first request, no JS required. These reach fail2ban fastest. Examples: empty_ua (no User-Agent at all), fake_chrome_build (Chrome UA with an inconsistent build number), fake_legit_bot (claims Googlebot but reverse-DNS denies it), html_entities_in_url (URL containing &%3B — a scraper replaying links copied from HTML source), posting_first_visit (POST to posting.php on the very first hit — no human does that).
2. JS signals — AJAX telemetry
The browser runs a script that measures: screen resolution, window size, presence of navigator.webdriver, scroll profile (speed, plateaus, jumps), mouse or touch events. Everything is posted to the secured POST /stats/px endpoint (session-bound link token + same-origin check). A headless bot either never runs that script or leaves nonsensical traces: ajax_webdriver, ajax_scroll_too_fast, cursor_no_movement, no_screen_res.
3. Learned statistical signals — the human baseline
Instead of hardcoded thresholds, the extension learns from logged-in members’ real activity (humans by definition). Reading-speed distribution, scroll density, jump ratio, interaction frequency — all measured, and the learn_*_outlier signals fire when a visitor strays too far from that distribution. Upside: the baseline auto-calibrates per forum.
4. Shared-identity signals — cloned cookies & fingerprints
Every visitor gets a signed session cookie, hashed in the database. While valid, it’s the primary session anchor — even when the IP changes (IPv6 privacy extensions, mobile NAT). If the same cookie or browser fingerprint is presented from multiple distinct IPs, the extension emits guest_cookie_clone_multi_ip or guest_fp_clone_multi_ip — near-certain proof of a botnet sharing stolen identities.
5. Deferred signals — the geo_async cron
Some detections only make sense after the fact. The geo_async cron resolves IPs via ip-api.com (DB cache per IP and per /24 prefix to save API calls), then does two things: (a) it flags guests from targeted countries who viewed a single page and left within 5 minutes (cn_no_interaction_5m); (b) it spots distributed downloads: too many attachments viewed from too many nearby IPs in a short window → xip_dl_soft_v1 or xip_dl_hard_v1 depending on score.

The fail2ban bridge: where observation becomes action
The extension writes two line types into /var/log/security_audit.log (path configurable in ACP):
2025-11-01 14:32:17 PHPBB-SIGNAL ip=1.2.3.4 session=abc123 user_id=0 signals="fake_legit_bot,no_screen_res" ua="Mozilla/5.0 ..." page="/viewtopic.php?t=42" cc=CN 2025-11-01 14:33:05 PHPBB-XIP ip=1.2.3.4 cc=DE method=xip_dl_soft_v1 severity=soft score=72 topic_id=18 downloads=9 views=0 period_sec=3600
PHPBB-SIGNAL is emitted live during the visit. PHPBB-XIP is emitted after the fact by the cron. Both carry the IP, country code and signal list — everything fail2ban needs.
2026-05-20 18:22:48 PHPBB-SIGNAL ip=123.21.186.XXX session=2fdcda1c…XXX user_id=0 signals="old_chrome_106" ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/106.0.5249.119" page="/download/file.php?id=2817&sid=…" cc=VN 2026-05-20 18:25:59 PHPBB-SIGNAL ip=177.128.53.XXX session=b584ea20…XXX user_id=0 signals="posting_first_visit" ua="Opera/8.13.(Windows NT 4.0; mni-IN) Presto/2.9.177" page="/posting.php?mode=quote&p=201159" cc=BR 2026-05-20 18:28:21 PHPBB-SIGNAL ip=180.191.236.XXX session=346bd8f9…XXX user_id=0 signals="old_chrome_42,fake_chrome_build" ua="Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_10_0) Chrome/42.0.887.0" page="/download/file.php?id=90985" cc=PH 2026-05-20 18:32:11 PHPBB-XIP ip=192.99.7.XXX cc=CA method=xip_dl_soft_v1 severity=soft score=68 topic_id=24 downloads=11 views=0 period_sec=3600
Filters and jails shipped with the extension
The extension’s fail2ban/ folder ships 13 ready-to-use filters. On this server they’re symlinked into /etc/fail2ban/filter.d/ and read by matching jails. The ones doing the heavy lifting:
- phpbb-badbot-confirmed — high-confidence signals (
fake_legit_bot,posting_first_visit,fake_chrome_build,empty_ua, etc.): 1 hit is enough, 24 h ban, ×2 backoff up to 30 days. - phpbb-badbot-suspicious — moderate signals (
no_screen_res,ajax_scroll_*,learn_*_outlier): 3 hits, 12 h ban, with backoff. - phpbb-guest-cookie-clone / phpbb-guest-fingerprint-clone — cookie or fingerprint shared across multiple IPs: 1 hit, progressive ban up to 14 days.
- phpbb-cn-no-interaction — guest from a targeted country who doesn’t stay: bans the entire /24 for 3 days (custom nftables action).
- phpbb-crossip-soft / phpbb-crossip-hard — distributed download detected by the cron: hard = long bans with incremental backoff.
- phpbb-combo-scraper — aggregator that bans when several different signals converge on the same IP in a short window.
Every jail uses bantime.increment = true: repeat offenders see their ban double each strike, all the way to several weeks. Short bans are the norm, long bans are earned.
What this looks like in production on this server
A few production figures from forum.debucquoi.com at the time of writing:
- 22,049 IPs banned in total by
phpbb-badbot-confirmed— 5,191 of them currently active. - 462 /24 subnets banned by
phpbb-cn-no-interaction(366 currently active). - 44,433
PHPBB-SIGNALevents in the log over a few months. - 14,132 hits on the
posting_first_visitsignal — that many bots tried a direct POST on/posting.phpwithout ever navigating. - ~10,000 cumulative hits on the
old_chrome_*family — UAs forged on older Chrome versions with mismatched builds.
$ fail2ban-client status phpbb-badbot-confirmed Status for the jail: phpbb-badbot-confirmed |- Filter | |- Currently failed: 0 | |- Total failed: 10141 | `- File list: /var/log/security_audit.log `- Actions |- Currently banned: 5399 |- Total banned: 22869 `- Banned IP list: 45.155.205.XXX 185.220.101.XXX 80.82.78.XXX 91.197.91.XXX …
Why this holds up without false positives
- Reverse-DNS verification for legitimate bots: a Googlebot or Bingbot UA is checked against reverse-DNS; mismatches trigger
fake_legit_bot. Real Google/Bing crawlers pass. - Per-site learned human baseline: the
learn_*thresholds aren’t hardcoded — they come from the actual distribution of logged-in members. A slow forum and a fast forum get different thresholds, automatically. - Signed cookie as stable anchor: a session no longer depends on the IP. A mobile that changes IP every 30 min isn’t counted as two visitors, and a scraper rotating 200 IPs can’t hide if it keeps the same cookie.
- Hard signals vs soft signals separated: hard ones trigger an immediate ban; soft ones require multiple hits. No human gets banned on a single weak indicator.