ip2location/ip2proxy-laravel
Laravel integration for IP2Proxy, letting you detect proxy, VPN, Tor, and other anonymizing connections from visitor IPs using IP2Location IP2Proxy databases. Includes service provider, configuration, and simple query helpers.
Install and wire up.
composer require ip2location/ip2proxy-laravel
php artisan vendor:publish --provider="IP2Proxy\IP2ProxyServiceProvider"
Publishing gives you config/ip2proxy.php, whose only meaningful key is the path to the binary database. Then:
.BIN file. Download IP2Proxy LITE (free, PX1–PX11, one file per proxy-type tier) from IP2Location, or use a licensed commercial database. The LITE files are refreshed monthly — treat the download as a deploy artefact, not a repo file.storage/app/ip2proxy/IP2PROXY-LITE-PX*.BIN) and point the config at an absolute path, ideally via .env:// config/ip2proxy.php
'database' => env('IP2PROXY_DATABASE', storage_path('app/ip2proxy/ip2proxy.bin')),
ip2proxy/ip2proxy-php reader:use IP2Proxy\IP2Proxy; // or the registered facade alias
$record = IP2Proxy::get($request->ip(), 'ALL'); // array/object of fields
Where to look first. The facade is thin — almost all real behaviour lives in ip2proxy/ip2proxy-php (the vendor library in vendor/ip2location/). When something behaves unexpectedly, read that class, not the Laravel wrapper. The wrapper's surface is: service provider binding, a config file, and a facade root. That's it.
First use case. The one that pays for itself fastest: gate registration, login, password-reset and checkout endpoints on a proxy check, and log the verdict rather than blocking outright for the first two weeks. You want to see your false-positive rate against real traffic before you start rejecting customers. A corporate VPN user who can't log in is a support ticket; a TOR exit that completes a card-testing signup is fraud.
Wrap the facade in your own service — never call it from controllers. A one-class ProxyIntelligence service that takes an IP, returns a small typed DTO (isProxy, proxyType, countryCode, usageType, threat), and swallows library exceptions gives you one place to swap providers, add caching, and fake in tests.
final class ProxyLookup
{
public function check(?string $ip): ProxyVerdict
{
if (! $ip || ! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return ProxyVerdict::unknown(); // don't even ask the DB
}
try {
$r = IP2Proxy::get($ip, 'ALL');
return ProxyVerdict::fromRecord($r);
} catch (\Throwable $e) {
report($e);
return ProxyVerdict::unknown(); // fail open
}
}
}
Middleware, with a risk tier. Don't apply proxy blocking globally. Use a named middleware on the handful of routes where it matters (auth, register, checkout) and a config-driven action per route group: log, challenge (force 2FA/CAPTCHA), or block. Middleware that 403s everything is the classic day-one mistake.
Cache by IP, not by request. A single page load can hit the check several times (middleware, a validation rule, a Blade partial). Memoize per-request in a container-scoped array, and put a short-TTL cache (Cache::remember("ip2proxy:{$ip}", now()->addHours(6), ...)) in front of it for anything repeated across requests. Proxy status for an IP is stable enough that hours of TTL cost you nothing.
Store the verdict, not just the decision. On user creation, persist the proxy fields you got at signup time (is_proxy, proxy_type, country_code, asn) on the user/order row. Later fraud review can then reason over historical data without re-querying, and it survives a BIN database upgrade changing its answer.
Run batch work in a job, never in a request loop. Backfilling 50k signups or scanning an access log is a console command/chunked job. The BIN reader is fast per lookup, but a web request that loops over thousands of IPs will blow your response time and memory.
Deploy the database, don't commit it. Add a deploy step that downloads the current BIN to storage/app/ip2proxy/ and atomically swaps it (mv into place). Keep the previous file so rollback is a symlink flip. If you commit a 40–100 MB binary to git you will regret it in clone times and diffs forever.
For tests, bind a fake. Swap the container binding or IP2Proxy::shouldReceive() in feature tests. Your CI should never need a real BIN file, and never depend on a specific IP's real-world classification (those change monthly).
'ALL' on a PX1 database will not return threat or provider; the underlying library raises an exception when a requested field isn't present in the BIN. Pick the database tier that matches the fields you actually read, and catch IP2ProxyException around every call. This is the #1 surprise.isProxy return type is a truthiness trap. Depending on version it comes back as 'YES'/'NO' strings, 1/0, or similar. Never do === true. Normalise once, in your wrapper: in_array($value, [true, 1, '1', 'YES'], true).$request->ip() may be 10.x, 172.16.x, or 127.0.0.1 — the database has no answer. Validate and short-circuit before the lookup, and make sure TrustProxies is configured or every check runs against your LB's address instead of the customer's.-IPV6 variant to answer for v6 clients. If you're dual-stack, you need both or you'll silently get "not a proxy" for half your traffic.env() works in config/ip2proxy.php only because config is built before caching — but after config:cache the value is frozen. Always use absolute paths (storage_path()/base_path()); a relative path resolves against the current working directory and will work in php artisan serve and break under a queue worker or Octane.proxyType, asn, and threat are useful to attackers for probing what you detect. Keep them server-side; log them.ProxyLookup abstraction rather than trying to extend the vendor class.How can I help you explore Laravel packages today?