# Copyright (C) 1998-2006 David Muir Sharnoff <muir@idiom.org>
# Copyright (C) 2011-2013 Google, Inc.
# Copyright (C) 2018-2021 Joelle Maslak <jmaslak@antelope.net>
package Net::Netmask;
$Net::Netmask::VERSION = '2.0001';
use 5.006_001;
# ABSTRACT: Understand and manipulate IP netmasks
# Disable one-arg bless to preserve the existing interface.
## no critic (ClassHierarchies::ProhibitOneArgBless)
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(findNetblock findOuterNetblock findAllNetblock
cidrs2contiglists range2cidrlist sort_by_ip_address
dumpNetworkTable sort_network_blocks cidrs2cidrs
cidrs2inverse);
@EXPORT_OK = (
@EXPORT, qw(ascii2int int2quad quad2int %quadmask2bits
%quadhostmask2bits imask i6mask int2ascii sameblock cmpblocks contains)
);
my $remembered = {};
my %imask2bits;
my %size2bits;
my @imask;
my @i6mask;
our $SHORTNET_DEFAULT = undef;
use vars qw($error $debug %quadmask2bits %quadhostmask2bits);
$debug = 1;
use strict;
use warnings;
use Carp;
use Math::BigInt;
use POSIX qw(floor);
use overload
'""' => \&desc,
'<=>' => \&cmp_net_netmask_block,
'cmp' => \&cmp_net_netmask_block,
'fallback' => 1;
sub new {
my ( $package, $net, @params) = @_;
my $mask = '';
if (@params % 2) {
$mask = shift(@params);
$mask = '' if !defined($mask);
}
my (%options) = @params;
my $shortnet = ( ( exists($options{shortnet}) && $options{shortnet} ) || $SHORTNET_DEFAULT );
my $base;
my $bits;
my $ibase;
my $proto = 'IPv4';
undef $error;
if ( $net =~ m,^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$, ) {
( $base, $bits ) = ( $1, $2 );
} elsif ( $net =~ m,^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)[:/]([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$, ) {
$base = $1;
my $quadmask = $2;
if ( exists $quadmask2bits{$quadmask} ) {
$bits = $quadmask2bits{$quadmask};
} else {
$error = "illegal netmask: $quadmask";
}
} elsif ( $net =~ m,^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)[#]([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$, ) {
$base = $1;
my $hostmask = $2;
if ( exists $quadhostmask2bits{$hostmask} ) {
$bits = $quadhostmask2bits{$hostmask};
} else {
$error = "illegal hostmask: $hostmask";
}
} elsif ( ( $net =~ m,^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$, )
&& ( $mask =~ m,[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$, ) )
{
$base = $net;
if ( exists $quadmask2bits{$mask} ) {
$bits = $quadmask2bits{$mask};
} else {
$error = "illegal netmask: $mask";
}
} elsif ( ( $net =~ m,^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$, )
&& ( $mask =~ m,0x[a-f0-9]+,i ) )
{
$base = $net;
my $imask = hex($mask);
if ( exists $imask2bits{$imask} ) {
$bits = $imask2bits{$imask};
} else {
$error = "illegal netmask: $mask ($imask)";
}
} elsif ( $net =~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && !$mask ) {
( $base, $bits ) = ( $net, 32 );
} elsif ( $net =~ /^[0-9]+\.[0-9]+\.[0-9]+$/ && !$mask && $shortnet ) {
( $base, $bits ) = ( "$net.0", 24 );
} elsif ( $net =~ /^[0-9]+\.[0-9]+$/ && !$mask && $shortnet ) {
( $base, $bits ) = ( "$net.0.0", 16 );
} elsif ( $net =~ /^[0-9]+$/ && !$mask && $shortnet ) {
( $base, $bits ) = ( "$net.0.0.0", 8 );
} elsif ( $net =~ m,^([0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$, && $shortnet ) {
( $base, $bits ) = ( "$1.0", $2 );
} elsif ( $net =~ m,^([0-9]+\.[0-9]+)/([0-9]+)$, && $shortnet ) {
( $base, $bits ) = ( "$1.0.0", $2 );
} elsif ( $net =~ m,^([0-9]+)/([0-9]+)$, && $shortnet ) {
( $base, $bits ) = ( "$1.0.0.0", $2 );
} elsif ( $net eq 'default' || $net eq 'any' ) {
( $base, $bits ) = ( "0.0.0.0", 0 );
} elsif ( $net =~ m,^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s*-\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$, )
{
# whois format
$ibase = quad2int($1);
my $end = quad2int($2);
$error = "illegal dotted quad: $net"
unless defined($ibase) && defined($end);
my $diff = ( $end || 0 ) - ( $ibase || 0 ) + 1;
$bits = $size2bits{$diff};
$error = "could not find exact fit for $net"
if !defined $error
&& ( !defined $bits
|| ( $ibase & ~$imask[$bits] ) );
} elsif ( $net =~ m,^([0-9a-f]*:[0-9a-f]*:[0-9a-f:]*)/([0-9]+)$, ) {
# IPv6 with netmask - ex: 2001:db8::/32
if ( $mask ne '' ) { $error = "mask ignored for IPv6 address" }
( $base, $bits, $proto ) = ( $1, $2, 'IPv6' );
} elsif ( $net =~ m,^([0-9a-f]*:[0-9a-f]*:[0-9a-f:]*)$, ) {
# IPv6 without netmask - ex: 2001:db8::1234
if ( $mask ne '' ) { $error = "mask ignored for IPv6 address" }
( $base, $bits, $proto ) = ( $1, 128, 'IPv6' );
} elsif ( $net eq 'default6' || $net eq 'any6' ) {
if ( $mask ne '' ) { $error = "mask ignored for IPv6 address" }
( $base, $bits, $proto ) = ( "::", 0, 'IPv6' );
} else {
$error = "could not parse $net";
$error .= " $mask" if $mask;
}
carp $error if $error && $debug;
$bits = 0 unless $bits;
if ( ( $proto eq 'IPv4' ) && ( $bits > 32 ) ) {
$error = "illegal number of bits: $bits"
unless $error;
$bits = 32;
} elsif ( ( $proto eq 'IPv6' ) && ( $bits > 128 ) ) {
$error = "illegal number of bits: $bits"
unless $error;
$bits = 128;
}
$ibase = ascii2int( ( $base || '::' ), $proto ) unless (defined $ibase or $error);
unless ( defined($ibase) || defined($error) ) {
$error = "could not parse $net";
$error .= " $mask" if $mask;
}
if ($error) {
$ibase = 0;
$bits = 0;
}
$ibase = i_getnet_addr( $ibase, $bits, $proto );
return bless {
'IBASE' => $ibase,
'BITS' => $bits,
'PROTOCOL' => $proto,
( $error ? ( 'ERROR' => $error ) : () ),
};
}
sub i_getnet_addr {
my ( $ibase, $bits, $proto ) = @_;
if ( !defined($ibase) ) { return; }
if ( $proto eq 'IPv4' ) {
return $ibase & $imask[$bits];
} else {
return $ibase & $i6mask[$bits];
}
}
sub new2 {
goto &safe_new;
}
sub safe_new {
local ($debug) = 0;
my $net = new(@_);
return if $error;
return $net;
}
sub errstr { return $error; }
sub debug { my $this = shift; return ( @_ ? $debug = shift : $debug ) }
sub base { my ($this) = @_; return int2ascii( $this->{IBASE}, $this->{PROTOCOL} ); }
sub bits { my ($this) = @_; return $this->{'BITS'}; }
sub protocol { my ($this) = @_; return $this->{'PROTOCOL'}; }
sub size {
my ($this) = @_;
if ( $this->{PROTOCOL} eq 'IPv4' ) {
return 2**( 32 - $this->{'BITS'} );
} else {
return Math::BigInt->new(2)->bpow( 128 - $this->{'BITS'} );
}
}
sub next { ## no critic: (Subroutines::ProhibitBuiltinHomonyms)
my ($this) = @_;
# TODO: CONSOLIDATE
if ( $this->{PROTOCOL} eq 'IPv4' ) {
return int2quad( $this->{'IBASE'} + $this->size() );
} else {
return $this->_ipv6next( $this->size );
}
}
sub broadcast {
my ($this) = @_;
return int2ascii( $this->{'IBASE'} + $this->size() - 1, $this->{PROTOCOL} );
}
*first = \&base;
*last = \&broadcast;
sub desc {
return int2ascii( $_[0]->{IBASE}, $_[0]->{PROTOCOL} ) . '/' . $_[0]->{BITS};
}
sub imask {
return ( 2**32 - ( 2**( 32 - $_[0] ) ) );
}
sub i6mask {
my $bits = shift;
return Math::BigInt->new(2)->bpow(128) - Math::BigInt->new(2)->bpow( 128 - $bits );
}
sub mask {
my ($this) = @_;
if ( $this->{PROTOCOL} eq 'IPv4' ) {
return int2quad( $imask[ $this->{'BITS'} ] );
} else {
return int2ascii( $i6mask[ $this->{'BITS'} ], $this->{PROTOCOL} );
}
}
sub hostmask {
my ($this) = @_;
if ( $this->{PROTOCOL} eq 'IPv4' ) {
return int2quad( ~$imask[ $this->{BITS} ] );
} else {
return int2ascii( $i6mask[ $this->{BITS} ] ^ $i6mask[128], $this->{PROTOCOL} );
}
}
sub nth {
my ( $this, $index, $bitstep ) = @_;
my $maxbits = $this->{PROTOCOL} eq 'IPv4' ? 32 : 128;
my $size = $this->size();
my $ibase = $this->{'IBASE'};
$bitstep = $maxbits unless $bitstep;
my $increment = 2**( $maxbits - $bitstep );
$index *= $increment;
$index += $size if $index < 0;
return if $index < 0;
return if $index >= $size;
my $i = $ibase + $index;
return int2ascii( $i, $this->{PROTOCOL} );
}
sub enumerate {
my ( $this, $bitstep ) = @_;
my $proto = $this->{PROTOCOL};
# Set default step size by protocol
$bitstep = ( $proto eq 'IPv4' ? 32 : 128 ) unless $bitstep;
my $size = $this->size();
my @ary;
### We should be able to consolidate this
if ( $proto eq 'IPv4' ) {
my $increment = 2**( 32 - $bitstep );
my $ibase = $this->{'IBASE'};
for ( my $i = 0; $i < $size; $i += $increment ) {
push( @ary, int2quad( $ibase + $i ) );
}
} else {
my $increment = Math::BigInt->new(2)->bpow( 128 - $bitstep );
if ( ( $size / $increment ) > 1_000_000_000 ) {
# Let's help the user out and catch really obvious issues.
# Asking for a billion IP addresses is probably one of them.
#
# That said, please contact the author if this number causes
# you issues!
confess("More than 1,000,000,000 results would be returned, dieing");
}
for ( my $i = Math::BigInt->new(0); $i < $size; $i += $increment ) {
push( @ary, $this->_ipv6next($i) );
}
}
return @ary;
}
sub _ipv6next {
my ( $this, $bitstep ) = @_;
my $istart = $this->{IBASE};
my $val = $istart + $bitstep;
return ipv6Cannonical( int2ascii( $val, $this->{PROTOCOL} ) );
}
sub inaddr {
my ($this) = @_;
if ( $this->{PROTOCOL} eq 'IPv4' ) {
return $this->inaddr4();
} else {
return $this->inaddr6();
}
}
sub inaddr4 {
my ($this) = @_;
my $ibase = $this->{'IBASE'};
my $blocks = floor( $this->size() / 256 );
return (
join( '.', unpack( 'xC3', pack( 'V', $ibase ) ) ) . ".in-addr.arpa",
$ibase % 256,
$ibase % 256 + $this->size() - 1
) if $blocks == 0;
my @ary;
for ( my $i = 0; $i < $blocks; $i++ ) {
push( @ary,
join( '.', unpack( 'xC3', pack( 'V', $ibase + $i * 256 ) ) ) . ".in-addr.arpa",
0, 255 );
}
return @ary;
}
sub inaddr6 {
my ($this) = @_;
my (@digits) = split //, $this->{IBASE}->to_hex;
my $static = floor( $this->{BITS} / 4 );
my $len = floor( ( $static + 3 ) / 4 );
my $remainder = $this->{BITS} % 4;
my $blocks = $remainder ? ( 2**( 4 - $remainder ) ) : 1;
my @tail;
if ( !$len ) {
# Specal case: 0 len
return ('ip6.arpa');
}
push @tail, reverse( @digits[ 0 .. ( $static - 1 ) ] ), 'ip6.arpa';
if ( !$remainder ) {
# Special case - at nibble boundary already
return ( join '.', @tail );
}
my $last = hex $digits[$static];
my @ary;
for ( my $i = 0; $i < $blocks; $i++ ) {
push @ary, join( '.', sprintf( "%x", $last ), @tail );
$last++;
}
return @ary;
}
sub tag {
my $this = shift;
my $tag = shift;
my $val = $this->{ 'T' . $tag };
$this->{ 'T' . $tag } = $_[0] if @_;
return $val;
}
sub quad2int {
my @bytes = split( /\./, $_[0] );
return unless @bytes == 4;
return unless !grep { !( /^(([0-9])|([1-9][0-9]*))$/ && $_ < 256 ) } @bytes;
return unpack( "N", pack( "C4", @bytes ) );
}
sub int2quad {
return join( '.', unpack( 'C4', pack( "N", $_[0] ) ) );
}
# Uses the internal "raw" representation (such as IBASE).
# For IPv4, this is an integer
# For IPv6, this is a raw bit string.
sub int2ascii {
if ( $_[1] eq 'IPv4' ) {
return join( '.', unpack( 'C4', pack( "N", $_[0] ) ) );
} elsif ( $_[1] eq 'IPv6' ) {
my $addr = ( ref $_[0] ) ne '' ? $_[0]->to_hex : Math::BigInt->new( $_[0] )->to_hex;
return ipv6Cannonical($addr);
} else {
confess("Incorrect call");
}
}
# Produces the internal "raw" representation (such as IBASE).
# For IPv4, this is an integer
# For IPv6, this is a raw bit string.
sub ascii2int {
if ( $_[1] eq 'IPv4' ) {
return quad2int( $_[0] );
} elsif ( $_[1] eq 'IPv6' ) {
return ipv6ascii2int( $_[0] );
} else {
confess("Incorrect call");
}
}
# Take an IPv6 ASCII address and produce a raw value
sub ipv6ascii2int {
my $addr = shift;
$addr = ipv6NonCompacted($addr);
$addr = join '', map { sprintf( "%04x", hex($_) ) } split( /:/, $addr );
return Math::BigInt->from_hex($addr);
}
# Takes an IPv6 address and produces a standard version seperated by
# colons (without compacting)
sub ipv6NonCompacted {
my $addr = shift;
if ( $addr !~ /:/ ) {
if ( length($addr) < 32 ) {
$addr = ( "0" x ( 32 - length($addr) ) ) . $addr;
}
$addr =~ s/(....)(?=....)/$1:/gsx;
}
# Handle address format with trailing IPv6
# Ex: 0:0:0:0:1.2.3.4
if ( $addr =~ m/^[0-9a-f:]+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/i ) {
my ( $l, $r1, $r2, $r3, $r4 ) =
$addr =~ m/^([0-9a-f:]+)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/i;
$addr = sprintf( "%s%02x%02x:%02x%02x", $l, $r1, $r2, $r3, $r4 );
}
my ( $left, $right ) = split /::/, $addr;
if ( !defined($right) ) { $right = '' }
my (@lparts) = split /:/, $left;
my (@rparts) = split /:/, $right;
# Strip leading 0's & lowercase
@lparts = map { $_ =~ s/^0+([0-9a-f]+)/$1/; lc($_) } @lparts;
@rparts = map { $_ =~ s/^0+([0-9a-f]+)/$1/; lc($_) } @rparts;
# Expand ::
my $missing = 8 - ( @lparts + @rparts );
if ($missing) {
$addr = join ':', @lparts, ( 0, 0, 0, 0, 0, 0, 0, 0 )[ 0 .. $missing - 1 ], @rparts;
} else {
$addr = join ':', @lparts, @rparts;
}
return $addr;
}
# Compacts an IPv6 address (reduces successive :0: runs)
sub ipv6AsciiCompact {
my $addr = shift;
# Compress, per RFC5952
if ( $addr =~ s/^0:0:0:0:0:0:0:0$/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0:0:0:0:0:0:0(:?:|$)/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0:0:0:0:0:0(:?:|$)/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0:0:0:0:0(:?:|$)/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0:0:0:0(:?:|$)/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0:0:0(:?:|$)/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0:0(:?:|$)/::/ ) {
return $addr;
} elsif ( $addr =~ s/(:?^|:)0(:?:|$)/::/ ) {
return $addr;
}
return $addr;
}
# Cannonicalize IPv6 addresses in ascii format
sub ipv6Cannonical {
my $addr = shift;
$addr = ipv6NonCompacted($addr);
$addr = ipv6AsciiCompact($addr);
return $addr;
}
# IPv6 addresses are stored with a leading zero.
sub storeNetblock {
my ( $this, $t ) = @_;
$t = $remembered unless $t;
my $base = $this->{'IBASE'};
if ( $this->{PROTOCOL} eq 'IPv6' ) {
$base = "0$base";
}
$t->{$base} = [] unless exists $t->{$base};
my $mb = maxblock($this);
my $bits = $this->{'BITS'};
my $i = $bits - $mb;
return ( $t->{$base}[$i] = $this );
}
sub deleteNetblock {
my ( $this, $t ) = @_;
$t = $remembered unless $t;
my $base = $this->{'IBASE'};
if ( $this->{PROTOCOL} eq 'IPv6' ) {
$base = "0$base";
}
my $mb = maxblock($this);
my $bits = $this->{'BITS'};
my $i = $bits - $mb;
return unless defined $t->{$base};
undef $t->{$base}->[$i];
for my $x ( @{ $t->{$base} } ) {
return if $x;
}
return delete $t->{$base};
}
sub findNetblock {
my ( $ascii, $t ) = @_;
$t = $remembered unless $t;
my $proto = ( $ascii =~ m/:/ ) ? 'IPv6' : 'IPv4';
my $ip = ascii2int( $ascii, $proto );
return unless defined $ip;
my %done;
my $maxbits = $proto eq 'IPv6' ? 128 : 32;
for ( my $bits = $maxbits; $bits >= 0; $bits-- ) {
my $nb = i_getnet_addr( $ip, $bits, $proto );
if ( $proto eq 'IPv6' ) {
$nb = "0$nb";
}
next unless exists $t->{$nb};
my $mb = imaxblock( $nb, $maxbits, $proto );
next if $done{$mb}++;
my $i = $bits - $mb;
while ( $i >= 0 ) {
return $t->{$nb}->[$i]
if defined $t->{$nb}->[$i];
$i--;
}
}
return;
}
sub findOuterNetblock {
my ( $ipstr, $t ) = @_;
$t = $remembered unless $t;
my $proto;
my $maxbits;
my $ip;
my $len;
if ( ref($ipstr) ) {
$proto = $ipstr->{PROTOCOL};
$maxbits = $proto eq 'IPv4' ? 32 : 128;
$ip = $ipstr->{IBASE};
$len = $ipstr->{BITS};
} else {
$proto = ( $ipstr =~ m/:/ ) ? 'IPv6' : 'IPv4';
$maxbits = $proto eq 'IPv4' ? 32 : 128;
$ip = ascii2int( $ipstr, $proto );
$len = $maxbits;
}
for ( my $bits = 0; $bits <= $len; $bits++ ) {
my $nb = $ip & ( $proto eq 'IPv4' ? $imask[$bits] : $i6mask[$bits] );
if ( $proto eq 'IPv6' ) {
$nb = "0$nb";
}
next unless exists $t->{$nb};
my $mb = imaxblock( $nb, $len, $proto );
my $i = $bits - $mb;
confess "$mb, $bits, $ipstr, $nb" if $i < 0;
confess "$mb, $bits, $ipstr, $nb" if $i > $maxbits;
while ( $i >= 0 ) {
return $t->{$nb}->[$i]
if defined $t->{$nb}->[$i];
$i--;
}
}
return;
}
sub findAllNetblock {
my ( $ipstr, $t ) = @_;
$t = $remembered unless $t;
my $proto = ( $ipstr =~ m/:/ ) ? 'IPv6' : 'IPv4';
my $maxbits = $proto eq 'IPv4' ? 32 : 128;
my $ip = ascii2int( $ipstr, $proto );
my %done;
my @ary;
for ( my $bits = $maxbits; $bits >= 0; $bits-- ) {
my $nb = $ip & ( $proto eq 'IPv4' ? $imask[$bits] : $i6mask[$bits] );
if ( $proto eq 'IPv6' ) {
$nb = "0$nb";
}
next unless exists $t->{$nb};
my $mb = imaxblock( $nb, $maxbits, $proto );
next if $done{$mb}++;
my $i = $bits - $mb;
confess "$mb, $bits, $ipstr, $nb" if $i < 0;
confess "$mb, $bits, $ipstr, $nb" if $i > $maxbits;
while ( $i >= 0 ) {
push( @ary, $t->{$nb}->[$i] )
if defined $t->{$nb}->[$i];
$i--;
}
}
return @ary;
}
sub dumpNetworkTable {
my ($t) = @_;
$t = $remembered unless $t;
my @ary;
foreach my $base ( keys %$t ) {
push @ary, grep { defined($_) } @{ $t->{base} };
for my $x ( @{ $t->{$base} } ) {
push( @ary, $x )
if defined $x;
}
}
return ( sort @ary );
}
sub checkNetblock {
my ( $this, $t ) = @_;
$t = $remembered unless $t;
my $base = $this->{'IBASE'};
my $mb = maxblock($this);
my $bits = $this->{'BITS'};
my $i = $bits - $mb;
return defined $t->{$base}->[$i];
}
sub match {
my ( $this, $ip ) = @_;
my $proto = $this->{PROTOCOL};
# Two different protocols: return undef
if ( $ip =~ /:/ ) {
if ( $proto ne 'IPv6' ) { return }
} else {
if ( $proto ne 'IPv4' ) { return }
}
my $i = ascii2int( $ip, $this->{PROTOCOL} );
my $ia = i_getnet_addr( $i, $this->{BITS}, $proto );
if ( $proto eq 'IPv4' ) {
if ( $ia == $this->{IBASE} ) {
return ( ( $i & ~( $this->{IBASE} ) ) || "0 " );
} else {
return 0;
}
} else {
if ( $ia == $this->{IBASE} ) {
return ( ( $i - $this->{IBASE} ) || "0 " );
} else {
return 0;
}
}
}
sub maxblock {
my ($this) = @_;
return ( !defined $this->{ERROR} )
? imaxblock( $this->{IBASE}, $this->{BITS}, $this->{PROTOCOL} )
: undef;
}
sub nextblock {
my ( $this, $index ) = @_;
$index = 1 unless defined $index;
my $ibase = $this->{IBASE};
if ( $this->{PROTOCOL} eq 'IPv4' ) {
$ibase += $index * 2**( 32 - $this->{BITS} );
} else {
$ibase += $index * Math::BigInt->new(2)->bpow( 128 - $this->{BITS} );
}
my $newblock = bless {
IBASE => $ibase,
BITS => $this->{BITS},
PROTOCOL => $this->{PROTOCOL},
};
if ( $this->{PROTOCOL} eq 'IPv4' ) {
return if $newblock->{IBASE} >= 2**32;
} else {
return if $newblock->{IBASE} >= Math::BigInt->new(2)->bpow(128);
}
return if $newblock->{IBASE} < 0;
return $newblock;
}
sub imaxblock {
my ( $ibase, $tbit, $proto ) = @_;
confess unless defined $ibase;
if ( !defined($proto) ) { $proto = 'IPv4'; }
while ( $tbit > 0 ) {
my $ia = i_getnet_addr( $ibase, $tbit - 1, $proto );
last if ( $ia != $ibase );
$tbit--;
}
return $tbit;
}
sub range2cidrlist {
my ( $startip, $endip ) = @_;
my $proto;
if ( $startip =~ m/:/ ) {
if ( $endip =~ m/:/ ) { $proto = 'IPv6'; }
} else {
if ( $endip !~ m/:/ ) { $proto = 'IPv4'; }
}
if ( !defined($proto) ) { confess("Cannot mix IPv4 and IPv6 in range2cidrlist()"); }
my $start = ascii2int( $startip, $proto );
my $end = ascii2int( $endip, $proto );
( $start, $end ) = ( $end, $start )
if $start > $end;
return irange2cidrlist( $start, $end, $proto );
}
sub irange2cidrlist {
my ( $start, $end, $proto ) = @_;
if ( !defined($proto) ) { $proto = 'IPv4' }
my $bits = $proto eq 'IPv4' ? 32 : 128;
my @result;
while ( $end >= $start ) {
my $maxsize = imaxblock( $start, $bits, $proto );
my $maxdiff;
if ( $proto eq 'IPv4' ) {
$maxdiff = $bits - _log2( $end - $start + 1 );
} else {
$maxdiff = $bits - ( $end - $start + 1 )->blog(2);
}
$maxsize = $maxdiff if $maxsize < $maxdiff;
push(
@result,
bless {
'IBASE' => $start,
'BITS' => $maxsize,
'PROTOCOL' => $proto,
}
);
if ( $proto eq 'IPv4' ) {
$start += 2**( 32 - $maxsize );
} else {
$start += Math::BigInt->new(2)->bpow( $bits - $maxsize );
}
}
return @result;
}
sub cidrs2contiglists {
my (@cidrs) = sort_network_blocks(@_);
my @result;
while (@cidrs) {
my (@r) = shift(@cidrs);
my $max = $r[0]->{IBASE} + $r[0]->size;
while ( $cidrs[0] && $cidrs[0]->{IBASE} <= $max ) {
my $nm = $cidrs[0]->{IBASE} + $cidrs[0]->size;
$max = $nm if $nm > $max;
push( @r, shift(@cidrs) );
}
push( @result, [@r] );
}
return @result;
}
sub cidrs2cidrs {
my (@cidrs) = sort_network_blocks(@_);
my @result;
my $proto;
if ( scalar(@cidrs) ) {
$proto = $cidrs[0]->{PROTOCOL};
if ( grep { $proto ne $_->{PROTOCOL} } @cidrs ) {
confess("Cannot call cidrs2cidrs with mixed protocol arguments");
}
}
while (@cidrs) {
my (@r) = shift(@cidrs);
my $max = $r[0]->{IBASE} + $r[0]->size;
while ( $cidrs[0] && $cidrs[0]->{IBASE} <= $max ) {
my $nm = $cidrs[0]->{IBASE} + $cidrs[0]->size;
$max = $nm if $nm > $max;
push( @r, shift(@cidrs) );
}
my $start = $r[0]->{IBASE};
my $end = $max - 1;
push( @result, irange2cidrlist( $start, $end, $proto ) );
}
return @result;
}
sub cidrs2inverse {
my $outer = shift;
$outer = __PACKAGE__->new2($outer) || croak($error) unless ref($outer);
# cidrs2cidrs validates that everything is in the same address
# family
my (@cidrs) = cidrs2cidrs(@_);
my $proto;
if ( scalar(@cidrs) ) {
$proto = $cidrs[0]->{PROTOCOL};
}
my $first = $outer->{IBASE};
my $last = $first + $outer->size() - 1;
shift(@cidrs) while $cidrs[0] && $cidrs[0]->{IBASE} + $cidrs[0]->size < $first;
my @r;
while ( @cidrs && $first <= $last ) {
if ( $first < $cidrs[0]->{IBASE} ) {
if ( $last <= $cidrs[0]->{IBASE} - 1 ) {
return ( @r, irange2cidrlist( $first, $last, $proto ) );
}
push( @r, irange2cidrlist( $first, $cidrs[0]->{IBASE} - 1, $proto ) );
}
last if $cidrs[0]->{IBASE} > $last;
$first = $cidrs[0]->{IBASE} + $cidrs[0]->size;
shift(@cidrs);
}
if ( $first <= $last ) {
push( @r, irange2cidrlist( $first, $last, $proto ) );
}
return @r;
}
sub by_net_netmask_block {
return $a->{'IBASE'} <=> $b->{'IBASE'}
|| $a->{'BITS'} <=> $b->{'BITS'};
}
sub sameblock {
return !cmpblocks(@_);
}
sub cmpblocks {
my $this = shift;
my $class = ref $this;
my $other = ( ref $_[0] ) ? shift : $class->new(@_);
return cmp_net_netmask_block( $this, $other );
}
sub contains {
my $this = shift;
my $class = ref $this;
my $other = ( ref $_[0] ) ? shift : $class->new(@_);
return 0 if $this->{IBASE} > $other->{IBASE};
return 0 if $this->{BITS} > $other->{BITS};
return 0 if $other->{IBASE} > $this->{IBASE} + $this->size - 1;
return 1;
}
sub cmp_net_netmask_block {
if ( ( $_[0]->{PROTOCOL} eq 'IPv4' ) && ( $_[1]->{PROTOCOL} eq 'IPv4' ) ) {
# IPv4
return ( $_[0]->{IBASE} <=> $_[1]->{IBASE} || $_[0]->{BITS} <=> $_[1]->{BITS} );
} elsif ( ( $_[0]->{PROTOCOL} eq 'IPv6' ) && ( $_[1]->{PROTOCOL} eq 'IPv6' ) ) {
# IPv6
return ( $_[0]->{IBASE} <=> $_[1]->{IBASE} || $_[0]->{BITS} <=> $_[1]->{BITS} );
} else {
# IPv4 to IPv6, order by protocol
return ( $_[0]->{PROTOCOL} cmp $_[1]->{PROTOCOL} );
}
}
sub sort_network_blocks {
return map { $_->[0] }
sort { $a->[3] cmp $b->[3] || $a->[1] <=> $b->[1] || $a->[2] <=> $b->[2] }
map { [ $_, $_->{IBASE}, $_->{BITS}, $_->{PROTOCOL} ] } @_;
}
sub sort_by_ip_address {
return map { $_->[0] }
sort { $a->[1] cmp $b->[1] }
map { [ $_, pack( "C4", split( /\./, $_ ) ) ] } @_;
}
sub split ## no critic: (Subroutines::ProhibitBuiltinHomonyms)
{
my ( $self, $parts ) = @_;
my $num_ips = $self->size;
confess "Parts must be defined and greater than 0."
unless defined($parts) && $parts > 0;
confess "Netmask only contains $num_ips IPs. Cannot split into $parts."
unless $num_ips >= $parts;
my $log2 = _log2($parts);
confess "Parts count must be a number of base 2. Got: $parts"
unless ( 2**$log2 ) == $parts;
my $new_mask = $self->bits + $log2;
return map { Net::Netmask->new( $_ . "/" . $new_mask ) }
map { $self->nth( ( $num_ips / $parts ) * $_ ) } ( 0 .. ( $parts - 1 ) );
}
# Implement log2 sub routine directly, to avoid precision problems with floor()
# problems with perls built with uselongdouble defined.
# Credit: xenu, on IRC
sub _log2 {
my $n = shift;
my $ret = 0;
$ret++ while ( $n >>= 1 );
return $ret;
}
BEGIN {
for ( my $i = 0; $i <= 32; $i++ ) {
$imask[$i] = imask($i);
$imask2bits{ $imask[$i] } = $i;
$quadmask2bits{ int2quad( $imask[$i] ) } = $i;
$quadhostmask2bits{ int2quad( ~$imask[$i] ) } = $i;
$size2bits{ 2**( 32 - $i ) } = $i;
}
for ( my $i = 0; $i <= 128; $i++ ) {
$i6mask[$i] = i6mask($i);
}
}
1;