#!/usr/bin/perl -w

use strict;

# $Id: aclxml2.pl,v 1.37 2002-08-22 19:30:37-05 bobn Exp bobn $
# Generate access lists for Cisco router from XML files

# you will be much happier viewing this file if
# you SET TABSTOPS to 4.


select STDERR; $| = 1;
select STDOUT; $| = 1;

use Data::Dumper;
$Data::Dumper::Deepcopy = 1;

use Getopt::Long;
use Socket;
use XML::Parser;
use Net::Netmask;
use constant MAXBITS => 32; # IPv4

# globals upper case...
use vars qw( %GROUP %SYNONYMS  $XMLORIG %IP %NAME $INFILE %LANS %INTS @NETNAMES $TEST_ACLS
				$SHO_USAGE $DEBUG $ALL_ONES @MAKEPARMS %ACCESS $REFLEXIVE 
				@INTFCS $CUR_GROUP @NETNAMES_SORT $IP_RE $IP_RANGE_RE %OPT_CTL 
				$IGNORING $PASS1 $NO_EXTERNAL %NO_AUTOGROUP);

%NAME = %IP =  @NETNAMES = %NO_AUTOGROUP = ();
$CUR_GROUP = '';
$PASS1 = $NO_EXTERNAL = 0;

$IP_RE = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
$IP_RANGE_RE = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d+';
# no parentheses so later routines can use them as needed

# values for $REFLEXIVE
use constant NO_RETURN => -1;
use constant REFLEXIVE => 1;
use constant NON_REFLEXIVE => 0;


use constant RETURN_ONLY => 1;

%OPT_CTL = (       
              	"use_reflexive_ACLs|x!", \$REFLEXIVE,
              			"show_usage|h!", \$SHO_USAGE,
              				"debug|d=i", \$DEBUG,
          "create_test_acls_on_ATM|ta!", \$TEST_ACLS,
 #   "no_acl_if_both_ends_external|ne!", \$NO_EXTERNAL,
);

( $SHO_USAGE, $REFLEXIVE, $TEST_ACLS, $DEBUG) = ( '', 0, 0, 0, );

if ( not GetOptions(%OPT_CTL) or $SHO_USAGE ) { usage() }

# 'self-document' switch options
sub usage
{
    my (@f, %switch, $out, ) = ( );

	$out = "\nUsage: $0 [ -x ] [ -ta ] [ -d number ] file [file...] \n
			where the files are input XML files that define hosts, 
			network connectivity, groups, synonyms and ACLs.

			Options are:\n\n";

	$out =~ s/^[\cI ]*//mg;
	$out .= "\n";

    for ( keys %OPT_CTL )
    {
        @f = split(/\||=|!/);
        $switch{$f[1]} = $f[0];
    }
    for ( sort keys %switch )
    {
        my $def = $switch{$_};
        $def =~ s/\_/ /g;
        $_ = '-' . $_;
        $out .= sprintf "%4s: %s\n", $_, $def;
    }
    print "$out\n";
    exit;
}

$IGNORING = 0;
$ALL_ONES = inet_aton('255.255.255.255');

##########
#
# Process the input XML files listed on the command line.
# parses each one individually to alllow error messages to
# say which file the parse error is in (usually).
#
for $INFILE (@ARGV)
{
	warn "parsing $INFILE\n";
	open(IN, "<$INFILE");

	$XMLORIG ='';

	# get file into a string for use in helpfull error messages
	while (<IN>) { $XMLORIG .= $_ ; }
	close IN;

	my $p1 = new XML::Parser(Handlers => { Start => \&handle_start,
											End => \&handle_end, } ); 
	# parse XML file										
	eval { my $p2 = $p1->parsefile($INFILE, ErrorContext => 3) };

	# if XML was not well-formed
	if ($@)
	{
		print STDERR "\nwhile parsing file '$INFILE':\n";
		$@ =~ s/at\s+\S+\s+line\s+\d+//; # suppress program line number that might confuse user
		die "$@\n"; 
	}
}

print STDERR Data::Dumper->Dump( [ \%LANS, \%INTS, \%IP, \%NAME, \%GROUP, \%SYNONYMS, ], 
							[ qw(*LANS *INTS *IP *NAME *GROUP *SYNONYMS) ] ) if $DEBUG;

##############
#
# receives start-of-tag events.  Receives an expat object, the name of the element and the 
# attributes (as name,val,name,val...) - looks at the element name and generally passes 
# these on for the specific element handlers.
#
sub handle_start
{
	my ( $x, $elname, %attrib, ) = @_;

	for my $t ( $elname )
	{
		# since XML comments don't nest, I provide a separate mechanism.  The <IGNORE>
		# and <IGNORE/>  elements mark the beginning and end, respectively, 
		# of a block to be ignored.  THESE NEST. 

		# What's in between <IGNORE> and </IGNORE> must still be well-formed XML. 
		
		for ($x->context) { return if /^IGNORE$/; }
		# if contained within an IGNORE element, do so.
		
		return if ( $t =~ /^(hosts|acls|nets|groups|synonyms|IGNORE)$/ );

		$t eq 'host' 		&& do { proc_host(@_); 		return; };
		$t eq 'group' 		&& do { proc_group(@_); 	return; };
		$t eq 'synonym' 	&& do { proc_synonym(@_); 	return; };
		$t eq 'net' 		&& do { proc_net(@_); 		return; };
		$t eq 'acl' 		&& do { proc_acl(@_); 		return; };
		$t eq 'member' 		&& do { proc_member(@_); 	return; };

		die "UNKNOWN ELEMENT: '$t' in main loop in file: '$INFILE'\n"; 
	}
	return;
}
		
print STDERR Data::Dumper->Dump( [ \%LANS, \%INTS, \%IP, \%NAME, \%GROUP, \%SYNONYMS, ], 
							[ qw(*LANS *INTS *IP *NAME *GROUP *SYNONYMS) ] ) if $DEBUG;
print STDERR Data::Dumper->Dump( [ \%ACCESS, ], 
							[ qw(*ACCESS) ] ) if $DEBUG;

##########
#
#
sub handle_end
{
	my ( $x, $elname, %attrib, ) = @_;
	$CUR_GROUP = '' if $elname eq 'group';
	# clear group so that lone <member> elements don't end up in the wrong place.

	# sort the @NETNAMES, used to order searches in %LANS,
	# so that the most specific match (longest netmask) happens first.
	# and auto-create the groups of hosts in each network.
	if ($elname eq 'net' and exists $LANS{DEFAULT} )
	{
		sort_nets();
		make_net_groups();
	}
}

####
#
# expects a string which is 1 thing  or multiple CSV, and optionally a number which is used to
# limit recursion due to looped definitions. (the number is added in recursive calls.)
# since aliases (<synonyms>) can be anything, no consistency checking is done.
# returns an array conatining everything thatn the expansion created.
# synonyms can be nested.
#
sub expand_alias
{
	my ( $in, $recurse) = @_;
	my @ret = ();
	my @temp;
	$recurse = ( (defined $recurse) ? $recurse + 1 : 1 );
	@temp =  split(/,/, $in);
	die "Looped group definitions for @temp\n" if $recurse > 20;
	TERM:
	for my $t ( @temp )
	{
		if (exists $SYNONYMS{$t})
		{
			push @ret, expand_alias($SYNONYMS{$t}, $recurse);
		}
		else 
		{
			push @ret, $t;
		}
	}
	return (@ret);
}

####
#
# expects an array refernce, and optionally a number which is used to
# limit recursion due to looped definitions 
# (this is inserted autmatically in recursive calls).
# the elements of the array referenced can be single values or CSV.
# all things  in arrayref must ultimately resolve, via <groups> 
# and/or <hosts>, to an ip address or ip address range (eg 0.0.0.0/0)
# or an ip addressand mask (eg '172.22.1.3 255.255.254.0' ). 
# returns an array conatining everything thatn the expansion created.
# groups can be nested.
#
sub expand_host
{
	my ( $in, $recurse) = @_;
	my @ret = ();
	my @temp;
	my @in = @{$in};
	$recurse = ( (defined $recurse) ? $recurse + 1 : 1 );
	
	die "Looped group definitions for @in" if $recurse > 20;
	# if you group recursively more than 20 deep, you need to stop.

	for my $h ( @in ) { push @temp, split(/,/, $h); }
	
	TERM:
	for my $t ( @temp )
	{
		if ( is_valid_entry($t) )
		{
			# if it's 'IPaddr/bits' or 'IPaddr' or 'IPaddr netmask'
			push @ret, $t;
		}
		elsif (exists $IP{$t})
		{
			# if it's a host name
			push @ret, $IP{$t};
		}
		elsif (exists $GROUP{$t})
		{
			# if it's a group name
			push(@ret, expand_host($GROUP{$t},$recurse));
		}
		else
		{
			# oops: user error (probably)
			die "non-resolvable term '$t' in expression @in in expand_host()\n";
		}
	}
	return (@ret);
}

########
#
# create a group for each <net> consisting of all <host> entries
# on that network.  For the net named "net", the group will be "gr_net".
#
sub make_net_groups
{
	# sort by IP for deterministic output, esp under older perls
	my @names = keys %NAME;
	@names = 	map  { $_->[1] }
				sort { $a->[0] cmp $b->[0] }
				map  { 	my $c = $_; 			# preserve input
						s/(\/|\s+).*$//; 		#discard non-useful part of ranges 
						[ inet_aton($_), $c  ] 	# put out what's needed
					} @names;
	
	for my $net ( @NETNAMES)
	{
		next if $net eq 'DEFAULT';
		my $gname = 'gr_' . $net;
		print STDERR 
			"WARNING: group name '$gname' is used more than once - is this right?\n" 
				if exists $GROUP{$gname};
		$GROUP{$gname} = [] unless exists $GROUP{$gname};
			
		my @use_names = @names; # preseerve for next loop
		
		for my $host ( @use_names )
		{
			next if exists $NO_AUTOGROUP{$NAME{$host}};
			# if <host.../> element and the no_autogroup flag, skip this one.
			
			my $real_host = $host;
			# use to save address ranges correctly
			
			$host =~ s/(\/|\s+).*$//;  # lose bit count or netmask if this is a range
			push @{$GROUP{$gname}}, $real_host if lan($host,1) eq $net;
			# use the raw version of the lan() call - no interface forcing!
		}
	}
}

#######
#
# use cached value if it exists, else call the real lan() function
# need to remove this (or reset %cache) if I ever allow on-the-fly mods to %LANS
# this takes a typical run from 6+ seconds to 2.2 seconds
#
{ 
	my %cache; # private for this sub
	sub lan 
	{
		my ($ip, $raw) = @_;
		$raw ||= 0;
		return $cache{$raw}{$ip} if exists $cache{$raw}{$ip};
		return ( $cache{$raw}{$ip} = lan_real(@_) );
	}
}

##########
#
# given an ip address, find which interface (network name)
# is associated with the smallest defined network conatiaining that ip.
# if the network had an 'int' attribnute, return that name instead
# unless $raw is set - then return the net name ignoring interface forcing
#
sub lan_real
{
	my ($ip, $raw) = @_;
	KEY:
	for ( @NETNAMES_SORT ) # search the networks in the most specific first order 
	{
		my $blk = findNetblock($ip, $LANS{$_});

		if ( defined $blk and $blk )
		{ 
			# $raw true stops interface forcing. 
			return $INTS{$_} if ( exists $INTS{$_} and not $raw ); 
			return $_; 
		}
	}
	die "NO net for $ip. \nShould be impossible to reach this point.  \nCheck the DEFAULT network definition\n";
}

#####
#
# receive expat, element name ('synonym') and attributes hash.
# only the attributes get used.
# popuplate the %LANS with Net::Netmask tables of the address ranges
# if an interface is forced (int='name'), populate the ints hash too.
#
sub proc_net
{
	my ($x, $elname, %attrib, ) = @_;
	my $hr = \%attrib; # get attributes
	tst_attribs_exist( \%attrib, $elname, qw( name ip ) );
	my $n = $hr->{name};

	# don't allow more than one network per name because then we couldn't ensure
	# most specific match first.
	if ( exists $LANS{$n} )
	{
		my $s = dmpr($hr);
		err_msg_die("duplicate net name '$n' in net statment: $s", $n);
	}

	# store a list of the names to use 
	# (after sorting for most-specific first - separate routine)
	# when searching the %LANS
	push @NETNAMES, $n;
	
	my $i = $hr->{ip};

	# used to 'assign' an address range to an int other than that named
	# in the element.  See the doc in the sample XML files.
	$INTS{$n} = $hr->{int} if ( exists $hr->{int} );

	# create table for this interface if none exists;
	$LANS{$n} = {} unless exists $LANS{$n};

	# add this network to the table for this interface
	my $blk = new2 Net::Netmask ($i);
	if ( defined $blk->{ERROR} or ( not defined $blk )  or $blk->{BITS} > MAXBITS )
	{
		my $s = dmpr($hr);
		err_msg_die("Invalid block: '$i': $blk->{ERROR}\n in group definition : $s\n", $i);
	}
	$blk->storeNetblock($LANS{$n});
}

############
#
# sort the @NETNAMES, used to order searches in %LANS,
# so that the most specific match (longest netmask) happens first.
#

sub sort_nets
{
	my ( %bitcount, );
	for my $name (keys %LANS)
	{
		my $hr = $LANS{$name};
		for ( keys %$hr )
		{
			my $ar = $hr->{$_};
			for ( @$ar )
			{
				next unless ref($_) eq 'Net::Netmask';
				# the tables made by Net::Netmask::storeNetblock have undef entries
				$bitcount{$name} = $_->bits();
			}
		}
	}
	@NETNAMES_SORT = sort { $bitcount{$b} <=> $bitcount{$a} or $b cmp $a } @NETNAMES;
}
#
###########

##############
#
# called to process each <acl> element.  Receives an expat object (ignored), the name of
# element ('acl', duh, also ignored) and the atributes as a list of key,value,key,value...
# expands all the elements.
# src and dst can be single ip address or range or group,
# or a CSV list of any or all of those. 
# srcprt and dstprt can be a number, a range (1024-65535),
# an operator of form eq# neq# le# gt#, a synonym or a CSV of any or all of these.
# int can be and interface name, a synonyam or a CSV of teither or both of these.
#
sub proc_acl
{
	my ($x, $elname, %attrib, ) = @_;
	my $hr = \%attrib; # get attributes

	# warn flagged( 'comment', $hr)   ;   
	# warn flagged( 'comment', $hr)   if flagged( 'comment', $hr);   
	
	tst_attribs_exist( \%attrib, $elname, qw( act proto ) ); 
	
	print STDERR Data::Dumper->Dump( [ \%LANS, \%INTS, \%IP, \%NAME, \%GROUP, \%SYNONYMS, ], 
							[ qw(*LANS *INTS *IP *NAME *GROUP *SYNONYMS) ] ) if ( (not $PASS1) and $DEBUG);
	$PASS1 = 1;

	# expand forced interfaces, if any
	my $int = ( ( exists $hr->{int} and $hr->{int}) ? $hr->{int} : '');
	my @locints =  ( $int ? expand_alias($int) : ( '' ) );

	# kludge alert.  need to learn to handle more than tcp/udp
	if ( $hr->{act} eq 'plug' )
	{
		for my $locint ( @locints )
		{
			push_for_make_one( $hr->{act}, $hr->{proto}, 0, 0, 0, 0, 0, 0, 0, 0, $locint);
		}
		return;
	}
	
	tst_attribs_exist( \%attrib, $elname, qw( act proto src srcprt dst dstprt ) ); 
	
	my ( $act, $pro, @srcs, @sprts, @dsts, @dprts, $return_only, $no_return);
	my ( $siprt, $slprt, $diprt, $dlprt, @ints, $locint, $locreflexive, $comment, );
	
	$act = $hr->{act}; 
	$pro = $hr->{proto}; 
	
	# expand the host/range CSV lists - resolve to IP address or ranges.
	@srcs = expand_host( [$hr->{src}] );
	@dsts = expand_host( [$hr->{dst}] );

	@sprts = expand_alias($hr->{srcprt});  # get and expand src ports
	@dprts = expand_alias($hr->{dstprt});  # get and expand dst ports
	
	if ( @sprts > 1 and @dprts > 1 )
	{
		# is this block necessary or even good? Arbitrary limitation
		err_msg_acl("more than one source port AND more than 1 dest port ", $hr);
	}
	
	# process expanded port CSV lists into arrays of arrayrefs: [ firstport, lastport ]
	@sprts = map { [ proc_ports($_, $hr) ] } @sprts;
	@dprts = map { [ proc_ports($_, $hr) ] } @dprts;

	if ( exists $hr->{no_return} and $hr->{no_return} eq 'y' ) 
	{
		$locreflexive = -1
	}
	elsif ( exists $hr->{non_reflexive} and $hr->{non_reflexive} eq 'y' )
	{
		$locreflexive = 0
	}
	else { $locreflexive = $REFLEXIVE }

	$comment = flagged( 'comment', $hr);

	$return_only = ( ( exists $hr->{return_only} and $hr->{return_only} eq 'y' ) ? 1 : 0 );

	$locreflexive = 0 if $return_only; # has to be.
	
	# store parms for the make_one calls
	for my $src ( @srcs )
	{
		for my $sprt ( @sprts )
		{
			($siprt, $slprt) = ( @$sprt );
			for my $dst ( @dsts )
			{
				for my $dprt ( @dprts )
				{
					($diprt, $dlprt) = ( @$dprt );
					for my $locint ( @locints )
					{
						push_for_make_one( $act, $pro, $src, $siprt, $slprt, $dst, $diprt, $dlprt, 
										$locreflexive, $return_only, $locint, $comment);
					}
				}
			}
		}
	}
}

sub push_for_make_one
{							
	my @parms = @_;
	# parms are: ( $act, $pro, $src, $siprt, $slprt, $dst, $diprt, $dlprt, 
	#				$locreflexive, $return_only, $locint);

	warn "make_one(" . join(',', @parms) . ") in " . (caller(1))[3] . ";\n" if $DEBUG;
	push @MAKEPARMS, [@parms];
}

######
#
# receives an error message string, a reference to the attributes hash for an <acl>
# and a string containing the offending element.
# prints the message, along with the attributes hash, tells which input file
# had the problem and on which lines of the input file the bad thing occurred.
#
sub err_msg_acl
{
	my ( $msg, $hr, $bad) = @_;
	my $s = dmpr($hr);
	print STDERR "\n$msg in acl: $s\n in file: $INFILE\n";
	if ( defined $bad and $bad )
	{
		my $i = 1;
		my @lines = split(/\n/, $XMLORIG);
		my $qmbad = quotemeta($bad);
		for ( @lines )
		{
			print STDERR "'$bad' occurs in line $i of input file $INFILE\n" if /$qmbad/;
			$i++;
		}
	}
	die "\n";
}

######
#
# recieves a string with the port from one of the attributes (srcprt or dstprt)
# of an <acl> element, and a reference to the attributes hash for the <acl>.  
# Uses the attributes hash only for error messages.
#
sub proc_ports 
{
	my ($prt, $hr) = @_;
	#match range eg 1024-65535
	if ( $prt =~ /^(\d+)\-(\d+)$/ ) { return ($1, $2); }
	#match single 
	if ( $prt =~ /^(\d+)$/ ) { return ($1, 0); }
	#match the other conditionals
	if ( $prt =~ /^(gt|lt|eq|neq)(\d+)$/ ) { return ("$1$2", 0); }
	else 
	{
		err_msg_acl("invalid ports spec '$prt' ", $hr, $prt);
	}
}

#########
#
# receives an expat object, an element name, and a list of key, valule... for the attributes
# populates %SYNONYMS hash.
#
sub proc_synonym
{
	my ($x, $elname, %attrib, ) = @_;
	my $hr = \%attrib; # get attributes
	tst_attribs_exist( \%attrib, $elname, qw( alias real ) ); 

	my $n = $hr->{alias};
	my $i = $hr->{real};
	# allow whitespace after commas in alias attribute so 'one-liners' 
	# can be more than one line 
	$i =~ s/,[\s\r]+/,/mgs;

	if ( exists $SYNONYMS{$n} )
	{
		my $s = dmpr($hr);
		err_msg_die("Duplicate synonym name: '$n' in synoym $s\n", $n);
	}
	$SYNONYMS{$n} = $i;
}

####
#
# called to process each host elemnt.
# recieves an expat object, the elemnt name, and the key,value,key,value for the 
# attributes.
# populates the %NAME and %IP hashes
# ip can be single or ip/bits or 'ip mask'
#
sub proc_host
{
	my ($x, $elname, %attrib, ) = @_;
	{
		tst_attribs_exist( \%attrib, $elname, qw( name ip) );
		my $n = $attrib{name};
		my $i = $attrib{ip};

		err_msg_die("Duplicate name: $n", $n) if exists $IP{$n};
		err_msg_die("Name $n duplicates group name", $n) if exists $GROUP{$n};
		err_msg_die("Duplicate IP: $i", $i) if  exists $NAME{$i};

		err_msg_die
		  ("'$i' NOT valid: must be ip_address, ip_address/bits or 'ip_address net_mask'", $i)
			unless ( is_valid_entry($i) );

		$NAME{$i} = $n;
		$IP{$n} = $i;

		$NO_AUTOGROUP{$n}++ if flagged('no_autogroup', \%attrib);
	}
}

###################
#
# Accepts a string $s and a ref to hash $hr
# detrmines if $s is one of CSV list in $h->{flags}
#
sub flagged
{
	my ($flag, $hr, ) = @_;
	return 0 unless exists $hr->{flags};

	my @flags = split(/\,/, $hr->{flags});
	for (@flags)
	{
		return 1 if $_ eq $flag;
		my @f = split(/\:/, $_, 2);
		return $f[1] if $f[0] eq $flag;
	}
	return 0;
}

#####
#
# process the <group> element.  initializes the entry in the %GROUP hash.
# popluats the entry if the members attribute is defined.
#
sub proc_group
{
	my ($x, $elname, %attrib, ) = @_;
	my $hr = \%attrib;
	tst_attribs_exist( \%attrib, $elname, qw( name ) );
	if ( exists $GROUP{$hr->{name}} )
	{
		my $s = dmpr($hr);
		err_msg_die("duplicate group name '$hr->{name}' in group: $s \n", $hr->{name}); 
	}
			
	if ( exists $IP{$hr->{name}} )
	{
		my $s = dmpr($hr);
		err_msg_die("group name '$hr->{name}' duplicates host name in group: $s \n", $hr->{name}); 
	}
			
	$CUR_GROUP = $hr->{name}; # save for processing member elements, if any
	$GROUP{$hr->{name}} = [];

	if ( keys %attrib > 1)
	{
		if ( exists $hr->{members} ) 
		{
			# handle one-liner groups (members in attributes as CSV)

			# allow whitespace aftter commas in members attribute so 'one-liners' 
			# can be more than one line 
			my $members = $hr->{members};
			$members =~ s/,[\s\r]+/,/mgs;

			my @tmp_members = split(/\,/, $members);

			# check that each of the members resolve.
			for ( @tmp_members )
			{
				my $s;
				eval {expand_host( [$_] ) };
				$s = dmpr($hr), die "\n$@ in group: $s in file: '$INFILE'\n\n" if $@;
			}
			push @{$GROUP{$hr->{name}}}, @tmp_members;
		}
		
	}
}

#########
#
# process the <member> element. 1 attribute, name, whose 
# value (or values, as CSV) is added to the GROUP entry set 
# in processing the <group> element
#
sub proc_member
{
	# uses global $CUR_GROUP which was set in proc_group 
	my ($x, $elname, %attrib, ) = @_;
	tst_attribs_exist( \%attrib, $elname, qw( name ) );
	my $hr = \%attrib;
	unless ( $CUR_GROUP )
	{
		my $s = dmpr($hr);
		err_msg_die("Missing group name for member '$hr->{name}'$s \n", $hr->{name});
	}
	
	# allow whitespace aftter commas in members attribute so 'one-liners' 
	# can be more than one line 
	my $n = $hr->{name};
	$n =~ s/,[\s\r]+/,/mgs;

	$GROUP{$CUR_GROUP} = [] unless exists $GROUP{$CUR_GROUP};

	# verify that the new member expands to IP address or range.
	eval {expand_host( [$n] ) };
	if ( $@ )
	{
		my $s = dmpr($GROUP{$CUR_GROUP});
		die "\n$@ in group: $s in file: '$INFILE'\n\n";
	}

	push @{$GROUP{$CUR_GROUP}}, $n;
}

###########
#
# receives a hash reference, the element name and a list of keys to test
# verfies the hash reference has those keys, or dies with an error message.
#
sub tst_attribs_exist 
{
	my ($hr, $name, @keys ) = @_;
	for ( @keys )
	{
		unless ( exists $hr->{$_} )
		{
			err_msg_die("In file '$INFILE', missing attribute '$_' in $name element: "
							. dmpr($hr) . "\n");
		}
	}
}

#####
#
# print the message and die
#
sub err_msg_die
{
	my ($msg_in, $var, ) = @_;
	print STDERR "\n$msg_in\n";
	err_msg($XMLORIG, $var);
	die "\n";
}

####
#
# uses Data::Dumper to show the object passed in,
# minus the 'VAR1 = ' prt which might 
# confuse the user
#
sub dmpr
{
	my ($in, ) = @_;
	return $in unless ref($in);
	my $s = Dumper($in);
	$s =~ s/^\$VAR1 = //;
	return $s;
}

#######
#
# give line numbers where $bad occurs in $txt
# $bad is an offending item that provoked an errer
# $txt is typically the XML from the input file.
#
sub err_msg
{
	my ($txt, $bad, ) = @_;
	my @lines = split(/\n/, $txt);
	my $i = 1;

	return unless $bad;
	$bad =  quotemeta($bad);

	for ( @lines )
	{
		print STDERR "'$bad' occurs in line $i of input file $INFILE\n" if /$bad/;
		$i++;
	}
}

########
#
# test for something like 192.168.0.0 OR 
# test for something like 192.168.0.0 255.255.0.0 OR
# test for something like 192.168.0.0/16 
#
sub is_valid_entry
{
	my ( $in, ) = @_;

	return 	
	(
		   is_valid_ip_addr($in) 
		or is_valid_ip_and_mask($in)
		or is_valid_ip_range($in)
	);
}

# test for something like 192.168.0.0
sub is_valid_ip_addr
{
	my ( $in ) = @_;
	return 0 unless $in =~ /^$IP_RE$/;
	eval { inet_ntoa(inet_aton($in)) };
	return 0 if $@;
	return 0 unless $in eq inet_ntoa(inet_aton($in)); 
	return 1;
}

# test for something like '192.168.0.0 255.255.0.0'
sub is_valid_ip_and_mask 
{
	my @junk;
	my ( $in ) = @_;
	return 0 unless $in =~ /^($IP_RE)\s+($IP_RE)$/;
	return ( is_valid_ip_addr($1) and is_valid_ip_addr($2) );
}

# test for something like 192.168.0.0/16
sub is_valid_ip_range
{
	my @junk;
	my ( $in ) = @_;
	return 0 unless $in =~ m/^($IP_RE)\/(\d+)$/;
	my ($p1, $p2) = ($1, $2);
	return ( is_valid_ip_addr($p1) and $p2 >= 0 and $p2 <= MAXBITS );
}

#
#####




####
#
# initialize accesslists
#

@INTFCS = grep { $_ ne 'DEFAULT' } @NETNAMES;

warn qq(@INTFCS) if $DEBUG;

for my $intfc ( @INTFCS )
{
	print "no ip access-list extended "     .  $intfc  .  "_in\n";
}

for my $intfc ( @INTFCS )
{
	$ACCESS{$intfc} = '';
	$ACCESS{$intfc} .= "ip access-list extended "     .  $intfc  .  "_in\n";
}

# here it is!
for my $parms ( @MAKEPARMS ) { make_one(@{$parms}); }

#####
#
# Finish access lists with reflexive statements (if using reflexive)

if ( $REFLEXIVE )
{
	for my $src_intfc ( @INTFCS )
	{
		for my $dst_intfc ( @INTFCS )
		{
			next if  $dst_intfc eq $src_intfc;
			my $rflt_name = $src_intfc . '_' . $dst_intfc;
			$ACCESS{$dst_intfc} .= " evaluate $rflt_name\n";
		}
	}
}

##############
# Finish access lists with blanket logging deny
#
for my $intfc ( @INTFCS )
{
	$ACCESS{$intfc} .= " deny ip any any log\n";
}
#
#
#####

for my $intfc ( @INTFCS,  )
{
	print "\n", $ACCESS{$intfc}, "\n";
}
#
# Production access lists done (except for trailing "^end" in config
#
#################

##################
#
# CHICAGO DMZ SPECIFIC
#
# create test access lists.  
# the first duplicates the production ATM list but
# excludes web traffic.  Used for testing BigIP/3DNS
# functions.  Apply this to the ATM PVCs to a CC site
# to test the CC site down function, while still permitting
# other connectivity (especially Citrix, allowing this to be done
# remotely.  
#
if ( $TEST_ACLS and exists $ACCESS{'atm'} and defined $ACCESS{'atm'} and $ACCESS{'atm'} )
{
	if (0) # no room for this one
	{
		my @test_acl_lines = split(/\n/, $ACCESS{'atm'});
		my $test_acl = "\nno ip access-list extended atm_no_web_in\n";

		for ( @test_acl_lines )
		{
			s/atm_in/atm_no_web_in/;
			if ( /^ permit.*( 80$| 80 | 443$ | 443 )/ )
			{
				s/permit/deny/;
			}
			$test_acl .= "$_\n";
		}
		$test_acl .= "\n";
		print $test_acl;
	}

	# this list uses ACLs to simulate a link down for everything except
	# citrix.
	#
	my @test_acl_lines = split(/\n/, $ACCESS{'atm'});
	my $test_acl = "\nno ip access-list extended atm_only_citrix_in\n";
	$test_acl .= "\nip access-list extended atm_only_citrix_in\n";

	ACL_LINE:
	for ( @test_acl_lines )
	{
		if (  / 1494 | 1494$/ )
		{
			$test_acl .= "$_\n";
		}
	}
	$test_acl .= " deny ip any any\n\n";
	print $test_acl;
}
#
###############################

print "end\n";
	
# print Data::Dumper->Dump( [ \%ACCESS ], [ *ACCESS ]);

# END of EXECUTION
exit;

#############
#
#  receives address as "172.28.129.74/27" or "172.28.129.74 27"
#  or "172.28.129.74/255.255.255.224" or "172.28.129.74 255.255.255.224"
# 
# Or address can just be 172.28.129.74, with mask separat as either '27' or '255.255.255.224'
# or can just be address 172.28.129.74, with no mask provided
#
# returns ( address, network, broadcast, netmask) all as dotted quads.
#
# if no mask was provided, returns ( address, , , )
#
# network is the address and'ed with the 1s complement of the netmask.
# broadcast is the adddress or'd with the 1s complement of the network mask.
# (This assumes an 'all 1's host' broadcast. If you're using something else,
# time to upgrade.)
#
# This exists, even though I'm using Net::Netmask elsewhere, because
# it permits non- contiguous netmasks.  I've never found a use for those,
# but, hey, it could happen.

sub do_for_network
{
	my ( $addr, $mask ) = @_;
	my ( $octet, $accum, @octets ) = ( 0, 0, (), );
	my ( $netmask, $network, $broadcast, ) = ( '', '', '', );

	# if mask wasn't passed in, try and get it from the address
	unless ( defined $mask and $mask ) { ( $addr, $mask ) = split(/[\s\/]/, $addr); }

	if (defined $mask  )
	{
		# if the mask is a dotted quad
		if ( $mask =~ /^(\d{1,3}\.){3}\d{1,3}$/ )  
		{ 
			$netmask = $mask;
			$mask = inet_aton($mask);
		}

		# if the mask was a bit count
		elsif ( $mask =~ /^\d+$/ ) 
		{
			if ( $mask < 0 or $mask > 255 ) { die "mask value $mask is invalid" }

			for ( my $i = $mask - 1 ; $i > -1 ; $i-- ) { $accum += ( 2 ** (31 - $i) ); }

			for my $i ( 3, 2, 1, 0 ) 
			{
				$octet = int($accum / ( 256 ** $i ) );
				push @octets, $octet;
				$accum = $accum % ( 256 ** $i );
			}

			$netmask = join('.', @octets);
			$mask = inet_aton($netmask);
		}

		# if no mask was given
		# elsif ( defined $mask and not $mask ) { $netmask = '0.0.0.0'; }
		elsif ( not ( defined $mask and $mask) ) { $netmask = ''; }
		else { die "mask '$mask' format invalid"; }

		$network = inet_ntoa( inet_aton($addr) & $mask) if ( $mask && length($mask) == 4); 
		$broadcast = inet_ntoa( inet_aton($addr) | ( $mask ^ $ALL_ONES) );
	}

	return ($addr, $network, $broadcast, $netmask, );
}
#
#
#####################

###
#
# make acl entry 
#
sub make_one
{
	my ( $act, $proto, $src, $src_port1, $src_port2, 
						$dst, $dst_port1, $dst_port2, $reflexive, $return_only, $int, $comment) = @_;
	$return_only ||= 0;
	print STDERR "make_one($act, $proto, $src, $src_port1, $src_port2, 
										$dst, $dst_port1, $dst_port2, $reflexive, $return_only, $int, $comment)\n" if $DEBUG;

	# act = permit|deny (not tested
	#   if act eq 'plug' then proto is a string to be added literally to the int in $int.  all else is ignored.
	# proto = tcp|udp (actually not tested, other than for 'established' in non-relfexive return for tcp)
	# src = addr/mask or 'addr mask' where mask is bit count or dotted quad.
	# mask IS A NETMASK, NOT A CISCO WILDCARD (which is 1s complement of a netmask)
	# src_port1 is port number  or first port in range (optional) or a port preceded by 'lt', 'eq', 'neq' or 'gt'
	#   if a single number, same as number preceded by 'eq'
	# src_port2 is second portin range (optional)
	# dst, dst_port1, dst_port2 as for src entries
	# reflexive  > 0 causes use of reflexive lists - no reverse entry created
	# reflexive == 0 causes reverse direction to be created, using 'established' if tcp.
	# reflexive  < 0 causes neither - used for NO_RETURN such as traffic to 
	#	i'INTERNAL' addreses (e.g. 224.0.0.2 for hsrp)
	# return_only true causes the forward going access-list not to be made.
	# int - overrides autmatic logic that determines which interface to put this on 
	# comment - text to be inserted prior to ACL entries, pre-pended w/ the ! character

	my ( $initial, $final, $addr, $network, $broadcast, $netmask, $src_addr, $dst_addr,
	      $src_port, $dst_port, $it, $i, $aclcmd );

	# kludge alert.  learn how to handle other protocols.
	if ( $act eq 'plug' )
	{
		$ACCESS{$int} .= " $proto\n";
		return;
	}

	# figure out the src and dst address formats
	# $initial and $final are used literally in the ACL
	# $src_addr and $dst_addr are used to determine where the ACLs go.
	( $initial, $src_addr )  = myaddr($src);
	( $final, $dst_addr ) =  myaddr($dst);

	# if both ends are external and the interface isn't forced
	# and the -ne flag is set, skip this acl.
	if (0)   # sadly, this doesn't work yet.
	{
	my $default_lan = lan('0.0.0.0');
	return if ( 	$NO_EXTERNAL 
				and lan($src_addr) eq $default_lan
				and lan($dst_addr)  eq $default_lan
				and not $int );
	}

	# jam $int if provided, or get interface from address
	my $from_vlan = $int || lan($src_addr);
	my $to_vlan = lan($dst_addr);

	warn "$from_vlan,$to_vlan,"  if $DEBUG;

	# figure out port actions for src and dst
	# if none, none; if one, 'eq'; if two, 'range...' if eq# or lt# or gt# or neq#, that's it
	$src_port = myport( $src_port1, $src_port2 );
	$dst_port = myport( $dst_port1, $dst_port2 );

	# create the list names used for the reflect statements if in reflexive mode
	my $reflect = ( $reflexive > 0 ? "reflect ${from_vlan}_$to_vlan" : '' );

	# forward direction
	unless ( $return_only )
	{
		$aclcmd = init_aclcmd($comment);
		$aclcmd .= " $act $proto $initial $src_port $final $dst_port $reflect\n";
		$ACCESS{$from_vlan} .= $aclcmd;
	}

	# reverse direction as needed
	if  ( $reflexive == 0 ) 
	{
		$aclcmd = init_aclcmd($comment);
		if ( $proto eq 'tcp' ) 
		{
			$aclcmd .= " $act $proto $final $dst_port $initial $src_port established \n"; 
		}
		else
		{
			$aclcmd .= " $act $proto $final $dst_port $initial $src_port \n";
		}
		$ACCESS{$to_vlan} .= $aclcmd; 
	}
}

#############
#
# given the 2 ports ("first" and "last") for src or dst
# make the ACL expression for it.
# if none, none; if one, 'eq'; if two, 'range...' if eq# or lt# or gt# or neq#, that's it
#
sub myport
{
	my ( $port1, $port2 ) = @_;
	if ( $port1 and $port2 )
	{
		return  "range $port1 $port2 ";
	}
	elsif  ( $port1 and $port1 =~ /^(lt|neq|gt|eq)(\d+)$/ )
	{
		return  "$1 $2 ";
	}
	elsif ( $port1 )
	{
		return  "eq $port1 ";
	}
	else
	{
		return  ""; 
	}
}

#######
#
# use cached value if it exists, else calle the real myaddr() function
# this saved about 10% of exec time (aftering caching lan() saved about 60%)
#
{ 
	my %cache; # private for this sub
	sub myaddr 
	{
		my ($addr_in, ) = @_;
		return @{$cache{$addr_in}} if exists $cache{$addr_in};
		return @{( $cache{$addr_in} = [ myaddr_real(@_) ] )};
	}
}

##########
# accepts the address in one of  addr or addr/bits or 'addr mask'
# returns the acl expression for the address or address range
# and the address.
#
sub myaddr_real
{
	my $acl_exp;
	my ($addr_in, ) = @_;
	my ($addr, $network, $broadcast, $netmask, ) = do_for_network($addr_in);
	if ( $netmask )
	{
		my $nmask = inet_aton($netmask);
		$nmask = inet_ntoa( $nmask ^ $ALL_ONES ); # invert netmask because Cisco does.
		$acl_exp = "$addr $nmask ";
		if ( "$addr $nmask" eq "0.0.0.0 255.255.255.255")
		{
			# special case for any host.
			$acl_exp = "any ";
		}
	}
	else { $acl_exp = "host $addr "; }
	return ( $acl_exp, $addr );
}


exit;


####
#
# set aclcmd to '' if no comment.
# if comment, ensure it's really a comment and return it.
#
sub init_aclcmd
{
	my ($comment, ) = @_;
	return '' unless $comment;
	$comment =~ s/[\n\r]*//g;
	$comment = '! ' . $comment if ($comment !~ /^\!/ );
	return "$comment\n";
}


__END__

# TODO: 
# allow null srcprts?
#
# fix the interface between proc_acl and make_one:
#    duplication of effort in validation and parsing.
#    make_one ultimately should have a named parameter interface
#    luckily, the user never sees this.
#
# is there any reason not to recursively expand groups and synonyms 
# when parsing <group> and <synonym> instead of when parsing <acl>?
#
# copyright Bob Niederman, 2002.
# comments and patches to bob@bob-n.com
# This is licensed under the FSF GNU GPL.
# $Log: aclxml2.pl,v $
# Revision 1.37  2002-08-22 19:30:37-05  bobn
# slight improvements in caching code; incorporated pod file
#
# Revision 1.36  2002-08-22 11:47:51-05  bobn
# allow autogrouping of ip ranges.  Comment/format changes
#
# Revision 1.35  2002-08-22 11:21:40-05  bobn
# comments only
#
# Revision 1.34  2002-08-15 18:14:18-05  bobn
# comments; changed name of err_msg_typ to err_msg_die
#
# Revision 1.33  2002-08-15 17:53:35-05  bobn
# in make_group_nets, sort by IP for deterministic output, esp under older perls
#
# Revision 1.32  2002-08-14 10:44:51-05  bobn
# factored comment code
# force $locreflexive to 0 if return_only is set
#
# Revision 1.31  2002-08-13 23:26:51-05  bobn
# added code to allow comments as flags of acl elements
#
# Revision 1.30  2002-08-13 14:29:27-05  bobn
# cahed myaddr(); some comment and usage() changes
#
# Revision 1.29  2002-08-13 12:33:04-05  bobn
# added caching of lan() sub significant speed improvement.
# added tst_attribs_exist for act='plug' case
# added a little more to the doc
#
# Revision 1.28  2002-08-13 10:27:11-05  bobn
# mainly cosmstic: use tst_attribs_exist in proc_acl instead of home-rolled
#
# Revision 1.27  2002-08-13 02:24:28-05  bobn
# Added sample input, structures and output to doc at end of file
#
# Revision 1.26  2002-08-10 17:22:30-05  bobn
# added logic for flags in elements
# implemnted the no_autogroup flag
# added code to allow whitespace between entrys (after the comma
#  in vlaues for real attr of <synonym/>, members attr of <group>
#  and name attr of <member/> elment
#
# Revision 1.25  2002-08-09 22:23:23-05  bobn
# minor changes to allow eliminationof the superflous 'plurally-named' container elements
#
# Revision 1.24  2002-08-09 20:00:04-05  bobn
# added code to test that required attributes of all elemnts exist.
# (note to self: would not need this if I had a validating parser.)
#
# Revision 1.23  2002-08-08 01:33:27-05  bobn
# don't allow more than one network per net name
#
# Revision 1.22  2002-08-07 08:23:44-05  bobn
# added checks for matches between host names and group names
#
# Revision 1.21  2002-08-04 07:56:04-05  bobn
# comments changes only
#
# Revision 1.20  2002-08-03 09:31:45-05  bobn
# use raw call of lan() in make_net_groups
#
# Revision 1.19  2002-08-02 00:15:19-05  bobn
# got rid of one test ACL, made the other much shorter
#
# Revision 1.18  2002-08-01 08:21:03-05  bobn
# changed misleadingly all-caps named lexical $REFLEXIVE to $reflexive
#
# Revision 1.17  2002-08-01 08:17:16-05  bobn
# cleanup and doc in expand_host
#
# Revision 1.16  2002-08-01 06:36:09-05  bobn
# minor changes in format and comments
#
# Revision 1.15  2002-07-31 23:15:14-05  bobn
# cut down atm_citrix_only_in list to bare minimums
# eliminated useless subroutine in reflexive processing
# eliminated redundantant expand_hosts of whole grups in proc_group
#
# Revision 1.14  2002-07-31 18:43:43-05  bobn
# blew away the <IGNORE/> <REGARD/> idiom in favor of the
# <IGNORE>  </IGNORE/> idiom, which naturally nests
#
# Revision 1.13  2002-07-31 08:48:56-05  bobn
# don't allow negative port nums.
#
# Revision 1.12  2002-07-31 08:26:27-05  bobn
# cleanup in proc_acl: re-odering, pulling expansions out of loop
#
# Revision 1.11  2002-07-30 22:30:38-05  bobn
# commented/if (0)'d out the previous - it's not quite right.
# it may be impossible.
#
# Revision 1.10  2002-07-30 21:58:01-05  bobn
# added the code for ACL with both ends external.
#
# Revision 1.9  2002-07-30 20:37:23-05  bobn
# factored more routines relted to validating
# ip-related validation
#
# Revision 1.8  2002-07-30 08:05:00-05  bobn
# factoring some subroutines
#
# Revision 1.7  2002-07-29 23:27:10-05  bobn
# all global var names in UPPER CASE
#
# Revision 1.6  2002-07-29 22:01:53-05  bobn
# much cleanup, including some truly dumb stuff in make_one
# that involved references to arrays of refernces that became
# functions.  what a concept.
#
# Revision 1.5  2002-07-29 13:00:13-05  bobn
# added routine to sort nets, most specific first.
#
# Revision 1.4  2002-07-29 11:23:00-05  bobn
# many changes and cleanups for mods to XML processing.
#
# Revision 1.3  2002-07-28 20:07:57-05  bobn
# cleanup
#
# Revision 1.2  2002-07-28 19:11:38-05  bobn
# used Start and End hndlers to improve tolerance of errors in file
# and simplify the code, too.
#
# Revision 1.1  2002-07-28 10:40:57-05  bobn
# Initial revision
#
# Revision 1.6  2002-07-27 10:49:32-05  bobn
# added use of separate parser object to look for CHAR conntent
# which is illegal in this DTD (all info actually in attirbutes).
# (but later found this wasn't true and removed this)
#
# Revision 1.5  2002-07-26 04:31:16-05  bobn
# more validation - fixes for the 'addr mask' cases
#
# Revision 1.4  2002-07-26 03:16:35-05  bobn
# finished validation of IP stuff in expand_hosts; more comments
#
# Revision 1.3  2002-07-26 02:31:59-05  bobn
# added comment, subtracted old cruft, started validation of
# IP info in expan_hosts()
#
# Revision 1.2  2002-07-26 01:56:49-05  bobn
# rcs info change
#
# Revision 1.1  2002/07/26 06:54:38  bobn
# Initial revision
#

THIS XML AS THE ONLY INPUT FILE:


<?xml version="1.0"?>
<!DOCTYPE acls SYSTEM "acls.dtd">
	<acls>
		<host name='server1' ip='192.168.129.8'/>
		<host name='server2' ip='192.168.129.9'/>
		<host name='server3' ip='192.168.129.100'/>
		<host name='server4' ip='192.168.129.101'/>
		<host name='snmp_nms' ip='192.168.129.76'/>

		<group name='https' members='server1,server2'/>
		
		<group name='http' members='https,server3,server4'/>
		groups can nest

		<group name='SMTPrelays' members='172.21.34.34,172.23.14.10' />
		
		<group name='any' members='0.0.0.0/0'/>
		use groups of 1 member for aliases - don't use hosts for this
			groups and hosts must resolve to an ip address or ip range.

		<net name='vlan2' ip='192.168.129.0/27' />
		<net name='vlan4' ip='192.168.129.64/27' />
		<net name='vlan5' ip='192.168.129.96/27' />
		<net name='atm'  ip='192.168.254.28/29' />
		<net name='DEFAULT' ip='0.0.0.0/0' int='atm' />
			this says that the interface atm is the default route out

			synonyms can equate to anything.  
		<synonym alias='any' real='0-0' />
			groups and synonyms are different namespaces - no clash
		<synonym alias='unpriv' real='gt1023' />

		<acl act='permit' proto='tcp' src='any' srcprt='unpriv'
					dst='http' dstprt='80' />
									
		<acl act='permit' proto='tcp' src='any' srcprt='unpriv'
					dst='https' dstprt='443' />

		<acl act='permit' proto='tcp' src='gr_vlan2,gr_vlan4,gr_vlan5' srcprt='any'
					dst='SMTPrelays' dstprt='25' />

		<acl act='permit' proto='udp' src='snmp_nms' srcprt='any'
					dst='gr_vlan2,gr_vlan5' dstprt='161' />
	
		<acl act='permit' proto='udp' src='gr_vlan2,gr_vlan5' srcprt='any'
					dst='snmp_nms' dstprt='162' no_return='y' />
				snmp trivia - traps are not acknowledged
	</acls>


PRODUCES THESE STRUCTURES:

%LANS = (
          'vlan4' => {
                       '3232268608' => [
                                         undef,
                                         bless( {
                                                  'ERROR' => undef,
                                                  'IBASE' => -1062698688,
                                                  'BITS' => '27'
                                                }, 'Net::Netmask' )
                                       ]
                     },
          'vlan5' => {
                       '3232268640' => [
                                         bless( {
                                                  'ERROR' => undef,
                                                  'IBASE' => -1062698656,
                                                  'BITS' => '27'
                                                }, 'Net::Netmask' )
                                       ]
                     },
          'atm' => {
                     '3232300568' => [
                                       bless( {
                                                'ERROR' => undef,
                                                'IBASE' => -1062666728,
                                                'BITS' => '29'
                                              }, 'Net::Netmask' )
                                     ]
                   },
          'vlan2' => {
                       '3232268544' => [
                                         undef,
                                         undef,
                                         undef,
                                         bless( {
                                                  'ERROR' => undef,
                                                  'IBASE' => -1062698752,
                                                  'BITS' => '27'
                                                }, 'Net::Netmask' )
                                       ]
                     },
          'DEFAULT' => {
                         '0' => [
                                  bless( {
                                           'ERROR' => undef,
                                           'IBASE' => 0,
                                           'BITS' => '0'
                                         }, 'Net::Netmask' )
                                ]
                       }
        );
%INTS = (
          'DEFAULT' => 'atm'
        );
%IP = (
        'server2' => '192.168.129.9',
        'server3' => '192.168.129.100',
        'server4' => '192.168.129.101',
        'snmp_nms' => '192.168.129.76',
        'server1' => '192.168.129.8'
      );
%NAME = (
          '192.168.129.100' => 'server3',
          '192.168.129.8' => 'server1',
          '192.168.129.101' => 'server4',
          '192.168.129.9' => 'server2',
          '192.168.129.76' => 'snmp_nms'
        );
%GROUP = (
           'any' => [
                      '0.0.0.0/0'
                    ],
           'http' => [
                       'https',
                       'server3',
                       'server4'
                     ],
           'gr_vlan2' => [
                           '192.168.129.8',
                           '192.168.129.9'
                         ],
           'https' => [
                        'server1',
                        'server2'
                      ],
           'gr_vlan4' => [
                           '192.168.129.76'
                         ],
           'gr_vlan5' => [
                           '192.168.129.100',
                           '192.168.129.101'
                         ],
           'gr_atm' => [],
           'SMTPrelays' => [
                             '172.21.34.34',
                             '172.23.14.10'
                           ]
         );
%SYNONYMS = (
              'any' => '0-0',
              'unpriv' => 'gt1023'
            );


AND PRODUCES THIS OUTPUT:

no ip access-list extended vlan2_in
no ip access-list extended vlan4_in
no ip access-list extended vlan5_in
no ip access-list extended atm_in

ip access-list extended vlan2_in
 permit tcp host 192.168.129.8  eq 80  any  gt 1023  established 
 permit tcp host 192.168.129.9  eq 80  any  gt 1023  established 
 permit tcp host 192.168.129.8  eq 443  any  gt 1023  established 
 permit tcp host 192.168.129.9  eq 443  any  gt 1023  established 
 permit tcp host 192.168.129.8   host 172.21.34.34  eq 25  
 permit tcp host 192.168.129.8   host 172.23.14.10  eq 25  
 permit tcp host 192.168.129.9   host 172.21.34.34  eq 25  
 permit tcp host 192.168.129.9   host 172.23.14.10  eq 25  
 permit udp host 192.168.129.8  eq 161  host 192.168.129.76   
 permit udp host 192.168.129.9  eq 161  host 192.168.129.76   
 permit udp host 192.168.129.8   host 192.168.129.76  eq 162  
 permit udp host 192.168.129.9   host 192.168.129.76  eq 162  
 deny ip any any log


ip access-list extended vlan4_in
 permit tcp host 192.168.129.76   host 172.21.34.34  eq 25  
 permit tcp host 192.168.129.76   host 172.23.14.10  eq 25  
 permit udp host 192.168.129.76   host 192.168.129.8  eq 161  
 permit udp host 192.168.129.76   host 192.168.129.9  eq 161  
 permit udp host 192.168.129.76   host 192.168.129.100  eq 161  
 permit udp host 192.168.129.76   host 192.168.129.101  eq 161  
 deny ip any any log


ip access-list extended vlan5_in
 permit tcp host 192.168.129.100  eq 80  any  gt 1023  established 
 permit tcp host 192.168.129.101  eq 80  any  gt 1023  established 
 permit tcp host 192.168.129.100   host 172.21.34.34  eq 25  
 permit tcp host 192.168.129.100   host 172.23.14.10  eq 25  
 permit tcp host 192.168.129.101   host 172.21.34.34  eq 25  
 permit tcp host 192.168.129.101   host 172.23.14.10  eq 25  
 permit udp host 192.168.129.100  eq 161  host 192.168.129.76   
 permit udp host 192.168.129.101  eq 161  host 192.168.129.76   
 permit udp host 192.168.129.100   host 192.168.129.76  eq 162  
 permit udp host 192.168.129.101   host 192.168.129.76  eq 162  
 deny ip any any log


ip access-list extended atm_in
 permit tcp any  gt 1023  host 192.168.129.8  eq 80  
 permit tcp any  gt 1023  host 192.168.129.9  eq 80  
 permit tcp any  gt 1023  host 192.168.129.100  eq 80  
 permit tcp any  gt 1023  host 192.168.129.101  eq 80  
 permit tcp any  gt 1023  host 192.168.129.8  eq 443  
 permit tcp any  gt 1023  host 192.168.129.9  eq 443  
 permit tcp host 172.21.34.34  eq 25  host 192.168.129.8   established 
 permit tcp host 172.23.14.10  eq 25  host 192.168.129.8   established 
 permit tcp host 172.21.34.34  eq 25  host 192.168.129.9   established 
 permit tcp host 172.23.14.10  eq 25  host 192.168.129.9   established 
 permit tcp host 172.21.34.34  eq 25  host 192.168.129.76   established 
 permit tcp host 172.23.14.10  eq 25  host 192.168.129.76   established 
 permit tcp host 172.21.34.34  eq 25  host 192.168.129.100   established 
 permit tcp host 172.23.14.10  eq 25  host 192.168.129.100   established 
 permit tcp host 172.21.34.34  eq 25  host 192.168.129.101   established 
 permit tcp host 172.23.14.10  eq 25  host 192.168.129.101   established 
 deny ip any any log

end



=pod



=head1 NAME

aclxml2.pl  - Produce Cisco ACLs from XML definition files.

=head1 SYNOPSIS


	perl aclxml2.pl [-x] [ -d number] file1.xml [file2.xml...]

	where 	-x causes reflexive ACLs to be generated
		-d plus a number causes debugging information to be output.
			The amount increases with the number through 3

		The file(s) are XML files describing the network and
		the ACLs desired.

Chicago Specific Examples:

	perl aclxml2.pl net.xml r1.xml acl.xml >accx.r1.date_stamp
	perl aclxml2.pl net.xml r2.xml acl.xml >accx.r2.date_stamp


=head1 DESCRIPTION

The definitions can be in one or many files, which will be parsed in the order 
they are named on the command line.

The ACLs are printed to standard output and are captured using file redirection.  
Error and debugging messages are printed to standard error and so 
appear on the terminal even if standard output is redirected.

The program allows specification of the ACLs by address, hostnames or groups thereof,
as well a groups of ports.  The program automatically figures out where the pairs
of ACL entries go.  Options are provided in the file formats to override various 
automatic behavior.

This is particularly useful for paranoid installations where ACLs are desired on a 'per host' 
basis rather than a 'per network' basis.

As an example, the installation that this was developed for eventually boiled down 
to 85 ACL statements in the XML files, which generated more than 1200 'permit....' 
or 'deny...' entries on the output. (264 total XML elements were used defining the 
network and ACLs in this case.)

=head1 the 37 second tour of XML

XML consists of markup and character data.

Markup is anything between innermost containing B<E<lt>> and B<E<gt>>  
and character data is everything else.

Character data cannot contain the B<E<lt>> or B<E<gt>> or B<E<amp>> symbols, 
though comments can.

An XML document must have a top level element which contains all other elements.  So the
input files used by this program must have the start markup or tag of

	<acls>

(this is the start tag of the acls element)


and an ending tag of 

	</acls>

(this is the end tag of the acls element).

When elements have a start tag and an end tag, as the acls element above does, they can 
contain other elements or character data or both.

An element can also be "empty" in which case the start and end tags are combined 
into one tag, as in

	<host />

Of course, for our application, a tag like C<<host />> usually isn't too interesting, 
so we add attributes:


	<host name='snmp_mgr' ip='192.168.129.76' />

The host element above is still considered "empty", because it contains no other elements or non-markup
data, but it has 2 attributes, 'name' and 'ip', with values 'snmp_mgr' and '192.168.129.76', 
respectively (minus the quote marks).

Only markup is important to this program.  All the real information is in the attributes.

So, for this program, anything that is not markup is, in effect, a comment.

Comments can also be created by enclosing the comment text between C<<!--> and C<-->> sequences. 
Text so enclosed is comment text to all XML applications, and a good XML editor will display comment 
text differently from non-comment text, such as a different color.  This can be useful. 
The only problem is, this will NOT nest, so if you do something like this:

	<!-- here I'm commenting out a bunch of stuff for test reasons
	...
	...
	<!-- here's a comment that was here before I commented out this section -->
	<host name='gus' ip='199.169.199.169' />

	here's the end of the attempt to comment out a bunch of stuff -->

the parser will puke, as by definition this is not well-formed XML.

Options are: leave comments as character data (without using the  C<<!--> and C<-->> sequences) 
and only use the  C<<!--> and C<-->> sequences for commenting out sections for testing, and/or 
make use of the 

	<IGNORE>
	</IGNORE>

tags as documented below.  Note that if you use a B<E<lt>IGNOREE<gt> ... E<lt>/IGNOREE<gt>> sequence, 
the text contained between them must be valid XML or comments.


=head1 Input File Format

All files start with a line like this:

	<acls>

I< (Actually, you may see something like this before the > E<lt>aclsE<gt>:

	<?xml version="1.0"?>
	<!DOCTYPE acls SYSTEM "acls.dtd">

I<but this is neither necesssary nor harmful to the aclxml2.pl program, 
as of this writing (20020810).)>

and end with a line like this:

	</acls>

Including these lines a sample file can look like this:


	<acls>
		<host name='server1' ip='192.168.129.8'/>
		<host name='server2' ip='192.168.129.9'/>
		<host name='server3' ip='192.168.129.100'/>
		<host name='server4' ip='192.168.129.101'/>
		<host name='snmp_nms' ip='192.168.129.76'/>

		<group name='https' members='server1,server2'/>
		
		<group name='http' members='https,server3,server4'/>
		groups can nest

		<group name='SMTPrelays' members='172.21.34.34,172.23.14.10' />
		
		<group name='any' members='0.0.0.0/0'/>
		use groups of 1 member for aliases - don't use hosts for this
			groups and hosts must resolve to an ip address or ip range.

		<net name='vlan2' ip='192.168.129.0/27' />
		<net name='vlan4' ip='192.168.129.64/27' />
		<net name='vlan5' ip='192.168.129.96/27' />
		<net name='atm'  ip='192.168.254.28/29' />
		<net name='DEFAULT' ip='0.0.0.0/0' int='atm' />
			this says that the interface atm is the default route out

			synonyms can equate to anything.  
		<synonym alias='any' real='0-0' />
			groups and synonyms are different namespaces - no clash
		<synonym alias='unpriv' real='gt1023' />

		<acl act='permit' proto='tcp' src='any' srcprt='unpriv'
					dst='http' dstprt='80' />
									
		<acl act='permit' proto='tcp' src='any' srcprt='unpriv'
					dst='https' dstprt='443' />

		<acl act='permit' proto='tcp' src='gr_vlan2,gr_vlan4,gr_vlan5' srcprt='any'
					dst='SMTPrelays' dstprt='25' />

		<acl act='permit' proto='udp' src='snmp_nms' srcprt='any'
					dst='gr_vlan2,gr_vlan5' dstprt='161' />
	
		<acl act='permit' proto='udp' src='gr_vlan2,gr_vlan5' srcprt='any'
					dst='snmp_nms' dstprt='162' no_return='y' />
				snmp trivia - traps are not acknowledged
	</acls>

Comments can appear within a file as bare text.  Not a feature of XML in general,
but of the fact that in this project, all the information is contained
in attributes.

Comments could also be contained between C<<!--> and C<-->> sequences.  The bad news about 
these is they DON'T NEST, eg if you put a comment of this sort inside 
another comment of this sort, your typical XML parser will puke.

For this program, B<whitespace> (spaces, tabs, and end-of-line sequences) can occur anywhere between B<except>:

=over 4

=item -
between the opening < and the name of an element in  a start tag

=item -
between the / and the trailing > of an end tag.


=item -
within the names of elements and the names of attributes


=item -
within the the values of attributes

=over 4

=item
(except that whitespace is allowed in the value of the following attributes,
between the comma ending one name and the start of the 
next name in the CSV list, so you can split long lists across lines and use 
tabs and spaces for formatting):

=item *
the members attribute of the <group ... /> element,

=item *
the name attribute of the <member ... /> element,

=item *
the alias attribute of the <synonym ... /> element

=back
=back

=back

=head1 OUTPUT PRODUCED

The XML above produces this output:


    no ip access-list extended vlan2_in
    no ip access-list extended vlan4_in
    no ip access-list extended vlan5_in
    no ip access-list extended atm_in
    
    ip access-list extended vlan2_in
     permit tcp host 192.168.129.8  eq 80  any  gt 1023  established 
     permit tcp host 192.168.129.9  eq 80  any  gt 1023  established 
     permit tcp host 192.168.129.8  eq 443  any  gt 1023  established 
     permit tcp host 192.168.129.9  eq 443  any  gt 1023  established 
     permit tcp host 192.168.129.8   host 172.21.34.34  eq 25  
     permit tcp host 192.168.129.8   host 172.23.14.10  eq 25  
     permit tcp host 192.168.129.9   host 172.21.34.34  eq 25  
     permit tcp host 192.168.129.9   host 172.23.14.10  eq 25  
     permit udp host 192.168.129.8  eq 161  host 192.168.129.76   
     permit udp host 192.168.129.9  eq 161  host 192.168.129.76   
     permit udp host 192.168.129.8   host 192.168.129.76  eq 162  
     permit udp host 192.168.129.9   host 192.168.129.76  eq 162  
     deny ip any any log
    
    
    ip access-list extended vlan4_in
     permit tcp host 192.168.129.76   host 172.21.34.34  eq 25  
     permit tcp host 192.168.129.76   host 172.23.14.10  eq 25  
     permit udp host 192.168.129.76   host 192.168.129.8  eq 161  
     permit udp host 192.168.129.76   host 192.168.129.9  eq 161  
     permit udp host 192.168.129.76   host 192.168.129.100  eq 161  
     permit udp host 192.168.129.76   host 192.168.129.101  eq 161  
     deny ip any any log
    
    
    ip access-list extended vlan5_in
     permit tcp host 192.168.129.100  eq 80  any  gt 1023  established 
     permit tcp host 192.168.129.101  eq 80  any  gt 1023  established 
     permit tcp host 192.168.129.100   host 172.21.34.34  eq 25  
     permit tcp host 192.168.129.100   host 172.23.14.10  eq 25  
     permit tcp host 192.168.129.101   host 172.21.34.34  eq 25  
     permit tcp host 192.168.129.101   host 172.23.14.10  eq 25  
     permit udp host 192.168.129.100  eq 161  host 192.168.129.76   
     permit udp host 192.168.129.101  eq 161  host 192.168.129.76   
     permit udp host 192.168.129.100   host 192.168.129.76  eq 162  
     permit udp host 192.168.129.101   host 192.168.129.76  eq 162  
     deny ip any any log
    
    
    ip access-list extended atm_in
     permit tcp any  gt 1023  host 192.168.129.8  eq 80  
     permit tcp any  gt 1023  host 192.168.129.9  eq 80  
     permit tcp any  gt 1023  host 192.168.129.100  eq 80  
     permit tcp any  gt 1023  host 192.168.129.101  eq 80  
     permit tcp any  gt 1023  host 192.168.129.8  eq 443  
     permit tcp any  gt 1023  host 192.168.129.9  eq 443  
     permit tcp host 172.21.34.34  eq 25  host 192.168.129.8   established 
     permit tcp host 172.23.14.10  eq 25  host 192.168.129.8   established 
     permit tcp host 172.21.34.34  eq 25  host 192.168.129.9   established 
     permit tcp host 172.23.14.10  eq 25  host 192.168.129.9   established 
     permit tcp host 172.21.34.34  eq 25  host 192.168.129.76   established 
     permit tcp host 172.23.14.10  eq 25  host 192.168.129.76   established 
     permit tcp host 172.21.34.34  eq 25  host 192.168.129.100   established 
     permit tcp host 172.23.14.10  eq 25  host 192.168.129.100   established 
     permit tcp host 172.21.34.34  eq 25  host 192.168.129.101   established 
     permit tcp host 172.23.14.10  eq 25  host 192.168.129.101   established 
     deny ip any any log

    end


=head1 ELEMENTS:

Below are listed the different elements.

=head3 C<<host name='something' ip='an_IP_address_or_range' />>

This 'empty' element is more or less like a line in the /etc/hosts file - it equates 
a name with an IP address.  It is different in that it can also equate a name to 
an IP network.  Valid formats looks like: '192.168.5.4' or '192.168.5.0/24' or 
'192.168.5.0 255.255.255.0'.

Most of the time, you will want to use this only for real hosts, because there is 
an automatic function to create a group of all the hosts on a given network.  If you've put 
something that matches such a group as a host entry, then when you write ACLs against that group, 
you'll see weird stuff in the output.

Instead, use C<<group .../>> elements of 1 member to alias multiple names to a host, or
to name an IP range so it won't get autogrouped. 

=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<host name='something' ip='an_IP_address_or_range' flags='no_autogroup' />>

Same as above, but with the  B<flags> attribute. 
The B<flags> attribute is B<optional>.  As of now, the only defined value is B<no_autogroup>,
which prevents the inclusion of this host in autogrouping by network (see the B<E<lt>net.../E<gt>> 
element below).



=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<group name='something' members='CSV_list_of_IP_or_hostname_or_groupnames' />>

There are 2 forms of the group element.  This is the 'empty' or 'one-liner' version. 
The name is a name that can be used as a member in other group definitions, or as the src or dst 
in an acl element.  members is one or items as a comma separated list.  These items can be 
other (already defined) group names, or anything that's valid in the IP attribute of a host element. 
These items will be evaluated recursively to ensure that these conditions are met.


=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<group name='something'>> C<<member name='something_else'/>>  C<</group>> 

This is an element that contains member elements.  The member elements 
serve the same purpose as the members attribute, so the following snippets are equivalent:

	<group name='httpservers' members='server1,server2,server3'/>

and

	<group name='httpservers'>
	<member name='server1'/>
	<member name='server2'/>
	<member name='server3'/>
	</group>`

and 

	<group name='httpservers' members='server1'>
	<member name='server2'/>
	<member name='server3'/>
	</group>

are all the same group.

The values of the name attribute in a member element is subject to the same 
constraints as the values of  members attribute in the group element.



=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html


=head3 C<<net name='interface_name' ip='IP_address_range' int='other_interface_name' />>

This element is critically important.  The program uses the information in the B<E<lt>net ... /E<gt>> 
elements to determine which interfaces the endpoints of an ACL are on.

The 'int' attribute is optional (except in the C<<net name='DEFAULT".../>> element).  It allows
the network range to be assigned to a different interface.  This is useful when you want
to tell the program where the default route is,which is where stuff to/from networks not defined 
leave/enter the router, or to use the auto-grouping feature for a network to 
which the router doesn't directly connected.

Auto-grouping is a convenience feature.  When a C<<net.../>> tag is processed
and a C<<net name='DEFAULT" ... />> exists, then groups are created for each net name.
The groups are named with the net name perpended with 'gr_' and contain as their members
the names of all the host elements whose IP entries fall within the IP range for that interface.

Example:

		<net name='vlan1' ip='192.168.129.0/27' />
		<net name='vlan2' ip='192.168.129.32/27' />
		<net name='atm' ip='192.168.254.28/29' />
		<net name='vlan6' ip='192.168.169.32/27' int='atm'/>
		<net name='DEFAULT' ip='0.0.0.0/0' int='atm' />

Access lists will be created named vlan1_in, vlan2_in and atm_in.  Endpoints falling 
into IP address ranges not defined in a E<lt>net ... /E<gt> entry (other than the DEFAULT entry) 
will be considered as connected via the atm interface because of 
the B<int='atm'> in the B<C<<net name='DEFAULT'.../>>> entry.  Endpoints falling in the vlan6 range 
will also be considered as connected via interface atm, due to the int='atm' in the 
C<<net name='vlan6'... />> entry.

A group named 'gr_vlan1' to be auto-created that will contain 
the ip attribute of each host element whose ip attribute was in the range 192.168.129.1-192.168.129.31 
at the time the C<<net name='DEFAULT' .../>> tag was processed.

A group named 'gr_vlan2' to be auto-created that will contain the ip attribute of each 
host element whose ip attribute was in the range 192.168.129.33-192.168.129.63, 
at the time the C<<net name='DEFAULT' .../>> was processed.

A group named 'gr_vlan6' will be auto-created that will contain the ip attributes of all host elements 
whose ip attributes were in the range 192.168.169.33-192.168.169.63 
at the time the C<<net name='DEFAULT' .../>> was processed.

If you have a host that falls into the range of one of the nets defined in the 
C<<net .../>> elements, but you B<don't> want it in the auto-created groups, you can:

=over 8

=item * 

Use the flags='no_autogroup' feature of the 
E<lt>host.../E<gt> element.

=item *

Put it's C<<host .../>> entry in B<AFTER> the C<<net name='DEFAULT' .../>> element. 
Of course, this means that anything that refers to that host by name must also occur 
after this C<<host.../>> element.  

=item *

Define it in a E<lt>group.../E<gt> 
element instead of a host element.  

=back


The 'DEFAULT' entry is important.  It tells the program you're finished defining nets.  
It tells the program where to send stuff that you haven't defined nets for, by using the 
int attribute. 

B<The DEFAULT net entry, which MUST be the last E<lt>net.../E<gt> entry>, will always look like this:

B<E<lt>net name='DEFAULT' ip='0.0.0.0/0' int='some_interface' /E<gt>>


Except that B<the  E<lt>net name='DEFAULT'.../E<gt> MUST be the final E<lt>net../E<gt> entry>, 
the order of the C<<net .../>> entries isn't important, as they will be sorted so that 
testing will match the most-specific net (longest netmask) first.


=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<synonym alias='something' real='something_else'/>>

These are used to alias one text string for another.  The alias can be chose arbitrarily. 
synonym aliases and group/host names are different namespaces, so 'any' as a group or host name 
doesn't clash with 'any' as the alias of a synonym.  This is because groups and hosts are used 
for endpoints, where as synonyms are used for other attributes (ports and ints ) of the acl entry. 
The 'real' attribute can be pretty much anything.  
When used in an attribute of an E<lt>acl ...E<gt> element, it will be expanded recursively if it is 
the alias attribute of a previously defined synonym.  If it contains commas, it is treated as a list of 
literals or aliases to be expanded.


=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<acl .../>>

Finally, the reason we're doing all this.  several forms exist.

=head3 C<<acl act='plug'..../>> - the degenerate form

The crudest of these is:

C<<acl act='plug' proto='literal ACL expression' int='interfaces_to_apply_this_to' />>

This form is for ACL forms I haven't written the code to handle yet.  It could also be used to 
put comments into the ACLs output file.  The value of the proto attribute is literally 
applied to any interfaces listed in the int attribute.  

The value of the int attribute can be a single interface, an alias name, or a CSV list of either of both. 
Aliases are expanded recursively here.

No validation of any sort is performed on the proto statement.


=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<acl ... />> - the normal form

The next form of the C<<acl .../>> element looks like:

	<acl act='permit' proto='tcp' src='endpoint_spec' srcprt='portspec' 
		dst='other_endpoint_spec' dstprt='other_port_spec' 
		int='int_spec' no_return='y' non_reflexive='y' return_only='y' />


 The attributes are defined as follows:


=head3 MANDATORY Attributes of the normal form E<lt>acl ... /E<gt> element:

=head4 	act

this is 'permit' or 'deny'

=head4 proto

this is actually anything that can fit the format.  
if 'tcp', then in non-reflexive mode, the return path will have the 'established' 
key word added. 
Otherwise, not tested.

=head4 src dst

These are the endpoints.  they can be an IP address, a range of same, 
a hostname, or a group name or a CSV list of any combination of these.

=head4  srcprt dstprt

ports	for ACL.  if '0-0' or synonym therefore, none specified in ACL. 
if num1-num2, then ACL will contain 'range num1 num2' if 'num1', 'eq num1' in ACL. 
if 'eqNUM' or 'gtNUM' or 'ltNUM' or 'neqNUM', the expected text is created. 
a port spec can be a CSV list of any of these, plus aliases, but only  
one of srcprt and dstprt can be (or expand to) a list.  The author 
thinks that a list in both places is too weird. 


=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 OPTIONAL Attributes of the normal form E<lt>acl ... /E<gt> element:

=head4	int

Normally, the program uses the ip address information for each src and dst to 
figure out where the ACL entries go.  This attribute overrides the interface 
where the source is considered to be connected.  Useful for when the source is 
0.0.0.0/0 (typically aliased to 'any'), but the ACL is to be applied to 
other than the DEFAULT interface.  This can be an interface name, an alias, or a CSV 
of either or both.  Aliases are expanded recursively.

=head4	no_return

This is used to suppress the return direction.  Useful when the target is contained 
within the router for which ACLs are being generated (such as SNMP or HSRP) and the 
return ACL would be on the same interface as the forward ACL and would be gibberish. 
return_only	Only make the ACL from dst to src.

=head4 return_only

Used to create only the return direction (from dst to src).

=head4 non_reflexive

Even if the command-line switch for reflexive is specified, do this ACL 
non-reflexively.   If you think this connection can pass through this 
router in one direction but not the other, use this.

=head4 flags

This is a CSV list of flags.  A flag can be just a flag whose presence does its job, or if can 
be in the form B<C<flag_name:flag_value>>.  

flag_name and flag_value can contain neither commas nor colons (:).  

The only flag currently defined for the E<lt>acl ... /E<gt> element is
the 'comment', in the form: B<C<flags='comment:comment_text_for_ACL_output_here>>.  If the commant text 
doesn't start with a ! symbol, a 2 character '! ' sequence will be prepended.  This comment will then 
precede each ACL entry, forward or reverse, created by this E<lt>acl ... /E<gt> element.

=begin html
<A HREF="#__index__"><SMALL>Back to Top</SMALL></A>
<hr>

=end html

=head3 C<<IGNORE>> and C<</IGNORE>>

Any elements between these element will be ignored.  

Unlike XML comment 
delimiters B<S<C<  E<lt>!--  >>> and B<S<C<  --E<gt>  >>>  these nest, 
the outermost pair define the start/end of 'ignoring'.

Note that if you use a B<E<lt>IGNOREE<gt> ... E<lt>/IGNOREE<gt>> sequence, 
the text contained between them must be valid XML or comments.

=head1 CAVEATS

This hasn't been tested much in reflexive mode.

Empty ACLs are created for interfaces even if their networks were forced.

If you have an E<lt>acl ... /E<gt> element for which both src and dst are external via forcing 
of their networks to the same interface, both the src and dst entries will appear in the ACL for that 
interface.  This is sort of undesireable, since in this case, legitimate traffic wouldn't 
typically flow this way, plus at least one of the entries looks silly.  Someday I'll figure 
out correct logic to deal with this.


=head1 AUTHOR

Send bug reports, hints, tips, suggestions to Bob Niederman at:
bob@bob-n.com


