feat(ipenrich): offline IP ASN + VPN enrichment crate (BUNYIP-437) #34
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-437-ipenrich-crate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
dunite-ipenrich, the ASN + VPN enrichment counterpart todunite-geoip, as slice 1 of BUNYIP-437 (ASN and VPN enrichment of client IPs as a shared service). This is the standalone shared crate; the bunyip consumer that surfaces the signal to admins is a separate follow-up PR that bumps the pinned rev.What it does
IpEnrichService::new(path)opens an IP2Proxy PX.BINandenrich(ip)resolves an address to anIpEnrichment:asn+organization(owning AS) +isp+provider+threat: the raw dataset strings, trimmed, with IP2Proxy's"-"placeholder dropped toNone.category: NetworkCategory- residential / commercial / hosting / unknown, classified from theusage_typecode.vpn: VpnLikelihood- not-a-proxy / VPN / Tor / public / web / residential proxy / data centre / other / unknown, classified fromis_proxy+proxy_type. This is the distinct advisory VPN-likelihood field the ticket asks for.Why IP2Proxy (not IPinfo)
The ticket named IPinfo, but IPinfo's ASN + privacy dataset needs a paid token and a new dependency and is not offline-free. The
ip2locationcrate already in this workspace reads IP2Proxy PX.BINdatabases (theRecord::ProxyDbbranchdunite-geoipcurrently discards), one lookup gives every field above, and IP2Proxy has a free LITE tier with monthly refresh. So this mirrorsdunite-geoipexactly rather than introducing a new data path. Decision confirmed with the team lead on this pass.Design (mirrors dunite-geoip)
IpEnrichError; nodunite-coredependency, so it stays a leaf (the DEV-515 version-cascade rule).is_non_public_ipfilters private / reserved addresses before any lookup; pointing the service at a plain IP2Location file opens fine but returnsNonefor every lookup, so a misconfiguration degrades to "no signal" rather than a crash.Advisory, never a verdict
VpnLikelihooddescribes an address; it never decides a request is abuse. The ticket was explicit that a legitimate user behind a VPN must not be auto-classified as spam, so the type stays descriptive and every decision belongs to the consumer.Tests
The residential / cloud-provider / VPN classification mappings are pure (string in, enum out) and unit-tested here (
classify_category,classify_vpn,from_proxy_record, plus theis_non_public_ipfilter): 12 tests. Opening a real.BINis the consumer's integration test, asdunite-geoipleaves its.BINlookup to the consuming app (no fixture.BINis shipped).Verification
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings, andcargo test --workspaceall pass in the pinnedrust-builder-glibc:v1.0.1-rust1.94-trixieimage. No new workspace dependency (reusesip2location), no members-list edit (workspace globscrates/*).🤖 Generated with Claude Code
https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9