Audit: search logs by partial IP address / subnet (v4 + v6)
- Dominant language
- PHP
- Stars
- 6.7k
- Forks
- 883
- Avg merge
- 15h 16m
- Merged PRs (30d)
- 73
Description
From [discuss #39624](https://discuss.flarum.org/d/39624-search-by-partial-ip-address-in-the-audit-extension):
> It would be helpful to search the logs by partial IP address. I would use this to search entire /64 ipv6 ranges. An entire /64 would be a single end user subset. I would also search across a /60, etc. The simplest implementation would be to allow wildcard searches. A more advanced approach would be to implement ipv4 and ipv6 subnet recognition. For my use case a wildcard would be sufficient.
Today the audit `ip:` gambit is an **exact** match (`IpFilter` uses `whereIn('ip_address', …)`). The ask is to match **partial IPs / subnets** — the primary use case being whole IPv6 `/64` (and `/60`) ranges.
### Why a wildcard/text-prefix is not sufficient (despite the request)
The requester suggests a wildcard would do, but the data shows it silently fails on their own use case:
- `ip_address` is a **VARCHAR** column storing the IP verbatim from `REMOTE_ADDR`, no normalisation.
- On a real forum, roughly **half the entries are IPv6**, stored in **mixed representations** — full 8-group *and* compressed (`2a09:bac3:4868:1f5::32:db`, abbreviated groups).
- IPv6 compression is **content-dependent, not positional**: `2a02:390:9e5c:beef:0:0:0:1` canonicalises to `2a02:390:9e5c:beef::1`, so a text `/64` prefix of `2a02:390:9e5c:beef` **misses it**. Normalising the stored string does not help — the `::` can eat the boundary.
So a text wildcard cannot reliably match an IPv6 `/64`, which is exactly what was requested. Correct subnet matching requires comparing the **binary** IP value against the subnet range.
### Proposed approach
Add an **indexed binary IP column** and match subnets with an indexed range query:
- New `ip_address_binary` column (packed via PHP `inet_pton`, normalised to 16 bytes / IPv4-mapped-IPv6 so v4 and v6 compare uniformly), indexed.
- Matching = compute `[low, high]` binary bounds in PHP → `whereBetween('ip_address_binary', …)`. Correct for v4 + v6, representation-agnostic, any subnet, and **index-using at scale** (no per-row PHP scan — audit tables can be very large).
- **PHP-side packing (not SQL `INET6_ATON`/`inet`)** keeps it driver-uniform across MySQL/MariaDB/PostgreSQL/**SQLite** (which has no IP functions); the DB only does an indexed `BETWEEN` on a binary column.
- Populate the binary at write time in `AuditLogger::log()`. The filter routes by input shape: CIDR (`…/24`, `…/64`) and bare partials (`192.168` → its `/16`, `2a02:390:9e5c:beef` → its `/64`) both become binary bounds; a full IP is an exact packed lookup. Multi-value OR, negation, and the existing `limitedIpAddress` permission gate are preserved.
- **Backfill** existing rows (potentially millions, including tables inherited from the kilowhat extensions) — a **queued/console backfill** (`audit:backfill-ip`, chunked/resumable) is preferable to an inline migration, so upgrading a large install does not stall.
- Update the `ip:` gambit help in the audit browser to advertise the partial/CIDR forms.
### Deferred to 2.1
This is a net-new feature carrying a **schema migration + heavy backfill** on a potentially very large table — too much upgrade risk to introduce during 2.0 RC stabilisation.
Contributor guide
Research direction
Start by reading the audit IpFilter and AuditLogger::log() entry points, then trace the audit browser's ip: gambit help and the existing schema migration path. Define the binary representation, subnet-bound calculation, write-time population, and resumable audit:backfill-ip flow for all supported databases. Done means exact, partial, and CIDR IPv4/IPv6 searches work while preserving multi-value OR, negation, permissions, and large-table upgrade safety.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- backend, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100