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             package Mail::SpamAssassin::NetSet;
20              
21 40     40   286 use strict;
  40         73  
  40         1243  
22 40     40   204 use warnings;
  40         87  
  40         1321  
23             # use bytes;
24 40     40   209 use re 'taint';
  40         88  
  40         1394  
25 40     40   209 use Time::HiRes qw(time);
  40         76  
  40         387  
26 40     40   7185 use NetAddr::IP 4.000;
  40         726  
  40         285  
27              
28 40     40   5796 use Mail::SpamAssassin::Util;
  40         80  
  40         1736  
29 40     40   260 use Mail::SpamAssassin::Logger;
  40         79  
  40         4546  
30              
31             our $have_patricia;
32             BEGIN {
33 40     40   164 eval {
34 40         93700 require Net::Patricia;
35 0         0 Net::Patricia->VERSION(1.16); # need AF_INET6 support
36 0         0 import Net::Patricia;
37 0         0 $have_patricia = 1;
38             };
39             }
40              
41             ###########################################################################
42              
43             sub new {
44 408     408 0 1004 my ($class,$netset_name) = @_;
45 408   33     1639 $class = ref($class) || $class;
46              
47 408 100       1009 $netset_name = '' if !defined $netset_name; # object name for debugging
48 408         1978 my $self = {
49             name => $netset_name, num_nets => 0,
50             cache_hits => 0, cache_attempts => 0,
51             };
52 408 50       1087 $self->{pt} = Net::Patricia->new(&AF_INET6) if $have_patricia;
53              
54 408         856 bless $self, $class;
55 408         949 $self;
56             }
57              
58             ###########################################################################
59              
60             sub DESTROY {
61 285     285   4695 my($self) = shift;
62 285 100       35498 if (exists $self->{cache}) {
63 86         465 local($@, $!, $_); # protect outer layers from a potential surprise
64 86         283 my($hits, $attempts) = ($self->{cache_hits}, $self->{cache_attempts});
65             dbg("netset: cache %s hits/attempts: %d/%d, %.1f %%",
66 86 50       540 $self->{name}, $hits, $attempts, 100*$hits/$attempts) if $attempts > 0;
67             }
68             }
69              
70             ###########################################################################
71              
72             sub add_cidr {
73 576     576 0 1904 my ($self, @nets) = @_;
74              
75 576   100     2622 $self->{nets} ||= [ ];
76 576         879 my $numadded = 0;
77 576         944 delete $self->{cache}; # invalidate cache (in case of late additions)
78              
79 576         1161 foreach my $cidr_orig (@nets) {
80 576         1012 my $cidr = $cidr_orig; # leave original unchanged, useful for logging
81              
82             # recognizes syntax:
83             # [IPaddr%scope]/len or IPaddr%scope/len or IPv4addr/mask
84             # optionally prefixed by a '!' to indicate negation (exclusion);
85             # the %scope (i.e. interface), /len or /mask are optional
86              
87 576         2901 local($1,$2,$3,$4);
88 576         2061 $cidr =~ s/^\s+//;
89 576 100       1840 my $exclude = ($cidr =~ s/^!\s*//) ? 1 : 0;
90              
91 576         887 my $masklen; # netmask or a prefix length
92 576 100       3304 $masklen = $1 if $cidr =~ s{ / (.*) \z }{}xs;
93              
94             # discard optional brackets
95 576 50       1654 $cidr = $1 if $cidr =~ /^ \[ ( [^\]]* ) \] \z/xs;
96              
97 576         743 my $scope;
98             # IPv6 Scoped Address (RFC 4007, RFC 6874, RFC 3986 "unreserved" charset)
99 576 50       1495 if ($cidr =~ s/ % ( [A-Z0-9._~-]* ) \z //xsi) { # scope <zone_id> ?
100 0         0 $scope = $1; # interface specification
101             # discard interface specification, currently just ignored
102 0         0 info("netset: ignoring interface scope '%%%s' in IP address %s",
103             $scope, $cidr_orig);
104             }
105              
106 576         943 my $is_ip4 = 0;
107 576 100       2019 if ($cidr =~ /^ \d+ (\. | \z) /x) { # looks like an IPv4 address
108 322 100       1685 if ($cidr =~ /^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z/x) {
    100          
    100          
    50          
109             # also strips leading zeroes, not liked by inet_pton
110 248         2079 $cidr = sprintf('%d.%d.%d.%d', $1,$2,$3,$4);
111 248 100       713 $masklen = 32 if !defined $masklen;
112             } elsif ($cidr =~ /^ (\d+) \. (\d+) \. (\d+) \.? \z/x) {
113 8         51 $cidr = sprintf('%d.%d.%d.0', $1,$2,$3);
114 8 100       26 $masklen = 24 if !defined $masklen;
115             } elsif ($cidr =~ /^ (\d+) \. (\d+) \.? \z/x) {
116 35         209 $cidr = sprintf('%d.%d.0.0', $1,$2);
117 35 100       137 $masklen = 16 if !defined $masklen;
118             } elsif ($cidr =~ /^ (\d+) \.? \z/x) {
119 31         151 $cidr = sprintf('%d.0.0.0', $1);
120 31 100       92 $masklen = 8 if !defined $masklen;
121             } else {
122 0         0 warn "netset: illegal IPv4 address given: '$cidr_orig'\n";
123 0         0 next;
124             }
125 322         521 $is_ip4 = 1;
126             }
127              
128 576 50       1731 if ($self->{pt}) {
129 0 0       0 if (defined $masklen) {
130 0 0       0 $masklen =~ /^\d{1,3}\z/
131             or die "Network mask not supported, use a CIDR syntax: '$cidr_orig'";
132             }
133 0         0 my $key = $cidr;
134 0         0 my $prefix_len = $masklen;
135 0 0       0 if ($is_ip4) {
136 0         0 $key = '::ffff:' . $key; # turn it into an IPv4-mapped IPv6 addresses
137 0 0       0 $prefix_len += 96 if defined $prefix_len;
138             }
139 0 0       0 $prefix_len = 128 if !defined $prefix_len;
140 0         0 $key .= '/' . $prefix_len;
141             # dbg("netset: add_cidr (patricia trie) %s => %s",
142             # $cidr_orig, $exclude ? '!'.$key : $key);
143 0 0       0 defined eval {
144 0 0       0 $self->{pt}->add_string($key, $exclude ? '!'.$key : $key)
145             } or warn "netset: illegal IP address given (patricia trie): ".
146             "'$key': $@\n";
147             }
148              
149 576 100       1574 $cidr .= '/' . $masklen if defined $masklen;
150              
151 576         4199 my $ip = NetAddr::IP->new($cidr);
152 576 50       114355 if (!defined $ip) {
153 0         0 warn "netset: illegal IP address given: '$cidr_orig'\n";
154 0         0 next;
155             }
156             # dbg("netset: add_cidr %s => %s => %s", $cidr_orig, $cidr, $ip);
157              
158             # if this is an IPv4 address, create an IPv6 representation, too
159 576         1045 my ($ip4, $ip6);
160 576 100       1462 if ($is_ip4) {
161 322         494 $ip4 = $ip;
162 322         1332 $ip6 = $self->_convert_ipv4_cidr_to_ipv6($cidr);
163             } else {
164 254         488 $ip6 = $ip;
165             }
166              
167             # bug 5931: this is O(n^2). bad if there are lots of nets. There are good
168             # reasons to keep it for linting purposes, though, so don't start skipping
169             # it until we have over 200 nets in our list
170 576 50       62989 if (scalar @{$self->{nets}} < 200) {
  576         1751  
171 576 100       1766 next if ($self->is_net_declared($ip4, $ip6, $exclude, 0));
172             }
173              
174             # note: it appears a NetAddr::IP object takes up about 279 bytes
175 568         790 push @{$self->{nets}}, {
  568         3084  
176             exclude => $exclude,
177             ip4 => $ip4,
178             ip6 => $ip6,
179             as_string => $cidr_orig,
180             };
181 568         2442 $numadded++;
182             }
183              
184 576         1093 $self->{num_nets} += $numadded;
185 576         1351 $numadded;
186             }
187              
188             sub get_num_nets {
189 0     0 0 0 my ($self) = @_;
190 0         0 return $self->{num_nets};
191             }
192              
193             sub _convert_ipv4_cidr_to_ipv6 {
194 406     406   1057 my ($self, $cidr) = @_;
195              
196             # only do this for IPv4 addresses
197 406 50       2058 return unless $cidr =~ /^\d+[.\/]/;
198              
199 406 100       1667 if ($cidr !~ /\//) { # no mask
200 84         363 return NetAddr::IP->new6("::ffff:".$cidr);
201             }
202              
203             # else we have a CIDR mask specified. use new6() to do this
204             #
205 322         1433 my $ip6 = NetAddr::IP->new6($cidr)->cidr;
206             # 127.0.0.1 -> 0:0:0:0:0:0:7F00:0001/128
207             # 127/8 -> 0:0:0:0:0:0:7F00:0/104
208              
209             # now, move that from 0:0:0:0:0:0: space to 0:0:0:0:0:ffff: space
210 322 50 33     58567 if (!defined $ip6 || $ip6 !~ /^0:0:0:0:0:0:(.*)$/) {
211 0         0 warn "oops! unparseable IPv6 address for $cidr: $ip6";
212 0         0 return;
213             }
214              
215 322         1659 return NetAddr::IP->new6("::ffff:$1");
216             }
217              
218             sub _nets_contains_network {
219 621     621   103997 my ($self, $net4, $net6, $exclude, $quiet, $netname, $declared) = @_;
220              
221 621 50       1636 return 0 unless (defined $self->{nets});
222              
223 621         914 foreach my $net (@{$self->{nets}}) {
  621         1848  
224             # check to see if the new network is contained by the old network
225 443   100     1815 my $in4 = defined $net4 && defined $net->{ip4} && $net->{ip4}->contains($net4);
226 443   66     4892 my $in6 = defined $net6 && defined $net->{ip6} && $net->{ip6}->contains($net6);
227 443 100 100     10278 if ($in4 || $in6) {
228             warn sprintf("netset: cannot %s %s as it has already been %s\n",
229             $exclude ? "exclude" : "include",
230             $netname,
231 47 100       180 $net->{exclude} ? "excluded" : "included") unless $quiet;
    100          
    100          
232             # a network that matches an excluded network isn't contained by "nets"
233             # return 0 if we're not just looking to see if the network was declared
234 47 100 100     1936 return 0 if (!$declared && $net->{exclude});
235 46         263 return 1;
236             }
237             }
238 574         1834 return 0;
239             }
240              
241             sub is_net_declared {
242 576     576 0 1338 my ($self, $net4, $net6, $exclude, $quiet) = @_;
243 576   66     2762 return $self->_nets_contains_network($net4, $net6, $exclude,
244             $quiet, $net4 || $net6, 1);
245             }
246              
247             sub contains_ip {
248 139     139 0 375 my ($self, $ip) = @_;
249 139         227 my $result = 0;
250              
251 139 100       385 if (!$self->{num_nets}) { return 0 }
  22         95  
252              
253 117         261 $self->{cache_attempts}++;
254 117 100 100     516 if ($self->{cache} && exists $self->{cache}{$ip}) {
    50          
255             dbg("netset: %s cached lookup on %s, %d networks, result: %s",
256 17         77 $self->{name}, $ip, $self->{num_nets}, $self->{cache}{$ip});
257 17         38 $self->{cache_hits}++;
258 17         85 return $self->{cache}{$ip};
259              
260             } elsif ($self->{pt}) {
261             # do a quick lookup on a Patricia Trie
262 0         0 my $t0 = time;
263 0         0 local($1,$2,$3,$4); local $_ = $ip;
  0         0  
264 0 0       0 $_ = $1 if /^ \[ ( [^\]]* ) \] \z/xs; # discard optional brackets
265 0         0 s/%[A-Z0-9:._-]+\z//si; # discard interface specification
266 0 0       0 if (m{^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z}x) {
267 0         0 $_ = sprintf('::ffff:%d.%d.%d.%d', $1,$2,$3,$4);
268             } else {
269 0         0 s/^IPv6://si; # discard optional 'IPv6:' prefix
270             }
271 0 0       0 eval { $result = $self->{pt}->match_string($_); 1 } or undef $result;
  0         0  
  0         0  
272 0 0 0     0 $result = defined $result && $result !~ /^!/ ? 1 : 0;
273             dbg("netset: %s patricia lookup on %s, %d networks, result: %s, %.3f ms",
274 0         0 $self->{name}, $ip, $self->{num_nets}, $result, 1000*(time - $t0));
275             } else {
276             # do a sequential search on a list of NetAddr::IP objects
277 100         329 my $t0 = time;
278 100         173 my ($ip4, $ip6);
279 100 100       482 if ($ip =~ /^\d+\./) {
280 84         488 $ip4 = NetAddr::IP->new($ip);
281 84         12205 $ip6 = $self->_convert_ipv4_cidr_to_ipv6($ip);
282             } else {
283 16         84 $ip6 = NetAddr::IP->new($ip);
284             }
285 100         28522 foreach my $net (@{$self->{nets}}) {
  100         345  
286 196 100 100     4766 if ((defined $ip4 && defined $net->{ip4} && $net->{ip4}->contains($ip4))
      100        
      33        
      66        
      100        
287             || (defined $ip6 && defined $net->{ip6} && $net->{ip6}->contains($ip6))){
288 70         1347 $result = !$net->{exclude};
289 70         112 last;
290             }
291             }
292             dbg("netset: %s lookup on %s, %d networks, result: %s, %.3f ms",
293 100         1410 $self->{name}, $ip, $self->{num_nets}, $result, 1000*(time - $t0));
294             }
295              
296 100         383 $self->{cache}{$ip} = $result;
297 100         482 return $result;
298             }
299              
300             sub contains_net {
301 45     45 0 118 my ($self, $net) = @_;
302 45         73 my $exclude = $net->{exclude};
303 45         86 my $net4 = $net->{ip4};
304 45         65 my $net6 = $net->{ip6};
305 45         100 return $self->_nets_contains_network($net4, $net6, $exclude, 1, "", 0);
306             }
307              
308             sub ditch_cache {
309 291     291 0 516 my ($self) = @_;
310 291 50       974 if (exists $self->{cache}) {
311 0         0 dbg("netset: ditch cache on %s", $self->{name});
312 0         0 delete $self->{cache};
313             }
314             }
315              
316             sub clone {
317 9     9 0 15 my ($self) = @_;
318 9         27 my $dup = Mail::SpamAssassin::NetSet->new($self->{name});
319 9 100       22 if ($self->{nets}) {
320 6         7 @{$dup->{nets}} = @{$self->{nets}};
  6         14  
  6         10  
321             }
322 9 50       19 if ($self->{pt}) {
323 0         0 my $dup_pt = $dup->{pt};
324             $self->{pt}->climb(sub {
325 0     0   0 my $key = $_[0]; $key =~ s/^!//;
  0         0  
326 0 0       0 defined eval { $dup_pt->add_string($key, $_[0]) }
  0         0  
327             or die "Adding a network $_[0] to a patricia trie failed: $@";
328 0         0 1;
329 0         0 });
330             }
331 9         13 $dup->{num_nets} = $self->{num_nets};
332 9         18 return $dup;
333             }
334              
335             ###########################################################################
336              
337             1;