vectorface/whip
Whip is a lightweight PHP library for reliably determining a client’s real IP address, even behind proxies, load balancers, and CDNs. It supports IPv4/IPv6, trusted proxy headers, Cloudflare, and multi-proxy setups, helping PHP and Laravel apps log, rate-limit, and secure requests accurately.
vectorface/whip is a small, framework-agnostic client-IP resolver. It answers the question "who is actually calling me?" when your app sits behind a CDN, load balancer, or reverse proxy that rewrites/forwards client IPs via headers like X-Forwarded-For, CF-Connecting-IP, or X-Real-IP. Laravel's own TrustProxies middleware handles the simple single-proxy case; Whip is the tool you reach for when you need multiple header fallbacks, CIDR-based trusted-proxy matching, and safe validation without trusting spoofable headers outright.
composer require vectorface/whip
Then check the repo README first — it carries the canonical list of HEADER_* constants, which is the part you'll reference most. There is no Laravel service provider shipped; this is a plain library you wire up yourself.
First use case — resolving a real IP behind Cloudflare + a load balancer:
use Vectorface\Whip\Whip;
$whip = new Whip();
// Only trust these upstreams, using CIDR notation
$whip->addTrustedProxy('173.245.48.0/20') // Cloudflare range
->addTrustedProxy('10.0.0.0/8'); // internal LB
// Only trust headers your infrastructure actually sets
$whip->addTrustedHeader(Whip::HEADER_CF_CONNECTING_IP)
->addTrustedHeader(Whip::HEADER_X_FORWARDED_FOR);
$ip = $whip->getValidIpAddress(); // string|false — validated with filter_var
Wrap it in a tiny Laravel binding so controllers/services can inject it, and register a Request::macro or a small ClientIp value object rather than calling new Whip() all over the codebase.
Bind as a singleton/service in a provider. Configure the trusted proxies and headers once from config/whip.php, then inject:
// AppServiceProvider::register()
$this->app->singleton(Whip::class, fn () => tap(new Whip(), function (Whip $whip) {
foreach (config('whip.trusted_proxies', []) as $proxy) {
$whip->addTrustedProxy($proxy);
}
foreach (config('whip.trusted_headers', []) as $header) {
$whip->addTrustedHeader($header);
}
$whip->setCache($this->app->make(CacheInterface::class));
});
Use the PSR-16 cache for hostname-based proxies. Trusted proxies may be hostnames; Whip resolves them (gethostbyname) and uses the cache to avoid a DNS lookup per request. Pass your Laravel cache store (which implements PSR-16) via setCache() — this matters under load.
Prefer getValidIpAddress() over getIpAddress(). getIpAddress() returns whatever the header contained, unvalidated and spoofable. getValidIpAddress() walks the chain, discards entries that fail filter_var, and returns false when nothing trustworthy is found. Always have a fallback:
$ip = $whip->getValidIpAddress() ?: $request->server('REMOTE_ADDR');
Middleware pattern. Resolve once, stash it, and let the rest of the app read it:
class ResolveClientIp
{
public function handle($request, Closure $next)
{
$request->attributes->set('client_ip', app(Whip::class)->getValidIpAddress() ?: $request->ip());
return $next($request);
}
}
Integration tip — pick one source of truth. If you keep Laravel's TrustProxies middleware active, have it trust the same upstreams Whip does, or disable one. Two independent trusted-proxy configurations drifting apart is a classic source of "wrong IP in logs but right IP in Stripe webhook" bugs.
Useful beyond HTTP. Whip is genuinely handy in queue workers, CLI cron scripts, and webhook verifiers where you have a raw header bag but not a Request — its only input is $_SERVER, which you can control.
vectorface/whip:^0.4. Laravel 9+ projects on PHP 8.0+ can upgrade with no code changes — the public API is unchanged in this release.composer.json if the IP logic is security-sensitive.Whip and forget addTrustedHeader(), you get no header parsing at all — some developers find this silently confusing. Conversely, calling setTrustedHeaders() with the full list of constants effectively trusts every header, which is exactly the spoofing hole the library exists to prevent. Enumerate only what your edge actually injects.10.0.0.0/8, 2001:db8::/32). A bare hostname is also accepted (with DNS resolution + cache). Malformed entries are where things fail silently — log/validate your proxy list at boot rather than discovering it in production.X-Forwarded-For chain semantics. The header is append-only left-to-right; the rightmost entry is the one your own proxy added, the leftmost is the claimed original client. Whip walks from the right past trusted proxies, discarding untrusted/unvalidated entries. Do not naively explode(',') and take [0] anywhere else in the app — that's the classic spoof.filter_var validation is correct for both, but if you're storing the result in a VARCHAR(15) column or using ip2long, you'll silently truncate IPv6 addresses. Use VARCHAR(45) + inet_pton.$whip->getValidIpAddress() alongside $request->ip() and $request->server('REMOTE_ADDR') when they disagree. A mismatch almost always means a trusted-proxy entry is missing (so Whip never steps past the LB) or a trusted header is missing (so Whip returns false).How can I help you explore Laravel packages today?