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   254 use strict;
  40         84  
  40         1246  
22 40     40   192 use warnings;
  40         92  
  40         1748  
23             # use bytes;
24 40     40   1214 use re 'taint';
  40         112  
  40         1381  
25 40     40   243 use Time::HiRes qw(time);
  40         69  
  40         263  
26 40     40   31946 use NetAddr::IP 4.000;
  40         898986  
  40         273  
27              
28 40     40   22445 use Mail::SpamAssassin::Util;
  40         117  
  40         2647  
29 40     40   300 use Mail::SpamAssassin::Logger;
  40         76  
  40         3909  
30              
31             our $have_patricia;
32             BEGIN {
33 40     40   142 eval {
34 40         92204 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 438     438 0 1035 my ($class,$netset_name) = @_;
45 438   33     1792 $class = ref($class) || $class;
46              
47 438 100       1047 $netset_name = '' if !defined $netset_name; # object name for debugging
48 438         2280 my $self = {
49             name => $netset_name, num_nets => 0,
50             cache_hits => 0, cache_attempts => 0,
51             };
52 438 50       1200 $self->{pt} = Net::Patricia->new(&AF_INET6) if $have_patricia;
53              
54 438         876 bless $self, $class;
55 438         1067 $self;
56             }
57              
58             ###########################################################################
59              
60             sub DESTROY {
61 321     321   6308 my($self) = shift;
62 321 100       44987 if (exists $self->{cache}) {
63 86         451 local($@, $!, $_); # protect outer layers from a potential surprise
64 86         290 my($hits, $attempts) = ($self->{cache_hits}, $self->{cache_attempts});
65             dbg("netset: cache %s hits/attempts: %d/%d, %.1f %%",
66 86 50       537 $self->{name}, $hits, $attempts, 100*$hits/$attempts) if $attempts > 0;
67             }
68             }
69              
70             ###########################################################################
71              
72             sub add_cidr {
73 616     616 0 2317 my ($self, @nets) = @_;
74              
75 616   100     2968 $self->{nets} ||= [ ];
76 616         982 my $numadded = 0;
77 616         1022 delete $self->{cache}; # invalidate cache (in case of late additions)
78              
79 616         1292 foreach my $cidr_orig (@nets) {
80 616         1152 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 616         3141 local($1,$2,$3,$4);
88 616         2097 $cidr =~ s/^\s+//;
89 616 100       1942 my $exclude = ($cidr =~ s/^!\s*//) ? 1 : 0;
90              
91 616         830 my $masklen; # netmask or a prefix length
92 616 100       3665 $masklen = $1 if $cidr =~ s{ / (.*) \z }{}xs;
93              
94             # discard optional brackets
95 616 50       1786 $cidr = $1 if $cidr =~ /^ \[ ( [^\]]* ) \] \z/xs;
96              
97 616         1265 my $scope;
98             # IPv6 Scoped Address (RFC 4007, RFC 6874, RFC 3986 "unreserved" charset)
99 616 50       1574 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 616         914 my $is_ip4 = 0;
107 616 100       2327 if ($cidr =~ /^ \d+ (\. | \z) /x) { # looks like an IPv4 address
108 342 100       2258 if ($cidr =~ /^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z/x) {
    100          
    100          
    50          
109             # also strips leading zeroes, not liked by inet_pton
110 268         2295 $cidr = sprintf('%d.%d.%d.%d', $1,$2,$3,$4);
111 268 100       942 $masklen = 32 if !defined $masklen;
112             } elsif ($cidr =~ /^ (\d+) \. (\d+) \. (\d+) \.? \z/x) {
113 8         52 $cidr = sprintf('%d.%d.%d.0', $1,$2,$3);
114 8 100       27 $masklen = 24 if !defined $masklen;
115             } elsif ($cidr =~ /^ (\d+) \. (\d+) \.? \z/x) {
116 35         239 $cidr = sprintf('%d.%d.0.0', $1,$2);
117 35 100       150 $masklen = 16 if !defined $masklen;
118             } elsif ($cidr =~ /^ (\d+) \.? \z/x) {
119 31         187 $cidr = sprintf('%d.0.0.0', $1);
120 31 100       218 $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 342         655 $is_ip4 = 1;
126             }
127              
128 616 50       1565 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 616 100       1999 $cidr .= '/' . $masklen if defined $masklen;
150              
151 616         4185 my $ip = NetAddr::IP->new($cidr);
152 616 50       129859 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 616         1119 my ($ip4, $ip6);
160 616 100       1475 if ($is_ip4) {
161 342         529 $ip4 = $ip;
162 342         1379 $ip6 = $self->_convert_ipv4_cidr_to_ipv6($cidr);
163             } else {
164 274         522 $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 616 50       70747 if (scalar @{$self->{nets}} < 200) {
  616         2424  
171 616 100       2178 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 608         925 push @{$self->{nets}}, {
  608         3556  
176             exclude => $exclude,
177             ip4 => $ip4,
178             ip6 => $ip6,
179             as_string => $cidr_orig,
180             };
181 608         3370 $numadded++;
182             }
183              
184 616         1182 $self->{num_nets} += $numadded;
185 616         1630 $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 426     426   1347 my ($self, $cidr) = @_;
195              
196             # only do this for IPv4 addresses
197 426 50       2632 return unless $cidr =~ /^\d+[.\/]/;
198              
199 426 100       1848 if ($cidr !~ /\//) { # no mask
200 84         495 return NetAddr::IP->new6("::ffff:".$cidr);
201             }
202              
203             # else we have a CIDR mask specified. use new6() to do this
204             #
205 342         1622 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 342 50 33     65514 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 342         1744 return NetAddr::IP->new6("::ffff:$1");
216             }
217              
218             sub _nets_contains_network {
219 661     661   113114 my ($self, $net4, $net6, $exclude, $quiet, $netname, $declared) = @_;
220              
221 661 50       2076 return 0 unless (defined $self->{nets});
222              
223 661         1135 foreach my $net (@{$self->{nets}}) {
  661         1957  
224             # check to see if the new network is contained by the old network
225 463   100     2155 my $in4 = defined $net4 && defined $net->{ip4} && $net->{ip4}->contains($net4);
226 463   66     5358 my $in6 = defined $net6 && defined $net->{ip6} && $net->{ip6}->contains($net6);
227 463 100 100     11037 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       206 $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     1832 return 0 if (!$declared && $net->{exclude});
235 46         324 return 1;
236             }
237             }
238 614         2411 return 0;
239             }
240              
241             sub is_net_declared {
242 616     616 0 1489 my ($self, $net4, $net6, $exclude, $quiet) = @_;
243 616   66     3110 return $self->_nets_contains_network($net4, $net6, $exclude,
244             $quiet, $net4 || $net6, 1);
245             }
246              
247             sub contains_ip {
248 139     139 0 527 my ($self, $ip) = @_;
249 139         263 my $result = 0;
250              
251 139 100       400 if (!$self->{num_nets}) { return 0 }
  22         113  
252              
253 117         212 $self->{cache_attempts}++;
254 117 100 100     636 if ($self->{cache} && exists $self->{cache}{$ip}) {
    50          
255             dbg("netset: %s cached lookup on %s, %d networks, result: %s",
256 17         110 $self->{name}, $ip, $self->{num_nets}, $self->{cache}{$ip});
257 17         54 $self->{cache_hits}++;
258 17         120 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         314 my $t0 = time;
278 100         172 my ($ip4, $ip6);
279 100 100       547 if ($ip =~ /^\d+\./) {
280 84         491 $ip4 = NetAddr::IP->new($ip);
281 84         12812 $ip6 = $self->_convert_ipv4_cidr_to_ipv6($ip);
282             } else {
283 16         79 $ip6 = NetAddr::IP->new($ip);
284             }
285 100         30216 foreach my $net (@{$self->{nets}}) {
  100         349  
286 196 100 100     4858 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         1437 $result = !$net->{exclude};
289 70         127 last;
290             }
291             }
292             dbg("netset: %s lookup on %s, %d networks, result: %s, %.3f ms",
293 100         1391 $self->{name}, $ip, $self->{num_nets}, $result, 1000*(time - $t0));
294             }
295              
296 100         405 $self->{cache}{$ip} = $result;
297 100         514 return $result;
298             }
299              
300             sub contains_net {
301 45     45 0 127 my ($self, $net) = @_;
302 45         79 my $exclude = $net->{exclude};
303 45         79 my $net4 = $net->{ip4};
304 45         82 my $net6 = $net->{ip6};
305 45         116 return $self->_nets_contains_network($net4, $net6, $exclude, 1, "", 0);
306             }
307              
308             sub ditch_cache {
309 291     291 0 519 my ($self) = @_;
310 291 50       1041 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         36 my $dup = Mail::SpamAssassin::NetSet->new($self->{name});
319 9 100       18 if ($self->{nets}) {
320 6         9 @{$dup->{nets}} = @{$self->{nets}};
  6         18  
  6         10  
321             }
322 9 50       20 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         22 return $dup;
333             }
334              
335             ###########################################################################
336              
337             1;