File Coverage

blib/lib/Cisco/ACL.pm
Criterion Covered Total %
statement 181 243 74.4
branch 54 82 65.8
condition 9 12 75.0
subroutine 21 23 91.3
pod 2 16 12.5
total 267 376 71.0


line stmt bran cond sub pod time code
1             #
2             # $Id: ACL.pm 86 2004-06-18 20:18:01Z james $
3             #
4              
5             =head1 NAME
6              
7             Cisco::ACL - generate access control lists for Cisco IOS
8              
9             =head1 SYNOPSIS
10              
11             =for example begin
12              
13 1     1   1694 use Cisco::ACL;
  1         4  
  1         85  
14             my $acl = Cisco::ACL->new(
15             permit => 1,
16             src_addr => '10.1.1.1/24',
17             dst_addr => '10.1.2.1/24',
18             );
19             print "$_\n" for( $acl->acls );
20              
21             =for example end
22              
23             =head1 DESCRIPTION
24              
25             Cisco::ACL is a module to create cisco-style access lists. IOS uses a
26             wildcard syntax that is almost but not entirely unlike netmasks, but
27             backwards (at least that's how it has always seemed to me).
28              
29             This module makes it easy to think in CIDR but emit IOS-compatible access
30             lists.
31              
32             =cut
33              
34             package Cisco::ACL;
35              
36 6     6   4930 use strict;
  6         17  
  6         239  
37 6     6   33 use warnings;
  6         14  
  6         332  
38              
39             our $VERSION = '0.12';
40              
41 6     6   44 use Carp qw|croak|;
  6         11  
  6         418  
42 6     6   9032 use Params::Validate qw|:all|;
  6         75238  
  6         1626  
43              
44             # set up class methods
45             use Class::MethodMaker(
46 6         77 new_with_init => 'new',
47             boolean => [ qw|
48             permit
49             established
50             |],
51             get_set => [ qw|
52             protocol
53             |],
54             list => [ qw|
55             src_port
56             dst_port
57             src_addr
58             dst_addr
59             |],
60 6     6   5603 );
  6         137508  
61              
62             # initialize a newly constructed object
63             sub init
64             {
65              
66 15     15 0 14642 my $self = shift;
67            
68             # validate args
69 15         993 my %args = validate(@_,{
70             permit => { type => BOOLEAN,
71             optional => 1 },
72             deny => { type => BOOLEAN,
73             optional => 1 },
74             established => { type => BOOLEAN,
75             default => 0 },
76             src_addr => { type => SCALAR|ARRAYREF,
77             optional => 1 },
78             dst_addr => { type => SCALAR|ARRAYREF,
79             optional => 1 },
80             src_port => { type => SCALAR|ARRAYREF,
81             optional => 1 },
82             dst_port => { type => SCALAR|ARRAYREF,
83             optional => 1 },
84             protocol => { type => SCALAR,
85             optional => 1 },
86             });
87              
88             # permit and deny are mutually exclusive
89 13 100 100     205 if( exists $args{permit} && exists $args{deny} ) {
90 1         12 croak "'permit' and 'deny' are mutually exclusive";
91             }
92            
93             # do we have allow and is it true?
94 12 100 100     113 if( exists $args{permit} && $args{permit} ) {
95 4         142 $self->permit(1);
96             }
97            
98             # do we only want to match established sessions
99 12 50 33     127 if( exists $args{established} && $args{established} ) {
100 0         0 $self->established(1);
101             }
102              
103             # populate the object
104 12         487 $self->protocol( $args{protocol} );
105 12         157 for( qw|src_addr src_port dst_addr dst_port| ) {
106 48 100 66     721 if( ref $args{$_} eq 'ARRAY' && @{ $args{$_} } ) {
  32 50       111  
107 32         35 $self->$_( @{ $args{$_} } );
  32         1047  
108             }
109             elsif( $args{$_} ) {
110 0         0 $self->$_( $args{$_} );
111             }
112            
113             }
114              
115 12         574 return $self;
116              
117             }
118              
119             # generate the access lists
120             sub acls
121             {
122              
123 20     20 1 24599 my $self = shift;
124            
125             # generate the ACLs
126 20         57 my $acls = $self->_generate();
127            
128 20 50       82 return wantarray ? @{ $acls } : $acls;
  0         0  
129              
130             }
131              
132             # reset the object attributes
133             sub reset
134             {
135              
136 2     2 1 1889 my $self = shift;
137              
138 2         74 $self->clear_permit;
139 2         87 $self->clear_established;
140 2         82 $self->clear_protocol;
141 2         91 $self->clear_src_addr;
142 2         87 $self->clear_src_port;
143 2         76 $self->clear_dst_addr;
144 2         74 $self->clear_dst_port;
145              
146             }
147              
148             ## all code below here is from the original acl.pl with minor tweaks
149             sub _generate
150             {
151              
152 20     20   67 my $self = shift;
153 20 100       658 my @source_addr_elements = breakout_addrs(
154             $self->src_addr_count ? $self->src_addr : 'any'
155             );
156 20 100       662 my @destinatione_addr_elements = breakout_addrs(
157             $self->dst_addr_count ? $self->dst_addr : 'any'
158             );
159 20 100       675 my @source_port_elements = breakout_ports(
160             $self->src_port_count ? $self->src_port : 'any'
161             );
162 20 100       648 my @destination_port_elements = breakout_ports(
163             $self->dst_port_count ? $self->dst_port : 'any'
164             );
165              
166 20         37 my @rules;
167 20         33 for my $current_src_addr (@source_addr_elements) {
168 26         37 for my $current_dst_addr (@destinatione_addr_elements) {
169 26         37 for my $current_src_port (@source_port_elements) {
170 26         43 for my $current_dst_port (@destination_port_elements) {
171 30 100       1229 my $rule = make_rule(
172             $self->permit,
173             $self->protocol ? $self->protocol : 'tcp',
174             $current_src_addr,
175             $current_dst_addr,
176             $current_src_port,
177             $current_dst_port,
178             $self->established,
179             );
180 30         140 push @rules, $rule;
181             }
182             }
183             }
184             };
185            
186 20         73 return \@rules;
187              
188             #
189             #-------------------------------------------------------------------
190             #
191              
192             sub make_rule {
193            
194             # Return the rule as a string, withOUT a final CR.
195              
196 30     30 0 3488 my($action, $protocol, $src_addr, $dst_addr,
197             $src_port, $dst_port, $established) = @_;
198              
199             # $src_port and $dst_port are ready to be inserted in the rule string
200             # as is; the clean_input routine prepared them, including prepending
201             # "eq ". They will be "" if the port was "any".
202              
203 30         39 my ($rule_string,$src_elem,$dst_elem,$src_p_elem,$dst_p_elem);
204              
205 30 50       102 if ($protocol eq "both") {
206 0         0 $protocol = "ip";
207             };
208              
209 30 100       65 $rule_string = $action ? "permit" : "deny";
210 30         64 $rule_string .= " $protocol ";
211              
212 30 100       167 if ($src_addr =~ /\//) {
    100          
213 18         34 $src_elem = parse_cidr($src_addr);
214             }
215             elsif ($src_addr =~ /any/) {
216 5         11 $src_elem = "any";
217             }
218             else {
219 7         31 $src_elem = "host $src_addr";
220             };
221              
222 30 100       113 if ($dst_addr =~ /\//) {
    100          
223 4         10 $dst_elem = parse_cidr($dst_addr);
224             }
225             elsif ($dst_addr =~ /any/) {
226 16         27 $dst_elem = "any";
227             }
228             else {
229 10         18 $dst_elem = "host $dst_addr";
230             };
231              
232 30 100       79 if ($src_port =~ /any/) {
233 23         33 $src_p_elem = "";
234             }
235             else {
236 7         10 $src_p_elem = $src_port;
237             };
238              
239 30 100       80 if ($dst_port =~ /any/) {
240 12         14 $dst_p_elem = "";
241             }
242             else {
243 18         28 $dst_p_elem = $dst_port;
244             };
245              
246 30         79 $rule_string .= "$src_elem $src_p_elem $dst_elem $dst_p_elem";
247 30 100       68 if( $established ) {
248 2         3 $rule_string .= " established";
249             }
250 30         307 $rule_string =~ s/\s+/ /g;
251 30         133 $rule_string =~ s/\s+$//;
252 30         71 return $rule_string;
253              
254             };
255              
256             #
257             #-------------------------------------------------------------------
258             #
259              
260             sub breakout_addrs {
261              
262             # Split on commas, return a list where every element is either a
263             # single address or a single cidr specification.
264              
265 40     40 0 1804 my @list = @_;
266 40 100       131 if ($list[0] =~ /any/) { return("any"); };
  15         41  
267              
268 25         36 my (@elements,$addr,@endpoints,@octets1,@octets2,$start,$end,$i,
269             $number_of_endpoints,$number_of_octets,$done,$dec_start,$dec_end,@george,$remaining);
270              
271 25         43 foreach $addr( @list ) {
272 31 50       66 if ($addr !~ /\-/) {
273 31         77 push @elements, $addr; # Not a range and we're returning single addresses and
274             # cidr notation as is, so nothing to do
275             }
276             else {
277 0         0 @endpoints = split(/\-/, $addr);
278 0         0 $number_of_endpoints = @endpoints;
279 0 0       0 if ($number_of_endpoints != 2) {
280 0         0 next; # something is screwey; probably something like
281             # 10.10.10.10-20-30. Silently shitcan it.
282             };
283              
284             # Two cases left; x.x.x.x-y.y.y.y and x.x.x.x-y
285             #
286 0         0 @octets2 = split(/\./, $endpoints[1]);
287 0         0 $number_of_octets = @octets2;
288 0 0       0 if ($number_of_octets == 4) {
289 0         0 $dec_start = ip_to_decimal($endpoints[0]);
290 0         0 $dec_end = ip_to_decimal($endpoints[1]);
291 0         0 push @elements, ferment("$dec_start-$dec_end");
292             }
293             else {
294 0         0 @octets1 = split(/\./, $endpoints[0]);
295 0         0 my $newend = "$octets1[0].$octets1[1].$octets1[2].$octets2[0]";
296 0         0 $dec_start = ip_to_decimal($endpoints[0]);
297 0         0 $dec_end = ip_to_decimal($newend);
298 0         0 push @elements, ferment("$dec_start-$dec_end");
299             }
300             }
301             }
302 25         79 return(@elements);
303             }
304              
305             #
306             #-------------------------------------------------------------------
307             #
308              
309             sub breakout_ports {
310 40     40 0 1715 my @list = @_;
311 40         45 my ($tidbit,@endpoints,$start,$end,$i,$number_of_endpoints,@elements);
312            
313 40         71 foreach $tidbit( @list ) {
314              
315 42 100       101 if ($tidbit =~ /\-/) {
316              
317 6         18 @endpoints = split(/\-/, $tidbit);
318            
319 6         9 $number_of_endpoints = @endpoints;
320 6 50       26 if ($number_of_endpoints != 2) {
321 0         0 next;
322             };
323            
324 6         8 $start = $endpoints[0];
325 6         8 $end = $endpoints[1];
326            
327             # flip range ends if they are backward
328 6 100       19 if ($start >= $end) {
329 4         11 ($start, $end) = ($end, $start);
330             };
331            
332 6         19 push @elements, "range $start $end";
333            
334             }
335             else {
336            
337 36         104 push @elements, "eq $tidbit";
338            
339             }
340             };
341            
342 40         133 return(@elements);
343            
344             };
345            
346             #
347             #-------------------------------------------------------------------
348             #
349              
350             sub parse_cidr {
351 22     22 0 31 my $bob = $_[0];
352 22         22 my ($address, $block, $start, $end, $mask, $rev_mask);
353 22         79 ($address, $block) = split(/\//, $bob);
354 22         46 ($start, $end) = ip_to_endpoints($address, $block);
355 22         48 $mask = find_mask($block);
356 22         37 my $bin_mask = ip_to_bin($mask);
357 22         215 my @bits = split(//, $bin_mask);
358 22         56 foreach my $toggle_bait (@bits) {
359 704 100       949 if ($toggle_bait eq "1") {
360 510         589 $toggle_bait = "0";
361             }
362             else {
363 194         236 $toggle_bait = "1";
364             };
365             };
366 22         70 my $inv_bin = join "",@bits;
367 22         39 my $inv_mask = bin_to_ip($inv_bin);
368 22         127 return "$start $inv_mask ";
369             }
370              
371             #
372             #-------------------------------------------------------------------
373             #
374              
375             sub ferment {
376              
377             # Ferment = "cidr-ize" the address range (ha ha, ok, I'll keep
378             # my day job.) Take the range given as xxxx-yyyy (it's decimal!!)
379             # and find the most concise way to express it in cidr notation.
380              
381             # Return: The list of elements, or "" if the range given was ""
382              
383             # Arguments: the range, the list of elements to add to.
384              
385 0     0 0 0 my $range = shift(@_);
386 0         0 my @list_to_date = @_;
387 0         0 my ($start,$end,$difference,$i,$got_it,@working_list,
388             $trial_start,$trial_end,$dotted_start,$block_found,$remaining_range);
389              
390 0 0       0 if ($range eq "") { return(@list_to_date) }; # an end condition
  0         0  
391              
392 0         0 ($start, $end) = split(/\-/, $range);
393 0         0 $difference = $end - $start;
394              
395 0 0       0 if ($difference == 0) {
396              
397             # The range is one address (i.e. start and end are the same);
398             # return it in dotted notation and we're at another end condition.
399              
400 0         0 push @list_to_date, decimal_to_ip($start);
401 0         0 return(@list_to_date);
402             };
403              
404 0         0 $got_it = 0;
405 0         0 for ($i = 1; $i < 31; $i++) {
406              
407             # We'll only try to put 1 block per call of this subroutine
408 0 0       0 if ($got_it) { last };
  0         0  
409              
410             # Using the cidr size for this loop iteration, calculate what
411             # the block of that size would be for the start address we
412             # have, then compare that to the range we're looking for.
413             #
414 0         0 ($trial_start, $trial_end) = ip_to_endpoints(decimal_to_ip($start),$i); # dotted
415 0         0 $trial_start = ip_to_decimal($trial_start); # now decimal
416 0         0 $trial_end = ip_to_decimal($trial_end);
417              
418             #
419             # Ok, now these are in decimal
420             #
421 0 0       0 if ($trial_start == $start) {
422             # Woo hoo, the start of the range is aligned with a cidr boundary.
423             # Is it the right one? We know it's the biggest possible,
424             # but it may be too big. If so, just move on to the next
425             # $i (i.e. next smaller sized block) and try again.
426             #
427 0 0       0 if ($trial_end > $end) { next; };
  0         0  
428              
429             # otherwise, it's the money...
430             #
431 0         0 $got_it = 1;
432 0         0 $dotted_start = decimal_to_ip($start);
433 0         0 $block_found = "$dotted_start/$i";
434 0         0 $start += (($trial_end - $start) + 1);
435             #
436             # Ok, now we've reduced the range by the amount of space
437             # in the block we just found.
438             #
439             # The extra '+1' above means that the next start point
440             # will be one address beyond the end of the block we
441             # just found (otherwise we'd find a few individual addresses
442             # twice). However, it also means that for the final block,
443             # $start is > $end by 1. We have to check for that before
444             # returning the values; if we let it through we'll
445             # spin forever...
446             #
447             }
448             else {
449 0         0 next; # try the next smaller size block
450             }
451             } # for loop
452              
453             # Ok, we're done trying cidr blocks. If we found one, return it
454             # and the remaining range. Otherwise, return 1 address and the
455             # remaining range.
456              
457 0 0       0 if ($got_it) {
458             # We already calculated $block_found
459 0         0 $remaining_range = "$start-$end";
460 0 0       0 if ($start > $end) { $remaining_range = "" }
  0         0  
461             }
462             else {
463 0         0 $block_found = decimal_to_ip($start);
464 0         0 $start++;
465 0         0 $remaining_range = "$start-$end";
466 0 0       0 if ($start > $end) { $remaining_range = "" }
  0         0  
467             }
468              
469 0         0 push @list_to_date, $block_found;
470 0         0 return(ferment($remaining_range,@list_to_date));
471              
472             };
473              
474             #
475             #-------------------------------------------------------------------
476             #
477              
478             sub ip_to_endpoints {
479             #
480             # Various of these routings use strings for bit masks where
481             # it would undoubtedly be much more efficient to use real binary
482             # data, but... it's fast enough, and this was easier. :)
483             #
484 22     22 0 21 my($address,$cidr,$zeros,$ones,$bin_address);
485 22         28 $address = $_[0];
486 22         39 $bin_address = ip_to_bin($address);
487 22         28 $cidr = $_[1];
488 22         29 $zeros = "00000000000000000000000000000000";
489 22         22 $ones = "11111111111111111111111111111111";
490 22         53 for(my $i=0; $i<=($cidr-1); $i++) {
491 510         606 substr($zeros,$i,1) = substr($bin_address,$i,1);
492 510         1033 substr($ones,$i,1) = substr($bin_address,$i,1)
493             };
494 22         40 return(bin_to_ip($zeros), bin_to_ip($ones));
495             };
496              
497             ###########################################################################
498              
499             sub find_mask {
500 22     22 0 23 my($cidr,$bin,$i);
501 22         26 $cidr = $_[0];
502 22         25 $bin = "00000000000000000000000000000000";
503 22         48 for ($i=0; $i<=31; $i++) {
504 704 100       1430 if ($i <= ($cidr-1)) {
505 510         935 substr($bin,$i,1) = "1"
506             }
507             }
508 22         43 my $mask = bin_to_ip($bin);
509 22         42 return($mask);
510             };
511              
512             ############################################################################
513              
514             sub ip_to_decimal {
515 44     44 0 48 my($address, $i, $a, $b, $c, $d);
516 44         65 $address = shift(@_);
517 44         125 ($a, $b, $c, $d) = split(/\./, $address);
518 44         113 $i = (256**3)*$a + (256**2)*$b + 256*$c + $d ;
519 44         61 return($i);
520             };
521              
522             ############################################################################
523             #
524             # Ok, so, it's a hack... sue me. :)
525             #
526              
527             sub decimal_to_ip {
528 0     0 0 0 return bin_to_ip(decimal_to_bin($_[0]));
529             };
530              
531             ############################################################################
532              
533             sub decimal_to_bin {
534 44     44 0 42 my($decimal,@bits,$i,$bin_string);
535 44         55 $decimal = $_[0];
536 44         67 @bits = "";
537 44         94 for ($i=0;$i<=31;$i++) {
538 1408         2649 $bits[$i] = "0";
539             };
540 44 50       91 if ($decimal >= 2**32) {
541 0         0 die "Error: exceeded MAXINT.\n\n";
542             };
543            
544 44         112 for ($i=0; $i<=31; $i++) {
545 1408 100       3157 if ($decimal >= 2**(31 - $i)) {
546 672         721 $bits[$i] = "1";
547 672         1427 $decimal -= 2**(31 - $i);
548             }
549             };
550              
551 44         63 $bin_string = "";
552 44         130 $bin_string = join('',@bits);
553              
554 44 50       81 if ($decimal != 0) {
555 0         0 print "\nWARNING!!\nDANGER, WILL ROBINSON!!\nTHERE IS A GRUE NEARBY!!\n\n";
556 0         0 print "A really simple check of decimal-to binary conversion choked!\n\n";
557 0         0 print "Decimal value (expected zero): $decimal\nBinary result: $bin_string\n";
558 0         0 die "\nSuddenly the lights go out...\n\nYou hear a grumbling sound...\n\nYou have been eaten by a grue.\n\n";
559             };
560 44         130 return($bin_string);
561             };
562              
563             ##############################################################
564              
565             sub bin_to_ip {
566 88     88 0 87 my($bin,$ip,@octets,$binoct1,$binoct2,$binoct3,$binoct4,$address);
567 88         109 $bin = $_[0];
568 88         226 @octets = "";
569 88         128 $binoct1 = substr($bin,0,8);
570 88         108 $binoct2 = substr($bin,8,8);
571 88         100 $binoct3 = substr($bin,16,8);
572 88         107 $binoct4 = substr($bin,24,8);
573 88         134 $octets[0] = bin_to_decimal($binoct1);
574 88         152 $octets[1] = bin_to_decimal($binoct2);
575 88         171 $octets[2] = bin_to_decimal($binoct3);
576 88         149 $octets[3] = bin_to_decimal($binoct4);
577 88         290 $address = join('.',@octets);
578 88         273 return($address);
579             };
580              
581             ##############################################################
582             # ip_to_bin
583             #
584              
585             sub ip_to_bin {
586 44     44 0 50 my($ipaddr,$x,$y);
587 44         96 $ipaddr = $_[0];
588 44         74 $x = ip_to_decimal($ipaddr);
589 44         81 $y = decimal_to_bin($x);
590 44         77 return($y);
591             };
592              
593             ############################################################################
594              
595             sub bin_to_decimal {
596              
597             # Assume 8-bit unsigned integer max
598             # This is only meant to be called from bin_to_ip
599              
600 352     352 0 333 my($binary,$decimal,$i,$power,$bit,$total);
601 352         363 $binary = $_[0];
602 352         345 $total = 0;
603 352         661 for ($i=0; $i<=7; $i++) {
604 2816         2610 $power = 7 - $i;
605 2816         3203 $bit = substr($binary,$i,1);
606 2816 100       5908 if ($bit) {
607 1158         2353 $total += 2**$power;
608             }
609             };
610 352         621 return($total);
611             };
612              
613             }
614              
615             # keep require happy
616             1;
617              
618              
619             __END__