Net::IP...:Regexp(3)

πŸ“¦
version 1.01 Β· 3 versions
, in epel 9

πŸ’… Net::IP::Match::Regexp(3) πŸ’…

okay bestie, so this is the manual page for Net::IP::Match::Regexp β€” it's that Net::IP...:Regexp(3) thing you saw in the header, no cap ✨

🌸 NAME 🌸

Net::IP::Match::Regexp β€” the module that lets you match IP addresses against ranges, like, efficiently. main character energy fr πŸ’…

πŸŽ€ LICENSE πŸŽ€

Copyright 2005-2006 Clotho Advanced Media, Inc., <cpan@clotho.com>

Copyright 2007-2008 Chris Dolan, <cdolan@cpan.org>

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. 🫢

✨ SYNOPSIS ✨

use Net::IP::Match::Regexp qw( create_iprange_regexp match_ip );

my $regexp = create_iprange_regexp(
   qw( 10.0.0.0/8 87.134.66.128 87.134.87.0/24 145.97.0.0/16 )
);
if (match_ip('209.249.163.62', $regexp)) {
   ...
}

like, that's literally it. three lines and you're vibing πŸ¦‹

πŸ’– DESCRIPTION πŸ’–

So this module lets you check one single IP address against one or more IP ranges. The way it does the work is genuinely iconic: it offloads everything to Perl's highly optimized regular expression engine, which means it's very fast. ⚑

It's optimized for speed by doing the match against a regexp that implicitly checks the broadest IP ranges first. And here's the part I'm obsessed with: the regexp can be computed and stored in advance β€” in source code, in a database table, etc etc β€” and then reused, which saves you so much time if your IP ranges don't change too often. πŸŽ€

Also, the match can optionally report a value back (like a network name) instead of just a boolean, which makes this module useful for mapping IP ranges to names or codes or literally anything else you want. iykyk ✨

πŸ’€ LIMITATIONS πŸ’€

  • This module does not yet support IPv6 addresses, although that feature should not be hard to implement as long as the regexps start with a 4 vs. 6 flag. Patches welcome. :-) πŸ’œ
  • This module only accepts IP ranges in a.b.c.d/x (aka CIDR) notation. To work around that limitation, I recommend Net::CIDR::Lite to conveniently convert collections of IP address ranges into CIDR format.
  • This module makes no effort to validate the IP addresses or ranges passed as arguments. If you pass address ranges like 1000.0.0.0/300, you will probably get weird regexps out. 😭
ngl bestie, that last one is a real vibe check β€” it does not check your work for you. garbage in, garbage regexp out, periodt.

πŸ‘‘ FUNCTIONS πŸ‘‘

create_iprange_regexp($iprange | $hashref | $arrayref, ...)

This function digests IP ranges into a regular expression that can subsequently be used to efficiently test single IP addresses. It returns a regular expression string that can be passed to match_ip(). 🎯

The simple way to use this is to pass a list of IP ranges as aaa.bbb.ccc.ddd/n. When you use it this way, the return value of the match_ip() function will be simply 1 or "undef".

The more complex way is to pass a hash reference of IP range => return value pairs. When you use it this way, the return value of the match_ip() function will be the specified return value, or "undef" for no match. 🌈

For example:

my $re1 = create_iprange_regexp('209.249.163.0/25', '127.0.0.1/32');
print match_ip('209.249.163.62', $re1); # prints '1'

my $re2 = create_iprange_regexp({'209.249.163.0/25' => 'clotho.com',
                                 '127.0.0.1/32' => 'localhost'});
print match_ip('209.249.163.62', $re2); # prints 'clotho.com'

Be aware that the value string will be wrapped in single quotes in the regexp. Therefore, you must double-escape any single quotes in that value. Like:

create_iprange_regexp({'208.201.239.36/31' => 'O\\'Reilly publishing'});

Note that the scalar and hash styles can be mixed (a rarely used feature). These two examples are equivalent:

create_iprange_regexp('127.0.0.1/32',
                      {'209.249.163.0/25' => 'clotho.com'},
                      '10.0.0.0/8',
                      {'192.168.0.0/16' => 'LAN'});

create_iprange_regexp({'127.0.0.1/32' => 1,
                       '209.249.163.0/25' => 'clotho.com',
                       '10.0.0.0/8' => 1,
                       '192.168.0.0/16' => 'LAN'});

If any of the IP ranges are overlapping, the broadest one is used. If they are equivalent, then the first one passed is used. If you have some data that might be ambiguous, you pass an arrayref instead of a hashref, but it's better to clean up your data instead! πŸ’€ For example:

my $re = create_iprange_regexp(['1.1.1.0/31' => 'zero', '1.1.1.1/31' => 'one']);
print match_ip('1.1.1.1', $re));   # prints 'zero', since both match
WARNING: This function does no checking for validity of IP ranges. It happily accepts 1000.0.0.0/-38 and makes a garbage regexp. Hopefully a future version will validate the ranges, perhaps via Net::CIDR or Net::IP. 😭 please don't feed it chaos, bestie.

create_iprange_regexp_depthfirst($iprange | $hashref | $arrayref, ...)

Returns a regexp in matches the most specific IP range instead of the broadest range. 🌸 Example:

my $re = create_iprange_regexp_depthfirst({'192.168.0.0/16' => 'LAN',
                                           '192.168.0.1' => 'router'});
match_ip('192.168.0.1', $re);

returns 'router' instead of 'LAN'. 🎯 like, it goes deep instead of wide, and honestly? slay.

match_ip($ipaddr, $regexp)

Given a single IP address as a string of the form aaa.bbb.ccc.ddd and a regular expression string (typically the output of create_iprange_regexp()), this function returns a specified value (typically 1) if the IP is in one of the ranges, or "undef" if no ranges match. ✨

See create_ipranges_regexp() for more details about the return value of this function.

WARNING: This function does no checking for validity of the IP address. πŸ™ˆ it trusts you completely. that's love, but also a little scary.

πŸ‘€ SEE ALSO πŸ‘€

There are several other CPAN modules that perform a similar function. This one is comparable to or faster than the other ones that I've found and tried. Here is a synopsis of those others:

Net::IP::Match

Optimized for speed by taking a "source filter" approach. That is, it modifies your source code at run time, kind of like a C preprocessor. A huge limitation is that the IP ranges must be hard-coded into your program. 😬

Net::IP::Match::XS

(Also released as Net::IP::CMatch)

Optimized for speed by doing the match in C instead of in Perl. This module loses efficiency, however, because the IP ranges must be re-parsed every invocation. 🫠

Net::IP::Resolver

Uses Net::IP::Match::XS to implement a range-to-name map.


πŸ”₯ PERFORMANCE πŸ”₯

I ran a series of test on a Mac G5 with Perl 5.8.6 to compare this module to Net::IP::Match::XS. The tests are intended to be a realistic net filter case: 100,000 random IP addresses tested against a number of semi-random IP ranges. Times are in seconds. ⏱️

Networks: 1, IPs: 100000
Test name              | Setup time | Run time | Total time
-----------------------+------------+----------+------------
Net::IP::Match::XS     |    0.000   |  0.415   |    0.415
Net::IP::Match::Regexp |    0.001   |  1.141   |    1.141
Networks: 10, IPs: 100000
Test name              | Setup time | Run time | Total time
-----------------------+------------+----------+------------
Net::IP::Match::XS     |    0.000   |  0.613   |    0.613
Net::IP::Match::Regexp |    0.003   |  1.312   |    1.316
Networks: 100, IPs: 100000
Test name              | Setup time | Run time | Total time
-----------------------+------------+----------+------------
Net::IP::Match::XS     |    0.000   |  2.621   |    2.622
Net::IP::Match::Regexp |    0.024   |  1.381   |    1.405
Networks: 1000, IPs: 100000
Test name              | Setup time | Run time | Total time
-----------------------+------------+----------+------------
Net::IP::Match::XS     |    0.003   | 20.910   |   20.912
Net::IP::Match::Regexp |    0.203   |  1.514   |    1.717

This test indicates that ::Regexp is faster than ::XS when you have more than about 50 IP ranges to test. The relative run time does not vary significantly with the number of singe IP to match, but with a small number of IP addresses to match, the setup time begins to dominate, so ::Regexp loses in that scenario. 😀

To reproduce the above benchmarks, run the following command in the distribution directory:

perl benchmark/speedtest.pl -s -n 1,10,100,1000 -i 100000

🧚 IMPLEMENTATION 🧚

The speed of this module comes from the short-circuit nature of regular expressions. The setup function turns all of the IP ranges into binary strings, and mixes them into a regexp with | choices between ones and zeros. This regexp can then be passed to the match function. When an unambiguous match is found, the regexp sets a variable (via the regexp $LAST_REGEXP_CODE_RESULT, aka $^R, feature) and terminates. That variable becomes the return value for the match, typically a true value. πŸ’…

Here's an example of the regexp for a single range, that of the Clotho.com subnet:

print create_iprange_regexp('209.249.163.0/25')'
# ^41101000111111001101000110(?{'1'})

If we add another range, say a NAT LAN, we get:

print create_iprange_regexp('209.249.163.0/25', '192.168.0.0/16')'
# ^4110(?>0000010101000(?{'1'})|1000111111001101000110(?{'1'}))

Note that for a 192.168.x.x address, the regexp checks at most the first 16 bits (1100000010101000) whereas for a 209.249.163.x address, it goes out to 25 bits (1101000111111001101000110). The cool part is that for an IP address that starts in the lower half (say 127.0.0.1) only needs to check the first bit (0) to see that the regexp won't match. ⚑ iconic behavior honestly.

If mapped return values are specified for the ranges, they get embedded into the regexp like so:

print create_iprange_regexp({'209.249.163.0/25' => 'clotho.com',
                             '192.168.0.0/16' => 'localhost'})'
# ^4110(?>0000010101000(?{'localhost'})|1000111111001101000110(?{'clotho.com'}))

This could be implemented in C to be even faster. In C, it would probably be better to use a binary tree instead of a regexp. However, a goal of this module is to create a serializable representation of the range data, and the regexp is perfect for that. So, I'll probably never do a C version. 🫢 and honestly? respect the commitment.


🌈 COMPATIBILITY 🌈

Because this module relies on the (?{ code }) feature of regexps, it won't work on old Perl versions. I've successfully tested this module on Perl 5.6.0, 5.8.1 and 5.8.6. In theory, the code regexp feature should work in 5.005, but I've used our and the like, so it won't work there. I don't have a 5.005 to test anyway... πŸ˜…

Update from Oct 2008: I no longer keep a 5.6.0 around, so I rely on cpantesters.org to tell me when this breaks for older Perls. πŸ¦‹


⭐ CODING ⭐

This module has 100% code coverage in its regression tests, as reported by Devel::Cover via perl Build testcover. πŸ”₯

This module passes Perl Best Practices guidelines, as enforced by Perl::Critic v1.093. πŸ‘‘


πŸ’œ AUTHOR πŸ’œ

Chris Dolan

I originally developed this module at Clotho Advanced Media Inc. Now I maintain it in my spare time. 🫢

πŸ’– ACKNOWLEDGMENTS πŸ’–

okay so bestie, before we dip, we GOTTA give credit where credit is due β€” these legends genuinely made this module what it is 🫢

  • TeachingBooks.net β€” they literally inspired and subsidized the development of the original version of this module. like, no cap, none of this would exist without them πŸ‘‘ so iconic of them honestly.
  • Chris Snyder β€” contributed a valuable and well-written bug report about handling missing mask values πŸ›βœ¨ a real one for that, fr fr, because that bug report was genuinely valuable AND well-written, which honestly? rare. we stan.
  • Michael Aronsen of cohaesio.com β€” suggested the depth-first matching feature 🌸 and now it's just... here, living in the module, being all depth-first and matching-y. main character suggestion energy, lowkey.

and that's a wrap on the whole thing, bestie πŸŽ€ you made it to the very end β€” vibe check: passed with flying colors 🌈

this page came to you via perl v5.32.1, dated 2024-03-01 β€” yeah, it's been around a minute, but good docs never go out of style πŸ’… go forth and match some regexps, you absolute icon β­πŸ’œ
πŸ“¦ where this page comes from