robertboes/laravel-cloudflare-proxies
Laravel package for handling Cloudflare proxy IPs so your app sees the real visitor IP. It helps configure trusted proxies and resolve client addresses correctly behind Cloudflare, keeping rate limiting, logging, and security checks accurate.
This is a pre-1.0 package, so pin the version ("vendor/package": "0.1.*") and read the changelog on each bump — patch releases have already changed runtime behavior, not just internals.
composer require the package, then publish its config:
php artisan vendor:publish --tag=<package>-config
refresh — it controls whether the subnet/range list used to classify an address is rebuilt at runtime instead of taken as-is. Leave it off until you understand what it does; the entire v0.1.1 change only applies when it is on.composer show symfony/http-foundation
Laravel 12 pins Symfony 7.2, whose IpUtils::PRIVATE_SUBNETS does not contain carrier-grade NAT or the documentation ranges. The package now supplies those itself.// pseudo-usage: the package exposes a boolean check on an IP
if ($checker->isPublic($request->ip())) {
// treat as a real, routable internet client
}
Middleware gate for internal-only routes. The most common integration is a small middleware that aborts unless the resolved client IP is non-public. Keep it as a separate middleware rather than inlining the check, so you can register it per-route-group and test it in isolation.
public function handle($request, Closure $next)
{
abort_unless($this->checker->isPublic($request->ip()), 404);
return $next($request);
}
Resolve once per request, not per call. The list being matched against is instance state; if you inject the checker into several services, bind it as a singleton in a service provider so a single refresh populates one list.
Pair it with Laravel's own trusted-proxy handling. $request->ip() already reflects TrustProxies; if you also read X-Forwarded-For yourself you will classify a different address than the one the package sees. Decide which address is authoritative (usually the one after proxy resolution) and never parse headers by hand.
Refresh lifecycle. If refresh is on, treat it as a cache-warming concern: it is cheap in a normal FPM request that does one check, but in Octane or a queue worker the process is long-lived, so decide deliberately whether you want a per-request refresh (fresh, slightly wasteful) or a scheduled refresh via a small job/command. Warm it in a bootstrapping path so the first user request is not the one paying for it.
Form requests and validation rules. Wrap the check in a Rule object when the outcome should produce a validation error rather than a 403 — e.g. rejecting a user-supplied "callback URL" or "webhook target" that points at a private address.
Test both sides of the boundary. Use a data provider with one address from each category — RFC1918, loopback, link-local, CGNAT, a documentation range, and a genuinely public address — and assert on the same set after upgrading, because those categories are exactly what v0.1.1 altered.
CGNAT is now refused, and that includes Tailscale. 100.64.0.0/10 is carrier-grade NAT and the range Tailscale hands out. Any developer or CI runner on a Tailscale tailnet will now be classified as non-public. If your middleware treats non-public as "internal, allow", that is fine; if it treats it as "untrusted, deny", tailnet traffic silently starts failing. Audit which direction your check runs before deploying v0.1.1.
Documentation ranges are now refused too. 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 and 2001:db8::/32 are the classic test-fixture addresses. Tests that previously asserted "public" for 203.0.113.10 will flip. Update fixtures rather than reverting the package — the new behavior is the correct one.
Do not call IpUtils::PRIVATE_SUBNETS directly in your own code. The constant's contents vary across Symfony minors, which is the exact bug v0.1.1 fixed internally. If you need the list, ask the package, or define your own explicit constant — otherwise you reintroduce the same Laravel-12-vs-newer-Symfony drift.
Scope the blast radius when triaging. The release note is explicit: "Only matters with refresh on." If something broke after upgrading, first confirm the flag:
php artisan config:show <package> # or grep the config file
and check whether config:cache is serving a stale value from before the upgrade.
config:cache and runtime refresh interact badly. A cached config is fine, but if refresh reads from config at boot and the app is deployed with a stale cache, you will debug a list that no longer matches the code. Always php artisan config:clear (or re-cache) after the upgrade.
Debugging a surprising verdict. Log the classified address and the matched range, not just the boolean. Without the matched range you cannot tell a genuine public IP from one that fell into a newly-refused block. Remember to log $request->ip() post-proxy-resolution so the logged value matches what was classified.
Upgrade note for v0.1.1. No API change, no deprecation — a pure behavioral fix for Symfony version drift (Laravel 12 ships Symfony 7.2, whose PRIVATE_SUBNETS omits CGNAT and the documentation ranges; the package now supplements it on every version). Safe to take as a patch, but re-run your address-category tests and grep for hard-coded 100.64. / 192.0.2. / 203.0.113. fixtures.
Extension point. The classification list is the natural place to add project-specific ranges (VPN CIDR, office egress, a partner's reserved block). Prefer extending through the package's config over subclassing the checker, so future Symfony-constant fixes land underneath your additions rather than conflicting with them.
How can I help you explore Laravel packages today?