Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ip Lib Laravel Package

mlocati/ip-lib

A PHP library to parse, match, and manipulate IPv4 and IPv6 addresses, CIDR subnets, and address ranges, with support for wildcards, interval arithmetic, and network containment checks—useful for validation, access control, and IP math in any PHP project.

View on GitHub
Deep Wiki
Context7

Getting Started

mlocati/ip-lib is a dependency-free, framework-agnostic IP parsing/value library. There is no service provider, facade, config file or migration to publish — installation is just:

composer require mlocati/ip-lib

Pin at least ^1.22.1 — that patch release fixes several parsing/formatting bugs (Subnet::asPattern() end addresses, IPv6 Pattern::toString() with IPv4-mapped prefixes, IPv4-mapped range typing) that you do not want to inherit.

Then import the factory and parse:

use IPLib\Factory;

$ip    = Factory::parseAddressString(trim($request->ip())); // AddressInterface|null
$range = Factory::parseRangeString('10.0.0.0/8');            // RangeInterface|null

if ($ip !== null && $range !== null && $range->contains($ip)) {
    // allowed
}

Where to look first:

  1. IPLib\Factory — the only entry point you normally need (parseAddressString, parseRangeString, rangeFromBoundaries).
  2. IPLib\Address\AddressInterface — toString(), getVersion(), toBytes().
  3. IPLib\Range\RangeInterface — contains(), containsRange(), getStartAddress(), getEndAddress(), getSize().
  4. IPLib\ParseStringFlag — bit flags such as IPV4_AS_V6 and ALLOW_LEADING_ZEROS for unusual input.

First realistic use case: an allow-list of CIDRs for an admin panel or tenant, validated with a custom Laravel rule instead of loose ip/ipv4 validation:

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use IPLib\Factory;

class IpInRanges implements ValidationRule
{
    public function __construct(private array $cidrs) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $ip = Factory::parseAddressString(trim((string) $value));
        if ($ip === null) {
            $fail("The {$attribute} must be a valid IP address.");
            return;
        }

        foreach ($this->cidrs as $cidr) {
            $range = Factory::parseRangeString($cidr);
            if ($range !== null && $range->contains($ip)) {
                return;
            }
        }

        $fail("The {$attribute} is not in an allowed network.");
    }
}

This immediately replaces hand-rolled ip2long/inet_pton substring math and handles IPv6 on the same code path. Note the trim(): since 1.22.1 the parser rejects trailing whitespace and trailing text, so form/textarea input must be cleaned before parsing.

Implementation Patterns

Normalize on the way in. Parse user input once, store $ip->toString() (canonical form), and never compare raw strings later. Keep the original input in an audit column if the exact representation matters. Trim first — parsing is intentionally strict now, and "10.0.0.1\n" is null, not an address.

Containment middleware. Restrict routes by network at the request boundary:

public function handle(Request $request, Closure $next)
{
    $ip = Factory::parseAddressString((string) $request->ip());
    $allowed = collect(config('access.admin_cidrs'))
        ->map(fn ($c) => Factory::parseRangeString(trim($c)))
        ->filter();

    abort_unless($ip && $allowed->contains(fn ($r) => $r->contains($ip)), 403);

    return $next($request);
}

Memoize the parsed config ranges in a singleton or with Laravel's once() helper so you parse CIDRs once per request, not on every middleware call.

Range overlap checks. For blocking lists, audit windows or tenant boundaries, compare boundaries rather than iterating addresses:

$overlaps = $rangeA->containsRange($rangeB) || $rangeB->containsRange($rangeA);

For arbitrary ranges you can also compare start/end addresses directly using getStartAddress() / getEndAddress(), but remember addresses of different families are not comparable as raw bytes — and as of 1.22.1 a range parsed from an IPv4-mapped IPv6 string may resolve to an IPv4 range, so normalise via toString()/toBytes() before persisting rather than assuming the object type from the input string.

Database-backed range lookup. The library is PHP-side only; it won't help a WHERE clause. The common pattern is to compute boundaries in PHP and store them:

  • Store ip_version, ip_start, ip_end as VARBINARY(16) (pad IPv4 to 16 bytes, or keep a separate 4-byte column).
  • Insert with $range->getStartAddress()->toBytes() / $range->getEndAddress()->toBytes().
  • Query with WHERE ip_version = ? AND ip_start <= ? AND ip_end >= ? using $ip->toBytes().
  • Add a composite index on (ip_version, ip_start, ip_end).

This keeps containment O(log n) at scale instead of loading every row into PHP. Decide early whether IPv4-mapped input (::ffff:192.0.2.1) is stored in the v4 or v6 column — 1.22.1 made mapped ranges report as IPv4, so mixed historical rows are a real risk during upgrades.

Subnet-keyed rate limiting. Group clients by network for throttling or abuse detection:

$subnet = Factory::parseRangeString($request->ip() . '/24'); // or tenant-supplied prefix
$key = $subnet?->getStartAddress()->toString() ?? $request->ip();
RateLimiter::hit("api:{$key}");

Model cast / mutator. Treat IPs as value objects at the edge and strings in the database:

protected function ipAddress(): Attribute
{
    return Attribute::make(
        set: fn ($v) => Factory::parseAddressString(trim((string) $v))?->toString() ?? $v,
    );
}

Config-driven policy objects. Put CIDR lists in config/networks.php and resolve a small NetworkPolicy class from the container that parses them once and exposes allows(AddressInterface $ip), allowsRange(RangeInterface $range), and overlaps(RangeInterface $range). This keeps controllers free of Factory calls and makes the policy unit-testable without HTTP.

IPv4-mapped IPv6 handling. Proxy chains frequently yield ::ffff:192.0.2.1. If your allow-list is IPv4-only, explicitly normalize or parse with ParseStringFlag::IPV4_AS_V6 / reject mapped forms, otherwise contains() will correctly return false and confuse you in staging. Assert getVersion() on both the address and the range before comparing them, since the range and the address produced from the same textual value can now report different versions.

Pattern generation. If you emit wildcard patterns (192.168.*.*) back to users, regenerate them from the range object rather than string-munging, and re-parse before storing. Subnet::asPattern() had an incorrect end address before 1.22.1, so verify any patterns persisted by older versions round-trip to the same range.

Gotchas and Tips

  • Everything returns null, not exceptions. parseAddressString, parseRangeString and rangeFromBoundaries fail soft. Always null-check; a chained ->contains() on null is a 500. If you prefer exceptions, wrap parsing in a small helper that throws a domain exception.
  • Trailing whitespace/newlines now fail parsing (1.22.1). A value copied from a textarea, .env file or CSV with a trailing \n used to be tolerated and now returns null. trim() at every boundary — middleware, validation rules, casts, config loaders — and consider TrimStrings coverage for non-request input paths.
  • Trailing garbage is rejected too. IPv6 reverse-DNS (*.ip6.arpa) parsing no longer accepts trailing text after the name. Still, never trust partially-matched input: validate the whole string via parseAddressString rather than str_contains-style prefix checks.
  • Don't compare toString() output across families. 1.2.3.4 and ::ffff:1.2.3.4 are different address objects. Normalize family before storing or comparing, and store a version discriminator alongside binary columns.
  • Mapped ranges may not be IPv6 (1.22.1). IPv4-mapped IPv6 ranges now use the IPv4 range type, so getVersion(), toBytes() length and getStartAddress()->toString() can differ from what you got in ≤1.22.0. Audit any code or stored rows that assumed ::ffff:a.b.c.d/120 was 16 bytes wide; write a migration/test asserting the version for these inputs if you persisted boundaries.
  • toBytes() lengths differ: 4 bytes for IPv4, 16 for IPv6. Raw binary comparisons between them are meaningless. Either map IPv4 into IPv6 space consistently or compare only within the same getVersion().
  • getSize() can exceed PHP int on large IPv6 ranges. Full ::/0 or big prefixes may return a string/float. Don't assume int; don't use it for loop bounds.
  • Non-canonical input is rejected by default. Leading zeros (010.0.0.1), short IPv6 forms, and some legacy notations return null unless you pass the relevant ParseStringFlag. Decide deliberately whether to accept quirky input; accepting it can be an SSRF/allow-list bypass vector if one code path parses and another doesn't.
  • Range syntax is broader than CIDR. parseRangeString accepts subnets (10.0.0.0/8), explicit ranges (10.0.0.1-10.0.0.50), and wildcard patterns (192.168.*.*). Validate the kind you expect if the input is user-supplied; an explicit range is still a valid range object.
  • rangeFromBoundaries() can return null. Mixed families or reversed boundaries fail. Check the result and assert getVersion() matches what you intended.
  • Proxy IPs. $request->ip() returns the socket address unless TrustProxies is configured correctly. X-Forwarded-For is client-spoofable; only trust known load balancers. IP allow-list middleware is only as good as the IP source.
  • No DNS, GeoIP, ASN, or reverse lookup. The package is pure address/range math. Don't expect hostname resolution or country data; layer those separately. (Reverse-DNS name parsing exists but is format validation only — it performs no query.)
  • No query-builder integration. Eloquent/DB has no whereIpInRange. Either filter in PHP (fine for small sets, dangerous for large tables) or compute binary boundaries and index them as described above. Test MySQL/MariaDB INET6_ATON behavior for IPv4 vs IPv6 explicitly — mixed-length results are a classic source of silently failing range queries.
  • Caching parsed ranges across requests. Parse cost is low, but if you cache, cache the string and re-parse, or verify serialization behavior for your installed version rather than assuming object cache round-trips are stable across upgrades. Patch releases in this package change object behaviour, not just bug internals.
  • No Laravel auto-discovery. Keep Factory usage behind a small service/policy class so a future swap (e.g. to Symfony's IP utils or a DB-side solution) is a one-file change.
  • Extension points are limited by design. Value objects are immutable; if you need custom behavior, compose around AddressInterface/RangeInterface rather than extending the concrete classes, and pin the version — the older Factory::addressFromString()/rangeFromString() API was deprecated in favor of the parse* methods, so avoid copying old snippets.
  • Testing tip: Factory::parseRangeString('127.0.0.1/32') and '::1/128' make convenient fixtures. Assert both the accepted case and the null case for malformed input — including "1.2.3.4\n" and "1.2.3.4 ", which are the most common production failure mode after 1.22.1. Add a regression test for ::ffff:x.x.x.x mappings so a future patch can't silently flip your getVersion()/byte-length assumptions.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager