File Coverage

lib/IP/Random.pm
Criterion Covered Total %
statement 105 111 94.5
branch 18 22 81.8
condition 9 11 81.8
subroutine 17 17 100.0
pod 3 3 100.0
total 152 164 92.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             #
4             # Copyright (C) 2016-2018 Joelle Maslak
5             # All Rights Reserved - See License
6             #
7              
8             package IP::Random;
9             $IP::Random::VERSION = '1.008';
10             # ABSTRACT: Generate IP Addresses Randomly
11              
12              
13             # Some boilerplate
14 6     6   1201882 use v5.20;
  6         48  
15 6     6   35 use strict;
  6         13  
  6         133  
16 6     6   30 use warnings;
  6         13  
  6         253  
17              
18 6     6   40 use feature 'signatures';
  6         11  
  6         842  
19 6     6   41 no warnings 'experimental::signatures';
  6         12  
  6         283  
20              
21 6     6   42 use Carp;
  6         10  
  6         457  
22              
23             # We need a version of List::Util with uniq in it
24 6     6   44 use List::Util 1.50 qw(any none notall pairs uniq);
  6         123  
  6         518  
25 6     6   3457 use Socket qw(inet_aton);
  6         23460  
  6         10315  
26              
27             my $IPV4_EXCLUDE = {
28             '0.0.0.0/8' => [ 'default', 'rfc1122' ],
29             '10.0.0.0/8' => [ 'default', 'rfc1918' ],
30             '100.64.0.0/10' => [ 'default', 'rfc6598' ],
31             '127.0.0.0/8' => [ 'default', 'rfc1122' ],
32             '169.254.0.0/16' => [ 'default', 'rfc3927' ],
33             '172.16.0.0/12' => [ 'default', 'rfc1918' ],
34             '192.0.0.0/24' => [ 'default', 'rfc5736' ],
35             '192.0.2.0/24' => [ 'default', 'rfc5737' ],
36             '192.88.99.0/24' => [ 'default', 'rfc3068' ],
37             '192.168.0.0/16' => [ 'default', 'rfc1918' ],
38             '198.18.0.0/15' => [ 'default', 'rfc2544' ],
39             '198.51.100.0/24' => [ 'default', 'rfc5737' ],
40             '203.0.113.0/24' => [ 'default', 'rfc5737' ],
41             '224.0.0.0/4' => [ 'default', 'rfc3171' ],
42             '240.0.0.0/4' => [ 'default', 'rfc1112' ],
43             '255.255.255.255/32' => [ 'default', 'rfc919' ],
44             };
45              
46             # Build cache of valid types
47             my %VALID_TYPES = map { $_, 1 } uniq sort map { @$_ } values %$IPV4_EXCLUDE;
48              
49              
50 13298     13298 1 398148 sub random_ipv4 ( %args ) {
  13298         18486  
  13298         13820  
51 13298   100 65508   69321 $args{rand} //= sub { int( rand( shift() + 1 ) ) };
  65508         116956  
52              
53             # Can't have exclude and additional_types_allowed both existing
54 13298 50 66     27231 if ( exists( $args{exclude} ) && exists( $args{additional_types_allowed} ) ) {
55 0         0 croak( "Cannot define both 'exclude' and " . "'additional_types_allowed' parameters" );
56             }
57              
58             # This saves us some later branches
59             # Define defaults
60 13298   100     30702 $args{additional_types_allowed} //= [];
61 13298   100     36022 $args{additional_exclude} //= [];
62              
63             # What are valid option names?
64 13298         33552 my $optre = qr/\A(?:rand|exclude|additional_(?:types_allowed|exclude))\z/;
65              
66             # Make sure all options are valid
67 13298 50   43993   47095 if ( notall { m/$optre/ } keys %args ) {
  43993         166657  
68 0         0 my (@bad) = grep { !m/$optre/ } keys %args;
  0         0  
69 0         0 croak( "unknown named argument passed to random_ipv4: " . $bad[0] );
70             }
71              
72             # Get default excludes
73 13298 100       38156 if ( !defined( $args{exclude} ) ) {
74             $args{exclude} =
75 9199         15627 _get_ipv4_excludes( $args{additional_types_allowed} );
76             }
77              
78             # Expand out tags in exclude list
79 71449         189307 my (@exclude_cidrs) = grep { m/^\d+\.\d+\.\d+\.\d+(:?\/\d+)$/ } @{ $args{exclude} },
  13297         18571  
80 13297         18685 @{ $args{additional_exclude} };
  13297         19448  
81              
82 71449         160999 my (@exclude_tags) = grep { !m/^\d+\.\d+\.\d+\.\d+(:?\/\d+)$/ } @{ $args{exclude} },
  13297         17775  
83 13297         18044 @{ $args{additional_exclude} };
  13297         18474  
84              
85 13297         26925 my (@exclude_expanded) = ( @exclude_cidrs, map { @{ _get_ipv4_excludes( $args{additional_types_allowed}, $_ ) } } @exclude_tags );
  8194         9062  
  8194         12121  
86              
87 13295         60199 my (@exclude_all) = uniq sort @exclude_expanded;
88              
89             # Build a closure for checking to see if an address is excluded
90 16378     16378   16795 my $is_not_excluded = sub($addr) {
  16378         18832  
  16378         18227  
91 16378         44258 none { in_ipv4_subnet( $_, $addr ) } @exclude_all;
  100055         126344  
92 13295         44903 };
93              
94 13295         16978 my $addr;
95 13295         14768 do {
96 16378         16322 my @parts;
97 16378         24879 for my $octet ( 1 .. 4 ) {
98 65512         91836 push @parts, $args{rand}->( 255, $octet );
99             }
100 16378         58492 $addr = join '.', @parts;
101             } until $is_not_excluded->($addr);
102              
103 13295         102274 return $addr;
104             }
105              
106             # Private sub to build the default list of excludes, when passed a list
107             # of additional types allowed
108             #
109             # Returns a list ref
110 17394     17394   18367 sub _get_ipv4_excludes ( $addl_types, $tag = 'default' ) {
  17394         18682  
  17394         19951  
  17394         16816  
111 17394         26563 foreach my $t (@$addl_types) {
112 57349 100       90855 if ( !exists( $VALID_TYPES{$t} ) ) {
113 1         129 confess("Type '$t' is not a valid type");
114             }
115             }
116 17393 100       28757 if ( !exists( $VALID_TYPES{$tag} ) ) {
117 2         411 confess("Type '$tag' is not a valid type");
118             }
119              
120 17391         19407 my @ret;
121              
122             NEXT_EXCLUDE:
123 17391         45872 foreach my $default_exclude ( keys %$IPV4_EXCLUDE ) {
124 278256 100   409328   495457 if ( none { $_ eq $tag } @{ $IPV4_EXCLUDE->{$default_exclude} } ) {
  409328         565024  
  278256         476882  
125 118784         208772 next NEXT_EXCLUDE;
126             }
127              
128 159472         242634 foreach my $checktype ( @{ $IPV4_EXCLUDE->{$default_exclude} } ) {
  159472         220626  
129 318944 100   1480830   784076 if ( any { $_ eq $checktype } @$addl_types ) {
  1480830         1671241  
130             # Not excluded.
131 83978         172454 next NEXT_EXCLUDE;
132             }
133             }
134 75494         126669 push @ret, $default_exclude;
135             }
136              
137 17391         54043 return \@ret;
138             }
139              
140              
141 105063     105063 1 100509 sub in_ipv4_subnet ( $sub_cidr, $ip ) {
  105063         105279  
  105063         98331  
  105063         94466  
142 105063 50       138711 if ( !defined($sub_cidr) ) { confess("subnet_cidr is not defined"); }
  0         0  
143 105063 50       128813 if ( !defined($ip) ) { confess("ip is not defined"); }
  0         0  
144              
145 105063 100       284742 if ( $sub_cidr !~ m/\A(?:[0-9\.]+)(?:\/(?:[0-9]+))?\z/ ) {
146 1         376 confess("$sub_cidr is not in the format A.B.C.D/N");
147             }
148 105062         295889 my ( $sub_net, $sub_mask ) = $sub_cidr =~ m/\A([0-9\.]+)(?:\/([0-9]+))?\z/ms;
149 105062   50     165948 $sub_mask //= 32;
150              
151 105062         239220 my $addr = unpack( 'N', inet_aton($ip) );
152 105062         198351 my $sub = unpack( 'N', inet_aton($sub_net) );
153              
154 105062         122177 my $mask = 0;
155 105062         194484 for ( 1 .. $sub_mask ) {
156 1252792         1094941 $mask = $mask >> 1;
157 1252792         1223739 $mask = $mask | ( 1 << 31 );
158             }
159              
160 105062 100       154766 if ( ( $addr & $mask ) == ( $sub & $mask ) ) {
161 3087         8645 return 1;
162             }
163              
164 101975         162915 return;
165             }
166              
167              
168 1     1 1 2 sub default_ipv4_exclude() {
  1         2  
169 1         4 return _get_ipv4_excludes( [] );
170             }
171              
172             1;
173              
174             __END__