diff --git a/Shorewall-common/lib.base b/Shorewall-common/lib.base index 82e2cc9c7..9969635c6 100644 --- a/Shorewall-common/lib.base +++ b/Shorewall-common/lib.base @@ -35,7 +35,7 @@ # SHOREWALL_LIBVERSION=40000 -SHOREWALL_CAPVERSION=40200 +SHOREWALL_CAPVERSION=40202 [ -n "${VARDIR:=/var/lib/shorewall}" ] [ -n "${SHAREDIR:=/usr/share/shorewall}" ] @@ -1047,6 +1047,7 @@ determine_capabilities() { CONNTRACK_MATCH= NEW_CONNTRACK_MATCH= + OLD_CONNTRACK_MATCH= MULTIPORT= XMULTIPORT= POLICY_MATCH= @@ -1105,6 +1106,7 @@ determine_capabilities() { if [ -n "$CONNTRACK_MATCH" ]; then qt $IPTABLES -A $chain -m conntrack -p tcp --ctorigdstport 22 -j ACCEPT && NEW_CONNTRACK_MATCH=Yes + qt $IPTABLES -A $chain -m conntrack ! --ctorigdst 1.2.3.4 || OLD_CONNTRACK_MATCH=Yes fi if qt $IPTABLES -A $chain -p tcp -m multiport --dports 21,22 -j ACCEPT; then @@ -1211,7 +1213,10 @@ report_capabilities() { report_capability "Multi-port Match" $MULTIPORT [ -n "$MULTIPORT" ] && report_capability "Extended Multi-port Match" $XMULTIPORT report_capability "Connection Tracking Match" $CONNTRACK_MATCH - [ -n "$CONNTRACK_MATCH" ] && report_capability "Extended Connection Tracking Match Support" $NEW_CONNTRACK_MATCH + if [ -n "$CONNTRACK_MATCH" ]; then + report_capability "Extended Connection Tracking Match Support" $NEW_CONNTRACK_MATCH + report_capability "Old Connection Tracking Match Syntax" $OLD_CONNTRACK_MATCH + fi report_capability "Packet Type Match" $USEPKTTYPE report_capability "Policy Match" $POLICY_MATCH report_capability "Physdev Match" $PHYSDEV_MATCH @@ -1263,6 +1268,7 @@ report_capabilities1() { report_capability1 XMULTIPORT report_capability1 CONNTRACK_MATCH report_capability1 NEW_CONNTRACK_MATCH + report_capability1 OLD_CONNTRACK_MATCH report_capability1 USEPKTTYPE report_capability1 POLICY_MATCH report_capability1 PHYSDEV_MATCH diff --git a/Shorewall-perl/Shorewall/Actions.pm b/Shorewall-perl/Shorewall/Actions.pm index a06476a91..e10c6431c 100644 --- a/Shorewall-perl/Shorewall/Actions.pm +++ b/Shorewall-perl/Shorewall/Actions.pm @@ -43,9 +43,11 @@ our @EXPORT = qw( merge_levels process_actions3 find_macro + find_6macro split_action substitute_param merge_macro_source_dest + merge_6macro_source_dest merge_macro_column %usedactions @@ -53,6 +55,7 @@ our @EXPORT = qw( merge_levels %actions %macros + %macros6 $macro_commands ); our @EXPORT_OK = qw( initialize ); @@ -82,6 +85,7 @@ our %actions; our %logactionchains; our %macros; +our %macros6; # # Commands that can be embedded in a macro file and how many total tokens on the line (0 => unlimited). @@ -105,6 +109,8 @@ sub initialize() { QUEUE => 'none' ); %actions = (); %logactionchains = (); + %macros = (); + %macros6 = (); } INIT { @@ -162,6 +168,19 @@ sub find_macro( $ ) } } +sub find_6macro( $ ) +{ + my $macro = $_[0]; + my $macrofile = find_file "macro.$macro"; + + if ( -f $macrofile ) { + $macros6{$macro} = $macrofile; + $targets6{$macro} = MACRO; + } else { + 0; + } +} + # # Return ( action, level[:tag] ) from passed full action # @@ -212,6 +231,22 @@ sub merge_macro_source_dest( $$ ) { $body || ''; } +sub merge_6macro_source_dest( $$ ) { + my ( $body, $invocation ) = @_; + + if ( $invocation ) { + if ( $body ) { + return $body if $invocation eq '-'; + return "$body;$invocation" if $invocation =~ /:|.*?\.*?\.|^\+|^~|^!~/; + return "$invocation;$body"; + } + + return $invocation; + } + + $body || ''; +} + sub merge_macro_column( $$ ) { my ( $body, $invocation ) = @_; diff --git a/Shorewall-perl/Shorewall/Chains.pm b/Shorewall-perl/Shorewall/Chains.pm index aa2df052e..4ba0be5fb 100644 --- a/Shorewall-perl/Shorewall/Chains.pm +++ b/Shorewall-perl/Shorewall/Chains.pm @@ -47,6 +47,15 @@ our @EXPORT = qw( $nat_table $mangle_table $filter_table + + new_6chain + new_manual_6chain + ensure_manual_6chain + + %chain6_table + $mangle6_table + $filter6_table + ); our %EXPORT_TAGS = ( @@ -91,11 +100,6 @@ our %EXPORT_TAGS = ( syn_flood_chain mac_chain macrecent_target - dynamic_fwd - dynamic_in - dynamic_out - dynamic_chains - zone_dynamic_chain dnat_chain snat_chain ecn_chain @@ -114,6 +118,7 @@ our %EXPORT_TAGS = ( newexclusionchain clearrule do_proto + do_proto6 mac_match verify_mark verify_small_mark @@ -149,6 +154,32 @@ our %EXPORT_TAGS = ( $section %sections %targets + + use_output_6chain + ensure_6chain + ensure_accounting_6chain + ensure_mangle_6chain + new_standard_6chain + new_builtin_6chain + ensure_filter_6chain + initialize_6chain_table + finish_6section + newexclusion6chain + match_source_6net + match_dest_6net + match_source_6dev + match_dest_6dev + get_interface_6address + get_interface_6addresses + get_interface_6bcasts + get_interface_6gateway + expand_6rule + set_global_6variables + create_netfilter6_load + create_6chainlist_reload + $section6 + %sections6 + %targets6 ) ], ); @@ -157,27 +188,27 @@ Exporter::export_ok_tags('internal'); our $VERSION = 4.1.5; # -# Chain Table +# Chain Table6 # -# %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 => [ -# +# %chain[6]_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 => [ +# # ... # ] # } , @@ -196,6 +227,11 @@ our %chain_table; our $nat_table; our $mangle_table; our $filter_table; + +our %chain6_table; +our $mangle6_table; +our $filter6_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 @@ -204,6 +240,9 @@ our $filter_table; our %sections; our $section; +our %sections6; +our $section6; + our $comment; use constant { STANDARD => 1, #defined by Netfilter @@ -220,6 +259,8 @@ use constant { STANDARD => 1, #defined by Netfilter }; our %targets; +our %targets6; + # # expand_rule() restrictions # @@ -240,6 +281,11 @@ our %interfacenets; our %interfacemacs; our %interfacebcasts; our %interfacegateways; +our %interface6addr; +our %interface6addrs; +our %interface6nets; +our %interface6bcasts; +our %interface6gateways; our @builtins = qw(PREROUTING INPUT FORWARD OUTPUT POSTROUTING); @@ -341,6 +387,59 @@ sub initialize() { %interfacemacs = (); %interfacebcasts = (); %interfacegateways = (); + + %chain6_table = ( raw => {} , + mangle => {}, + filter => {} ); + + $mangle6_table = $chain6_table{mangle}; + $filter6_table = $chain6_table{filter}; + + # + # These get set to 1 as sections are encountered. + # + %sections6 = ( ESTABLISHED => 0, + RELATED => 0, + NEW => 0 + ); + # + # Current rules file section. + # + $section6 = 'ESTABLISHED'; + # + # As new targets (Actions and Macros) are discovered, they are added to the table + # + %targets6 = ('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, + 'allowinUPnP' => BUILTIN + ACTION, + 'forwardUPnP' => BUILTIN + ACTION, + 'Limit' => BUILTIN + ACTION, + ); + # + # Keep track of which interfaces have active '6address', '6addresses', '6networks', etc. variables + # + %interface6addr = (); + %interface6addrs = (); + %interface6nets = (); + %interface6bcasts = (); + %interface6gateways = (); } INIT { @@ -652,13 +751,13 @@ sub use_input_chain($) { # # Interface associated with a single zone -- use the zone's input chain if it has one # - my $chainref = $filter_table->{zone_input_chain $interfaceref->{zone4}}; + 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->{zone4} , '2' , firewall_zone )}; + $chainref = $filter_table->{join( '' , $interfaceref->{zone} , '2' , firewall_zone )}; ! $chainref->{referenced}; } @@ -696,13 +795,13 @@ sub use_output_chain($) { # # Interface associated with a single zone -- use the zone's output chain if it has one # - my $chainref = $filter_table->{zone_output_chain $interfaceref->{zone4}}; + 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->{zone4} )}; + $chainref = $filter_table->{join( '', firewall_zone , '2', $interfaceref->{zone} )}; ! $chainref->{referenced}; } @@ -735,36 +834,6 @@ sub macrecent_target($) $config{MACLIST_TTL} ? chain_base($_[0]) . '_rec' : 'RETURN'; } -# -# Functions for creating dynamic zone rules -# -sub dynamic_fwd( $ ) -{ - chain_base($_[0]) . '_dynf'; -} - -sub dynamic_in( $ ) -{ - chain_base($_[0]) . '_dyni'; -} - -sub dynamic_out( $ ) # $1 = interface -{ - chain_base($_[0]) . '_dyno'; -} - -sub dynamic_chains( $ ) #$1 = interface -{ - my $c = chain_base($_[0]); - - ( $c . '_dyni' , $c . '_dynf' , $c . '_dyno' ); -} - -sub zone_dynamic_chain( $ ) # $1 = zone -{ - $_[0] . '_dyn'; - -} # # DNAT Chain from a zone # @@ -1187,6 +1256,105 @@ sub do_proto( $$$ ) $output; } +sub do_proto6( $$$ ) +{ + 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 ) { + 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 == IPv6_ICMP ) { + 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 "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]; @@ -1437,6 +1605,16 @@ sub match_source_dev( $ ) { } } +sub match_source_6dev( $ ) { + my $interface = shift; + my $interfaceref = known_6interface( $interface ); + if ( $interfaceref && $interfaceref->{options}{port} ) { + "-i $interfaceref->{bridge} -m physdev --physdev-in $interface "; + } else { + "-i $interface "; + } +} + # # Match Dest device # @@ -1454,6 +1632,20 @@ sub match_dest_dev( $ ) { } } +sub match_dest_6dev( $ ) { + my $interface = shift; + my $interfaceref = known_6interface( $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. # @@ -1557,7 +1749,7 @@ sub match_orig_dest ( $ ) { if ( $net =~ s/^!// ) { validate_net $net, 1; - $capabilities{NEW_CONNTRACK_MATCH} ? "-m conntrack ! --ctorigdst $net " : "-m conntrack --ctorigdst ! $net "; + $capabilities{OLD_CONNTRACK_MATCH} ? "-m conntrack --ctorigdst ! $net " : "-m conntrack ! --ctorigdst $net "; } else { validate_net $net, 1; $net eq ALLIPv4 ? '' : "-m conntrack --ctorigdst $net "; @@ -2578,4 +2770,918 @@ sub create_chainlist_reload($) { emit "}\n"; } +# +# Returns true if we're to use the interface's output chain +# +sub use_output_6chain($) { + my $interface = $_[0]; + my $interfaceref = find_interface6($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 = $filter6_table->{zone_output_chain $interfaceref->{zone4}}; + + return 0 if $chainref; + # + # Use the 'fw2' chain if it is referenced. + # + $chainref = $filter6_table->{join( '', firewall_zone , '2', $interfaceref->{zone} )}; + + ! $chainref->{referenced}; +} + +# +# Create a new 6chain and return a reference to it. +# +sub new_6chain($$) +{ + my ($table, $chain) = @_; + + warning_message "Internal error in new_6chain()" if $chain6_table{$table}{$chain}; + + $chain6_table{$table}{$chain} = { name => $chain, + rules => [], + table => $table, + loglevel => '', + log => 1, + cmdlevel => 0 }; +} + +# +# Create a 6chain if it doesn't exist already +# +sub ensure_6chain($$) +{ + my ($table, $chain) = @_; + + my $ref = $chain6_table{$table}{$chain}; + + return $ref if $ref; + + new_6chain $table, $chain; +} + +sub finish_6chain_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_6chain( $$ ) +{ + my ($chain, $populate) = @_; + + my $chainref = $filter6_table->{$chain}; + + $chainref = new_6chain 'filter' , $chain unless $chainref; + + if ( $populate and ! $chainref->{referenced} ) { + if ( $section eq 'NEW' or $section eq 'DONE' ) { + finish_6chain_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_6chain( $ ) +{ + my ($chain) = @_; + + my $chainref = $filter6_table->{$chain}; + + if ( $chainref ) { + fatal_error "Non-accounting chain ($chain) used in accounting rule" if ! $chainref->{accounting}; + } else { + $chainref = new_6chain 'filter' , $chain unless $chainref; + $chainref->{accounting} = 1; + $chainref->{referenced} = 1; + } + + $chainref; +} + +sub ensure_mangle_6chain($) { + my $chain = $_[0]; + + my $chainref = ensure_6chain 'mangle', $chain; + + $chainref->{referenced} = 1; + + $chainref; +} + +# +# Add a builtin chain +# +sub new_builtin_6chain($$$) +{ + my ( $table, $chain, $policy ) = @_; + + my $chainref = new_6chain $table, $chain; + $chainref->{referenced} = 1; + $chainref->{policy} = $policy; + $chainref->{builtin} = 1; +} + +sub new_standard_6chain($) { + my $chainref = new_6chain 'filter' ,$_[0]; + $chainref->{referenced} = 1; + $chainref; +} + +sub new_manual_6chain($) { + my $chain = $_[0]; + fatal_error "Duplicate Chain Name ($chain)" if $targets6{$chain} || $filter6_table->{$chain}; + $targets6{$chain} = CHAIN; + ( my $chainref = ensure_filter_6chain( $chain, 0) )->{manual} = 1; + $chainref->{referenced} = 1; + $chainref; +} + +sub ensure_manual_6chain($) { + my $chain = $_[0]; + my $chainref = $filter6_table->{$chain} || new_manual_6chain($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_6chain_table() +{ + for my $chain qw(OUTPUT PREROUTING) { + new_builtin_6chain 'raw', $chain, 'ACCEPT'; + } + + for my $chain qw(INPUT OUTPUT FORWARD) { + new_builtin_6chain 'filter', $chain, 'DROP'; + } + + for my $chain qw(PREROUTING INPUT FORWARD OUTPUT POSTROUTING ) { + new_builtin_6chain 'mangle', $chain, 'ACCEPT'; + } +} + +# +# Add ESTABLISHED,RELATED rules and synparam jumps to the passed chain +# +sub finish_6chain_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 ($sections6{NEW} ) { + if ( $chainref->{is_policy} ) { + if ( $chainref->{synparams} ) { + my $synchainref = ensure_6chain '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 = $filter6_table->{$chainref->{policychain}}; + if ( $policychainref->{synparams} ) { + my $synchainref = ensure_6chain 'filter', syn_flood_chain $policychainref; + add_rule $chainref, "-p tcp --syn -j $synchainref->{name}"; + } + } + } + + $comment = $savecomment; +} + +# +# Do section-end processing +# +sub finish_6section ( $ ) { + my $sections = $_[0]; + + for my $section ( split /,/, $sections ) { + $sections6{$section} = 1; + } + + for my $zone ( all_6zones ) { + for my $zone1 ( all_6zones ) { + my $chainref = $chain6_table{'filter'}{"${zone}2${zone1}"}; + if ( $chainref->{referenced} ) { + finish_6chain_section $chainref, $sections; + } + } + } +} + +# +# Match a Source. Handles IP addresses and ranges and MAC addresses +# +sub match_source_6net( $;$ ) { + my ( $net, $restriction) = @_; + + $restriction |= NO_RESTRICT; + + if ( $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_6net $net, 1; + "-s ! $net "; + } else { + validate_6net $net, 1; + $net eq ALLIPv6 ? '' : "-s $net "; + } +} + +# +# Match a Source. Currently only handles IP addresses and ranges +# +sub match_dest_6net( $ ) { + my $net = $_[0]; + + if ( $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_6net $net, 1; + "-d ! $net "; + } else { + validate_6net $net, 1; + $net eq ALLIPv4 ? '' : "-d $net "; + } +} + +# +# Returns the name of the shell variable holding the first address of the passed interface +# +sub interface_6address( $ ) { + my $variable = chain_base( $_[0] ) . '_6address'; + uc $variable; +} + +# +# Record that the ruleset requires the first IP address on the passed interface +# +sub get_interface_6address ( $ ) { + my ( $interface ) = $_[0]; + + my $variable = interface_6address( $interface ); + my $function = interface6_is_optional( $interface ) ? 'find_first_interface_6address_if_any' : 'find_first_interface_6address'; + + $interface6addr{$interface} = "$variable=\$($function $interface)\n"; + + "\$$variable"; +} + +# +# Returns the name of the shell variable holding the broadcast addresses of the passed interface +# +sub interface_6bcasts( $ ) { + my $variable = chain_base( $_[0] ) . '_6bcasts'; + uc $variable; +} + +# +# Record that the ruleset requires the broadcast addresses on the passed interface +# +sub get_interface_6bcasts ( $ ) { + my ( $interface ) = $_[0]; + + my $variable = interface_6bcasts( $interface ); + + $interface6bcasts{$interface} = qq($variable="\$(get_interface_6bcasts $interface)); + + "\$$variable"; +} + +# +# Returns the name of the shell variable holding the gateway through the passed interface +# +sub interface_6gateway( $ ) { + my $variable = chain_base( $_[0] ) . '_6gateway'; + uc $variable; +} + +# +# Record that the ruleset requires the gateway address on the passed interface +# +sub get_interface_6gateway ( $ ) { + my ( $interface ) = $_[0]; + + my $variable = interface_6gateway( $interface ); + + if ( interface_is_optional $interface ) { + $interface6gateways{$interface} = qq([ -n "\$$variable" ] || $variable=\$(detect_6gateway $interface)\n); + } else { + $interface6gateways{$interface} = qq([ -n "\$$variable" ] || $variable=\$(detect_6gateway $interface) +[ -n "\$$variable" ] || fatal_error "Unable to detect the IPv6 gateway through interface $interface" +); + } + + "\$$variable"; +} + +# +# Returns the name of the shell variable holding the addresses of the passed interface +# +sub interface_6addresses( $ ) { + my $variable = chain_base( $_[0] ) . '_6addresses'; + uc $variable; +} + +# +# Record that the ruleset requires the IP addresses on the passed interface +# +sub get_interface_6addresses ( $ ) { + my ( $interface ) = $_[0]; + + my $variable = interface_6addresses( $interface ); + + if ( interface_is_optional $interface ) { + $interface6addrs{$interface} = qq($variable=\$(find_interface_6addresses $interface)\n); + } else { + $interface6addrs{$interface} = qq($variable=\$(find_interface_6addresses $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_6nets( $ ) { + my $variable = chain_base( $_[0] ) . '_6networks'; + uc $variable; +} + +# +# Record that the ruleset requires the networks routed out of the passed interface +# +sub get_interface_6nets ( $ ) { + my ( $interface ) = $_[0]; + + my $variable = interface_6nets( $interface ); + + if ( interface_is_optional $interface ) { + $interface6nets{$interface} = qq($variable=\$(get_routed_6networks $interface)\n); + } else { + $interface6nets{$interface} = qq($variable=\$(get_routed_6networks $interface) +[ -n "\$$variable" ] || fatal_error "Unable to determine the IPv6 routes 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_6rule( $$$$$$$$$ ) +{ + 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 + $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 ); + 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_6command( $$$ ) { + 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 = $chain6_table{$chainref->{table}}{$disposition}; + $targetref->{referenced} = 1 if $targetref; + } + + # + # Isolate Source Interface, if any + # + if ( $source ) { + if ( $source eq '-' ) { + $source = ''; + } elsif ( $source =~ /^(.+);(.+)$/ ) { + $iiface = $1; + $inets = $2; + } elsif ( $source =~ /\+|~|\..*\.|:/ ) { + $inets = $source; + } else { + $iiface = $source; + } + } else { + $source = ''; + } + + # + # Verify Interface, if any + # + if ( $iiface ) { + fatal_error "Unknown IPv6 Interface ($iiface)" unless known_6interface $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_6nets ( $iiface ); + + push_6command $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_6dev( $iiface ); + } + } + + # + # Isolate Destination Interface, if any + # + if ( $dest ) { + if ( $dest eq '-' ) { + $dest = ''; + } elsif ( $dest =~ /^(.+);(.+)$/ ) { + $diface = $1; + $dnets = $2; + } elsif ( $dest =~ /\+|~|\..*\.|:/ ) { + $dnets = $dest; + } else { + $diface = $dest; + } + } else { + $dest = ''; + } + + # + # Verify Destination Interface, if any + # + if ( $diface ) { + fatal_error "Unknown IPv6 Interface ($diface)" unless known_6interface $diface; + + if ( $restriction & PREROUTE_RESTRICT ) { + # + # ADDRESS 'detect' in the masq file. + # + fatal_error "Bridge port ($diface) not allowed" if port_to_bridge( $diface ); + push_6command( $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_6bridge( $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_6bridge( $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_6dev( $diface ); + } + } else { + $diface = ''; + } + + # + # 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 = ALLIPv6 unless $inets; + $dnets = ALLIPv6 unless $dnets; + + fatal_error "Input interface may not be specified with a source IP address in the POSTROUTING chain" if $restriction == POSTROUTE_RESTRICT && $iiface && $inets ne ALLIPv6; + fatal_error "Output interface may not be specified with a destination IP address in the PREROUTING chain" if $restriction == PREROUTE_RESTRICT && $diface && $dnets ne ALLIPv6; + + if ( $iexcl || $dexcl ) { + # + # We have non-trivial exclusion -- need to create an exclusion chain + # + fatal_error "Exclusion is not possible in ACCEPT+/CONTINUE rules" if $disposition eq 'RETURN'; + + my $echain = newexclusionchain; + + # + # Use the current rule and sent all possible matches to the exclusion chain + # + 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_6net( $inet, $restriction ), match_dest_6net( $dnet ), "-j $echain" ), 1 ); + } + } + # + # Create the Exclusion Chain + # + my $echainref = new_6chain $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 ); + # + # 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 $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 ) ) ); + } + + unless ( $disposition eq 'LOG' ) { + add_rule( + $chainref, + join( '', $rule, match_source_net ($inet , $restriction ), match_dest_net( $dnet ), $target ) , + 1 ); + } + } + } + } + + while ( @ends ) { + decr_cmd_level $chainref; + add_command $chainref, pop @ends; + } + + $diface; +} + +# +# Generate setting of global variables +# +sub set_global_6variables() { + + our ( $emitted_comment, $emitted_test ) = (0, 0); + + for ( values %interface6addr ) { + emit_comment unless $emitted_comment; + emit $_; + } + + for ( values %interface6gateways ) { + emit_comment unless $emitted_comment; + emit $_; + } + + for ( values %interface6addrs ) { + emit_comment unless $emitted_comment; + emit_test unless $emitted_test; + emit $_; + } + + for ( values %interface6nets ) { + emit_comment unless $emitted_comment; + emit_test unless $emitted_test; + emit $_; + } + + pop_indent, emit "fi\n" if $emitted_test; + +} + +# +# Generate the netfilter input +# +sub create_netfilter_6_load() { + + my @table_list; + + push @table_list, 'raw' if $capabilities{RAW_TABLE}; + push @table_list, 'mangle' if $capabilities{MANGLE_ENABLED} && $config{MANGLE_ENABLED}; + push @table_list, 'filter'; + + $mode = NULL_MODE; + + emit ( 'setup_netfilter_6()', + '{' + ); + + push_indent; + + save_progress_message "Preparing ip6tables-restore input..."; + + emit ''; + + emit 'exec 3>${VARDIR}/.ip6tables-restore-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 = $chain6_table{$table}{$chain}; + if ( $chainref ) { + fatal_error "Internal error in create_netfilter_6_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 $chain6_table{$table}{$_}->{referenced} , ( sort keys %{$chain6_table{$table}} ) ) { + my $chainref = $chain6_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 $_ 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 ip6tables-restore command + # + emit( 'exec 3>&-', + '', + '[ -n "$DEBUG" ] && command=debug_restore_input || command=$IP6TABLES_RESTORE', + '', + 'progress_message2 "Running $command..."', + '', + 'cat ${VARDIR}/.ip6tables-restore-input | $command # Use this nonsensical form to appease SELinux', + 'if [ $? != 0 ]; then', + ' fatal_error "iptables-restore Failed. Input is in ${VARDIR}/.ip6tables-restore-input"', + "fi\n" + ); + + pop_indent; + + emit "}\n"; +} + +# +# Generate the netfilter input for refreshing a list of chains +# +sub create_6chainlist_reload($) { + + my $chains = $_[0]; + + my @chains = split_list $chains, 'chain'; + + unless ( @chains ) { + @chains = qw( blacklst ) if $filter6_table->{blacklst}; + push @chains, 'mangle:' if $capabilities{MANGLE_ENABLED} && $config{MANGLE_ENABLED}; + $chains = join( ',', @chains ) if @chains; + } + + $mode = NULL_MODE; + + emit( 'chainlist_6_reload()', + '{' + ); + + push_indent; + + if ( @chains ) { + if ( @chains == 1 ) { + progress_message2 "Compiling ip6tables-restore input for chain @chains..."; + save_progress_message "Preparing ip6tables-restore input for chain @chains..."; + } else { + progress_message2 "Compiling ip6tables-restore input for chains $chains..."; + save_progress_message "Preparing ip6tables-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 =~ /^(mangle|filter)$/; + + $chains{$table} = [] unless $chains{$table}; + + if ( $chain ) { + fatal_error "No $table chain found with name $chain" unless $chain6_table{$table}{$chain}; + fatal_error "Built-in chains may not be refreshed" if $chain6_table{table}{$chain}{builtin}; + push @{$chains{$table}}, $chain; + } else { + while ( my ( $chain, $chainref ) = each %{$chain6_table{$table}} ) { + push @{$chains{$table}}, $chain if $chainref->{referenced} && ! $chainref->{builtin}; + } + } + } + + emit 'exec 3>${VARDIR}/.ip6tables-restore-input'; + + enter_cat_mode; + + for $table qw(mangle filter) { + next unless $chains{$table}; + + emit_unindented "*$table"; + + my $tableref=$chain6_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 $_ 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 ip6tables-restore..."', + '', + 'cat ${VARDIR}/.ip6tables-restore-input | $IP6TABLES_RESTORE -n # Use this nonsensical form to appease SELinux', + 'if [ $? != 0 ]; then', + ' fatal_error "ip6tables-restore Failed. Input is in ${VARDIR}/.ip6tables-restore-input"', + "fi\n" + ); + } else { + emit('true'); + } + + pop_indent; + + emit "}\n"; +} + 1; diff --git a/Shorewall-perl/Shorewall/Compiler.pm b/Shorewall-perl/Shorewall/Compiler.pm index d4e6f7549..6e20446f8 100644 --- a/Shorewall-perl/Shorewall/Compiler.pm +++ b/Shorewall-perl/Shorewall/Compiler.pm @@ -41,7 +41,7 @@ use Shorewall::Proxyarp; our @ISA = qw(Exporter); our @EXPORT = qw( compiler EXPORT TIMESTAMP DEBUG ); our @EXPORT_OK = qw( $export ); -our $VERSION = 4.1.4; +our $VERSION = 4.3.0; our $export; @@ -168,6 +168,13 @@ sub generate_script_1() { emit( 'IPTABLES_RESTORE=${IPTABLES}-restore', '[ -x "$IPTABLES_RESTORE" ] || startup_error "$IPTABLES_RESTORE does not exist or is not executable"' ); + if ( $config{IPV6} eq 'On' ) { + emit( 'IP6TABLES=$(dirname ${IPTABLES})/ip6tables', + '[ -x "$IPTABLES_RESTORE" ] || startup_error "$IP6TABLES_RESTORE does not exist or is not executable"' ); + emit( 'IP6TABLES_RESTORE=$(dirname ${IPTABLES})/ip6tables-restore', + '[ -x "$IP6TABLES_RESTORE" ] || startup_error "$IP6TABLES_RESTORE does not exist or is not executable"' ); + } + append_file 'params' if $config{EXPORTPARAMS}; emit ( '', diff --git a/Shorewall-perl/Shorewall/Config.pm b/Shorewall-perl/Shorewall/Config.pm index be3b8cfe1..ade1bc212 100644 --- a/Shorewall-perl/Shorewall/Config.pm +++ b/Shorewall-perl/Shorewall/Config.pm @@ -114,7 +114,7 @@ our %EXPORT_TAGS = ( internal => [ qw( create_temp_object Exporter::export_ok_tags('internal'); -our $VERSION = 4.2.0; +our $VERSION = 4.3.0; # # describe the current command, it's present progressive, and it's completion. @@ -163,7 +163,7 @@ our %config; # # Config options and global settings that are to be copied to object script # -our @propagateconfig = qw/ DISABLE_IPV6 MODULESDIR MODULE_SUFFIX LOGFORMAT SUBSYSLOCK LOCKFILE /; +our @propagateconfig = qw/ IPV6 MODULESDIR MODULE_SUFFIX LOGFORMAT SUBSYSLOCK LOCKFILE /; our @propagateenv = qw/ LOGLIMIT LOGTAGONLY LOGRULENUMBERS /; # # From parsing the capabilities file @@ -177,6 +177,8 @@ our %capdesc = ( NAT_ENABLED => 'NAT', MULTIPORT => 'Multi-port Match' , XMULTIPORT => 'Extended Multi-port Match', CONNTRACK_MATCH => 'Connection Tracking Match', + OLD_CONNTRACK_MATCH => + 'Old conntrack match syntax', NEW_CONNTRACK_MATCH => 'Extended Connection Tracking Match', USEPKTTYPE => 'Packet Type Match', @@ -271,7 +273,7 @@ sub initialize() { LOGPARMS => '', TC_SCRIPT => '', VERSION => "4.2.1", - CAPVERSION => 40200 , + CAPVERSION => 40202 , ); # # From shorewall.conf file @@ -348,6 +350,7 @@ sub initialize() { DELAYBLACKLISTLOAD => undef, MODULE_SUFFIX => undef, DISABLE_IPV6 => undef, + IPV6 => undef, DYNAMIC_ZONES => undef, PKTTYPE=> undef, RFC1918_STRICT => undef, @@ -389,6 +392,7 @@ sub initialize() { XMULTIPORT => undef, CONNTRACK_MATCH => undef, NEW_CONNTRACK_MATCH => undef, + OLD_CONNTRACK_MATCH => undef, USEPKTTYPE => undef, POLICY_MATCH => undef, PHYSDEV_MATCH => undef, @@ -765,12 +769,14 @@ sub copy( $ ) { open IF , $file or fatal_error "Unable to open $file: $!"; while ( ) { + chomp; if ( /^\s*$/ ) { print $object "\n" unless $lastlineblank; $lastlineblank = 1; } else { s/^/$indent/ if $indent; print $object $_; + print $object "\n"; $lastlineblank = 0; } } @@ -791,6 +797,7 @@ sub copy1( $ ) { my $do_indent = 1; while ( ) { + chomp; if ( /^\s*$/ ) { print $object "\n"; $do_indent = 1; @@ -799,6 +806,7 @@ sub copy1( $ ) { s/^/$indent/ if $indent && $do_indent; print $object $_; + print $object "\n"; $do_indent = ! ( /\\$/ ); } @@ -1557,8 +1565,9 @@ sub determine_capabilities( $ ) { $capabilities{CONNTRACK_MATCH} = qt1( "$iptables -A $sillyname -m conntrack --ctorigdst 192.168.1.1 -j ACCEPT" ); - if ( $capabilities{CONNTRACL_MATCH} ) { + if ( $capabilities{CONNTRACK_MATCH} ) { $capabilities{NEW_CONNTRACK_MATCH} = qt1( "$iptables -A $sillyname -m conntrack -p tcp --ctorigdstport 22 -j ACCEPT" ); + $capabilities{OLD_CONNTRACK_MATCH} = ! qt1( "$iptables -A $sillyname -m conntrack ! --ctorigdstport 1.2.3.4" ); } if ( qt1( "$iptables -A $sillyname -p tcp -m multiport --dports 21,22 -j ACCEPT" ) ) { @@ -1892,7 +1901,22 @@ sub get_configuration( $ ) { default_yes_no 'ADMINISABSENTMINDED' , ''; default_yes_no 'BLACKLISTNEWONLY' , ''; - default_yes_no 'DISABLE_IPV6' , ''; + + if ( defined $config{IPV6} ) { + if ( $config{IPV6} =~ /on/i ) { + $config{IPV6} = 'On'; + } elsif ( $config{IPV6} =~ /off/i ) { + $config{IPV6} = 'Off'; + } elsif ( $config{IPV6} =~ /keep/i ) { + $config{IPV6} = ''; + } + } + + default_yes_no 'DISABLE_IPV6' , ''; + + fatal_error "Incompatible settings of IPV6 (On) and DISABLE_IPV6 (Yes)" if $config{IPV6} eq 'On' && $config{DISABLE_IPV6} eq 'Yes'; + + $config{IPV6} = $config{DISABLE_IPV6} ? 'Off' : '' unless defined $config{IPV6}; unsupported_yes_no 'DYNAMIC_ZONES'; unsupported_yes_no 'BRIDGING'; diff --git a/Shorewall-perl/Shorewall/IPAddrs.pm b/Shorewall-perl/Shorewall/IPAddrs.pm index 5e72637a6..292777203 100644 --- a/Shorewall-perl/Shorewall/IPAddrs.pm +++ b/Shorewall-perl/Shorewall/IPAddrs.pm @@ -26,25 +26,36 @@ # package Shorewall::IPAddrs; require Exporter; +use Socket6; use Shorewall::Config qw( :DEFAULT split_list require_capability in_hex8); use strict; our @ISA = qw(Exporter); our @EXPORT = qw( ALLIPv4 + ALLIPv6 TCP UDP ICMP + IPv6_ICMP SCTP + F_INET + F_INET6 + validate_address + validate_6address validate_net + validate_6net decompose_net validate_host + validate_6host validate_range + validate_6range ip_range_explicit expand_port_range allipv4 + allipv6 rfc1918_networks resolve_proto proto_name @@ -54,14 +65,23 @@ our @EXPORT = qw( ALLIPv4 validate_icmp ); our @EXPORT_OK = qw( ); -our $VERSION = 4.1.5; +our $VERSION = 4.3.0; # -# Some IPv4 useful stuff +# Some IPv4/6 useful stuff # our @allipv4 = ( '0.0.0.0/0' ); +our @allipv6 = ( '::/0' ); -use constant { ALLIPv4 => '0.0.0.0/0' , ICMP => 1, TCP => 6, UDP => 17 , SCTP => 132 }; +use constant { ALLIPv4 => '0.0.0.0/0' , + ALLIPv6 => '::/0' , + F_INET => 1, + F_INET6 => 2, + ICMP => 1, + TCP => 6, + UDP => 17, + ICMPv6_ICMP => 58, + SCTP => 132 }; our @rfc1918_networks = ( "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ); @@ -95,8 +115,7 @@ sub validate_address( $$ ) { if ( defined wantarray ) { shift @addrs for (1..4); for ( @addrs ) { - my (@a) = unpack('C4',$_); - $_ = join('.', @a ); + $_ = inet_htoa $_; } } } @@ -223,6 +242,10 @@ sub allipv4() { @allipv4; } +sub allipv6() { + @allipv6; +} + sub rfc1918_networks() { @rfc1918_networks } @@ -231,7 +254,7 @@ sub rfc1918_networks() { # Protocol/port validation # -our %nametoproto = ( all => 0, ALL => 0, icmp => 1, ICMP => 1, tcp => 6, TCP => 6, udp => 17, UDP => 17 ); +our %nametoproto = ( all => 0, ALL => 0, icmp => 1, ICMP => 1, tcp => 6, TCP => 6, udp => 17, UDP => 17 ); our @prototoname = ( 'all', 'icmp', '', '', '', '', 'tcp', '', '', '', '', '', '', '', '', '', '', 'udp' ); # @@ -419,4 +442,121 @@ sub expand_port_range( $$ ) { } } +sub valid_6address( $ ) { + my $address = $_[0]; + + my @address = split /:/, $address; + + return 0 if @address > 8; + return 0 if @address < 8 && ! $address =~ /::/; + return 0 if $address =~ /:::/ || $address =~ /::.*::/; + + if ( $address =~ /^:/ ) { + unless ( $address eq '::' ) { + return 0 if $address =~ /:$/ || $address =~ /^:.*::/; + } + } elsif ( $address =~ /:$/ ) { + return 0 if $address =~ /::.*:$/; + } + + for my $a ( @address ) { + return 0 unless $a eq '' || ( $a =~ /^[a-fA-f\d]+$/ && oct "0x$a" < 65536 ); + } + + 1; +} + +sub validate_6address( $$ ) { + my ( $addr, $allow_name ) = @_; + + my @addrs = ( $addr ); + + unless ( valid_6address $addr ) { + fatal_error "Invalid IPv6 Address ($addr)" unless $allow_name; + fatal_error "Unknown Host ($addr)" unless (@addrs = gethostbyname2 $addr, AF_INET6); + + if ( defined wantarray ) { + shift @addrs for (1..4); + for ( @addrs ) { + $_ = inet_ntop AF_INET6, $_; + } + } + } + + defined wantarray ? wantarray ? @addrs : $addrs[0] : undef; +} +} + +sub validate_6net( $ ) { + my ($net, $vlsm, $rest) = split( '/', $_[0], 3 ); + + fatal_error "An ipset name ($net) is not allowed in this context" if substr( $net, 0, 1 ) eq '+'; + + if ( defined $vlsm ) { + fatal_error "Invalid VLSM ($vlsm)" unless $vlsm =~ /^\d+$/ && $vlsm <= 64; + fatal_error "Invalid Network address ($_[0])" if defined $rest; + fatal_error "Invalid IPv6 address ($net)" unless valid_6address $net; + } else { + fatal_error "Invalid Network address ($_[0])" if $_[0] =~ '/' || ! defined $net; + validate_6address $net; + } +} + +sub validate_6range( $$ ) { + my ( $low, $high ) = @_; + + validate_6address $low, 0; + validate_6address $high, 0; + + my @low = split ":", $low; + my @high = split ":", $high; + + if ( @low == @high ) { + my ( $l, $h) = ( pop @low, pop @high ); + + return 1 if hex "0x$l" <= hex "0x$h" && join( ":", @low ) eq join( ":", @high ); + } + + fatal_error "Invalid IPv6 Range ($low-$high)"; +} + +my %ipv6_icmp_types = ( any => 'any', + 'destination-unreachable' => 1, + 'no-route' => '1/0', + 'communication-prohibited' => '1/1', + 'address-unreachable' => '1/2', + 'port-unreachable' => '1/3', + 'packet-too-big' => 2, + 'time-exceeded' => 3, + 'ttl-exceeded' => 3, + 'ttl-zero-during-transit' => '3/0', + 'ttl-zero-during-reassembly' => '3/1', + 'parameter-problem' => 4 + 'bad-header' => '4/0', + 'unknown-header-type' => '4/1', + 'unknown-option' => '4/2', + 'echo-request' => 128, + 'echo-reply' => 129, + 'router-solicitation' => 133, + 'router-advertisement' => 134, + 'neighbour-solicitation' => 135, + 'neighbour-advertisement' => 136, + redirect => 137 ); + + +sub validate_icmp6( $ ) { + my $type = $_[0]; + + my $value = $ipv6_icmp_types{$type}; + + return $value if defined $value; + + if ( $type =~ /^(\d+)(\/(\d+))?$/ ) { + return $type if $1 < 256 && ( ! $2 || $3 < 256 ); + } + + fatal_error "Invalid IPv6 ICMP Type ($type)" +} + + 1; diff --git a/Shorewall-perl/Shorewall/Policy.pm b/Shorewall-perl/Shorewall/Policy.pm index 2d9d3c9c3..4ee7ebcb3 100644 --- a/Shorewall-perl/Shorewall/Policy.pm +++ b/Shorewall-perl/Shorewall/Policy.pm @@ -26,20 +26,33 @@ package Shorewall::Policy; require Exporter; use Shorewall::Config qw(:DEFAULT :internal); use Shorewall::Zones; +use Shorewall::IPAddrs; use Shorewall::Chains qw( :DEFAULT :internal) ; use Shorewall::Actions; use strict; our @ISA = qw(Exporter); -our @EXPORT = qw( validate_policy apply_policy_rules complete_standard_chain sub setup_syn_flood_chains ); +our @EXPORT = qw( validate_policy + validate_6policy + apply_policy_rules + apply_6policy_rules + complete_standard_chain + complete_standard_6chain + setup_syn_flood_chains + setup_syn_flood_6chains ); + our @EXPORT_OK = qw( ); -our $VERSION = 4.1.1; +our $VERSION = 4.3.0; # @policy_chains is a list of references to policy chains in the filter table our @policy_chains; +# @policy_6chains is a list of references to policy chains in the filter6 table + +our @policy_6chains; + # # Initialize globals -- we take this novel approach to globals initialization to allow # the compiler to run multiple times in the same process. The @@ -51,6 +64,7 @@ our @policy_chains; sub initialize() { @policy_chains = (); + @policy_6chains = (); } INIT { @@ -85,6 +99,20 @@ sub new_policy_chain($$$$) $chainref; } +# +# Create a new policy 6chain and return a reference to it. +# +sub new_policy_6chain($$$$) +{ + my ($source, $dest, $policy, $optional) = @_; + + my $chainref = new_6chain( 'filter', "${source}2${dest}" ); + + convert_to_policy_chain( $chainref, $source, $dest, $policy, $optional ); + + $chainref; +} + # # Set the passed chain's policychain and policy to the passed values. # @@ -142,6 +170,21 @@ sub add_or_modify_policy_chain( $$ ) { } } +sub add_or_modify_policy_6chain( $$ ) { + my ( $zone, $zone1 ) = @_; + my $chain = "${zone}2${zone1}"; + my $chainref = $filter6_table->{$chain}; + + if ( $chainref ) { + unless( $chainref->{is_policy} ) { + convert_to_policy_chain( $chainref, $zone, $zone1, 'CONTINUE', OPTIONAL ); + push @policy_6chains, $chainref; + } + } else { + push @policy_6chains, ( new_policy_chain $zone, $zone1, 'CONTINUE', OPTIONAL ); + } +} + sub print_policy($$$$) { my ( $source, $dest, $policy , $chain ) = @_; unless ( ( $source eq 'all' ) || ( $dest eq 'all' ) ) { @@ -220,11 +263,17 @@ sub validate_policy() my $clientwild = ( "\L$client" eq 'all' ); - fatal_error "Undefined zone ($client)" unless $clientwild || defined_zone( $client ); + unless ( $clientwild ) { + fatal_error "Undefined zone ($client)" unless defined_zone( $client ); + fatal_error "IPv6 zone ($client) not permitted in policy file" unless zone_family($client) & F_INET; + } my $serverwild = ( "\L$server" eq 'all' ); - fatal_error "Undefined zone ($server)" unless $serverwild || defined_zone( $server ); + unless ( $serverwild ) { + fatal_error "Undefined zone ($server)" unless defined_zone( $server ); + fatal_error "IPv6 zone ($server) not permitted in policy file" unless zone_family($server) & F_INET; + } my ( $policy, $default, $remainder ) = split( /:/, $originalpolicy, 3 ); @@ -344,6 +393,205 @@ sub validate_policy() } } + +sub validate_6policy() +{ + my %validpolicies = ( + ACCEPT => undef, + REJECT => undef, + DROP => undef, + CONTINUE => undef, + QUEUE => undef, + NFQUEUE => undef, + NONE => undef + ); + + my %map = ( DROP_DEFAULT => 'DROP' , + REJECT_DEFAULT => 'REJECT' , + ACCEPT_DEFAULT => 'ACCEPT' , + QUEUE_DEFAULT => 'QUEUE' , + NFQUEUE_DEFAULT => 'NFQUEUE' ); + + my $zone; + my @zonelist = $config{EXPAND_POLICIES} ? all_6zones : ( all_6zones, 'all' ); + + for my $option qw/DROP_DEFAULT REJECT_DEFAULT ACCEPT_DEFAULT QUEUE_DEFAULT NFQUEUE_DEFAULT/ { + my $action = $config{$option}; + next if $action eq 'none'; + my $actiontype = $targets{$action}; + + if ( defined $actiontype ) { + fatal_error "Invalid setting ($action) for $option" unless $actiontype & ACTION; + } else { + fatal_error "Default Action $option=$action not found"; + } + + unless ( $usedactions{$action} ) { + $usedactions{$action} = 1; + createactionchain $action; + } + + $default_actions{$map{$option}} = $action; + } + + for $zone ( all_6zones ) { + push @policy_6chains, ( new_policy_6chain $zone, $zone, 'ACCEPT', OPTIONAL ); + + if ( $config{IMPLICIT_CONTINUE} && ( @{find_zone( $zone )->{parents}} ) ) { + for my $zone1 ( all_6zones ) { + unless( $zone eq $zone1 ) { + add_or_modify_policy_chain( $zone, $zone1 ); + add_or_modify_policy_chain( $zone1, $zone ); + } + } + } + } + + my $fn = open_file '6policy'; + + first_entry "$doing $fn..."; + + while ( read_a_line ) { + + my ( $client, $server, $originalpolicy, $loglevel, $synparams, $connlimit ) = split_line 3, 6, '6policy file'; + + $loglevel = '' if $loglevel eq '-'; + $synparams = '' if $synparams eq '-'; + $connlimit = '' if $connlimit eq '-'; + + my $clientwild = ( "\L$client" eq 'all' ); + + unless ( $clientwild ) { + fatal_error "Undefined zone ($client)" unless defined_zone( $client ); + fatal_error "IPv4 zone ($client) not permitted in policy file" unless zone_family($client) & F_INET6; + } + + my $serverwild = ( "\L$server" eq 'all' ); + + unless ( $serverwild ) { + fatal_error "Undefined zone ($server)" unless defined_zone( $server ); + fatal_error "IPv4 zone ($server) not permitted in policy file" unless zone_family($server) & F_INET6; + } + + my ( $policy, $default, $remainder ) = split( /:/, $originalpolicy, 3 ); + + fatal_error "Invalid or missing POLICY ($originalpolicy)" unless $policy; + + fatal_error "Invalid default action ($default:$remainder)" if defined $remainder; + + ( $policy , my $queue ) = get_target_param $policy; + + if ( $default ) { + if ( "\L$default" eq 'none' ) { + $default = 'none'; + } else { + my $defaulttype = $targets6{$default} || 0; + + if ( $defaulttype & ACTION ) { + unless ( $usedactions{$default} ) { + $usedactions{$default} = 1; + createactionchain $default; + } + } else { + fatal_error "Unknown Default Action ($default)"; + } + } + } else { + $default = $default_actions{$policy} || ''; + } + + fatal_error "Invalid policy ($policy)" unless exists $validpolicies{$policy}; + + if ( defined $queue ) { + fatal_error "Invalid policy ($policy($queue))" unless $policy eq 'NFQUEUE'; + require_capability( 'NFQUEUE_TARGET', 'An NFQUEUE Policy', 's' ); + my $queuenum = numeric_value( $queue ); + fatal_error "Invalid NFQUEUE queue number ($queue)" unless defined( $queuenum) && $queuenum <= 65535; + $policy = "NFQUEUE --queue-num $queuenum"; + } elsif ( $policy eq 'NONE' ) { + fatal_error "NONE policy not allowed with \"all\"" + if $clientwild || $serverwild; + fatal_error "NONE policy not allowed to/from firewall zone" + if ( zone_type( $client ) eq 'firewall' ) || ( zone_type( $server ) eq 'firewall' ); + } + + unless ( $clientwild || $serverwild ) { + if ( zone_type( $server ) eq 'bport6' ) { + fatal_error "Invalid policy - DEST zone is a Bridge Port zone but the SOURCE zone is not associated with the same bridge" + unless find_zone( $client )->{bridge} eq find_zone( $server)->{bridge} || single_interface( $client ) eq find_zone( $server )->{bridge}; + } + } + + my $chain = "${client}2${server}"; + my $chainref; + + if ( defined $filter_table->{$chain} ) { + $chainref = $filter_table->{$chain}; + + if ( $chainref->{is_policy} ) { + if ( $chainref->{is_optional} ) { + $chainref->{is_optional} = 0; + $chainref->{policy} = $policy; + } else { + fatal_error qq(Policy "$client $server $policy" duplicates earlier policy "@{$chainref->{policypair}} $chainref->{policy}"); + } + } elsif ( $chainref->{policy} ) { + fatal_error qq(Policy "$client $server $policy" duplicates earlier policy "@{$chainref->{policypair}} $chainref->{policy}"); + } else { + convert_to_policy_chain( $chainref, $client, $server, $policy, 0 ); + push @policy_6chains, ( $chainref ) unless $config{EXPAND_POLICIES} && ( $clientwild || $serverwild ); + } + } else { + $chainref = new_policy_6chain $client, $server, $policy, 0; + push @policy_6chains, ( $chainref ) unless $config{EXPAND_POLICIES} && ( $clientwild || $serverwild ); + } + + $chainref->{loglevel} = validate_level( $loglevel ) if defined $loglevel && $loglevel ne ''; + + if ( $synparams ne '' || $connlimit ne '' ) { + my $value = ''; + fatal_error "Invalid CONNLIMIT ($connlimit)" if $connlimit =~ /^!/; + $value = do_ratelimit $synparams, 'ACCEPT' if $synparams ne ''; + $value .= do_connlimit $connlimit if $connlimit ne ''; + $chainref->{synparams} = $value; + $chainref->{synchain} = $chain + } + + $chainref->{default} = $default if $default; + + if ( $clientwild ) { + if ( $serverwild ) { + for my $zone ( @zonelist ) { + for my $zone1 ( @zonelist ) { + set_policy_chain $client, $server, "${zone}2${zone1}", $chainref, $policy; + print_policy $zone, $zone1, $policy, $chain; + } + } + } else { + for my $zone ( all_zones ) { + set_policy_chain $client, $server, "${zone}2${server}", $chainref, $policy; + print_policy $zone, $server, $policy, $chain; + } + } + } elsif ( $serverwild ) { + for my $zone ( @zonelist ) { + set_policy_chain $client, $server, "${client}2${zone}", $chainref, $policy; + print_policy $client, $zone, $policy, $chain; + } + + } else { + print_policy $client, $server, $policy, $chain; + } + } + + for $zone ( all_zones ) { + for my $zone1 ( all_zones ) { + fatal_error "No policy defined from zone $zone to zone $zone1" unless $filter_table->{"${zone}2${zone1}"}{policy}; + } + } +} + + # # Policy Rule application # @@ -361,6 +609,19 @@ sub policy_rules( $$$$$ ) { } } +sub policy_6rules( $$$$$ ) { + my ( $chainref , $target, $loglevel, $default, $dropmulticast ) = @_; + + unless ( $target eq 'NONE' ) { + add_rule $chainref, "-j $default" if $default && $default ne 'none'; + log_rule $loglevel , $chainref , $target , '' if $loglevel ne ''; + fatal_error "Null target in policy_rules()" unless $target; + $target = 'reject' if $target eq 'REJECT'; + + add_jump( $chainref , $target ) unless $target eq 'CONTINUE'; + } +} + sub report_syn_flood_protection() { progress_message ' Enabled SYN flood protection'; } @@ -400,8 +661,43 @@ sub default_policy( $$$ ) { } +sub default_6policy( $$$ ) { + my $chainref = $_[0]; + my $policyref = $filter6_table->{$chainref->{policychain}}; + my $synparams = $policyref->{synparams}; + my $default = $policyref->{default}; + my $policy = $policyref->{policy}; + my $loglevel = $policyref->{loglevel}; + + fatal_error "Internal error in default_6policy()" unless $policyref; + + if ( $chainref eq $policyref ) { + policy_6rules $chainref , $policy, $loglevel , $default, $config{MULTICAST}; + } else { + if ( $policy eq 'ACCEPT' || $policy eq 'QUEUE' || $policy =~ /^NFQUEUE/ ) { + if ( $synparams ) { + report_syn_flood_protection; + policy_6rules $chainref , $policy , $loglevel , $default, $config{MULTICAST}; + } else { + add_jump $chainref, $policyref; + $chainref = $policyref; + } + } elsif ( $policy eq 'CONTINUE' ) { + report_syn_flood_protection if $synparams; + policy_6rules $chainref , $policy , $loglevel , $default, $config{MULTICAST}; + } else { + report_syn_flood_protection if $synparams; + add_jump $chainref , $policyref; + $chainref = $policyref; + } + } + + progress_message " Policy $policy from $_[1] to $_[2] using chain $chainref->{name}"; + +} + sub apply_policy_rules() { - progress_message2 'Applying Policies...'; + progress_message2 'Applying IPv4 Policies...'; for my $chainref ( @policy_chains ) { my $policy = $chainref->{policy}; @@ -434,6 +730,40 @@ sub apply_policy_rules() { } } +sub apply_6policy_rules() { + progress_message2 'Applying IPv6 Policies...'; + + for my $chainref ( @policy_6chains ) { + my $policy = $chainref->{policy}; + my $loglevel = $chainref->{loglevel}; + my $optional = $chainref->{is_optional}; + my $default = $chainref->{default}; + my $name = $chainref->{name}; + + if ( $policy ne 'NONE' ) { + if ( ! $chainref->{referenced} && ( ! $optional && $policy ne 'CONTINUE' ) ) { + ensure_filter_6chain $name, 1; + } + + if ( $name =~ /^all2|2all$/ ) { + run_user_exit $chainref; + policy_6rules $chainref , $policy, $loglevel , $default, $config{MULTICAST}; + } + } + } + + for my $zone ( all_6zones ) { + for my $zone1 ( all_6zones ) { + my $chainref = $filter6_table->{"${zone}2${zone1}"}; + + if ( $chainref->{referenced} ) { + run_user_exit $chainref; + default_6policy $chainref, $zone, $zone1; + } + } + } +} + # # Complete a standard chain # @@ -461,6 +791,33 @@ sub complete_standard_chain ( $$$$ ) { policy_rules $stdchainref , $policy , $loglevel, $defaultaction, 0; } +# +# Complete a standard chain +# +# - run any supplied user exit +# - search the policy file for an applicable policy and add rules as +# appropriate +# - If no applicable policy is found, add rules for an assummed +# policy of DROP INFO +# +sub complete_standard_6chain ( $$$$ ) { + my ( $stdchainref, $zone, $zone2, $default ) = @_; + + add_rule $stdchainref, '-m state --state ESTABLISHED,RELATED -j ACCEPT' unless $config{FASTACCEPT}; + + run_user_exit $stdchainref; + + my $ruleschainref = $filter6_table->{"${zone}2${zone2}"}; + my ( $policy, $loglevel, $defaultaction ) = ( $default , 6, $config{$default . '_DEFAULT'} ); + my $policychainref; + + $policychainref = $filter6_table->{$ruleschainref->{policychain}} if $ruleschainref; + + ( $policy, $loglevel, $defaultaction ) = @{$policychainref}{'policy', 'loglevel', 'default' } if $policychainref; + + policy_rules $stdchainref , $policy , $loglevel, $defaultaction, 0; +} + # # Create and populate the synflood chains corresponding to entries in /etc/shorewall/policy # @@ -478,4 +835,21 @@ sub setup_syn_flood_chains() { } } +# +# Create and populate the synflood chains corresponding to entries in /etc/shorewall/policy +# +sub setup_syn_flood_6chains() { + for my $chainref ( @policy_6chains ) { + my $limit = $chainref->{synparams}; + if ( $limit && ! $filter6_table->{syn_flood_chain $chainref} ) { + my $level = $chainref->{loglevel}; + my $synchainref = new_6chain 'filter' , syn_flood_chain $chainref; + add_rule $synchainref , "${limit}-j RETURN"; + log_rule_limit $level , $synchainref , $chainref->{name} , 'DROP', '-m limit --limit 5/min --limit-burst 5 ' , '' , 'add' , '' + if $level ne ''; + add_rule $synchainref, '-j DROP'; + } + } +} + 1; diff --git a/Shorewall-perl/Shorewall/Rules.pm b/Shorewall-perl/Shorewall/Rules.pm index 0b5622a65..861f0d8d7 100644 --- a/Shorewall-perl/Shorewall/Rules.pm +++ b/Shorewall-perl/Shorewall/Rules.pm @@ -52,6 +52,7 @@ our $VERSION = 4.1.5; # Set to one if we find a SECTION # our $sectioned; +our $sectioned6; our $macro_nest_level; our $current_param; our @param_stack; @@ -74,6 +75,7 @@ my %rules_commands = ( COMMENT => 0, sub initialize() { $sectioned = 0; + $sectioned6 = 0; $macro_nest_level = 0; $current_param = ''; @param_stack = (); @@ -386,6 +388,125 @@ sub process_criticalhosts() { \@critical; } +sub setup_6blacklist() { + + my $hosts = find_hosts_by_option '6blacklist'; + my $chainref; + my ( $level, $disposition ) = @config{'BLACKLIST_LOGLEVEL', 'BLACKLIST_DISPOSITION' }; + my $target = $disposition eq 'REJECT' ? 'reject' : $disposition; + + if ( @$hosts ) { + $chainref = new_standard_6chain 'blacklst'; + + if ( defined $level && $level ne '' ) { + my $logchainref = new_standard_6chain 'blacklog'; + + log_rule_limit( $level , $logchainref , 'blacklst' , $disposition , "$globals{LOGLIMIT}" , '', 'add', '' ); + + add_rule $logchainref, "-j $target" ; + + $target = 'blacklog'; + } + } + + BLACKLIST: + { + if ( my $fn = open_file '6blacklist' ) { + + my $first_entry = 1; + + first_entry "$doing $fn..."; + + while ( read_a_line ) { + + if ( $first_entry ) { + unless ( @$hosts ) { + warning_message q(The entries in $fn have been ignored because there are no 'blacklist' interfaces); + close_file; + last BLACKLIST; + } + + $first_entry = 0; + } + + my ( $networks, $protocol, $ports ) = split_line 1, 3, 'blacklist file'; + + expand_rule( + $chainref , + NO_RESTRICT , + do_proto6( $protocol , $ports, '' ) , + $networks , + '' , + '' , + '' , + "-j $target" , + '' , + $disposition , + '' ); + + progress_message " \"$currentline\" added to blacklist"; + } + } + + my $state = $config{BLACKLISTNEWONLY} ? '-m state --state NEW,INVALID ' : ''; + + for my $hostref ( @$hosts ) { + my $interface = $hostref->[0]; + my $ipsec = $hostref->[1]; + my $policy = $capabilities{POLICY_MATCH} ? "-m policy --pol $ipsec --dir in " : ''; + my $network = $hostref->[2]; + my $source = match_source_6net $network; + + for my $chain ( first_chains $interface ) { + add_rule $filter_table->{$chain} , "${source}${state}${policy}-j blacklst"; + } + + progress_message " Blacklisting enabled on ${interface};${network}"; + } + } +} + +sub process_critical6hosts() { + + my @critical = (); + + my $fn = open_file '6routestopped'; + + first_entry "$doing $fn for critical IPv6 hosts..."; + + while ( read_a_line ) { + + my $routeback = 0; + + my ($interface, $hosts, $options ) = split_line 1, 3, 'routestopped file'; + + fatal_error "Unknown interface ($interface)" unless known_6interface $interface; + + $hosts = ALLIPv6 unless $hosts ne '-'; + + my @hosts; + + for my $host ( split_list $hosts, 'host' ) { + validate_host $host, 1; + push @hosts, "$interface;$host"; + } + + unless ( $options eq '-' ) { + for my $option (split_list $options, 'option' ) { + unless ( $option eq 'routeback' || $option eq 'source' || $option eq 'dest' ) { + if ( $option eq 'critical' ) { + push @critical, @hosts; + } else { + warning_message "Unknown routestopped option ( $option ) ignored"; + } + } + } + } + } + + \@critical; +} + sub process_routestopped() { my ( @allhosts, %source, %dest ); @@ -480,6 +601,100 @@ sub process_routestopped() { } } +sub process_6routestopped() { + + my ( @allhosts, %source, %dest ); + + my $fn = open_file '6routestopped'; + + first_entry "$doing $fn..."; + + while ( read_a_line ) { + + my $routeback = 0; + + my ($interface, $hosts, $options ) = split_line 1, 3, '6routestopped file'; + + fatal_error "Unknown interface ($interface)" unless known_6interface $interface; + + $hosts = ALLIPv6 unless $hosts && $hosts ne '-'; + + my @hosts; + + for my $host ( split /,/, $hosts ) { + validate_6host $host, 1; + push @hosts, "$interface;$host"; + } + + unless ( $options eq '-' ) { + for my $option (split /,/, $options ) { + if ( $option eq 'routeback' ) { + if ( $routeback ) { + warning_message "Duplicate 'routeback' option ignored"; + } else { + $routeback = 1; + + for my $host ( split /,/, $hosts ) { + my $source = match_source_6net $host; + my $dest = match_dest_6net $host; + + emit "run_ip6tables -A FORWARD -i $interface -o $interface $source $dest -j ACCEPT"; + clearrule; + } + } + } elsif ( $option eq 'source' ) { + for my $host ( split /,/, $hosts ) { + $source{"$interface;$host"} = 1; + } + } elsif ( $option eq 'dest' ) { + for my $host ( split /,/, $hosts ) { + $dest{"$interface;$host"} = 1; + } + } else { + warning_message "Unknown 6routestopped option ( $option ) ignored" unless $option eq 'critical'; + } + } + } + + push @allhosts, @hosts; + } + + for my $host ( @allhosts ) { + my ( $interface, $h ) = split /;/, $host; + my $source = match_source_6net $h; + my $dest = match_dest_6net $h; + my $sourcei = match_source_6dev $interface; + my $desti = match_dest_6dev $interface; + + emit "\$IP6TABLES -A INPUT $sourcei $source -j ACCEPT"; + emit "\$IP6TABLES -A OUTPUT $desti $dest -j ACCEPT" unless $config{ADMINISABSENTMINDED}; + + my $matched = 0; + + if ( $source{$host} ) { + emit "\$IP6TABLES -A FORWARD $sourcei $source -j ACCEPT"; + $matched = 1; + } + + if ( $dest{$host} ) { + emit "\$IP6TABLES -A FORWARD $desti $dest -j ACCEPT"; + $matched = 1; + } + + unless ( $matched ) { + for my $host1 ( @allhosts ) { + unless ( $host eq $host1 ) { + my ( $interface1, $h1 ) = split /;/, $host1; + my $dest1 = match_dest_6net $h1; + my $desti1 = match_dest_6dev $interface1; + emit "\$IP6TABLES -A FORWARD $sourcei $desti1 $source $dest1 -j ACCEPT"; + clearrule; + } + } + } + } +} + sub setup_mss(); sub add_common_rules() { @@ -1447,6 +1662,642 @@ sub process_rules() { $section = 'DONE'; } +sub process_6rule1 ( $$$$$$$$$$$$ ); + +# +# Expand a macro rule from the rules file +# +sub process_6macro ( $$$$$$$$$$$$$$ ) { + my ($macro, $target, $param, $source, $dest, $proto, $ports, $sports, $rate, $user, $mark, $connlimit, $time, $wildcard ) = @_; + + my $nocomment = no_comment; + + my $format = 1; + + macro_comment $macro; + + my $macrofile = $macros{$macro}; + + progress_message "..Expanding Macro $macrofile..."; + + push_open $macrofile; + + while ( read_a_line ) { + + my ( $mtarget, $msource, $mdest, $mproto, $mports, $msports, $morigdest, $mrate, $muser ); + + if ( $format == 1 ) { + ( $mtarget, $msource, $mdest, $mproto, $mports, $msports, $mrate, $muser, $morigdest ) = split_line1 1, 9, 'macro file', $macro_commands; + } else { + ( $mtarget, $msource, $mdest, $mproto, $mports, $msports, $morigdest, $mrate, $muser ) = split_line1 1, 9, 'macro file', $macro_commands; + } + + if ( $mtarget eq 'COMMENT' ) { + process_comment unless $nocomment; + next; + } + + if ( $mtarget eq 'FORMAT' ) { + fatal_error "Invalid FORMAT ($msource)" unless $msource =~ /^[12]$/; + $format = $msource; + next; + } + + if ( $morigdest ne '-' ) { + fatal_error "Invalid macro file entry (too many columns)" if $format == 1; + fatal_error "A macro with ORIGINAL DEST cannot be used with IPv6"; + } + + $mtarget = merge_levels $target, $mtarget; + + if ( $mtarget =~ /^PARAM(:.*)?$/ ) { + fatal_error 'PARAM requires a parameter to be supplied in macro invocation' unless $param ne ''; + $mtarget = substitute_param $param, $mtarget; + } + + my $action = isolate_basic_target $mtarget; + + fatal_error "Invalid or missing ACTION ($mtarget)" unless defined $action; + + my $actiontype = $targets6{$action} || find_6macro( $action ); + + fatal_error "Invalid Action ($mtarget) in macro" unless $actiontype & ( ACTION + STANDARD + NATRULE + MACRO ); + + if ( $msource ) { + if ( $msource eq '-' ) { + $msource = $source || ''; + } elsif ( $msource =~ s/^DEST:?// ) { + $msource = merge_6macro_source_dest $msource, $dest; + } else { + $msource =~ s/^SOURCE:?//; + $msource = merge_6macro_source_dest $msource, $source; + } + } else { + $msource = ''; + } + + if ( $mdest ) { + if ( $mdest eq '-' ) { + $mdest = $dest || ''; + } elsif ( $mdest =~ s/^SOURCE:?// ) { + $mdest = merge_6macro_source_dest $mdest , $source; + } else { + $mdest =~ s/DEST:?//; + $mdest = merge_6macro_source_dest $mdest, $dest; + } + } else { + $mdest = ''; + } + + process_6rule1( + $mtarget, + $msource, + $mdest, + merge_macro_column( $mproto, $proto ) , + merge_macro_column( $mports, $ports ) , + merge_macro_column( $msports, $sports ) , + merge_macro_column( $mrate, $rate ) , + merge_macro_column( $muser, $user ) , + $mark, + $connlimit, + $time, + $wildcard + ); + + progress_message " IPv6 Rule \"$currentline\" $done"; + } + + pop_open; + + progress_message "..End Macro $macrofile"; + + clear_comment unless $nocomment; + +} +# +# Once a rule has been expanded via wildcards (source and/or dest zone == 'all'), it is processed by this function. If +# the target is a macro, the macro is expanded and this function is called recursively for each rule in the expansion. +# +sub process_rule1 ( $$$$$$$$$$$$$ ) { + my ( $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time, $wildcard ) = @_; + my ( $action, $loglevel) = split_action $target; + my ( $basictarget, $param ) = get_target_param $action; + my $rule = ''; + my $actionchainref; + my $optimize = $wildcard ? ( $basictarget =~ /!$/ ? 0 : $config{OPTIMIZE} ) : 0; + + unless ( defined $param ) { + ( $basictarget, $param ) = ( $1, $2 ) if $action =~ /^(\w+)[(](.*)[)]$/; + } + + $param = '' unless defined $param; + + # + # Determine the validity of the action + # + my $actiontype = $targets{$basictarget} || find_macro( $basictarget ); + + fatal_error "Unknown action ($action)" unless $actiontype; + + if ( $actiontype == MACRO ) { + # + # process_macro() will call process_rule1() recursively for each rule in the macro body + # + fatal_error "Macro invocations nested too deeply" if ++$macro_nest_level > MAX_MACRO_NEST_LEVEL; + + if ( $param ne '' ) { + push @param_stack, $current_param; + $current_param = $param; + } + + process_macro( $basictarget, + $target , + $current_param, + $source, + $dest, + $proto, + $ports, + $sports, + $origdest, + $ratelimit, + $user, + $mark, + $connlimit, + $time, + $wildcard ); + + $macro_nest_level--; + + $current_param = pop @param_stack if $param ne ''; + + return; + + } elsif ( $actiontype & NFQ ) { + require_capability( 'NFQUEUE_TARGET', 'NFQUEUE Rules', '' ); + my $paramval = $param eq '' ? 0 : numeric_value( $param ); + fatal_error "Invalid value ($param) for NFQUEUE queue number" unless defined($paramval) && $paramval <= 65535; + $action = "NFQUEUE --queue-num $paramval"; + } else { + fatal_error "The $basictarget TARGET does not accept a parameter" unless $param eq ''; + } + # + # We can now dispense with the postfix character + # + $action =~ s/[\+\-!]$//; + # + # Mark target as used + # + if ( $actiontype & ACTION ) { + unless ( $usedactions{$target} ) { + $usedactions{$target} = 1; + createactionchain $target; + } + } + # + # Take care of irregular syntax and targets + # + if ( $actiontype & REDIRECT ) { + my $z = $actiontype & NATONLY ? '' : firewall_zone; + if ( $dest eq '-' ) { + $dest = join( '', $z, '::' , $ports =~ /[:,]/ ? '' : $ports ); + } else { + $dest = join( '', $z, '::', $dest ) unless $dest =~ /:/; + } + } elsif ( $action eq 'REJECT' ) { + $action = 'reject'; + } elsif ( $action eq 'CONTINUE' ) { + $action = 'RETURN'; + } elsif ( $actiontype & LOGRULE ) { + fatal_error 'LOG requires a log level' unless defined $loglevel and $loglevel ne ''; + } + # + # Isolate and validate source and destination zones + # + my $sourcezone; + my $destzone; + my $sourceref; + my $destref; + my $origdstports; + + if ( $source =~ /^(.+?):(.*)/ ) { + fatal_error "Missing SOURCE Qualifier ($source)" if $2 eq ''; + $sourcezone = $1; + $source = $2; + } else { + $sourcezone = $source; + $source = ALLIPv4; + } + + if ( $dest =~ /^(.*?):(.*)/ ) { + fatal_error "Missing DEST Qualifier ($dest)" if $2 eq ''; + $destzone = $1; + $dest = $2; + } else { + $destzone = $dest; + $dest = ALLIPv4; + } + + fatal_error "Missing source zone" if $sourcezone eq '-' || $sourcezone =~ /^:/; + fatal_error "Unknown source zone ($sourcezone)" unless $sourceref = defined_zone( $sourcezone ); + + if ( $actiontype & NATONLY ) { + warning_message "Destination zone ($destzone) ignored" unless $destzone eq '-' || $destzone eq ''; + } else { + fatal_error "Missing destination zone" if $destzone eq '-' || $destzone eq ''; + fatal_error "Unknown destination zone ($destzone)" unless $destref = defined_zone( $destzone ); + } + + my $restriction = NO_RESTRICT; + + if ( $sourcezone eq firewall_zone ) { + $restriction = $destzone eq firewall_zone ? ALL_RESTRICT : OUTPUT_RESTRICT; + } else { + $restriction = INPUT_RESTRICT if $destzone eq firewall_zone; + } + + my ( $chain, $chainref, $policy ); + # + # For compatibility with older Shorewall versions + # + $origdest = ALLIPv4 if $origdest eq 'all'; + + # + # Take care of chain + # + + unless ( $actiontype & NATONLY ) { + # + # Check for illegal bridge port rule + # + if ( $destref->{type} eq 'bport4' ) { + unless ( $sourceref->{bridge} eq $destref->{bridge} || single_interface( $sourcezone ) eq $destref->{bridge} ) { + return 1 if $wildcard; + fatal_error "Rules with a DESTINATION Bridge Port zone must have a SOURCE zone on the same bridge"; + } + } + + $chain = "${sourcezone}2${destzone}"; + $chainref = ensure_chain 'filter', $chain; + $policy = $chainref->{policy}; + + if ( $policy eq 'NONE' ) { + return 1 if $wildcard; + fatal_error "Rules may not override a NONE policy"; + } + # + # Handle Optimization + # + if ( $optimize > 0 ) { + my $loglevel = $filter_table->{$chainref->{policychain}}{loglevel}; + if ( $loglevel ne '' ) { + return 1 if $target eq "${policy}:$loglevel}"; + } else { + return 1 if $basictarget eq $policy; + } + } + # + # Mark the chain as referenced and add appropriate rules from earlier sections. + # + $chainref = ensure_filter_chain $chain, 1; + } + + # + # Generate Fixed part of the rule + # + $rule = join( '', do_proto($proto, $ports, $sports), do_ratelimit( $ratelimit, $basictarget ) , do_user( $user ) , do_test( $mark , 0xFF ) , do_connlimit( $connlimit ), do_time( $time ) ); + + unless ( $section eq 'NEW' ) { + fatal_error "Entries in the $section SECTION of the rules file not permitted with FASTACCEPT=Yes" if $config{FASTACCEPT}; + fatal_error "$basictarget rules are not allowed in the $section SECTION" if $actiontype & ( NATRULE | NONAT ); + $rule .= "-m state --state $section " + } + + # + # Generate NAT rule(s), if any + # + if ( $actiontype & NATRULE ) { + my ( $server, $serverport ); + my $randomize = $dest =~ s/:random$// ? '--random ' : ''; + + require_capability( 'NAT_ENABLED' , "$basictarget rules", '' ); + # + # Isolate server port + # + if ( $dest =~ /^(.*)(:(.+))$/ ) { + # + # Server IP and Port + # + $server = $1; # May be empty + $serverport = $3; # Not Empty due to RE + $origdstports = $ports; + if ( $serverport =~ /^(\d+)-(\d+)$/ ) { + # + # Server Port Range + # + fatal_error "Invalid port range ($serverport)" unless $1 < $2; + my @ports = ( $1, $2 ); + $_ = validate_port( proto_name( $proto ), $_) for ( @ports ); + ( $ports = $serverport ) =~ tr/-/:/; + } else { + $serverport = $ports = validate_port( proto_name( $proto ), $serverport ); + } + } elsif ( $dest eq ':' ) { + # + # Rule with no server IP or port ( zone:: ) + # + $server = $serverport = ''; + } else { + # + # Simple server IP address (may be empty or "-") + # + $server = $dest; + $serverport = ''; + } + + # + # Generate the target + # + my $target = ''; + + if ( $actiontype & REDIRECT ) { + fatal_error "A server IP address may not be specified in a REDIRECT rule" if $server; + $target = '-j REDIRECT '; + $target .= "--to-port $serverport " if $serverport; + if ( $origdest eq '' || $origdest eq '-' ) { + $origdest = ALLIPv4; + } elsif ( $origdest eq 'detect' ) { + if ( $config{DETECT_DNAT_IPADDRS} && $sourcezone ne firewall_zone ) { + my $interfacesref = $sourceref->{interfaces}; + my @interfaces = keys %$interfacesref; + $origdest = @interfaces ? "detect:@interfaces" : ALLIPv4; + } else { + $origdest = ALLIPv4; + } + } + } else { + fatal_error "A server must be specified in the DEST column in $action rules" if $server eq ''; + + if ( $server =~ /^(.+)-(.+)$/ ) { + validate_range( $1, $2 ); + } else { + $server = validate_address $server, 1; + } + + if ( $action eq 'SAME' ) { + fatal_error 'Port mapping not allowed in SAME rules' if $serverport; + fatal_error 'SAME not allowed with SOURCE=$FW' if $sourcezone eq firewall_zone; + fatal_error "':random' is not supported by the SAME target" if $randomize; + warning_message 'Netfilter support for SAME is being dropped in early 2008'; + $target = '-j SAME '; + for my $serv ( split /,/, $server ) { + $target .= "--to $serv "; + } + } elsif ( $action eq 'DNAT' ) { + $target = '-j DNAT '; + $serverport = ":$serverport" if $serverport; + for my $serv ( split /,/, $server ) { + $target .= "--to-destination ${serv}${serverport} "; + } + } + + unless ( $origdest && $origdest ne '-' && $origdest ne 'detect' ) { + if ( $config{DETECT_DNAT_IPADDRS} && $sourcezone ne firewall_zone ) { + my $interfacesref = $sourceref->{interfaces}; + my @interfaces = keys %$interfacesref; + $origdest = @interfaces ? "detect:@interfaces" : ALLIPv4; + } else { + $origdest = ALLIPv4; + } + } + } + + $target .= $randomize; + + # + # And generate the nat table rule(s) + # + expand_rule ( ensure_chain ('nat' , $sourceref->{type} eq 'firewall' ? 'OUTPUT' : dnat_chain $sourcezone ), + PREROUTE_RESTRICT , + $rule , + $source , + $origdest , + '' , + '' , + $target , + $loglevel , + $action , + $serverport ? do_proto( $proto, '', '' ) : '' ); + # + # After NAT: + # - the destination port will be the server port ($ports) -- we did that above + # - the destination IP will be the server IP ($dest) + # - there will be no log level (we log NAT rules in the nat table rather than in the filter table). + # - the target will be ACCEPT. + # + unless ( $actiontype & NATONLY ) { + $rule = join( '', do_proto( $proto, $ports, $sports ), do_ratelimit( $ratelimit, 'ACCEPT' ), do_user $user , do_test( $mark , 0xFF ) ); + $loglevel = ''; + $dest = $server; + $action = 'ACCEPT'; + } + } elsif ( $actiontype & NONAT ) { + # + # NONAT or ACCEPT+ -- May not specify a destination interface + # + fatal_error "Invalid DEST ($dest) in $action rule" if $dest =~ /:/; + + $origdest = '' unless $origdest and $origdest ne '-'; + + if ( $origdest eq 'detect' ) { + my $interfacesref = $sourceref->{interfaces}; + my $interfaces = "@$interfacesref"; + $origdest = $interfaces ? "detect:$interfaces" : ALLIPv4; + } + + expand_rule( ensure_chain ('nat' , $sourceref->{type} eq 'firewall' ? 'OUTPUT' : dnat_chain $sourcezone) , + PREROUTE_RESTRICT , + $rule , + $source , + $dest , + $origdest , + '', + '-j RETURN ' , + $loglevel , + $action , + '' ); + } + + # + # Add filter table rule, unless this is a NATONLY rule type + # + unless ( $actiontype & NATONLY ) { + + if ( $actiontype & ACTION ) { + $action = (find_logactionchain $target)->{name}; + $loglevel = ''; + } + + unless ( $origdest eq '-' ) { + require_capability( 'CONNTRACK_MATCH', 'ORIGINAL DEST in a non-NAT rule', 's' ) unless $actiontype & NATRULE; + } else { + $origdest = ''; + } + + expand_rule( ensure_chain( 'filter', $chain ) , + $restriction , + $rule , + $source , + $dest , + $origdest , + $origdstports , + "-j $action " , + $loglevel , + $action , + '' ); + } +} + +# +# Process a Record in the rules file +# +# Deals with the ugliness of wildcard zones ('all' in SOURCE and/or DEST column). +# +sub process_rule ( $$$$$$$$$$$$ ) { + my ( $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit , $time ) = @_; + my $intrazone = 0; + my $includesrcfw = 1; + my $includedstfw = 1; + my $thisline = $currentline; + # + # Section Names are optional so once we get to an actual rule, we need to be sure that + # we close off any missing sections. + # + unless ( $sectioned ) { + finish_section 'ESTABLISHED,RELATED'; + $sections{$section = 'NEW'} = 1; + $sectioned = 1; + } + + # + # Handle Wildcards + # + if ( $source =~ /^all[-+]/ ) { + if ( $source eq 'all+' ) { + $source = 'all'; + $intrazone = 1; + } elsif ( ( $source eq 'all+-' ) || ( $source eq 'all-+' ) ) { + $source = 'all'; + $intrazone = 1; + $includesrcfw = 0; + } elsif ( $source eq 'all-' ) { + $source = 'all'; + $includesrcfw = 0; + } else { + fatal_error "Invalid SOURCE ($source)"; + } + } + + if ( $dest =~ /^all[-+]/ ) { + if ( $dest eq 'all+' ) { + $dest = 'all'; + $intrazone = 1; + } elsif ( ( $dest eq 'all+-' ) || ( $dest eq 'all-+' ) ) { + $dest = 'all'; + $intrazone = 1; + $includedstfw = 0; + } elsif ( $dest eq 'all-' ) { + $dest = 'all'; + $includedstfw = 0; + } else { + fatal_error "Invalid DEST ($dest)"; + } + + } + + my $action = isolate_basic_target $target; + + fatal_error "Invalid or missing ACTION ($target)" unless defined $action; + + if ( $source eq 'all' ) { + for my $zone ( all_zones ) { + if ( $includesrcfw || ( zone_type( $zone ) ne 'firewall' ) ) { + if ( $dest eq 'all' ) { + for my $zone1 ( all_zones ) { + if ( $includedstfw || ( zone_type( $zone1 ) ne 'firewall' ) ) { + if ( $intrazone || ( $zone ne $zone1 ) ) { + process_rule1 $target, $zone, $zone1 , $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time, 1; + } + } + } + } else { + my $destzone = (split( /:/, $dest, 2 ) )[0]; + $destzone = firewall_zone unless defined_zone( $destzone ); # We do this to allow 'REDIRECT all ...'; process_rule1 will catch the case where the dest zone is invalid + if ( $intrazone || ( $zone ne $destzone ) ) { + process_rule1 $target, $zone, $dest , $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time, 1; + } + } + } + } + } elsif ( $dest eq 'all' ) { + for my $zone ( all_zones ) { + my $sourcezone = ( split( /:/, $source, 2 ) )[0]; + if ( ( $includedstfw || ( zone_type( $zone ) ne 'firewall') ) && ( ( $sourcezone ne $zone ) || $intrazone) ) { + process_rule1 $target, $source, $zone , $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time, 1; + } + } + } else { + process_rule1 $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time, 0; + } + + progress_message " Rule \"$thisline\" $done"; +} + +# +# Process the Rules File +# +sub process_rules() { + + my $fn = open_file 'rules'; + + first_entry "$doing $fn..."; + + while ( read_a_line ) { + + my ( $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time ) = split_line1 1, 12, 'rules file', \%rules_commands; + + if ( $target eq 'COMMENT' ) { + process_comment; + } elsif ( $target eq 'SECTION' ) { + # + # read_a_line has already verified that there are exactly two tokens on the line + # + fatal_error "Invalid SECTION ($source)" unless defined $sections{$source}; + fatal_error "Duplicate or out of order SECTION $source" if $sections{$source}; + $sectioned = 1; + $sections{$source} = 1; + + if ( $source eq 'RELATED' ) { + $sections{ESTABLISHED} = 1; + finish_section 'ESTABLISHED'; + } elsif ( $source eq 'NEW' ) { + @sections{'ESTABLISHED','RELATED'} = ( 1, 1 ); + finish_section ( ( $section eq 'RELATED' ) ? 'RELATED' : 'ESTABLISHED,RELATED' ); + } + + $section = $source; + } else { + if ( "\L$source" =~ /^none(:.*)?$/ || "\L$dest" =~ /^none(:.*)?$/ ) { + progress_message "Rule \"$currentline\" ignored." + } else { + process_rule $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time; + } + } + } + + clear_comment; + $section = 'DONE'; +} + # # To quote an old comment, "generate_matrix makes a sow's ear out of a silk purse". # diff --git a/Shorewall-perl/Shorewall/Zones.pm b/Shorewall-perl/Shorewall/Zones.pm index ea4a8f902..b607cc3ea 100644 --- a/Shorewall-perl/Shorewall/Zones.pm +++ b/Shorewall-perl/Shorewall/Zones.pm @@ -44,28 +44,44 @@ our @EXPORT = qw( NOTHING firewall_zone defined_zone zone_type + zone_family all_zones + all_6zones complex_zones non_firewall_zones + non_firewall_6zones single_interface validate_interfaces_file + validate_6interfaces_file all_interfaces + all_6interfaces interface_number find_interface + find_6interface known_interface + known_6interface have_bridges + have_6bridges port_to_bridge + port_to_6bridge source_port_to_bridge + source_port_to_6bridge interface_is_optional + interface6_is_optional find_interfaces_by_option + find_6interfaces_by_option get_interface_option + get_6interface_option set_interface_option + set_6interface_option validate_hosts_file + validate_6hosts_file find_hosts_by_option + find_6hosts_by_option ); our @EXPORT_OK = qw( initialize ); -our $VERSION = 4.1.5; +our $VERSION = 4.3.0; # # IPSEC Option types @@ -82,7 +98,7 @@ use constant { NOTHING => 'NOTHING', # # @zones contains the ordered list of zones with sub-zones appearing before their parents. # -# %zones{ => {type = > 'firewall', 'ipv4', 'ipsec4', 'bport4'; +# %zones{ => {type = > 'firewall', 'ipv4', 'ipsec4', 'bport4', 'ipv6', 'ipsec6', 'bport6'; # options => { complex => 0|1 # nested => 0|1 # in_out => < policy match string > @@ -93,6 +109,7 @@ use constant { NOTHING => 'NOTHING', # children => [ ] # interfaces => [ ] # bridge => +# family => 1 = IPv4, 2 = IPv6, 3 = firewall # hosts { } => [ { => { ipsec => 'ipsec'|'none' # options => { => # ... @@ -120,14 +137,15 @@ our %reservedName = ( all => 1, # # Interface Table. # -# @interfaces lists the interface names in the order that they appear in the interfaces file. +# @interfaces lists the interface names in the order that they appear in the interfaces file. +# @interfaces6 lists the interface names in the order that they appear in the interfaces6 file. # # %interfaces { => { name => # root => # options => { = , # ... # } -# zone4 => +# zone => # nets => # bridge => # broadcasts => 'none', 'detect' or [ , , ... ] @@ -135,9 +153,25 @@ our %reservedName = ( all => 1, # } # } # +# %interfaces6 { => { name => +# root => +# options => { = , +# ... +# } +# zone => +# nets => +# bridge => +# broadcasts => 'none', 'detect' or [ , , ... ] +# number => +# } +# } +# our @interfaces; our %interfaces; +our @interfaces6; +our %interfaces6; our @bport_zones; +our @bport_6zones; # # Initialize globals -- we take this novel approach to globals initialization to allow @@ -156,6 +190,9 @@ sub initialize() { @interfaces = (); %interfaces = (); @bport_zones = (); + @interfaces6 = (); + %interfaces6 = (); + @bport_6zones = (); } INIT { @@ -219,7 +256,7 @@ sub parse_zone_option_list($$) if ( $key{$e} ) { $h{$e} = $val; } else { - fatal_error "The \"$e\" option may only be specified for ipsec zones" unless $zonetype eq 'ipsec4'; + fatal_error "The \"$e\" option may only be specified for ipsec zones" unless $zonetype =~ /^ipsec/; $options .= $invert; $options .= "--$e "; $options .= "$val "if defined $val; @@ -240,6 +277,7 @@ sub determine_zones() my @z; my $ipv4 = 0; + my $ipv6 = 0; my $fn = open_file 'zones'; @@ -262,28 +300,43 @@ sub determine_zones() push @{$zones{$p}{children}}, $zone; } } - + fatal_error "Invalid zone name ($zone)" unless "\L$zone" =~ /^[a-z]\w*$/ && length $zone <= $globals{MAXZONENAMELENGTH}; fatal_error "Invalid zone name ($zone)" if $reservedName{$zone} || $zone =~ /^all2|2all$/; fatal_error( "Duplicate zone name ($zone)" ) if $zones{$zone}; $type = "ipv4" unless $type; + my $family = F_INET; + if ( $type =~ /ipv4/i ) { $type = 'ipv4'; $ipv4 = 1; + } elsif ( $type =~ /ipv6/i ) { + $type = 'ipv6'; + $ipv6 = 1; + $family = F_INET6; } elsif ( $type =~ /^ipsec4?$/i ) { $type = 'ipsec4'; + } elsif ( $type =~ /^ipsec6?$/i ) { + $type = 'ipsec6'; + $family = F_INET6; } elsif ( $type =~ /^bport4?$/i ) { warning_message "Bridge Port zones should have a parent zone" unless @parents; $type = 'bport4'; push @bport_zones, $zone; + } elsif ( $type =~ /^bport6?$/i ) { + warning_message "Bridge Port zones should have a parent zone" unless @parents; + $type = 'bport6'; + $family = F_INET6; + push @bport_6zones, $zone; } elsif ( $type eq 'firewall' ) { fatal_error 'Firewall zone may not be nested' if @parents; fatal_error "Only one firewall zone may be defined ($zone)" if $firewall_zone; $firewall_zone = $zone; $ENV{FW} = $zone; $type = "firewall"; + $family = F_INET | F_INET6; } elsif ( $type eq '-' ) { $type = 'ipv4'; $ipv4 = 1; @@ -291,6 +344,10 @@ sub determine_zones() fatal_error "Invalid zone type ($type)" ; } + for ( @parents ) { + fatal_error "Incompatible Parent/Child Zones Types ($_)" unless $zones{$_}{family} == $family + } + for ( $options, $in_options, $out_options ) { $_ = '' if $_ eq '-'; } @@ -299,10 +356,11 @@ sub determine_zones() parents => \@parents, exclusions => [], bridge => '', + family => $family, options => { in_out => parse_zone_option_list( $options || '', $type ) , in => parse_zone_option_list( $in_options || '', $type ) , out => parse_zone_option_list( $out_options || '', $type ) , - complex => ($type eq 'ipsec4' || $options || $in_options || $out_options ? 1 : 0) , + complex => ($type =~ /^ipsec/ || $options || $in_options || $out_options ? 1 : 0) , nested => @parents > 0 } , interfaces => {} , children => [] , @@ -311,8 +369,8 @@ sub determine_zones() push @z, $zone; } - fatal_error "No firewall zone defined" unless $firewall_zone; - fatal_error "No IPv4 zones defined" unless $ipv4; + fatal_error "No firewall zone defined" unless $firewall_zone; + fatal_error "No IPv4 or IPv6 zones defined" unless $ipv4 || $ipv6; my %ordered; @@ -336,7 +394,7 @@ sub determine_zones() } # -# Return true of we have any ipsec zones +# Return true of we have any ipse4c zones # sub haveipseczones() { for my $zoneref ( values %zones ) { @@ -346,6 +404,17 @@ sub haveipseczones() { 0; } +# +# Return true of we have any ipse4c zones +# +sub haveipsec6zones() { + for my $zoneref ( values %zones ) { + return 1 if $zoneref->{type} eq 'ipsec6'; + } + + 0; +} + # # Report about zones. # @@ -374,7 +443,7 @@ sub zone_report() my $hosts = $groupref->{hosts}; if ( $hosts ) { my $grouplist = join ',', ( @$hosts ); - progress_message " $interface:$grouplist"; + progress_message " $interface $grouplist"; $printed = 1; } } @@ -384,7 +453,7 @@ sub zone_report() } unless ( $printed ) { - fatal_error "No bridge has been associated with zone $zone" if $type eq 'bport4' && ! $zoneref->{bridge}; + fatal_error "No bridge has been associated with zone $zone" if $type =~ /^bport/ && ! $zoneref->{bridge}; warning_message "*** $zone is an EMPTY ZONE ***" unless $type eq 'firewall'; } @@ -402,7 +471,7 @@ sub dump_zone_contents() my $exclusions = $zoneref->{exclusions}; my $entry = "$zone $type"; - $entry .= ":$zoneref->{bridge}" if $type eq 'bport4'; + $entry .= ":$zoneref->{bridge}" if $type =~ /^bport/; if ( $hostref ) { for my $type ( sort keys %$hostref ) { @@ -414,7 +483,7 @@ sub dump_zone_contents() my $hosts = $groupref->{hosts}; if ( $hosts ) { my $grouplist = join ',', ( @$hosts ); - $entry .= " $interface:$grouplist"; + $entry .= " $interface\($grouplist\)"; } } } @@ -455,7 +524,7 @@ sub add_group_to_zone($$$$$) my $arrayref; my $zoneref = $zones{$zone}; my $zonetype = $zoneref->{type}; - my $ifacezone = $interfaces{$interface}{zone4}; + my $ifacezone = $interfaces{$interface}{zone}; $zoneref->{interfaces}{$interface} = 1; @@ -481,7 +550,7 @@ sub add_group_to_zone($$$$$) unless ( $switched ) { if ( $type eq $zonetype ) { fatal_error "Duplicate Host Group ($interface:$host) in zone $zone" if $ifacezone eq $zone; - $ifacezone = $zone if $host eq ALLIPv4; + $ifacezone = $zone if $host eq ALLIPv4 || $host eq ALLIPv6; } } @@ -506,7 +575,7 @@ sub add_group_to_zone($$$$$) push @{$arrayref}, { options => $options, hosts => \@newnetworks, - ipsec => $type eq 'ipsec4' ? 'ipsec' : 'none' }; + ipsec => $type =~ /^ipsec/ ? 'ipsec' : 'none' }; } # @@ -527,20 +596,36 @@ sub zone_type( $ ) { find_zone( $_[0] )->{type}; } +sub zone_family( $ ) { + find_zone( $_[0] )->{family}; +} + sub defined_zone( $ ) { $zones{$_[0]}; } sub all_zones() { - @zones; + grep ( ! $zones{$_}{family} & F_INET , @zones ); +} + +sub all_6zones() { + grep ( ! $zones{$_}{family} & F_INET6 , @zones ); } sub non_firewall_zones() { - grep ( $zones{$_}{type} ne 'firewall' , @zones ); + grep ( $zones{$_}{family} == F_INET , @zones ); +} + +sub non_firewall_6zones() { + grep ( $zones{$_}{family} == F_INET6 , @zones ); } sub complex_zones() { - grep( $zones{$_}{options}{complex} , @zones ); + grep( $zones{$_}{options}{complex} && $zones{$_}{family} == F_INET , @zones ); +} + +sub complex_6zones() { + grep( $zones{$_}{options}{complex} && $zones{$_}{family} == F_INET6 , @zones ); } sub firewall_zone() { @@ -551,20 +636,19 @@ sub firewall_zone() { # Parse the interfaces file. # +use constant { SIMPLE_IF_OPTION => 1, + BINARY_IF_OPTION => 2, + ENUM_IF_OPTION => 3, + NUMERIC_IF_OPTION => 4, + OBSOLETE_IF_OPTION => 5, + MASK_IF_OPTION => 7, + IF_OPTION_ZONEONLY => 8 }; + sub validate_interfaces_file( $ ) { my $export = shift; my $num = 0; - use constant { SIMPLE_IF_OPTION => 1, - BINARY_IF_OPTION => 2, - ENUM_IF_OPTION => 3, - NUMERIC_IF_OPTION => 4, - OBSOLETE_IF_OPTION => 5, - MASK_IF_OPTION => 7, - - IF_OPTION_ZONEONLY => 8 }; - my %validoptions = (arp_filter => BINARY_IF_OPTION, arp_ignore => ENUM_IF_OPTION, blacklist => SIMPLE_IF_OPTION, @@ -609,6 +693,7 @@ sub validate_interfaces_file( $ ) fatal_error "Unknown zone ($zone)" unless $zoneref; fatal_error "Firewall zone not allowed in ZONE column of interface record" if $zoneref->{type} eq 'firewall'; + fatal_error "IPv6 Zones not allowed in the interfaces file ($zone}" if $zoneref->{type} =~ /6/; } $networks = '' if $networks eq '-'; @@ -752,7 +837,7 @@ sub validate_interfaces_file( $ ) add_group_to_zone( $zone, $zoneref->{type}, $interface, \@networks, $optionsref ) if $zone; - $interfaces{$interface}{zone4} = $zone; #Must follow the call to add_group_to_zone() + $interfaces{$interface}{zone} = $zone; #Must follow the call to add_group_to_zone() progress_message " Interface \"$currentline\" Validated"; @@ -782,6 +867,225 @@ sub validate_interfaces_file( $ ) fatal_error "No network interfaces defined" unless @interfaces; } +# +# Parse the interfaces file. +# + +sub validate_6interfaces_file( $ ) +{ + my $export = shift; + my $num = 0; + + my %validoptions = (blacklist => SIMPLE_IF_OPTION, + bridge => SIMPLE_IF_OPTION, + maclist => SIMPLE_IF_OPTION, + nosmurfs => SIMPLE_IF_OPTION, + optional => SIMPLE_IF_OPTION, + proxyndp => BINARY_IF_OPTION, + routeback => SIMPLE_IF_OPTION + IF_OPTION_ZONEONLY, + sourceroute => BINARY_IF_OPTION, + tcpflags => SIMPLE_IF_OPTION, + mss => NUMERIC_IF_OPTION, + ); + + my $fn = open_file '6interfaces'; + + my $first_entry = 1; + + my @ifaces; + + while ( read_a_line ) { + + if ( $first_entry ) { + progress_message2 "$doing $fn..."; + $first_entry = 0; + } + + my ($zone, $originalinterface, $networks, $options ) = split_line 2, 4, '6interfaces file'; + my $zoneref; + my $bridge = ''; + + if ( $zone eq '-' ) { + $zone = ''; + } else { + $zoneref = $zones{$zone}; + + fatal_error "Unknown zone ($zone)" unless $zoneref; + fatal_error "Firewall zone not allowed in ZONE column of interface record" if $zoneref->{type} eq 'firewall'; + fatal_error "IPv4 Zones not allowed in the 6interfaces file ($zone}" if $zoneref->{type} =~ /4/; + } + + $networks = '' if $networks eq '-'; + $options = '' if $options eq '-'; + + my ($interface, $port, $extra) = split /:/ , $originalinterface, 3; + + fatal_error "Invalid INTERFACE ($originalinterface)" if ! $interface || defined $extra; + + fatal_error "Invalid Interface Name (+)" if $interface eq '+'; + + if ( defined $port ) { + fatal_error qq("Virtual" interfaces are not supported -- see http://www.shorewall.net/Shorewall_and_Aliased_Interfaces.html) if $port =~ /^\d+$/; + require_capability( 'PHYSDEV_MATCH', 'Bridge Ports', ''); + fatal_error "Your iptables is not recent enough to support bridge ports" unless $capabilities{KLUDGEFREE}; + fatal_error "Duplicate Interface ($port)" if $interfaces{$port}; + fatal_error "$interface is not a defined bridge" unless $interfaces{$interface} && $interfaces{$interface}{options}{bridge}; + fatal_error "Bridge Ports may only be associated with 'bport' zones" if $zone && $zoneref->{type} ne 'bport4'; + + if ( $zone ) { + if ( $zoneref->{bridge} ) { + fatal_error "Bridge Port zones may only be associated with a single bridge" if $zoneref->{bridge} ne $interface; + } else { + $zoneref->{bridge} = $interface; + } + } + + fatal_error "Bridge Ports may not have options" if $options && $options ne '-'; + + next if $port eq ''; + + fatal_error "Invalid Interface Name ($interface:$port)" unless $port =~ /^[\w.@%-]+\+?$/; + + $bridge = $interface; + $interface = $port; + } else { + fatal_error "Duplicate Interface ($interface)" if $interfaces{$interface}; + fatal_error "Zones of type 'bport' may only be associated with bridge ports" if $zone && $zoneref->{type} eq 'bport4'; + $bridge = $interface; + } + + my $wildcard = 0; + my $root; + + if ( $interface =~ /\+$/ ) { + $wildcard = 1; + $root = substr( $interface, 0, -1 ); + } else { + $root = $interface; + } + + my $broadcasts; + + unless ( $networks eq '' || $networks eq 'detect' ) { + my @broadcasts = split $networks, 'address'; + + for my $address ( @broadcasts ) { + fatal_error 'Invalid BROADCAST address' unless $address =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + } + + if ( $capabilities{ADDRTYPE} ) { + warning_message 'Shorewall no longer uses broadcast addresses in rule generation when Address Type Match is available'; + } else { + $broadcasts = \@broadcasts; + } + } + + my $optionsref = {}; + + my %options; + + if ( $options ) { + + for my $option (split_list $options, 'option' ) { + next if $option eq '-'; + + ( $option, my $value ) = split /=/, $option; + + fatal_error "Invalid Interface option ($option)" unless my $type = $validoptions{$option}; + + fatal_error "The \"$option\" option may not be specified on a multi-zone interface" if $type & IF_OPTION_ZONEONLY && ! $zone; + + $type &= MASK_IF_OPTION; + + if ( $type == SIMPLE_IF_OPTION ) { + fatal_error "Option $option does not take a value" if defined $value; + $options{$option} = 1; + } elsif ( $type == BINARY_IF_OPTION ) { + $value = 1 unless defined $value; + fatal_error "Option value for $option must be 0 or 1" unless ( $value eq '0' || $value eq '1' ); + fatal_error "The $option option may not be used with a wild-card interface name" if $wildcard; + $options{$option} = $value; + } elsif ( $type == ENUM_IF_OPTION ) { + fatal_error "The $option option may not be used with a wild-card interface name" if $wildcard; + if ( $option eq 'arp_ignore' ) { + if ( defined $value ) { + if ( $value =~ /^[1-3,8]$/ ) { + $options{arp_ignore} = $value; + } else { + fatal_error "Invalid value ($value) for arp_ignore"; + } + } else { + $options{arp_ignore} = 1; + } + } else { + fatal_error "Internal Error in validate_interfaces_file"; + } + } elsif ( $type == NUMERIC_IF_OPTION ) { + fatal_error "The $option option requires a value" unless defined $value; + my $numval = numeric_value $value; + fatal_error "Invalid value ($value) for option $option" unless defined $numval; + $options{$option} = $numval; + } else { + warning_message "Support for the $option interface option has been removed from Shorewall-perl"; + } + } + + $zoneref->{options}{in_out}{routeback} = 1 if $zoneref && $options{routeback}; + + if ( $options{bridge} ) { + require_capability( 'PHYSDEV_MATCH', 'The "bridge" option', 's'); + fatal_error "Bridges may not have wildcard names" if $wildcard; + } + } elsif ( $port ) { + $options{port} = 1; + } + + $optionsref = \%options; + + $interfaces6{$interface} = { name => $interface , + bridge => $bridge , + nets => 0 , + number => ++$num , + root => $root , + broadcasts => $broadcasts , + options => $optionsref }; + + push @ifaces, $interface; + + my @networks = allipv4; + + add_group_to_zone( $zone, $zoneref->{type}, $interface, \@networks, $optionsref ) if $zone; + + $interfaces6{$interface}{zone6} = $zone; #Must follow the call to add_group_to_zone() + + progress_message " Interface \"$currentline\" Validated"; + + } + + # + # We now assemble the @interfaces6 array such that bridge ports immediately precede their associated bridge + # + for my $interface ( @ifaces ) { + my $interfaceref = $interfaces6{$interface}; + + if ( $interfaceref->{options}{bridge} ) { + my @ports = grep $interfaces{$_}{options}{port} && $interfaces{$_}{bridge} eq $interface, @ifaces; + + if ( @ports ) { + push @interfaces, @ports; + } else { + $interfaceref->{options}{routeback} = 1; #so the bridge will work properly + } + } + + push @interfaces, $interface unless $interfaceref->{options}{port}; + } + # + # Be sure that we have at least one interface + # + fatal_error "No network interfaces defined" unless @interfaces; +} + # # Returns true if passed interface matches an entry in /etc/shorewall/interfaces # @@ -809,6 +1113,33 @@ sub known_interface($) 0; } +# +# Returns true if passed interface matches an entry in /etc/shorewall/interfaces +# +# If the passed name matches a wildcard, a entry for the name is added in %interfaces to speed up validation of other references to that name. +# +sub known_6interface($) +{ + my $interface = $_[0]; + my $interfaceref = $interfaces6{$interface}; + + return $interfaceref if $interfaceref; + + for my $i ( @interfaces6 ) { + $interfaceref = $interfaces6{$i}; + my $val = $interfaceref->{root}; + next if $val eq $i; + if ( substr( $interface, 0, length $val ) eq $val ) { + # + # Cache this result for future reference. We set the 'name' to the name of the entry that appears in /etc/shorewall/interfaces. + # + return $interfaces6{$interface} = { options => $interfaceref->{options}, bridge => $interfaceref->{bridge} , name => $i , number => $interfaceref->{number} }; + } + } + + 0; +} + # # Return interface number # @@ -823,12 +1154,38 @@ sub all_interfaces() { @interfaces; } +# +# Return 6interface number +# +sub interface6_number( $ ) { + $interfaces6{$_[0]}{number} || 256; +} + +# +# Return the 6interfaces list +# +sub all_interfaces6() { + @interfaces6; +} + # # Return a reference to the interfaces table entry for an interface # sub find_interface( $ ) { my $interface = $_[0]; - my $interfaceref = $interfaces{ $interface }; + my $interfaceref = $interfaces6{ $interface }; + + fatal_error "Unknown Interface ($interface)" unless $interfaceref; + + $interfaceref; +} + +# +# Return a reference to the interfaces6 table entry for an interface +# +sub find_interface6( $ ) { + my $interface = $_[0]; + my $interfaceref = $interfaces6{ $interface }; fatal_error "Unknown Interface ($interface)" unless $interfaceref; @@ -842,6 +1199,13 @@ sub have_bridges() { @bport_zones > 0; } +# +# Returns true if there are bridge port zones defined in the config +# +sub have_6bridges() { + @bport_6zones > 0; +} + # # Return the bridge associated with the passed interface. If the interface is not a bridge port, # return '' @@ -851,6 +1215,15 @@ sub port_to_bridge( $ ) { return $portref && $portref->{options}{port} ? $portref->{bridge} : ''; } +# +# Return the bridge associated with the passed interface. If the interface is not a bridge port, +# return '' +# +sub port_to_6bridge( $ ) { + my $portref = $interfaces6{$_[0]}; + return $portref && $portref->{options}{port} ? $portref->{bridge} : ''; +} + # # Return the bridge associated with the passed interface. # @@ -859,6 +1232,14 @@ sub source_port_to_bridge( $ ) { return $portref ? $portref->{bridge} : ''; } +# +# Return the bridge associated with the passed 6interface. +# +sub source_port_to_6bridge( $ ) { + my $portref = $interfaces6{$_[0]}; + return $portref ? $portref->{bridge} : ''; +} + # # Return the 'optional' setting of the passed interface # @@ -867,6 +1248,14 @@ sub interface_is_optional($) { $optionsref && $optionsref->{optional}; } +# +# Return the 'optional' setting of the passed interface +# +sub interface6_is_optional($) { + my $optionsref = $interfaces6{$_[0]}{options}; + $optionsref && $optionsref->{optional}; +} + # # Returns reference to array of interfaces with the passed option # @@ -884,6 +1273,23 @@ sub find_interfaces_by_option( $ ) { \@ints; } +# +# Returns reference to array of interfaces6 with the passed option +# +sub find_interfaces6_by_option( $ ) { + my $option = $_[0]; + my @ints = (); + + for my $interface ( @interfaces ) { + my $optionsref = $interfaces{$interface}{options}; + if ( $optionsref && defined $optionsref->{$option} ) { + push @ints , $interface + } + } + + \@ints; +} + # # Return the value of an option for an interface # @@ -902,6 +1308,24 @@ sub set_interface_option( $$$ ) { $interfaces{$interface}{options}{$option} = $value; } +# +# Return the value of an option for an interface6 +# +sub get_interface6_option( $$ ) { + my ( $interface, $option ) = @_; + + $interfaces6{$interface}{options}{$option}; +} + +# +# Set an option for an interface6 +# +sub set_interface6_option( $$$ ) { + my ( $interface, $option, $value ) = @_; + + $interfaces6{$interface}{options}{$option} = $value; +} + # # Validates the hosts file. Generates entries in %zone{..}{hosts} # @@ -910,10 +1334,8 @@ sub validate_hosts_file() my %validoptions = ( blacklist => 1, maclist => 1, - norfc1918 => 1, nosmurfs => 1, routeback => 1, - routefilter => 1, tcpflags => 1, broadcast => 1, destonly => 1, @@ -939,6 +1361,7 @@ sub validate_hosts_file() fatal_error "Unknown ZONE ($zone)" unless $type; fatal_error 'Firewall zone not allowed in ZONE column of hosts record' if $type eq 'firewall'; + fatal_error 'IPv6 zones not allowed in ZONE column of hosts record' if $type =~ /6/; my $interface; @@ -951,7 +1374,7 @@ sub validate_hosts_file() fatal_error "Invalid HOST(S) column contents: $hosts"; } - if ( $type eq 'bport4' ) { + if ( $type eq 'bport6' ) { if ( $zoneref->{bridge} eq '' ) { fatal_error 'Bridge Port Zones may only be associated with bridge ports' unless $interfaces{$interface}{options}{port}; $zoneref->{bridge} = $interfaces{$interface}{bridge}; @@ -969,7 +1392,7 @@ sub validate_hosts_file() for my $option ( @options ) { if ( $option eq 'ipsec' ) { - $type = 'ipsec4'; + $type = 'ipsec6'; $zoneref->{options}{complex} = 1; $ipsec = 1; } elsif ( $validoptions{$option}) { @@ -995,7 +1418,7 @@ sub validate_hosts_file() # # Take care of case where the hosts list begins with '!' # - $hosts = join( '', ALLIPv4 , $hosts ) if substr($hosts, 0, 2 ) eq ',!'; + $hosts = join( '', ALLIPv6 , $hosts ) if substr($hosts, 0, 2 ) eq ',!'; add_group_to_zone( $zone, $type , $interface, [ split_list( $hosts, 'host' ) ] , $optionsref); @@ -1005,6 +1428,108 @@ sub validate_hosts_file() $capabilities{POLICY_MATCH} = '' unless $ipsec || haveipseczones; } +# +# Validates the 6hosts file. Generates entries in %zone{..}{hosts} +# +sub validate_6hosts_file() +{ + my %validoptions = ( + blacklist => 1, + maclist => 1, + nosmurfs => 1, + routeback => 1, + tcpflags => 1, + broadcast => 1, + destonly => 1, + sourceonly => 1, + ); + + my $ipsec = 0; + my $first_entry = 1; + + my $fn = open_file '6hosts'; + + while ( read_a_line ) { + + if ( $first_entry ) { + progress_message2 "$doing $fn..."; + $first_entry = 0; + } + + my ($zone, $hosts, $options ) = split_line 2, 3, '6hosts file'; + + my $zoneref = $zones{$zone}; + my $type = $zoneref->{type}; + + fatal_error "Unknown ZONE ($zone)" unless $type; + fatal_error 'Firewall zone not allowed in ZONE column of hosts record' if $type eq 'firewall'; + fatal_error 'IPv4 zonea not allowed in ZONE column of 6hosts record' if $type =~ /4/; + + my $interface; + + if ( $hosts =~ /^([\w.@%-]+\+?)\[(.*)\]$/ ) { + $interface = $1; + $hosts = $2; + $zoneref->{options}{complex} = 1 if $hosts =~ /^\+/; + fatal_error "Unknown 6interface ($interface)" unless $interfaces6{$interface}{root}; + } else { + fatal_error "Invalid HOST(S) column contents: $hosts"; + } + + if ( $type eq 'bport6' ) { + if ( $zoneref->{bridge} eq '' ) { + fatal_error 'Bridge Port Zones may only be associated with bridge ports' unless $interfaces6{$interface}{options}{port}; + $zoneref->{bridge} = $interfaces6{$interface}{bridge}; + } elsif ( $zoneref->{bridge} ne $interfaces6{$interface}{bridge} ) { + fatal_error "Interface $interface is not a port on bridge $zoneref->{bridge}"; + } + } + + my $optionsref = {}; + + if ( $options ne '-' ) { + my @options = split_list $options, 'option'; + my %options; + + for my $option ( @options ) + { + if ( $option eq 'ipsec' ) { + $type = 'ipsec6'; + $zoneref->{options}{complex} = 1; + $ipsec = 1; + } elsif ( $validoptions{$option}) { + $options{$option} = 1; + } else { + fatal_error "Invalid option ($option)"; + } + } + + $optionsref = \%options; + } + + # + # Looking for the '!' at the beginning of a list element is more straight-foward than looking for it in the middle. + # + # Be sure we don't have a ',!' in the original + # + fatal_error "Invalid hosts list" if $hosts =~ /,!/; + # + # Now add a comma before '!'. Do it globally - add_group_to_zone() correctly checks for multiple exclusions + # + $hosts =~ s/!/,!/g; + # + # Take care of case where the hosts list begins with '!' + # + $hosts = join( '', ALLIPv6 , $hosts ) if substr($hosts, 0, 2 ) eq ',!'; + + add_group_to_zone( $zone, $type , $interface, [ split_list( $hosts, 'host' ) ] , $optionsref); + + progress_message " Host \"$currentline\" validated"; + } + + $capabilities{POLICY_MATCH} = 'Yes' if $ipsec || haveipseczones; +} + # # Returns a reference to a array of host entries. Each entry is a # reference to an array containing ( interface , polciy match type {ipsec|none} , network ); @@ -1013,7 +1538,7 @@ sub find_hosts_by_option( $ ) { my $option = $_[0]; my @hosts; - for my $zone ( grep $zones{$_}{type} ne 'firewall' , @zones ) { + for my $zone ( grep $zones{$_}{family} == F_INET , @zones ) { while ( my ($type, $interfaceref) = each %{$zones{$zone}{hosts}} ) { while ( my ( $interface, $arrayref) = ( each %{$interfaceref} ) ) { for my $host ( @{$arrayref} ) { @@ -1028,7 +1553,7 @@ sub find_hosts_by_option( $ ) { } for my $interface ( @interfaces ) { - if ( ! $interfaces{$interface}{zone4} && $interfaces{$interface}{options}{$option} ) { + if ( ! $interfaces{$interface}{zone} && $interfaces{$interface}{options}{$option} ) { push @hosts, [ $interface, 'none', ALLIPv4 ]; } } @@ -1036,4 +1561,35 @@ sub find_hosts_by_option( $ ) { \@hosts; } +# +# Returns a reference to a array of host entries. Each entry is a +# reference to an array containing ( interface , polciy match type {ipsec|none} , network ); +# +sub find_6hosts_by_option( $ ) { + my $option = $_[0]; + my @hosts; + + for my $zone ( grep $zones{$_}{family} == F_INET6 , @zones ) { + while ( my ($type, $interfaceref) = each %{$zones{$zone}{hosts}} ) { + while ( my ( $interface, $arrayref) = ( each %{$interfaceref} ) ) { + for my $host ( @{$arrayref} ) { + if ( $host->{options}{$option} ) { + for my $net ( @{$host->{hosts}} ) { + push @hosts, [ $interface, $host->{ipsec} , $net ]; + } + } + } + } + } + } + + for my $interface ( @interfaces6 ) { + if ( ! $interfaces6{$interface}{zone} && $interfaces6{$interface}{options}{$option} ) { + push @hosts, [ $interface, 'none', ALLIPv6 ]; + } + } + + \@hosts; +} + 1; diff --git a/manpages/shorewall-interfaces.xml b/manpages/shorewall-interfaces.xml index 555c794dc..98a5376c7 100644 --- a/manpages/shorewall-interfaces.xml +++ b/manpages/shorewall-interfaces.xml @@ -1,4 +1,6 @@ + shorewall-interfaces @@ -95,9 +97,9 @@ loc eth2 - The broadcast address(es) for the network(s) to which the - interface belongs. For P-T-P interfaces, this column is left - blank. If the interface has multiple addresses on multiple subnets - then list the broadcast addresses as a comma-separated list. + interface belongs. For P-T-P interfaces, this column is left blank. + If the interface has multiple addresses on multiple subnets then + list the broadcast addresses as a comma-separated list. If you use the special value detect, Shorewall will detect the broadcast @@ -257,8 +259,8 @@ loc eth2 - - you have a static IP but are on a LAN segment with - lots of DHCP clients. + the interface has a static IP but is on a LAN + segment with lots of DHCP clients. @@ -266,6 +268,9 @@ loc eth2 - port and DHCP clients on another port. + + This option allows DHCP datagrams to enter and leave the + interface. diff --git a/docs/images/100_0269.jpg b/web/images/100_0269.jpg similarity index 100% rename from docs/images/100_0269.jpg rename to web/images/100_0269.jpg