File Coverage

blib/lib/Net/CIDR/ORTC.pm
Criterion Covered Total %
statement 138 152 90.7
branch 66 90 73.3
condition 20 40 50.0
subroutine 19 19 100.0
pod 5 11 45.4
total 248 312 79.4


line stmt bran cond sub pod time code
1             package Net::CIDR::ORTC;
2              
3 2     2   74903 use 5.010;
  2         10  
  2         93  
4 2     2   13 use strict;
  2         4  
  2         969  
5 2     2   14 use warnings;
  2         24  
  2         83  
6              
7 2     2   11 use Carp qw/carp croak/;
  2         4  
  2         267  
8              
9             our $VERSION = '0.02';
10              
11             =head1 NAME
12              
13             Net::CIDR::ORTC - CIDR map compression
14              
15             =head1 SYNOPSIS
16              
17             use Net::CIDR::ORTC;
18              
19             my $map = Net::CIDR::ORTC->new();
20              
21             $map->add('0.0.0.0/0', 0);
22             $map->add('192.168.0.0/24', 'value1');
23             $map->add('192.168.1.0/24', 'value1');
24              
25             $map->compress();
26              
27             my $prefixes = $map->list;
28              
29             foreach (@$prefixes) {
30             say $_->[0] . "\t" . $_->[1];
31             }
32              
33             =head1 DESCRIPTION
34              
35             This module implements Optimal Routing Table Compressor (ORTC) algorithm as described in
36             L.
37              
38             This module intended for offline data processing and not optimal in terms of
39             CPU time and memory usage, but output table should have smallest number of
40             prefixes whits same behaviour (with longest-prefix match lookup).
41              
42             Sometimes this algorithm makes unnecessary changes to input data (prefixes
43             changed, but number of prefixes in output is same as in input), but it is not
44             easy to fix this without making algorithm non-optimal (increasing number of
45             output prefixes in general case).
46              
47             =cut
48              
49 2     2   17 use constant IPv4_BITS => 32;
  2         2  
  2         213  
50 2     2   9 use constant ALL_ONES => 2**IPv4_BITS - 1;
  2         4  
  2         125  
51              
52             # node array fields
53             use constant {
54 2         5638 LEFT => 0,
55             RIGHT => 1,
56             VALUE => 2,
57             OLD_VAL => 3,
58 2     2   10 };
  2         3  
59              
60             sub new {
61 7     7 1 21 my $class = shift;
62 7         23 my $self = bless {}, $class;
63             # tree root node (head)
64 7         24 $self->{root} = [];
65 7         19 return $self;
66             }
67              
68             sub add {
69 40     40 1 878 my $self = shift;
70 40         117 my ($ip, $pref_len) = split '/', shift;
71 40         54 my $value = shift;
72              
73 40 50 33     182 croak 'missing required argument: prefix in ip/len form' unless defined $ip && defined $pref_len;
74 40 50       63 croak 'value should be defined' unless defined $value;
75 40 50 33     371 croak "bad prefix length: $pref_len in prefix $ip/$pref_len" unless $pref_len =~ /^\d+$/ && $pref_len >= 0 && $pref_len <= IPv4_BITS;
      33        
76              
77 40         80 my $i_ip = dd2int($ip);
78 40 50       84 croak "bad ip address: $ip in prefix $ip/$pref_len" unless defined $i_ip;
79 40 50       75 carp "low address bits of $ip/$pref_len are meaningless" unless is_valid_prefix($i_ip, $pref_len);
80              
81 40         70 my $mask = len2mask($pref_len);
82             # start from most significant bit
83 40         49 my $bit_to_test = 1 << (IPv4_BITS - 1);
84              
85 40         64 my $node = $self->{root};
86 40         50 my $next = $self->{root};
87              
88 40         80 while ($bit_to_test & $mask) {
89 535 100       731 if ($i_ip & $bit_to_test) {
90 117         151 $next = $node->[RIGHT]
91             }
92             else {
93 418         491 $next = $node->[LEFT]
94             }
95 535 100       874 last unless defined $next;
96              
97 503         458 $bit_to_test >>= 1;
98 503         874 $node = $next;
99             }
100              
101 40 100       79 if (defined $next) {
102 8 50       19 carp "prefix $ip/$pref_len already exists with value ". $next->[VALUE] if defined $next->[VALUE];
103 8         13 $next->[VALUE] = $value;
104 8         22 return;
105             }
106              
107 32         65 while ($bit_to_test & $mask) {
108 192         231 $next = [];
109 192 100       294 if ($i_ip & $bit_to_test) {
110 49         84 $node->[RIGHT] = $next;
111             }
112             else {
113 143         263 $node->[LEFT] = $next;
114             }
115              
116 192         200 $bit_to_test >>= 1;
117 192         338 $node = $next;
118             }
119 32         116 $node->[VALUE] = $value;
120             }
121              
122             sub remove {
123 1     1 1 2 my $self = shift;
124 1         3 my ($ip, $pref_len) = split '/', shift;
125 1         2 my $value = shift;
126              
127 1 50 33     12 croak "bad prefix length: $pref_len in prefix $ip/$pref_len" unless $pref_len =~ /^\d+$/ && $pref_len >= 0 && $pref_len <= IPv4_BITS;
      33        
128              
129 1         2 my $i_ip = dd2int($ip);
130 1 50       4 croak "bad ip address: $ip in prefix $ip/$pref_len" unless defined $i_ip;
131              
132 1         2 my $mask = len2mask($pref_len);
133             # start from most significant bit
134 1         2 my $bit_to_test = 1 << (IPv4_BITS - 1);
135              
136 1         2 my $node = $self->{root};
137 1         2 my $prev;
138              
139 1   33     7 while ($node && ($bit_to_test & $mask)) {
140 0         0 $prev = $node;
141 0 0       0 if ($i_ip & $bit_to_test) {
142 0         0 $node = $node->[RIGHT];
143             } else {
144 0         0 $node = $node->[LEFT];
145             }
146 0         0 $bit_to_test >>= 1;
147             }
148 1 50       3 return undef unless defined $node;
149              
150 1 50 33     5 if ($node->[LEFT] || $node->[RIGHT]) {
151 1         2 undef $node->[VALUE];
152             } else {
153             # delete leaf node
154 0         0 $bit_to_test <<= 1;
155 0 0       0 if ($i_ip & $bit_to_test) {
156 0         0 undef $prev->[RIGHT];
157             } else {
158 0         0 undef $prev->[LEFT];
159             }
160             }
161 1         3 return 1;
162             }
163              
164             # dump all prefixes into array ref
165             sub list {
166 10     10 1 66 my $self = shift;
167              
168 10         23 my $r = [];
169              
170 10         31 _list($self->{root}, 0, 0, $r);
171              
172 10         70 return $r;
173             }
174              
175             # recursive depth-first preorder tree traversal
176             sub _list {
177 278     278   313 my ($node, $int_ip, $depth, $r) = @_;
178              
179 278 100       447 if (defined $node->[VALUE]) {
180 41         67 my $ip = int2dd($int_ip);
181 41         157 push @$r, [ "$ip/$depth", $node->[VALUE] ];
182             }
183              
184 278         253 $depth++;
185 278 100       638 _list($node->[LEFT], $int_ip, $depth, $r)
186             if $node->[LEFT];
187             # set current bit to 1
188 278 100       562 _list($node->[RIGHT], $int_ip | (1 << IPv4_BITS - $depth), $depth, $r)
189             if $node->[RIGHT];
190             }
191              
192             sub compress {
193 8     8 1 32 my $self = shift;
194              
195 8 50       26 croak 'value for default (0.0.0.0/0) should be defined' unless defined $self->{root}->[VALUE];
196              
197 8         19 pass_one_and_two($self->{root});
198 8         26 pass_three($self->{root});
199             }
200              
201             # internal functions
202              
203             # recursive tree traversal
204             sub pass_one_and_two {
205 414     414 0 489 my ($node, $parent_value) = @_;
206              
207 414 100       829 $parent_value = $node->[VALUE] if defined $node->[VALUE];
208              
209             # expand (deaggregate) tree
210             # if node has exactly one child - create second one
211             # this operation performed in depth-first preorder
212 414 100 100     2184 if ($node->[LEFT] xor $node->[RIGHT]) {
213 180         274 my $new_node = [];
214 180         284 $new_node->[VALUE] = $parent_value;
215 180 100       340 $node->[LEFT] = $new_node unless $node->[LEFT];
216 180 100       388 $node->[RIGHT] = $new_node unless $node->[RIGHT];
217             }
218              
219 414 100       1001 pass_one_and_two($node->[LEFT], $parent_value) if $node->[LEFT];
220 414 100       956 pass_one_and_two($node->[RIGHT], $parent_value) if $node->[RIGHT];
221              
222             # at this point all nodes has two or no children
223              
224             # this operation performed depth-first postorder
225 414 100       811 if ($node->[LEFT]) { # if node has 2 children
226              
227             # compute nexthops(left) # nexthops(right)
228 43         88 my %left = ref $node->[LEFT]->[VALUE] eq 'ARRAY' ?
229 203 100       614 map { $_ => 1 } @{ $node->[LEFT]->[VALUE] } :
  18         38  
230             ( $node->[LEFT]->[VALUE] => 1 );
231 18         37 my %right = ref $node->[RIGHT]->[VALUE] eq 'ARRAY' ?
232 203 100       605 map { $_ => 1 } @{ $node->[RIGHT]->[VALUE] } :
  8         19  
233             ( $node->[RIGHT]->[VALUE] => 1);
234 203         333 my @intersect = grep { $left{$_} } keys %right;
  213         476  
235              
236 203 100       439 if (scalar @intersect == 1) {
    100          
237             # old value don't need for node with single new value
238 177         446 $node->[VALUE] = $intersect[0];
239             }
240             elsif (scalar @intersect > 1) {
241 1 50       6 $node->[OLD_VAL] = $node->[VALUE] if defined $node->[VALUE];
242 1         5 $node->[VALUE] = \@intersect;
243             }
244             else {
245             # intersect empty, use union
246 25 50       61 $node->[OLD_VAL] = $node->[VALUE] if defined $node->[VALUE];
247 25         89 my %union = (%left, %right);
248 25         116 $node->[VALUE] = [ keys %union ];
249             }
250             }
251             }
252              
253             # recursive depth-first preorder traversal
254             sub pass_three {
255 414     414 0 547 my ($node, $parent, $parent_value) = @_;
256              
257 414 100       712 if ($parent_value ~~ $node->[VALUE]) {
258             # parent value is member of node's potential values
259 373         425 undef $node->[VALUE];
260             }
261             else {
262 41 100       104 if (ref $node->[VALUE] ne 'ARRAY') {
263             # only one value, leave it as is
264 34         49 $parent_value = $node->[VALUE];
265             } else {
266             # there are several values
267 7 50       16 if (!defined $node->[OLD_VAL]) {
    0          
268             # there is more than one new values in this node (so this node has children
269             # with different values) but in original tree there is no value for this node
270             # remove this value (prefixes from children will be used)
271 7         17 undef $node->[VALUE];
272             } elsif ($node->[OLD_VAL] ~~ $node->[VALUE]) {
273             # use old value if it found in set of potential new values
274 0         0 $node->[VALUE] = $node->[OLD_VAL];
275 0         0 $parent_value = $node->[VALUE];
276             } else {
277             # last resort: use arbitrary value e. g. first one
278 0         0 $node->[VALUE] = $node->[VALUE]->[0];
279 0         0 $parent_value = $node->[VALUE];
280             }
281             }
282             }
283 414         427 undef $node->[OLD_VAL];
284              
285 414 100       917 pass_three($node->[LEFT], $node, $parent_value) if $node->[LEFT];
286 414 100       862 pass_three($node->[RIGHT], $node, $parent_value) if $node->[RIGHT];
287              
288             # delete empty leaf nodes
289 414 100 100     1784 if (!defined $node->[VALUE] && !$node->[LEFT] && !$node->[RIGHT]) {
290 195 100 100     1152 if (ref $parent->[LEFT] && $parent->[LEFT] == $node) {
    50 33        
291 38         85 undef $parent->[LEFT];
292             } elsif (ref $parent->[RIGHT] && $parent->[RIGHT] == $node) {
293 157         288 undef $parent->[RIGHT];
294             } else {
295 0         0 die 'internal error: bad parent for this node';
296             }
297             }
298             }
299              
300             # utility functions
301              
302             # same as unpack('N*',inet_aton($x));
303             # Parameters:
304             # - ip in dot-decimal form, e. g. 192.0.2.1
305             # Returns:
306             # - undef if ip is bad
307             # - integer ip
308             sub dd2int {
309 63     63 0 233 my @oct = split /\./, $_[0];
310 63 50       148 return undef unless @oct == IPv4_BITS / 8;
311 63         71 my $ip = 0;
312 63         104 foreach(@oct) {
313 252 50 33     924 return undef if $_ > 255 || $_ < 0;
314 252         399 $ip = $ip<<8 | $_;
315             }
316 63         208 return $ip;
317             }
318              
319             # ip from integer to dot-decimal (text) form
320             # reverse to dd2int
321             sub int2dd {
322 54     54 0 4539 return join '.', unpack('C*', pack('N', $_[0]));
323             }
324              
325             # convert prefix length to netmask as integer
326             sub len2mask {
327 91 50 33 91 0 789 die "bad prefix length $_[0]" if $_[0] < 0 || $_[0] > IPv4_BITS;
328 91         294 return ALL_ONES - 2**(IPv4_BITS - $_[0]) + 1;
329             }
330              
331             # $net - is integer
332             # $len - is prefix length 0 .. 32
333             sub is_valid_prefix {
334 44     44 0 59 my ($net, $len) = @_;
335 44         71 return (($net & len2mask($len)) == $net);
336             }
337              
338             1;
339             __END__