# # Shorewall-perl 4.2 -- /usr/share/shorewall-perl/Shorewall/Chains.pm # # This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt] # # (c) 2007,2008 - Tom Eastep (teastep@shorewall.net) # # Complete documentation is available at http://shorewall.net # # This program is free software; you can redistribute it and/or modify # it under the terms of Version 2 of the GNU General Public License # as published by the Free Software Foundation. # # 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. # # This is the low-level iptables module. It provides the basic services # of chain and rule creation. It is used by the higher level modules such # as Rules to create iptables-restore input. # package Shorewall::Chains; require Exporter; use Scalar::Util 'reftype'; use Shorewall::Config qw(:DEFAULT :internal); use Shorewall::Zones; use Shorewall::IPAddrs; use strict; our @ISA = qw(Exporter); our @EXPORT = qw( add_rule add_jump insert_rule new_chain new_manual_chain ensure_manual_chain log_rule_limit %chain_table $nat_table $mangle_table $filter_table ); our %EXPORT_TAGS = ( internal => [ qw( STANDARD NATRULE BUILTIN NONAT NATONLY REDIRECT ACTION MACRO LOGRULE NFQ CHAIN NO_RESTRICT PREROUTE_RESTRICT INPUT_RESTRICT OUTPUT_RESTRICT POSTROUTE_RESTRICT ALL_RESTRICT chain_family add_command add_commands move_rules process_comment no_comment macro_comment clear_comment incr_cmd_level decr_cmd_level chain_base forward_chain zone_forward_chain use_forward_chain input_chain zone_input_chain use_input_chain output_chain zone_output_chain use_output_chain masq_chain syn_flood_chain mac_chain macrecent_target dnat_chain snat_chain ecn_chain first_chains ensure_chain ensure_accounting_chain ensure_mangle_chain ensure_nat_chain new_standard_chain new_builtin_chain new_nat_chain ensure_filter_chain initialize_chain_table finish_section setup_zone_mss newexclusionchain clearrule do_proto mac_match verify_mark verify_small_mark validate_mark do_test do_ratelimit do_connlimit do_time do_user do_length do_tos do_connbytes do_helper match_source_dev match_dest_dev iprange_match match_source_net match_dest_net match_orig_dest match_ipsec_in match_ipsec_out log_rule expand_rule addnatjump get_interface_address get_interface_addresses get_interface_bcasts get_interface_acasts get_interface_gateway get_interface_mac set_global_variables create_netfilter_load create_chainlist_reload $section %sections %targets ) ], ); Exporter::export_ok_tags('internal'); our $VERSION = 4.3.0; # # Chain Table # # %chain_table { => { => { name => # table =>
# is_policy => undef|1 -- if 1, this is a policy chain # is_optional => undef|1 -- See below. # referenced => undef|1 -- If 1, will be written to the iptables-restore-input. # builtin => undef|1 -- If 1, one of Netfilter's built-in chains. # manual => undef|1 -- If 1, a manual chain. # accounting => undef|1 -- If 1, an accounting chain # log => # policy => # policychain => -- self-reference if this is a policy chain # policypair => [ , ] -- Used for reporting duplicated policies # loglevel => # synparams => # synchain => # default => # cmdlevel => # rules => [ # # ... # ] # } , # => ... # } # } # # 'is_optional' only applies to policy chains; when true, indicates that this is a provisional policy chain which might be # replaced. Policy chains created under the IMPLICIT_CONTINUE=Yes option are marked with is_optional == 1. # # Only 'referenced' chains get written to the iptables-restore input. # # 'loglevel', 'synparams', 'synchain' and 'default' only apply to policy chains. # our %chain_table; our $nat_table; our $mangle_table; our $filter_table; # # It is a layer violation to keep information about the rules file sections in this module but in Shorewall, the rules file # and the filter table are very closely tied. By keeping the information here, we avoid making several other modules dependent # on Shorewall::Rules. # our %sections; our $section; our $comment; use constant { STANDARD => 1, #defined by Netfilter NATRULE => 2, #Involves NAT BUILTIN => 4, #A built-in action NONAT => 8, #'NONAT' or 'ACCEPT+' NATONLY => 16, #'DNAT-' or 'REDIRECT-' REDIRECT => 32, #'REDIRECT' ACTION => 64, #An action (may be built-in) MACRO => 128, #A Macro LOGRULE => 256, #'LOG' NFQ => 512, #'NFQUEUE' CHAIN => 1024, #Manual Chain }; our %targets; # # expand_rule() restrictions # use constant { NO_RESTRICT => 0, # FORWARD chain rule - Both -i and -o may be used in the rule PREROUTE_RESTRICT => 1, # PREROUTING chain rule - -o converted to -d
using main routing table INPUT_RESTRICT => 4, # INPUT chain rule - -o not allowed OUTPUT_RESTRICT => 8, # OUTPUT chain rule - -i not allowed POSTROUTE_RESTRICT => 16, # POSTROUTING chain rule - -i converted to -s
using main routing table ALL_RESTRICT => 12 # fw->fw rule - neither -i nor -o allowed }; our $exclseq; our $iprangematch; our $chainseq; our %interfaceaddr; our %interfaceaddrs; our %interfacenets; our %interfacemacs; our %interfacebcasts; our %interfaceacasts; our %interfacegateways; our @builtins = qw(PREROUTING INPUT FORWARD OUTPUT POSTROUTING); # # Mode of the generator. # use constant { NULL_MODE => 0 , # Generating neither shell commands nor iptables-restore input CAT_MODE => 1 , # Generating iptables-restore input CMD_MODE => 2 }; # Generating shell commands. our $mode; our $family; # # Initialize globals -- we take this novel approach to globals initialization to allow # the compiler to run multiple times in the same process. The # initialize() function does globals initialization for this # module and is called from an INIT block below. The function is # also called by Shorewall::Compiler::compiler at the beginning of # the second and subsequent calls to that function. # sub initialize( $ ) { %chain_table = ( raw => {} , mangle => {}, nat => {}, filter => {} ); $nat_table = $chain_table{nat}; $mangle_table = $chain_table{mangle}; $filter_table = $chain_table{filter}; # # These get set to 1 as sections are encountered. # %sections = ( ESTABLISHED => 0, RELATED => 0, NEW => 0 ); # # Current rules file section. # $section = 'ESTABLISHED'; # # Contents of last COMMENT line. # $comment = ''; # # Used to sequence 'exclusion' chains with names 'excl0', 'excl1', ... # $exclseq = 0; # # Used to suppress duplicate match specifications. # $iprangematch = 0; # # Sequence for naming temporary chains # $chainseq = undef; # # Keep track of which interfaces have active 'address', 'addresses', 'networks', etc. variables # %interfaceaddr = (); %interfaceaddrs = (); %interfacenets = (); %interfacemacs = (); %interfacebcasts = (); %interfaceacasts = (); %interfacegateways = (); $family = shift; } INIT { initialize( F_IPV4 ); } # # Add a run-time command to a chain. Arguments are: # # Chain reference , Command # # # Process a COMMENT line (in $currentline) # sub process_comment() { if ( $capabilities{COMMENTS} ) { ( $comment = $currentline ) =~ s/^\s*COMMENT\s*//; $comment =~ s/\s*$//; } else { warning_message "COMMENT ignored -- requires comment support in iptables/Netfilter"; } } # # Returns True if there is a current COMMENT or if COMMENTS are not available. # sub no_comment() { $comment ? 1 : $capabilities{COMMENTS} ? 0 : 1; } # # Clear the $comment variable # sub clear_comment() { $comment = ''; } # # Set $comment to the passed unless there is a current comment # sub macro_comment( $ ) { my $macro = $_[0]; $comment = $macro unless $comment || ! ( $capabilities{COMMENTS} && $config{AUTO_COMMENT} ); } # # Functions to manipulate cmdlevel # sub incr_cmd_level( $ ) { $_[0]->{cmdlevel}++; } sub decr_cmd_level( $ ) { fatal_error "Internal error in decr_cmd_level()" if --$_[0]->{cmdlevel} < 0; } sub add_command($$) { my ($chainref, $command) = @_; push @{$chainref->{rules}}, join ('', ' ' x $chainref->{cmdlevel} , $command ); $chainref->{referenced} = 1; } sub add_commands { my $chainref = shift @_; for my $command ( @_ ) { push @{$chainref->{rules}}, join ('', ' ' x $chainref->{cmdlevel} , $command ); } $chainref->{referenced} = 1; } sub push_rule( $$ ) { my ($chainref, $rule) = @_; $rule .= qq( -m comment --comment "$comment") if $comment; if ( $chainref->{cmdlevel} ) { $rule =~ s/"/\\"/g; #Must preserve quotes in the rule add_command $chainref , qq(echo "-A $chainref->{name} $rule" >&3); } else { # # We omit the chain name for now -- this makes it easier to move rules from one # chain to another # push @{$chainref->{rules}}, join( ' ', '-A' , $rule ); $chainref->{referenced} = 1; } } # # Add a rule to a chain. Arguments are: # # Chain reference , Rule [, Expand-long-dest-port-lists ] # sub add_rule($$;$) { my ($chainref, $rule, $expandports) = @_; fatal_error 'Internal Error in add_rule()' if reftype $rule; $iprangematch = 0; # # Pre-processing the port lists as was done in Shorewall-shell results in port-list # processing driving the rest of rule generation. # # By post-processing each rule generated by expand_rule(), we avoid all of that # messiness and replace it with the following localized messiness. # # Because source ports are seldom specified and source port lists are rarer still, # we only worry about the destination ports. # if ( $expandports && $rule =~ /^(.* --dports\s+)([^ ]+)(.*)$/ ) { # # Rule has a --dports specification # my ($first, $ports, $rest) = ( $1, $2, $3 ); if ( ( $ports =~ tr/:,/:,/ ) > 14 ) { # # More than 15 ports specified # my @ports = split '([,:])', $ports; while ( @ports ) { my $count = 0; my $newports = ''; while ( @ports && $count < 15 ) { my ($port, $separator) = ( shift @ports, shift @ports ); $separator ||= ''; if ( ++$count == 15 ) { if ( $separator eq ':' ) { unshift @ports, $port, ':'; chop $newports; last; } else { $newports .= $port; } } else { $newports .= "${port}${separator}"; } } push_rule ( $chainref, join( '', $first, $newports, $rest ) ); } } else { push_rule ( $chainref, $rule ); } } else { push_rule ( $chainref, $rule ); } } # # Add a jump from the chain represented by the reference in the first argument to # the target in the second argument. The optional third argument specifies any # matches to be included in the rule and must end with a space character if it is non-null. # sub add_jump( $$$;$ ) { my ( $fromref, $to, $goto_ok, $predicate ) = @_; $predicate |= ''; my $toref; # # The second argument may be a scalar (chain name or builtin target) or a chain reference # if ( reftype $to ) { $toref = $to; $to = $toref->{name}; } else { # # Ensure that we have the chain unless it is a builtin like 'ACCEPT' # $toref = ensure_chain( $fromref->{table} , $to ) unless ( $targets{$to} || 0 ) & STANDARD; } # # If the destination is a chain, mark it referenced # $toref->{referenced} = 1 if $toref; my $param = $goto_ok && $toref && $capabilities{GOTO_TARGET} ? 'g' : 'j'; add_rule ($fromref, join( '', $predicate, "-$param $to" ) ); } # # Insert a rule into a chain. Arguments are: # # Chain reference , Rule Number, Rule # sub insert_rule($$$) { my ($chainref, $number, $rule) = @_; fatal_error 'Internal Error in insert_rule()' if $chainref->{cmdlevel}; $rule .= "-m comment --comment \"$comment\"" if $comment; splice( @{$chainref->{rules}}, $number - 1, 0, join( ' ', '-A', $rule ) ); $iprangematch = 0; $chainref->{referenced} = 1; } # # Move the rules from one chain to another # # The rules generated by interface options are added to the interfaces's input chain and # forward chain. Shorewall::Rules::generate_matrix() may decide to move those rules to # a zone-oriented chain, hence this function. # # The source chain must not have any run-time code included in its rules. # sub move_rules( $$ ) { my ($chain1, $chain2 ) = @_; if ( $chain1->{referenced} ) { my @rules = @{$chain1->{rules}}; for ( @rules ) { fatal_error "Internal Error in move_rules()" unless /^-A/; } splice @{$chain2->{rules}}, 0, 0, @rules; $chain2->{referenced} = 1; $chain1->{referenced} = 0; $chain1->{rules} = []; } } # # Change the passed interface name so it is a legal shell variable name. # sub chain_base($) { my $chain = $_[0]; $chain =~ s/^@/at_/; $chain =~ tr/[.\-%@]/_/; $chain =~ s/\+$//; $chain; } # # Forward Chain for an interface # sub forward_chain($) { $_[0] . '_fwd'; } # # Forward Chain for a zone # sub zone_forward_chain($) { $_[0] . '_frwd'; } # # Returns true if we're to use the interface's forward chain # sub use_forward_chain($) { my $interface = $_[0]; my $interfaceref = find_interface($interface); # # We must use the interfaces's chain if the interface is associated with multiple zone nets # $interfaceref->{nets} > 1; } # # Input Chain for an interface # sub input_chain($) { $_[0] . '_in'; } # # Input Chain for a zone # sub zone_input_chain($) { $_[0] . '_input'; } # # Returns true if we're to use the interface's input chain # sub use_input_chain($) { my $interface = $_[0]; my $interfaceref = find_interface($interface); my $nets = $interfaceref->{nets}; # # We must use the interfaces's chain if the interface is associated with multiple zone nets # return 1 if $nets > 1; # # Don't need it if it isn't associated with any zone # return 0 unless $nets; # # Interface associated with a single zone -- use the zone's input chain if it has one # my $chainref = $filter_table->{zone_input_chain $interfaceref->{zone}}; return 0 if $chainref; # # Use the '2fw' chain if it is referenced. # $chainref = $filter_table->{join( '' , $interfaceref->{zone} , '2' , firewall_zone )}; ! ( $chainref->{referenced} || $chainref->{is_policy} ) } # # Output Chain for an interface # sub output_chain($) { $_[0] . '_out'; } # # Output Chain for a zone # sub zone_output_chain($) { $_[0] . '_output'; } # # Returns true if we're to use the interface's output chain # sub use_output_chain($) { my $interface = $_[0]; my $interfaceref = find_interface($interface); my $nets = $interfaceref->{nets}; # # We must use the interfaces's chain if the interface is associated with multiple zone nets # return 1 if $nets > 1; # # Don't need it if it isn't associated with any zone # return 0 unless $nets; # # Interface associated with a single zone -- use the zone's output chain if it has one # my $chainref = $filter_table->{zone_output_chain $interfaceref->{zone}}; return 0 if $chainref; # # Use the 'fw2' chain if it is referenced. # $chainref = $filter_table->{join( '', firewall_zone , '2', $interfaceref->{zone} )}; ! ( $chainref->{referenced} || $chainref->{is_policy} ) } # # Masquerade Chain for an interface # sub masq_chain($) { $_[0] . '_masq'; } # # Syn_flood_chain -- differs from the other _chain functions in that the argument is a chain table reference # sub syn_flood_chain ( $ ) { '@' . $_[0]->{synchain}; } # # MAC Verification Chain for an interface # sub mac_chain( $ ) { $_[0] . '_mac'; } sub macrecent_target($) { $config{MACLIST_TTL} ? $_[0] . '_rec' : 'RETURN'; } # # DNAT Chain from a zone # sub dnat_chain( $ ) { $_[0] . '_dnat'; } # # SNAT Chain to an interface # sub snat_chain( $ ) { $_[0] . '_snat'; } # # ECN Chain to an interface # sub ecn_chain( $ ) { $_[0] . '_ecn'; } # # First chains for an interface # sub first_chains( $ ) #$1 = interface { my $c = $_[0]; ( $c . '_fwd', $c . '_in' ); } # # Create a new chain and return a reference to it. # sub new_chain($$) { my ($table, $chain) = @_; fatal_error "Internal error in new_chain()" if $chain_table{$table}{$chain}; $chain_table{$table}{$chain} = { name => $chain, rules => [], table => $table, loglevel => '', log => 1, cmdlevel => 0 }; } # # Create a chain if it doesn't exist already # sub ensure_chain($$) { my ($table, $chain) = @_; fatal_error 'Internal Error in ensure_chain' unless $table && $chain; my $ref = $chain_table{$table}{$chain}; return $ref if $ref; new_chain $table, $chain; } sub finish_chain_section( $$ ); # # Create a filter chain if necessary. Optionally populate it with the appropriate ESTABLISHED,RELATED rule(s) and perform SYN rate limiting. # sub ensure_filter_chain( $$ ) { my ($chain, $populate) = @_; my $chainref = $filter_table->{$chain}; $chainref = new_chain 'filter' , $chain unless $chainref; if ( $populate and ! $chainref->{referenced} ) { if ( $section eq 'NEW' or $section eq 'DONE' ) { finish_chain_section $chainref , 'ESTABLISHED,RELATED'; } elsif ( $section eq 'RELATED' ) { finish_chain_section $chainref , 'ESTABLISHED'; } } $chainref->{referenced} = 1; $chainref; } # # Create an accounting chain if necessary. # sub ensure_accounting_chain( $ ) { my ($chain) = @_; my $chainref = $filter_table->{$chain}; if ( $chainref ) { fatal_error "Non-accounting chain ($chain) used in accounting rule" if ! $chainref->{accounting}; } else { $chainref = new_chain 'filter' , $chain unless $chainref; $chainref->{accounting} = 1; $chainref->{referenced} = 1; } $chainref; } sub ensure_mangle_chain($) { my $chain = $_[0]; my $chainref = ensure_chain 'mangle', $chain; $chainref->{referenced} = 1; $chainref; } sub ensure_nat_chain($) { my $chain = $_[0]; my $chainref = ensure_chain 'nat', $chain; $chainref->{referenced} = 1; $chainref; } # # Add a builtin chain # sub new_builtin_chain($$$) { my ( $table, $chain, $policy ) = @_; my $chainref = new_chain $table, $chain; $chainref->{referenced} = 1; $chainref->{policy} = $policy; $chainref->{builtin} = 1; } sub new_standard_chain($) { my $chainref = new_chain 'filter' ,$_[0]; $chainref->{referenced} = 1; $chainref; } sub new_nat_chain($) { my $chainref = new_chain 'nat' ,$_[0]; $chainref->{referenced} = 1; $chainref; } sub new_manual_chain($) { my $chain = $_[0]; fatal_error "Duplicate Chain Name ($chain)" if $targets{$chain} || $filter_table->{$chain}; $targets{$chain} = CHAIN; ( my $chainref = ensure_filter_chain( $chain, 0) )->{manual} = 1; $chainref->{referenced} = 1; $chainref; } sub ensure_manual_chain($) { my $chain = $_[0]; my $chainref = $filter_table->{$chain} || new_manual_chain($chain); fatal_error "$chain exists and is not a manual chain" unless $chainref->{manual}; $chainref; } # # Add all builtin chains to the chain table # # sub initialize_chain_table() { if ( $family == F_IPV4 ) { # # As new targets (Actions, Macros and Manual Chains) are discovered, they are added to the table # %targets = ('ACCEPT' => STANDARD, 'ACCEPT+' => STANDARD + NONAT, 'ACCEPT!' => STANDARD, 'NONAT' => STANDARD + NONAT + NATONLY, 'DROP' => STANDARD, 'DROP!' => STANDARD, 'REJECT' => STANDARD, 'REJECT!' => STANDARD, 'DNAT' => NATRULE, 'DNAT-' => NATRULE + NATONLY, 'REDIRECT' => NATRULE + REDIRECT, 'REDIRECT-' => NATRULE + REDIRECT + NATONLY, 'LOG' => STANDARD + LOGRULE, 'CONTINUE' => STANDARD, 'CONTINUE!' => STANDARD, 'QUEUE' => STANDARD, 'QUEUE!' => STANDARD, 'NFQUEUE' => STANDARD + NFQ, 'NFQUEUE!' => STANDARD + NFQ, 'SAME' => NATRULE, 'SAME-' => NATRULE + NATONLY, 'dropBcast' => BUILTIN + ACTION, 'allowBcast' => BUILTIN + ACTION, 'dropNotSyn' => BUILTIN + ACTION, 'rejNotSyn' => BUILTIN + ACTION, 'dropInvalid' => BUILTIN + ACTION, 'allowInvalid' => BUILTIN + ACTION, 'allowinUPnP' => BUILTIN + ACTION, 'forwardUPnP' => BUILTIN + ACTION, 'Limit' => BUILTIN + ACTION, ); for my $chain qw(OUTPUT PREROUTING) { new_builtin_chain 'raw', $chain, 'ACCEPT'; } for my $chain qw(INPUT OUTPUT FORWARD) { new_builtin_chain 'filter', $chain, 'DROP'; } for my $chain qw(PREROUTING POSTROUTING OUTPUT) { new_builtin_chain 'nat', $chain, 'ACCEPT'; } for my $chain qw(PREROUTING INPUT OUTPUT ) { new_builtin_chain 'mangle', $chain, 'ACCEPT'; } if ( $capabilities{MANGLE_FORWARD} ) { for my $chain qw( FORWARD POSTROUTING ) { new_builtin_chain 'mangle', $chain, 'ACCEPT'; } } } else { # # As new targets (Actions, Macros and Manual Chains) are discovered, they are added to the table # %targets = ('ACCEPT' => STANDARD, 'ACCEPT!' => STANDARD, 'DROP' => STANDARD, 'DROP!' => STANDARD, 'REJECT' => STANDARD, 'REJECT!' => STANDARD, 'LOG' => STANDARD + LOGRULE, 'CONTINUE' => STANDARD, 'CONTINUE!' => STANDARD, 'QUEUE' => STANDARD, 'QUEUE!' => STANDARD, 'NFQUEUE' => STANDARD + NFQ, 'NFQUEUE!' => STANDARD + NFQ, 'dropBcast' => BUILTIN + ACTION, 'allowBcast' => BUILTIN + ACTION, 'dropNotSyn' => BUILTIN + ACTION, 'rejNotSyn' => BUILTIN + ACTION, 'dropInvalid' => BUILTIN + ACTION, 'allowInvalid' => BUILTIN + ACTION, ); for my $chain qw(OUTPUT PREROUTING) { new_builtin_chain 'raw', $chain, 'ACCEPT'; } for my $chain qw(INPUT OUTPUT FORWARD) { new_builtin_chain 'filter', $chain, 'DROP'; } for my $chain qw(PREROUTING POSTROUTING OUTPUT) { new_builtin_chain 'nat', $chain, 'ACCEPT'; } for my $chain qw(PREROUTING INPUT OUTPUT FORWARD POSTROUTING ) { new_builtin_chain 'mangle', $chain, 'ACCEPT'; } } } # # Add ESTABLISHED,RELATED rules and synparam jumps to the passed chain # sub finish_chain_section ($$) { my ($chainref, $state ) = @_; my $chain = $chainref->{name}; my $savecomment = $comment; $comment = ''; add_rule $chainref, "-m state --state $state -j ACCEPT" unless $config{FASTACCEPT}; if ($sections{NEW} ) { if ( $chainref->{is_policy} ) { if ( $chainref->{synparams} ) { my $synchainref = ensure_chain 'filter', syn_flood_chain $chainref; if ( $section eq 'DONE' ) { if ( $chainref->{policy} =~ /^(ACCEPT|CONTINUE|QUEUE|NFQUEUE)/ ) { add_rule $chainref, "-p tcp --syn -j $synchainref->{name}"; } } else { add_rule $chainref, "-p tcp --syn -j $synchainref->{name}"; } } } else { my $policychainref = $filter_table->{$chainref->{policychain}}; if ( $policychainref->{synparams} ) { my $synchainref = ensure_chain 'filter', syn_flood_chain $policychainref; add_rule $chainref, "-p tcp --syn -j $synchainref->{name}"; } } } $comment = $savecomment; } # # Do section-end processing # sub finish_section ( $ ) { my $sections = $_[0]; for my $section ( split /,/, $sections ) { $sections{$section} = 1; } for my $zone ( all_zones ) { for my $zone1 ( all_zones ) { my $chainref = $chain_table{'filter'}{"${zone}2${zone1}"}; if ( $chainref->{referenced} ) { finish_chain_section $chainref, $sections; } } } } # # Helper for set_mss # sub set_mss1( $$ ) { my ( $chain, $mss ) = @_; my $chainref = ensure_chain 'filter', $chain; if ( $chainref->{policy} ne 'NONE' ) { my $match = $capabilities{TCPMSS_MATCH} ? "-m tcpmss --mss $mss: " : ''; insert_rule $chainref, 1, "-p tcp --tcp-flags SYN,RST SYN ${match}-j TCPMSS --set-mss $mss" } } # # Set up rules to set MSS to and/or from zone "$zone" # sub set_mss( $$$ ) { my ( $zone, $mss, $direction) = @_; for my $z ( all_zones ) { if ( $direction eq '_in' ) { set_mss1 "${zone}2${z}" , $mss; } elsif ( $direction eq '_out' ) { set_mss1 "${z}2${zone}", $mss; } else { set_mss1 "${z}2${zone}", $mss; set_mss1 "${zone}2${z}", $mss; } } } # # Interate over non-firewall zones and interfaces with 'mss=' setting adding TCPMSS rules as appropriate. # sub setup_zone_mss() { for my $zone ( all_zones ) { my $zoneref = find_zone( $zone ); set_mss( $zone, $zoneref->{options}{in_out}{mss}, '' ) if $zoneref->{options}{in_out}{mss}; set_mss( $zone, $zoneref->{options}{in}{mss}, '_in' ) if $zoneref->{options}{in}{mss}; set_mss( $zone, $zoneref->{options}{out}{mss}, '_out' ) if $zoneref->{options}{out}{mss}; } } sub newexclusionchain() { my $seq = $exclseq++; "excl${seq}"; } sub clearrule() { $iprangematch = 0; } # # Handle parsing of PROTO, DEST PORT(S) , SOURCE PORTS(S). Returns the appropriate match string. # sub do_proto( $$$ ) { my ($proto, $ports, $sports ) = @_; # # Return the number of ports represented by the passed list # sub port_count( $ ) { ( $_[0] =~ tr/,:/,:/ ) + 1; } my $output = ''; $proto = '' if $proto eq '-'; $ports = '' if $ports eq '-'; $sports = '' if $sports eq '-'; if ( $proto ne '' ) { my $synonly = ( $proto =~ s/:syn$//i ); my $protonum = resolve_proto $proto; if ( defined $protonum ) { # # Protocol is numeric and <= 65535 or is defined in /etc/protocols or NSS equivalent # my $pname = proto_name( $proto = $protonum ); # # $proto now contains the protocol number and $pname contains the canonical name of the protocol # unless ( $synonly ) { $output = "-p $proto "; } else { fatal_error '":syn" is only allowed with tcp' unless $proto == TCP; $output = "-p $proto --syn "; } PROTO: { if ( $proto == TCP || $proto == UDP || $proto == SCTP || $proto == DCCP ) { my $multiport = 0; if ( $ports ne '' ) { if ( $ports =~ tr/,/,/ > 0 || $sports =~ tr/,/,/ > 0 ) { fatal_error "Port lists require Multiport support in your kernel/iptables" unless $capabilities{MULTIPORT}; fatal_error "Multiple ports not supported with SCTP" if $proto == SCTP; $ports = validate_port_list $pname , $ports; $output .= "-m multiport --dports $ports "; $multiport = 1; } else { $ports = validate_portpair $pname , $ports; $output .= "--dport $ports "; } } else { $multiport = ( ( $sports =~ tr/,/,/ ) > 0 ); } if ( $sports ne '' ) { if ( $multiport ) { fatal_error "Too many entries in SOURCE PORT(S) list" if port_count( $sports ) > 15; $sports = validate_port_list $pname , $sports; $output .= "-m multiport --sports $sports "; } else { $sports = validate_portpair $pname , $sports; $output .= "--sport $sports "; } } last PROTO; } if ( $proto == ICMP ) { fatal_error "ICMP not permitted in an IPv6 configuration" if $family == F_IPV6; if ( $ports ne '' ) { fatal_error 'Multiple ICMP types are not permitted' if $ports =~ /,/; $ports = validate_icmp $ports; $output .= "--icmp-type $ports "; } fatal_error 'SOURCE PORT(S) not permitted with ICMP' if $sports ne ''; last PROTO; } if ( $proto == IPv6_ICMP ) { fatal_error "IPv6_ICMP not permitted in an IPv4 configuration" if $family == F_IPV4; if ( $ports ne '' ) { fatal_error 'Multiple ICMP types are not permitted' if $ports =~ /,/; $ports = validate_icmp6 $ports; $output .= "--icmpv6-type $ports "; } fatal_error 'SOURCE PORT(S) not permitted with IPv6-ICMP' if $sports ne ''; last PROTO; } fatal_error "SOURCE/DEST PORT(S) not allowed with PROTO $pname" if $ports ne '' || $sports ne ''; } # PROTO } else { fatal_error '":syn" is only allowed with tcp' if $synonly; if ( $proto =~ /^(ipp2p(:(tcp|udp|all))?)$/i ) { my $p = $2 ? lc $3 : 'tcp'; require_capability( 'IPP2P_MATCH' , "PROTO = $proto" , 's' ); $proto = '-p ' . proto_name($p) . ' '; $ports = 'ipp2p' unless $ports; $output .= "${proto}-m ipp2p --$ports "; } else { fatal_error "Invalid/Unknown protocol ($proto)" } } } else { # # No protocol # fatal_error "SOURCE/DEST PORT(S) not allowed without PROTO" if $ports ne '' || $sports ne ''; } $output; } sub mac_match( $ ) { my $mac = $_[0]; $mac =~ s/^(!?)~//; my $invert = ( $1 ? '! ' : ''); $mac =~ tr/-/:/; fatal_error "Invalid MAC address ($mac)" unless $mac =~ /^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/; "--match mac --mac-source ${invert}$mac "; } # # Mark validatation functions # sub verify_mark( $ ) { my $mark = $_[0]; my $limit = $config{HIGH_ROUTE_MARKS} ? 0xFFFF : 0xFF; my $value = numeric_value( $mark ); fatal_error "Invalid Mark or Mask value ($mark)" unless defined( $value ) && $value <= $limit; fatal_error "Invalid High Mark or Mask value ($mark)" if ( $value > 0xFF && $value & 0xFF ); } sub verify_small_mark( $ ) { verify_mark ( (my $mark) = $_[0] ); fatal_error "Mark value ($mark) too large" if numeric_value( $mark ) > 0xFF; } sub validate_mark( $ ) { for ( split '/', $_[0] ) { verify_mark $_; } } # # Generate an appropriate -m [conn]mark match string for the contents of a MARK column # sub do_test ( $$ ) { my ($testval, $mask) = @_; my $originaltestval = $testval; return '' unless defined $testval and $testval ne '-'; $mask = '' unless defined $mask; my $invert = $testval =~ s/^!// ? '! ' : ''; my $match = $testval =~ s/:C$// ? "-m connmark ${invert}--mark" : "-m mark ${invert}--mark"; fatal_error "Invalid MARK value ($originaltestval)" if $testval eq '/'; validate_mark $testval; $testval = join( '/', $testval, in_hex($mask) ) unless ( $testval =~ '/' ); "$match $testval "; } my %norate = ( DROP => 1, REJECT => 1 ); # # Create a "-m limit" match for the passed LIMIT/BURST # sub do_ratelimit( $$ ) { my ( $rate, $action ) = @_; return '' unless $rate and $rate ne '-'; fatal_error "Rate Limiting not available with $action" if $norate{$action}; if ( $rate =~ /^(\d+(\/(sec|min|hour|day))?):(\d+)$/ ) { "-m limit --limit $1 --limit-burst $4 "; } elsif ( $rate =~ /^(\d+)(\/(sec|min|hour|day))?$/ ) { "-m limit --limit $rate "; } else { fatal_error "Invalid rate ($rate)"; } } # # Create a "-m connlimit" match for the passed CONNLIMIT # sub do_connlimit( $ ) { my ( $limit ) = @_; return '' unless $limit and $limit ne '-'; require_capability 'CONNLIMIT_MATCH', 'A non-empty CONNLIMIT', 's'; my $invert = $limit =~ s/^!// ? '' : '! '; # Note Carefully -- we actually do 'connlimit-at-or-below' if ( $limit =~ /^(\d+):(\d+)$/ ) { fatal_error "Invalid Mask ($2)" unless $2 > 0 || $2 < 31; "-m connlimit ${invert}--connlimit-above $1 --connlimit-mask $2 "; } elsif ( $limit =~ /^(\d+)$/ ) { "-m connlimit ${invert}--connlimit-above $limit "; } else { fatal_error "Invalid connlimit ($limit)"; } } sub do_time( $ ) { my ( $time ) = @_; return '' unless $time ne '-'; require_capability 'TIME_MATCH', 'A non-empty TIME', 's'; my $result = '-m time '; for my $element (split /&/, $time ) { fatal_error "Invalid time element list ($time)" unless defined $element && $element; if ( $element =~ /^(timestart|timestop)=(\d{1,2}:\d{1,2}(:\d{1,2})?)$/ ) { $result .= "--$1 $2 "; } elsif ( $element =~ /^weekdays=(.*)$/ ) { my $days = $1; for my $day ( split /,/, $days ) { fatal_error "Invalid weekday ($day)" unless $day =~ /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)$/ || ( $day =~ /^\d$/ && $day && $day <= 7);0 } $result .= "--weekday $days "; } elsif ( $element =~ /^monthdays=(.*)$/ ) { my $days = $1; for my $day ( split /,/, $days ) { fatal_error "Invalid day of the month ($day)" unless $day =~ /^\d{1,2}$/ && $day && $day <= 31; } } elsif ( $element =~ /^(datestart|datestop)=(\d{4}(-\d{2}(-\d{2}(T\d{1,2}(:\d{1,2}){0,2})?)?)?)$/ ) { $result .= "--$1 $2 "; } elsif ( $element =~ /^(utc|localtz)$/ ) { $result .= "--$1 "; } else { fatal_error "Invalid time element ($element)"; } } $result; } # # Create a "-m owner" match for the passed USER/GROUP # sub do_user( $ ) { my $user = $_[0]; my $rule = '-m owner '; return '' unless defined $user and $user ne '-'; if ( $user =~ /^(!)?(.*)\+(.*)$/ ) { $rule .= "! --cmd-owner $2 " if defined $2 && $2 ne ''; $user = "!$1"; } elsif ( $user =~ /^(.*)\+(.*)$/ ) { $rule .= "--cmd-owner $2 " if defined $2 && $2 ne ''; $user = $1; } if ( $user =~ /^(!)?(.*):(.*)$/ ) { my $invert = $1 ? '! ' : ''; my $group = defined $3 ? $3 : ''; if ( defined $2 && $2 ne '' ) { $user = $2; fatal_error "Unknown user ($user)" unless $user =~ /^\d+$/ || $globals{EXPORT} || defined getpwnam( $user ); $rule .= "${invert}--uid-owner $user "; } if ( $group ne '' ) { fatal_error "Unknown group ($group)" unless $group =~ /\d+$/ || $globals{EXPORT} || defined getgrnam( $group ); $rule .= "${invert}--gid-owner $group "; } } elsif ( $user =~ /^(!)?(.*)$/ ) { my $invert = $1 ? '! ' : ''; $user = $2; fatal_error "Invalid USER/GROUP (!)" if $user eq ''; fatal_error "Unknown user ($user)" unless $user =~ /^\d+$/ || $globals{EXPORT} || defined getpwnam( $user ); $rule .= "${invert}--uid-owner $user "; } else { fatal_error "Unknown user ($user)" unless $user =~ /^\d+$/ || $globals{EXPORT} || defined getpwnam( $user ); $rule .= "--uid-owner $user "; } $rule; } # # Create a "-m tos" match for the passed TOS # sub do_tos( $ ) { my $tos = $_[0]; $tos ne '-' ? "-m tos --tos $tos " : ''; } my %dir = ( O => 'original' , R => 'reply' , B => 'both' ); my %mode = ( P => 'packets' , B => 'bytes' , A => 'avgpkt' ); # # Create a "-m connbytes" match for the passed argument # sub do_connbytes( $ ) { my $connbytes = $_[0]; return '' if $connbytes eq '-'; # 1 2 3 5 6 fatal_error "Invalid CONNBYTES ($connbytes)" unless $connbytes =~ /^(!)? (\d+): (\d+)? ((:[ORB]) (:[PBA])?)?$/x; my $invert = $1 || ''; $invert = '! ' if $invert; my $min = $2; $min = 0 unless defined $min; my $max = $3; $max = '' unless defined $max; fatal_error "Invalid byte range ($min:$max)" if $max ne '' and $min > $max; my $dir = $5 || 'B'; my $mode = $6 || 'B'; $dir =~ s/://; $mode =~ s/://; "${invert}-m connbytes --connbytes $min:$max --connbytes-dir $dir{$dir} --connbytes-mode $mode{$mode} "; } # # Create a "-m helper" match for the passed argument # sub do_helper( $ ) { my $helper = shift; return '' if $helper eq '-'; qq(-m helper --helper "$helper" ); } # # Create a "-m length" match for the passed TOS # sub do_length( $ ) { my $length = $_[0]; require_capability( 'LENGTH_MATCH' , 'A Non-empty LENGTH' , 's' ); $length ne '-' ? "-m length --length $length " : ''; } # # Match Source Interface # sub match_source_dev( $ ) { my $interface = shift; return '' if $interface eq '+'; my $interfaceref = known_interface( $interface ); if ( $interfaceref && $interfaceref->{options}{port} ) { "-i $interfaceref->{bridge} -m physdev --physdev-in $interface "; } else { "-i $interface "; } } # # Match Dest device # sub match_dest_dev( $ ) { my $interface = shift; return '' if $interface eq '+'; my $interfaceref = known_interface( $interface ); if ( $interfaceref && $interfaceref->{options}{port} ) { if ( $capabilities{PHYSDEV_BRIDGE} ) { "-o $interfaceref->{bridge} -m physdev --physdev-is-bridged --physdev-out $interface "; } else { "-o $interfaceref->{bridge} -m physdev --physdev-out $interface "; } } else { "-o $interface "; } } # # Avoid generating a second '-m iprange' in a single rule. # sub iprange_match() { my $match = ''; require_capability( 'IPRANGE_MATCH' , 'Address Ranges' , '' ); unless ( $iprangematch ) { $match = '-m iprange '; $iprangematch = 1 unless $capabilities{KLUDGEFREE}; } $match; } # # Get set flags (ipsets). # sub get_set_flags( $$ ) { my ( $setname, $option ) = @_; my $options = $option; $setname =~ s/^!//; # Caller has already taken care of leading ! if ( $setname =~ /^(.*)\[([1-6])\]$/ ) { $setname = $1; my $count = $2; $options .= ",$option" while --$count > 0; } elsif ( $setname =~ /^(.*)\[(.*)\]$/ ) { $setname = $1; $options = $2; } $setname =~ s/^\+//; fatal_error "Invalid ipset name ($setname)" unless $setname =~ /^[a-zA-Z]\w*/; "--set $setname $options " } # # Match a Source. Handles IP addresses and ranges and MAC addresses # sub match_source_net( $;$ ) { my ( $net, $restriction) = @_; $restriction |= NO_RESTRICT; if ( $net =~ /^(!?)(\d+\.\d+\.\d+\.\d+)-(\d+\.\d+\.\d+\.\d+)$/ ) { my ($addr1, $addr2) = ( $2, $3 ); $net =~ s/!// if my $invert = $1 ? '! ' : ''; validate_range $addr1, $addr2; iprange_match . "${invert}--src-range $net "; } elsif ( $net =~ /^!?~/ ) { fatal_error "MAC address cannot be used in this context" if $restriction >= OUTPUT_RESTRICT; mac_match $net; } elsif ( $net =~ /^(!?)\+/ ) { require_capability( 'IPSET_MATCH' , 'ipset names in Shorewall configuration files' , '' ); join( '', '-m set ', $1 ? '! ' : '', get_set_flags( $net, 'src' ) ); } elsif ( $net =~ s/^!// ) { validate_net $net, 1; "-s ! $net "; } else { validate_net $net, 1; $net eq ALLIP ? '' : "-s $net "; } } # # Match a Source. Currently only handles IP addresses and ranges # sub match_dest_net( $ ) { my $net = $_[0]; if ( $net =~ /^(!?)(\d+\.\d+\.\d+\.\d+)-(\d+\.\d+\.\d+\.\d+)$/ ) { my ($addr1, $addr2) = ( $2, $3 ); $net =~ s/!// if my $invert = $1 ? '! ' : ''; validate_range $addr1, $addr2; iprange_match . "${invert}--dst-range $net "; } elsif ( $net =~ /^(!?)\+/ ) { require_capability( 'IPSET_MATCH' , 'ipset names in Shorewall configuration files' , ''); join( '', '-m set ', $1 ? '! ' : '', get_set_flags( $net, 'dst' ) ); } elsif ( $net =~ /^!/ ) { $net =~ s/!//; validate_net $net, 1; "-d ! $net "; } else { validate_net $net, 1; $net eq ALLIP ? '' : "-d $net "; } } # # Match original destination # sub match_orig_dest ( $ ) { my $net = $_[0]; return '' if $net eq ALLIP; return '' unless $capabilities{CONNTRACK_MATCH}; if ( $net =~ s/^!// ) { validate_net $net, 1; $capabilities{OLD_CONNTRACK_MATCH} ? "-m conntrack --ctorigdst ! $net " : "-m conntrack ! --ctorigdst $net "; } else { validate_net $net, 1; $net eq ALLIP ? '' : "-m conntrack --ctorigdst $net "; } } # # Match Source IPSEC # sub match_ipsec_in( $$ ) { my ( $zone , $hostref ) = @_; my $match = '-m policy --dir in --pol '; my $zoneref = find_zone( $zone ); my $optionsref = $zoneref->{options}; if ( $zoneref->{type} eq 'ipsec' ) { $match .= "ipsec $optionsref->{in_out}{ipsec}$optionsref->{in}{ipsec}"; } elsif ( $capabilities{POLICY_MATCH} ) { $match .= "$hostref->{ipsec} $optionsref->{in_out}{ipsec}$optionsref->{in}{ipsec}"; } else { ''; } } # # Match Dest IPSEC # sub match_ipsec_out( $$ ) { my ( $zone , $hostref ) = @_; my $match = '-m policy --dir out --pol '; my $zoneref = find_zone( $zone ); my $optionsref = $zoneref->{options}; if ( $zoneref->{type} eq 'ipsec' ) { $match .= "ipsec $optionsref->{in_out}{ipsec}$optionsref->{out}{ipsec}"; } elsif ( $capabilities{POLICY_MATCH} ) { $match .= "$hostref->{ipsec} $optionsref->{in_out}{ipsec}$optionsref->{out}{ipsec}" } else { ''; } } # # Generate a log message # sub log_rule_limit( $$$$$$$$ ) { my ($level, $chainref, $chain, $disposition, $limit, $tag, $command, $predicates ) = @_; my $prefix = ''; $level = validate_level $level; # Do this here again because this function can be called directly from user exits. return 1 if $level eq ''; $predicates .= ' ' if $predicates && substr( $predicates, -1, 1 ) ne ' '; unless ( $predicates =~ /-m limit / ) { $limit = $globals{LOGLIMIT} unless $limit && $limit ne '-'; $predicates .= $limit if $limit; } if ( $config{LOGFORMAT} =~ /^\s*$/ ) { if ( $level =~ '^ULOG' ) { $prefix = "-j $level "; } elsif ( $level =~ /^NFLOG/ ) { $prefix = "-j $level "; } else { $prefix = "-j LOG $globals{LOGPARMS}--log-level $level "; } } else { if ( $tag ) { if ( $config{LOGTAGONLY} ) { $chain = $tag; $tag = ''; } else { $tag .= ' '; } } else { $tag = '' unless defined $tag; } $disposition =~ s/\s+.*//; if ( $globals{LOGRULENUMBERS} ) { $prefix = (sprintf $config{LOGFORMAT} , $chain , $chainref->{log}++, $disposition ) . $tag; } else { $prefix = (sprintf $config{LOGFORMAT} , $chain , $disposition) . $tag; } if ( length $prefix > 29 ) { $prefix = substr( $prefix, 0, 28 ) . ' '; warning_message "Log Prefix shortened to \"$prefix\""; } if ( $level =~ '^ULOG' ) { $prefix = "-j $level --ulog-prefix \"$prefix\" "; } elsif ( $level =~ /^NFLOG/ ) { $prefix = "-j $level --nflog-prefix \"$prefix\" "; } else { $prefix = "-j LOG $globals{LOGPARMS}--log-level $level --log-prefix \"$prefix\" "; } } if ( $command eq 'add' ) { add_rule ( $chainref, $predicates . $prefix , 1 ); } else { insert_rule ( $chainref , 1 , $predicates . $prefix ); } } sub log_rule( $$$$ ) { my ( $level, $chainref, $disposition, $predicates ) = @_; log_rule_limit $level, $chainref, $chainref->{name} , $disposition, $globals{LOGLIMIT}, '', 'add', $predicates; } # # Split a comma-separated source or destination host list but keep [...] together. # sub mysplit( $ ) { my @input = split_list $_[0], 'host'; return @input unless $_[0] =~ /\[/; my @result; while ( @input ) { my $element = shift @input; if ( $element =~ /\[/ ) { while ( substr( $element, -1, 1 ) ne ']' ) { last unless @input; $element .= ( ',' . shift @input ); } fatal_error "Invalid Host List ($_[0])" unless substr( $element, -1, 1 ) eq ']'; } push @result, $element; } @result; } # # Returns the name of the shell variable holding the first address of the passed interface # sub interface_address( $ ) { my $variable = chain_base( $_[0] ) . '_address'; uc $variable; } # # Record that the ruleset requires the first IP address on the passed interface # sub get_interface_address ( $ ) { my ( $interface ) = $_[0]; my $variable = interface_address( $interface ); my $function = interface_is_optional( $interface ) ? 'find_first_interface_address_if_any' : 'find_first_interface_address'; $interfaceaddr{$interface} = "$variable=\$($function $interface)\n"; "\$$variable"; } # # Returns the name of the shell variable holding the broadcast addresses of the passed interface # sub interface_bcasts( $ ) { my $variable = chain_base( $_[0] ) . '_bcasts'; uc $variable; } # # Record that the ruleset requires the broadcast addresses on the passed interface # sub get_interface_bcasts ( $ ) { my ( $interface ) = $_[0]; my $variable = interface_bcasts( $interface ); $interfacebcasts{$interface} = qq($variable="\$(get_interface_bcasts $interface) 255.255.255.255"); "\$$variable"; } # # Returns the name of the shell variable holding the anycast addresses of the passed interface # sub interface_acasts( $ ) { my $variable = chain_base( $_[0] ) . '_acasts'; uc $variable; } # # Record that the ruleset requires the anycast addresses on the passed interface # sub get_interface_acasts ( $ ) { my ( $interface ) = $_[0]; my $variable = interface_acasts( $interface ); $interfaceacasts{$interface} = qq($variable="\$(get_interface_acasts $interface) ff00::/10"); "\$$variable"; } # # Returns the name of the shell variable holding the gateway through the passed interface # sub interface_gateway( $ ) { my $variable = chain_base( $_[0] ) . '_gateway'; uc $variable; } # # Record that the ruleset requires the gateway address on the passed interface # sub get_interface_gateway ( $ ) { my ( $interface ) = $_[0]; my $variable = interface_gateway( $interface ); if ( interface_is_optional $interface ) { $interfacegateways{$interface} = qq([ -n "\$$variable" ] || $variable=\$(detect_gateway $interface)\n); } else { $interfacegateways{$interface} = qq([ -n "\$$variable" ] || $variable=\$(detect_gateway $interface) [ -n "\$$variable" ] || fatal_error "Unable to detect the gateway through interface $interface" ); } "\$$variable"; } # # Returns the name of the shell variable holding the addresses of the passed interface # sub interface_addresses( $ ) { my $variable = chain_base( $_[0] ) . '_addresses'; uc $variable; } # # Record that the ruleset requires the IP addresses on the passed interface # sub get_interface_addresses ( $ ) { my ( $interface ) = $_[0]; my $variable = interface_addresses( $interface ); if ( interface_is_optional $interface ) { $interfaceaddrs{$interface} = qq($variable=\$(find_interface_addresses $interface)\n); } else { $interfaceaddrs{$interface} = qq($variable=\$(find_interface_addresses $interface) [ -n "\$$variable" ] || fatal_error "Unable to determine the IP address(es) of $interface" ); } "\$$variable"; } # # Returns the name of the shell variable holding the networks routed out of the passed interface # sub interface_nets( $ ) { my $variable = chain_base( $_[0] ) . '_networks'; uc $variable; } # # Record that the ruleset requires the networks routed out of the passed interface # sub get_interface_nets ( $ ) { my ( $interface ) = $_[0]; my $variable = interface_nets( $interface ); if ( interface_is_optional $interface ) { $interfacenets{$interface} = qq($variable=\$(get_routed_networks $interface)\n); } else { $interfacenets{$interface} = qq($variable=\$(get_routed_networks $interface) [ -n "\$$variable" ] || fatal_error "Unable to determine the routes through interface \\"$interface\\"" ); } "\$$variable"; } # # Returns the name of the shell variable holding the MAC address of the gateway for the passed provider out of the passed interface # sub interface_mac( $$ ) { my $variable = join( '_' , chain_base( $_[0] ) , chain_base( $_[1] ) , 'mac' ); uc $variable; } # # Record the fact that the ruleset requires MAC address of the passed gateway IP routed out of the passed interface for the passed provider number # sub get_interface_mac( $$$ ) { my ( $ipaddr, $interface , $table ) = @_; my $variable = interface_mac( $interface , $table ); if ( interface_is_optional $interface ) { $interfacemacs{$table} = qq($variable=\$(find_mac $ipaddr $interface)\n); } else { $interfacemacs{$table} = qq($variable=\$(find_mac $ipaddr $interface) [ -n "\$$variable" ] || fatal_error "Unable to determine the MAC address of $ipaddr through interface \\"$interface\\"" ); } "\$$variable"; } # # This function provides a uniform way to generate rules (something the original Shorewall sorely needed). # # Returns the destination interface specified in the rule, if any. # sub expand_rule( $$$$$$$$$$$ ) { my ($chainref , # Chain $restriction, # Determines what to do with interface names in the SOURCE or DEST $rule, # Caller's matches that don't depend on the SOURCE, DEST and ORIGINAL DEST $source, # SOURCE $dest, # DEST $origdest, # ORIGINAL DEST $oport, # original destination port $target, # Target ('-j' part of the rule) $loglevel , # Log level (and tag) $disposition, # Primative part of the target (RETURN, ACCEPT, ...) $exceptionrule # Caller's matches used in exclusion case ) = @_; my ($iiface, $diface, $inets, $dnets, $iexcl, $dexcl, $onets , $oexcl ); my $chain = $chainref->{name}; our @ends = (); # # In the generated rules, we sometimes need run-time loops or conditional blocks. This function is used # to define such a loop or block. # # $chainref = Reference to the chain # $command = The shell command that begins the loop or conditional # $end = The shell keyword ('done' or 'fi') that ends the loop or conditional # # All open loops and conditionals are closed just before expand_rule() exits # sub push_command( $$$ ) { my ( $chainref, $command, $end ) = @_; add_command $chainref, $command; incr_cmd_level $chainref; push @ends, $end; } # # Handle Log Level # my $logtag; if ( $loglevel ne '' ) { ( $loglevel, $logtag, my $remainder ) = split( /:/, $loglevel, 3 ); fatal_error "Invalid log tag" if defined $remainder; if ( $loglevel =~ /^none!?$/i ) { return if $disposition eq 'LOG'; $loglevel = $logtag = ''; } else { $loglevel = validate_level( $loglevel ); $logtag = '' unless defined $logtag; } } elsif ( $disposition eq 'LOG' ) { fatal_error "LOG requires a level"; } # # Mark Target as referenced, if it's a chain # if ( $disposition ) { my $targetref = $chain_table{$chainref->{table}}{$disposition}; $targetref->{referenced} = 1 if $targetref; } # # Isolate Source Interface, if any # if ( $source ) { if ( $source eq '-' ) { $source = ''; } elsif ( $family == F_IPV4 ) { if ( $source =~ /^(.+?):(.+)$/ ) { $iiface = $1; $inets = $2; } elsif ( $source =~ /\+|~|\..*\./ ) { $inets = $source; } else { $iiface = $source; } } elsif ( $source =~ /^(.+?):\[(.+)\]\s*$/ ) { $iiface = $1; $inets = $2; } elsif ( $source =~ /:/ ) { if ( $source =~ /^\[(.+)\]$/ ) { $inets = $1; } else { $inets = $source; } } elsif ( $source =~ /\+|~|\..*\./ ) { $inets = $source; } else { $iiface = $source; } } else { $source = ''; } # # Verify Interface, if any # if ( $iiface ) { fatal_error "Unknown Interface ($iiface)" unless known_interface $iiface; if ( $restriction & POSTROUTE_RESTRICT ) { # # An interface in the SOURCE column of a masq file # fatal_error "Bridge ports may not appear in the SOURCE column of this file" if port_to_bridge( $iiface ); my $networks = get_interface_nets ( $iiface ); push_command $chainref, join( '', 'for source in ', $networks, '; do' ), 'done'; $rule .= '-s $source '; } else { fatal_error "Source Interface ($iiface) not allowed when the source zone is the firewall zone" if $restriction & OUTPUT_RESTRICT; $rule .= match_source_dev( $iiface ); } } # # Isolate Destination Interface, if any # if ( $dest ) { if ( $dest eq '-' ) { $dest = ''; } elsif ( ( $restriction & PREROUTE_RESTRICT ) && $dest =~ /^detect:(.*)$/ ) { # # DETECT_DNAT_IPADDRS=Yes and we're generating the nat rule # my @interfaces = split /\s+/, $1; if ( @interfaces > 1 ) { my $list = ""; my $optional; for my $interface ( @interfaces ) { $optional++ if interface_is_optional $interface; $list = join( ' ', $list , get_interface_address( $interface ) ); } push_command( $chainref , "for address in $list; do" , 'done' ); push_command( $chainref , 'if [ $address != 0.0.0.0 ]; then' , 'fi' ) if $optional; $rule .= '-d $address '; } else { my $interface = $interfaces[0]; my $variable = get_interface_address( $interface ); push_command( $chainref , "if [ $variable != 0.0.0.0 ]; then" , 'fi') if interface_is_optional( $interface ); $rule .= "-d $variable "; } $dest = ''; } elsif ( $family == F_IPV4 ) { if ( $dest =~ /^(.+?):(.+)$/ ) { $diface = $1; $dnets = $2; } elsif ( $dest =~ /\+|~|\..*\./ ) { $dnets = $dest; } else { $diface = $dest; } } elsif ( $dest =~ /^(.+?):\[(.+)\]\s*$/ ) { $diface = $1; $dnets = $2; } elsif ( $dest =~ /:/ ) { if ( $dest =~ /^\[(.+)\]$/ ) { $dnets = $1; } else { $dnets = $dest; } } elsif ( $dest =~ /\+|~|\..*\./ ) { $dnets = $dest; } else { $diface = $dest; } } else { $dest = ''; } # # Verify Destination Interface, if any # if ( $diface ) { fatal_error "Unknown Interface ($diface)" unless known_interface $diface; if ( $restriction & PREROUTE_RESTRICT ) { # # ADDRESS 'detect' in the masq file. # fatal_error "Bridge port ($diface) not allowed" if port_to_bridge( $diface ); push_command( $chainref , 'for dest in ' . get_interface_addresses( $diface) . '; do', 'done' ); $rule .= '-d $dest '; } else { fatal_error "Bridge Port ($diface) not allowed in OUTPUT or POSTROUTING rules" if ( $restriction & ( POSTROUTE_RESTRICT + OUTPUT_RESTRICT ) ) && port_to_bridge( $diface ); fatal_error "Destination Interface ($diface) not allowed when the destination zone is the firewall zone" if $restriction & INPUT_RESTRICT; if ( $iiface ) { my $bridge = port_to_bridge( $diface ); fatal_error "Source interface ($iiface) is not a port on the same bridge as the destination interface ( $diface )" if $bridge && $bridge ne source_port_to_bridge( $iiface ); } $rule .= match_dest_dev( $diface ); } } else { $diface = ''; } $oport = '' if defined $oport && $oport eq '-'; if ( $origdest ) { if ( $origdest eq '-' || ! $capabilities{CONNTRACK_MATCH} ) { $origdest = ''; $rule .= "-m conntrack --ctorigdstport $oport " if $capabilities{NEW_CONNTRACK_MATCH} && $oport; } elsif ( $origdest =~ /^detect:(.*)$/ ) { # # Either the filter part of a DNAT rule or 'detect' was given in the ORIG DEST column # my @interfaces = split /\s+/, $1; if ( @interfaces > 1 ) { my $list = ""; my $optional; for my $interface ( @interfaces ) { $optional++ if interface_is_optional $interface; $list = join( ' ', $list , get_interface_address( $interface ) ); } push_command( $chainref , "for address in $list; do" , 'done' ); push_command( $chainref , 'if [ $address != 0.0.0.0 ]; then' , 'fi' ) if $optional; $rule .= '-m conntrack --ctorigdst $address '; $rule .= "--ctorigdstport $oport " if $capabilities{NEW_CONNTRACK_MATCH} && $oport; } else { my $interface = $interfaces[0]; my $variable = get_interface_address( $interface ); push_command( $chainref , "if [ $variable != 0.0.0.0 ]; then" , 'fi' ) if interface_is_optional( $interface ); $rule .= "-m conntrack --ctorigdst $variable "; $rule .= "--ctorigdstport $oport " if $capabilities{NEW_CONNTRACK_MATCH} && $oport; } $origdest = ''; } else { fatal_error "Invalid ORIGINAL DEST" if $origdest =~ /^([^!]+)?,!([^!]+)$/ || $origdest =~ /.*!.*!/; if ( $origdest =~ /^([^!]+)?!([^!]+)$/ ) { # # Exclusion # $onets = $1; $oexcl = $2; } else { $oexcl = ''; $onets = $origdest; } unless ( $onets ) { my @oexcl = mysplit $oexcl; if ( @oexcl == 1 ) { $rule .= match_orig_dest( "!$oexcl" ); $oexcl = ''; } } $rule .= "-m conntrack --ctorigdstport $oport " if $capabilities{NEW_CONNTRACK_MATCH} && $oport; } } else { $oexcl = ''; $rule .= "-m conntrack --ctorigdstport $oport " if $capabilities{NEW_CONNTRACK_MATCH} && $oport; } # # Determine if there is Source Exclusion # if ( $inets ) { fatal_error "Invalid SOURCE" if $inets =~ /^([^!]+)?,!([^!]+)$/ || $inets =~ /.*!.*!/; if ( $inets =~ /^([^!]+)?!([^!]+)$/ ) { $inets = $1; $iexcl = $2; } else { $iexcl = ''; } unless ( $inets || ( $iiface && $restriction & POSTROUTE_RESTRICT ) ) { my @iexcl = mysplit $iexcl; if ( @iexcl == 1 ) { $rule .= match_source_net "!$iexcl" , $restriction; $iexcl = ''; } } } else { $iexcl = ''; } # # Determine if there is Destination Exclusion # if ( $dnets ) { fatal_error "Invalid DEST" if $dnets =~ /^([^!]+)?,!([^!]+)$/ || $dnets =~ /.*!.*!/; if ( $dnets =~ /^([^!]+)?!([^!]+)$/ ) { $dnets = $1; $dexcl = $2; } else { $dexcl = ''; } unless ( $dnets ) { my @dexcl = mysplit $dexcl; if ( @dexcl == 1 ) { $rule .= match_dest_net "!$dexcl"; $dexcl = ''; } } } else { $dexcl = ''; } $inets = ALLIP unless $inets; $dnets = ALLIP unless $dnets; $onets = ALLIP unless $onets; fatal_error "Input interface may not be specified with a source IP address in the POSTROUTING chain" if $restriction == POSTROUTE_RESTRICT && $iiface && $inets ne ALLIP; fatal_error "Output interface may not be specified with a destination IP address in the PREROUTING chain" if $restriction == PREROUTE_RESTRICT && $diface && $dnets ne ALLIP; if ( $iexcl || $dexcl || $oexcl ) { # # We have non-trivial exclusion -- need to create an exclusion chain # fatal_error "Exclusion is not possible in ACCEPT+/CONTINUE/NONAT rules" if $disposition eq 'RETURN'; my $echain = newexclusionchain; # # Use the current rule and sent all possible matches to the exclusion chain # for my $onet ( mysplit $onets ) { $onet = match_orig_dest $onet; for my $inet ( mysplit $inets ) { for my $dnet ( mysplit $dnets ) { # # We evaluate the source net match in the inner loop to accomodate systems without $capabilities{KLUDGEFREE} # add_rule( $chainref, join( '', $rule, match_source_net( $inet, $restriction ), match_dest_net( $dnet ), $onet, "-j $echain" ), 1 ); } } } # # Create the Exclusion Chain # my $echainref = new_chain $chainref->{table}, $echain; # # Generate RETURNs for each exclusion # add_rule $echainref, ( match_source_net $_ , $restriction ) . '-j RETURN' for ( mysplit $iexcl ); add_rule $echainref, ( match_dest_net $_ ) . '-j RETURN' for ( mysplit $dexcl ); add_rule $echainref, ( match_orig_dest $_ ) . '-j RETURN' for ( mysplit $oexcl ); # # Log rule # log_rule_limit $loglevel , $echainref , $chain, $disposition , '', $logtag , 'add' , '' if $loglevel; # # Generate Final Rule # add_rule( $echainref, $exceptionrule . $target, 1 ) unless $disposition eq 'LOG'; } else { # # No exclusions # for my $onet ( mysplit $onets ) { $onet = match_orig_dest $onet; for my $inet ( mysplit $inets ) { # # We defer evaluating the source net match to accomodate system without $capabilities{KLUDGEFREE} # for my $dnet ( mysplit $dnets ) { if ( $loglevel ne '' ) { log_rule_limit $loglevel , $chainref , $chain, $disposition , '' , $logtag , 'add' , join( '', $rule, match_source_net( $inet , $restriction ) , match_dest_net( $dnet ), $onet ); } unless ( $disposition eq 'LOG' ) { add_rule( $chainref, join( '', $rule, match_source_net ($inet , $restriction ), match_dest_net( $dnet ), $onet, $target ) , 1 ); } } } } } while ( @ends ) { decr_cmd_level $chainref; add_command $chainref, pop @ends; } $diface; } # # If the destination chain exists, then at the end of the source chain add a jump to the destination. # sub addnatjump( $$$ ) { my ( $source , $dest, $predicates ) = @_; my $destref = $nat_table->{$dest} || {}; if ( $destref->{referenced} ) { add_rule $nat_table->{$source} , $predicates . "-j $dest"; } else { clearrule; } } sub emit_comment() { emit ( '#', '# Establish the values of shell variables used in the following function calls', '#' ); our $emitted_comment = 1; } sub emit_test() { emit ( 'if [ "$COMMAND" != restore ]; then' , '' ); push_indent; our $emitted_test = 1; } # # Generate setting of global variables # sub set_global_variables() { our ( $emitted_comment, $emitted_test ) = (0, 0); for ( values %interfaceaddr ) { emit_comment unless $emitted_comment; emit $_; } for ( values %interfacegateways ) { emit_comment unless $emitted_comment; emit $_; } for ( values %interfacemacs ) { emit_comment unless $emitted_comment; emit $_; } for ( values %interfaceaddrs ) { emit_comment unless $emitted_comment; emit_test unless $emitted_test; emit $_; } for ( values %interfacenets ) { emit_comment unless $emitted_comment; emit_test unless $emitted_test; emit $_; } unless ( $capabilities{ADDRTYPE} ) { emit_comment unless $emitted_comment; emit_test unless $emitted_test; if ( $family == F_IPV4 ) { emit 'ALL_BCASTS="$(get_all_bcasts) 255.255.255.255"'; for ( values %interfacebcasts ) { emit $_; } } else { emit 'ALL_ACASTS="$(get_all_acasts)"'; for ( values %interfaceacasts ) { emit $_; } } } pop_indent, emit "fi\n" if $emitted_test; } # # What follows is the code that generates the input to iptables-restore # # We always write the iptables-restore input into a file then pass the # file to iptables-restore. That way, if things go wrong, the user (and Shorewall support) # has (have) something to look at to determine the error # # We may have to generate part of the input at run-time. The rules array in each chain # table entry may contain rules (begin with '-A') or shell source. We alternate between # writing the rules ('-A') into the temporary file to be bassed to iptables-restore # (CAT_MODE) and and writing shell source into the generated script (CMD_MODE). # # The following two functions are responsible for the mode transitions. # sub enter_cat_mode() { emit ''; emit 'cat >&3 << __EOF__'; $mode = CAT_MODE; } sub enter_cmd_mode() { emit_unindented "__EOF__\n" if $mode == CAT_MODE; $mode = CMD_MODE; } # # Emits the passed rule (input to iptables-restore) or command # sub emitr( $$ ) { my ( $name, $rule ) = @_; if ( $rule && substr( $rule, 0, 2 ) eq '-A' ) { # # A rule # enter_cat_mode unless $mode == CAT_MODE; emit_unindented join( ' ', '-A', $name, substr( $rule, 3 ) ); } else { # # A command # enter_cmd_mode unless $mode == CMD_MODE; emit $rule; } } # # Generate the netfilter input # sub create_netfilter_load() { my @table_list; if ( $family == F_IPV4 ) { push @table_list, 'raw' if $capabilities{RAW_TABLE}; push @table_list, 'nat' if $capabilities{NAT_ENABLED}; push @table_list, 'mangle' if $capabilities{MANGLE_ENABLED} && $config{MANGLE_ENABLED}; push @table_list, 'filter'; } else { @table_list = qw( raw mangle filter ); } $mode = NULL_MODE; emit ( 'setup_netfilter()', '{' ); push_indent; my $utility = $family == F_IPV4 ? 'iptables-restore' : 'ip6tables-restore'; my $UTILITY = $family == F_IPV4 ? 'IPTABLES_RESTORE' : 'IP6TABLES_RESTORE'; save_progress_message "Preparing $utility input..."; emit ''; emit "exec 3>\${VARDIR}/.${utility}-input"; enter_cat_mode; for my $table ( @table_list ) { emit_unindented "*$table"; my @chains; # # iptables-restore seems to be quite picky about the order of the builtin chains # for my $chain ( @builtins ) { my $chainref = $chain_table{$table}{$chain}; if ( $chainref ) { fatal_error "Internal error in create_netfilter_load()" if $chainref->{cmdlevel}; emit_unindented ":$chain $chainref->{policy} [0:0]"; push @chains, $chainref; } } # # First create the chains in the current table # for my $chain ( grep $chain_table{$table}{$_}->{referenced} , ( sort keys %{$chain_table{$table}} ) ) { my $chainref = $chain_table{$table}{$chain}; unless ( $chainref->{builtin} ) { fatal_error "Internal error in create_netfilter_load()" if $chainref->{cmdlevel}; emit_unindented ":$chainref->{name} - [0:0]"; push @chains, $chainref; } } # # Then emit the rules # for my $chainref ( @chains ) { emitr $chainref->{name}, $_ for ( @{$chainref->{rules}} ); } # # Commit the changes to the table # enter_cat_mode unless $mode == CAT_MODE; emit_unindented 'COMMIT'; } enter_cmd_mode; # # Now generate the actual ip[6]tables-restore command # emit( 'exec 3>&-', '', '[ -n "$DEBUG" ] && command=debug_restore_input || command=$' . $UTILITY, '', 'progress_message2 "Running $command..."', '', "cat \${VARDIR}/.${utility}-input | \$command # Use this nonsensical form to appease SELinux", 'if [ $? != 0 ]; then', qq( fatal_error "iptables-restore Failed. Input is in \${VARDIR}/.${utility}-input"), "fi\n" ); pop_indent; emit "}\n"; } # # Generate the netfilter input for refreshing a list of chains # sub create_chainlist_reload($) { my $chains = $_[0]; my @chains = split_list $chains, 'chain'; unless ( @chains ) { @chains = qw( blacklst ) if $filter_table->{blacklst}; push @chains, 'mangle:' if $capabilities{MANGLE_ENABLED} && $config{MANGLE_ENABLED}; $chains = join( ',', @chains ) if @chains; } $mode = NULL_MODE; emit( 'chainlist_reload()', '{' ); push_indent; if ( @chains ) { if ( @chains == 1 ) { progress_message2 "Compiling iptables-restore input for chain @chains..."; save_progress_message "Preparing iptables-restore input for chain @chains..."; } else { progress_message2 "Compiling iptables-restore input for chains $chains..."; save_progress_message "Preparing iptables-restore input for chains $chains..."; } emit ''; my $table = 'filter'; my %chains; for my $chain ( @chains ) { ( $table , $chain ) = split ':', $chain if $chain =~ /:/; fatal_error "Invalid table ( $table )" unless $table =~ /^(nat|mangle|filter)$/; $chains{$table} = [] unless $chains{$table}; if ( $chain ) { fatal_error "No $table chain found with name $chain" unless $chain_table{$table}{$chain}; fatal_error "Built-in chains may not be refreshed" if $chain_table{table}{$chain}{builtin}; push @{$chains{$table}}, $chain; } else { while ( my ( $chain, $chainref ) = each %{$chain_table{$table}} ) { push @{$chains{$table}}, $chain if $chainref->{referenced} && ! $chainref->{builtin}; } } } emit 'exec 3>${VARDIR}/.iptables-restore-input'; enter_cat_mode; for $table qw(nat mangle filter) { next unless $chains{$table}; emit_unindented "*$table"; my $tableref=$chain_table{$table}; @chains = sort @{$chains{$table}}; for my $chain ( @chains ) { my $chainref = $tableref->{$chain}; emit_unindented ":$chainref->{name} - [0:0]"; } for my $chain ( @chains ) { my $chainref = $tableref->{$chain}; my @rules = @{$chainref->{rules}}; @rules = () unless @rules; # # Emit the chain rules # emitr $chain, $_ for ( @rules ); } # # Commit the changes to the table # enter_cat_mode unless $mode == CAT_MODE; emit_unindented 'COMMIT'; } enter_cmd_mode; # # Now generate the actual iptables-restore command # emit( 'exec 3>&-', '', 'progress_message2 "Running iptables-restore..."', '', 'cat ${VARDIR}/.iptables-restore-input | $IPTABLES_RESTORE -n # Use this nonsensical form to appease SELinux', 'if [ $? != 0 ]; then', ' fatal_error "iptables-restore Failed. Input is in ${VARDIR}/.iptables-restore-input"', "fi\n" ); } else { emit('true'); } pop_indent; emit "}\n"; } 1;