File Coverage

lib/IP/Random.pm
Criterion Covered Total %
statement 108 114 94.7
branch 18 22 81.8
condition 9 11 81.8
subroutine 18 18 100.0
pod 3 3 100.0
total 156 168 92.8


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__