forked from extern/shorewall_code
cdf5ad45d5
Signed-off-by: Tom Eastep <teastep@shorewall.net>
5864 lines
168 KiB
Perl
5864 lines
168 KiB
Perl
#
|
|
# Shorewall 5.1 -- /usr/share/shorewall/Shorewall/Rules.pm
|
|
#
|
|
# This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
|
|
#
|
|
# (c) 2007-2017 - Tom Eastep (teastep@shorewall.net)
|
|
#
|
|
# Complete documentation is available at http://shorewall.net
|
|
#
|
|
# This program is part of Shorewall.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation, either version 2 of the license or, at your
|
|
# option, any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# This module handles policies and rules. It contains:
|
|
#
|
|
# process_() and it's associated helpers.
|
|
# process_rules() and it's associated helpers for handling Actions and Macros.
|
|
#
|
|
# This module combines the former Policy, Rules and Actions modules.
|
|
#
|
|
package Shorewall::Rules;
|
|
require Exporter;
|
|
use Shorewall::Config qw(:DEFAULT :internal);
|
|
use Shorewall::Zones;
|
|
use Shorewall::Chains qw(:DEFAULT :internal);
|
|
use Shorewall::IPAddrs;
|
|
use Shorewall::Nat qw(:rules);
|
|
use Shorewall::Raw qw( handle_helper_rule );
|
|
use Scalar::Util 'reftype';
|
|
use Shorewall::Providers qw( provider_realm );
|
|
|
|
use strict;
|
|
|
|
our @ISA = qw(Exporter);
|
|
our @EXPORT = qw(
|
|
process_policies
|
|
complete_policy_chains
|
|
complete_standard_chain
|
|
setup_syn_flood_chains
|
|
save_policies
|
|
ensure_rules_chain
|
|
optimize_policy_chains
|
|
process_actions
|
|
process_rules
|
|
verify_audit
|
|
perl_action_helper
|
|
perl_action_tcp_helper
|
|
check_state
|
|
process_reject_action
|
|
setup_snat
|
|
);
|
|
|
|
our @EXPORT_OK = qw( initialize process_rule );
|
|
|
|
our %EXPORT_TAGS = ( Traffic => [ qw( process_tc_rule
|
|
process_mangle_rule
|
|
|
|
%classids
|
|
%tcdevices
|
|
%tcclasses
|
|
%tosoptions
|
|
%restrictions
|
|
|
|
$mangle
|
|
$sticky
|
|
) , ]
|
|
);
|
|
|
|
Exporter::export_ok_tags('Traffic');
|
|
|
|
our $VERSION = 'MODULEVERSION';
|
|
#
|
|
# Globals are documented in the initialize() function
|
|
#
|
|
our %sections;
|
|
|
|
our $section;
|
|
our $next_section;
|
|
|
|
use constant { NULL_SECTION => 0x00,
|
|
BLACKLIST_SECTION => 0x01,
|
|
ALL_SECTION => 0x02,
|
|
ESTABLISHED_SECTION => 0x04,
|
|
RELATED_SECTION => 0x08,
|
|
INVALID_SECTION => 0x10,
|
|
UNTRACKED_SECTION => 0x20,
|
|
NEW_SECTION => 0x40,
|
|
POLICYACTION_SECTION => 0x80 };
|
|
#
|
|
# Number of elements in the action tuple
|
|
#
|
|
use constant { ACTION_TUPLE_ELEMENTS => 5 };
|
|
#
|
|
# Section => name function
|
|
#
|
|
our %section_functions = ( ALL_SECTION , \&rules_chain,
|
|
BLACKLIST_SECTION , \&blacklist_chain,
|
|
ESTABLISHED_SECTION, \&established_chain,
|
|
RELATED_SECTION, \&related_chain,
|
|
INVALID_SECTION, \&invalid_chain,
|
|
UNTRACKED_SECTION, \&untracked_chain,
|
|
NEW_SECTION, \&rules_chain );
|
|
|
|
#
|
|
# Section => STATE map - initialized in process_rules().
|
|
#
|
|
our %section_states;
|
|
#
|
|
# These are the sections that may appear in a section header
|
|
#
|
|
our %section_map = ( ALL => ALL_SECTION,
|
|
ESTABLISHED => ESTABLISHED_SECTION,
|
|
RELATED => RELATED_SECTION,
|
|
INVALID => INVALID_SECTION,
|
|
UNTRACKED => UNTRACKED_SECTION,
|
|
NEW => NEW_SECTION );
|
|
#
|
|
# Reverse map
|
|
#
|
|
our %section_rmap = ( ALL_SECTION , 'ALL',
|
|
BLACKLIST_SECTION , 'BLACKLIST',
|
|
ESTABLISHED_SECTION, 'ESTABLISHED',
|
|
RELATED_SECTION, 'RELATED',
|
|
INVALID_SECTION, 'INVALID',
|
|
UNTRACKED_SECTION, 'UNTRACKED',
|
|
NEW_SECTION, 'NEW' );
|
|
|
|
our @policy_chains;
|
|
|
|
our %policy_actions;
|
|
|
|
our %macros;
|
|
|
|
our $family;
|
|
|
|
#
|
|
# Commands that can be embedded in a basic rule and how many total tokens on the line (0 => unlimited).
|
|
#
|
|
our $rule_commands = {};
|
|
our $action_commands = { DEFAULTS => 2 };
|
|
our $macro_commands = { DEFAULT => 2 };
|
|
#
|
|
# There is an implicit assumption that the last column of the @rulecolumns hash is always the last column of the @columns array.
|
|
# The @columns array doesn't include the ACTION but does include a 'wildcard' last element.
|
|
#
|
|
use constant { LAST_COLUMN => 14 };
|
|
|
|
our %rulecolumns = ( action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
origdest => 6,
|
|
rate => 7,
|
|
user => 8,
|
|
mark => 9,
|
|
connlimit => 10,
|
|
time => 11,
|
|
headers => 12,
|
|
switch => 13,
|
|
helper => LAST_COLUMN,
|
|
);
|
|
|
|
use constant { MAX_MACRO_NEST_LEVEL => 10 };
|
|
|
|
our $macro_nest_level;
|
|
|
|
our @actionstack;
|
|
our %active;
|
|
|
|
# Action Table
|
|
#
|
|
# %actions{ actchain => used to eliminate collisions }
|
|
#
|
|
our %actions;
|
|
#
|
|
# Contains an entry for each used <action>:<level>:[<tag>]:[<calling chain>]:[<params>] that maps to the associated chain.
|
|
# See normalize_action().
|
|
#
|
|
our %usedactions;
|
|
|
|
#
|
|
# Policies for which AUDIT is allowed
|
|
#
|
|
our %auditpolicies = ( ACCEPT => 1,
|
|
DROP => 1,
|
|
REJECT => 1
|
|
);
|
|
#
|
|
# Columns $source through $wildcard -- with the exception of the latter, these correspond to the rules file columns
|
|
# The columns array is a hidden argument to perl_action_helper() and perl_action_tcp_helper() that allows Perl
|
|
# code in inline actions to generate proper rules.
|
|
#
|
|
our @columns;
|
|
#
|
|
# Hidden return from perl_action_[tcp_]helper that indicates that a rule was generated
|
|
#
|
|
our $actionresult;
|
|
#
|
|
# See process_rules() and finish_chain_section().
|
|
#
|
|
our %statetable;
|
|
#
|
|
# Tracks which of the state match actions (action.Invalid, etc.) that is currently being expanded
|
|
#
|
|
our $statematch;
|
|
#
|
|
# Remembers NAT-oriented columns from top-level action invocations
|
|
#
|
|
our %nat_columns;
|
|
|
|
#
|
|
# Action/Inline options
|
|
#
|
|
use constant { INLINE_OPT => 1 ,
|
|
NOINLINE_OPT => 2 ,
|
|
NOLOG_OPT => 4 ,
|
|
BUILTIN_OPT => 8 ,
|
|
RAW_OPT => 16 ,
|
|
MANGLE_OPT => 32 ,
|
|
FILTER_OPT => 64 ,
|
|
NAT_OPT => 128 ,
|
|
TERMINATING_OPT => 256 ,
|
|
AUDIT_OPT => 512 ,
|
|
LOGJUMP_OPT => 1024 ,
|
|
SECTION_OPT => 2048 ,
|
|
};
|
|
|
|
our %options = ( inline => INLINE_OPT ,
|
|
noinline => NOINLINE_OPT ,
|
|
nolog => NOLOG_OPT ,
|
|
builtin => BUILTIN_OPT ,
|
|
raw => RAW_OPT ,
|
|
mangle => MANGLE_OPT ,
|
|
filter => FILTER_OPT ,
|
|
nat => NAT_OPT ,
|
|
terminating => TERMINATING_OPT ,
|
|
audit => AUDIT_OPT ,
|
|
logjump => LOGJUMP_OPT ,
|
|
section => SECTION_OPT ,
|
|
);
|
|
|
|
our %reject_options;
|
|
################################################################################
|
|
# Declarations moved from the Tc module in 5.0.7 #
|
|
################################################################################
|
|
use constant { NOMARK => 0 ,
|
|
SMALLMARK => 1 ,
|
|
HIGHMARK => 2
|
|
};
|
|
|
|
our %designator = ( F => 'tcfor' ,
|
|
T => 'tcpost' );
|
|
|
|
our %tosoptions = ( 'tos-minimize-delay' => '0x10/0x10' ,
|
|
'tos-maximize-throughput' => '0x08/0x08' ,
|
|
'tos-maximize-reliability' => '0x04/0x04' ,
|
|
'tos-minimize-cost' => '0x02/0x02' ,
|
|
'tos-normal-service' => '0x00/0x1e' );
|
|
|
|
our %restrictions = ( tcpre => PREROUTE_RESTRICT ,
|
|
PREROUTING => PREROUTE_RESTRICT ,
|
|
tcpost => POSTROUTE_RESTRICT ,
|
|
tcfor => NO_RESTRICT ,
|
|
tcin => INPUT_RESTRICT ,
|
|
tcout => OUTPUT_RESTRICT ,
|
|
);
|
|
our %tcdevices;
|
|
our %tcclasses;
|
|
our %classids;
|
|
|
|
our $mangle;
|
|
|
|
our $sticky;
|
|
|
|
our $divertref; # DIVERT chain
|
|
|
|
our %validstates = ( NEW => 0,
|
|
RELATED => 0,
|
|
ESTABLISHED => 0,
|
|
UNTRACKED => 0,
|
|
INVALID => 0,
|
|
);
|
|
#
|
|
# Rather than initializing globals in an INIT block or during declaration,
|
|
# we initialize them in a function. This is done for two reasons:
|
|
#
|
|
# 1. Proper initialization depends on the address family which isn't
|
|
# known until the compiler has started.
|
|
#
|
|
# 2. The compiler can run multiple times in the same process so it has to be
|
|
# able to re-initialize the state of its dependent modules.
|
|
#
|
|
sub initialize( $ ) {
|
|
$family = shift;
|
|
#
|
|
# Chains created as a result of entries in the policy file
|
|
#
|
|
@policy_chains = ();
|
|
#
|
|
# This is updated from the *_DEFAULT settings in shorewall.conf. Those settings were stored
|
|
# in the %config hash when shorewall[6].conf was processed.
|
|
#
|
|
%policy_actions = ( DROP => [] ,
|
|
REJECT => [] ,
|
|
BLACKLIST => [] ,
|
|
ACCEPT => [] ,
|
|
QUEUE => [] ,
|
|
NFQUEUE => [] ,
|
|
CONTINUE => [] ,
|
|
NONE => [] ,
|
|
);
|
|
#
|
|
# These are set to 1 as sections are encountered.
|
|
#
|
|
%sections = ( ALL => 0,
|
|
ESTABLISHED => 0,
|
|
RELATED => 0,
|
|
INVALID => 0,
|
|
UNTRACKED => 0,
|
|
NEW => 0
|
|
);
|
|
#
|
|
# Current rules file section.
|
|
#
|
|
$section = NULL_SECTION;
|
|
$next_section = NULL_SECTION;
|
|
#
|
|
# Macro=><macro file> mapping
|
|
#
|
|
%macros = ();
|
|
#
|
|
# Stack of nested action calls while parsing action.* files.
|
|
#
|
|
@actionstack = ();
|
|
#
|
|
# This hash provides keyed access to @actionstack
|
|
#
|
|
%active = ();
|
|
#
|
|
# Self-explainatory
|
|
#
|
|
$macro_nest_level = 0;
|
|
#
|
|
# All actions mentioned in /etc/shorewall[6]/actions and /usr/share/shorewall[6]/actions.std
|
|
#
|
|
%actions = ();
|
|
#
|
|
# Action variants actually used. Key is <action>:<loglevel>:<tag>:<caller>:<params>; value is corresponding chain name
|
|
#
|
|
%usedactions = ();
|
|
|
|
@columns = ( ( '-' ) x LAST_COLUMN, 0 );
|
|
|
|
if ( $family == F_IPV4 ) {
|
|
%reject_options = ( 'icmp-net-unreachable' => 1,
|
|
'icmp-host-unreachable' => 1,
|
|
'icmp-port-unreachable' => 1,
|
|
'icmp-proto-unreachable' => 1,
|
|
'icmp-net-prohibited' => 1,
|
|
'icmp-host-prohibited' => 1,
|
|
'icmp-admin-prohibited' => 1,
|
|
'icmp-tcp-reset' => 2,
|
|
'tcp-reset' => 2,
|
|
);
|
|
|
|
} else {
|
|
%reject_options = ( 'icmp6-no-route' => 1,
|
|
'no-route' => 1,
|
|
'icmp6-adm-prohibited' => 1,
|
|
'adm-prohibited' => 1,
|
|
'icmp6-addr-unreachable' => 1,
|
|
'addr-unreach' => 1,
|
|
'icmp6-port-unreachable' => 1,
|
|
'tcp-reset' => 2,
|
|
);
|
|
}
|
|
|
|
%nat_columns = ( dest => '-', proto => '-', ports => '-' );
|
|
|
|
############################################################################
|
|
# Initialize variables moved from the Tc module in Shorewall 5.0.7 #
|
|
############################################################################
|
|
%classids = ();
|
|
%tcdevices = ();
|
|
%tcclasses = ();
|
|
$sticky = 0;
|
|
$divertref = 0;
|
|
}
|
|
|
|
#
|
|
# Create a rules chain
|
|
#
|
|
sub new_rules_chain( $ ) {
|
|
my $chainref = new_chain( 'filter', $_[0] );
|
|
|
|
if ( $config{FASTACCEPT} ) {
|
|
if ( $globals{RELATED_TARGET} eq 'ACCEPT' && ! $config{RELATED_LOG_LEVEL} ) {
|
|
$chainref->{sections} = { ESTABLISHED => 1, RELATED => 1 };
|
|
} else {
|
|
$chainref->{sections} = { ESTABLISHED => 1 };
|
|
}
|
|
} else {
|
|
$chainref->{sections} = {};
|
|
}
|
|
|
|
$chainref;
|
|
}
|
|
|
|
###############################################################################
|
|
# Functions moved from the former Policy Module
|
|
###############################################################################
|
|
#
|
|
# Convert a chain into a policy chain.
|
|
#
|
|
sub convert_to_policy_chain($$$$$$)
|
|
{
|
|
my ($chainref, $source, $dest, $policy, $provisional, $audit ) = @_;
|
|
|
|
$chainref->{is_policy} = 1;
|
|
$chainref->{policy} = $policy;
|
|
$chainref->{provisional} = $provisional;
|
|
$chainref->{audit} = $audit;
|
|
$chainref->{policychain} = $chainref->{name};
|
|
$chainref->{policypair} = [ $source, $dest ];
|
|
$chainref->{pactions} = [];
|
|
}
|
|
|
|
#
|
|
# Create a new policy chain and return a reference to it.
|
|
#
|
|
sub new_policy_chain($$$$$)
|
|
{
|
|
my ($source, $dest, $policy, $provisional, $audit) = @_;
|
|
|
|
my $chainref = new_rules_chain( rules_chain( ${source}, ${dest} ) );
|
|
|
|
convert_to_policy_chain( $chainref, $source, $dest, $policy, $provisional, $audit );
|
|
|
|
$chainref;
|
|
}
|
|
|
|
#
|
|
# Set the passed chain's policychain and policy to the passed values.
|
|
#
|
|
sub set_policy_chain($$$$$$)
|
|
{
|
|
my ( $chain, $source, $dest, $polchainref, $policy, $intrazone ) = @_;
|
|
|
|
my $chainref = $filter_table->{$chain};
|
|
|
|
if ( $chainref ) {
|
|
if ( $intrazone && $source eq $dest && $chainref->{provisional} ) {
|
|
$chainref->{policychain} = '';
|
|
$chainref->{provisional} = '';
|
|
}
|
|
} else {
|
|
$chainref = new_rules_chain $chain;
|
|
}
|
|
|
|
unless ( $chainref->{policychain} ) {
|
|
if ( $config{EXPAND_POLICIES} ) {
|
|
#
|
|
# We convert the canonical chain into a policy chain, using the settings of the
|
|
# passed policy chain.
|
|
#
|
|
$chainref->{policychain} = $chain;
|
|
$chainref->{loglevel} = $polchainref->{loglevel} if defined $polchainref->{loglevel};
|
|
$chainref->{audit} = $polchainref->{audit} if defined $polchainref->{audit};
|
|
|
|
if ( defined $polchainref->{synparams} ) {
|
|
$chainref->{synparams} = $polchainref->{synparams};
|
|
$chainref->{synchain} = $polchainref->{synchain};
|
|
}
|
|
|
|
$chainref->{pactions} = $polchainref->{pactions} || [];
|
|
$chainref->{is_policy} = 1;
|
|
push @policy_chains, $chainref;
|
|
} else {
|
|
$chainref->{policychain} = $polchainref->{name};
|
|
}
|
|
|
|
$chainref->{policy} = $policy;
|
|
$chainref->{policypair} = [ $source, $dest ];
|
|
$chainref->{origin} = $polchainref->{origin};
|
|
}
|
|
}
|
|
|
|
#
|
|
# Process the policy file
|
|
#
|
|
use constant { PROVISIONAL => 1 };
|
|
|
|
sub add_or_modify_policy_chain( $$$ ) {
|
|
my ( $zone, $zone1, $audit ) = @_;
|
|
my $chain = rules_chain( ${zone}, ${zone1} );
|
|
my $chainref = $filter_table->{$chain};
|
|
|
|
if ( $chainref ) {
|
|
unless( $chainref->{is_policy} ) {
|
|
convert_to_policy_chain( $chainref, $zone, $zone1, 'CONTINUE', PROVISIONAL, $audit );
|
|
push @policy_chains, $chainref;
|
|
}
|
|
} else {
|
|
push @policy_chains, ( new_policy_chain $zone, $zone1, 'CONTINUE', PROVISIONAL, $audit );
|
|
}
|
|
}
|
|
|
|
sub print_policy($$$$) {
|
|
my ( $source, $dest, $policy , $chain ) = @_;
|
|
unless ( ( $source eq 'all' ) || ( $dest eq 'all' ) ) {
|
|
if ( $policy eq 'CONTINUE' ) {
|
|
my ( $sourceref, $destref ) = ( find_zone($source) ,find_zone( $dest ) );
|
|
warning_message "CONTINUE policy between two un-nested zones ($source, $dest)" if ! ( @{$sourceref->{parents}} || @{$destref->{parents}} );
|
|
}
|
|
progress_message_nocompress " Policy for $source to $dest is $policy using chain $chain" unless $source eq $dest;
|
|
}
|
|
}
|
|
|
|
sub use_policy_action( $$ );
|
|
sub normalize_action( $$$ );
|
|
sub normalize_action_name( $ );
|
|
sub normalize_single_action( $ );
|
|
|
|
sub process_policy_action( $$$$ ) {
|
|
my ( $originalpolicy, $policy, $paction, $level ) = @_;
|
|
|
|
if ( supplied $paction ) {
|
|
my $paction_option = ( $policy =~ /_DEFAULT$/ );
|
|
my ( $act, $param ) = get_target_param( $paction );
|
|
|
|
if ( supplied $level ) {
|
|
validate_level( $level );
|
|
} else {
|
|
$level = 'none';
|
|
}
|
|
|
|
if ( ( $targets{$act} || 0 ) & ACTION ) {
|
|
$paction = supplied $param ? normalize_action( $act, $level, $param ) :
|
|
$level eq 'none' ? normalize_action_name $act :
|
|
normalize_action( $act, $level, '' );
|
|
} elsif ( ( $targets{$act} || 0 ) == INLINE ) {
|
|
$paction = $act;
|
|
$paction = "$act($param)" if supplied $param;
|
|
$paction = join( ':', $paction, $level ) if $level ne 'none';
|
|
} elsif ( $paction_option ) {
|
|
fatal_error "Unknown Action ($paction) in $policy setting";
|
|
} else {
|
|
fatal_error "Unknown Policy Action ($paction)";
|
|
}
|
|
|
|
} else {
|
|
$paction = $policy_actions{$policy};
|
|
}
|
|
|
|
$paction;
|
|
}
|
|
|
|
sub process_policy_actions( $$$ ) {
|
|
my ( $originalpolicy, $policy, $pactions ) = @_;
|
|
|
|
if ( supplied $pactions ) {
|
|
my @pactions;
|
|
|
|
if ( lc $pactions ne 'none' ) {
|
|
@pactions = @{$policy_actions{$policy}} if $pactions =~ s/^\+//;
|
|
|
|
for my $paction ( split_list3( $pactions, 'Policy Action' ) ) {
|
|
my ( $action, $level, $remainder ) = split( /:/, $paction, 3 );
|
|
|
|
fatal_error "Invalid policy action ($paction:$level:$remainder)" if defined $remainder;
|
|
|
|
push @pactions, process_policy_action( $originalpolicy, $policy, $action, $level );
|
|
}
|
|
}
|
|
|
|
\@pactions;
|
|
} else {
|
|
$policy_actions{$policy};
|
|
}
|
|
}
|
|
|
|
#
|
|
# Verify an NFQUEUE specification and return the appropriate ip[6]tables target
|
|
#
|
|
sub handle_nfqueue( $$ ) {
|
|
my ($params, $allow_bypass ) = @_;
|
|
my ( $action, $bypass, $fanout );
|
|
my ( $queue1, $queue2, $queuenum1, $queuenum2 );
|
|
|
|
require_capability( 'NFQUEUE_TARGET', 'NFQUEUE Rules and Policies', '' );
|
|
|
|
if ( supplied( $params ) ) {
|
|
( my $queue, $bypass, my $junk ) = split ',', $params, 3;
|
|
|
|
fatal_error "Invalid NFQUEUE parameter list ($params)" if defined $junk;
|
|
|
|
if ( supplied $queue ) {
|
|
if ( $queue eq 'bypass' ) {
|
|
fatal_error "'bypass' is not allowed in this context" unless $allow_bypass;
|
|
fatal_error "Invalid NFQUEUE options (bypass,$bypass)" if supplied $bypass;
|
|
return 'NFQUEUE --queue-bypass';
|
|
}
|
|
|
|
( $queue1, $queue2 ) = split ':', $queue, 2;
|
|
|
|
fatal_error "Invalid NFQUEUE parameter list ($params)" unless supplied $queue1;
|
|
|
|
$queuenum1 = numeric_value( $queue1 );
|
|
|
|
fatal_error "Invalid NFQUEUE queue number ($queue1)" unless defined( $queuenum1) && $queuenum1 >= 0 && $queuenum1 <= 65535;
|
|
|
|
if ( supplied $queue2 ) {
|
|
$fanout = $queue2 =~ s/c$// ? ' --queue-cpu-fanout' : '';
|
|
$queuenum2 = numeric_value( $queue2 );
|
|
|
|
fatal_error "Invalid NFQUEUE queue number ($queue2)" unless defined( $queuenum2) && $queuenum2 >= 0 && $queuenum2 <= 65535 && $queuenum1 < $queuenum2;
|
|
}
|
|
} else {
|
|
$queuenum1 = 0;
|
|
}
|
|
} else {
|
|
$queuenum1 = 0;
|
|
}
|
|
|
|
if ( supplied $bypass ) {
|
|
fatal_error "Invalid NFQUEUE option ($bypass)" if $bypass ne 'bypass';
|
|
fatal_error "'bypass' is not allowed in this context" unless $allow_bypass;
|
|
|
|
$bypass =' --queue-bypass';
|
|
} else {
|
|
$bypass = '';
|
|
}
|
|
|
|
if ( supplied $queue2 ) {
|
|
require_capability 'CPU_FANOUT', '"c"', 's' if $fanout;
|
|
return "NFQUEUE --queue-balance ${queuenum1}:${queuenum2}${fanout}${bypass}";
|
|
} else {
|
|
return "NFQUEUE --queue-num ${queuenum1}${bypass}";
|
|
}
|
|
}
|
|
|
|
#
|
|
# Process an entry in the policy file.
|
|
#
|
|
sub process_a_policy1($$$$$$$) {
|
|
|
|
our %validpolicies;
|
|
our @zonelist;
|
|
|
|
my ( $client, $server, $originalpolicy, $loglevel, $synparams, $connlimit, $intrazone ) = @_;
|
|
|
|
my $clientwild = ( "\L$client" =~ /^all(\+)?$/ );
|
|
|
|
$intrazone ||= $clientwild && $1;
|
|
|
|
fatal_error "Undefined zone ($client)" unless $clientwild || defined_zone( $client );
|
|
|
|
my $serverwild = ( "\L$server" =~ /^all(\+)?/ );
|
|
$intrazone ||= ( $serverwild && $1 );
|
|
|
|
fatal_error "Undefined zone ($server)" unless $serverwild || defined_zone( $server );
|
|
|
|
my $audit = ( $originalpolicy =~ s/:audit$// );
|
|
|
|
require_capability 'AUDIT_TARGET', ":audit", "s" if $audit;
|
|
|
|
my ( $policy, $pactions ) = split( /:/, $originalpolicy, 2 );
|
|
|
|
fatal_error "Invalid or missing POLICY ($originalpolicy)" unless $policy;
|
|
|
|
( $policy , my $queue ) = get_target_param $policy;
|
|
|
|
fatal_error "Invalid policy ($policy)" unless exists $validpolicies{$policy};
|
|
|
|
if ( $audit ) {
|
|
fatal_error "A $policy policy may not be audited" unless $auditpolicies{$policy};
|
|
}
|
|
|
|
my $pactionref = process_policy_actions( $originalpolicy, $policy, $pactions );
|
|
|
|
if ( defined $queue ) {
|
|
$policy = handle_nfqueue( $queue,
|
|
0 # Don't allow 'bypass'
|
|
);
|
|
} 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 ) == FIREWALL ) || ( zone_type( $server ) == FIREWALL );
|
|
} elsif ( $policy eq 'BLACKLIST' ) {
|
|
fatal_error 'BLACKLIST policies require ipset-based dynamic blacklisting' unless $config{DYNAMIC_BLACKLIST} =~ /^ipset/;
|
|
}
|
|
|
|
unless ( $clientwild || $serverwild ) {
|
|
if ( zone_type( $server ) & BPORT ) {
|
|
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 = rules_chain( ${client}, ${server} );
|
|
my $chainref;
|
|
|
|
if ( defined $filter_table->{$chain} ) {
|
|
$chainref = $filter_table->{$chain};
|
|
|
|
if ( $chainref->{is_policy} ) {
|
|
if ( $chainref->{provisional} ) {
|
|
$chainref->{provisional} = 0;
|
|
$chainref->{policy} = $policy;
|
|
} else {
|
|
fatal_error qq(Policy "$client $server $originalpolicy" duplicates earlier policy "@{$chainref->{policypair}} $chainref->{policy}");
|
|
}
|
|
} elsif ( $chainref->{policy} ) {
|
|
fatal_error qq(Policy "$client $server $originalpolicy" duplicates earlier policy "@{$chainref->{policypair}} $chainref->{policy}");
|
|
} else {
|
|
convert_to_policy_chain( $chainref, $client, $server, $policy, 0 , $audit );
|
|
push @policy_chains, ( $chainref ) unless $config{EXPAND_POLICIES} && ( $clientwild || $serverwild );
|
|
}
|
|
} else {
|
|
$chainref = new_policy_chain $client, $server, $policy, 0, $audit;
|
|
push @policy_chains, ( $chainref ) unless $config{EXPAND_POLICIES} && ( $clientwild || $serverwild );
|
|
}
|
|
|
|
$chainref->{loglevel} = validate_level( $loglevel ) if supplied $loglevel;
|
|
|
|
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->{pactions} = $pactionref;
|
|
$chainref->{origin} = shortlineinfo('');
|
|
|
|
if ( $clientwild ) {
|
|
if ( $serverwild ) {
|
|
for my $zone ( @zonelist ) {
|
|
for my $zone1 ( @zonelist ) {
|
|
set_policy_chain rules_chain( ${zone}, ${zone1} ), $zone, $zone1, $chainref, $policy, $intrazone;
|
|
print_policy $zone, $zone1, $originalpolicy, $chain;
|
|
}
|
|
}
|
|
} else {
|
|
for my $zone ( all_zones ) {
|
|
set_policy_chain rules_chain( ${zone}, ${server} ), $zone, $server, $chainref, $policy, $intrazone;
|
|
print_policy $zone, $server, $originalpolicy, $chain;
|
|
}
|
|
}
|
|
} elsif ( $serverwild ) {
|
|
for my $zone ( @zonelist ) {
|
|
set_policy_chain rules_chain( ${client}, ${zone} ), $client, $zone, $chainref, $policy, $intrazone;
|
|
print_policy $client, $zone, $originalpolicy, $chain;
|
|
}
|
|
} else {
|
|
print_policy $client, $server, $originalpolicy, $chain;
|
|
}
|
|
}
|
|
|
|
sub process_a_policy() {
|
|
|
|
our %validpolicies;
|
|
our @zonelist;
|
|
|
|
my ( $clients, $servers, $policy, $loglevel, $synparams, $connlimit ) =
|
|
split_line2( 'policy file',
|
|
{ source => 0, dest => 1, policy => 2, loglevel => 3, limit => 4, rate => 4, connlimit => 5 } ,
|
|
{} , # nopad
|
|
6 , # maxcolumns
|
|
);
|
|
|
|
$loglevel = '' if $loglevel eq '-';
|
|
$synparams = '' if $synparams eq '-';
|
|
$connlimit = '' if $connlimit eq '-';
|
|
|
|
my ( $intrazone, $clientlist, $serverlist );
|
|
|
|
if ( $clientlist = ( $clients =~ /,/ ) ) {
|
|
$intrazone = ( $clients =~ s/\+$// );
|
|
}
|
|
|
|
if ( $serverlist = ( $servers =~ /,/ ) ) {
|
|
$intrazone ||= ( $servers =~ s/\+$// );
|
|
}
|
|
|
|
fatal_error 'SOURCE must be specified' if $clients eq '-';
|
|
fatal_error 'DEST must be specified' if $servers eq '-';
|
|
fatal_error 'POLICY must be specified' if $policy eq '-';
|
|
|
|
if ( $clientlist || $serverlist ) {
|
|
for my $client ( split_list( $clients, 'zone' ) ) {
|
|
for my $server ( split_list( $servers, 'zone' ) ) {
|
|
process_a_policy1( $client, $server, $policy, $loglevel, $synparams, $connlimit, $intrazone ) if $intrazone || $client ne $server;
|
|
}
|
|
}
|
|
} else {
|
|
process_a_policy1( $clients, $servers, $policy, $loglevel, $synparams, $connlimit, 0 );
|
|
}
|
|
}
|
|
|
|
#
|
|
# Generate contents of the /var/lib/shorewall[6]/.policies file as 'here documents' in the generated script
|
|
#
|
|
sub save_policies() {
|
|
for my $zone1 ( all_zones ) {
|
|
for my $zone2 ( all_zones ) {
|
|
my $chainref = $filter_table->{ rules_chain( $zone1, $zone2 ) };
|
|
my $policyref = $filter_table->{ $chainref->{policychain} };
|
|
|
|
if ( $policyref->{referenced} ) {
|
|
emit_unindented "$zone1 \t=>\t$zone2\t" . $policyref->{policy} . ' using chain ' . $policyref->{name};
|
|
} elsif ( $zone1 ne $zone2 ) {
|
|
emit_unindented "$zone1 \t=>\t$zone2\t" . $policyref->{policy};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Process the policy file
|
|
#
|
|
sub process_policies()
|
|
{
|
|
our %validpolicies = (
|
|
ACCEPT => undef,
|
|
REJECT => undef,
|
|
DROP => undef,
|
|
CONTINUE => undef,
|
|
BLACKLIST => undef,
|
|
QUEUE => undef,
|
|
NFQUEUE => undef,
|
|
NONE => undef
|
|
);
|
|
|
|
our %map = ( DROP_DEFAULT => 'DROP' ,
|
|
REJECT_DEFAULT => 'REJECT' ,
|
|
BLACKLIST_DEFAULT => 'BLACKLIST' ,
|
|
ACCEPT_DEFAULT => 'ACCEPT' ,
|
|
QUEUE_DEFAULT => 'QUEUE' ,
|
|
NFQUEUE_DEFAULT => 'NFQUEUE' );
|
|
|
|
my $zone;
|
|
my $firewall = firewall_zone;
|
|
our @zonelist = $config{EXPAND_POLICIES} ? all_zones : ( all_zones, 'all' );
|
|
|
|
for my $option ( qw( DROP_DEFAULT REJECT_DEFAULT BLACKLIST_DEFAULT ACCEPT_DEFAULT QUEUE_DEFAULT NFQUEUE_DEFAULT) ) {
|
|
my $actions = $config{$option};
|
|
|
|
if ( $actions eq 'none' ) {
|
|
$actions = [];
|
|
} else {
|
|
$actions = process_policy_actions( $actions, $option, $actions );
|
|
}
|
|
|
|
$policy_actions{$map{$option}} = $actions;
|
|
}
|
|
|
|
for $zone ( all_zones ) {
|
|
push @policy_chains, ( new_policy_chain $zone, $zone, 'ACCEPT', PROVISIONAL, 0 );
|
|
push @policy_chains, ( new_policy_chain firewall_zone, $zone, 'NONE', PROVISIONAL, 0 ) if zone_type( $zone ) & BPORT;
|
|
|
|
my $zoneref = find_zone( $zone );
|
|
my $type = $zoneref->{type};
|
|
|
|
if ( $type == LOCAL ) {
|
|
for my $zone1 ( off_firewall_zones ) {
|
|
unless ( $zone eq $zone1 ) {
|
|
my $name = rules_chain( $zone, $zone1 );
|
|
my $name1 = rules_chain( $zone1, $zone );
|
|
set_policy_chain( $name, $zone, $zone1, ensure_rules_chain( $name ), 'NONE', 0 );
|
|
set_policy_chain( $name1, $zone1, $zone, ensure_rules_chain( $name1 ), 'NONE', 0 );
|
|
}
|
|
}
|
|
} elsif ( $type == LOOPBACK ) {
|
|
for my $zone1 ( off_firewall_zones ) {
|
|
unless ( $zone eq $zone1 || zone_type( $zone1 ) == LOOPBACK ) {
|
|
my $name = rules_chain( $zone, $zone1 );
|
|
my $name1 = rules_chain( $zone1, $zone );
|
|
set_policy_chain( $name, $zone, $zone1, ensure_rules_chain( $name ), 'NONE', 0 );
|
|
set_policy_chain( $name1, $zone1, $zone, ensure_rules_chain( $name1 ), 'NONE', 0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $config{IMPLICIT_CONTINUE} && ( @{$zoneref->{parents}} || $zoneref->{type} & VSERVER ) ) {
|
|
for my $zone1 ( all_zones ) {
|
|
unless( $zone eq $zone1 ) {
|
|
add_or_modify_policy_chain( $zone, $zone1, 0 );
|
|
add_or_modify_policy_chain( $zone1, $zone , 0 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( my $fn = open_file 'policy' ) {
|
|
first_entry "$doing $fn...";
|
|
process_a_policy while read_a_line( NORMAL_READ );
|
|
} else {
|
|
fatal_error q(The 'policy' file does not exist or has zero size);
|
|
}
|
|
|
|
for $zone ( all_zones ) {
|
|
for my $zone1 ( all_zones ) {
|
|
fatal_error "No policy defined from zone $zone to zone $zone1" unless $filter_table->{rules_chain( ${zone}, ${zone1} )}{policy};
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Policy Rule application
|
|
#
|
|
sub process_inline ($$$$$$$$$$$$$$$$$$$$$$);
|
|
|
|
#
|
|
# Determine the protocol to be used in the jump to the passed action
|
|
#
|
|
sub determine_action_protocol( $$ ) {
|
|
my ( $action, $proto ) = @_;
|
|
|
|
if ( my $actionproto = $actions{$action}{proto} ) {
|
|
if ( $proto eq '-' ) {
|
|
$proto = $actionproto;
|
|
} else {
|
|
if ( defined( my $protonum = resolve_proto( $proto ) ) ) {
|
|
fatal_error( "The $action action is only usable with " . proto_name( $actionproto ) ) unless $actionproto == $protonum;
|
|
$proto = $protonum;
|
|
} else {
|
|
fatal_error( "Unknown protocol ($proto)" );
|
|
}
|
|
}
|
|
}
|
|
|
|
$proto;
|
|
}
|
|
|
|
sub add_policy_rules( $$$$$ ) {
|
|
my ( $chainref , $target, $loglevel, $pactions, $dropmulticast ) = @_;
|
|
|
|
unless ( $target eq 'NONE' ) {
|
|
add_ijump $chainref, j => 'RETURN', d => '224.0.0.0/4' if $dropmulticast && $target ne 'CONTINUE' && $target ne 'ACCEPT';
|
|
|
|
for my $paction ( @$pactions ) {
|
|
my ( $action ) = split ':', $paction;
|
|
|
|
if ( ( $targets{$action} || 0 ) & ACTION ) {
|
|
#
|
|
# Policy action is a regular action -- jump to the action chain
|
|
#
|
|
if ( ( my $proto = determine_action_protocol( $action, '-' ) ) ne '-' ) {
|
|
add_ijump( $chainref, j => use_policy_action( $paction, $chainref->{name} ), p => $proto );
|
|
} else {
|
|
add_ijump $chainref, j => use_policy_action( $paction, $chainref->{name} );
|
|
}
|
|
} else {
|
|
#
|
|
# Policy action is an inline
|
|
#
|
|
( undef, my $level ) = split /:/, $paction, 2;
|
|
( $action, my $param ) = get_target_param( $action );
|
|
|
|
process_inline( $action, #Inline
|
|
$chainref, #Chain
|
|
'', #Matches
|
|
'', #Matches1
|
|
$level || '', #Log Level and Tag
|
|
$paction, #Target
|
|
$param || '', #Param
|
|
'-', #Source
|
|
'-', #Dest
|
|
'-', #Proto
|
|
'-', #Ports
|
|
'-', #Sports
|
|
'-', #Original Dest
|
|
'-', #Rate
|
|
'-', #User
|
|
'-', #Mark
|
|
'-', #ConnLimit
|
|
'-', #Time
|
|
'-', #Headers
|
|
'-', #Condition
|
|
'-', #Helper
|
|
0, #Wildcard
|
|
);
|
|
}
|
|
}
|
|
|
|
log_rule $loglevel , $chainref , $target , '' if $loglevel ne '';
|
|
assert( $target );
|
|
|
|
if ( $target eq 'BLACKLIST' ) {
|
|
my ( $dbl_type, $dbl_ipset, $dbl_level, $dbl_tag ) = split( ':', $config{DYNAMIC_BLACKLIST} );
|
|
|
|
if ( my $timeout = $globals{DBL_TIMEOUT} ) {
|
|
add_ijump( $chainref, j => "SET --add-set $dbl_ipset src --exist --timeout $timeout" );
|
|
} else {
|
|
add_ijump( $chainref, j => "SET --add-set $dbl_ipset src --exist" );
|
|
}
|
|
|
|
$target = 'DROP';
|
|
} else {
|
|
add_ijump( $chainref , j => 'AUDIT', targetopts => '--type ' . lc $target ) if $chainref->{audit};
|
|
}
|
|
|
|
add_ijump( $chainref , g => $target eq 'REJECT' ? 'reject' : $target ) unless $target eq 'CONTINUE';
|
|
}
|
|
}
|
|
|
|
sub report_syn_flood_protection() {
|
|
progress_message_nocompress ' Enabled SYN flood Protection';
|
|
}
|
|
|
|
#
|
|
# Complete a policy chain - Add policy-enforcing rules and syn flood, if specified
|
|
#
|
|
sub complete_policy_chain( $$$ ) { #Chainref, Source Zone, Destination Zone
|
|
my $chainref = $_[0];
|
|
my $policyref = $filter_table->{$chainref->{policychain}};
|
|
my $synparams = $policyref->{synparams};
|
|
my $defaults = $policyref->{pactions};
|
|
my $policy = $policyref->{policy};
|
|
my $loglevel = $policyref->{loglevel};
|
|
|
|
assert( $policyref );
|
|
|
|
if ( $chainref eq $policyref ) {
|
|
add_policy_rules $chainref , $policy, $loglevel , $defaults, $config{MULTICAST};
|
|
} else {
|
|
if ( $policy eq 'ACCEPT' || $policy eq 'QUEUE' || $policy =~ /^NFQUEUE/ ) {
|
|
if ( $synparams ) {
|
|
report_syn_flood_protection;
|
|
add_policy_rules $chainref , $policy , $loglevel , $defaults, $config{MULTICAST};
|
|
} else {
|
|
add_ijump $chainref, g => $policyref;
|
|
$chainref = $policyref;
|
|
}
|
|
} elsif ( $policy eq 'CONTINUE' ) {
|
|
report_syn_flood_protection if $synparams;
|
|
add_policy_rules $chainref , $policy , $loglevel , $defaults, $config{MULTICAST};
|
|
} else {
|
|
report_syn_flood_protection if $synparams;
|
|
add_ijump $chainref , g => $policyref;
|
|
$chainref = $policyref;
|
|
}
|
|
}
|
|
|
|
progress_message_nocompress " Policy $policy from $_[1] to $_[2] using chain $chainref->{name}";
|
|
}
|
|
|
|
sub ensure_rules_chain( $ );
|
|
|
|
#
|
|
# Finish all policy Chains
|
|
#
|
|
sub complete_policy_chains() {
|
|
progress_message2 'Applying Policies...';
|
|
|
|
for my $chainref ( @policy_chains ) {
|
|
unless ( ( my $policy = $chainref->{policy} ) eq 'NONE' ) {
|
|
my $loglevel = $chainref->{loglevel};
|
|
my $provisional = $chainref->{provisional};
|
|
my $defaults = $chainref->{pactions};
|
|
my $name = $chainref->{name};
|
|
my $synparms = $chainref->{synparms};
|
|
|
|
unless ( $chainref->{referenced} || $provisional || $policy eq 'CONTINUE' ) {
|
|
if ( $config{OPTIMIZE} & 2 ) {
|
|
#
|
|
# This policy chain is empty and the only thing that we would put in it is
|
|
# the policy-related stuff. Don't create it if all we are going to put in it
|
|
# is a single jump. Generate_matrix() will just use the policy target when
|
|
# needed.
|
|
#
|
|
ensure_rules_chain $name if ( @$defaults ||
|
|
$loglevel ||
|
|
$synparms ||
|
|
$config{MULTICAST} ||
|
|
! ( $policy eq 'ACCEPT' || $config{FASTACCEPT} ) );
|
|
} else {
|
|
ensure_rules_chain $name;
|
|
}
|
|
}
|
|
|
|
if ( $name =~ /^all[-2]|[-2]all$/ ) {
|
|
add_policy_rules $chainref , $policy, $loglevel , $defaults, $config{MULTICAST};
|
|
}
|
|
}
|
|
}
|
|
|
|
for my $zone ( all_zones ) {
|
|
for my $zone1 ( all_zones ) {
|
|
my $chainref = $filter_table->{rules_chain( ${zone}, ${zone1} )};
|
|
|
|
if ( $chainref->{referenced} ) {
|
|
complete_policy_chain $chainref, $zone, $zone1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# 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_chain ( $$$$ ) {
|
|
my ( $stdchainref, $zone, $zone2, $default ) = @_;
|
|
|
|
my $ruleschainref = $filter_table->{rules_chain( ${zone}, ${zone2} ) } || $filter_table->{rules_chain( 'all', 'all' ) };
|
|
my ( $policy, $loglevel ) = ( $default , 6 );
|
|
my $policy_actions = $policy_actions{$policy};
|
|
my $policychainref;
|
|
|
|
$policychainref = $filter_table->{$ruleschainref->{policychain}} if $ruleschainref;
|
|
|
|
if ( $policychainref ) {
|
|
( $policy, $loglevel, $policy_actions ) = @{$policychainref}{'policy', 'loglevel', 'pactions' };
|
|
$stdchainref->{origin} = $policychainref->{origin};
|
|
}
|
|
|
|
add_policy_rules $stdchainref , $policy , $loglevel, $policy_actions, 0;
|
|
}
|
|
|
|
#
|
|
# Create and populate the synflood chains corresponding to entries in /etc/shorewall/policy
|
|
#
|
|
sub setup_syn_flood_chains() {
|
|
my @zones = ( non_firewall_zones );
|
|
for my $chainref ( @policy_chains ) {
|
|
my $limit = $chainref->{synparams};
|
|
if ( $limit && ! $filter_table->{syn_flood_chain $chainref} ) {
|
|
my $level = $chainref->{loglevel};
|
|
my $synchainref = @zones > 1 ?
|
|
new_chain 'filter' , syn_flood_chain $chainref :
|
|
new_chain( 'filter' , '@' . $chainref->{name} );
|
|
add_rule $synchainref , "${limit}-j RETURN";
|
|
log_irule_limit( $level ,
|
|
$synchainref ,
|
|
$synchainref->{name} ,
|
|
'DROP',
|
|
@{$globals{LOGILIMIT}} ? $globals{LOGILIMIT} : [ limit => "--limit 5/min --limit-burst 5" ] ,
|
|
'' ,
|
|
'add',
|
|
'' )
|
|
if $level ne '';
|
|
add_ijump $synchainref, j => 'DROP';
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Optimize Policy chains with ACCEPT policy
|
|
#
|
|
sub optimize_policy_chains() {
|
|
for my $chainref ( grep $_->{policy} eq 'ACCEPT', @policy_chains ) {
|
|
optimize_chain ( $chainref );
|
|
}
|
|
#
|
|
# Often, fw->all has an ACCEPT policy. This code allows optimization in that case
|
|
#
|
|
my $outputrules = $filter_table->{OUTPUT}{rules};
|
|
|
|
if ( @{$outputrules} && $outputrules->[-1]->{target} eq 'ACCEPT' ) {
|
|
optimize_chain( $filter_table->{OUTPUT} );
|
|
}
|
|
|
|
progress_message ' Policy chains optimized';
|
|
progress_message '';
|
|
}
|
|
|
|
################################################################################
|
|
# Modules moved from the Chains module in 4.4.18
|
|
################################################################################
|
|
|
|
#
|
|
# Add ESTABLISHED,RELATED,INVALID,UNTRACKED rules and synparam jumps to the passed chain
|
|
#
|
|
sub finish_chain_section ($$$) {
|
|
my ($chainref,
|
|
$chain1ref,
|
|
$state ) = @_;
|
|
my $chain = $chainref->{name};
|
|
my $save_comment = push_comment;
|
|
my %state;
|
|
|
|
$state{$_} = 1 for split ',', $state;
|
|
|
|
for ( qw/ESTABLISHED RELATED INVALID UNTRACKED/ ) {
|
|
delete $state{$_} if $chain1ref->{sections}{$_};
|
|
}
|
|
|
|
$chain1ref->{sections}{$_} = 1 for keys %state;
|
|
|
|
for ( qw( ESTABLISHED RELATED INVALID UNTRACKED ) ) {
|
|
if ( $state{$_} ) {
|
|
my ( $char, $level, $tag, $target , $origin, $level_origin ) = @{$statetable{$_}};
|
|
my $twochains = substr( $chainref->{name}, 0, 1 ) eq $char;
|
|
|
|
if ( $twochains || $level || $target ne 'ACCEPT' ) {
|
|
if ( $level ) {
|
|
my $chain2ref;
|
|
|
|
if ( $twochains ) {
|
|
$chain2ref = $chainref;
|
|
} else {
|
|
$chain2ref = new_chain( 'filter', "${char}$chainref->{name}" );
|
|
}
|
|
|
|
log_rule_limit( $level,
|
|
$chain2ref,
|
|
$chain2ref->{name},
|
|
uc $target,
|
|
$globals{LOGLIMIT},
|
|
$tag ,
|
|
'add' ,
|
|
'',
|
|
$level_origin );
|
|
|
|
$target = ensure_audit_chain( $target ) if ( $targets{$target} || 0 ) & AUDIT;
|
|
|
|
add_ijump_extended( $chain2ref, g => $target , $origin ) if $target;
|
|
|
|
$target = $chain2ref->{name} unless $twochains;
|
|
}
|
|
|
|
if ( $twochains ) {
|
|
add_ijump_extended $chainref, g => $target , $origin if $target;
|
|
delete $state{$_};
|
|
last;
|
|
}
|
|
|
|
if ( $target ) {
|
|
$target = ensure_audit_chain( $target ) if ( $targets{$target} || 0 ) & AUDIT;
|
|
#
|
|
# Always handle ESTABLISHED first
|
|
#
|
|
if ( $state{ESTABLISHED} && $_ ne 'ESTABLISHED' ) {
|
|
add_ijump( $chain1ref, j => 'ACCEPT', state_imatch 'ESTABLISHED' );
|
|
delete $state{ESTABLISHED};
|
|
}
|
|
|
|
add_ijump_extended( $chainref, j => $target, $origin, state_imatch $_ );
|
|
}
|
|
|
|
delete $state{$_};
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( keys %state ) {
|
|
my @state;
|
|
|
|
unless ( $config{FASTACCEPT} ) {
|
|
for ( qw/ESTABLISHED RELATED/ ) {
|
|
push @state, $_ if $state{$_};
|
|
}
|
|
}
|
|
|
|
push( @state, 'UNTRACKED' ),if $state{UNTRACKED} && $globals{UNTRACKED_TARGET} eq 'ACCEPT';
|
|
|
|
add_ijump( $chain1ref, j => 'ACCEPT', state_imatch join(',', @state ) ) if @state;
|
|
}
|
|
|
|
if ($sections{NEW} ) {
|
|
if ( $chain1ref->{is_policy} ) {
|
|
if ( $chain1ref->{synparams} ) {
|
|
my $synchainref = ensure_chain 'filter', syn_flood_chain $chain1ref;
|
|
if ( $section == POLICYACTION_SECTION ) {
|
|
if ( $chain1ref->{policy} =~ /^(ACCEPT|CONTINUE|QUEUE|NFQUEUE)/ ) {
|
|
add_ijump $chain1ref, j => $synchainref, p => 'tcp --syn';
|
|
}
|
|
} else {
|
|
add_ijump $chain1ref, j => $synchainref, p => 'tcp --syn';
|
|
}
|
|
}
|
|
} else {
|
|
my $policychainref = $filter_table->{$chain1ref->{policychain}};
|
|
if ( $policychainref->{synparams} ) {
|
|
my $synchainref = ensure_chain 'filter', syn_flood_chain $policychainref;
|
|
add_ijump $chain1ref, j => $synchainref, p => 'tcp --syn';
|
|
}
|
|
}
|
|
|
|
$chain1ref->{new} = @{$chain1ref->{rules}};
|
|
}
|
|
|
|
pop_comment( $save_comment );
|
|
}
|
|
|
|
#
|
|
# Create a rules chain if necessary and populate it with the appropriate ESTABLISHED,RELATED rule(s) and perform SYN rate limiting.
|
|
#
|
|
# Return a reference to the chain's table entry.
|
|
#
|
|
sub ensure_rules_chain( $ )
|
|
{
|
|
my ($chain) = @_;
|
|
|
|
my $chainref = $filter_table->{$chain};
|
|
|
|
$chainref = new_rules_chain( $chain ) unless $chainref;
|
|
|
|
unless ( $chainref->{referenced} ) {
|
|
if ( $section & ( NEW_SECTION | POLICYACTION_SECTION ) ) {
|
|
finish_chain_section $chainref , $chainref, 'ESTABLISHED,RELATED,INVALID,UNTRACKED';
|
|
} elsif ( $section == UNTRACKED_SECTION ) {
|
|
finish_chain_section $chainref , $chainref, 'ESTABLISHED,RELATED,INVALID';
|
|
} elsif ( $section == INVALID_SECTION ) {
|
|
finish_chain_section $chainref , $chainref, 'ESTABLISHED,RELATED';
|
|
} elsif ( $section == RELATED_SECTION ) {
|
|
finish_chain_section $chainref , $chainref, 'ESTABLISHED';
|
|
}
|
|
|
|
$chainref->{referenced} = 1;
|
|
}
|
|
|
|
$chainref;
|
|
}
|
|
|
|
#
|
|
# Do section-end processing
|
|
#
|
|
sub finish_section ( $ ) {
|
|
my $sections = $_[0];
|
|
|
|
$sections{$_} = 1 for split /,/, $sections;
|
|
|
|
my $function = $section_functions{$section} || \&rules_chain;
|
|
|
|
for my $zone ( all_zones ) {
|
|
for my $zone1 ( all_zones ) {
|
|
my $chainref = $filter_table->{$function->( $zone, $zone1 )};
|
|
my $chain1ref = $filter_table->{rules_chain( $zone, $zone1 )};
|
|
finish_chain_section $chainref || $chain1ref, $chain1ref, $sections if $chain1ref->{referenced};
|
|
}
|
|
}
|
|
}
|
|
################################################################################
|
|
# Functions moved from the Actions module in 4.4.16
|
|
################################################################################
|
|
#
|
|
# Create a normalized action name from the passed pieces.
|
|
#
|
|
# Internally, action invocations are uniquely identified by a 5-tuple that
|
|
# includes the action name, log level, log tag, calling chain and params.
|
|
# The pieces of the tuple are separated by ":". The calling chain is non-empty
|
|
# only when the action refers to @CALLER.
|
|
#
|
|
sub normalize_action( $$$ ) {
|
|
my ( $action, $level, $param ) = @_;
|
|
my $caller = ''; #We assume that the action doesn't use @CALLER
|
|
|
|
( $level, my $tag ) = split ':', $level;
|
|
|
|
if ( $actions{$action}{options} & LOGJUMP_OPT ) {
|
|
$level = 'none';
|
|
} else {
|
|
$level = 'none' unless supplied $level;
|
|
}
|
|
#
|
|
# Note: SNAT actions store the current interface's name in the tag
|
|
#
|
|
$tag = '' unless defined $tag;
|
|
|
|
if ( defined( $param ) ) {
|
|
#
|
|
# Normalize the parameters by removing trailing omitted
|
|
# parameters
|
|
#
|
|
1 while $param =~ s/,-$//;
|
|
|
|
$param = '' if $param eq '-';
|
|
} else {
|
|
$param = '';
|
|
}
|
|
|
|
join( ':', $action, $level, $tag, $caller, $param );
|
|
}
|
|
|
|
#
|
|
# Add the actual caller into an existing normalised name
|
|
#
|
|
sub insert_caller($$) {
|
|
my ( $normalized, $caller ) = @_;
|
|
|
|
my ( $action, $level, $tag, undef, $param ) = split /:/, $normalized;
|
|
|
|
join( ':', $action, $level, $tag, $caller, $param );
|
|
}
|
|
|
|
#
|
|
# Accepts a rule target and returns a normalized tuple
|
|
#
|
|
sub normalize_action_name( $ ) {
|
|
my $target = shift;
|
|
my ( $action, $loglevel) = split_action $target;
|
|
|
|
normalize_action( $action, $loglevel, '' );
|
|
}
|
|
|
|
#
|
|
# Create an action tuple from a single target name
|
|
#
|
|
sub normalize_single_action( $ ) {
|
|
join(":", $_[0], 'none', '', '', '' );
|
|
}
|
|
|
|
#
|
|
# Produce a recognizable target from a normalized action
|
|
#
|
|
sub external_name( $ ) {
|
|
my ( $target, $level, $tag, undef, $params ) = split /:/, shift, ACTION_TUPLE_ELEMENTS;
|
|
|
|
$target = join( '', $target, '(', $params , ')' ) if $params;
|
|
$target .= ":$level" if $level && $level ne 'none';
|
|
$target .= ":$tag" if $tag;
|
|
$target;
|
|
}
|
|
|
|
#
|
|
# Define an Action
|
|
#
|
|
sub new_action( $$$$$$ ) {
|
|
|
|
my ( $action , $type, $options , $actionfile , $state, $proto ) = @_;
|
|
|
|
fatal_error "Reserved action name ($action)" if reserved_name( $action );
|
|
|
|
$actions{$action} = { file => $actionfile, actchain => '' , type => $type, options => $options , state => $state, proto => $proto };
|
|
|
|
$targets{$action} = $type;
|
|
}
|
|
|
|
#
|
|
# Create and record a log action chain -- Log action chains have names
|
|
# that are formed from the action name by prepending a "%" and appending
|
|
# a 1- or 2-digit sequence number.
|
|
#
|
|
# The maximum length of a chain name is 30 characters -- since the log
|
|
# action chain name is 2-3 characters longer than the base chain name,
|
|
# this function truncates the original chain name where necessary before
|
|
# it adds the leading "%" and trailing sequence number.
|
|
#
|
|
sub createlogactionchain( $$$$$$ ) {
|
|
my ( $table, $normalized, $action, $level, $tag, $param ) = @_;
|
|
my $chain = $action;
|
|
my $actionref = $actions{$action};
|
|
my $chainref;
|
|
my $tableref = $chain_table{$table};
|
|
|
|
validate_level $level;
|
|
|
|
assert( $actionref );
|
|
|
|
$chain = substr $chain, 0, 28 if ( length $chain ) > 28;
|
|
|
|
if ( $tableref->{$chain} ) {
|
|
CHECKDUP:
|
|
{
|
|
$actionref->{actchain}++ while $chain_table{filter}{'%' . $chain . $actionref->{actchain}};
|
|
$chain = substr( $chain, 0, 27 ), redo CHECKDUP if ( $actionref->{actchain} || 0 ) >= 10 and length $chain == 28;
|
|
}
|
|
|
|
$usedactions{$normalized} = $chainref = new_action_chain( $table, '%' . $chain . $actionref->{actchain}++ );
|
|
|
|
fatal_error "Too many invocations of Action $action" if $actionref->{actchain} > 99;
|
|
} else {
|
|
$usedactions{$normalized} = $chainref = new_action_chain( $table, $chain );
|
|
}
|
|
|
|
$chainref->{action} = $normalized;
|
|
|
|
$chainref;
|
|
}
|
|
|
|
sub createsimpleactionchain( $$ ) {
|
|
my ( $table, $action ) = @_;
|
|
my $normalized = normalize_action_name( $action );
|
|
|
|
return createlogactionchain( $table, $normalized, $action, 'none', '', '' ) if $filter_table->{$action} || $nat_table->{$action};
|
|
|
|
my $chainref = new_action_chain( $table, $action );
|
|
|
|
$usedactions{$normalized} = $chainref;
|
|
|
|
$chainref->{action} = $normalized;
|
|
|
|
$chainref;
|
|
}
|
|
|
|
#
|
|
# Create an action chain and run its associated user exit
|
|
#
|
|
sub createactionchain( $$ ) {
|
|
my ( $table, $normalized ) = @_;
|
|
|
|
my ( $target, $level, $tag, $caller, $param ) = split /:/, $normalized, ACTION_TUPLE_ELEMENTS;
|
|
|
|
assert( defined $param );
|
|
|
|
my $chainref;
|
|
|
|
if ( $level eq 'none' && $tag eq '' && $param eq '' ) {
|
|
createsimpleactionchain($table, $target );
|
|
} else {
|
|
createlogactionchain( $table, $normalized, $target , $level , $tag, $param );
|
|
}
|
|
}
|
|
|
|
#
|
|
# Mark an action as used and create its chain. Returns a reference to the chain if the chain was
|
|
# created on this call or 0 otherwise.
|
|
#
|
|
sub use_action( $$ ) {
|
|
my ( $table, $normalized ) = @_;
|
|
|
|
if ( $usedactions{$normalized} ) {
|
|
0;
|
|
} else {
|
|
createactionchain( $table, $normalized );
|
|
}
|
|
}
|
|
|
|
#
|
|
# This function determines the logging and params for a subordinate action or a rule within a superior action
|
|
#
|
|
sub merge_levels ($$) {
|
|
my ( $superior, $subordinate ) = @_;
|
|
|
|
return $subordinate if $subordinate =~ /^(?:FORMAT|COMMENT|DEFAULTS?)$/;
|
|
|
|
my @supparts = split /:/, $superior;
|
|
my @subparts = split /:/, $subordinate;
|
|
|
|
my $subparts = @subparts;
|
|
|
|
my $target = $subparts[0];
|
|
|
|
fatal_error "Missing ACTION" unless supplied $target;
|
|
|
|
push @subparts, '' while @subparts < 3; #Avoid undefined values
|
|
|
|
my $sublevel = $subparts[1];
|
|
my $level = $supparts[1];
|
|
my $tag = $supparts[2];
|
|
|
|
if ( @supparts == 3 ) {
|
|
return "$subordinate:$tag" if $target =~ /^(?:NFLOG|ULOG)\b/;
|
|
return "$target:none!:$tag" if $level eq 'none!';
|
|
return "$target:$level:$tag" if $level =~ /!$/;
|
|
return $subordinate if $subparts >= 2;
|
|
return "$target:$level:$tag";
|
|
}
|
|
|
|
if ( @supparts == 2 ) {
|
|
return $subordinate if $target =~ /^(?:NFLOG|ULOG)\b/;
|
|
return "$target:none!" if $level eq 'none!';
|
|
return "$target:$level" if ($level =~ /!$/) || ($subparts < 2);
|
|
}
|
|
|
|
$subordinate;
|
|
}
|
|
|
|
#
|
|
# Try to find a macro file -- RETURNS false if the file doesn't exist or MACRO if it does.
|
|
# If the file exists, the macro is entered into the 'targets' table and the fully-qualified
|
|
# name of the file is stored in the 'macro' table.
|
|
#
|
|
sub find_macro( $ )
|
|
{
|
|
my $macro = $_[0];
|
|
|
|
$macro =~ s/^macro\.//;
|
|
|
|
my $macrofile = find_file "macro.$macro";
|
|
|
|
if ( -f $macrofile ) {
|
|
$macros{$macro} = $macrofile;
|
|
$targets{$macro} = MACRO;
|
|
} else {
|
|
0;
|
|
}
|
|
}
|
|
|
|
#
|
|
# This function substitutes the second argument for the first part of the first argument up to the first colon (":")
|
|
#
|
|
# Example:
|
|
#
|
|
# substitute_param DNAT PARAM:info:FTP
|
|
#
|
|
# produces "DNAT:info:FTP"
|
|
#
|
|
sub substitute_param( $$ ) {
|
|
my ( $param, $action ) = @_;
|
|
|
|
if ( $action =~ /:/ ) {
|
|
my $logpart = (split_action $action)[1];
|
|
$logpart =~ s!/$!!;
|
|
return "$param:$logpart";
|
|
}
|
|
|
|
$param;
|
|
}
|
|
|
|
#
|
|
# Combine fields from a macro body with one from the macro invocation
|
|
#
|
|
sub merge_macro_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 || '';
|
|
}
|
|
|
|
#
|
|
# This one is used by snat inline
|
|
#
|
|
sub merge_inline_source_dest( $$ ) {
|
|
my ( $body, $invocation ) = @_;
|
|
|
|
if ( $invocation ) {
|
|
if ( supplied $body && $body ne '-' ) {
|
|
return $body if $invocation eq '-';
|
|
|
|
if ( $family == F_IPV4 ) {
|
|
fatal_error 'Interface names cannot appear in the DEST column within an action body' if $body =~ /:/;
|
|
|
|
if ( $invocation =~ /:/ ) {
|
|
$invocation =~ s/:.*//;
|
|
return join( ':', $invocation, $body );
|
|
}
|
|
} else {
|
|
fatal_error 'Interface names cannot appear in the DEST column within an action body' if $body =~ /:\[|:\+/;
|
|
|
|
if ( $invocation =~ /:\[|:\+/ ) {
|
|
$invocation =~ s/:.*//;
|
|
return join( ':', $invocation, $body );
|
|
}
|
|
}
|
|
|
|
return "$invocation:$body";
|
|
}
|
|
|
|
return $invocation;
|
|
}
|
|
|
|
$body || '';
|
|
}
|
|
|
|
#
|
|
# This one is used by perl_action_helper()
|
|
#
|
|
sub merge_action_column( $$ ) {
|
|
my ( $body, $invocation ) = @_;
|
|
|
|
if ( supplied( $body ) && $body ne '-' ) {
|
|
$body;
|
|
} else {
|
|
$invocation;
|
|
}
|
|
}
|
|
|
|
sub merge_macro_column( $$ ) {
|
|
my ( $body, $invocation ) = @_;
|
|
|
|
if ( supplied( $invocation ) && $invocation ne '-' ) {
|
|
$invocation;
|
|
} else {
|
|
$body;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Get Macro Name -- strips away trailing /*, :* and (*) from the first column in a rule, macro or action.
|
|
#
|
|
sub isolate_basic_target( $ ) {
|
|
my $target = $_[0];
|
|
|
|
if ( $target =~ /[\/]/ ) {
|
|
( $target ) = split( '/', $target);
|
|
} else {
|
|
( $target ) = split_list2( $target, 'parameter' );
|
|
}
|
|
|
|
$target =~ /^(\w+)[(].*[)]$/ ? $1 : $target;
|
|
}
|
|
|
|
sub process_rule ( $$$$$$$$$$$$$$$$$$$$ );
|
|
sub process_mangle_rule1( $$$$$$$$$$$$$$$$$$$ );
|
|
sub process_snat1( $$$$$$$$$$$$ );
|
|
sub perl_action_helper( $$;$$ );
|
|
|
|
#
|
|
# Populate an action invocation chain. As new action tuples are encountered,
|
|
# the function will be called recursively by process_rule().
|
|
#
|
|
# Note that the first two parameters are passed by reference and may be
|
|
# modified by this function.
|
|
#
|
|
sub process_action(\$\$$) {
|
|
my ( $wholeactionref, $chainrefref, $caller ) = @_;
|
|
my $wholeaction = ${$wholeactionref};
|
|
my $chainref = ${$chainrefref};
|
|
my ( $action, $level, $tag, undef, $param ) = split /:/, $wholeaction, ACTION_TUPLE_ELEMENTS;
|
|
my $type = $targets{$action};
|
|
my $actionref = $actions{$action};
|
|
my $matches = fetch_inline_matches;
|
|
|
|
if ( $type & MANGLE_TABLE ) {
|
|
fatal_error "Action $action may only be used in the mangle file" unless $chainref->{table} eq 'mangle';
|
|
} else {
|
|
fatal_error "Action $action may not be used in the mangle file" if $chainref->{table} eq 'mangle';
|
|
}
|
|
|
|
if ( $type & NAT_TABLE ) {
|
|
fatal_error "Action $action may only be used in the snat file" unless $chainref->{table} eq 'nat';
|
|
} else {
|
|
fatal_error "Action $action may not be used in the snat file" if $chainref->{table} eq 'nat';
|
|
}
|
|
|
|
$param = $1 if $param =~ /^.*\|(.*)$/; #Strip interface name off of the parameters
|
|
|
|
my $actionfile = $actionref->{file};
|
|
|
|
progress_message2 "$doing $actionfile for chain $chainref->{name}...";
|
|
|
|
my $oldparms = push_action_params( $action, $chainref, $param, $level, $tag, $caller );
|
|
my $options = $actionref->{options};
|
|
my $nolog = $options & ( NOLOG_OPT | LOGJUMP_OPT );
|
|
|
|
push_open $actionfile, 2, 1, undef, 2;
|
|
|
|
setup_audit_action( $action ) if $options & AUDIT_OPT;
|
|
|
|
$active{$action}++;
|
|
push @actionstack, $wholeaction;
|
|
|
|
my $save_comment = push_comment;
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
unless ( $type & ( MANGLE_TABLE | NAT_TABLE | RAW_TABLE ) ) {
|
|
my ($target, $source, $dest, $protos, $ports, $sports, $origdest, $rate, $users, $mark, $connlimit, $time, $headers, $condition, $helper );
|
|
|
|
if ( $file_format == 1 ) {
|
|
fatal_error( "FORMAT-1 actions are no longer supported" );
|
|
} else {
|
|
($target, $source, $dest, $protos, $ports, $sports, $origdest, $rate, $users, $mark, $connlimit, $time, $headers, $condition, $helper )
|
|
= split_line2( 'action file',
|
|
\%rulecolumns,
|
|
$action_commands,
|
|
undef,
|
|
1 );
|
|
}
|
|
|
|
fatal_error 'TARGET must be specified' if $target eq '-';
|
|
|
|
if ( $target eq 'DEFAULTS' ) {
|
|
default_action_params( $action, split_list $source, 'defaults' );
|
|
|
|
if ( my $state = $actionref->{state} ) {
|
|
my ( $action ) = get_action_params( 1 );
|
|
|
|
if ( my $check = check_state( $state ) ) {
|
|
perl_action_helper( $action, $check == 1 ? state_match( $state ) : '' , $state );
|
|
}
|
|
}
|
|
|
|
next;
|
|
}
|
|
|
|
for my $proto ( split_list( $protos, 'Protocol' ) ) {
|
|
for my $user ( split_list( $users, 'User/Group' ) ) {
|
|
process_rule( $chainref,
|
|
'',
|
|
'',
|
|
$nolog ? $target : merge_levels( join(':', @actparams{'chain','loglevel','logtag'}), $target ),
|
|
'',
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$origdest,
|
|
$rate,
|
|
$user,
|
|
$mark,
|
|
$connlimit,
|
|
$time,
|
|
$headers,
|
|
$condition,
|
|
$helper,
|
|
0 );
|
|
|
|
set_inline_matches( $matches );
|
|
}
|
|
}
|
|
} elsif ( $type & MANGLE_TABLE ) {
|
|
my ( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state, $time, $conditional );
|
|
|
|
if ( $family == F_IPV4 ) {
|
|
( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $probability, $dscp, $state, $time, $conditional ) =
|
|
split_line2( 'mangle file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
probability => 12 ,
|
|
scp => 13,
|
|
state => 14,
|
|
time => 15,
|
|
switch => 16,
|
|
},
|
|
{},
|
|
17,
|
|
1 );
|
|
$headers = '-';
|
|
} else {
|
|
( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability, $dscp, $state, $time, $conditional ) =
|
|
split_line2( 'action file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
headers => 12,
|
|
probability => 13,
|
|
dscp => 14,
|
|
state => 15,
|
|
time => 16,
|
|
switch => 17,
|
|
},
|
|
{},
|
|
18,
|
|
1 );
|
|
}
|
|
|
|
fatal_error 'ACTION must be specified' if $originalmark eq '-';
|
|
|
|
if ( $originalmark eq 'DEFAULTS' ) {
|
|
default_action_params( $action, split_list $source, 'defaults' );
|
|
next;
|
|
}
|
|
|
|
for my $proto (split_list( $protos, 'Protocol' ) ) {
|
|
process_mangle_rule1( $chainref,
|
|
$originalmark,
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$user,
|
|
$testval,
|
|
$length,
|
|
$tos ,
|
|
$connbytes,
|
|
$helper,
|
|
$headers,
|
|
$probability ,
|
|
$dscp ,
|
|
$state,
|
|
$time,
|
|
$conditional );
|
|
set_inline_matches( $matches );
|
|
}
|
|
} else {
|
|
my ( $action, $source, $dest, $protos, $port, $ipsec, $mark, $user, $condition, $origdest, $probability) =
|
|
split_line2( 'snat file',
|
|
{ action =>0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
port => 4,
|
|
ipsec => 5,
|
|
mark => 6,
|
|
user => 7,
|
|
switch => 8,
|
|
origdest => 9,
|
|
probability => 10,
|
|
},
|
|
{},
|
|
11,
|
|
1 );
|
|
|
|
fatal_error 'ACTION must be specified' if $action eq '-';
|
|
|
|
if ( $action eq 'DEFAULTS' ) {
|
|
default_action_params( $chainref, split_list( $source, 'defaults' ) );
|
|
next;
|
|
}
|
|
|
|
for my $proto (split_list( $protos, 'Protocol' ) ) {
|
|
process_snat1( $chainref,
|
|
$nolog ? $action : merge_levels( join(':', @actparams{'chain','loglevel','logtag'}), $action ),
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$port,
|
|
$ipsec,
|
|
$mark,
|
|
$user,
|
|
$condition,
|
|
$origdest,
|
|
$probability,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pop_comment( $save_comment );
|
|
|
|
$active{$action}--;
|
|
pop @actionstack;
|
|
|
|
pop_open;
|
|
|
|
unless ( @{$chainref->{rules}} ) {
|
|
my $file = find_file( $action );
|
|
|
|
fatal_error "File action.${action} is empty and file $action exists - the two must be combined as described in the Migration Considerations section of the Shorewall release notes" if -f $file;
|
|
}
|
|
|
|
#
|
|
# Pop the action parameters
|
|
#
|
|
if ( ( my $result = pop_action_params( $oldparms ) ) & PARMSMODIFIED ) {
|
|
#
|
|
# The action modified its parameters -- delete it from %usedactions
|
|
#
|
|
delete $usedactions{$wholeaction};
|
|
} elsif ( $result & USEDCALLER ) {
|
|
#
|
|
# The chain uses @CALLER but doesn't modify the action parameters.
|
|
# We need to see if this caller has already invoked this action
|
|
#
|
|
my $renormalized_action = insert_caller( $wholeaction, $caller );
|
|
my $chain1ref = $usedactions{$renormalized_action};
|
|
|
|
if ( $chain1ref ) {
|
|
#
|
|
# It has -- use the prior chain
|
|
#
|
|
${$chainrefref} = $chain1ref;
|
|
#
|
|
# We leave the new chain in place but delete it from %usedactions below
|
|
# The optimizer will drop it from the final ruleset.
|
|
#
|
|
} else {
|
|
#
|
|
# This is the first time that the current chain has invoked this action
|
|
#
|
|
$usedactions{$renormalized_action} = $chainref;
|
|
#
|
|
# Update the action member
|
|
#
|
|
$chainref->{action} = $renormalized_action;
|
|
}
|
|
#
|
|
# Delete the usedactions entry with the original normalized key
|
|
#
|
|
delete $usedactions{$wholeaction};
|
|
#
|
|
# New normalized target
|
|
#
|
|
${$wholeactionref} = $renormalized_action;
|
|
}
|
|
}
|
|
|
|
#
|
|
# This function is called prior to processing of the policy file. It:
|
|
#
|
|
# - Reads actions.std and actions (in that order) and for each entry:
|
|
# o Adds the action to the target table
|
|
# o Verifies that the corresponding action file exists
|
|
#
|
|
|
|
sub process_actions() {
|
|
|
|
progress_message2 "Locating Action Files...";
|
|
|
|
for my $file ( qw/actions.std actions/ ) {
|
|
open_file( $file, 2 );
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
my ( $action, $options ) = split_line2( 'action file',
|
|
{ action => 0, options => 1 },
|
|
{}, #Nopad
|
|
undef, #Columns
|
|
1 ); #Allow inline matches
|
|
|
|
my $type = ( $action eq $config{REJECT_ACTION} ? INLINE : ACTION );
|
|
|
|
my $opts = $type == INLINE ? NOLOG_OPT : 0;
|
|
|
|
my $state = '';
|
|
my $proto = 0;
|
|
|
|
if ( $action =~ /:/ ) {
|
|
warning_message 'Policy Actions are now specified in /etc/shorewall/shorewall.conf';
|
|
$action =~ s/:.*$//;
|
|
}
|
|
|
|
fatal_error "Invalid Action Name ($action)" unless $action =~ /^[a-zA-Z][\w-]*!?$/;
|
|
|
|
if ( $options ne '-' ) {
|
|
for ( split_list( $options, 'option' ) ) {
|
|
if ( /^state=(NEW|ESTABLISHED|RELATED|INVALID|UNTRACKED)$/ ) {
|
|
if ( $file eq 'actions.std' ) {
|
|
$state = $1;
|
|
} else {
|
|
fatal_error( q(The 'state' option is reserved for use in the actions.std file) );
|
|
}
|
|
} elsif ( /^proto=(.+)$/ ) {
|
|
fatal_error "Unknown Protocol ($1)" unless defined( $proto = resolve_proto( $1 ) );
|
|
fatal_error "A protocol may not be specified on the REJECT_ACTION ($action)" if $action eq $config{REJECT_ACTION};
|
|
} else {
|
|
fatal_error "Invalid option ($_)" unless $options{$_};
|
|
$opts |= $options{$_};
|
|
}
|
|
}
|
|
|
|
unless ( $type & INLINE ) {
|
|
$type = INLINE if $opts & INLINE_OPT;
|
|
}
|
|
|
|
fatal_error "Conflicting OPTIONS ($options)" if ( $opts & NOINLINE_OPT && $type == INLINE ) || ( $opts & INLINE_OPT && $opts & BUILTIN_OPT );
|
|
}
|
|
|
|
if ( my $actiontype = $targets{$action} ) {
|
|
if ( ( $actiontype & ACTION ) && ( $type == INLINE ) ) {
|
|
if ( $actions{$action}{options} & NOINLINE_OPT ) {
|
|
warning_message "'inline' option ignored on action $action -- that action may not be in-lined";
|
|
next;
|
|
}
|
|
|
|
$proto = $actions{$action}{proto} unless $proto;
|
|
delete $actions{$action};
|
|
delete $targets{$action};
|
|
} elsif ( ( $actiontype & INLINE ) && ( $type == ACTION ) && $opts & NOINLINE_OPT ) {
|
|
$proto = $actions{$action}{proto} unless $proto;
|
|
delete $actions{$action};
|
|
delete $targets{$action};
|
|
} else {
|
|
warning_message "Duplicate Action Name ($action) Ignored" unless $actiontype & ( ACTION | INLINE );
|
|
next;
|
|
}
|
|
}
|
|
|
|
if ( $opts & BUILTIN_OPT ) {
|
|
warning_message( "The 'proto' option has no effect when specified on a builtin action" ) if $proto;
|
|
|
|
my $actiontype = USERBUILTIN | OPTIONS;
|
|
$actiontype |= MANGLE_TABLE if $opts & MANGLE_OPT;
|
|
$actiontype |= RAW_TABLE if $opts & RAW_OPT;
|
|
$actiontype |= NAT_TABLE if $opts & NAT_OPT;
|
|
#
|
|
# For backward compatibility, we assume that user-defined builtins are valid in the filter table
|
|
#
|
|
$actiontype |= FILTER_TABLE if $opts & FILTER_OPT || ! ( $opts & ( MANGLE_OPT | RAW_OPT | NAT_OPT ) );
|
|
|
|
if ( $builtin_target{$action} ) {
|
|
$builtin_target{$action} |= $actiontype;
|
|
} else {
|
|
$builtin_target{$action} = $actiontype;
|
|
}
|
|
|
|
$targets{$action} = $actiontype;
|
|
|
|
make_terminating( $action ) if $opts & TERMINATING_OPT
|
|
} else {
|
|
fatal_error "The 'raw' table may not be specified for non-builtin actions" if $opts & RAW_OPT;
|
|
|
|
$type |= MANGLE_TABLE if $opts & MANGLE_OPT;
|
|
|
|
if ( $opts & NAT_OPT ) {
|
|
fatal_error q(The 'mangle' and 'nat' options are mutually exclusive) if $opts & MANGLE_OPT;
|
|
$type |= NAT_TABLE;
|
|
}
|
|
|
|
my $actionfile = find_file( "action.$action" );
|
|
|
|
fatal_error "Missing Action File ($actionfile)" unless -f $actionfile;
|
|
|
|
new_action ( $action, $type, $opts, $actionfile , $state , $proto );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( my $action = $config{REJECT_ACTION} ) {
|
|
my $type = $targets{$action};
|
|
fatal_error "REJECT_ACTION ($action) was not defined" unless $type;
|
|
fatal_error "REJECT_ACTION ($action) is not an action" unless $type == INLINE;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Create a policy action if it doesn't already exist
|
|
#
|
|
sub use_policy_action( $$ ) {
|
|
my ( $normalized_target, $caller ) = @_;
|
|
|
|
my $ref = use_action( 'filter', $normalized_target );
|
|
|
|
if ( $ref ) {
|
|
process_action( $normalized_target, $ref, $caller );
|
|
} else {
|
|
$ref = $usedactions{$normalized_target};
|
|
}
|
|
|
|
$ref;
|
|
}
|
|
|
|
#
|
|
# Process the REJECT_ACTION
|
|
#
|
|
sub process_reject_action() {
|
|
my $rejectref = $filter_table->{reject};
|
|
my $action = $config{REJECT_ACTION};
|
|
#
|
|
# This gets called very early in the compilation process so we fake the section
|
|
#
|
|
$section = POLICYACTION_SECTION;
|
|
|
|
if ( ( $targets{$action} || 0 ) == ACTION ) {
|
|
add_ijump $rejectref, j => use_policy_action( $action, $rejectref->{name} );
|
|
} else {
|
|
progress_message2 "$doing $actions{$action}->{file} for chain reject...";
|
|
|
|
process_inline( $action, #Inline
|
|
$rejectref, #Chain
|
|
'', #Matches
|
|
'', #Matches1
|
|
'', #Log Level and Tag
|
|
$action, #Target
|
|
'', #Param
|
|
'-', #Source
|
|
'-', #Dest
|
|
'-', #Proto
|
|
'-', #Ports
|
|
'-', #Sports
|
|
'-', #Original Dest
|
|
'-', #Rate
|
|
'-', #User
|
|
'-', #Mark
|
|
'-', #ConnLimit
|
|
'-', #Time
|
|
'-', #Headers
|
|
'-', #Condition
|
|
'-', #Helper
|
|
0, #Wildcard
|
|
);
|
|
}
|
|
|
|
$section = '';
|
|
}
|
|
|
|
################################################################################
|
|
# End of functions moved from the Actions module in 4.4.16
|
|
################################################################################
|
|
#
|
|
# Expand a macro rule from the rules file
|
|
#
|
|
sub process_macro ($$$$$$$$$$$$$$$$$$$$$) {
|
|
my ($macro, $chainref, $matches, $matches1, $target, $param, $source, $dest, $proto, $ports, $sports, $origdest, $rate, $user, $mark, $connlimit, $time, $headers, $condition, $helper, $wildcard ) = @_;
|
|
|
|
my $generated = 0;
|
|
|
|
|
|
my $macrofile = $macros{$macro};
|
|
my $save_matches = fetch_inline_matches;
|
|
|
|
progress_message "..Expanding Macro $macrofile...";
|
|
|
|
push_open $macrofile, 2, 1, no_comment, 2;
|
|
|
|
macro_comment $macro;
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
|
|
my ( $mtarget, $msource, $mdest, $mprotos, $mports, $msports, $morigdest, $mrate, $musers, $mmark, $mconnlimit, $mtime, $mheaders, $mcondition, $mhelper);
|
|
|
|
if ( $file_format == 1 ) {
|
|
fatal_error( "FORMAT-1 macros are no longer supported" );
|
|
} else {
|
|
( $mtarget,
|
|
$msource,
|
|
$mdest,
|
|
$mprotos,
|
|
$mports,
|
|
$msports,
|
|
$morigdest,
|
|
$mrate,
|
|
$musers,
|
|
$mmark,
|
|
$mconnlimit,
|
|
$mtime,
|
|
$mheaders,
|
|
$mcondition,
|
|
$mhelper ) = split_line2( 'macro file',
|
|
\%rulecolumns,
|
|
$rule_commands,
|
|
undef, #Columns
|
|
1 ); #Allow inline matches
|
|
}
|
|
|
|
fatal_error 'TARGET must be specified' if $mtarget eq '-';
|
|
|
|
if ( $mtarget =~ /^DEFAULTS?$/ ) {
|
|
$param = $msource unless supplied $param;
|
|
next;
|
|
}
|
|
|
|
$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 = $targets{$action} || find_macro( $action );
|
|
|
|
fatal_error( "Invalid Action ($mtarget) in macro") unless $actiontype & ( ACTION + STANDARD + NATRULE + MACRO + CHAIN );
|
|
|
|
if ( $msource ) {
|
|
if ( $msource eq '-' ) {
|
|
$msource = $source || '';
|
|
} elsif ( $msource =~ s/^DEST:?// ) {
|
|
$msource = merge_macro_source_dest $msource, $dest;
|
|
} else {
|
|
$msource =~ s/^SOURCE:?//;
|
|
$msource = merge_macro_source_dest $msource, $source;
|
|
}
|
|
} else {
|
|
$msource = '';
|
|
}
|
|
|
|
if ( $mdest ) {
|
|
if ( $mdest eq '-' ) {
|
|
$mdest = $dest || '';
|
|
} elsif ( $mdest =~ s/^SOURCE:?// ) {
|
|
$mdest = merge_macro_source_dest $mdest , $source;
|
|
} else {
|
|
$mdest =~ s/DEST:?//;
|
|
$mdest = merge_macro_source_dest $mdest, $dest;
|
|
}
|
|
} else {
|
|
$mdest = '';
|
|
}
|
|
|
|
for my $mp ( split_list( $mprotos, 'Protocol' ) ) {
|
|
for my $mu ( split_list( $musers, 'User/Group' ) ) {
|
|
$generated |= process_rule( $chainref,
|
|
$matches,
|
|
$matches1,
|
|
$mtarget,
|
|
$param,
|
|
$msource,
|
|
$mdest,
|
|
merge_macro_column( $mp, $proto ) ,
|
|
merge_macro_column( $mports, $ports ) ,
|
|
merge_macro_column( $msports, $sports ) ,
|
|
merge_macro_column( $morigdest, $origdest ) ,
|
|
merge_macro_column( $mrate, $rate ) ,
|
|
merge_macro_column( $mu, $user ) ,
|
|
merge_macro_column( $mmark, $mark ) ,
|
|
merge_macro_column( $mconnlimit, $connlimit) ,
|
|
merge_macro_column( $mtime, $time ),
|
|
merge_macro_column( $mheaders, $headers ),
|
|
merge_macro_column( $mcondition, $condition ),
|
|
merge_macro_column( $mhelper, $helper ),
|
|
$wildcard
|
|
);
|
|
|
|
set_inline_matches( $save_matches );
|
|
}
|
|
}
|
|
|
|
progress_message " Rule \"$currentline\" $done";
|
|
}
|
|
|
|
pop_open;
|
|
|
|
progress_message "..End Macro $macrofile";
|
|
|
|
return $generated;
|
|
}
|
|
|
|
#
|
|
# Expand an inline action rule from the rules file
|
|
#
|
|
sub process_inline ($$$$$$$$$$$$$$$$$$$$$$) {
|
|
my ($inline, $chainref, $matches, $matches1, $loglevel, $target, $param, $source, $dest, $proto, $ports, $sports, $origdest, $rate, $user, $mark, $connlimit, $time, $headers, $condition, $helper, $wildcard ) = @_;
|
|
|
|
my $generated = 0;
|
|
|
|
my ( $level, $tag ) = split( ':', $loglevel, 2 );
|
|
|
|
my $oldparms = push_action_params( $inline,
|
|
$chainref,
|
|
$param,
|
|
supplied $level ? $level : 'none',
|
|
defined $tag ? $tag : '' ,
|
|
$chainref->{name} ,
|
|
);
|
|
|
|
my $actionref = $actions{$inline};
|
|
my $inlinefile = $actionref->{file};
|
|
my $options = $actionref->{options};
|
|
my $nolog = $options & NOLOG_OPT;
|
|
my $save_matches = fetch_inline_matches;
|
|
|
|
setup_audit_action( $inline ) if $options & AUDIT_OPT;
|
|
|
|
progress_message "..Expanding inline action $inlinefile..." unless $inline eq $config{REJECT_ACTION};
|
|
|
|
push_open $inlinefile, 2, 1, undef , 2;
|
|
|
|
my $save_comment = push_comment;
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
my ( $mtarget,
|
|
$msource,
|
|
$mdest,
|
|
$mprotos,
|
|
$mports,
|
|
$msports,
|
|
$morigdest,
|
|
$mrate,
|
|
$musers,
|
|
$mmark,
|
|
$mconnlimit,
|
|
$mtime,
|
|
$mheaders,
|
|
$mcondition,
|
|
$mhelper ) = split_line2( 'inline action file',
|
|
\%rulecolumns,
|
|
$rule_commands,
|
|
undef, #Columns
|
|
1 ); #Allow inline matches
|
|
|
|
|
|
fatal_error 'TARGET must be specified' if $mtarget eq '-';
|
|
|
|
if ( $mtarget eq 'DEFAULTS' ) {
|
|
default_action_params( $chainref, split_list( $msource, 'defaults' ) );
|
|
|
|
if ( my $state = $actionref->{state} ) {
|
|
my ( $action ) = get_action_params( 1 );
|
|
|
|
if ( my $check = check_state( $state ) ) {
|
|
perl_action_helper( $action, $check == 1 ? state_match( $state ) : '' , $state );
|
|
}
|
|
}
|
|
|
|
next;
|
|
}
|
|
|
|
$mtarget = merge_levels( join(':', @actparams{'chain','loglevel','logtag'}), $mtarget ) unless $nolog;
|
|
|
|
my $action = isolate_basic_target $mtarget;
|
|
|
|
fatal_error "Invalid or missing ACTION ($mtarget)" unless defined $action;
|
|
|
|
my $actiontype = $targets{$action} || find_macro( $action );
|
|
|
|
fatal_error( "Invalid Action ($mtarget) in inline action" ) unless $actiontype & ( ACTION + STANDARD + NATRULE + MACRO + CHAIN + INLINE + INLINERULE );
|
|
|
|
if ( $msource ) {
|
|
if ( $msource eq '-' ) {
|
|
$msource = $source || '';
|
|
} elsif ( $msource =~ s/^DEST:?// ) {
|
|
$msource = merge_macro_source_dest $msource, $dest;
|
|
} else {
|
|
$msource =~ s/^SOURCE:?//;
|
|
$msource = merge_macro_source_dest $msource, $source;
|
|
}
|
|
} else {
|
|
$msource = '';
|
|
}
|
|
|
|
if ( $mdest ) {
|
|
if ( $mdest eq '-' ) {
|
|
$mdest = $dest || '';
|
|
} elsif ( $mdest =~ s/^SOURCE:?// ) {
|
|
$mdest = merge_macro_source_dest $mdest , $source;
|
|
} else {
|
|
$mdest =~ s/DEST:?//;
|
|
$mdest = merge_macro_source_dest $mdest, $dest;
|
|
}
|
|
} else {
|
|
$mdest = '';
|
|
}
|
|
|
|
for my $mp ( split_list( $mprotos, 'Protocol' ) ) {
|
|
for my $mu ( split_list( $musers, 'User/Group' ) ) {
|
|
$generated |= process_rule( $chainref,
|
|
$matches,
|
|
$matches1,
|
|
$mtarget,
|
|
$param,
|
|
$msource,
|
|
$mdest,
|
|
merge_macro_column( $mp, $proto ) ,
|
|
merge_macro_column( $mports, $ports ) ,
|
|
merge_macro_column( $msports, $sports ) ,
|
|
merge_macro_column( $morigdest, $origdest ) ,
|
|
merge_macro_column( $mrate, $rate ) ,
|
|
merge_macro_column( $mu, $user ) ,
|
|
merge_macro_column( $mmark, $mark ) ,
|
|
merge_macro_column( $mconnlimit, $connlimit) ,
|
|
merge_macro_column( $mtime, $time ),
|
|
merge_macro_column( $mheaders, $headers ),
|
|
merge_macro_column( $mcondition, $condition ),
|
|
merge_macro_column( $mhelper, $helper ),
|
|
$wildcard
|
|
);
|
|
|
|
set_inline_matches( $save_matches );
|
|
}
|
|
}
|
|
|
|
progress_message " Rule \"$currentline\" $done";
|
|
}
|
|
|
|
pop_comment( $save_comment );
|
|
|
|
pop_open;
|
|
|
|
progress_message "..End inline action $inlinefile";
|
|
|
|
pop_action_params( $oldparms );
|
|
|
|
return $generated;
|
|
}
|
|
|
|
#
|
|
# Confirm that we have AUDIT_TARGET capability and ensure the appropriate AUDIT chain.
|
|
#
|
|
sub verify_audit($;$$) {
|
|
my ($target, $audit, $tgt ) = @_;
|
|
|
|
require_capability 'AUDIT_TARGET', "$target rules", '';
|
|
|
|
return ensure_audit_chain $target, $audit, $tgt;
|
|
}
|
|
|
|
#
|
|
# Once a rule has been expanded via wildcards (source and/or dest zone eq '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.
|
|
# Similarly, if a new action tuple is encountered, this function is called recursively for each rule in the action
|
|
# body. In this latter case, a reference to the tuple's chain is passed in the first ($chainref) argument. A chain
|
|
# reference is also passed when rules are being generated during processing of a macro used as a policy action.
|
|
#
|
|
|
|
sub process_rule ( $$$$$$$$$$$$$$$$$$$$ ) {
|
|
my ( $chainref, #reference to Action Chain if we are being called from process_action(); undef otherwise
|
|
$rule, #Matches
|
|
$matches1, #Matches after the ones generated by the columns
|
|
$target,
|
|
$current_param,
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$origdest,
|
|
$ratelimit,
|
|
$user,
|
|
$mark,
|
|
$connlimit,
|
|
$time,
|
|
$headers,
|
|
$condition,
|
|
$helper,
|
|
$wildcard ) = @_;
|
|
|
|
my ( $action, $loglevel) = split_action $target;
|
|
my ( $basictarget, $param ) = get_target_param $action;
|
|
my $optimize = $wildcard ? ( $basictarget =~ /!$/ ? 0 : $config{OPTIMIZE} & 5 ) : 0;
|
|
my $actiontype;
|
|
my $inaction = ''; # Set to true when we are processing rules in an action file
|
|
my $inchain = ''; # Set to true when a chain reference is passed.
|
|
my $normalized_target;
|
|
my $normalized_action;
|
|
my $blacklist = ( $section == BLACKLIST_SECTION );
|
|
my $matches = $rule;
|
|
my $raw_matches = '';
|
|
my $exceptionrule = '';
|
|
my $usergenerated;
|
|
my $prerule = '';
|
|
my %save_nat_columns = %nat_columns;
|
|
my $generated = 0;
|
|
#
|
|
# Subroutine for handling MARK and CONNMARK.
|
|
#
|
|
sub handle_mark( $$$ ) {
|
|
my ( $target, $param, $marktype ) = @_;
|
|
my $and_or = $param =~ s/^([|&])// ? $1 : '';
|
|
|
|
require_capability( 'MARK_ANYWHERE', "The $target action", 's' );
|
|
|
|
fatal_error "Mark Ranges are not supported in the rules file" if $param =~ /-/;
|
|
#
|
|
# A Single Mark
|
|
#
|
|
my $mark = $param;
|
|
my $val;
|
|
|
|
if ( supplied $mark ) {
|
|
if ( $marktype == SMALLMARK ) {
|
|
$val = verify_small_mark( $mark );
|
|
} else {
|
|
$val = validate_mark( $mark );
|
|
}
|
|
} else {
|
|
$val = numeric_value( $mark = $globals{TC_MASK} );
|
|
}
|
|
|
|
$target = join( ' ', $target, $and_or eq '|' ? '--or-mark' : $and_or ? '--and-mark' : '--set-mark' );
|
|
|
|
( $mark, my $mask ) = split '/', $mark;
|
|
|
|
if ( supplied $mask ) {
|
|
$target = join( ' ', $target , join( '/', $mark , $mask ) );
|
|
} else {
|
|
$target = join( ' ', $target , $mark );
|
|
}
|
|
|
|
$target;
|
|
};
|
|
|
|
if ( $inchain = defined $chainref ) {
|
|
( $inaction, undef, undef, undef ) = split /:/, $normalized_action = $chainref->{action}, 4 if $chainref->{action};
|
|
}
|
|
|
|
$param = '' unless defined $param;
|
|
|
|
if ( $basictarget eq 'INLINE' ) {
|
|
( $action, $basictarget, $param, $loglevel, $raw_matches ) = handle_inline( FILTER_TABLE, 'filter', $action, $basictarget, $param, $loglevel );
|
|
} else {
|
|
$raw_matches = get_inline_matches(0);
|
|
}
|
|
#
|
|
# Handle early matches
|
|
#
|
|
if ( $raw_matches =~ s/s*\+// ) {
|
|
$prerule = $raw_matches;
|
|
$raw_matches = '';
|
|
}
|
|
#
|
|
# Determine the validity of the action
|
|
#
|
|
$actiontype = $targets{$basictarget} || find_macro( $basictarget );
|
|
|
|
fatal_error "Unknown ACTION ($action)" unless $actiontype;
|
|
|
|
$usergenerated = $actiontype & IPTABLES;
|
|
#
|
|
# For now, we'll just strip the parens from the SOURCE and DEST. In a later release, we might be able to do something more with them
|
|
#
|
|
|
|
if ( $actiontype == MACRO ) {
|
|
#
|
|
# process_macro() will call process_rule() recursively for each rule in the macro body
|
|
#
|
|
fatal_error "Macro/Inline invocations nested too deeply" if ++$macro_nest_level > MAX_MACRO_NEST_LEVEL;
|
|
|
|
$current_param = $param unless $param eq '' || $param eq 'PARAM';
|
|
|
|
$generated = process_macro( $basictarget,
|
|
$chainref,
|
|
$rule . $raw_matches,
|
|
$matches1,
|
|
$target,
|
|
$current_param,
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$origdest,
|
|
$ratelimit,
|
|
$user,
|
|
$mark,
|
|
$connlimit,
|
|
$time,
|
|
$headers,
|
|
$condition,
|
|
$helper,
|
|
$wildcard );
|
|
|
|
$macro_nest_level--;
|
|
goto EXIT;
|
|
} elsif ( $actiontype & NFQ ) {
|
|
$action = handle_nfqueue( $param,
|
|
1 # Allow 'bypass'
|
|
);
|
|
} elsif ( $actiontype & SET ) {
|
|
require_capability( 'IPSET_MATCH', 'SET and UNSET rules', '' );
|
|
fatal_error "$action rules require a set name parameter" unless $param;
|
|
} elsif ( ( $actiontype & AUDIT ) && ( $basictarget eq 'AUDIT' ) ) {
|
|
require_capability ( 'AUDIT_TARGET', 'The AUDIT action', 's' );
|
|
$param = $param eq '' ? 'drop' : $param;
|
|
fatal_error "Invalid AUDIT type ($param) -- must be 'accept', 'drop' or 'reject'" unless $param =~ /^(?:accept|drop|reject)$/;
|
|
$actiontype = STANDARD;
|
|
} elsif ( ! $usergenerated ) {
|
|
if ( $actiontype & NFLOG ) {
|
|
validate_level( $action );
|
|
$loglevel = supplied $loglevel ? join( ':', $action, $loglevel ) : $action;
|
|
$action = 'LOG';
|
|
} elsif ( ! ( $actiontype & (ACTION | INLINE | IPTABLES | TARPIT ) ) ) {
|
|
fatal_error "'builtin' actions may only be used in INLINE or IP[6]TABLES rules" if $actiontype == USERBUILTIN;
|
|
fatal_error "The $basictarget TARGET does not accept a parameter" unless $param eq '' || $actiontype & OPTIONS;
|
|
}
|
|
}
|
|
#
|
|
# We can now dispense with the postfix character
|
|
#
|
|
fatal_error "The +, - and ! modifiers are not allowed in the blrules file" if $action =~ s/[-+!]$// && $blacklist;
|
|
|
|
unless ( $actiontype & ( ACTION | INLINE | IPTABLES | TARPIT ) ) {
|
|
#
|
|
# Catch empty parameter list
|
|
#
|
|
fatal_error "The $basictarget TARGET does not accept parameters" if $action =~ s/\(\)$//;
|
|
}
|
|
|
|
if ( $actiontype & (NATRULE | NONAT | NATONLY ) ) {
|
|
$targets{$inaction} |= NATRULE if $inaction;
|
|
fatal_error "NAT rules are only allowed in the NEW section" unless $section == NEW_SECTION;
|
|
}
|
|
|
|
if ( $actiontype & HELPER ) {
|
|
fatal_error "HELPER rules are only allowed in the NEW section" unless $section == NEW_SECTION;
|
|
}
|
|
#
|
|
# Take care of irregular syntax and targets
|
|
#
|
|
my $log_action = $action;
|
|
|
|
unless ( $actiontype & ( ACTION | MACRO | NFLOG | NFQ | CHAIN | INLINE ) ) {
|
|
my $bt = $basictarget;
|
|
|
|
$bt =~ s/[-+!]$//;
|
|
|
|
my %functions =
|
|
( ACCEPT => sub() {
|
|
if ( $blacklist ) {
|
|
$action = 'RETURN';
|
|
} elsif ( $helper ne '-' ) {
|
|
$actiontype |= HELPER if $section == NEW_SECTION;
|
|
}
|
|
} ,
|
|
|
|
AUDIT => sub() {
|
|
$action = "AUDIT --type $param";
|
|
} ,
|
|
|
|
CONNMARK => sub() {
|
|
$action = handle_mark( 'CONNMARK', $param, HIGHMARK );
|
|
} ,
|
|
|
|
REDIRECT => sub () {
|
|
my $z = $actiontype & NATONLY ? '' : firewall_zone;
|
|
|
|
if ( $dest eq '-' ) {
|
|
if ( $family == F_IPV4 ) {
|
|
$dest = ( $inchain ) ? '' : join( '', $z, '::' , $ports =~ /[:,]/ ? '' : $ports );
|
|
} else {
|
|
$dest = ( $inchain ) ? '' : join( '', $z, ':[]:' , $ports =~ /[:,]/ ? '' : $ports );
|
|
}
|
|
} elsif ( $inchain ) {
|
|
if ( $family == F_IPV4 ) {
|
|
$dest = ":$dest";
|
|
} else {
|
|
$dest = "[]:$dest";
|
|
}
|
|
} else {
|
|
if ( $family == F_IPV4 ) {
|
|
$dest = join( '', $z, '::', $dest ) unless $dest =~ /^[^\d].*:/;
|
|
} else {
|
|
$dest = join( '', $z, ':[]:', $dest ) unless $dest =~ /^[^\d].*:/;
|
|
}
|
|
}
|
|
} ,
|
|
|
|
REJECT => sub {
|
|
if ( supplied( $param ) ) {
|
|
my $option = $reject_options{$param};
|
|
fatal_error "Invalid REJECT option ($param)" unless $option;
|
|
if ( $option == 2 ) {
|
|
#
|
|
# tcp-reset
|
|
#
|
|
fatal_error "tcp-reset may only be used with PROTO tcp" unless ( resolve_proto( $proto ) || 0 ) == TCP;
|
|
$exceptionrule = '-p 6 ';
|
|
$param = 'tcp-reset';
|
|
}
|
|
|
|
$action = "REJECT --reject-with $param";
|
|
} else {
|
|
$action = 'reject';
|
|
}
|
|
},
|
|
|
|
CONTINUE => sub { $action = 'RETURN'; } ,
|
|
|
|
WHITELIST => sub {
|
|
fatal_error "'WHITELIST' may only be used in the blrules file" unless $blacklist;
|
|
$action = 'RETURN';
|
|
} ,
|
|
|
|
COUNT => sub { $action = ''; } ,
|
|
|
|
LOG => sub { fatal_error 'LOG requires a log level' unless supplied $loglevel; } ,
|
|
|
|
HELPER => sub {
|
|
fatal_error "HELPER requires require that the helper be specified in the HELPER column" if $helper eq '-';
|
|
fatal_error "HELPER rules may only appear in the NEW section" unless $section == NEW_SECTION;
|
|
$action = ''; } ,
|
|
|
|
IPTABLES => sub {
|
|
if ( $param ) {
|
|
fatal_error "Unknown ACTION (IPTABLES)" unless $family == F_IPV4;
|
|
my ( $tgt, $options ) = split / /, $param, 2;
|
|
my $target_type = $builtin_target{$tgt};
|
|
fatal_error "Unknown target ($tgt)" unless $target_type;
|
|
fatal_error "The $tgt TARGET is not allowed in the filter table" unless $target_type & FILTER_TABLE;
|
|
$action = $param;
|
|
} else {
|
|
$action = '';
|
|
}
|
|
},
|
|
|
|
IP6TABLES => sub {
|
|
if ( $param ) {
|
|
fatal_error "Unknown ACTION (IP6TABLES)" unless $family == F_IPV6;
|
|
my ( $tgt, $options ) = split / /, $param, 2;
|
|
my $target_type = $builtin_target{$tgt};
|
|
fatal_error "Unknown target ($tgt)" unless $target_type;
|
|
fatal_error "The $tgt TARGET is not allowed in the filter table" unless $target_type & FILTER_TABLE;
|
|
$action = $param;
|
|
} else {
|
|
$action = '';
|
|
}
|
|
},
|
|
|
|
MARK => sub() {
|
|
$action = handle_mark( 'MARK', $param, HIGHMARK );
|
|
} ,
|
|
|
|
TARPIT => sub {
|
|
require_capability 'TARPIT_TARGET', 'TARPIT', 's';
|
|
|
|
fatal_error "TARPIT is only valid with PROTO tcp (6)" if ( resolve_proto( $proto ) || 0 ) != TCP;
|
|
|
|
if ( supplied $param ) {
|
|
fatal_error "TARPIT Parameter must be 'tarpit', 'honeypot' or 'reset'" unless $param =~ /^(tarpit|honeypot|reset)$/;
|
|
$action = "TARPIT --$param";
|
|
$log_action = 'TARPIT';
|
|
} else {
|
|
$action = $log_action = 'TARPIT';
|
|
}
|
|
|
|
$exceptionrule = '-p 6 ';
|
|
},
|
|
);
|
|
|
|
my $function = $functions{ $bt };
|
|
|
|
if ( $function ) {
|
|
$function->();
|
|
} elsif ( $actiontype & NATRULE && $helper ne '-' ) {
|
|
$actiontype |= HELPER;
|
|
} elsif ( $actiontype & SET ) {
|
|
my %xlate = ( ADD => 'add-set' , DEL => 'del-set' );
|
|
my ( $setname, $flags, $timeout, $rest ) = split ':', $param, 4;
|
|
|
|
fatal_error "Invalid ADD/DEL parameter ($param)" if $rest;
|
|
$setname =~ s/^\+//;
|
|
fatal_error "Expected ipset name ($setname)" unless $setname =~ /^(6_)?[a-zA-Z][-\w]*$/;
|
|
fatal_error "Invalid flags ($flags)" unless defined $flags && $flags =~ /^(dst|src)(,(dst|src)){0,5}$/;
|
|
|
|
$action = join( ' ', 'SET --' . $xlate{$basictarget} , $setname , $flags );
|
|
|
|
if ( supplied $timeout ) {
|
|
fatal_error "A timeout may only be supplied in an ADD rule" unless $basictarget eq 'ADD';
|
|
fatal_error "Invalid Timeout ($timeout)" unless $timeout && $timeout =~ /^\d+$/;
|
|
|
|
$action .= " --timeout $timeout --exist";
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Isolate and validate source and destination zones
|
|
#
|
|
my $sourcezone = '-';
|
|
my $destzone = '-';
|
|
my $sourceref;
|
|
my $destref;
|
|
my $origdstports;
|
|
|
|
unless ( $inchain ) {
|
|
if ( $source =~ /^(.+?):(.*)/ ) {
|
|
fatal_error "Missing SOURCE Qualifier ($source)" if $2 eq '';
|
|
$sourcezone = $1;
|
|
$source = $2;
|
|
} else {
|
|
$sourcezone = $source;
|
|
$source = $actiontype == INLINE ? '-' : ALLIP;
|
|
}
|
|
|
|
if ( $dest =~ /^(.*?):(.*)/ ) {
|
|
fatal_error "Missing DEST Qualifier ($dest)" if $2 eq '';
|
|
$destzone = $1;
|
|
$dest = $2;
|
|
} elsif ( $dest =~ /.*\..*\./ ) {
|
|
#
|
|
# Appears to be an IPv4 address (no NAT in IPv6)
|
|
#
|
|
$destzone = '-';
|
|
} else {
|
|
$destzone = $dest;
|
|
$dest = $actiontype == INLINE ? '-' : ALLIP;
|
|
}
|
|
|
|
fatal_error "Missing source zone" if $sourcezone eq '-' || $sourcezone =~ /^:/;
|
|
fatal_error "Unknown source zone ($sourcezone)" unless $sourceref = defined_zone( $sourcezone );
|
|
fatal_error 'USER/GROUP may only be specified when the SOURCE zone is $FW' unless $user eq '-' || $sourcezone eq firewall_zone;
|
|
}
|
|
|
|
if ( $actiontype & NATONLY ) {
|
|
unless ( $destzone eq '-' || $destzone eq '' ) {
|
|
$destref = defined_zone( $destzone );
|
|
|
|
if ( $destref ) {
|
|
warning_message "The destination zone ($destzone) is ignored in $log_action rules";
|
|
} else {
|
|
$dest = join ':', $destzone, $dest;
|
|
$destzone = '';
|
|
}
|
|
}
|
|
} elsif ( ! $inchain ) {
|
|
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;
|
|
|
|
unless ( $inchain ) {
|
|
if ( $sourceref && ( $sourceref->{type} & ( FIREWALL | VSERVER ) ) ) {
|
|
$restriction = $destref && ( $destref->{type} & ( FIREWALL | VSERVER ) ) ? ALL_RESTRICT : OUTPUT_RESTRICT;
|
|
} else {
|
|
$restriction = INPUT_RESTRICT if $destref && ( $destref->{type} & ( FIREWALL | VSERVER ) );
|
|
}
|
|
}
|
|
|
|
#
|
|
# For compatibility with older Shorewall versions
|
|
#
|
|
$origdest = ALLIP if $origdest eq 'all';
|
|
|
|
#
|
|
# Take care of chain
|
|
#
|
|
my $chain;
|
|
|
|
if ( $inchain ) {
|
|
#
|
|
# We are generating rules in a chain -- get its name
|
|
#
|
|
$chain = $chainref->{name};
|
|
#
|
|
# If we are processing an inline action, we need the source zone for NAT.
|
|
#
|
|
$sourceref = find_zone( $chainref->{sourcezone} ) if $chainref->{sourcezone};
|
|
#
|
|
# And we need the dest zone for local/loopback/off-firewall/destonly checks
|
|
#
|
|
$destref = find_zone( $chainref->{destzone} ) if $chainref->{destzone};
|
|
} elsif ( ! ( $actiontype & NATONLY ) ) {
|
|
#
|
|
# Check for illegal bridge port rule
|
|
#
|
|
if ( $destref->{type} & BPORT ) {
|
|
unless ( $sourceref->{bridge} eq $destref->{bridge} || single_interface( $sourcezone ) eq $destref->{bridge} ) {
|
|
goto EXIT if $wildcard;
|
|
fatal_error "Rules with a DESTINATION Bridge Port zone must have a SOURCE zone on the same bridge";
|
|
}
|
|
}
|
|
|
|
$chain = rules_chain( ${sourcezone}, ${destzone} );
|
|
#
|
|
# Ensure that the chain exists but don't mark it as referenced until after optimization is checked
|
|
#
|
|
( $chainref = ensure_chain( 'filter', $chain ) )->{sourcezone} = $sourcezone;
|
|
$chainref->{destzone} = $destzone;
|
|
|
|
my $policy = $chainref->{policy};
|
|
|
|
if ( $policy eq 'NONE' ) {
|
|
goto EXIT if $wildcard;
|
|
fatal_error "Rules may not override a NONE policy";
|
|
}
|
|
#
|
|
# Handle Optimization level 1 when specified alone
|
|
#
|
|
if ( $optimize == 1 && $section == NEW_SECTION ) {
|
|
my $loglevel = $filter_table->{$chainref->{policychain}}{loglevel};
|
|
if ( $loglevel ne '' ) {
|
|
goto EXIT if $target eq "${policy}:${loglevel}";
|
|
} else {
|
|
goto EXIT if $basictarget eq $policy;
|
|
}
|
|
}
|
|
#
|
|
# Mark the chain as referenced and add appropriate rules from earlier sections.
|
|
#
|
|
$chainref = ensure_rules_chain $chain;
|
|
#
|
|
# Handle rules in the BLACKLIST, ESTABLISHED, RELATED, INVALID and UNTRACKED sections
|
|
#
|
|
if ( $section & ( BLACKLIST_SECTION | ESTABLISHED_SECTION | RELATED_SECTION | INVALID_SECTION | UNTRACKED_SECTION ) ) {
|
|
my $auxchain = $section_functions{$section}->( $sourcezone, $destzone );
|
|
my $auxref = $filter_table->{$auxchain};
|
|
|
|
unless ( $auxref ) {
|
|
my $save_comment = push_comment;
|
|
$auxref = new_chain 'filter', $auxchain;
|
|
$auxref->{blacklistsection} = 1 if $blacklist;
|
|
|
|
add_ijump( $chainref, j => $auxref, state_imatch( $section_states{$section} ) );
|
|
pop_comment( $save_comment );
|
|
}
|
|
|
|
$chain = $auxchain;
|
|
$chainref = $auxref;
|
|
}
|
|
}
|
|
#
|
|
# Handle 'local/loopback' warnings
|
|
#
|
|
unless ( $wildcard ) {
|
|
if ( $sourceref ) {
|
|
warning_message( "The SOURCE zone in this rule is 'destonly'" ) if $sourceref->{destonly};
|
|
|
|
if ( $destref ) {
|
|
warning_message( "\$FW to \$FW rules are ignored when there is a defined 'loopback' zone" ) if loopback_zones && $sourceref->{type} == FIREWALL && $destref->{type} == FIREWALL;
|
|
}
|
|
}
|
|
}
|
|
#
|
|
# Handle actions
|
|
#
|
|
my $actionchain; # Name of the action chain
|
|
|
|
if ( $actiontype & ACTION ) {
|
|
#
|
|
# Verify action 'proto', if any
|
|
#
|
|
$proto = determine_action_protocol( $basictarget, $proto );
|
|
#
|
|
# Save NAT-oriented column contents
|
|
#
|
|
@nat_columns{'dest', 'proto', 'ports' } = ( $dest,
|
|
$proto eq '-' ? $nat_columns{proto} : $proto,
|
|
$ports eq '-' ? $nat_columns{ports} : $ports );
|
|
#
|
|
# Push the current column array onto the column stack
|
|
#
|
|
my @savecolumns = @columns;
|
|
#
|
|
# And store the (modified) columns into the columns array for use by perl_action[_tcp]_helper. We
|
|
# only need the NAT-oriented columns
|
|
#
|
|
@columns = ( undef , undef, $dest, $proto, $ports);
|
|
#
|
|
# Handle 'section' option
|
|
#
|
|
$param = supplied $param ? join( ',' , $section_rmap{$section}, $param ) : $section_rmap{$section} if $actions{$basictarget}{options} & SECTION_OPT;
|
|
#
|
|
# Create the action:level:tag:param tuple.
|
|
#
|
|
$normalized_target = normalize_action( $basictarget, $loglevel, $param );
|
|
|
|
fatal_error( "Action $basictarget invoked Recursively (" . join( '->', map( external_name( $_ ), @actionstack , $normalized_target ) ) . ')' ) if $active{$basictarget};
|
|
|
|
if ( my $ref = use_action( 'filter', $normalized_target ) ) {
|
|
#
|
|
# First reference to this tuple
|
|
#
|
|
my $savestatematch = $statematch;
|
|
$statematch = '';
|
|
#
|
|
# process_action may modify both $normalized_target and $ref!!!
|
|
#
|
|
process_action( $normalized_target, $ref, $chain );
|
|
#
|
|
# Capture the name of the action chain
|
|
#
|
|
$actionchain = $ref->{name};
|
|
#
|
|
# Processing the action may determine that the action or one of it's dependents does NAT or HELPER, so:
|
|
#
|
|
# - Refresh $actiontype
|
|
# - Create the associated nat and/or table chain if appropriate.
|
|
#
|
|
ensure_chain( 'nat', $actionchain ) if ( $actiontype = $targets{$basictarget} ) & NATRULE;
|
|
ensure_chain( 'raw', $actionchain ) if ( $actiontype & HELPER );
|
|
|
|
$statematch = $savestatematch;
|
|
} else {
|
|
#
|
|
# We've seen this tuple before
|
|
#
|
|
$actionchain = $usedactions{$normalized_target}->{name};
|
|
}
|
|
|
|
$action = $basictarget; # Remove params, if any, from $action.
|
|
|
|
@columns = @savecolumns;
|
|
} elsif ( $actiontype & INLINE ) {
|
|
#
|
|
# process_inline() will call process_rule() recursively for each rule in the action body
|
|
#
|
|
fatal_error "Macro/Inline invocations nested too deeply" if ++$macro_nest_level > MAX_MACRO_NEST_LEVEL;
|
|
#
|
|
# Push the current column array onto the column stack
|
|
#
|
|
my $savecolumns = [ ( $actionresult, @columns ) ];
|
|
#
|
|
# And store the (modified) columns into the columns array for use by perl_action[_tcp]_helper
|
|
#
|
|
@columns = ( $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $mark, $connlimit, $time, $headers, $condition, $helper, $wildcard );
|
|
|
|
$actionresult = 0;
|
|
|
|
$generated = process_inline( $basictarget,
|
|
$chainref,
|
|
$prerule . $rule,
|
|
$matches1 . $raw_matches,
|
|
$loglevel,
|
|
$target,
|
|
$param,
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$origdest,
|
|
$ratelimit,
|
|
$user,
|
|
$mark,
|
|
$connlimit,
|
|
$time,
|
|
$headers,
|
|
$condition,
|
|
$helper,
|
|
$wildcard ) || $actionresult;
|
|
|
|
( $actionresult, @columns ) = @$savecolumns;;
|
|
|
|
$macro_nest_level--;
|
|
|
|
goto EXIT;
|
|
}
|
|
#
|
|
# Generate Fixed part of the rule
|
|
#
|
|
if ( $actiontype & ( NATRULE | NONAT ) && ! ( $actiontype & NATONLY ) ) {
|
|
#
|
|
# Either a DNAT, REDIRECT or ACCEPT+ rule or an Action with NAT;
|
|
# don't apply rate limiting twice
|
|
#
|
|
$rule .= join( '',
|
|
do_proto($proto, $ports, $sports),
|
|
do_user( $user ) ,
|
|
do_test( $mark , $globals{TC_MASK} ) ,
|
|
do_connlimit( $connlimit ),
|
|
do_time( $time ) ,
|
|
do_headers( $headers ) ,
|
|
do_condition( $condition , $chain ) ,
|
|
$raw_matches ,
|
|
);
|
|
} elsif ( $section & ( ESTABLISHED_SECTION | INVALID_SECTION | RELATED_SECTION | UNTRACKED_SECTION ) ) {
|
|
$rule .= join( '',
|
|
do_proto($proto, $ports, $sports),
|
|
do_ratelimit( $ratelimit, $basictarget ) ,
|
|
do_user( $user ) ,
|
|
do_test( $mark , $globals{TC_MASK} ) ,
|
|
do_connlimit( $connlimit ),
|
|
do_time( $time ) ,
|
|
do_headers( $headers ) ,
|
|
do_condition( $condition , $chain ) ,
|
|
do_helper( $helper ) ,
|
|
$matches1 . $raw_matches ,
|
|
);
|
|
} else {
|
|
$rule .= join( '',
|
|
do_proto($proto, $ports, $sports),
|
|
do_ratelimit( $ratelimit, $basictarget ) ,
|
|
do_user( $user ) ,
|
|
do_test( $mark , $globals{TC_MASK} ) ,
|
|
do_connlimit( $connlimit ),
|
|
do_time( $time ) ,
|
|
do_headers( $headers ) ,
|
|
do_condition( $condition , $chain ) ,
|
|
$matches1 . $raw_matches ,
|
|
);
|
|
}
|
|
|
|
unless ( $section & ( NEW_SECTION | POLICYACTION_SECTION ) ||
|
|
$inaction ||
|
|
$blacklist ||
|
|
$basictarget eq 'dropInvalid' ) {
|
|
if ( $config{FASTACCEPT} ) {
|
|
fatal_error "Entries in the $section_rmap{$section} SECTION of the rules file not permitted with FASTACCEPT=Yes" unless
|
|
( ( $section & ( UNTRACKED_SECTION | INVALID_SECTION | ALL_SECTION ) ) ||
|
|
( $section & ( RELATED_SECTION ) ) && ( $config{RELATED_DISPOSITION} ne 'ACCEPT' || $config{RELATED_LOG_LEVEL} ) )
|
|
}
|
|
|
|
fatal_error "$basictarget rules are not allowed in the $section_rmap{$section} SECTION" if $actiontype & ( NATRULE | NONAT );
|
|
$rule .= state_match('ESTABLISHED') if $section == ESTABLISHED_SECTION;
|
|
}
|
|
#
|
|
# Generate CT rules(s), if any
|
|
#
|
|
if ( $actiontype & HELPER ) {
|
|
handle_helper_rule( $helper,
|
|
$source,
|
|
$origdest ? $origdest : $dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$sourceref,
|
|
( $actiontype & ACTION ) ? $actionchain : '',
|
|
$inchain ? $chain : '' ,
|
|
$user ,
|
|
$rule ,
|
|
);
|
|
|
|
$targets{$inaction} |= HELPER if $inaction;
|
|
}
|
|
|
|
# Generate NAT rule(s), if any
|
|
#
|
|
if ( $actiontype & NATRULE ) {
|
|
require_capability( 'NAT_ENABLED' , "$basictarget rules", '' );
|
|
#
|
|
# Add the appropriate rule to the nat table
|
|
#
|
|
( $ports,
|
|
$origdstports,
|
|
$dest ) = handle_nat_rule( $dest,
|
|
$proto,
|
|
$ports,
|
|
$origdest,
|
|
( $actiontype & ACTION ) ? $actionchain : '',
|
|
$action,
|
|
$sourceref,
|
|
$inaction ? $chain : '',
|
|
$rule,
|
|
$source,
|
|
( $actiontype & ACTION ) ? '' : $loglevel,
|
|
$log_action,
|
|
$wildcard
|
|
);
|
|
|
|
#
|
|
# After NAT:
|
|
# - the destination port will be the server port ($ports) -- we did that above
|
|
# - the destination IP will be the server IP ($dest) -- also done above
|
|
# - 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( '',
|
|
$matches,
|
|
do_proto( $proto, $ports, $sports ),
|
|
do_ratelimit( $ratelimit, 'ACCEPT' ),
|
|
do_user $user,
|
|
do_test( $mark , $globals{TC_MASK} ),
|
|
do_condition( $condition , $chain ),
|
|
$raw_matches,
|
|
);
|
|
$loglevel = '';
|
|
$action = 'ACCEPT';
|
|
$origdest = ALLIP if $origdest =~ /[+]/;
|
|
$helper = '-';
|
|
}
|
|
} elsif ( $actiontype & NONAT ) {
|
|
#
|
|
# NONAT or ACCEPT+
|
|
#
|
|
handle_nonat_rule( $action,
|
|
$source,
|
|
$dest,
|
|
$origdest,
|
|
$sourceref,
|
|
$inaction,
|
|
$chain,
|
|
$loglevel,
|
|
$log_action,
|
|
$rule,
|
|
$wildcard
|
|
);
|
|
}
|
|
|
|
#
|
|
# Add filter table rule, unless this is a NATONLY rule type
|
|
#
|
|
unless ( $actiontype & NATONLY ) {
|
|
|
|
if ( $actiontype & ACTION ) {
|
|
$action = $actionchain;
|
|
|
|
if ( $actions{$basictarget}{options} & LOGJUMP_OPT ) {
|
|
$log_action = $basictarget;
|
|
} else {
|
|
$loglevel = '';
|
|
}
|
|
}
|
|
|
|
if ( $origdest ) {
|
|
unless ( $origdest eq '-' ) {
|
|
require_capability( 'CONNTRACK_MATCH', 'ORIGINAL DEST in a non-NAT rule', 's' ) unless $actiontype & NATRULE;
|
|
} else {
|
|
$origdest = '';
|
|
}
|
|
}
|
|
|
|
$rule .= "-m conntrack --ctorigdstport $origdstports " if have_capability( 'NEW_CONNTRACK_MATCH' ) && $origdstports;
|
|
|
|
verify_audit( $action ) if $actiontype & AUDIT;
|
|
|
|
expand_rule( $chainref ,
|
|
$restriction ,
|
|
$prerule ,
|
|
$rule ,
|
|
$source ,
|
|
$dest ,
|
|
$origdest ,
|
|
$action ,
|
|
$loglevel ,
|
|
$log_action ,
|
|
$exceptionrule ,
|
|
$usergenerated && ! $loglevel )
|
|
unless unreachable_warning( $wildcard || $section == POLICYACTION_SECTION, $chainref );
|
|
}
|
|
|
|
$generated = 1;
|
|
|
|
EXIT:
|
|
{
|
|
%nat_columns = %save_nat_columns;
|
|
}
|
|
|
|
return $generated;
|
|
}
|
|
|
|
|
|
sub check_state( $ );
|
|
#
|
|
# Check the passed connection state for conflict with the current section
|
|
#
|
|
# Returns non-zero value if the state is compatible with the section:
|
|
#
|
|
# 1: Emit the rule with state match
|
|
# 2: Emit the rule without
|
|
#
|
|
sub check_state( $ ) {
|
|
my $state = $_[0];
|
|
|
|
if ( $section == BLACKLIST_SECTION ) {
|
|
my $blacklist_states = $globals{BLACKLIST_STATES};
|
|
return 1 if $blacklist_states eq 'ALL';
|
|
return 2 if $blacklist_states eq $state;
|
|
for ( split ',', $blacklist_states ) {
|
|
return 1 if $_ eq $state;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
my $chainref = $actparams{0};
|
|
my $name = $chainref->{name};
|
|
my $statechainref;
|
|
|
|
if ( $name =~ /^([+_&])/ ) {
|
|
#
|
|
# This is a state chain
|
|
#
|
|
return $state eq 'RELATED' ? 2 : 0 if $1 eq '+';
|
|
return $state eq 'INVALID' ? 2 : 0 if $1 eq '_';
|
|
return $state eq 'UNTRACKED' ? 2 : 0;
|
|
}
|
|
|
|
my $sectionref = $chainref->{sections};
|
|
|
|
if ( $state eq 'ESTABLISHED' ) {
|
|
return ( $sectionref && $sectionref->{$state} ) ? 0 : $section == ESTABLISHED_SECTION ? 2 : 1;
|
|
}
|
|
|
|
if ( $state =~ /^(?:INVALID|UNTRACKED|RELATED|ESTABLISHED)$/ && $globals{"${state}_TARGET"} ) {
|
|
#
|
|
# One of the states that has its own state chain -- get the current action's chain
|
|
#
|
|
if ( $sectionref && $sectionref->{$state} ) {
|
|
#
|
|
# We're past that section -- see if there was a separate state chain
|
|
#
|
|
if ( my $statechainref = $filter_table->{"$statetable{$state}[0]$chainref->{name}"} ) {
|
|
#
|
|
# There was -- if the chain had a RETURN then we will emit the current rule; otherwise we won't
|
|
#
|
|
return has_return( $statechainref ) ? 1 : 0;
|
|
} else {
|
|
#
|
|
# There wasn't -- suppress the current rule
|
|
#
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $section & ( NEW_SECTION | POLICYACTION_SECTION ) ) {
|
|
if ( $state eq 'NEW' ) {
|
|
#
|
|
# If an INVALID or UNTRACKED rule would be emitted then we must include the state match
|
|
#
|
|
for ( qw/INVALID UNTRACKED/ ) {
|
|
return 1 if check_state( $_ );
|
|
}
|
|
|
|
2;
|
|
} else {
|
|
$state =~ /^(?:INVALID|UNTRACKED)$/;
|
|
}
|
|
} elsif ( $sectionref ) {
|
|
#
|
|
# we're dealing with a rules chain
|
|
#
|
|
$state eq $section_rmap{$section} ? 2 : 1;
|
|
} else {
|
|
#
|
|
# An action chain -- we can't predict where it will get invoked so populate it fully
|
|
#
|
|
1;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Helper for the perl_action_xxx functions
|
|
#
|
|
sub merge_target( $$ ) {
|
|
my ( $ref, $target ) = @_;
|
|
|
|
merge_levels( join( ':', @actparams{'chain','loglevel','logtag'}), $target );
|
|
}
|
|
|
|
#
|
|
# May be called by Perl code in action bodies (regular and inline) to generate a rule.
|
|
#
|
|
sub perl_action_helper($$;$$) {
|
|
my ( $target, $matches, $isstatematch , $matches1 ) = @_;
|
|
my $action = $actparams{action};
|
|
my $chainref = $actparams{0};
|
|
my $result;
|
|
|
|
assert( $chainref );
|
|
|
|
$matches .= ' ' unless $matches =~ /^(?:.+\s)?$/;
|
|
|
|
if ( $matches1 ) {
|
|
$matches1 .= ' ' unless $matches1 =~ /^(?:.+\s)?$/;
|
|
} else {
|
|
$matches1 = '';
|
|
}
|
|
|
|
set_inline_matches( $target =~ /^INLINE(?::.*)?$/ ? $matches : '' );
|
|
|
|
if ( $isstatematch ) {
|
|
if ( $statematch ) {
|
|
if ( $statematch eq $isstatematch ) {
|
|
#
|
|
# Same match -- pretend this isn't a state match
|
|
#
|
|
$isstatematch = '';
|
|
} else {
|
|
#
|
|
# Different state -- can't possibly match
|
|
#
|
|
return;
|
|
}
|
|
} else {
|
|
$statematch = $isstatematch;
|
|
}
|
|
}
|
|
|
|
my $ref = $actions{$action};
|
|
|
|
assert( $ref, $action );
|
|
|
|
if ( $ref->{type} & INLINE ) {
|
|
$result = &process_rule( $chainref,
|
|
$matches,
|
|
$matches1,
|
|
merge_target( $ref, $target ),
|
|
'', # CurrentParam
|
|
@columns );
|
|
} else {
|
|
if ( ( $targets{$target} || 0 ) & NATRULE ) {
|
|
$result = process_rule( $chainref,
|
|
$matches,
|
|
$matches1,
|
|
merge_target( $actions{$action}, $target ),
|
|
'', # Current Param
|
|
'-', # Source
|
|
merge_action_column( # Dest
|
|
$columns[2],
|
|
$nat_columns{dest}
|
|
),
|
|
merge_action_column( #Proto
|
|
$columns[3],
|
|
$nat_columns{proto}
|
|
),
|
|
merge_action_column( #Ports
|
|
$columns[4],
|
|
$nat_columns{ports}),
|
|
'-', # Source Port(s)
|
|
'-', # Original Dest
|
|
'-', # Rate Limit
|
|
'-', # User
|
|
'-', # Mark
|
|
'-', # Connlimit
|
|
'-', # Time
|
|
'-', # Headers,
|
|
'-', # condition,
|
|
'-', # helper,
|
|
0, # Wildcard
|
|
);
|
|
} else {
|
|
$result = process_rule( $chainref,
|
|
$matches,
|
|
$matches1,
|
|
merge_target( $actions{$action}, $target ),
|
|
'', # Current Param
|
|
'-', # Source
|
|
'-', # Dest
|
|
'-', # Proto
|
|
'-', # Port(s)
|
|
'-', # Source Port(s)
|
|
'-', # Original Dest
|
|
'-', # Rate Limit
|
|
'-', # User
|
|
'-', # Mark
|
|
'-', # Connlimit
|
|
'-', # Time
|
|
'-', # Headers,
|
|
'-', # condition,
|
|
'-', # helper,
|
|
0, # Wildcard
|
|
);
|
|
}
|
|
|
|
allow_optimize( $chainref );
|
|
}
|
|
#
|
|
# Record that we generated a rule to avoid bogus warning
|
|
#
|
|
$actionresult ||= $result;
|
|
|
|
$statematch = '' if $isstatematch;
|
|
}
|
|
|
|
#
|
|
# May be called by Perl code in action bodies (regular and inline) to generate a rule.
|
|
#
|
|
sub perl_action_tcp_helper($$) {
|
|
my ( $target, $proto ) = @_;
|
|
my $action = $actparams{action};
|
|
my $chainref = $actparams{0};
|
|
my $result;
|
|
my $passedproto = $columns[2];
|
|
|
|
assert( $chainref );
|
|
|
|
$proto .= ' ' unless $proto =~ /^(?:.+\s)?$/;
|
|
|
|
set_inline_matches( '' ) if $config{INLINE_MATCHES};
|
|
|
|
if ( $passedproto eq '-' || $passedproto eq 'tcp' || $passedproto eq '6' ) {
|
|
#
|
|
# For other protos, a 'no rule generated' warning will be issued
|
|
#
|
|
my $ref = $actions{$action};
|
|
|
|
assert( $ref, $action );
|
|
|
|
if ( $ref->{type} & INLINE ) {
|
|
$result = &process_rule( $chainref,
|
|
$proto,
|
|
'',
|
|
merge_target( $ref, $target ),
|
|
'',
|
|
@columns[0,1],
|
|
6,
|
|
@columns[3..LAST_COLUMN]
|
|
);
|
|
} else {
|
|
$result = process_rule( $chainref,
|
|
$proto,
|
|
'',
|
|
merge_target( $actions{$action}, $target ),
|
|
'', # Current Param
|
|
'-', # Source
|
|
'-', # Dest
|
|
'-', # Proto
|
|
'-', # Port(s)
|
|
'-', # Source Port(s)
|
|
'-', # Original Dest
|
|
'-', # Rate Limit
|
|
'-', # User
|
|
'-', # Mark
|
|
'-', # Connlimit
|
|
'-', # Time
|
|
'-', # Headers,
|
|
'-', # condition,
|
|
'-', # helper,
|
|
0, # Wildcard
|
|
);
|
|
|
|
allow_optimize( $chainref );
|
|
}
|
|
#
|
|
# Record that we generated a rule to avoid bogus warning
|
|
#
|
|
$actionresult ||= $result;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Helper functions for process_raw_rule(). That function deals with the ugliness of wildcard zones ('all' and 'any') and zone lists.
|
|
#
|
|
# Process a SECTION header
|
|
#
|
|
sub process_section ($) {
|
|
my $sect = shift;
|
|
#
|
|
# split_line2 has already verified that there are exactly two tokens on the line
|
|
#
|
|
fatal_error "Invalid SECTION ($sect)" unless defined $sections{$sect};
|
|
fatal_error "Duplicate or out of order SECTION $sect" if $sections{$sect};
|
|
|
|
if ( $sect eq 'BLACKLIST' ) {
|
|
fatal_error "The BLACKLIST section has been eliminated. Please move your BLACKLIST rules to the 'blrules' file";
|
|
} elsif ( $sect eq 'ESTABLISHED' ) {
|
|
$sections{ALL} = 1;
|
|
} elsif ( $sect eq 'RELATED' ) {
|
|
@sections{'ALL','ESTABLISHED'} = ( 1, 1);
|
|
} elsif ( $sect eq 'INVALID' ) {
|
|
@sections{'ALL','ESTABLISHED','RELATED'} = ( 1, 1, 1 );
|
|
} elsif ( $sect eq 'UNTRACKED' ) {
|
|
@sections{'ALL','ESTABLISHED','RELATED', 'INVALID' } = ( 1, 1, 1, 1 );
|
|
} elsif ( $sect eq 'NEW' ) {
|
|
@sections{'ALL','ESTABLISHED','RELATED','INVALID','UNTRACKED', 'NEW'} = ( 1, 1, 1, 1, 1, 1 );
|
|
}
|
|
|
|
|
|
|
|
$next_section = $section_map{$sect};
|
|
}
|
|
|
|
sub next_section() {
|
|
|
|
if ( $next_section == RELATED_SECTION ) {
|
|
finish_section 'ESTABLISHED';
|
|
} elsif ( $next_section == INVALID_SECTION ) {
|
|
finish_section ( 'ESTABLISHED,RELATED' );
|
|
} elsif ( $next_section == UNTRACKED_SECTION ) {
|
|
finish_section ( 'ESTABLISHED,RELATED,INVALID' );
|
|
} elsif ( $next_section == NEW_SECTION ) {
|
|
finish_section ( 'ESTABLISHED,RELATED,INVALID,UNTRACKED' );
|
|
}
|
|
|
|
$section = $next_section;
|
|
}
|
|
|
|
#
|
|
# Build a source or destination zone list
|
|
#
|
|
sub build_zone_list( $$$\$\$ ) {
|
|
my ($fw, $input, $which, $intrazoneref, $wildref ) = @_;
|
|
my $any = ( $input =~ s/^any/all/ );
|
|
my $exclude;
|
|
my $rest;
|
|
my %exclude;
|
|
my @result;
|
|
#
|
|
# Handle Wildcards
|
|
#
|
|
if ( $input =~ /^(all[-+]*)(![^:]+)?(:.*)?$/ ) {
|
|
$input = $1;
|
|
$exclude = $2;
|
|
$rest = $3;
|
|
|
|
$$wildref = 1;
|
|
|
|
if ( defined $exclude ) {
|
|
$exclude =~ s/!//;
|
|
fatal_error "Invalid exclusion list (!$exclude)" if $exclude =~ /^,|!|,,|,$/;
|
|
for ( split /,/, $exclude ) {
|
|
fatal_error "Unknown zone ($_)" unless defined_zone $_;
|
|
$exclude{$_} = 1;
|
|
}
|
|
}
|
|
|
|
unless ( $input eq 'all' ) {
|
|
if ( $input eq 'all+' ) {
|
|
$$intrazoneref = 1;
|
|
} elsif ( ( $input eq 'all+-' ) || ( $input eq 'all-+' ) ) {
|
|
$$intrazoneref = 1;
|
|
$exclude{$fw} = 1;
|
|
} elsif ( $input eq 'all-' ) {
|
|
$exclude{$fw} = 1;
|
|
} else {
|
|
fatal_error "Invalid $which ($input)";
|
|
}
|
|
}
|
|
|
|
@result = grep ! $exclude{$_}, $any ? all_parent_zones : non_firewall_zones;
|
|
|
|
unshift @result, $fw unless $exclude{$fw};
|
|
|
|
} elsif ( $input =~ /^([^:]+,[^:]+)(:.*)?$/ ) {
|
|
$input = $1;
|
|
$rest = $2;
|
|
$$wildref = 1;
|
|
|
|
$$intrazoneref = ( $input =~ s/\+$// );
|
|
|
|
@result = split_list $input, 'zone';
|
|
} else {
|
|
@result = ( $input );
|
|
}
|
|
|
|
if ( defined $rest ) {
|
|
$_ .= $rest for @result;
|
|
}
|
|
|
|
@result;
|
|
}
|
|
|
|
#
|
|
# Process a Record in the rules file
|
|
#
|
|
sub process_raw_rule1( $$$$$$$$$$$$$$$ ) {
|
|
my ( $target, $source, $dest, $protos, $ports, $sports, $origdest, $ratelimit, $users, $mark, $connlimit, $time, $headers, $condition, $helper ) = @_;
|
|
if ( $source =~ /^none(:.*)?$/i || $dest =~ /^none(:.*)?$/i ) {
|
|
progress_message "Rule \"$currentline\" ignored.";
|
|
return 1;
|
|
}
|
|
|
|
my $intrazone = 0;
|
|
my $wild = 0;
|
|
my $thisline = $currentline; #We must save $currentline because it is overwritten by macro expansion
|
|
my $action = isolate_basic_target $target;
|
|
my $fw = firewall_zone;
|
|
my @source = build_zone_list ( $fw, $source, 'SOURCE', $intrazone, $wild );
|
|
my @dest = build_zone_list ( $fw, $dest, 'DEST' , $intrazone, $wild );
|
|
my @protos = split_list1 $protos, 'Protocol';
|
|
my @users = split_list1 $users, 'USER/GROUP';
|
|
my $generated = 0;
|
|
|
|
$statematch = '';
|
|
|
|
fatal_error "Invalid or missing ACTION ($target)" unless defined $action;
|
|
|
|
if ( @protos > 1 ) {
|
|
fatal_error "Inversion not allowed in a PROTO list" if $protos =~ /!/;
|
|
}
|
|
|
|
for $source ( @source ) {
|
|
for $dest ( @dest ) {
|
|
my $sourcezone = (split( /:/, $source, 2 ) )[0];
|
|
my $destzone = (split( /:/, $dest, 2 ) )[0];
|
|
$destzone = $action =~ /^REDIRECT/ ? $fw : '' unless defined_zone $destzone;
|
|
if ( ! $wild || $intrazone || ( $sourcezone ne $destzone ) ) {
|
|
for my $proto ( @protos ) {
|
|
for my $user ( @users ) {
|
|
if ( process_rule( undef,
|
|
'',
|
|
'',
|
|
$target,
|
|
'',
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$origdest,
|
|
$ratelimit,
|
|
$user,
|
|
$mark,
|
|
$connlimit,
|
|
$time,
|
|
$headers,
|
|
$condition,
|
|
$helper,
|
|
$wild ) ) {
|
|
$generated = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
warning_message qq(Entry generated no $toolname rules) unless $generated;
|
|
|
|
progress_message qq( Rule "$thisline" $done);
|
|
}
|
|
|
|
sub process_raw_rule ( ) {
|
|
my ( $target, $source, $dest, $protos, $ports, $sports, $origdest, $ratelimit, $users, $mark, $connlimit, $time, $headers, $condition, $helper )
|
|
= split_line2( 'rules file',
|
|
\%rulecolumns,
|
|
$rule_commands,
|
|
undef, #Columns
|
|
1 ); #Allow inline matches
|
|
|
|
|
|
fatal_error 'ACTION must be specified' if $target eq '-';
|
|
#
|
|
# Section Names are optional so once we get to an actual rule, we need to be sure that
|
|
# we close off any missing sections.
|
|
#
|
|
next_section if $section != $next_section;
|
|
|
|
my ( @source, @dest );
|
|
|
|
if ( $source =~ /:\(.+\)/ ) {
|
|
@source = split_list3( $source, 'SOURCE' );
|
|
} else {
|
|
@source = ( $source );
|
|
}
|
|
|
|
if ( $dest =~ /:\(.+\)/ ) {
|
|
@dest = split_list3( $dest, 'DEST' );
|
|
} else {
|
|
@dest = ( $dest );
|
|
}
|
|
|
|
for $source ( @source ) {
|
|
$source = join(':', $1, $2 ) if $source =~ /^(.+?):\((.+)\)$/;
|
|
|
|
for $dest ( @dest ) {
|
|
$dest = join( ':', $1, $2 ) if $dest =~ /^(.+?):\((.+)\)$/;
|
|
|
|
process_raw_rule1( $target, $source, $dest, $protos, $ports, $sports, $origdest, $ratelimit, $users, $mark, $connlimit, $time, $headers, $condition, $helper );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
sub intrazone_allowed( $$ ) {
|
|
my ( $zone, $zoneref ) = @_;
|
|
|
|
$zoneref->{complex} && $filter_table->{rules_chain( $zone, $zone )}{policy} ne 'NONE';
|
|
}
|
|
|
|
#
|
|
# Process the BLRules and Rules Files
|
|
#
|
|
sub process_rules() {
|
|
my $blrules = 0;
|
|
my @zones = off_firewall_zones;
|
|
#
|
|
# Populate the state table
|
|
#
|
|
%statetable = ( ESTABLISHED => [ '^', '', '', 'ACCEPT' ] ,
|
|
RELATED => [ '+', $config{RELATED_LOG_LEVEL}, $globals{RELATED_LOG_TAG}, $globals{RELATED_TARGET} , $origin{RELATED_DISPOSITION} , $origin{RELATED_LOG_LEVEL} ] ,
|
|
INVALID => [ '_', $config{INVALID_LOG_LEVEL}, $globals{INVALID_LOG_TAG}, $globals{INVALID_TARGET} , $origin{INVALID_DISPOSITION} , $origin{INVALID_LOG_LEVEL} ] ,
|
|
UNTRACKED => [ '&', $config{UNTRACKED_LOG_LEVEL}, $globals{UNTRACKED_LOG_TAG}, $globals{UNTRACKED_TARGET} , $origin{UNTRACKED_DISPOSITION} , $origin{UNTRACKED_LOG_LEVEL} ] ,
|
|
);
|
|
%section_states = ( BLACKLIST_SECTION , $globals{BLACKLIST_STATES},
|
|
ESTABLISHED_SECTION, 'ESTABLISHED',
|
|
RELATED_SECTION, 'RELATED',
|
|
INVALID_SECTION, 'INVALID',
|
|
UNTRACKED_SECTION, 'UNTRACKED' );
|
|
|
|
#
|
|
# If A_REJECT was specified in shorewall[6].conf, the A_REJECT chain may already exist.
|
|
#
|
|
$usedactions{normalize_action_name( 'A_REJECT' )} = $filter_table->{A_REJECT} if $filter_table->{A_REJECT};
|
|
#
|
|
# Create zone-forwarding chains if required
|
|
#
|
|
for my $zone ( @zones ) {
|
|
my $zoneref = find_zone( $zone );
|
|
my $simple = @zones <= 2 && ! $zoneref->{complex};
|
|
|
|
unless ( @zones <= 2 && ! $zoneref->{complex} ) {
|
|
#
|
|
# Complex zone or we have more than one non-firewall zone -- create a zone forwarding chain
|
|
#
|
|
new_standard_chain zone_forward_chain( $zone );
|
|
}
|
|
}
|
|
#
|
|
# Process the blrules file
|
|
#
|
|
$section = $next_section = BLACKLIST_SECTION;
|
|
|
|
my $fn = open_file( 'blrules', 1, 1 );
|
|
|
|
if ( $fn ) {
|
|
first_entry( sub () {
|
|
my ( $level, $disposition , $tag ) = ( @config{'BLACKLIST_LOG_LEVEL', 'BLACKLIST_DISPOSITION' }, $globals{BLACKLIST_LOG_TAG} ) ;
|
|
my $audit = $disposition =~ /^A_/;
|
|
my $target = $disposition eq 'REJECT' ? 'reject' : $disposition;
|
|
|
|
progress_message2 "$doing $currentfilename...";
|
|
|
|
if ( supplied $level ) {
|
|
ensure_blacklog_chain( $target, $disposition, $level, $tag, $audit );
|
|
ensure_audit_blacklog_chain( $target, $disposition, $level ) if have_capability 'AUDIT_TARGET';
|
|
} elsif ( $audit ) {
|
|
require_capability 'AUDIT_TARGET', "BLACKLIST_DISPOSITION=$disposition", 's';
|
|
verify_audit( $disposition );
|
|
} elsif ( have_capability 'AUDIT_TARGET' ) {
|
|
verify_audit( 'A_' . $disposition );
|
|
}
|
|
|
|
$blrules = 1;
|
|
}
|
|
);
|
|
|
|
process_raw_rule while read_a_line( NORMAL_READ );
|
|
}
|
|
|
|
$section = NULL_SECTION;
|
|
$next_section = NEW_SECTION;
|
|
|
|
add_interface_options( $blrules );
|
|
|
|
#
|
|
# Handle MSS settings in the zones file
|
|
#
|
|
setup_zone_mss;
|
|
|
|
$fn = open_file( 'rules', 1, 1 );
|
|
|
|
if ( $fn ) {
|
|
|
|
set_section_function( &process_section );
|
|
|
|
first_entry "$doing $fn...";
|
|
|
|
process_raw_rule while read_a_line( NORMAL_READ );
|
|
|
|
clear_section_function;
|
|
}
|
|
#
|
|
# No need to finish the NEW section since no rules need to be generated
|
|
#
|
|
$section = $next_section = POLICYACTION_SECTION;
|
|
}
|
|
|
|
sub process_mangle_inline( $$$$$$$$$$$$$$$$$$$$ ) {
|
|
my ($inline, $chainref, $params, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state, $time, $conditional ) = @_;
|
|
|
|
my $oldparms = push_action_params( $inline,
|
|
$chainref,
|
|
$params,
|
|
'none',
|
|
'' ,
|
|
$chainref->{name} );
|
|
|
|
my $inlinefile = $actions{$inline}{file};
|
|
my $matches = fetch_inline_matches;
|
|
|
|
progress_message "..Expanding inline action $inlinefile...";
|
|
|
|
push_open $inlinefile, 2, 1, undef , 2;
|
|
|
|
my $save_comment = push_comment;
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
my ( $moriginalmark, $msource, $mdest, $mprotos, $mports, $msports, $muser, $mtestval, $mlength, $mtos , $mconnbytes, $mhelper, $mheaders, $mprobability , $mdscp , $mstate, $mtime, $mconditional );
|
|
if ( $family == F_IPV4 ) {
|
|
( $moriginalmark, $msource, $mdest, $mprotos, $mports, $msports, $muser, $mtestval, $mlength, $mtos , $mconnbytes, $mhelper, $mprobability, $mdscp, $mstate, $mtime, $mconditional ) =
|
|
split_line2( 'mangle file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
probability => 12 ,
|
|
scp => 13,
|
|
state => 14,
|
|
time => 15,
|
|
switch => 16,
|
|
},
|
|
{},
|
|
17,
|
|
1 );
|
|
$headers = $mheaders = '-';
|
|
} else {
|
|
( $moriginalmark, $msource, $mdest, $mprotos, $mports, $msports, $muser, $mtestval, $mlength, $mtos , $mconnbytes, $mhelper, $mheaders, $mprobability, $mdscp, $mstate, $mtime, $mconditional ) =
|
|
split_line2( 'mangle file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
headers => 12,
|
|
probability => 13,
|
|
dscp => 14,
|
|
state => 15,
|
|
time => 16,
|
|
switch => 17,
|
|
},
|
|
{},
|
|
18,
|
|
1 );
|
|
}
|
|
|
|
fatal_error 'ACTION must be specified' if $moriginalmark eq '-';
|
|
|
|
if ( $moriginalmark eq 'DEFAULTS' ) {
|
|
default_action_params( $chainref, split_list( $msource, 'defaults' ) );
|
|
next;
|
|
}
|
|
|
|
$msource = $source if $msource eq '-';
|
|
$mdest = $dest if $mdest eq '-';
|
|
$mprotos = $protos if $mprotos eq '-';
|
|
|
|
for my $proto (split_list( $mprotos, 'Protocol' ) ) {
|
|
process_mangle_rule1( $chainref,
|
|
$moriginalmark,
|
|
$msource,
|
|
$mdest,
|
|
$proto,
|
|
merge_macro_column( $mports, $ports ),
|
|
merge_macro_column( $msports, $sports ),
|
|
merge_macro_column( $muser, $muser ),
|
|
merge_macro_column( $mtestval, $testval ),
|
|
merge_macro_column( $mlength, $length ),
|
|
merge_macro_column( $mtos , $tos ),
|
|
merge_macro_column( $mconnbytes, $connbytes ),
|
|
merge_macro_column( $mhelper, $helper ),
|
|
merge_macro_column( $mheaders, $headers ),
|
|
merge_macro_column( $mprobability , $probability ),
|
|
merge_macro_column( $mdscp , $dscp ),
|
|
merge_macro_column( $mstate, $state ),
|
|
merge_macro_column( $mtime, $time ),
|
|
merge_macro_column( $mconditional, $conditional ),
|
|
);
|
|
}
|
|
|
|
progress_message " Rule \"$currentline\" $done";
|
|
|
|
set_inline_matches( $matches );
|
|
}
|
|
|
|
pop_comment( $save_comment );
|
|
|
|
pop_open;
|
|
|
|
progress_message "..End inline action $inlinefile";
|
|
|
|
pop_action_params( $oldparms );
|
|
}
|
|
|
|
################################################################################
|
|
# Code moved from the Tc module in Shorewall 5.0.7 #
|
|
################################################################################
|
|
#
|
|
# Process a rule from the mangle file. When the target is an action name, this
|
|
# function will be called recursively for each rule in the action body. Recursive
|
|
# calls pass a chain reference in the first argument and the generated rule is
|
|
# appended to that chain. The chain with be the action's chain unless the action
|
|
# is inlined, in which case it will be the chain which invoked the action.
|
|
#
|
|
sub process_mangle_rule1( $$$$$$$$$$$$$$$$$$$ ) {
|
|
my ( $chainref, $action, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state, $time, $condition) = @_;
|
|
|
|
my %designators = (
|
|
P => PREROUTING,
|
|
I => INPUT,
|
|
F => FORWARD,
|
|
O => OUTPUT,
|
|
T => POSTROUTING,
|
|
R => REALPREROUTING,
|
|
);
|
|
|
|
my %chainlabels = ( 1 => 'PREROUTING',
|
|
2 => 'INPUT',
|
|
4 => 'FORWARD',
|
|
8 => 'OUTPUT',
|
|
16 => 'POSTROUTING' );
|
|
|
|
my %chainnames = ( 1 => 'tcpre',
|
|
2 => 'tcin',
|
|
4 => 'tcfor',
|
|
8 => 'tcout',
|
|
16 => 'tcpost',
|
|
32 => 'sticky',
|
|
64 => 'sticko',
|
|
128 => 'PREROUTING',
|
|
);
|
|
|
|
my $inchain = defined $chainref;
|
|
my $inaction;
|
|
my $target = '';
|
|
my $junk = '';
|
|
my $raw_matches = '';
|
|
my $chain = 0;
|
|
my $matches = '';
|
|
my $params = '';
|
|
my $done = 0;
|
|
my $default_chain = 0;
|
|
my $restriction = NO_RESTRICT;
|
|
my $exceptionrule = '';
|
|
my $device = '';
|
|
my $cmd;
|
|
my $designator;
|
|
my $ttl = 0;
|
|
my $fw = firewall_zone;
|
|
my $usergenerated;
|
|
my $actiontype;
|
|
my $commandref;
|
|
my $prerule = '';
|
|
#
|
|
# Subroutine for handling MARK and CONNMARK. We use an enclosure so as to keep visibility of the
|
|
# function's local variables without making them static. process_mangle_rule1() is called
|
|
# recursively, so static (our) variables cannot be used unless they are saved/restored during
|
|
# recursion.
|
|
#
|
|
my $handle_mark_param = sub( ) {
|
|
my ( $option, $marktype ) = @_;
|
|
my $and_or = $params =~ s/^([|&])// ? $1 : '';
|
|
|
|
if ( $params =~ /-/ ) {
|
|
#
|
|
# A Mark Range
|
|
#
|
|
fatal_error "'|' and '&' may not be used with a mark range" if $and_or;
|
|
fatal_error "A mark range is is not allowed with ACTION $cmd" if $cmd !~ /^(?:CONN)?MARK$/;
|
|
my ( $mark, $mark2 ) = split /-/, $params, 2;
|
|
my $markval = validate_mark $mark;
|
|
fatal_error "Invalid mark range ($mark-$mark2)" if $mark =~ m'/';
|
|
my $mark2val = validate_mark $mark2;
|
|
fatal_error "Invalid mark range ($mark-$mark2)" unless $markval < $mark2val;
|
|
require_capability 'STATISTIC_MATCH', 'A mark range', 's';
|
|
( $mark2, my $mask ) = split '/', $mark2;
|
|
$mask = $globals{TC_MASK} unless supplied $mask;
|
|
|
|
my $increment = 1;
|
|
my $shift = 0;
|
|
|
|
$mask = numeric_value( $mask );
|
|
|
|
$increment <<= 1, $shift++ until $increment & $mask;
|
|
|
|
$mask = in_hex $mask;
|
|
|
|
my $marks = ( ( $mark2val - $markval ) >> $shift ) + 1;
|
|
|
|
$chain ||= $designator;
|
|
$chain ||= $default_chain;
|
|
|
|
$option ||= ( $and_or eq '|' ? '--or-mark' : $and_or ? '--and-mark' : '--set-mark' );
|
|
|
|
my $chainref = ensure_chain( 'mangle', $chain = $chainnames{$chain} );
|
|
|
|
$restriction |= $chainref->{restriction};
|
|
|
|
for ( my $packet = 0; $packet < $marks; $packet++, $markval += $increment ) {
|
|
my $match = "-m statistic --mode nth --every $marks --packet $packet ";
|
|
|
|
expand_rule( $chainref,
|
|
$restriction,
|
|
$prerule ,
|
|
do_proto( $proto, $ports, $sports ) .
|
|
$match .
|
|
do_user( $user ) .
|
|
do_test( $testval, $mask ) .
|
|
do_length( $length ) .
|
|
do_tos( $tos ) .
|
|
do_connbytes( $connbytes ) .
|
|
do_helper( $helper ) .
|
|
do_headers( $headers ) .
|
|
do_probability( $probability ) .
|
|
do_dscp( $dscp ) .
|
|
do_time( $time ) .
|
|
do_condition( $condition, $chainref->{name} ) .
|
|
state_match( $state ) .
|
|
$raw_matches ,
|
|
$source ,
|
|
$dest ,
|
|
'' ,
|
|
"$target $option " . join( '/', in_hex( $markval ) , $mask ) ,
|
|
'',
|
|
$target ,
|
|
$exceptionrule ,
|
|
'' );
|
|
}
|
|
|
|
$done = 1;
|
|
} else {
|
|
#
|
|
# A Single Mark
|
|
#
|
|
my $mark = $params;
|
|
my $val;
|
|
if ( supplied $mark ) {
|
|
if ( $marktype == SMALLMARK ) {
|
|
$val = verify_small_mark( $mark );
|
|
} else {
|
|
$val = validate_mark( $mark );
|
|
}
|
|
} else {
|
|
$val = numeric_value( $mark = $globals{TC_MASK} );
|
|
}
|
|
|
|
if ( $config{PROVIDER_OFFSET} ) {
|
|
my $limit = $globals{TC_MASK};
|
|
unless ( have_capability 'FWMARK_RT_MASK' ) {
|
|
fatal_error "Marks <= $limit may not be set in the PREROUTING or OUTPUT chains when HIGH_ROUTE_MARKS=Yes"
|
|
if $val && ( $chain && ( PREROUTING | OUTPUT ) ) && $val <= $globals{TC_MASK};
|
|
}
|
|
}
|
|
|
|
if ( $option ) {
|
|
$target = join( ' ', $target, $option );
|
|
} else {
|
|
$target = join( ' ', $target, $and_or eq '|' ? '--or-mark' : $and_or ? '--and-mark' : '--set-mark' );
|
|
}
|
|
|
|
( $mark, my $mask ) = split '/', $mark;
|
|
|
|
if ( supplied $mask ) {
|
|
$target = join( ' ', $target , join( '/', $mark , $mask ) );
|
|
} else {
|
|
$target = join( ' ', $target , $mark );
|
|
}
|
|
}
|
|
};
|
|
#
|
|
# Subroutine to handle ADD and DEL rules
|
|
#
|
|
my $ipset_command = sub () {
|
|
my %xlate = ( ADD => 'add-set' , DEL => 'del-set' );
|
|
|
|
require_capability( 'IPSET_MATCH', "$cmd rules", '' );
|
|
fatal_error "$cmd rules require a set name parameter" unless $params;
|
|
|
|
my ( $setname, $flags, $rest ) = split ':', $params, 3;
|
|
fatal_error "Invalid ADD/DEL parameter ($params)" if $rest;
|
|
$setname =~ s/^\+//;
|
|
fatal_error "Expected ipset name ($setname)" unless $setname =~ /^(6_)?[a-zA-Z][-\w]*$/;
|
|
fatal_error "Invalid flags ($flags)" unless defined $flags && $flags =~ /^(dst|src)(,(dst|src)){0,5}$/;
|
|
$target = join( ' ', 'SET --' . $xlate{$cmd} , $setname , $flags );
|
|
};
|
|
|
|
my %commands = (
|
|
ADD => {
|
|
defaultchain => PREROUTING,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
$ipset_command->();
|
|
}
|
|
},
|
|
|
|
CHECKSUM => {
|
|
defaultchain => POSTROUTING,
|
|
allowedchains => POSTROUTING | FORWARD | OUTPUT,
|
|
minparams => 0,
|
|
maxparams => 0 ,
|
|
function => sub() {
|
|
$target = 'CHECKSUM --checksum-fill';
|
|
},
|
|
},
|
|
|
|
CLASSIFY => {
|
|
defaultchain => POSTROUTING,
|
|
allowedchains => POSTROUTING | FORWARD | OUTPUT,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
fatal_error "Valid class ID expected ($params)" unless $params =~ /^([0-9a-fA-F]+):([0-9a-fA-F]+)$/;
|
|
|
|
my $classid = join( ':', normalize_hex( $1 ), normalize_hex( $2 ) );
|
|
|
|
$target = "CLASSIFY --set-class $classid";
|
|
|
|
if ( $config{TC_ENABLED} eq 'Internal' || $config{TC_ENABLED} eq 'Shared' ) {
|
|
fatal_error "Unknown Class ($params)" unless ( $device = $classids{$classid} );
|
|
|
|
fatal_error "IFB Classes may not be specified in tcrules" if @{$tcdevices{$device}{redirected}};
|
|
|
|
unless ( $tcclasses{$device}{hex_value $2}{leaf} ) {
|
|
warning_message "Non-leaf Class ($params) - tcrule ignored";
|
|
$done = 1;
|
|
}
|
|
|
|
if ( $dest eq '-' ) {
|
|
$dest = $device;
|
|
} else {
|
|
$dest = join( ':', $device, $dest ) unless $dest =~ /^[[:alpha:]]/;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
|
|
CONNMARK => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
$target = 'CONNMARK';
|
|
$handle_mark_param->('' , HIGHMARK );
|
|
},
|
|
},
|
|
|
|
CONTINUE => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 0,
|
|
function => sub () {
|
|
$target = 'RETURN';
|
|
},
|
|
},
|
|
|
|
DEL => {
|
|
defaultchain => PREROUTING,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
$ipset_command->();
|
|
}
|
|
},
|
|
|
|
DIVERT => {
|
|
defaultchain => REALPREROUTING,
|
|
allowedchains => PREROUTING | REALPREROUTING,
|
|
minparams => 0,
|
|
maxparams => 0,
|
|
function => sub () {
|
|
fatal_error 'DIVERT is only allowed in the PREROUTING chain' if $designator &&
|
|
$designator != PREROUTING &&
|
|
$designator != REALPREROUTING;
|
|
my $mark = in_hex( $globals{TPROXY_MARK} ) . '/' . in_hex( $globals{TPROXY_MARK} );
|
|
|
|
unless ( $divertref ) {
|
|
$divertref = new_chain( 'mangle', 'divert' );
|
|
add_ijump( $divertref , j => 'MARK', targetopts => "--set-mark $mark" );
|
|
add_ijump( $divertref , j => 'ACCEPT' );
|
|
}
|
|
|
|
$target = 'divert';
|
|
|
|
$matches = '! --tcp-flags FIN,SYN,RST,ACK SYN -m socket --transparent ';
|
|
},
|
|
},
|
|
|
|
DIVERTHA => {
|
|
defaultchain => REALPREROUTING,
|
|
allowedchains => PREROUTING | REALPREROUTING,
|
|
minparams => 0,
|
|
maxparams => 0,
|
|
function => sub () {
|
|
fatal_error 'DIVERTHA is only allowed in the PREROUTING chain' if $designator && $designator != PREROUTING;
|
|
my $mark = in_hex( $globals{TPROXY_MARK} ) . '/' . in_hex( $globals{TPROXY_MARK} );
|
|
|
|
unless ( $divertref ) {
|
|
$divertref = new_chain( 'mangle', 'divert' );
|
|
add_ijump( $divertref , j => 'MARK', targetopts => "--set-mark $mark" );
|
|
add_ijump( $divertref , j => 'ACCEPT' );
|
|
}
|
|
|
|
$target = 'divert';
|
|
|
|
$matches = '-m socket ';
|
|
},
|
|
},
|
|
|
|
DROP => {
|
|
defaultchain => 0,
|
|
allowedchains => PREROUTING | FORWARD | OUTPUT | POSTROUTING,
|
|
minparams => 0,
|
|
maxparams => 0,
|
|
function => sub() {
|
|
$target = 'DROP';
|
|
}
|
|
},
|
|
|
|
DSCP => {
|
|
defaultchain => POSTROUTING,
|
|
allowedchains => PREROUTING | FORWARD | OUTPUT | POSTROUTING,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
require_capability 'DSCP_TARGET', 'The DSCP action', 's';
|
|
my $dscp = numeric_value( $params );
|
|
$dscp = $dscpmap{$params} unless defined $dscp;
|
|
fatal_error( "Invalid DSCP ($params)" ) unless defined $dscp && $dscp <= 0x38 && ! ( $dscp & 1 );
|
|
$target = 'DSCP --set-dscp ' . in_hex( $dscp );
|
|
},
|
|
},
|
|
|
|
ECN => {
|
|
defaultchain => POSTROUTING,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 0,
|
|
function => sub() {
|
|
fatal_error "The ECN target is only available with IPv4" if $family == F_IPV6;
|
|
|
|
if ( $proto eq '-' ) {
|
|
$proto = TCP;
|
|
} else {
|
|
$proto = resolve_proto( $proto ) || 0;
|
|
fatal_error "Only PROTO tcp (6) is allowed with the ECN action" unless $proto == TCP;
|
|
}
|
|
|
|
$target = 'ECN --ecn-tcp-remove';
|
|
}
|
|
},
|
|
|
|
HL => {
|
|
defaultchain => FORWARD,
|
|
allowedchains => PREROUTING | FORWARD,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
fatal_error "HL is not supported in IPv4 - use TTL instead" if $family == F_IPV4;
|
|
|
|
$params =~ /^([-+]?(\d+))$/;
|
|
|
|
fatal_error "Invalid HL specification( HL($params) )" unless supplied( $1 ) && ( $1 eq $2 || $2 != 0 ) && ( $params = abs $params ) < 256;
|
|
|
|
$target = 'HL';
|
|
|
|
if ( $1 =~ /^\+/ ) {
|
|
$target .= " --hl-inc $params";
|
|
} elsif ( $1 =~ /\-/ ) {
|
|
$target .= " --hl-dec $params";
|
|
} else {
|
|
$target .= " --hl-set $params";
|
|
};
|
|
},
|
|
},
|
|
|
|
INLINE => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 0,
|
|
function => sub() {
|
|
$target ||= '';
|
|
},
|
|
},
|
|
|
|
IMQ => {
|
|
defaultchain => PREROUTING,
|
|
allowedchains => PREROUTING,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
require_capability 'IMQ_TARGET', 'IMQ', 's';
|
|
my $imq = numeric_value( $params );
|
|
fatal_error "Invalid IMQ number ($params)" unless defined $imq;
|
|
$target = "IMQ --todev $imq";
|
|
},
|
|
},
|
|
|
|
IPTABLES => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
fatal_error "Invalid ACTION (IPTABLES)" unless $family == F_IPV4;
|
|
my ( $tgt, $options ) = split( ' ', $params, 2 );
|
|
my $target_type = $builtin_target{$tgt};
|
|
fatal_error "Unknown target ($tgt)" unless $target_type;
|
|
fatal_error "The $tgt TARGET is not allowed in the mangle table" unless $target_type & MANGLE_TABLE;
|
|
$target = $params;
|
|
$usergenerated = 1;
|
|
},
|
|
},
|
|
|
|
IP6TABLES => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
fatal_error "Invalid ACTION (IP6TABLES)" unless $family == F_IPV6;
|
|
my ( $tgt, $options ) = split( ' ', $params, 2 );
|
|
my $target_type = $builtin_target{$tgt};
|
|
fatal_error "Unknown target ($tgt)" unless $target_type;
|
|
fatal_error "The $tgt TARGET is not allowed in the mangle table" unless $target_type & MANGLE_TABLE;
|
|
$target = $params;
|
|
$usergenerated = 1;
|
|
},
|
|
},
|
|
|
|
IPMARK => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 4,
|
|
function => sub () {
|
|
my ( $srcdst, $mask1, $mask2, $shift ) = ('src', 255, 0, 0 );
|
|
|
|
require_capability 'IPMARK_TARGET', 'IPMARK', 's';
|
|
|
|
my $val;
|
|
|
|
if ( supplied $params ) {
|
|
my ( $sd, $m1, $m2, $s , $bad ) = split ',', $params;
|
|
|
|
fatal_error "Invalid IPMARK parameters ($params)" if $bad;
|
|
fatal_error "Invalid IPMARK parameter ($sd)" unless ( $sd eq 'src' || $sd eq 'dst' );
|
|
$srcdst = $sd;
|
|
|
|
if ( supplied $m1 ) {
|
|
$val = numeric_value ($m1);
|
|
fatal_error "Invalid Mask ($m1)" unless defined $val && $val && $val <= 0xffffffff;
|
|
$mask1 = in_hex ( $val & 0xffffffff );
|
|
}
|
|
|
|
if ( supplied $m2 ) {
|
|
$val = numeric_value ($m2);
|
|
fatal_error "Invalid Mask ($m2)" unless defined $val && $val <= 0xffffffff;
|
|
$mask2 = in_hex ( $val & 0xffffffff );
|
|
}
|
|
|
|
if ( defined $s ) {
|
|
$val = numeric_value ($s);
|
|
fatal_error "Invalid Shift Bits ($s)" unless defined $val && $val >= 0 && $val < 128;
|
|
$shift = $s;
|
|
}
|
|
};
|
|
|
|
$target = "IPMARK --addr $srcdst --and-mask $mask1 --or-mask $mask2 --shift $shift";
|
|
},
|
|
},
|
|
|
|
MARK => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
mask => in_hex( $globals{TC_MASK} ),
|
|
function => sub () {
|
|
$target = 'MARK';
|
|
$handle_mark_param->('', , HIGHMARK );
|
|
},
|
|
},
|
|
|
|
NFLOG => {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS,
|
|
minparams => 0,
|
|
maxparams => 3,
|
|
function => sub () {
|
|
$target = validate_level( "NFLOG($params)" );
|
|
}
|
|
},
|
|
|
|
RESTORE => {
|
|
defaultchain => 0,
|
|
allowedchains => PREROUTING | INPUT | FORWARD | OUTPUT | POSTROUTING,
|
|
minparams => 0,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
$target = 'CONNMARK ';
|
|
if ( supplied $params ) {
|
|
$handle_mark_param->( '--restore-mark --mask ',
|
|
$config{TC_EXPERT} ? HIGHMARK : SMALLMARK );
|
|
} else {
|
|
$target .= '--restore-mark --mask ' . in_hex( $globals{TC_MASK} );
|
|
}
|
|
},
|
|
},
|
|
|
|
SAME => {
|
|
defaultchain => PREROUTING,
|
|
allowedchains => PREROUTING | OUTPUT | STICKY | STICKO,
|
|
minparams => 0,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
$target = ( $chain == OUTPUT ? 'sticko' : 'sticky' );
|
|
$restriction = DESTIFACE_DISALLOW;
|
|
ensure_mangle_chain( $target );
|
|
if (supplied $params) {
|
|
$ttl = numeric_value( $params );
|
|
fatal_error "The SAME timeout must be positive" unless $ttl;
|
|
} else {
|
|
$ttl = 300;
|
|
}
|
|
|
|
$sticky++;
|
|
},
|
|
},
|
|
|
|
SAVE => {
|
|
defaultchain => 0,
|
|
allowedchains => PREROUTING | INPUT | FORWARD | OUTPUT | POSTROUTING,
|
|
minparams => 0,
|
|
maxparams => 1,
|
|
function => sub () {
|
|
$target = 'CONNMARK ';
|
|
if ( supplied $params ) {
|
|
$handle_mark_param->( '--save-mark --mask ' ,
|
|
$config{TC_EXPERT} ? HIGHMARK : SMALLMARK );
|
|
} else {
|
|
$target .= '--save-mark --mask ' . in_hex( $globals{TC_MASK} );
|
|
}
|
|
},
|
|
},
|
|
|
|
TCPMSS => {
|
|
defaultchain => FORWARD,
|
|
allowedchains => FORWARD | POSTROUTING,
|
|
minparams => 0,
|
|
maxparams => 2,
|
|
function => sub () {
|
|
if ( $proto eq '-' ) {
|
|
$proto = TCP;
|
|
} else {
|
|
fatal_error 'TCPMSS only valid with TCP' unless $proto eq '6' || $proto eq 'tcp';
|
|
}
|
|
|
|
$target = 'TCPMSS ';
|
|
$matches .= '--tcp-flags SYN,RST SYN ';
|
|
|
|
if ( supplied $params ) {
|
|
my ( $mss, $ipsec ) = split /,/, $params;
|
|
|
|
if ( supplied $mss ) {
|
|
if ( $mss eq 'pmtu' ) {
|
|
$target .= '--clamp-mss-to-pmtu';
|
|
} else {
|
|
my $num = numeric_value $mss;
|
|
fatal_error "Invalid MSS ($mss)" unless defined $num && $num >= 500 && $num < 65534;
|
|
$target .= "--set-mss $num";
|
|
}
|
|
} else {
|
|
$target .= '--clamp-mss-to-pmtu';
|
|
}
|
|
if ( supplied $ipsec && $ipsec ne 'all' ) {
|
|
if ( $ipsec eq '-' || $ipsec eq 'none' ) {
|
|
$matches .= '-m policy --pol none --dir out ';
|
|
} elsif ( $ipsec eq 'ipsec' ) {
|
|
$matches .= '-m policy --pol ipsec --dir out ';
|
|
} else {
|
|
fatal_error "Invalid ipsec parameter ($ipsec)";
|
|
}
|
|
|
|
require_capability 'POLICY_MATCH', "The $ipsec ipsec option", 's';
|
|
}
|
|
} else {
|
|
$target .= '--clamp-mss-to-pmtu';
|
|
}
|
|
},
|
|
},
|
|
|
|
TOS => {
|
|
defaultchain => 0,
|
|
allowedchains => PREROUTING | FORWARD | OUTPUT | POSTROUTING,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
$target = 'TOS ' . decode_tos( $params , 2 );
|
|
},
|
|
},
|
|
|
|
TPROXY => {
|
|
defaultchain => REALPREROUTING,
|
|
allowedchains => PREROUTING | REALPREROUTING,
|
|
minparams => 0,
|
|
maxparams => 2,
|
|
function => sub() {
|
|
require_capability( 'TPROXY_TARGET', 'Use of TPROXY', 's');
|
|
|
|
fatal_error "TPROXY is not supported in FORMAT 1 tcrules files" if $file_format < 2;
|
|
|
|
my ( $port, $ip, $bad );
|
|
|
|
if ( $params ) {
|
|
( $port, $ip ) = split /,/, $params, 2;
|
|
}
|
|
|
|
my $mark = in_hex( $globals{TPROXY_MARK} ) . '/' . in_hex( $globals{TPROXY_MARK} );
|
|
|
|
if ( $port ) {
|
|
$port = validate_port( 'tcp', $port );
|
|
} else {
|
|
$port = 0;
|
|
}
|
|
|
|
$target = "TPROXY --on-port $port";
|
|
|
|
if ( supplied $ip ) {
|
|
if ( $family == F_IPV6 ) {
|
|
if ( $ip =~ /^\[(.+)\]$/ || $ip =~ /^<(.+)>$/ ) {
|
|
$ip = $1;
|
|
} elsif ( $ip =~ /^\[(.+)\]\/(\d+)$/ ) {
|
|
$ip = join( '/', $1, $2 );
|
|
}
|
|
}
|
|
|
|
validate_address $ip, 1;
|
|
$target .= " --on-ip $ip";
|
|
}
|
|
|
|
$target .= " --tproxy-mark $mark";
|
|
|
|
$exceptionrule = '-p tcp ';
|
|
|
|
},
|
|
},
|
|
|
|
TTL => {
|
|
defaultchain => FORWARD,
|
|
allowedchains => PREROUTING | FORWARD,
|
|
minparams => 1,
|
|
maxparams => 1,
|
|
function => sub() {
|
|
fatal_error "TTL is not supported in IPv6 - use HL instead" if $family == F_IPV6;
|
|
$target = 'TTL';
|
|
|
|
$params =~ /^([-+]?(\d+))$/;
|
|
|
|
fatal_error "Invalid TTL specification( TTL($params) )" unless supplied( $1 ) && ( $1 eq $2 || $2 != 0 ) && ( $params = abs $params ) < 256;
|
|
|
|
if ( $1 =~ /^\+/ ) {
|
|
$target .= " --ttl-inc $params";
|
|
} elsif ( $1 =~ /\-/ ) {
|
|
$target .= " --ttl-dec $params";
|
|
} else {
|
|
$target .= " --ttl-set $params";
|
|
}
|
|
},
|
|
},
|
|
);
|
|
#
|
|
# Subroutine for handling normal actions
|
|
#
|
|
my $actionref = {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS ,
|
|
minparams => 0 ,
|
|
maxparams => 16 ,
|
|
function => sub() {
|
|
fatal_error( qq(Action $cmd may not be used in the mangle file) ) unless $actiontype & MANGLE_TABLE;
|
|
#
|
|
# Verify action 'proto', if any
|
|
#
|
|
$proto = determine_action_protocol( $cmd, $proto );
|
|
#
|
|
# Create the action:level:tag:param tuple.
|
|
#
|
|
my $normalized_target = normalize_action( $cmd, '', $params );
|
|
fatal_error( "Action $cmd invoked Recursively (" . join( '->', map( external_name( $_ ), @actionstack , $normalized_target ) ) . ')' ) if $active{$cmd};
|
|
|
|
my $ref = use_action( 'mangle', $normalized_target );
|
|
|
|
if ( $ref ) {
|
|
#
|
|
# First reference to this tuple - process_action may modify both $normalized_target and $ref!!!
|
|
#
|
|
process_action( $normalized_target, $ref, $chainnames{$chain} );
|
|
#
|
|
# Capture the name of the action chain
|
|
#
|
|
} else {
|
|
#
|
|
# We've seen this tuple before
|
|
#
|
|
$ref = $usedactions{$normalized_target};
|
|
}
|
|
|
|
$target = $ref->{name};
|
|
$commandref->{allowedchains} = $ref->{allowedchains};
|
|
}
|
|
};
|
|
#
|
|
# Subroutine to resolve which chain to use
|
|
#
|
|
my $resolve_chain = sub() {
|
|
unless ( $chain ) {
|
|
$chain ||= $designator;
|
|
$chain ||= $commandref->{defaultchain};
|
|
$chain ||= $default_chain;
|
|
}
|
|
|
|
$chainref = ensure_chain( 'mangle', $chainnames{$chain} );
|
|
};
|
|
#
|
|
#
|
|
# Subroutine for handling inline actions
|
|
#
|
|
my $inlineref = {
|
|
defaultchain => 0,
|
|
allowedchains => ALLCHAINS ,
|
|
minparams => 0 ,
|
|
maxparams => 16 ,
|
|
function => sub() {
|
|
fatal_error( qq(Action $cmd may not be used in the mangle file) ) unless $actiontype & MANGLE_TABLE;
|
|
|
|
$resolve_chain->() unless $inchain;
|
|
|
|
process_mangle_inline( $cmd,
|
|
$chainref,
|
|
$params,
|
|
$source,
|
|
$dest,
|
|
$proto,
|
|
$ports,
|
|
$sports,
|
|
$user,
|
|
$testval,
|
|
$length,
|
|
$tos ,
|
|
$connbytes,
|
|
$helper,
|
|
$headers,
|
|
$probability ,
|
|
$dscp ,
|
|
$state,
|
|
$time,
|
|
$condition );
|
|
$done = 1;
|
|
}
|
|
};
|
|
#
|
|
# Function Body
|
|
#
|
|
if ( $inchain ) {
|
|
( $inaction ) = split /:/, $chainref->{action} if $chainref->{action};
|
|
#
|
|
# Set chain type
|
|
#
|
|
$chain = $chainref->{chainnumber} || ACTIONCHAIN;
|
|
}
|
|
|
|
( $cmd, $designator ) = split_action( $action );
|
|
|
|
if ( supplied $designator ) {
|
|
fatal_error "A chain designator may not be specified in an action body" if $inaction;
|
|
my $temp = $designators{$designator};
|
|
fatal_error "Invalid chain designator ( $designator )" unless $temp;
|
|
$designator = $temp;
|
|
}
|
|
|
|
( $cmd , $params ) = get_target_param1( $cmd );
|
|
|
|
$actiontype = $builtin_target{$cmd} || $targets{$cmd} || 0;
|
|
|
|
unless ( $commandref = $commands{$cmd} ) {
|
|
if ( $actiontype & ACTION ) {
|
|
$commandref = $actionref;
|
|
} elsif ( $actiontype & INLINE ) {
|
|
$commandref = $inlineref;
|
|
} else {
|
|
fatal_error "Invalid ACTION ($cmd)";
|
|
}
|
|
}
|
|
|
|
if ( $cmd eq 'INLINE' ) {
|
|
( $target, $cmd, $params, $junk, $raw_matches ) = handle_inline( MANGLE_TABLE, 'mangle', $action, $cmd, $params, '' );
|
|
} else {
|
|
$raw_matches = get_inline_matches(0);
|
|
}
|
|
#
|
|
# Handle early matches
|
|
#
|
|
if ( $raw_matches =~ s/s*\+// ) {
|
|
$prerule = $raw_matches;
|
|
$raw_matches = '';
|
|
}
|
|
|
|
if ( $source ne '-' ) {
|
|
if ( $source eq $fw ) {
|
|
fatal_error 'Rules with SOURCE $FW must use the OUTPUT chain' if $designator && $designator != OUTPUT;
|
|
$chain = OUTPUT;
|
|
$source = '-';
|
|
} elsif ( $source =~ s/^($fw):// ) {
|
|
fatal_error 'Rules with SOURCE $FW must use the OUTPUT chain' if $designator && $designator != OUTPUT;
|
|
$chain = OUTPUT;
|
|
}
|
|
}
|
|
|
|
if ( $dest ne '-' ) {
|
|
if ( $dest eq $fw ) {
|
|
fatal_error 'Rules with DEST $FW must use the INPUT chain' if $designator && $designator ne INPUT;
|
|
$chain = INPUT;
|
|
$dest = '-';
|
|
} elsif ( $dest =~ s/^$fw\:// ) {
|
|
fatal_error 'Rules with DEST $FW must use the INPUT chain' if $designator && $designator ne INPUT;
|
|
$chain = INPUT;
|
|
}
|
|
}
|
|
|
|
unless ( $default_chain ) {
|
|
$default_chain = $config{MARK_IN_FORWARD_CHAIN} ? FORWARD : PREROUTING;
|
|
}
|
|
|
|
my @params = split_list1( $params, 'parameter' );
|
|
|
|
if ( @params > $commandref->{maxparams} ) {
|
|
if ( $commandref->{maxparams} == 1 ) {
|
|
fatal_error "The $cmd ACTION only accepts one parmeter";
|
|
} else {
|
|
fatal_error "The $cmd ACTION only accepts $commandref->{maxparams} parmeters";
|
|
}
|
|
}
|
|
|
|
if ( @params < $commandref->{minparams} ) {
|
|
if ( $commandref->{maxparams} == 1 ) {
|
|
fatal_error "The $cmd requires a parameter";
|
|
} else {
|
|
fatal_error "The $cmd ACTION requires at least $commandref->{maxparams} parmeters";
|
|
}
|
|
}
|
|
|
|
if ( $state ne '-' ) {
|
|
my @state = split_list( $state, 'state' );
|
|
my %state = %validstates;
|
|
|
|
for ( @state ) {
|
|
fatal_error "Invalid STATE ($_)" unless exists $state{$_};
|
|
fatal_error "Duplicate STATE ($_)" if $state{$_}++;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Call the command's processing function
|
|
#
|
|
$commandref->{function}->();
|
|
|
|
unless ( $done ) {
|
|
if ( $inchain ) {
|
|
if ( $chain == ACTIONCHAIN ) {
|
|
fatal_error "$cmd rules are not allowed in the $chainlabels{$chain} chain" unless $commandref->{allowedchains} & $chainref->{allowedchains};
|
|
$chainref->{allowedchains} &= $commandref->{allowedchains};
|
|
$chainref->{allowedchains} &= (OUTPUT | POSTROUTING ) if $user ne '-';
|
|
} else {
|
|
#
|
|
# Inline within one of the standard chains
|
|
#
|
|
fatal_error "$cmd rules are not allowed in the $chainlabels{$chain} chain" unless $commandref->{allowedchains} & $chain;
|
|
unless ( $chain == OUTPUT || $chain == POSTROUTING ) {
|
|
fatal_error 'A USER/GROUP may only be specified when the SOURCE is $FW' unless $user eq '-';
|
|
}
|
|
}
|
|
} else {
|
|
$resolve_chain->();
|
|
fatal_error "$cmd rules are not allowed in the $chainlabels{$chain} chain" unless $commandref->{allowedchains} & $chain;
|
|
unless ( $chain == OUTPUT || $chain == POSTROUTING ) {
|
|
fatal_error 'A USER/GROUP may only be specified when the SOURCE is $FW' unless $user eq '-';
|
|
}
|
|
|
|
$chainref = ensure_chain( 'mangle', $chainnames{$chain} );
|
|
}
|
|
|
|
$restriction |= $chainref->{restriction};
|
|
|
|
expand_rule( $chainref ,
|
|
$restriction,
|
|
$prerule,
|
|
do_proto( $proto, $ports, $sports) . $matches .
|
|
do_user( $user ) .
|
|
do_test( $testval, $globals{TC_MASK} ) .
|
|
do_length( $length ) .
|
|
do_tos( $tos ) .
|
|
do_connbytes( $connbytes ) .
|
|
do_helper( $helper ) .
|
|
do_headers( $headers ) .
|
|
do_probability( $probability ) .
|
|
do_dscp( $dscp ) .
|
|
state_match( $state ) .
|
|
do_time( $time ) .
|
|
do_condition( $condition, $chainref->{name} ) .
|
|
( $ttl ? "-t $ttl " : '' ) .
|
|
$raw_matches ,
|
|
$source ,
|
|
$dest ,
|
|
'' ,
|
|
$target,
|
|
'' ,
|
|
$target ,
|
|
$exceptionrule ,
|
|
$usergenerated ,
|
|
'' , # Log Name
|
|
$device ,
|
|
$params
|
|
);
|
|
}
|
|
|
|
progress_message " Mangle Rule \"$currentline\" $done";
|
|
}
|
|
|
|
#
|
|
# Intermediate processing of a tcrules entry
|
|
#
|
|
sub process_tc_rule1( $$$$$$$$$$$$$$$$ ) {
|
|
my ( $originalmark, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state ) = @_;
|
|
|
|
my %tcs = ( T => { designator => ':T',
|
|
command => ''
|
|
} ,
|
|
CT => { designator => ':T',
|
|
command => 'CONNMARK'
|
|
} ,
|
|
C => { designator => '',
|
|
command => 'CONNMARK' ,
|
|
} ,
|
|
P => { designator => ':P',
|
|
command => ''
|
|
} ,
|
|
CP => { designator => ':P' ,
|
|
command => 'CONNMARK'
|
|
} ,
|
|
F => { designator => ':F',
|
|
command => ''
|
|
} ,
|
|
CF => { designator => ':F' ,
|
|
command => 'CONNMARK'
|
|
} ,
|
|
);
|
|
|
|
our %tccmd;
|
|
|
|
unless ( %tccmd ) {
|
|
%tccmd = ( ADD => { match => sub ( $ ) { $_[0] =~ /^ADD/ }
|
|
},
|
|
DEL => { match => sub ( $ ) { $_[0] =~ /^DEL/ }
|
|
},
|
|
SAVE => { match => sub ( $ ) { $_[0] eq 'SAVE' } ,
|
|
} ,
|
|
RESTORE => { match => sub ( $ ) { $_[0] eq 'RESTORE' },
|
|
} ,
|
|
CONTINUE => { match => sub ( $ ) { $_[0] eq 'CONTINUE' },
|
|
} ,
|
|
SAME => { match => sub ( $ ) { $_[0] =~ /^SAME(?:\(d+\))?$/ },
|
|
} ,
|
|
IPMARK => { match => sub ( $ ) { $_[0] =~ /^IPMARK/ },
|
|
} ,
|
|
TPROXY => { match => sub ( $ ) { $_[0] =~ /^TPROXY/ },
|
|
},
|
|
DIVERT => { match => sub( $ ) { $_[0] =~ /^DIVERT/ },
|
|
},
|
|
TTL => { match => sub( $ ) { $_[0] =~ /^TTL/ },
|
|
},
|
|
HL => { match => sub( $ ) { $_[0] =~ /^HL/ },
|
|
},
|
|
IMQ => { match => sub( $ ) { $_[0] =~ /^IMQ\(\d+\)$/ },
|
|
},
|
|
DSCP => { match => sub( $ ) { $_[0] =~ /^DSCP\(\w+\)$/ },
|
|
},
|
|
TOS => { match => sub( $ ) { $_[0] =~ /^TOS\(.+\)$/ },
|
|
},
|
|
CHECKSUM => { match => sub( $ ) { $_[0] eq 'CHECKSUM' },
|
|
},
|
|
INLINE => { match => sub( $ ) { $_[0] eq 'INLINE' },
|
|
},
|
|
DROP => { match => sub( $ ) { $_[0] eq 'DROP' },
|
|
},
|
|
);
|
|
}
|
|
|
|
fatal_error 'MARK must be specified' if $originalmark eq '-';
|
|
|
|
my ( $mark, $designator, $remainder ) = split( /:/, $originalmark, 3 );
|
|
|
|
fatal_error "Invalid MARK ($originalmark)" unless supplied $mark;
|
|
|
|
my $command = '';
|
|
|
|
if ( $remainder ) {
|
|
if ( $originalmark =~ /^\w+\(?.*\)$/ ) {
|
|
$mark = $originalmark; # Most likely, an IPv6 address is included in the parameter list
|
|
} else {
|
|
fatal_error "Invalid MARK ($originalmark)"
|
|
unless ( $mark =~ /^([0-9a-fA-F]+)$/ &&
|
|
$designator =~ /^([0-9a-fA-F]+)$/ &&
|
|
$designator{$remainder} );
|
|
$mark = join( ':', $mark, $designator );
|
|
$designator = $remainder;
|
|
$command = 'CLASSIFY';
|
|
}
|
|
}
|
|
|
|
my $tcsref;
|
|
|
|
if ( $designator ) {
|
|
my $tcsref = $tcs{$designator};
|
|
|
|
if ( $tcsref ) {
|
|
$designator = $tcsref->{designator};
|
|
if ( my $cmd = $tcsref->{command} ) {
|
|
$command |= $cmd;
|
|
}
|
|
} else {
|
|
unless ( $command ) {
|
|
fatal_error "Invalid MARK/CLASSIFY ($originalmark)" unless $mark =~ /^([0-9a-fA-F]+)$/ and $designator =~ /^([0-9a-fA-F]+)$/;
|
|
$mark = join( ':', $mark, $designator );
|
|
$command = 'CLASSIFY';
|
|
$designator = '';
|
|
}
|
|
}
|
|
} else {
|
|
$designator = '';
|
|
}
|
|
|
|
unless ( $command ) {
|
|
{
|
|
my ( $cmd, $rest ) = split( '/', $mark, 2 );
|
|
|
|
if ( $cmd =~ /^([A-Z]+)(?:\((.+)\))?/ ) {
|
|
if ( my $tccmd = $tccmd{$1} ) {
|
|
fatal_error "Invalid $1 ACTION ($originalmark)" unless $tccmd->{match}($cmd);
|
|
$command = $1;
|
|
if ( supplied $rest ) {
|
|
fatal_error "Invalid $1 ACTION ($originalmark)" if supplied $2;
|
|
$mark = $rest;
|
|
} elsif ( supplied $2 ) {
|
|
$mark = $2;
|
|
if ( supplied $mark && $command eq 'IPMARK' ) {
|
|
my @params = split ',', $mark;
|
|
$params[1] = '0xff' unless supplied $params[1];
|
|
$params[2] = '0x00' unless supplied $params[2];
|
|
$params[3] = '0' unless supplied $params[3];
|
|
$mark = join ',', @params;
|
|
}
|
|
} else {
|
|
$mark = '';
|
|
}
|
|
}
|
|
} else {
|
|
$command = 'MARK';
|
|
}
|
|
}
|
|
}
|
|
|
|
$command = ( $command ? supplied $mark ? "$command($mark)" : $command : $mark ) . $designator;
|
|
my $line = ( $family == F_IPV6 ?
|
|
"$command\t$source\t$dest\t$proto\t$ports\t$sports\t$user\t$testval\t$length\t$tos\t$connbytes\t$helper\t$headers\t$probability\t$dscp\t$state" :
|
|
"$command\t$source\t$dest\t$proto\t$ports\t$sports\t$user\t$testval\t$length\t$tos\t$connbytes\t$helper\t$probability\t$dscp\t$state" );
|
|
#
|
|
# Supress superfluous trailing dashes
|
|
#
|
|
$line =~ s/(?:\t-)+$//;
|
|
|
|
my $raw_matches = fetch_inline_matches;
|
|
|
|
if ( $raw_matches ne ' ' ) {
|
|
if ( $command =~ /^INLINE/ || $config{INLINE_MATCHES} ) {
|
|
$line .= join( '', ' ;', $raw_matches );
|
|
} else {
|
|
$line .= join( '', ' {', $raw_matches , ' }' );
|
|
}
|
|
}
|
|
|
|
print $mangle "$line\n";
|
|
}
|
|
|
|
sub process_tc_rule( ) {
|
|
my ( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state );
|
|
if ( $family == F_IPV4 ) {
|
|
( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $probability, $dscp, $state ) =
|
|
split_rawline2( 'tcrules file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
probability => 12 ,
|
|
scp => 13,
|
|
state => 14 },
|
|
{},
|
|
15,
|
|
1 );
|
|
$headers = '-';
|
|
} else {
|
|
( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability, $dscp, $state ) =
|
|
split_rawline2( 'tcrules file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
headers => 12,
|
|
probability => 13,
|
|
dscp => 14,
|
|
state => 15 },
|
|
{},
|
|
16,
|
|
1 );
|
|
}
|
|
|
|
for my $proto (split_list( $protos, 'Protocol' ) ) {
|
|
process_tc_rule1( $originalmark, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state );
|
|
}
|
|
}
|
|
|
|
sub process_mangle_rule( $ ) {
|
|
my ( $chainref ) = @_;
|
|
my ( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state, $time, $conditional );
|
|
if ( $family == F_IPV4 ) {
|
|
( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $probability, $dscp, $state, $time, $conditional ) =
|
|
split_line2( 'mangle file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
probability => 12 ,
|
|
scp => 13,
|
|
state => 14,
|
|
time => 15,
|
|
switch => 16,
|
|
},
|
|
{},
|
|
17,
|
|
1 );
|
|
$headers = '-';
|
|
} else {
|
|
( $originalmark, $source, $dest, $protos, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability, $dscp, $state, $time, $conditional ) =
|
|
split_line2( 'mangle file',
|
|
{ mark => 0,
|
|
action => 0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
dport => 4,
|
|
sport => 5,
|
|
user => 6,
|
|
test => 7,
|
|
length => 8,
|
|
tos => 9,
|
|
connbytes => 10,
|
|
helper => 11,
|
|
headers => 12,
|
|
probability => 13,
|
|
dscp => 14,
|
|
state => 15,
|
|
time => 16,
|
|
switch => 17,
|
|
},
|
|
{},
|
|
18,
|
|
1 );
|
|
}
|
|
|
|
for my $proto (split_list( $protos, 'Protocol' ) ) {
|
|
process_mangle_rule1( $chainref, $originalmark, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper, $headers, $probability , $dscp , $state, $time, $conditional );
|
|
}
|
|
}
|
|
|
|
sub process_snat_inline( $$$$$$$$$$$$$$ ) {
|
|
my ($inline, $chainref, $params, $loglevel, $source, $dest, $protos, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) = @_;
|
|
|
|
my ( $level,
|
|
$tag ) = split( ':', $loglevel, 2 );
|
|
my $oldparms = push_action_params( $inline,
|
|
$chainref,
|
|
$params,
|
|
supplied $level ? $level : 'none',
|
|
defined $tag ? $tag : '' ,
|
|
$chainref->{name} );
|
|
|
|
my $actionref = $actions{$inline};
|
|
my $inlinefile = $actionref->{file};
|
|
my $options = $actionref->{options};
|
|
my $nolog = $options & NOLOG_OPT;
|
|
my $matches = fetch_inline_matches;
|
|
|
|
progress_message "..Expanding inline action $inlinefile...";
|
|
|
|
push_open $inlinefile, 2, 1, undef , 2;
|
|
|
|
my $save_comment = push_comment;
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
my ( $maction, $msource, $mdest, $mprotos, $mports, $mipsec, $mmark, $muser, $mcondition, $morigdest, $mprobability) =
|
|
split_line2( 'snat file',
|
|
{ action =>0,
|
|
source => 1,
|
|
dest => 2,
|
|
proto => 3,
|
|
port => 4,
|
|
ipsec => 5,
|
|
mark => 6,
|
|
user => 7,
|
|
switch => 8,
|
|
origdest => 9,
|
|
probability => 10,
|
|
},
|
|
{},
|
|
11,
|
|
1 );
|
|
|
|
fatal_error 'ACTION must be specified' if $maction eq '-';
|
|
|
|
if ( $maction eq 'DEFAULTS' ) {
|
|
default_action_params( $chainref, split_list( $msource, 'defaults' ) );
|
|
next;
|
|
}
|
|
|
|
$maction = merge_levels( join(':', @actparams{'chain','loglevel','logtag'}), $maction ) unless $nolog;
|
|
|
|
$msource = $source if $msource eq '-';
|
|
|
|
if ( $mdest eq '-' ) {
|
|
$mdest = $dest;
|
|
} else {
|
|
$mdest = merge_inline_source_dest( $mdest, $dest );
|
|
}
|
|
|
|
$mprotos = $protos if $mprotos eq '-';
|
|
|
|
for my $proto (split_list( $mprotos, 'Protocol' ) ) {
|
|
process_snat1( $chainref,
|
|
$maction,
|
|
$msource,
|
|
$mdest,
|
|
$proto,
|
|
merge_macro_column( $mports, $ports ),
|
|
merge_macro_column( $mipsec, $ipsec ),
|
|
merge_macro_column( $mmark, $mark ),
|
|
merge_macro_column( $muser, $user ),
|
|
merge_macro_column( $mcondition, $condition ),
|
|
merge_macro_column( $morigdest , $origdest ),
|
|
merge_macro_column( $mprobability, $probability ),
|
|
);
|
|
}
|
|
|
|
progress_message " Rule \"$currentline\" $done";
|
|
|
|
set_inline_matches( $matches );
|
|
}
|
|
|
|
pop_comment( $save_comment );
|
|
|
|
pop_open;
|
|
|
|
progress_message "..End inline action $inlinefile";
|
|
|
|
pop_action_params( $oldparms );
|
|
}
|
|
|
|
#
|
|
# Process a record in the snat file
|
|
#
|
|
sub process_snat1( $$$$$$$$$$$$ ) {
|
|
my ( $chainref, $origaction, $source, $dest, $proto, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) = @_;
|
|
|
|
my $inchain;
|
|
my $inaction;
|
|
my $pre_nat;
|
|
my $add_snat_aliases = $family == F_IPV4 && $config{ADD_SNAT_ALIASES};
|
|
my $destnets = '';
|
|
my $baserule = '';
|
|
my $inlinematches = get_inline_matches(0);
|
|
my $prerule = '';
|
|
my $options = '';
|
|
my $addresses;
|
|
my $target;
|
|
my $params;
|
|
my $actiontype;
|
|
my $interfaces;
|
|
my $normalized_action;
|
|
my ( $action, $loglevel ) = split_action( $origaction );
|
|
my $logaction;
|
|
my $param;
|
|
|
|
if ( $action =~ /^MASQUERADE(\+)?(?:\((.+)\))?$/ ) {
|
|
$target = 'MASQUERADE';
|
|
$actiontype = $builtin_target{$action = $target};
|
|
$pre_nat = $1;
|
|
$addresses = ( $2 || '' );
|
|
$options = 'random' if $addresses =~ s/:?random$//;
|
|
$add_snat_aliases = '';
|
|
$logaction = 'MASQ';
|
|
} elsif ( $action =~ /^SNAT(\+)?\((.+)\)$/ ) {
|
|
$pre_nat = $1;
|
|
$addresses = $2;
|
|
$target = 'SNAT';
|
|
$actiontype = $builtin_target{$action = $target};
|
|
$options .= ':persistent' if $addresses =~ s/:persistent//;
|
|
$options .= ':random' if $addresses =~ s/:random//;
|
|
$options =~ s/^://;
|
|
$logaction = 'SNAT';
|
|
} elsif ( $action =~ /^CONTINUE(\+)?$/ ) {
|
|
$add_snat_aliases = 0;
|
|
$actiontype = $builtin_target{$target = 'RETURN'};
|
|
$pre_nat = $1;
|
|
$logaction = 'RETURN';
|
|
} elsif ( $action eq 'MASQUERADE' ) {
|
|
$actiontype = $builtin_target{$target = 'MASQUERADE'};
|
|
$add_snat_aliases = '';
|
|
$logaction = 'MASQ';
|
|
} else {
|
|
( $target , $params ) = get_target_param1( $action );
|
|
|
|
$pre_nat = ( $target =~ s/\+$// );
|
|
|
|
$actiontype = ( $targets{$target} || 0 );
|
|
|
|
if ( $actiontype & LOGRULE ) {
|
|
$logaction = 'LOG';
|
|
if ( $target eq 'LOG' ) {
|
|
fatal_error 'LOG requires a log level' unless supplied $loglevel;
|
|
} else {
|
|
$target = "$target($params)";
|
|
validate_level( $action );
|
|
$loglevel = supplied $loglevel ? join( ':', $target, $loglevel ) : $target;
|
|
$target = 'LOG';
|
|
}
|
|
} else {
|
|
fatal_error "Invalid ACTION ($action)" unless $actiontype & ( ACTION | INLINE );
|
|
$logaction = '';
|
|
}
|
|
}
|
|
|
|
if ( $inchain = defined $chainref ) {
|
|
( $inaction, undef,undef,undef,$param ) = split( /:/, $normalized_action = $chainref->{action}) if $chainref->{action};
|
|
fatal_error q('+' is not allowed within an action body) if $pre_nat;
|
|
}
|
|
#
|
|
# Next, parse the DEST column
|
|
#
|
|
if ( $inaction ) {
|
|
$destnets = $dest;
|
|
assert( $param =~ /^(.*)\|/ );
|
|
$interfaces=$1;
|
|
} elsif ( $family == F_IPV4 ) {
|
|
if ( $dest =~ /^([^:]+)::([^:]*)$/ ) {
|
|
$add_snat_aliases = 0;
|
|
$destnets = $2;
|
|
$interfaces = $1;
|
|
} elsif ( $dest =~ /^([^:]+:[^:]+):([^:]+)$/ ) {
|
|
$destnets = $2;
|
|
$interfaces = $1;
|
|
} elsif ( $dest =~ /^([^:]+):$/ ) {
|
|
$add_snat_aliases = 0;
|
|
$interfaces = $1;
|
|
} elsif ( $dest =~ /^([^:]+):([^:]*)$/ ) {
|
|
my ( $one, $two ) = ( $1, $2 );
|
|
if ( $2 =~ /\./ || $2 =~ /^[+%!]/ ) {
|
|
$interfaces = $one;
|
|
$destnets = $two;
|
|
} else {
|
|
$interfaces = $dest;
|
|
}
|
|
} else {
|
|
$interfaces = $dest;
|
|
}
|
|
} elsif ( $dest =~ /^(.+?):(.+)$/ ) {
|
|
$interfaces = $1;
|
|
$destnets = $2;
|
|
} else {
|
|
$interfaces = $dest;
|
|
}
|
|
#
|
|
# Handle Protocol, Ports and Condition
|
|
#
|
|
$baserule .= do_proto( $proto, $ports, '' );
|
|
#
|
|
# Handle Mark
|
|
#
|
|
$baserule .= do_test( $mark, $globals{TC_MASK} ) if $mark ne '-';
|
|
$baserule .= do_user( $user ) if $user ne '-';
|
|
$baserule .= do_probability( $probability ) if $probability ne '-';
|
|
|
|
my @interfaces = split_list( $interfaces, 'interface' );
|
|
|
|
for my $fullinterface ( @interfaces ) {
|
|
|
|
my $rule = '';
|
|
my $saveaddresses = $addresses;
|
|
my $savetarget = $target;
|
|
my $savebaserule = $baserule;
|
|
my $interface = $fullinterface;
|
|
|
|
if ( $inaction ) {
|
|
$interface =~ s/:.*// if $family == F_IPV4; #interface name may include 'alias'
|
|
} else {
|
|
if ( $interface eq firewall_zone ) {
|
|
if ( @interfaces == 1 ) {
|
|
fatal_error q('+' not valid when the DEST is $FW) if $pre_nat;
|
|
fatal_error q('MASQUERADE' not allowed when DEST is $FW) if $action eq 'MASQUERADE';
|
|
require_capability 'NAT_INPUT_CHAIN', '$FW in the DEST column', 's';
|
|
$interface = '';
|
|
} else {
|
|
fatal_error q($FW may not appear in a list of interfaces);
|
|
}
|
|
} else {
|
|
$interface =~ s/:.*// if $family == F_IPV4; #interface name may include 'alias'
|
|
|
|
if ( $interface =~ /(.*)[(](\w*)[)]$/ ) {
|
|
$interface = $1;
|
|
my $provider = $2;
|
|
|
|
fatal_error "Missing Provider ($dest)" unless supplied $provider;
|
|
|
|
$dest =~ s/[(]\w*[)]//;
|
|
my $realm = provider_realm( $provider );
|
|
|
|
fatal_error "$provider is not a shared-interface provider" unless $realm;
|
|
|
|
$rule .= "-m realm --realm $realm ";
|
|
}
|
|
|
|
fatal_error "Unknown interface ($interface)" unless my $interfaceref = known_interface( $interface );
|
|
|
|
if ( $interfaceref->{root} ) {
|
|
$interface = $interfaceref->{name} if $interface eq $interfaceref->{physical};
|
|
} else {
|
|
$rule .= match_dest_dev( $interface );
|
|
$interface = $interfaceref->{name};
|
|
}
|
|
}
|
|
|
|
$chainref = $interface ? ensure_chain('nat', $pre_nat ? snat_chain $interface : masq_chain $interface) : $nat_table->{INPUT};
|
|
}
|
|
|
|
$baserule .= do_condition( $condition , $chainref->{name} );
|
|
#
|
|
# Handle IPSEC options, if any
|
|
#
|
|
if ( $ipsec ne '-' ) {
|
|
fatal_error "Non-empty IPSEC column requires policy match support in your kernel and iptables" unless have_capability( 'POLICY_MATCH' );
|
|
|
|
my $dir = $interface ? 'out' : 'in';
|
|
|
|
if ( $ipsec =~ /^yes$/i ) {
|
|
$baserule .= do_ipsec_options $dir, 'ipsec', '';
|
|
} elsif ( $ipsec =~ /^no$/i ) {
|
|
$baserule .= do_ipsec_options $dir, 'none', '';
|
|
} else {
|
|
$baserule .= do_ipsec_options $dir, 'ipsec', $ipsec;
|
|
}
|
|
} elsif ( have_ipsec ) {
|
|
if ( $interface ) {
|
|
$baserule .= '-m policy --pol none --dir out ';
|
|
} else {
|
|
$baserule .= '-m policy --pol none --dir in ';
|
|
}
|
|
}
|
|
|
|
my $detectaddress = 0;
|
|
my $exceptionrule = '';
|
|
my $conditional = 0;
|
|
|
|
if ( $action eq 'SNAT' ) {
|
|
if ( $addresses eq 'detect' ) {
|
|
fatal_error q('detect' not allowed when the destination is $FW) unless $interface;
|
|
my $variable = get_interface_address $interface;
|
|
$target .= " --to-source $variable";
|
|
|
|
if ( interface_is_optional $interface ) {
|
|
add_commands( $chainref,
|
|
'',
|
|
"if [ \"$variable\" != 0.0.0.0 ]; then" );
|
|
incr_cmd_level( $chainref );
|
|
$detectaddress = 1;
|
|
}
|
|
} else {
|
|
fatal_error "SNAT rules must spacify a new source address and/or new source ports" unless supplied $addresses;
|
|
|
|
my $addrlist = '';
|
|
my @addrs = split_list $addresses, 'address';
|
|
|
|
fatal_error "Only one SNAT address may be specified" if @addrs > 1;
|
|
|
|
for my $addr ( @addrs ) {
|
|
if ( $addr =~ /^([&%])(.+)$/ ) {
|
|
my ( $type, $interface ) = ( $1, $2 );
|
|
|
|
my $ports = '';
|
|
|
|
if ( $interface =~ s/:(.+)$// ) {
|
|
validate_portpair1( $proto, $1 );
|
|
$ports = ":$1";
|
|
}
|
|
#
|
|
# Address Variable
|
|
#
|
|
if ( $interface =~ /^{([a-zA-Z_]\w*)}$/ ) {
|
|
#
|
|
# User-defined address variable
|
|
#
|
|
$conditional = conditional_rule( $chainref, $addr );
|
|
$addrlist .= ' --to-source ' . "\$${1}${ports} ";
|
|
} else {
|
|
if ( $conditional = conditional_rule( $chainref, $addr ) ) {
|
|
#
|
|
# Optional Interface -- rule is conditional
|
|
#
|
|
$addr = get_interface_address $interface;
|
|
} else {
|
|
#
|
|
# Interface is not optional
|
|
#
|
|
$addr = record_runtime_address( $type, $interface );
|
|
}
|
|
|
|
if ( $ports ) {
|
|
$addr =~ s/ $//;
|
|
$addr = $family == F_IPV4 ? "${addr}${ports} " : "[$addr]$ports ";
|
|
}
|
|
|
|
$addrlist .= ' --to-source ' . $addr;
|
|
}
|
|
} elsif ( $family == F_IPV4 ) {
|
|
if ( $addr =~ /^.*\..*\..*\./ ) {
|
|
my ($ipaddr, $rest) = split ':', $addr, 2;
|
|
if ( $ipaddr =~ /^(.+)-(.+)$/ ) {
|
|
validate_range( $1, $2 );
|
|
} else {
|
|
validate_address $ipaddr, 0;
|
|
}
|
|
|
|
if ( supplied $rest ) {
|
|
validate_portpair1( $proto, $rest );
|
|
$addrlist .= " --to-source $addr";
|
|
} else {
|
|
$addrlist .= " --to-source $ipaddr";
|
|
}
|
|
|
|
$exceptionrule = do_proto( $proto, '', '' ) if $addr =~ /:/;
|
|
} else {
|
|
my $ports = $addr;
|
|
$ports =~ s/^://;
|
|
fatal_error "Missing Address or Port[-range] ($addr)" unless supplied $ports && $ports ne '-';
|
|
validate_portpair1( $proto, $ports );
|
|
$addrlist .= " --to-source :$ports";
|
|
$exceptionrule = do_proto( $proto, '', '' );
|
|
}
|
|
} else {
|
|
if ( $addr =~ /^\[/ ) {
|
|
#
|
|
# Can have ports specified
|
|
#
|
|
my $ports;
|
|
|
|
if ( $addr =~ s/:([^]:]+)$// ) {
|
|
$ports = $1;
|
|
}
|
|
|
|
fatal_error "Invalid IPv6 Address ($addr)" unless $addr =~ /^\[(.+)\]$/;
|
|
|
|
$addr = $1;
|
|
|
|
if ( $addr =~ /^(.+)-(.+)$/ ) {
|
|
fatal_error "Correct address range syntax is '[<addr1>-<addr2>]'" if $addr =~ /]-\[/;
|
|
validate_range( $1, $2 );
|
|
} else {
|
|
validate_address $addr, 0;
|
|
}
|
|
|
|
if ( supplied $ports ) {
|
|
validate_portpair1( $proto, $ports );
|
|
$exceptionrule = do_proto( $proto, '', '' );
|
|
$addr = "[$addr]:$ports";
|
|
}
|
|
|
|
$addrlist .= " --to-source $addr";
|
|
} else {
|
|
if ( $addr =~ /^(.+)-(.+)$/ ) {
|
|
validate_range( $1, $2 );
|
|
} else {
|
|
validate_address $addr, 0;
|
|
}
|
|
|
|
$addrlist .= " --to-source $addr";
|
|
}
|
|
}
|
|
}
|
|
|
|
$target .= $addrlist;
|
|
}
|
|
} elsif ( $action eq 'MASQUERADE' ) {
|
|
fatal_error q('MASQUERADE' not allowed when the destination is $FW') unless $interface;
|
|
if ( supplied $addresses ) {
|
|
validate_portpair1($proto, $addresses );
|
|
$target .= " --to-ports $addresses";
|
|
$exceptionrule = do_proto( $proto, '', '' );
|
|
}
|
|
}
|
|
#
|
|
# And Generate the Rule(s)
|
|
#
|
|
if ( $actiontype & INLINE ) {
|
|
fatal_error( qq(Action $target may not be used in the snat file) ) unless $actiontype & NAT_TABLE;
|
|
|
|
process_snat_inline( $target,
|
|
$chainref,
|
|
$params,
|
|
$loglevel,
|
|
$source,
|
|
supplied( $destnets ) && $destnets ne '-' ? $inaction || $interface ? join( ':', $interface, $destnets ) : $destnets : $inaction ? '-' : $interface,
|
|
$proto,
|
|
$ports,
|
|
$ipsec,
|
|
$mark,
|
|
$user,
|
|
$condition,
|
|
$origdest,
|
|
$probability );
|
|
} else {
|
|
if ( $actiontype & ACTION ) {
|
|
fatal_error( qq(Action $target may not be used in the snat file) ) unless $actiontype & NAT_TABLE;
|
|
#
|
|
# Verify action 'proto', if any
|
|
#
|
|
$proto = determine_action_protocol( $target, $proto );
|
|
#
|
|
# Create the action:level:tag:param tuple. Since we don't allow logging out of nat POSTROUTING, we store
|
|
# the interface name in the log tag
|
|
#
|
|
my $normalized_target = normalize_action( $target, "$loglevel", "$interface|$params" );
|
|
fatal_error( "Action $target invoked Recursively (" . join( '->', map( external_name( $_ ), @actionstack , $normalized_target ) ) . ')' ) if $active{$target};
|
|
|
|
my $ref = use_action( 'nat', $normalized_target );
|
|
|
|
if ( $ref ) {
|
|
#
|
|
# First reference to this tuple - process_action may modify both $normalized_target and $ref!!!
|
|
#
|
|
process_action( $normalized_target, $ref, $chainref->{name} );
|
|
} else {
|
|
#
|
|
# We've seen this tuple before
|
|
#
|
|
$ref = $usedactions{$normalized_target};
|
|
}
|
|
|
|
$target = $ref->{name};
|
|
|
|
if ( $actions{$target}{options} & LOGJUMP_OPT ) {
|
|
$logaction = $target;
|
|
} else {
|
|
$loglevel = '';
|
|
}
|
|
} else {
|
|
for my $option ( split_list2( $options , 'option' ) ) {
|
|
if ( $option eq 'random' ) {
|
|
$target .= ' --random';
|
|
require_capability( 'MASQUERADE_TGT', "$action rules", '') if $family == F_IPV6;
|
|
} elsif ( $option eq 'persistent' ) {
|
|
fatal_error( "':persistent' is not allowed in a MASQUERADE rule" ) if $action eq 'MASQUERADE';
|
|
require_capability 'PERSISTENT_SNAT', ':persistent', 's';
|
|
$target .= ' --persistent';
|
|
} else {
|
|
fatal_error "Invalid $action option ($option)";
|
|
}
|
|
}
|
|
}
|
|
#
|
|
# If there is no source or destination then allow all addresses
|
|
#
|
|
$source = ALLIP if $source eq '-';
|
|
$destnets = ALLIP unless supplied $destnets && $destnets ne '-';
|
|
|
|
expand_rule( $chainref ,
|
|
$interface ? POSTROUTE_RESTRICT : INPUT_RESTRICT ,
|
|
$prerule ,
|
|
$baserule . $inlinematches . $rule ,
|
|
$source ,
|
|
$destnets ,
|
|
$origdest ,
|
|
$target ,
|
|
$loglevel ,
|
|
$logaction ,
|
|
$exceptionrule ,
|
|
'' )
|
|
unless unreachable_warning( 0, $chainref );
|
|
|
|
conditional_rule_end( $chainref ) if $detectaddress || $conditional;
|
|
|
|
if ( $interface && $add_snat_aliases && $addresses ) {
|
|
my ( $interface, $alias , $remainder ) = split( /:/, $fullinterface, 3 );
|
|
fatal_error "Invalid alias ($alias:$remainder)" if defined $remainder;
|
|
for my $address ( split_list $addresses, 'address' ) {
|
|
my ( $addrs, $port ) = split /:/, $address;
|
|
next unless $addrs;
|
|
next if $addrs eq 'detect';
|
|
for my $addr ( ip_range_explicit $addrs ) {
|
|
unless ( $addresses_to_add{$addr} ) {
|
|
$addresses_to_add{$addr} = 1;
|
|
if ( defined $alias ) {
|
|
push @addresses_to_add, $addr, "$interface:$alias";
|
|
$alias++;
|
|
} else {
|
|
push @addresses_to_add, $addr, $interface;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$addresses = $saveaddresses;
|
|
$target = $savetarget;
|
|
$baserule = $savebaserule;
|
|
}
|
|
|
|
progress_message " Snat record \"$currentline\" $done"
|
|
|
|
}
|
|
|
|
sub process_snat( )
|
|
{
|
|
my ($action, $source, $dest, $protos, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) =
|
|
split_line2( 'snat file',
|
|
{ action => 0, source => 1, dest => 2, proto => 3, port => 4, ipsec => 5, mark => 6, user => 7, switch => 8, origdest => 9, probability => 10 },
|
|
{}, #Nopad
|
|
undef, #Columns
|
|
1 ); #Allow inline matches
|
|
|
|
fatal_error 'ACTION must be specified' if $action eq '-';
|
|
fatal_error 'DEST must be specified' if $dest eq '-';
|
|
|
|
for my $proto ( split_list $protos, 'Protocol' ) {
|
|
process_snat1( undef, $action, $source, $dest, $proto, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability );
|
|
}
|
|
}
|
|
|
|
#
|
|
# Process the masq or snat file
|
|
#
|
|
sub setup_snat( $ ) # Convert masq->snat if true
|
|
{
|
|
my $fn;
|
|
my $have_masq;
|
|
|
|
if ( $_[0] ) {
|
|
convert_masq();
|
|
} elsif ( $fn = open_file( 'masq', 1, 1 ) ) {
|
|
first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'NAT_ENABLED' , "a non-empty masq file" , 's'; } );
|
|
process_one_masq(0), $have_masq = 1 while read_a_line( NORMAL_READ );
|
|
}
|
|
|
|
unless ( $have_masq ) {
|
|
#
|
|
# Masq file empty or didn't exist
|
|
#
|
|
if ( $fn = open_file( 'snat', 1, 1 ) ) {
|
|
first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'NAT_ENABLED' , "a non-empty snat file" , 's'; } );
|
|
process_snat while read_a_line( NORMAL_READ );
|
|
}
|
|
}
|
|
}
|
|
|
|
1;
|