trianity / laravel-ip-analyzer
Local IP country and ASN analysis with extensible observation-based rules for Laravel.
Requires
- php: ^8.4
- composer-runtime-api: ^2.0
- ext-curl: *
- ext-zlib: *
- geoip2/geoip2: ^3.4
- guzzlehttp/guzzle: ^7.9|^8.0
- guzzlehttp/psr7: ^2.7|^3.0
- illuminate/config: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/translation: ^12.0|^13.0
- maxmind-db/reader: ^1.14
- psr/http-message: ^1.1|^2.0
- symfony/process: ^7.2|^8.0
Requires (Dev)
- larastan/larastan: ^2.0|^3.0|^4.0
- laravel/pint: ^1.2|^2.0|^3.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0|^5.0
- pestphp/pest-plugin-laravel: ^4.0|^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Documentation for 2.3.0 · Changelog · Magyar quickstart
Local Country and ASN facts with configurable observation rules for Laravel 12–13.
PHP 8.4–8.5 is the tested range. The package reads manually installed MaxMind MMDB
files. Runtime lookups, ip-data:status, ip-data:verify and about perform no HTTP,
DNS, downloads or telemetry. The explicit ip-data:update command, including its
read-only --check mode, can contact the configured source over HTTPS.
The package does not make signup decisions, block requests, assign a global risk score or ship an application blacklist. Country/ASN data is not bot evidence; an empty match list does not certify safety.
Installation and use
Requirements: PHP 8.4 or 8.5, Laravel 12 or 13, and the PHP cURL and zlib extensions. For the 2.3 release series:
composer require trianity/laravel-ip-analyzer:^2.3 php artisan vendor:publish --tag=ip-analyzer-config
Releases are available on GitHub and Packagist. For a local source checkout, use the local development installation.
The provider is autodiscovered. Configure local files outside the public web root:
IP_ANALYZER_COUNTRY_DB=/srv/private/ip-data/GeoLite2-Country.mmdb IP_ANALYZER_ASN_DB=/srv/private/ip-data/GeoLite2-ASN.mmdb
The defaults are under storage/app/ip-analyzer/. Only the configuration file
reads environment variables. After changing paths, rebuild your application's
configuration cache and reload long-lived workers to pick up configuration changes.
Replacing the file at an unchanged path does not require restarting a reader.
use Trianity\IpAnalyzer\Contracts\IpAnalyzer; use Trianity\IpAnalyzer\Contracts\IpLookup; use Trianity\IpAnalyzer\Enums\LookupStatus; $result = app(IpAnalyzer::class)->analyze(request()->ip()); if ($result->facts->countryStatus === LookupStatus::Found) { $country = $result->facts->countryCode; } $observations = $result->matches; // list<RuleMatch>; no automatic decision $factsOnly = app(IpLookup::class)->lookup('8.8.8.8');
The application owns trusted-proxy configuration and source-IP selection. Do not pass a raw forwarded-header list. DTOs are readonly and expose no MaxMind objects.
Input policy and source states
Input must be an exact IPv4 or IPv6 address: no hostname, URL, port, whitespace,
zone ID or address list. Accepted input is canonicalized with inet_pton/inet_ntop.
IPv4-mapped IPv6 is converted to IPv4 before classification and rule matching.
NonPublic is a conservative special-purpose exclusion policy, not a claim
that every excluded address is unroutable. No database is opened for InvalidInput
or NonPublic. Configuration is still validated. See the exact ranges in
IP policy, based on the
IANA IPv4 registry
and IPv6 registry.
Each source is independent:
| Status | Meaning |
|---|---|
found |
Requested data exists; only then is its value populated |
not_found |
Address or requested field is absent |
unavailable |
File cannot be read, is corrupt, has an invalid record or wrong type |
invalid_input |
Input is not one literal IP |
non_public |
Address is excluded by the policy |
Country reads country.isoCode only, never registered-country fallback. Missing
data remains null, never HU, ASN 0 or a safe assessment. ASN organization may
be null even when the ASN number exists.
Stable source error codes are file_unreadable, invalid_database,
database_type_mismatch, and invalid_record. Public errors do not include paths.
Expected SDK data/IO errors become states; programming errors propagate from PHP
services. Custom-rule errors also propagate.
Lookup, status and explicit verification accept exactly GeoLite2-Country or
GeoIP2-Country for Country and GeoLite2-ASN for ASN. The built-in updater manages
the configured GeoLite2-Country and GeoLite2-ASN editions only. A manually installed
GeoIP2-Country remains readable and verifiable, but is edition-incompatible with
updater freshness state; check reports unknown freshness and a normal update attempts
to obtain a validated GeoLite2 replacement. Its build epoch still participates in
downgrade protection. City/ISP databases are deliberately not substitutes.
Metadata contains databaseType, buildEpoch, ageDays, stale, and futureBuild.
Age is clamped to zero for a future build; futureBuild=true makes that clock
anomaly explicit. Stale means age strictly greater than max_age_days (default
30); equality is fresh. The threshold must be a non-negative integer whose
conversion to seconds fits a PHP integer. Stale data remains readable.
Readers exist only within a source operation, with close in finally. Status reads metadata without probing an IP. New operations reopen the files, so an atomic replacement is visible even to a retained lookup instance. There is no persistent reader, Redis cache or per-lookup full-file hash. A concurrent replacement of the two files is not a transaction across both sources.
Observation rules
In config/ip-analyzer.php:
'rules' => [ 'ip_cidr' => [ [ 'id' => 'observed-test-network', 'value' => '192.0.2.0/24', 'reason_code' => 'operator_observation', 'severity' => 'warning', 'message' => 'Matches an operator-maintained observation.', ], ], 'asn' => [], 'country' => [], ], 'custom_rules' => [ App\IpRules\ObservedNetworkRule::class, ],
All three groups use the same five required fields. value is a literal IP or
CIDR, a positive integer ASN, or a two-letter country code (normalized uppercase).
Severity is info, warning or high. IDs must be non-empty and unique across
all configured and custom rules. Malformed entries, unknown fields/groups,
duplicates and invalid custom classes raise InvalidArgumentException.
Order is IP/CIDR, ASN, country, then custom class list order. The lookup runs once; every rule is evaluated and all matches are retained. ASN/country rules require the corresponding Found status. IP rules also apply to normalized NonPublic addresses, but never invalid input. CIDR compares binary prefixes within one address family, including /0, /32 and /128. Host bits in the configured CIDR are ignored. Mapped single IP rules normalize to IPv4; mapped IPv6 CIDRs are rejected as ambiguous—use their IPv4 CIDR equivalent.
Custom classes implement Contracts\Rule, with id(): string and
evaluate(IpFacts $facts): ?RuleMatch. The Laravel container resolves constructor
dependencies. Null means no match. A match must use the rule's ID. See
the custom-rule example. Keep custom rules
local and side-effect free; the package cannot enforce arbitrary application code.
Config contains class names, not closures or instantiated rules, so it can be cached.
Commands
php artisan ip-data:lookup 8.8.8.8 php artisan ip-data:lookup 2606:4700:4700::1111 --json php artisan ip-data:status --json php artisan ip-data:verify --database=country --json php artisan about
Human output includes localized status/error labels and an indented structured
result; --json emits one compact parseable document without decoration. Lookup includes facts, source states/metadata/errors and matches.
Status reports each source's readability/structural metadata status, without
opening records or making a geographic probe. Found in status means metadata was
read successfully; it is not an exhaustive integrity scan of every record.
ip-data:verify performs that full offline traversal and SHA-256 calculation for
both configured files by default, or for the requested --database selections.
Both configured sources are required for command availability.
| Exit | Lookup | Status | Verify |
|---|---|---|---|
| 0 | Usable check; includes NotFound, NonPublic, stale data and rule matches | Both sources readable, right type, not stale | Every selected database verified |
| 1 | At least one source Unavailable | At least one source Unavailable or stale | Failure, or busy mixed with a successful verification |
| 2 | Invalid input/configuration or rule failure | Invalid configuration or unexpected failure | Invalid database selection/configuration |
| 3 | — | — | Every selected operation was busy; no verification completed |
| 130 | — | — | Cooperative Ctrl-C interruption |
At the lookup/status CLI boundary failures are sanitized to
{"error":"invalid_configuration_or_rule"} and exit 2; exception details remain
available to callers of the PHP services. Verify uses the update-domain errors
documented in the updater guide. A future-build flag alone does not change the
lookup/status exit code. Lookup, status and verify never download data or create
directories/databases. about reads the installed Composer version, with a
root-package/development fallback, and does not open MMDB files.
Database downloads and updates
Manual MMDB installation still works without credentials. For the built-in updater, obtain the following from your MaxMind account:
| MaxMind setting | Where to obtain it | Host application setting |
|---|---|---|
AccountID |
Account Information (instructions) | .env: IP_ANALYZER_MAXMIND_ACCOUNT_ID; config: ip-analyzer.update.account_id |
LicenseKey |
License Keys, create a key (instructions) | .env: IP_ANALYZER_MAXMIND_LICENSE_KEY; config: ip-analyzer.update.license_key |
EditionIDs |
Download Databases, available database editions and Get Permalink(s) | Fixed mapping: country → GeoLite2-Country, asn → GeoLite2-ASN |
Use a License Key, not your account login password. Put the values in the
consuming Laravel application's .env, not in the package/vendor directory:
IP_ANALYZER_MAXMIND_ACCOUNT_ID=YOUR_ACCOUNT_ID IP_ANALYZER_MAXMIND_LICENSE_KEY=YOUR_LICENSE_KEY IP_ANALYZER_UPDATE_SCHEDULE=false IP_ANALYZER_VALIDATION_WORKERS=1 IP_ANALYZER_VALIDATION_WORKER_TIMEOUT=1800
The host application's config/ip-analyzer.php maps these environment variables
into the update section. Publish it with
php artisan vendor:publish --tag=ip-analyzer-config if it does not exist; retain
existing custom rules when upgrading. See the config example.
MaxMind's GeoIP.conf is for the separate geoipupdate program. This package
neither reads that file nor requires that program: copy the AccountID/LicenseKey
values into the settings above. There is no EditionIDs environment variable;
the updater supports Country and ASN, both selected by default or individually
with --database=country / --database=asn. GeoLite2-City from a MaxMind sample
is not supported by this package.
php artisan ip-data:update --check --json php artisan ip-data:verify --workers=2 php artisan ip-data:update --workers=2 php artisan ip-data:update --database=country php artisan ip-data:update --force --json
The first normal update invocation downloads missing files. Later invocations use
lightweight local metadata, persisted installation state and HEAD to avoid unnecessary
GETs. Prepared Country and ASN candidates can be validated concurrently, then are
renamed atomically per file. Older build epochs are rejected even with --force.
--check performs only lightweight metadata reads and HEAD, without record traversal,
full-file hashing, download, installation or state writes. Use ip-data:verify for a
full offline integrity scan of the installed files.
Downloads use HTTPS, origin-scoped Basic Auth and a checked redirect allowlist.
Successful lookups, provider boot, status/about and Composer installation never
start downloads. Long Retry-After responses persist a cooldown for normal update
runs; --force cannot bypass it.
See the updater guide for source URLs, configuration limits,
scheduler opt-in, status/exit codes, credential handling, notices, recovery and
filesystem/platform constraints. Upgrading from 1.x does not require overwriting
a published config: new update options receive defaults. Add credentials to the
environment used when building the config cache; republishing with --force
would overwrite your custom rules and paths.
Update and verification progress
Human update and verification output shows Country/ASN, the current phase,
measured work and elapsed time. A complete integrity scan can take several minutes;
the lightweight --check no longer performs that scan.
php artisan ip-data:update --check # default human progress php artisan ip-data:verify --workers=2 # full installed-file verification php artisan ip-data:update --no-progress # final result and total duration only php artisan ip-data:update --json # one final JSON document on STDOUT php artisan ip-data:update --json --progress # progress and duration on STDERR
--quiet suppresses all output, including explicit --progress.
--no-progress takes precedence over --progress. Since 2.2.1, an interactive
ANSI terminal keeps one stable row per selected database in Country/ASN order.
The compact block refreshes at most once per second, except that phase transitions,
completion, failure and cancellation are immediate. Rows are truncated to the
detected terminal width and completed rows remain visible while other work runs.
Capability detection uses the stream that receives progress, including STDERR for
--json --progress; the whole terminal is never cleared.
Redirected output, CI, unsupported output implementations and --no-ansi use
plain lines without cursor controls. Periodic events are limited to one line every
15 seconds per database; initial state, phase changes and terminal states remain
immediate. Human output includes a final total duration. The 2.2.1 rendering change
did not alter the then-current JSON result schema; 2.3 adds the documented freshness,
local-health and integrity fields and the separate verification result. Use --json
for machine parsing.
MMDB validation reports the actual number of processed CIDR ranges. Real database
traversal proved that metadata nodeCount + 1 is not a valid total for the SDK's
CIDR iterations: the counter can exceed that value. Since 2.2.1 validation is
therefore deliberately indeterminate, showing the processed count without a
percentage or ETA. It does not perform a second full scan merely to obtain a total.
Hashing and downloads use bytes when total size is known. Missing Content-Length
means no download percentage or ETA. Phase ETA uses a monotonic clock and smoothed
speed, starts only after at least one second and two samples, and becomes unknown
during a stall. It is not an estimate for the whole command. Since 2.2.1, elapsed
time has whole-second precision and ETA is shown as approximate rounded seconds
below one minute or rounded minutes thereafter. The 2.2.1 precision change did not
alter numeric progress snapshots. Progress collection remains independent of
rendering and does not add sleeps to update supervision. --check never shows
download, extraction or installation as performed phases.
Where optional PHP PCNTL signal handling is available, Ctrl-C requests cooperative cancellation, reports interruption and exits 130. Readers, staging files and owned locks are released at safe checkpoints. An in-flight blocking call may delay cancellation until it returns or reaches a callback/timeout. Installation already in its atomic rename/state section is completed before cancellation is observed; a previously completed database is not rolled back. Inspect status and rerun to reconcile an interrupted command; its JSON error does not claim overall success. No cleanup guarantee is made for SIGKILL. PCNTL is not a package requirement.
See 2.1 verification, the 2.1.2 ETA verification, and the 2.2.1 compact-progress verification.
Freshness and parallel validation (2.3)
Freshness and integrity are separate operations:
php artisan ip-data:update --check
php artisan ip-data:verify --workers=2
php artisan ip-data:update --workers=2
The check reads basic MMDB metadata and existing update state, then performs HEAD.
It reports freshness_unknown when compatible state or comparable remote metadata
is unavailable. up_to_date means no remote change is known; neither status claims
that the installed file passed a full integrity scan. Local metadata health is
reported separately. A normal update conservatively downloads a replacement when
freshness is unknown or local metadata is unusable.
Priority is the CLI workers option over ip-analyzer.validation.workers, whose default is 1. Effective concurrency is bounded by available tasks: Country + ASN verification or two prepared update candidates can use at most two workers. One task always runs directly in the main process; a value of 1 starts no subprocess. There is no CPU-count autodetection.
Each worker performs one complete MMDB traversal and hash using the same validator
as sequential execution. For verify it reads installed files; for update it reads
only prepared candidates, never unchanged or soon-to-be-replaced installed files.
Workers cannot perform HTTP, extraction, installation or state writes. The main
process retains locks, checks that the validated path was not replaced, and preserves
deterministic requested result order. Worker IPC is bounded internal NDJSON and is
not part of the public JSON schema; credentials are never passed to workers.
The ip-analyzer.validation.worker_timeout setting defaults to 1800 seconds. If proc_open, a readable Composer autoloader or a usable PHP CLI executable is unavailable before startup, the command reports a localized notice in progress mode and runs sequentially. A started worker crash, invalid protocol or timeout is a failure and is not silently retried. Supported Ctrl-C handling stops active children before returning exit 130; portable signal handling and SIGKILL cleanup are not promised. Subprocess termination and file-identity protection are verified on local Linux filesystems; Windows and network/distributed filesystems are not certified by this release.
Two workers may roughly double validation memory use and increase storage I/O. Speedup depends on CPU, filesystem/cache behavior and MMDB sizes and is not guaranteed. Download, HEAD, extraction, installation and state writes remain non-parallel. Rebuild Laravel's config cache after changing either environment setting. See the current updater guide for the complete 2.3 contract and 2.2 verification for the worker foundation.
Language and application overrides (2.1.1)
Package-owned human output supports English (en) and Hungarian (hu).
Every rendering uses the application's current Laravel translator locale, including
locale changes within the same application instance. The package never changes
app.locale, app.fallback_locale or the translator's settings.
Resolution is per key: current locale (including application overrides), then
explicit English (including English application overrides). The host fallback
locale is not used for package messages. There is no regional-locale mapping:
hu_HU and en_GB fall back to English unless the application supplies those
exact locale's package translations.
Translations work immediately, without publishing. Optional publication:
php artisan vendor:publish --tag=ip-analyzer-translations
The destination is the host application's lang_path('vendor/ip-analyzer').
For a partial Hungarian override, create
lang/vendor/ip-analyzer/hu/messages.php (under your configured language path):
<?php return [ 'progress' => [ 'start' => 'Az adatbázisok ellenőrzése elindult; ez több percig tarthat.', ], ];
Omitted keys retain package translations. An equivalent en/messages.php override
also applies when English is selected as fallback. Preserve the placeholders of
the overridden key; for example progress.elapsed uses :elapsed and
progress.summary uses :status and :elapsed. Their over-60-second counterparts
are progress.elapsed_minutes (:minutes, :seconds) and
progress.summary_minutes (:status, :minutes, :seconds). The compact ETA
keys use :remaining for seconds and :minutes for minutes; wait keys retain the
duration placeholders. Counted CIDR-range/byte messages use Laravel pluralization.
Keep overrides in the host application, not in vendor/.
Progress, summaries, status labels, sanitized errors/warnings and package command descriptions are localized. Human results include localized source labels followed by the structured result; machine codes and custom rule messages remain intact. Framework-owned help text is left to Laravel/Symfony. There are no package tables or table headers to translate.
--json output is locale-independent, including existing message fields.
With --json --progress, only STDERR human progress is localized; STDOUT remains
exactly one JSON document. The localization layer does not itself change quiet,
no-progress, timing, validation, installation or cancellation behavior; later
releases may add documented result fields or workflow changes. See the
2.1.1 verification record for test results.
Manual data maintenance
Obtain Country and ASN databases manually from MaxMind, under the applicable terms. The package does not supply production databases or credentials.
- Download and extract in a private staging directory on the same filesystem as the destination. Keep credentials out of the application repository.
- Verify the source/checksum using the vendor's distribution information. Check file ownership and read permissions for the PHP worker account.
- Validate candidate metadata with the local SDK: supported database type,
acceptable build date and no future-clock anomaly. In an isolated application
process, point the two config paths at staged files and run
ip-data:status --json. Ensure cached config is not still pointing at the installed files. - Run
ip-data:verify --jsonin that isolated process for a full traversal and SHA-256 calculation. Status validates metadata only; it does not inspect every reachable range. Do not overwrite/truncate a live MMDB in place. - Rename each validated candidate over its destination atomically on that same filesystem. Retain a rollback copy under the applicable data terms, and run status with the application's normal config again.
An operator-controlled atomic rename preserves active readers and lets the next operation see the replacement. The built-in updater automates bounded download, archive extraction, exact downloaded-edition validation, full candidate verification and per-file atomic replacement without changing offline lookup behavior. It does not fetch vendor checksum files or authenticate the producer; source/checksum verification remains an operator responsibility for manual installation.
Development and compatibility
Run development checks from a Git source checkout. Distribution archives exclude the tests, fixtures and development configuration.
composer install
composer validate --strict
composer test
vendor/bin/pint --test
vendor/bin/phpstan analyse
The tests are Pest functions, including the original provider tests, with the Pest Laravel plugin and Orchestra Testbench. Synthetic MMDB fixtures are committed; tests never download data. Their original generator, license and records are in fixture documentation in the source repository. See V2 verification notes for RED/GREEN evidence and actual versions.
The CI workflow covers PHP 8.4/8.5 with Laravel 12 (Testbench 10, Pest 4, PHPUnit 12)
and Laravel 13 (Testbench 11, Pest 5, PHPUnit 13), performs Composer validation and PHP lint,
then runs Pest with networking entry points disabled. PHP 8.3 is deliberately
outside this package's existing ^8.4 requirement and Pest 4/5 test baseline,
even though Laravel 13 itself supports PHP 8.3.
No Laravel/PHP version outside the tested matrix is claimed here.
Local development installation
Add the local directory as a path repository in the consuming Laravel application:
{
"repositories": [
{"type": "path", "url": "../packages/laravel-ip-analyzer"}
]
}
Then install the checkout and publish its configuration:
composer require trianity/laravel-ip-analyzer:@dev php artisan vendor:publish --tag=ip-analyzer-config
Package versions come from Git tags; composer.json deliberately has no
version field. See 2.0 verification for the local
implementation checks and known limits. The 1.0 verification remains in
the historical release record.
Licenses and limits
MIT applies to this project's code and its original synthetic fixtures. MaxMind SDK dependencies and downloaded databases have separate licenses; see THIRD-PARTY-NOTICES.md. The age threshold is an operational warning, not a guarantee of license compliance or data accuracy. Database acquisition, update/deletion obligations and attribution remain the operator's responsibility under the applicable terms.