File Coverage

blib/lib/Mail/SpamAssassin/NetSet.pm
Criterion Covered Total %
statement 137 183 74.8
branch 66 104 63.4
condition 32 44 72.7
subroutine 18 20 90.0
pod 0 8 0.0
total 253 359 70.4


line stmt bran cond sub pod time code
1             # Mail::SpamAssassin::NetSet - object to manipulate CIDR net IP addrs
2             # <@LICENSE>
3             # Licensed to the Apache Software Foundation (ASF) under one or more
4             # contributor license agreements. See the NOTICE file distributed with
5             # this work for additional information regarding copyright ownership.
6             # The ASF licenses this file to you under the Apache License, Version 2.0
7             # (the "License"); you may not use this file except in compliance with
8             # the License. You may obtain a copy of the License at:
9             #
10             # http://www.apache.org/licenses/LICENSE-2.0
11             #
12             # Unless required by applicable law or agreed to in writing, software
13             # distributed under the License is distributed on an "AS IS" BASIS,
14             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15             # See the License for the specific language governing permissions and
16             # limitations under the License.
17             # </@LICENSE>
18              
19              
20             use strict;
21 41     41   233 use warnings;
  41         66  
  41         1018  
22 41     41   168 # use bytes;
  41         84  
  41         997  
23             use re 'taint';
24 41     41   1233 use Time::HiRes qw(time);
  41         110  
  41         1319  
25 41     41   201 use NetAddr::IP 4.000;
  41         66  
  41         417  
26 41     41   26633  
  41         855923  
  41         240  
27             use Mail::SpamAssassin::Util;
28 41     41   16279 use Mail::SpamAssassin::Logger;
  41         107  
  41         2232  
29 41     41   272  
  41         68  
  41         3519  
30             our $have_patricia;
31             BEGIN {
32             eval {
33 41     41   132 require Net::Patricia;
34 41         81891 Net::Patricia->VERSION(1.16); # need AF_INET6 support
35 0         0 import Net::Patricia;
36 0         0 $have_patricia = 1;
37 0         0 };
38             }
39              
40             ###########################################################################
41              
42             my ($class,$netset_name) = @_;
43             $class = ref($class) || $class;
44 441     441 0 952  
45 441   33     1626 $netset_name = '' if !defined $netset_name; # object name for debugging
46             my $self = {
47 441 100       964 name => $netset_name, num_nets => 0,
48 441         2132 cache_hits => 0, cache_attempts => 0,
49             };
50             $self->{pt} = Net::Patricia->new(&AF_INET6) if $have_patricia;
51              
52 441 50       1109 bless $self, $class;
53             $self;
54 441         791 }
55 441         944  
56             ###########################################################################
57              
58             my($self) = shift;
59             if (exists $self->{cache}) {
60             local($@, $!, $_); # protect outer layers from a potential surprise
61 321     321   10863 my($hits, $attempts) = ($self->{cache_hits}, $self->{cache_attempts});
62 321 100       61521 dbg("netset: cache %s hits/attempts: %d/%d, %.1f %%",
63 86         410 $self->{name}, $hits, $attempts, 100*$hits/$attempts) if $attempts > 0;
64 86         246 }
65             }
66 86 50       469  
67             ###########################################################################
68              
69             my ($self, @nets) = @_;
70              
71             $self->{nets} ||= [ ];
72             my $numadded = 0;
73 620     620 0 1845 delete $self->{cache}; # invalidate cache (in case of late additions)
74              
75 620   100     2508 foreach my $cidr_orig (@nets) {
76 620         879 my $cidr = $cidr_orig; # leave original unchanged, useful for logging
77 620         901  
78             # recognizes syntax:
79 620         1137 # [IPaddr%scope]/len or IPaddr%scope/len or IPv4addr/mask
80 620         965 # optionally prefixed by a '!' to indicate negation (exclusion);
81             # the %scope (i.e. interface), /len or /mask are optional
82              
83             local($1,$2,$3,$4);
84             $cidr =~ s/^\s+//;
85             my $exclude = ($cidr =~ s/^!\s*//) ? 1 : 0;
86              
87 620         2771 my $masklen; # netmask or a prefix length
88 620         1960 $masklen = $1 if $cidr =~ s{ / (.*) \z }{}xs;
89 620 100       1721  
90             # discard optional brackets
91 620         779 $cidr = $1 if $cidr =~ /^ \[ ( [^\]]* ) \] \z/xs;
92 620 100       2968  
93             my $scope;
94             # IPv6 Scoped Address (RFC 4007, RFC 6874, RFC 3986 "unreserved" charset)
95 620 50       1632 if ($cidr =~ s/ % ( [A-Z0-9._~-]* ) \z //xsi) { # scope <zone_id> ?
96             $scope = $1; # interface specification
97 620         735 # discard interface specification, currently just ignored
98             info("netset: ignoring interface scope '%%%s' in IP address %s",
99 620 50       1367 $scope, $cidr_orig);
100 0         0 }
101              
102 0         0 my $is_ip4 = 0;
103             if ($cidr =~ /^ \d+ (\. | \z) /x) { # looks like an IPv4 address
104             if ($cidr =~ /^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z/x) {
105             # also strips leading zeroes, not liked by inet_pton
106 620         877 $cidr = sprintf('%d.%d.%d.%d', $1,$2,$3,$4);
107 620 100       1960 $masklen = 32 if !defined $masklen;
108 344 100       2019 } elsif ($cidr =~ /^ (\d+) \. (\d+) \. (\d+) \.? \z/x) {
    100          
    100          
    50          
109             $cidr = sprintf('%d.%d.%d.0', $1,$2,$3);
110 270         2352 $masklen = 24 if !defined $masklen;
111 270 100       777 } elsif ($cidr =~ /^ (\d+) \. (\d+) \.? \z/x) {
112             $cidr = sprintf('%d.%d.0.0', $1,$2);
113 8         46 $masklen = 16 if !defined $masklen;
114 8 100       26 } elsif ($cidr =~ /^ (\d+) \.? \z/x) {
115             $cidr = sprintf('%d.0.0.0', $1);
116 35         209 $masklen = 8 if !defined $masklen;
117 35 100       127 } else {
118             warn "netset: illegal IPv4 address given: '$cidr_orig'\n";
119 31         161 next;
120 31 100       88 }
121             $is_ip4 = 1;
122 0         0 }
123 0         0  
124             if ($self->{pt}) {
125 344         536 if (defined $masklen) {
126             $masklen =~ /^\d{1,3}\z/
127             or die "Network mask not supported, use a CIDR syntax: '$cidr_orig'";
128 620 50       1310 }
129 0 0       0 my $key = $cidr;
130 0 0       0 my $prefix_len = $masklen;
131             if ($is_ip4) {
132             $key = '::ffff:' . $key; # turn it into an IPv4-mapped IPv6 addresses
133 0         0 $prefix_len += 96 if defined $prefix_len;
134 0         0 }
135 0 0       0 $prefix_len = 128 if !defined $prefix_len;
136 0         0 $key .= '/' . $prefix_len;
137 0 0       0 # dbg("netset: add_cidr (patricia trie) %s => %s",
138             # $cidr_orig, $exclude ? '!'.$key : $key);
139 0 0       0 defined eval {
140 0         0 $self->{pt}->add_string($key, $exclude ? '!'.$key : $key)
141             } or warn "netset: illegal IP address given (patricia trie): ".
142             "'$key': $@\n";
143 0 0       0 }
144 0 0       0  
145             $cidr .= '/' . $masklen if defined $masklen;
146              
147             my $ip = NetAddr::IP->new($cidr);
148             if (!defined $ip) {
149 620 100       1797 warn "netset: illegal IP address given: '$cidr_orig'\n";
150             next;
151 620         3863 }
152 620 50       112105 # dbg("netset: add_cidr %s => %s => %s", $cidr_orig, $cidr, $ip);
153 0         0  
154 0         0 # if this is an IPv4 address, create an IPv6 representation, too
155             my ($ip4, $ip6);
156             if ($is_ip4) {
157             $ip4 = $ip;
158             $ip6 = $self->_convert_ipv4_cidr_to_ipv6($cidr);
159 620         978 } else {
160 620 100       1327 $ip6 = $ip;
161 344         447 }
162 344         1254  
163             # bug 5931: this is O(n^2). bad if there are lots of nets. There are good
164 276         445 # reasons to keep it for linting purposes, though, so don't start skipping
165             # it until we have over 200 nets in our list
166             if (scalar @{$self->{nets}} < 200) {
167             next if ($self->is_net_declared($ip4, $ip6, $exclude, 0));
168             }
169              
170 620 50       60313 # note: it appears a NetAddr::IP object takes up about 279 bytes
  620         1824  
171 620 100       1773 push @{$self->{nets}}, {
172             exclude => $exclude,
173             ip4 => $ip4,
174             ip6 => $ip6,
175 612         820 as_string => $cidr_orig,
  612         3146  
176             };
177             $numadded++;
178             }
179              
180             $self->{num_nets} += $numadded;
181 612         2894 $numadded;
182             }
183              
184 620         994 my ($self) = @_;
185 620         1436 return $self->{num_nets};
186             }
187              
188             my ($self, $cidr) = @_;
189 0     0 0 0  
190 0         0 # only do this for IPv4 addresses
191             return unless $cidr =~ /^\d+[.\/]/;
192              
193             if ($cidr !~ /\//) { # no mask
194 428     428   1306 return NetAddr::IP->new6("::ffff:".$cidr);
195             }
196              
197 428 50       2056 # else we have a CIDR mask specified. use new6() to do this
198             #
199 428 100       1512 my $ip6 = NetAddr::IP->new6($cidr)->cidr;
200 84         367 # 127.0.0.1 -> 0:0:0:0:0:0:7F00:0001/128
201             # 127/8 -> 0:0:0:0:0:0:7F00:0/104
202              
203             # now, move that from 0:0:0:0:0:0: space to 0:0:0:0:0:ffff: space
204             if (!defined $ip6 || $ip6 !~ /^0:0:0:0:0:0:(.*)$/) {
205 344         1396 warn "oops! unparseable IPv6 address for $cidr: $ip6";
206             return;
207             }
208              
209             return NetAddr::IP->new6("::ffff:$1");
210 344 50 33     57376 }
211 0         0  
212 0         0 my ($self, $net4, $net6, $exclude, $quiet, $netname, $declared) = @_;
213              
214             return 0 unless (defined $self->{nets});
215 344         1539  
216             foreach my $net (@{$self->{nets}}) {
217             # check to see if the new network is contained by the old network
218             my $in4 = defined $net4 && defined $net->{ip4} && $net->{ip4}->contains($net4);
219 665     665   100413 my $in6 = defined $net6 && defined $net->{ip6} && $net->{ip6}->contains($net6);
220             if ($in4 || $in6) {
221 665 50       1560 warn sprintf("netset: cannot %s %s as it has already been %s\n",
222             $exclude ? "exclude" : "include",
223 665         878 $netname,
  665         1698  
224             $net->{exclude} ? "excluded" : "included") unless $quiet;
225 465   100     1905 # a network that matches an excluded network isn't contained by "nets"
226 465   66     4806 # return 0 if we're not just looking to see if the network was declared
227 465 100 100     9794 return 0 if (!$declared && $net->{exclude});
228             return 1;
229             }
230             }
231 47 100       163 return 0;
    100          
    100          
232             }
233              
234 47 100 100     1567 my ($self, $net4, $net6, $exclude, $quiet) = @_;
235 46         274 return $self->_nets_contains_network($net4, $net6, $exclude,
236             $quiet, $net4 || $net6, 1);
237             }
238 618         1864  
239             my ($self, $ip) = @_;
240             my $result = 0;
241              
242 620     620 0 1254 if (!$self->{num_nets}) { return 0 }
243 620   66     2599  
244             $self->{cache_attempts}++;
245             if ($self->{cache} && exists $self->{cache}{$ip}) {
246             dbg("netset: %s cached lookup on %s, %d networks, result: %s",
247             $self->{name}, $ip, $self->{num_nets}, $self->{cache}{$ip});
248 139     139 0 444 $self->{cache_hits}++;
249 139         219 return $self->{cache}{$ip};
250              
251 139 100       314 } elsif ($self->{pt}) {
  22         90  
252             # do a quick lookup on a Patricia Trie
253 117         180 my $t0 = time;
254 117 100 100     487 local($1,$2,$3,$4); local $_ = $ip;
    50          
255             $_ = $1 if /^ \[ ( [^\]]* ) \] \z/xs; # discard optional brackets
256 17         84 s/%[A-Z0-9:._-]+\z//si; # discard interface specification
257 17         25 if (m{^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z}x) {
258 17         86 $_ = sprintf('::ffff:%d.%d.%d.%d', $1,$2,$3,$4);
259             } else {
260             s/^IPv6://si; # discard optional 'IPv6:' prefix
261             }
262 0         0 eval { $result = $self->{pt}->match_string($_); 1 } or undef $result;
263 0         0 $result = defined $result && $result !~ /^!/ ? 1 : 0;
  0         0  
264 0 0       0 dbg("netset: %s patricia lookup on %s, %d networks, result: %s, %.3f ms",
265 0         0 $self->{name}, $ip, $self->{num_nets}, $result, 1000*(time - $t0));
266 0 0       0 } else {
267 0         0 # do a sequential search on a list of NetAddr::IP objects
268             my $t0 = time;
269 0         0 my ($ip4, $ip6);
270             if ($ip =~ /^\d+\./) {
271 0 0       0 $ip4 = NetAddr::IP->new($ip);
  0         0  
  0         0  
272 0 0 0     0 $ip6 = $self->_convert_ipv4_cidr_to_ipv6($ip);
273             } else {
274 0         0 $ip6 = NetAddr::IP->new($ip);
275             }
276             foreach my $net (@{$self->{nets}}) {
277 100         255 if ((defined $ip4 && defined $net->{ip4} && $net->{ip4}->contains($ip4))
278 100         141 || (defined $ip6 && defined $net->{ip6} && $net->{ip6}->contains($ip6))){
279 100 100       445 $result = !$net->{exclude};
280 84         390 last;
281 84         10577 }
282             }
283 16         92 dbg("netset: %s lookup on %s, %d networks, result: %s, %.3f ms",
284             $self->{name}, $ip, $self->{num_nets}, $result, 1000*(time - $t0));
285 100         26298 }
  100         269  
286 196 100 100     3855  
      100        
      33        
      66        
      100        
287             $self->{cache}{$ip} = $result;
288 70         1135 return $result;
289 70         129 }
290              
291             my ($self, $net) = @_;
292             my $exclude = $net->{exclude};
293 100         1175 my $net4 = $net->{ip4};
294             my $net6 = $net->{ip6};
295             return $self->_nets_contains_network($net4, $net6, $exclude, 1, "", 0);
296 100         324 }
297 100         400  
298             my ($self) = @_;
299             if (exists $self->{cache}) {
300             dbg("netset: ditch cache on %s", $self->{name});
301 45     45 0 102 delete $self->{cache};
302 45         62 }
303 45         62 }
304 45         67  
305 45         96 my ($self) = @_;
306             my $dup = Mail::SpamAssassin::NetSet->new($self->{name});
307             if ($self->{nets}) {
308             @{$dup->{nets}} = @{$self->{nets}};
309 291     291 0 444 }
310 291 50       940 if ($self->{pt}) {
311 0         0 my $dup_pt = $dup->{pt};
312 0         0 $self->{pt}->climb(sub {
313             my $key = $_[0]; $key =~ s/^!//;
314             defined eval { $dup_pt->add_string($key, $_[0]) }
315             or die "Adding a network $_[0] to a patricia trie failed: $@";
316             1;
317 9     9 0 12 });
318 9         27 }
319 9 100       15 $dup->{num_nets} = $self->{num_nets};
320 6         7 return $dup;
  6         13  
  6         10  
321             }
322 9 50       16  
323 0         0 ###########################################################################
324              
325 0     0   0 1;
  0         0