package IPTables::SubnetSkeleton; use strict; use warnings; use Carp; # Remember to set the Perl @INC correct at a higher level. use Net::IPAddress; use IPTables::interface; # Local module vars (that perhaps should be moved somewhere else). my $packageid=__PACKAGE__; BEGIN { use Exporter (); our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); # Package version $VERSION = 0.3; @ISA = qw(Exporter); @EXPORT = qw(&insert_element &print_cidr_breaks &connect_to &worstcase_search); } # new($skeleton_name, $skeleton_type, @cidr_breaks) # # Create a new SubnetSkeleton Object. sub new { my ($skeleton_name, $skeleton_type, $table, @cidr_breaks) = @_; my $self = {}; $self->{'skeleton_name'} = $skeleton_name; #$self->{'skeleton_type'} = $skeleton_type; $self->{'cidr_breaks'} = \@cidr_breaks; # Netfilter table (possible: nat, mangle, filter, raw) if (defined $table && $table ne "") { $self->{'netfilter_table'} = $table; } else { # Defaults to table "filter" $self->{'netfilter_table'} = "filter"; } # Create a IPTables object and store it in this object. my $ipt_obj = IPTables::interface::new($self->{'netfilter_table'}); $self->{'iptables_object'} = $ipt_obj; bless($self); $self->skeleton_type($skeleton_type); # TODO: Implement a module Error function if ( ! $self->validate_cidr_breaks() ) { die "[ERROR] Invalid CIDR breaks."; } # Create the base skeleton chain my $chain = $self->get_chainname_skeleton; if ( ! $self->iptables_chain_exist("$chain") ) { $self->iptables_chain_new("$chain"); } return $self; } sub get_iptables_object() { my $self = shift; my $ipt_obj = $self->{'iptables_object'}; return $ipt_obj; } # connect_to($chainname) # # The subnet skeleton is not by default connected to # any build-in chain. You need to explicitly connect # it to the desired chain using this function. sub connect_to($) { my $self = shift; my $connect_chain = shift; my $success; my $jump_chain = $self->get_chainname_skeleton(); # Exist: $connect_chain if ( ! $self->iptables_chain_exist("$connect_chain") ) { carp "WARNING: Cannot connect_to(\"$connect_chain\") as it does not exist.\n"; } else { # Exist: $jump_chain if ( ! $self->iptables_chain_exist("$jump_chain") ) { # If our own base skeleton chain does not exist, it must be created! $self->iptables_chain_new("$jump_chain"); #carp "WARNING: Cannot use jump_chain \"$jump_chain\" as it does not exist.\n"; } $success = $self->iptables_append("$connect_chain", "", "$jump_chain"); } return $success; } # validate_cidr_breaks() # # Validates the CIDR breaks to lie within the 1..31 range. # returns 1 if this is the case and 0 otherwise. # sub validate_cidr_breaks { my $self = shift; my $cidr; my $cidr_old = 0; foreach $cidr (@{$self->{'cidr_breaks'}}) { # # Verify between 1 and 31 if ($cidr < 1 || $cidr > 31) { return 0; } # # Verify CIDR breaks is in increasing order. if ($cidr < $cidr_old) { return 0; } $cidr_old = $cidr; } return 1; } # print_cidr_breaks # # Prints current CIDR breaks sub print_cidr_breaks { my $self = shift; my $i; my $cidr_ref = $self->{'cidr_breaks'}; my $num_elem = $#{$cidr_ref}; print("CIDR breaks:$num_elem\n"); foreach $i (@{$cidr_ref}) { printf("%s\n", $i); } } # skeleton_type($skeleton_type) # # Sets the skeleton type to either 'src' or 'dst' depending # on whether you want to match on Source or Destination IP addresses. sub skeleton_type($) { my $self = shift; my $type = shift; if ($type eq 'src') { $self->{'skeleton_type'} = 'src'; } elsif ($type eq 'dst') { $self->{'skeleton_type'} = 'dst'; } else { die("${packageid}::skeleton_type not 'src' or 'dst'"); } } # Future idea function sub find_longest_search_path() { } # Future idea function sub show_tree() { } # What is the worst case search lenght sub worstcase_search() { my $self = shift; my $cidr; my $max_search; my @cidr_list = @{$self->{'cidr_breaks'}}; my $jumps = @cidr_list + 1; #the lenght + the initial chain my $leaf_network_elements = (1 << (32 - $cidr_list[-1])); my $prev = shift @cidr_list; push @cidr_list, 32; foreach $cidr (@cidr_list) { my $diff = $cidr - $prev; my $max_list_lenght = (1 << $diff); $max_search += $max_list_lenght; print "$cidr - $prev = $diff => " . (1 << $diff) . "\tmax_search:\t$max_search\n"; $prev = $cidr; } print "Tree depth (jump rules) :\t $jumps\n"; print "Leaf network max elements :\t $leaf_network_elements\n"; print "Longest search in the tree:\t " . ($max_search - $leaf_network_elements) . "\n"; print "Longest search in the tree:\t $max_search (incl. leaf network)\n"; # # We are actually missing the length of the "root-node" chain # as this can get very large dependent on the first CIDR # and most importantly the distribution of IP-ranges used. # return $max_search; } sub get_chainname_base($) { my $self = shift; my $name = $self->{'skeleton_name'} . "_" . $self->{'skeleton_type'}; return $name; } sub get_chainname_skeleton($) { my $self = shift; my $name = $self->get_chainname_base() . "_" . "skeleton"; return $name; } sub get_chainname_subnet($$$) { my $self = shift; my $subnet = shift; my $mask = shift; my $name = $self->get_chainname_base() . $subnet . "_" . $mask; return $name; } # our $debug; # #$debug = "true"; # sub iptables_command($$) # { # my $self = shift; # my $commands = shift; # my $table = $self->{'netfilter_table'}; # # my $full_command = "$iptables -t $table $commands 2>&1"; # # die "ERROR -- Use of obsolete function"; # # my $res; # my $status; # if ($debug) { # print "DEBUG: $full_command\n"; # $status = 1; # } else { # $res = `$full_command`; # # # # The exit value is in the high byte of the 16-bit status word # $status = $? >> 8; # } # # if ($status != 0) { # $self->{'iptables_error'} = $res; # } else { # $self->{'iptables_result'} = $res; # } # # if ($status == 3) { # warn "[iptables_command] WARNING: Permission problems?\n"; # } # return $status; # } sub iptables_chain_exist($$) { my $self = shift; my $chainname = shift; my $ipt = $self->get_iptables_object(); my $success= $ipt->is_chain("$chainname"); #my $status = $ipt->get_exitcode(); return $success; } sub iptables_chain_new($$) { my $self = shift; my $chainname = shift; my $ipt = $self->get_iptables_object(); #my $status = $self->iptables_command("-N $chainname"); my $success = $ipt->create_chain("$chainname"); return $success; } # # Small helper function to return the correct # iptables match type, that can be used in the rule. sub match_type($) { my $self = shift; my $match_type; my $type = $self->{'skeleton_type'}; if ($type eq 'src') { $match_type = "-s"; } elsif ($type eq 'dst') { $match_type = "-d"; } else { die("${packageid}::skeleton_type not 'src' or 'dst'"); } return $match_type; } # iptables_insert($insert_chain, $match, $target); # sub iptables_insert($$$) { my $self = shift; my $insert_chain = shift; my $match = shift; my $target = shift; my $match_type = $self->match_type(); my $ipt = $self->get_iptables_object(); my $the_match=""; if (defined $match && $match ne "") { $the_match="$match_type $match"; } my $success = $ipt->insert_rule("$insert_chain", "$the_match", "$target"); return $success; } # iptables_append($append_chain, $match, $target); # sub iptables_append($$$) { my $self = shift; my $append_chain = shift; my $match = shift; my $target = shift; my $match_type = $self->match_type(); my $ipt = $self->get_iptables_object(); my $the_match=""; if (defined $match && $match ne "") { $the_match="$match_type $match"; } my $success = $ipt->append_rule("$append_chain", "$the_match", "$target"); return $success; } # iptables_delete($chain, $match, $target); # sub iptables_delete($$$) { my $self = shift; my $chain = shift; my $match = shift; my $target = shift; my $match_type = $self->match_type(); my $ipt = $self->get_iptables_object(); #my $status = $self->iptables_command("-D $chain $match_type $match -j $target"); my $the_match=""; if (defined $match && $match ne "") { $the_match="$match_type $match"; } my $success = $ipt->delete_rule("$chain", "$the_match", "$target"); return $success; } # iptables_commit(); # sub iptables_commit() { my $self = shift; my $ipt = $self->get_iptables_object(); my $success = $ipt->commit(); return $success; } our $algo_debug; #$algo_debug = "true"; # insert_element($ip, $target_chain) # # Insert the $ip into the subnet skeleton. When matching the IP, # jump to $target_chain. sub insert_element($$) { my $self = shift; my $ip = shift; my $target_chain = shift; # Validate input parameters if ( ! Net::IPAddress::validaddr("$ip") ) { die "[insert_element] Not valid IP: $ip ... missing string quotes?\n"; } if ( !$self->iptables_chain_exist("$target_chain") ) { warn "[insert_element] ERROR: TargetChain \"$target_chain\" does not exist\n"; } # 1. Initializes # The initial chainname print "1." if ($algo_debug); my $POS = $self->get_chainname_skeleton; my $i = 0; # # Create the base chain if it don't exists. # - This check is removed, as it's created elsewhere. # if ( !$self->iptables_chain_exist("$POS") ){ # $self->iptables_chain_new("$POS"); # } my $cidr_ref = $self->{'cidr_breaks'}; #my $num_elem = $#{$cidr_ref}; # 2. "Check for termination", is done simply by processing all # elements in the list "cidr_breaks". # print "2." if ($algo_debug); # Loop over CIDR list foreach my $mask (@{$cidr_ref}) { # 3. Calculate subnet print "3." if ($algo_debug); my $SUBNET = Net::IPAddress::mask("$ip",$mask); #print "$ip/$mask => subnet: $SUBNET\n"; my $chainname = $self->get_chainname_subnet($SUBNET, $mask); # 4. Is subnet present? => goto 6 # Check that $chainname exists, if it does we assume that # the jump target also exists in the $POS chain. if ( !$self->iptables_chain_exist("$chainname") ){ print "5." if ($algo_debug); # 5. Create subnet chain and connect to $POS chain $self->iptables_chain_new($chainname); $self->iptables_insert($POS, "$SUBNET/$mask", $chainname); } # 6. "Enter subnet chain" $POS = $chainname; print "6." if ($algo_debug); } # 2. "Connect TargetChain", # at the end of the foreach loop we know that "bi == bn". print "end.\n" if ($algo_debug); # Extra : need to avoid inserting if it already exists # Simply solution: delete and insert $self->iptables_delete($POS, "$ip", $target_chain); $self->iptables_insert($POS, "$ip", $target_chain); } sub get_leaf_chainname($) { my $self = shift; my $ip = shift; my $cidr_ref = $self->{'cidr_breaks'}; #my $bottom_cidr = @{$cidr_ref}[-1]; my $bottom_cidr = $cidr_ref->[-1]; my $SUBNET = Net::IPAddress::mask("$ip",$bottom_cidr); my $chainname = $self->get_chainname_subnet($SUBNET, $bottom_cidr); return $chainname; } sub disconnect_element($$) { my $self = shift; my $ip = shift; my $target_chain = shift; my $leaf_chainname = $self->get_leaf_chainname("$ip"); my $success = 0; # Check that the $target_chain exist before trying to # delete a rule refering to it... as it will fail and die! if ( $self->iptables_chain_exist("$target_chain") ) { $success = $self->iptables_delete("$leaf_chainname", "$ip", "$target_chain"); } return $success; } # Idea: # 'disconnect_element' does not cleanup the tree (or actually trie). # This could be solved by, looking at neighbors at the leaf, if no # neighbors is left (no rules left in the chain), we can try # cleaning the parent node (recurse...). # Idea: Cleaning the tree. # A tree pruning or periodic cleanup, could be done by looking at # chains with zero "references". # # ... but it probably won't happen as we do not perform inner tree # cleanups (we only do leaf deletes). 1; # Usage example: # my $hat = IPTables::SubnetSkeleton::new(Access, src, "filter", 20, 24, 27); # $hat->validate_cidr_breaks(); # $hat->insert_element($IP, $TargetChain); __END__ # Below is stub documentation for your module. You'd better edit it! =head1 NAME IPTables::SubnetSkeleton - iptables subnet based IP search tree =head1 INTRODUCTION SubnetSkeleton is a Perl module for building search trees for netfilter/iptables. This is primarily to be used to cut down on the number of lookups needed in the IPTables rule space from O(n) to O(log n). Initially it was written because the author had 3000+ IP addresses and needed special treatment of each. Linear scans on 3000 addresses are far too many to be feasible. This the following package was developed in order to cut down on the number of lookups. =head1 SYNOPSIS use IPTables::SubnetSkeleton; my @netmasks = (8, 18, 20, 22, 24, 26, 28); $subnet_src = IPTables::SubnetSkeleton::new("shortname", "src", "filter", @netmasks); $subnet_dst = IPTables::SubnetSkeleton::new("shortname", "dst", "filter", @netmasks); # Connect subnet skeleton to build-in chain "FORWARD". $subnet_src->connect_to("FORWARD"); $subnet_dst->connect_to("FORWARD"); # SubnetSkeleton: Add a IP match that will jump to $userchain $subnet_src->insert_element("$IP", "$userchain"); $subnet_dst->insert_element("$IP", "$userchain"); # Remember to commit iptables changes to kernel $subnet_src->iptables_commit(); =head1 ALGORITHM FIXME: PLEASE insert algorithm description from algorithm_SubnetSkeleton.apt. ~/svn/dcu/trunk/documentation/technical/algorithm_SubnetSkeleton.apt =head1 DEPENDENCIES L, L. =head1 AUTHORS Jesper Louis Andersen (jla@comx.dk) or (jlouis@mongers.org). Jesper Dangaard Brouer (jdb@comx.dk) or (hawk@diku.dk). =head1 SVN revision $Date: 2008-09-10 19:55:59 +0200 (Wed, 10 Sep 2008) $ $LastChangedRevision: 969 $ =head1 COPYRIGHT AND LICENSE Copyright (C) 2006 by Jesper Dangaard Brouer. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. =head1 TODO Better documentation. Better descriptions on each function, describing what it does. Perform lenght check of the auto-generated userchain-"names". The lenght of iptables userchain-"names" is restricted to 29 charaters. =cut