feat(admin): advisory ASN/VPN enrichment of client IPs (BUNYIP-437) #466

Merged
longjacksonle merged 1 commit from feat/BUNYIP-437-ip-enrichment-consumer into main 2026-08-04 20:40:25 +02:00

Slice 2 of BUNYIP-437: the bunyip consumer of the dunite-ipenrich crate merged in dunite PR #34. Gives admins ASN / VPN context on a client IP as an advisory signal.

Manifest

The dunite pins move from 302af65 to ae62c27 (the merge that added dunite-ipenrich). That rev's parent is exactly 302af65, so it is a clean superset: no other dunite crate changed, and the member-scoped cargo update touched only the dunite packages (148 other deps unchanged). dunite-ipenrich is added to bunyip-domain.

Wiring (mirrors the geoip path)

  • Config: new IP2PROXY_DB_PATH -> Config.ip2proxy_db_path, pointing at an offline IP2Proxy PX .BIN.
  • Service: bunyip-api opens it into an optional IpEnrichService; unset or unreadable leaves it None and the feature reports nothing (never blocks a request). services::ip_enrich re-exports the shared crate as a stable seam.
  • Endpoint: GET /v1/admin/ip-enrichment?ip=<addr> (AdminUser-guarded) returns ASN, owning organization, ISP, the classified network category and VPN/proxy likelihood labels, a one-bit is_anonymizing, the raw proxy type, VPN provider, threat, and an always-true advisory marker. It is None-tolerant: a missing dataset, a private/reserved address, or an unknown address all return no data (mapped client-side to Ok(None)); only a malformed IP is a client error. This is the ticket's "narrow interface (address in, enrichment out)".

Admin surface

The IP-bans page renders an Address intel panel when opened for a specific address (the ?ip= look-up / ban link that feedback and autoban already produce). It shows the ASN/org/proxy fields, a network-category badge and a VPN/proxy badge, and is explicitly advisory:

  • a data-centre address is shown but not flagged anonymizing;
  • an absent lookup says "No enrichment available" rather than implying the address is clean;
  • the header states in words that this is never an automatic spam verdict, because a legitimate user can be behind a VPN.

This satisfies the "advisory signal shown to admins, never an automatic classification" AC. Screenshot of the three panel states (VPN, data-centre, no-data) below.

Tests

  • API response mapping: labels, is_anonymizing, always-advisory; a data-centre address is not anonymizing.
  • Web panel rendering: fields plus the advisory note are present; the no-data case notes absence and leaks no field labels.

Verification

just check-container green: fmt, clippy -D warnings, and 179 workspace tests in the pinned rust-builder-glibc:v1.0.1-rust1.94-trixie image. A screenshot render confirmed the three panel states. The tracked Tailwind stylesheet needed no rebuild (every utility the panel uses was already present).

Follow-ups (out of scope here)

  • Dataset-refresh automation and observable age (operational / deploy side).
  • The Mokosh consumer of the same crate (mokosh-server repo).
  • The two-hop client-IP TRUSTED_PROXY_CIDR verification (config item, tracked separately per the investigation).

🤖 Generated with Claude Code

https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9

Slice 2 of [BUNYIP-437](https://niceguyit.myjetbrains.com/youtrack/issue/BUNYIP-437): the bunyip consumer of the `dunite-ipenrich` crate merged in dunite PR #34. Gives admins ASN / VPN context on a client IP as an advisory signal. ## Manifest The dunite pins move from `302af65` to `ae62c27` (the merge that added `dunite-ipenrich`). That rev's parent is exactly `302af65`, so it is a clean superset: no other dunite crate changed, and the member-scoped `cargo update` touched only the dunite packages (148 other deps unchanged). `dunite-ipenrich` is added to `bunyip-domain`. ## Wiring (mirrors the geoip path) - **Config**: new `IP2PROXY_DB_PATH` -> `Config.ip2proxy_db_path`, pointing at an offline IP2Proxy PX `.BIN`. - **Service**: `bunyip-api` opens it into an optional `IpEnrichService`; unset or unreadable leaves it `None` and the feature reports nothing (never blocks a request). `services::ip_enrich` re-exports the shared crate as a stable seam. - **Endpoint**: `GET /v1/admin/ip-enrichment?ip=<addr>` (AdminUser-guarded) returns ASN, owning organization, ISP, the classified network category and VPN/proxy likelihood labels, a one-bit `is_anonymizing`, the raw proxy type, VPN provider, threat, and an always-true `advisory` marker. It is `None`-tolerant: a missing dataset, a private/reserved address, or an unknown address all return no data (mapped client-side to `Ok(None)`); only a malformed IP is a client error. This is the ticket's "narrow interface (address in, enrichment out)". ## Admin surface The IP-bans page renders an **Address intel** panel when opened for a specific address (the `?ip=` look-up / ban link that feedback and autoban already produce). It shows the ASN/org/proxy fields, a network-category badge and a VPN/proxy badge, and is explicitly advisory: - a data-centre address is shown but **not** flagged anonymizing; - an absent lookup says "No enrichment available" rather than implying the address is clean; - the header states in words that this is **never an automatic spam verdict**, because a legitimate user can be behind a VPN. This satisfies the "advisory signal shown to admins, never an automatic classification" AC. Screenshot of the three panel states (VPN, data-centre, no-data) below. ## Tests - API response mapping: labels, `is_anonymizing`, always-advisory; a data-centre address is not anonymizing. - Web panel rendering: fields plus the advisory note are present; the no-data case notes absence and leaks no field labels. ## Verification `just check-container` green: fmt, clippy `-D warnings`, and 179 workspace tests in the pinned `rust-builder-glibc:v1.0.1-rust1.94-trixie` image. A screenshot render confirmed the three panel states. The tracked Tailwind stylesheet needed no rebuild (every utility the panel uses was already present). ## Follow-ups (out of scope here) - Dataset-refresh automation and observable age (operational / deploy side). - The Mokosh consumer of the same crate (mokosh-server repo). - The two-hop client-IP `TRUSTED_PROXY_CIDR` verification (config item, tracked separately per the investigation). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9
feat(admin): advisory ASN/VPN enrichment of client IPs (BUNYIP-437)
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 24s
Check / fmt + clippy + build + tests (pull_request) Successful in 14m2s
Create release / Create release from merged PR (pull_request) Has been skipped
26d912b542
Consumes the new dunite-ipenrich crate to give admins ASN / VPN context on a client IP, slice 2 of BUNYIP-437 (slice 1 was the shared crate). The dunite pins move from 302af65 to ae62c27 (the merge that added dunite-ipenrich; a clean superset of the previous rev, so no other dunite crate changes), and dunite-ipenrich is added to bunyip-domain.

The wiring mirrors the existing geoip path exactly. A new IP2PROXY_DB_PATH config points at an offline IP2Proxy PX .BIN; bunyip-api opens it into an optional IpEnrichService (unset or unreadable leaves it None and the feature simply reports nothing, never blocking a request), and services::ip_enrich re-exports the shared crate as a stable seam.

A narrow admin endpoint GET /v1/admin/ip-enrichment?ip=<addr> returns the enrichment as JSON: ASN, owning organization, ISP, the classified network category and VPN/proxy likelihood labels, a one-bit is_anonymizing summary, the raw proxy type, VPN provider and threat, and an always-true advisory marker. It is None-tolerant: a missing dataset, a private/reserved address, or an unknown address all return no data (mapped client-side to Ok(None)); only a malformed IP is a client error.

The IP-bans admin page renders this as an "Address intel" panel when opened for a specific address (the ?ip= look-up / ban link). The panel shows the ASN/org/proxy fields and a category badge plus a VPN/proxy badge, and it is explicitly advisory: a data-centre address is shown but not flagged anonymizing, an absent lookup says so rather than implying the address is clean, and the header states in words that this is never an automatic spam verdict because a legitimate user can be behind a VPN. This satisfies the "advisory signal shown to admins, never an automatic classification" acceptance criterion.

Tests: the API response mapping (labels, is_anonymizing, always-advisory; data-centre is not anonymizing) and the web panel rendering (fields plus the advisory note present; the no-data case notes absence and leaks no field labels). Verified with just check-container (fmt, clippy -D warnings, 179 workspace tests) and a screenshot render of the three panel states. The tracked stylesheet needed no rebuild (every utility the panel uses was already present).

Dataset-refresh automation and its age observability, plus the Mokosh consumer, are separate follow-ups; the client-IP two-hop trusted-proxy verification is tracked separately per the investigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-04 20:27:14 +02:00
longjacksonle deleted branch feat/BUNYIP-437-ip-enrichment-consumer 2026-08-04 20:40:25 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!466
No description provided.