| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | #!/usr/bin/perl | 
| 2 |  |  |  |  |  |  |  | 
| 3 |  |  |  |  |  |  | # | 
| 4 |  |  |  |  |  |  | # Copyright (C) 2016-2020 Joelle Maslak | 
| 5 |  |  |  |  |  |  | # All Rights Reserved - See License | 
| 6 |  |  |  |  |  |  | # | 
| 7 |  |  |  |  |  |  |  | 
| 8 |  |  |  |  |  |  | package IP::Random; | 
| 9 |  |  |  |  |  |  | $IP::Random::VERSION = '1.200230'; | 
| 10 |  |  |  |  |  |  | # ABSTRACT: Generate IP Addresses Randomly | 
| 11 |  |  |  |  |  |  |  | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | # Some boilerplate | 
| 14 | 7 |  |  | 7 |  | 1259648 | use v5.20; | 
|  | 7 |  |  |  |  | 47 |  | 
| 15 | 7 |  |  | 7 |  | 31 | use strict; | 
|  | 7 |  |  |  |  | 11 |  | 
|  | 7 |  |  |  |  | 111 |  | 
| 16 | 7 |  |  | 7 |  | 24 | use warnings; | 
|  | 7 |  |  |  |  | 10 |  | 
|  | 7 |  |  |  |  | 163 |  | 
| 17 |  |  |  |  |  |  |  | 
| 18 | 7 |  |  | 7 |  | 31 | use feature 'signatures'; | 
|  | 7 |  |  |  |  | 11 |  | 
|  | 7 |  |  |  |  | 821 |  | 
| 19 | 7 |  |  | 7 |  | 38 | no warnings 'experimental::signatures'; | 
|  | 7 |  |  |  |  | 18 |  | 
|  | 7 |  |  |  |  | 229 |  | 
| 20 |  |  |  |  |  |  |  | 
| 21 | 7 |  |  | 7 |  | 41 | use Carp; | 
|  | 7 |  |  |  |  | 8 |  | 
|  | 7 |  |  |  |  | 415 |  | 
| 22 |  |  |  |  |  |  |  | 
| 23 | 7 |  |  | 7 |  | 36 | use Exporter; | 
|  | 7 |  |  |  |  | 11 |  | 
|  | 7 |  |  |  |  | 471 |  | 
| 24 |  |  |  |  |  |  | @IP::Random::ISA = qw(Exporter); | 
| 25 |  |  |  |  |  |  | @IP::Random::EXPORT_OK = qw(random_ipv4 in_ipv4_subnet default_ipv4_exclude); | 
| 26 |  |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  | # We need a version of List::Util with uniq in it | 
| 28 | 7 |  |  | 7 |  | 41 | use List::Util 1.50 qw(any none notall pairs uniq); | 
|  | 7 |  |  |  |  | 118 |  | 
|  | 7 |  |  |  |  | 469 |  | 
| 29 | 7 |  |  | 7 |  | 3311 | use Socket qw(inet_aton); | 
|  | 7 |  |  |  |  | 21627 |  | 
|  | 7 |  |  |  |  | 8653 |  | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | my $IPV4_EXCLUDE = { | 
| 32 |  |  |  |  |  |  | '0.0.0.0/8'          => [ 'default', 'rfc1122' ], | 
| 33 |  |  |  |  |  |  | '10.0.0.0/8'         => [ 'default', 'rfc1918' ], | 
| 34 |  |  |  |  |  |  | '100.64.0.0/10'      => [ 'default', 'rfc6598' ], | 
| 35 |  |  |  |  |  |  | '127.0.0.0/8'        => [ 'default', 'rfc1122' ], | 
| 36 |  |  |  |  |  |  | '169.254.0.0/16'     => [ 'default', 'rfc3927' ], | 
| 37 |  |  |  |  |  |  | '172.16.0.0/12'      => [ 'default', 'rfc1918' ], | 
| 38 |  |  |  |  |  |  | '192.0.0.0/24'       => [ 'default', 'rfc5736' ], | 
| 39 |  |  |  |  |  |  | '192.0.2.0/24'       => [ 'default', 'rfc5737' ], | 
| 40 |  |  |  |  |  |  | '192.88.99.0/24'     => [ 'default', 'rfc3068' ], | 
| 41 |  |  |  |  |  |  | '192.168.0.0/16'     => [ 'default', 'rfc1918' ], | 
| 42 |  |  |  |  |  |  | '198.18.0.0/15'      => [ 'default', 'rfc2544' ], | 
| 43 |  |  |  |  |  |  | '198.51.100.0/24'    => [ 'default', 'rfc5737' ], | 
| 44 |  |  |  |  |  |  | '203.0.113.0/24'     => [ 'default', 'rfc5737' ], | 
| 45 |  |  |  |  |  |  | '224.0.0.0/4'        => [ 'default', 'rfc3171' ], | 
| 46 |  |  |  |  |  |  | '240.0.0.0/4'        => [ 'default', 'rfc1112' ], | 
| 47 |  |  |  |  |  |  | '255.255.255.255/32' => [ 'default', 'rfc919' ], | 
| 48 |  |  |  |  |  |  | }; | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | # Build cache of valid types | 
| 51 |  |  |  |  |  |  | my %VALID_TYPES = map { $_, 1 } uniq sort map { @$_ } values %$IPV4_EXCLUDE; | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  |  | 
| 54 | 13299 |  |  | 13299 | 1 | 354443 | sub random_ipv4 ( %args ) { | 
|  | 13299 |  |  |  |  | 17130 |  | 
|  | 13299 |  |  |  |  | 13288 |  | 
| 55 | 13299 |  | 100 | 65540 |  | 61160 | $args{rand} //= sub { int( rand( shift() + 1 ) ) }; | 
|  | 65540 |  |  |  |  | 108232 |  | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | # Can't have exclude and additional_types_allowed both existing | 
| 58 | 13299 | 50 | 66 |  |  | 25169 | if ( exists( $args{exclude} ) && exists( $args{additional_types_allowed} ) ) { | 
| 59 | 0 |  |  |  |  | 0 | croak( "Cannot define both 'exclude' and " . "'additional_types_allowed' parameters" ); | 
| 60 |  |  |  |  |  |  | } | 
| 61 |  |  |  |  |  |  |  | 
| 62 |  |  |  |  |  |  | # This saves us some later branches | 
| 63 |  |  |  |  |  |  | # Define defaults | 
| 64 | 13299 |  | 100 |  |  | 28577 | $args{additional_types_allowed} //= []; | 
| 65 | 13299 |  | 100 |  |  | 32122 | $args{additional_exclude}       //= []; | 
| 66 |  |  |  |  |  |  |  | 
| 67 |  |  |  |  |  |  | # What are valid option names? | 
| 68 | 13299 |  |  |  |  | 29033 | my $optre = qr/\A(?:rand|exclude|additional_(?:types_allowed|exclude))\z/; | 
| 69 |  |  |  |  |  |  |  | 
| 70 |  |  |  |  |  |  | # Make sure all options are valid | 
| 71 | 13299 | 50 |  | 43996 |  | 41849 | if ( notall { m/$optre/ } keys %args ) { | 
|  | 43996 |  |  |  |  | 146172 |  | 
| 72 | 0 |  |  |  |  | 0 | my (@bad) = grep { !m/$optre/ } keys %args; | 
|  | 0 |  |  |  |  | 0 |  | 
| 73 | 0 |  |  |  |  | 0 | croak( "unknown named argument passed to random_ipv4: " . $bad[0] ); | 
| 74 |  |  |  |  |  |  | } | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | # Get default excludes | 
| 77 | 13299 | 100 |  |  |  | 34798 | if ( !defined( $args{exclude} ) ) { | 
| 78 |  |  |  |  |  |  | $args{exclude} = | 
| 79 | 9200 |  |  |  |  | 15316 | _get_ipv4_excludes( $args{additional_types_allowed} ); | 
| 80 |  |  |  |  |  |  | } | 
| 81 |  |  |  |  |  |  |  | 
| 82 |  |  |  |  |  |  | # Expand out tags in exclude list | 
| 83 | 71465 |  |  |  |  | 182571 | my (@exclude_cidrs) = grep { m/^\d+\.\d+\.\d+\.\d+(:?\/\d+)$/ } @{ $args{exclude} }, | 
|  | 13298 |  |  |  |  | 17112 |  | 
| 84 | 13298 |  |  |  |  | 15662 | @{ $args{additional_exclude} }; | 
|  | 13298 |  |  |  |  | 18573 |  | 
| 85 |  |  |  |  |  |  |  | 
| 86 | 71465 |  |  |  |  | 157845 | my (@exclude_tags) = grep { !m/^\d+\.\d+\.\d+\.\d+(:?\/\d+)$/ } @{ $args{exclude} }, | 
|  | 13298 |  |  |  |  | 15365 |  | 
| 87 | 13298 |  |  |  |  | 18404 | @{ $args{additional_exclude} }; | 
|  | 13298 |  |  |  |  | 15920 |  | 
| 88 |  |  |  |  |  |  |  | 
| 89 | 13298 |  |  |  |  | 23154 | my (@exclude_expanded) = ( @exclude_cidrs, map { @{ _get_ipv4_excludes( $args{additional_types_allowed}, $_ ) } } @exclude_tags ); | 
|  | 8194 |  |  |  |  | 8081 |  | 
|  | 8194 |  |  |  |  | 11376 |  | 
| 90 |  |  |  |  |  |  |  | 
| 91 | 13296 |  |  |  |  | 53306 | my (@exclude_all) = uniq sort @exclude_expanded; | 
| 92 |  |  |  |  |  |  |  | 
| 93 |  |  |  |  |  |  | # Build a closure for checking to see if an address is excluded | 
| 94 | 16386 |  |  | 16386 |  | 15433 | my $is_not_excluded = sub($addr) { | 
|  | 16386 |  |  |  |  | 19100 |  | 
|  | 16386 |  |  |  |  | 18603 |  | 
| 95 | 16386 |  |  |  |  | 40039 | none { in_ipv4_subnet( $_, $addr ) } @exclude_all; | 
|  | 96843 |  |  |  |  | 120101 |  | 
| 96 | 13296 |  |  |  |  | 37793 | }; | 
| 97 |  |  |  |  |  |  |  | 
| 98 | 13296 |  |  |  |  | 16103 | my $addr; | 
| 99 | 13296 |  |  |  |  | 13221 | do { | 
| 100 | 16386 |  |  |  |  | 16446 | my @parts; | 
| 101 | 16386 |  |  |  |  | 22655 | for my $octet ( 1 .. 4 ) { | 
| 102 | 65544 |  |  |  |  | 85514 | push @parts, $args{rand}->( 255, $octet ); | 
| 103 |  |  |  |  |  |  | } | 
| 104 | 16386 |  |  |  |  | 56328 | $addr = join '.', @parts; | 
| 105 |  |  |  |  |  |  | } until $is_not_excluded->($addr); | 
| 106 |  |  |  |  |  |  |  | 
| 107 | 13296 |  |  |  |  | 85227 | return $addr; | 
| 108 |  |  |  |  |  |  | } | 
| 109 |  |  |  |  |  |  |  | 
| 110 |  |  |  |  |  |  | # Private sub to build the default list of excludes, when passed a list | 
| 111 |  |  |  |  |  |  | # of additional types allowed | 
| 112 |  |  |  |  |  |  | # | 
| 113 |  |  |  |  |  |  | # Returns a list ref | 
| 114 | 17395 |  |  | 17395 |  | 18802 | sub _get_ipv4_excludes ( $addl_types, $tag = 'default' ) { | 
|  | 17395 |  |  |  |  | 17736 |  | 
|  | 17395 |  |  |  |  | 19697 |  | 
|  | 17395 |  |  |  |  | 16418 |  | 
| 115 | 17395 |  |  |  |  | 23533 | foreach my $t (@$addl_types) { | 
| 116 | 57349 | 100 |  |  |  | 79283 | if ( !exists( $VALID_TYPES{$t} ) ) { | 
| 117 | 1 |  |  |  |  | 90 | confess("Type '$t' is not a valid type"); | 
| 118 |  |  |  |  |  |  | } | 
| 119 |  |  |  |  |  |  | } | 
| 120 | 17394 | 100 |  |  |  | 27353 | if ( !exists( $VALID_TYPES{$tag} ) ) { | 
| 121 | 2 |  |  |  |  | 273 | confess("Type '$tag' is not a valid type"); | 
| 122 |  |  |  |  |  |  | } | 
| 123 |  |  |  |  |  |  |  | 
| 124 | 17392 |  |  |  |  | 18534 | my @ret; | 
| 125 |  |  |  |  |  |  |  | 
| 126 |  |  |  |  |  |  | NEXT_EXCLUDE: | 
| 127 | 17392 |  |  |  |  | 41233 | foreach my $default_exclude ( keys %$IPV4_EXCLUDE ) { | 
| 128 | 278272 | 100 |  | 409344 |  | 423518 | if ( none { $_ eq $tag } @{ $IPV4_EXCLUDE->{$default_exclude} } ) { | 
|  | 409344 |  |  |  |  | 519589 |  | 
|  | 278272 |  |  |  |  | 403070 |  | 
| 129 | 118784 |  |  |  |  | 183977 | next NEXT_EXCLUDE; | 
| 130 |  |  |  |  |  |  | } | 
| 131 |  |  |  |  |  |  |  | 
| 132 | 159488 |  |  |  |  | 221889 | foreach my $checktype ( @{ $IPV4_EXCLUDE->{$default_exclude} } ) { | 
|  | 159488 |  |  |  |  | 200687 |  | 
| 133 | 318976 | 100 |  | 1480830 |  | 696258 | if ( any { $_ eq $checktype } @$addl_types ) { | 
|  | 1480830 |  |  |  |  | 1503567 |  | 
| 134 |  |  |  |  |  |  | # Not excluded. | 
| 135 | 83978 |  |  |  |  | 131058 | next NEXT_EXCLUDE; | 
| 136 |  |  |  |  |  |  | } | 
| 137 |  |  |  |  |  |  | } | 
| 138 | 75510 |  |  |  |  | 121838 | push @ret, $default_exclude; | 
| 139 |  |  |  |  |  |  | } | 
| 140 |  |  |  |  |  |  |  | 
| 141 | 17392 |  |  |  |  | 43825 | return \@ret; | 
| 142 |  |  |  |  |  |  | } | 
| 143 |  |  |  |  |  |  |  | 
| 144 |  |  |  |  |  |  |  | 
| 145 | 101851 |  |  | 101851 | 1 | 102335 | sub in_ipv4_subnet ( $sub_cidr, $ip ) { | 
|  | 101851 |  |  |  |  | 106986 |  | 
|  | 101851 |  |  |  |  | 97070 |  | 
|  | 101851 |  |  |  |  | 95046 |  | 
| 146 | 101851 | 50 |  |  |  | 135113 | if ( !defined($sub_cidr) ) { confess("subnet_cidr is not defined"); } | 
|  | 0 |  |  |  |  | 0 |  | 
| 147 | 101851 | 50 |  |  |  | 123659 | if ( !defined($ip) )       { confess("ip is not defined"); } | 
|  | 0 |  |  |  |  | 0 |  | 
| 148 |  |  |  |  |  |  |  | 
| 149 | 101851 | 100 |  |  |  | 263063 | if ( $sub_cidr !~ m/\A(?:[0-9\.]+)(?:\/(?:[0-9]+))?\z/ ) { | 
| 150 | 1 |  |  |  |  | 276 | confess("$sub_cidr is not in the format A.B.C.D/N"); | 
| 151 |  |  |  |  |  |  | } | 
| 152 | 101850 |  |  |  |  | 272381 | my ( $sub_net, $sub_mask ) = $sub_cidr =~ m/\A([0-9\.]+)(?:\/([0-9]+))?\z/ms; | 
| 153 | 101850 |  | 50 |  |  | 157890 | $sub_mask //= 32; | 
| 154 |  |  |  |  |  |  |  | 
| 155 | 101850 |  |  |  |  | 218884 | my $addr = unpack( 'N', inet_aton($ip) ); | 
| 156 | 101850 |  |  |  |  | 192687 | my $sub  = unpack( 'N', inet_aton($sub_net) ); | 
| 157 |  |  |  |  |  |  |  | 
| 158 | 101850 |  |  |  |  | 117237 | my $mask = 0; | 
| 159 | 101850 |  |  |  |  | 179971 | for ( 1 .. $sub_mask ) { | 
| 160 | 1209152 |  |  |  |  | 1124471 | $mask = $mask >> 1; | 
| 161 | 1209152 |  |  |  |  | 1271367 | $mask = $mask | ( 1 << 31 ); | 
| 162 |  |  |  |  |  |  | } | 
| 163 |  |  |  |  |  |  |  | 
| 164 | 101850 | 100 |  |  |  | 149460 | if ( ( $addr & $mask ) == ( $sub & $mask ) ) { | 
| 165 | 3094 |  |  |  |  | 8267 | return 1; | 
| 166 |  |  |  |  |  |  | } | 
| 167 |  |  |  |  |  |  |  | 
| 168 | 98756 |  |  |  |  | 150951 | return; | 
| 169 |  |  |  |  |  |  | } | 
| 170 |  |  |  |  |  |  |  | 
| 171 |  |  |  |  |  |  |  | 
| 172 | 1 |  |  | 1 | 1 | 2 | sub default_ipv4_exclude() { | 
|  | 1 |  |  |  |  | 1 |  | 
| 173 | 1 |  |  |  |  | 3 | return _get_ipv4_excludes( [] ); | 
| 174 |  |  |  |  |  |  | } | 
| 175 |  |  |  |  |  |  |  | 
| 176 |  |  |  |  |  |  | 1; | 
| 177 |  |  |  |  |  |  |  | 
| 178 |  |  |  |  |  |  | __END__ |