mirror of
https://gitlab.com/shorewall/code.git
synced 2024-12-02 12:33:17 +01:00
ddefde2d10
Signed-off-by: Tom Eastep <teastep@shorewall.net>
1129 lines
30 KiB
Perl
1129 lines
30 KiB
Perl
#
|
|
# Shorewall 5.0 -- /usr/share/shorewall/Shorewall/Nat.pm
|
|
#
|
|
# This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
|
|
#
|
|
# (c) 2007-2016 - 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 contains code for dealing with the /etc/shorewall/masq,
|
|
# /etc/shorewall/nat and /etc/shorewall/netmap files.
|
|
#
|
|
package Shorewall::Nat;
|
|
require Exporter;
|
|
use Shorewall::Config qw(:DEFAULT :internal);
|
|
use Shorewall::IPAddrs;
|
|
use Shorewall::Zones;
|
|
use Shorewall::Chains qw(:DEFAULT :internal);
|
|
use Shorewall::Providers qw( provider_realm );
|
|
|
|
use strict;
|
|
|
|
our @ISA = qw(Exporter);
|
|
our @EXPORT = qw( setup_nat setup_netmap add_addresses );
|
|
our %EXPORT_TAGS = ( rules => [ qw ( handle_nat_rule handle_nonat_rule process_one_masq convert_masq @addresses_to_add %addresses_to_add ) ] );
|
|
our @EXPORT_OK = ();
|
|
|
|
Exporter::export_ok_tags('rules');
|
|
|
|
our $VERSION = 'MODULEVERSION';
|
|
|
|
our @addresses_to_add;
|
|
our %addresses_to_add;
|
|
our $family;
|
|
|
|
#
|
|
# Called by the compiler
|
|
#
|
|
sub initialize($) {
|
|
$family = shift;
|
|
@addresses_to_add = ();
|
|
%addresses_to_add = ();
|
|
}
|
|
|
|
#
|
|
# Process a single rule from the the masq file
|
|
#
|
|
sub process_one_masq1( $$$$$$$$$$$ )
|
|
{
|
|
my ( $interfacelist, $networks, $addresses, $proto, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) = @_;
|
|
|
|
my $pre_nat;
|
|
my $add_snat_aliases = $family == F_IPV4 && $config{ADD_SNAT_ALIASES};
|
|
my $destnets = '';
|
|
my $baserule = '';
|
|
my $inlinematches = '';
|
|
my $prerule = '';
|
|
my $savelist;
|
|
#
|
|
# Leading '+'
|
|
#
|
|
$pre_nat = 1 if $interfacelist =~ s/^\+//;
|
|
|
|
#
|
|
# Check for INLINE
|
|
#
|
|
if ( $interfacelist =~ /^INLINE\((.+)\)$/ ) {
|
|
$interfacelist = $1;
|
|
$inlinematches = get_inline_matches(0);
|
|
} else {
|
|
$inlinematches = get_inline_matches(0);
|
|
}
|
|
|
|
$savelist = $interfacelist;
|
|
#
|
|
# Handle early matches
|
|
#
|
|
if ( $inlinematches =~ s/s*\+// ) {
|
|
$prerule = $inlinematches;
|
|
$inlinematches = '';
|
|
}
|
|
#
|
|
# Parse the remaining part of the INTERFACE column
|
|
#
|
|
if ( $family == F_IPV4 ) {
|
|
if ( $interfacelist =~ /^([^:]+)::([^:]*)$/ ) {
|
|
$add_snat_aliases = 0;
|
|
$destnets = $2;
|
|
$interfacelist = $1;
|
|
} elsif ( $interfacelist =~ /^([^:]+:[^:]+):([^:]+)$/ ) {
|
|
$destnets = $2;
|
|
$interfacelist = $1;
|
|
} elsif ( $interfacelist =~ /^([^:]+):$/ ) {
|
|
$add_snat_aliases = 0;
|
|
$interfacelist = $1;
|
|
} elsif ( $interfacelist =~ /^([^:]+):([^:]*)$/ ) {
|
|
my ( $one, $two ) = ( $1, $2 );
|
|
if ( $2 =~ /\./ || $2 =~ /^%/ ) {
|
|
$interfacelist = $one;
|
|
$destnets = $two;
|
|
}
|
|
}
|
|
} elsif ( $interfacelist =~ /^(.+?):(.+)$/ ) {
|
|
$interfacelist = $1;
|
|
$destnets = $2;
|
|
}
|
|
#
|
|
# If there is no source or destination then allow all addresses
|
|
#
|
|
$networks = ALLIP if $networks eq '-';
|
|
$destnets = ALLIP if $destnets eq '-';
|
|
|
|
#
|
|
# 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' );
|
|
|
|
if ( $ipsec =~ /^yes$/i ) {
|
|
$baserule .= do_ipsec_options 'out', 'ipsec', '';
|
|
} elsif ( $ipsec =~ /^no$/i ) {
|
|
$baserule .= do_ipsec_options 'out', 'none', '';
|
|
} else {
|
|
$baserule .= do_ipsec_options 'out', 'ipsec', $ipsec;
|
|
}
|
|
} elsif ( have_ipsec ) {
|
|
$baserule .= '-m policy --pol none --dir out ';
|
|
}
|
|
|
|
#
|
|
# 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 $target;
|
|
|
|
for my $fullinterface (split_list $interfacelist, 'interface' ) {
|
|
my $rule = '';
|
|
|
|
$target = 'MASQUERADE ';
|
|
#
|
|
# Isolate and verify the interface part
|
|
#
|
|
( my $interface = $fullinterface ) =~ s/:.*//;
|
|
|
|
if ( $interface =~ /(.*)[(](\w*)[)]$/ ) {
|
|
$interface = $1;
|
|
my $provider = $2;
|
|
|
|
fatal_error "Missing Provider ($fullinterface)" unless supplied $provider;
|
|
|
|
$fullinterface =~ 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};
|
|
}
|
|
|
|
my $chainref = ensure_chain('nat', $pre_nat ? snat_chain $interface : masq_chain $interface);
|
|
|
|
$baserule .= do_condition( $condition , $chainref->{name} );
|
|
|
|
my $detectaddress = 0;
|
|
my $exceptionrule = '';
|
|
my $randomize = '';
|
|
my $persistent = '';
|
|
my $conditional = 0;
|
|
#
|
|
# Parse the ADDRESSES column
|
|
#
|
|
if ( $addresses ne '-' ) {
|
|
my $saveaddresses = $addresses;
|
|
if ( $addresses eq 'random' ) {
|
|
require_capability( 'MASQUERADE_TGT', 'Masquerade rules', '') if $family == F_IPV6;
|
|
$randomize = '--random ';
|
|
} else {
|
|
$addresses =~ s/:persistent$// and $persistent = ' --persistent ';
|
|
$addresses =~ s/:random$// and $randomize = ' --random ';
|
|
|
|
require_capability 'PERSISTENT_SNAT', ':persistent', 's' if $persistent;
|
|
|
|
if ( $addresses =~ /^SAME/ ) {
|
|
fatal_error "The SAME target is no longer supported";
|
|
} elsif ( $addresses eq 'detect' ) {
|
|
my $variable = get_interface_address $interface;
|
|
$target = "SNAT --to-source $variable";
|
|
|
|
if ( interface_is_optional $interface ) {
|
|
add_commands( $chainref,
|
|
'',
|
|
"if [ \"$variable\" != 0.0.0.0 ]; then" );
|
|
incr_cmd_level( $chainref );
|
|
$detectaddress = 1;
|
|
}
|
|
} elsif ( $addresses eq 'NONAT' ) {
|
|
fatal_error "'persistent' may not be specified with 'NONAT'" if $persistent;
|
|
fatal_error "'random' may not be specified with 'NONAT'" if $randomize;
|
|
$target = 'RETURN';
|
|
$add_snat_aliases = 0;
|
|
} elsif ( $addresses ) {
|
|
my $addrlist = '';
|
|
my @addrs = split_list $addresses, 'address';
|
|
|
|
fatal_error "Only one 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
|
|
#
|
|
$target = 'SNAT ';
|
|
|
|
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 =~ /^.*\..*\..*\./ ) {
|
|
$target = 'SNAT ';
|
|
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/^://;
|
|
validate_portpair1( $proto, $ports );
|
|
$addrlist .= "--to-ports $ports ";
|
|
$exceptionrule = do_proto( $proto, '', '' );
|
|
}
|
|
} else {
|
|
$target = 'SNAT ';
|
|
|
|
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;
|
|
} else {
|
|
fatal_error( "':persistent' is not allowed in a MASQUERADE rule" ) if $persistent;
|
|
require_capability( 'MASQUERADE_TGT', 'Masquerade rules', '' ) if $family == F_IPV6;
|
|
}
|
|
}
|
|
|
|
$target .= $randomize;
|
|
$target .= $persistent;
|
|
$addresses = $saveaddresses;
|
|
} else {
|
|
require_capability( 'MASQUERADE_TGT', 'Masquerade rules', '' ) if $family == F_IPV6;
|
|
$add_snat_aliases = 0;
|
|
}
|
|
#
|
|
# And Generate the Rule(s)
|
|
#
|
|
expand_rule( $chainref ,
|
|
POSTROUTE_RESTRICT ,
|
|
$prerule ,
|
|
$baserule . $inlinematches . $rule ,
|
|
$networks ,
|
|
$destnets ,
|
|
$origdest ,
|
|
$target ,
|
|
'' ,
|
|
'' ,
|
|
$exceptionrule ,
|
|
'' )
|
|
unless unreachable_warning( 0, $chainref );
|
|
|
|
conditional_rule_end( $chainref ) if $detectaddress || $conditional;
|
|
|
|
if ( $add_snat_aliases ) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
progress_message " Masq record \"$currentline\" $done";
|
|
|
|
}
|
|
|
|
sub convert_one_masq1( $$$$$$$$$$$$ )
|
|
{
|
|
my ( $snat, $interfacelist, $networks, $addresses, $proto, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) = @_;
|
|
|
|
my $pre_nat;
|
|
my $destnets = '';
|
|
my $savelist;
|
|
#
|
|
# Leading '+'
|
|
#
|
|
$pre_nat = ( $interfacelist =~ s/^\+// );
|
|
#
|
|
# Check for INLINE
|
|
#
|
|
if ( $interfacelist =~ /^INLINE\((.+)\)$/ ) {
|
|
$interfacelist = $1;
|
|
}
|
|
|
|
$savelist = $interfacelist;
|
|
#
|
|
# Parse the remaining part of the INTERFACE column
|
|
#
|
|
if ( $family == F_IPV4 ) {
|
|
if ( $interfacelist =~ /^([^:]+)::([^:]*)$/ ) {
|
|
$destnets = $2;
|
|
$interfacelist = $1;
|
|
} elsif ( $interfacelist =~ /^([^:]+:[^:]+):([^:]+)$/ ) {
|
|
$destnets = $2;
|
|
$interfacelist = $1;
|
|
} elsif ( $interfacelist =~ /^([^:]+):$/ ) {
|
|
$interfacelist = $1;
|
|
} elsif ( $interfacelist =~ /^([^:]+):([^:]*)$/ ) {
|
|
my ( $one, $two ) = ( $1, $2 );
|
|
if ( $2 =~ /\./ || $2 =~ /^%/ ) {
|
|
$interfacelist = $one;
|
|
$destnets = $two;
|
|
}
|
|
}
|
|
} elsif ( $interfacelist =~ /^(.+?):(.+)$/ ) {
|
|
$interfacelist = $1;
|
|
$destnets = $2;
|
|
}
|
|
#
|
|
# If there is no source or destination then allow all addresses
|
|
#
|
|
$networks = ALLIP if $networks eq '-';
|
|
$destnets = ALLIP if $destnets eq '-';
|
|
|
|
my $target;
|
|
#
|
|
# Parse the ADDRESSES column
|
|
#
|
|
if ( $addresses ne '-' ) {
|
|
my $saveaddresses = $addresses;
|
|
if ( $addresses ne 'random' ) {
|
|
$addresses =~ s/:persistent$//;
|
|
$addresses =~ s/:random$//;
|
|
|
|
if ( $addresses eq 'detect' ) {
|
|
$target = 'SNAT';
|
|
} elsif ( $addresses eq 'NONAT' ) {
|
|
$target = 'CONTINUE';
|
|
} elsif ( $addresses ) {
|
|
if ( $addresses =~ /^:/ ) {
|
|
$target = 'MASQUERADE';
|
|
} else {
|
|
$target = 'SNAT';
|
|
}
|
|
}
|
|
}
|
|
|
|
$addresses = $saveaddresses;
|
|
} else {
|
|
$target = 'MASQUERADE';
|
|
}
|
|
|
|
if ( $snat ) {
|
|
$target .= '+' if $pre_nat;
|
|
|
|
if ( $addresses ne '-' && $addresses ne 'NONAT' ) {
|
|
$addresses =~ s/^://;
|
|
$target .= '(' . $addresses . ')';
|
|
}
|
|
|
|
my $line = "$target\t$networks\t$savelist\t$proto\t$ports\t$ipsec\t$mark\t$user\t$condition\t$origdest\t$probability";
|
|
#
|
|
# Supress superfluous trailing dashes
|
|
#
|
|
$line =~ s/(?:\t-)+$//;
|
|
|
|
my $raw_matches = fetch_inline_matches;
|
|
|
|
$line .= join( '', ' ;;', $raw_matches ) if $raw_matches ne ' ';
|
|
|
|
print $snat "$line\n";
|
|
}
|
|
|
|
progress_message " Masq record \"$rawcurrentline\" Converted";
|
|
|
|
}
|
|
|
|
sub process_one_masq( $ )
|
|
{
|
|
my ( $snat ) = @_;
|
|
|
|
if ( $snat ) {
|
|
unless ( $rawcurrentline =~ /^\s*(?:#.*)?$/ ) {
|
|
#
|
|
# Line was not blank or all comment
|
|
#
|
|
my ($interfacelist, $networks, $addresses, $protos, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) =
|
|
split_rawline2( 'masq file',
|
|
{ interface => 0, source => 1, address => 2, proto => 3, port => 4, ipsec => 5, mark => 6, user => 7, switch => 8, origdest => 9, probability => 10 },
|
|
{}, #Nopad
|
|
undef, #Columns
|
|
1 ); #Allow inline matches
|
|
|
|
if ( $interfacelist ne '-' ) {
|
|
for my $proto ( split_list $protos, 'Protocol' ) {
|
|
convert_one_masq1( $snat, $interfacelist, $networks, $addresses, $proto, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability );
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
my ($interfacelist, $networks, $addresses, $protos, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability ) =
|
|
split_line2( 'masq file',
|
|
{ interface => 0, source => 1, address => 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 'INTERFACE must be specified' if $interfacelist eq '-';
|
|
|
|
for my $proto ( split_list $protos, 'Protocol' ) {
|
|
process_one_masq1( $interfacelist, $networks, $addresses, $proto, $ports, $ipsec, $mark, $user, $condition, $origdest, $probability );
|
|
}
|
|
}
|
|
}
|
|
|
|
sub open_snat_for_output( $ ) {
|
|
my ($fn ) = @_;
|
|
my ( $snat, $fn1 );
|
|
|
|
if ( -f ( $fn1 = find_writable_file( 'snat' ) ) ) {
|
|
open( $snat , '>>', $fn1 ) || fatal_error "Unable to open $fn1:$!";
|
|
} else {
|
|
open( $snat , '>', $fn1 ) || fatal_error "Unable to open $fn1:$!";
|
|
#
|
|
# Transfer permissions from the existing masq file to the new snat file
|
|
#
|
|
transfer_permissions( $fn, $fn1 );
|
|
|
|
if ( $family == F_IPV4 ) {
|
|
print $snat <<'EOF';
|
|
#
|
|
# Shorewall - SNAT/Masquerade File
|
|
#
|
|
# For information about entries in this file, type "man shorewall-snat"
|
|
#
|
|
# See http://shorewall.net/manpages/shorewall-snat.html for additional information
|
|
EOF
|
|
} else {
|
|
print $snat <<'EOF';
|
|
#
|
|
# Shorewall6 - SNAT/Masquerade File
|
|
#
|
|
# For information about entries in this file, type "man shorewall6-snat"
|
|
#
|
|
# See http://shorewall.net/manpages6/shorewall6-snat.html for additional information
|
|
EOF
|
|
}
|
|
|
|
print $snat <<'EOF';
|
|
###################################################################################################################
|
|
#ACTION SOURCE DEST PROTO PORT IPSEC MARK USER SWITCH ORIGDEST PROBABILITY
|
|
EOF
|
|
}
|
|
|
|
return ( $snat, $fn1 );
|
|
}
|
|
|
|
#
|
|
# Convert a masq file into the equivalent snat file
|
|
#
|
|
sub convert_masq() {
|
|
if ( my $fn = open_file( 'masq', 1, 1 ) ) {
|
|
my ( $snat, $fn1 ) = open_snat_for_output( $fn );
|
|
|
|
my $have_masq_rules;
|
|
|
|
directive_callback(
|
|
sub ()
|
|
{
|
|
if ( $_[0] eq 'OMITTED' ) {
|
|
#
|
|
# Convert the raw rule
|
|
#
|
|
process_one_masq( $snat) if $snat;
|
|
} else {
|
|
print $snat "$_[1]\n"; 0;
|
|
}
|
|
}
|
|
);
|
|
|
|
first_entry(
|
|
sub {
|
|
my $date = compiletime;
|
|
progress_message2 "Converting $fn...";
|
|
print( $snat
|
|
"#\n" ,
|
|
"# Rules generated from masq file $fn by Shorewall $globals{VERSION} - $date\n" ,
|
|
"#\n" );
|
|
}
|
|
);
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
#
|
|
# Process the file normally
|
|
#
|
|
process_one_masq(0);
|
|
#
|
|
# Now Convert it
|
|
#
|
|
process_one_masq($snat);
|
|
|
|
$have_masq_rules++;
|
|
}
|
|
|
|
if ( $have_masq_rules ) {
|
|
progress_message2 "Converted $fn to $fn1";
|
|
if ( rename $fn, "$fn.bak" ) {
|
|
progress_message2 "$fn renamed $fn.bak";
|
|
} else {
|
|
fatal_error "Cannot Rename $fn to $fn.bak: $!";
|
|
}
|
|
} else {
|
|
if ( unlink $fn ) {
|
|
warning_message "Empty masq file ($fn) removed";
|
|
} else {
|
|
warning_message "Unable to remove empty masq file $fn: $!";
|
|
}
|
|
}
|
|
|
|
close $snat, directive_callback( 0 );
|
|
}
|
|
}
|
|
|
|
#
|
|
# Validate the ALL INTERFACES or LOCAL column in the NAT file
|
|
#
|
|
sub validate_nat_column( $$ ) {
|
|
my $ref = $_[1];
|
|
my $val = $$ref;
|
|
|
|
if ( defined $val ) {
|
|
unless ( ( $val = "\L$val" ) eq 'yes' ) {
|
|
if ( ( $val eq 'no' ) || ( $val eq '-' ) ) {
|
|
$$ref = '';
|
|
} else {
|
|
fatal_error "Invalid value ($val) for $_[0]";
|
|
}
|
|
}
|
|
} else {
|
|
$$ref = '';
|
|
}
|
|
}
|
|
|
|
#
|
|
# Process a record from the NAT file
|
|
#
|
|
sub do_one_nat( $$$$$ )
|
|
{
|
|
my ( $external, $fullinterface, $internal, $allints, $localnat ) = @_;
|
|
|
|
my ( $interface, $alias, $remainder ) = split( /:/, $fullinterface, 3 );
|
|
|
|
fatal_error "Invalid alias ($alias:$remainder)" if defined $remainder;
|
|
|
|
sub add_nat_rule( $$ ) {
|
|
add_rule ensure_chain( 'nat', $_[0] ) , $_[1];
|
|
}
|
|
|
|
my $add_ip_aliases = $config{ADD_IP_ALIASES};
|
|
|
|
my $policyin = '';
|
|
my $policyout = '';
|
|
my $rulein = '';
|
|
my $ruleout = '';
|
|
|
|
fatal_error "Unknown interface ($interface)" unless my $interfaceref = known_interface( $interface );
|
|
|
|
if ( $interfaceref->{root} ) {
|
|
$interface = $interfaceref->{name} if $interface eq $interfaceref->{physical};
|
|
} else {
|
|
$rulein = match_source_dev $interface;
|
|
$ruleout = match_dest_dev $interface;
|
|
$interface = $interfaceref->{name};
|
|
}
|
|
|
|
if ( have_ipsec ) {
|
|
$policyin = ' -m policy --pol none --dir in';
|
|
$policyout = '-m policy --pol none --dir out';
|
|
}
|
|
|
|
fatal_error "Invalid nat file entry" unless defined $interface && defined $internal;
|
|
|
|
if ( $add_ip_aliases ) {
|
|
$add_ip_aliases = '' if defined( $alias ) && $alias eq '';
|
|
}
|
|
|
|
validate_nat_column 'ALL INTERFACES', \$allints;
|
|
validate_nat_column 'LOCAL' , \$localnat;
|
|
|
|
if ( $allints ) {
|
|
add_nat_rule 'nat_in' , "-d $external $policyin -j DNAT --to-destination $internal";
|
|
add_nat_rule 'nat_out' , "-s $internal $policyout -j SNAT --to-source $external";
|
|
} else {
|
|
add_nat_rule input_chain( $interface ) , $rulein . "-d $external $policyin -j DNAT --to-destination $internal";
|
|
add_nat_rule output_chain( $interface ) , $ruleout . "-s $internal $policyout -j SNAT --to-source $external";
|
|
}
|
|
|
|
add_nat_rule 'OUTPUT' , "-d $external $policyout -j DNAT --to-destination $internal " if $localnat;
|
|
|
|
if ( $add_ip_aliases ) {
|
|
unless ( $addresses_to_add{$external} ) {
|
|
$addresses_to_add{$external} = 1;
|
|
push @addresses_to_add, ( $external , $fullinterface );
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Process NAT file
|
|
#
|
|
sub setup_nat() {
|
|
|
|
if ( my $fn = open_file( 'nat', 1, 1 ) ) {
|
|
|
|
first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'NAT_ENABLED' , 'a non-empty nat file' , 's'; } );
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
|
|
my ( $external, $interfacelist, $internal, $allints, $localnat ) =
|
|
split_line1( 'nat file',
|
|
{ external => 0, interface => 1, internal => 2, allints => 3, local => 4 } );
|
|
|
|
( $interfacelist, my $digit ) = split /:/, $interfacelist;
|
|
|
|
$digit = defined $digit ? ":$digit" : '';
|
|
|
|
fatal_error 'EXTERNAL must be specified' if $external eq '-';
|
|
fatal_error 'INTERNAL must be specified' if $interfacelist eq '-';
|
|
|
|
for my $interface ( split_list $interfacelist , 'interface' ) {
|
|
fatal_error "Invalid Interface List ($interfacelist)" unless supplied $interface;
|
|
do_one_nat $external, "${interface}${digit}", $internal, $allints, $localnat;
|
|
}
|
|
|
|
progress_message " NAT entry \"$currentline\" $done";
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Setup Network Mapping
|
|
#
|
|
sub setup_netmap() {
|
|
|
|
if ( my $fn = open_file 'netmap', 1, 1 ) {
|
|
|
|
first_entry "$doing $fn...";
|
|
|
|
while ( read_a_line( NORMAL_READ ) ) {
|
|
|
|
my ( $type, $net1, $interfacelist, $net2, $net3, $proto, $dport, $sport ) =
|
|
split_line( 'netmap file',
|
|
{ type => 0, net1 => 1, interface => 2, net2 => 3, net3 => 4, proto => 5, dport => 6, sport => 7 } );
|
|
|
|
$net3 = ALLIP if $net3 eq '-';
|
|
|
|
for my $interface ( split_list $interfacelist, 'interface' ) {
|
|
|
|
my $iface = $interface;
|
|
|
|
fatal_error "Unknown interface ($interface)" unless my $interfaceref = known_interface( $interface );
|
|
|
|
my @rule = do_iproto( $proto, $dport, $sport );
|
|
|
|
my @rulein;
|
|
my @ruleout;
|
|
|
|
$net1 = validate_net $net1, 0;
|
|
$net2 = validate_net $net2, 0;
|
|
|
|
if ( $interfaceref->{root} ) {
|
|
$interface = $interfaceref->{name} if $interface eq $interfaceref->{physical};
|
|
} else {
|
|
@rulein = imatch_source_dev( $interface );
|
|
@ruleout = imatch_dest_dev( $interface );
|
|
$interface = $interfaceref->{name};
|
|
}
|
|
|
|
require_capability 'NETMAP_TARGET', 'Stateful Netmap Entries', '';
|
|
|
|
if ( $type eq 'DNAT' ) {
|
|
dest_iexclusion( ensure_chain( 'nat' , input_chain $interface ) ,
|
|
j => 'NETMAP' ,
|
|
"--to $net2",
|
|
$net1 ,
|
|
@rulein ,
|
|
imatch_source_net( $net3 ) );
|
|
} elsif ( $type eq 'SNAT' ) {
|
|
source_iexclusion( ensure_chain( 'nat' , output_chain $interface ) ,
|
|
j => 'NETMAP' ,
|
|
"--to $net2" ,
|
|
$net1 ,
|
|
@ruleout ,
|
|
imatch_dest_net( $net3 ) );
|
|
} else {
|
|
fatal_error 'TYPE must be specified' if $type eq '-';
|
|
fatal_error "Invalid type ($type)";
|
|
}
|
|
|
|
progress_message " Network $net1 on $iface mapped to $net2 ($type)";
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#
|
|
# Called from process_rule to add a rule to the NAT table
|
|
#
|
|
sub handle_nat_rule( $$$$$$$$$$$$$ ) {
|
|
my ( $dest, # <server>[:port]
|
|
$proto, # Protocol
|
|
$ports, # Destination port list
|
|
$origdest, # Original Destination
|
|
$action_target, # If the target is an action, the name of the log action chain to jump to
|
|
$action, # The Action
|
|
$sourceref, # Reference to the Source Zone's table entry in the Zones module
|
|
$action_chain, # Name of the action chain if the rule is in an action
|
|
$rule, # Matches
|
|
$source, # Source Address
|
|
$loglevel, # [<level>[:<tag>]]
|
|
$log_action, # Action name to include in the log message
|
|
$wildcard # Part of a wildcard rule
|
|
) = @_;
|
|
|
|
my ( $server, $serverport , $origdstports ) = ( '', '', '' );
|
|
my $randomize = $dest =~ s/:random$// ? ' --random' : '';
|
|
|
|
#
|
|
# Isolate server port
|
|
#
|
|
if ( ( $family == F_IPV4 && $dest =~ /^(.*)(?::(.+))$/ ) || ( $family == F_IPV6 && $dest =~ /^\[(.*)]:(.+)$/ ) ) {
|
|
#
|
|
# Server IP and Port
|
|
#
|
|
$server = $1; # May be empty
|
|
$serverport = $2; # Not Empty due to RE
|
|
|
|
my ( $p ) = split( ':', $proto ); # Might be "tcp:syn"
|
|
|
|
require_capability( 'UDPLITEREDIRECT', 'UDPLITE Port Redirection', 's' ) if resolve_proto( $p ) == UDPLITE;
|
|
|
|
$origdstports = validate_port( $proto, $ports ) if $ports && $ports ne '-' && port_count( $ports ) == 1;
|
|
|
|
if ( $serverport =~ /^(\d+)-(\d+)$/ ) {
|
|
#
|
|
# Server Port Range
|
|
#
|
|
fatal_error "Invalid port range ($serverport)" unless $1 < $2;
|
|
my @ports = ( $1, $2 );
|
|
$_ = validate_port( proto_name( $proto ), $_) for ( @ports );
|
|
( $ports = $serverport ) =~ tr/-/:/;
|
|
} else {
|
|
$serverport = $ports = validate_port( proto_name( $proto ), $serverport );
|
|
}
|
|
} elsif ( $dest ne ':' ) {
|
|
#
|
|
# Simple server IP address (may be empty or "-")
|
|
#
|
|
$server = $dest;
|
|
}
|
|
|
|
#
|
|
# Check for list in $server
|
|
#
|
|
fatal_error "An address list ($server) is not allowed in the DEST column of a $action RULE" if $server =~ /,/;
|
|
#
|
|
# Generate the target
|
|
#
|
|
my $target = '';
|
|
|
|
if ( $action eq 'REDIRECT' ) {
|
|
fatal_error "A server IP address ($server) may not be specified in a REDIRECT rule" if $server;
|
|
$target = 'REDIRECT';
|
|
$target .= " --to-port $serverport" if $serverport;
|
|
if ( $origdest eq '' || $origdest eq '-' ) {
|
|
$origdest = ALLIP;
|
|
} elsif ( $origdest eq 'detect' ) {
|
|
fatal_error 'ORIGINAL DEST "detect" is invalid in an action' if $action_chain;
|
|
|
|
if ( $config{DETECT_DNAT_IPADDRS} ) {
|
|
my $interfacesref = $sourceref->{interfaces};
|
|
my @interfaces = keys %$interfacesref;
|
|
$origdest = @interfaces ? "detect:@interfaces" : ALLIP;
|
|
} else {
|
|
$origdest = ALLIP;
|
|
}
|
|
}
|
|
} elsif ( $action_target ) {
|
|
fatal_error "A server port ($serverport) is not allowed in $action rule" if $serverport;
|
|
$target = $action_target;
|
|
} else {
|
|
if ( $server eq '' ) {
|
|
fatal_error "A server and/or port must be specified in the DEST column in $action rules" unless $serverport;
|
|
} elsif ( $server =~ /^(.+)-(.+)$/ ) {
|
|
if ( $family == F_IPV4 ) {
|
|
validate_range( $1, $2 );
|
|
} else {
|
|
my ( $addr1, $addr2 ) = ( $1, $2 );
|
|
|
|
if ( $server =~ /^\[(.+)\]$/ ) {
|
|
$server = $1;
|
|
fatal_error "Correct address range syntax is '[<addr1>-<addr2>]'" if $server =~ /]-\[/;
|
|
assert( $server =~ /^(.+)-(.+)$/ );
|
|
( $addr1, $addr2 ) = ( $1, $2 );
|
|
}
|
|
|
|
validate_range( $addr1, $addr2 );
|
|
$server = join( '-', $addr1, $addr2 );
|
|
}
|
|
} elsif ( $server eq ALLIP || $server eq NILIP ) {
|
|
fatal_error "Invalid or missing server IP address";
|
|
} else {
|
|
$server = $1 if $family == F_IPV6 && $server =~ /^\[(.+)\]$/;
|
|
fatal_error "Invalid server IP address ($server)" if $server eq ALLIP || $server eq NILIP;
|
|
|
|
my @servers;
|
|
|
|
if ( ( $server =~ /^([&%])(.+)/ ) ) {
|
|
$server = record_runtime_address( $1, $2 );
|
|
$server =~ s/ $//;
|
|
@servers = ( $server );
|
|
} else {
|
|
@servers = validate_address $server, 1;
|
|
}
|
|
|
|
$server = join ',', @servers;
|
|
}
|
|
|
|
if ( $action eq 'DNAT' ) {
|
|
$target = $action;
|
|
if ( $server ) {
|
|
$serverport = ":$serverport" if $serverport;
|
|
if ( $family == F_IPV4 ) {
|
|
for my $serv ( split /,/, $server ) {
|
|
$target .= " --to-destination ${serv}${serverport}";
|
|
}
|
|
} else {
|
|
for my $serv ( split /,/, $server ) {
|
|
$target .= " --to-destination [${serv}]${serverport}";
|
|
}
|
|
}
|
|
} else {
|
|
$target .= " --to-destination :$serverport";
|
|
}
|
|
}
|
|
|
|
unless ( $origdest && $origdest ne '-' && $origdest ne 'detect' ) {
|
|
if ( ! $action_chain && $config{DETECT_DNAT_IPADDRS} ) {
|
|
my $interfacesref = $sourceref->{interfaces};
|
|
my @interfaces = keys %$interfacesref;
|
|
$origdest = @interfaces ? "detect:@interfaces" : ALLIP;
|
|
} else {
|
|
$origdest = ALLIP;
|
|
}
|
|
}
|
|
}
|
|
|
|
$target .= $randomize;
|
|
#
|
|
# And generate the nat table rule(s)
|
|
#
|
|
my $firewallsource = $sourceref && ( $sourceref->{type} & ( FIREWALL | VSERVER ) );
|
|
|
|
my $chainref = ensure_chain ('nat' ,
|
|
( $action_chain ? $action_chain :
|
|
$firewallsource ? 'OUTPUT' :
|
|
dnat_chain $sourceref->{name} ) );
|
|
expand_rule ( $chainref,
|
|
$firewallsource ? OUTPUT_RESTRICT : PREROUTE_RESTRICT ,
|
|
'' ,
|
|
$rule ,
|
|
$source ,
|
|
$origdest ,
|
|
'' ,
|
|
$target ,
|
|
$loglevel ,
|
|
$log_action ,
|
|
$serverport ? do_proto( $proto, '', '' ) : '' ,
|
|
'' ,
|
|
)
|
|
unless unreachable_warning( $wildcard, $chainref );
|
|
|
|
( $ports, $origdstports, $server );
|
|
}
|
|
|
|
#
|
|
# Called from process_rule1() to handle the nat table part of the NONAT and ACCEPT+ actions
|
|
#
|
|
sub handle_nonat_rule( $$$$$$$$$$$ ) {
|
|
my ( $action, $source, $dest, $origdest, $sourceref, $inaction, $chain, $loglevel, $log_action, $rule, $wildcard ) = @_;
|
|
|
|
my $sourcezone = $sourceref->{name};
|
|
#
|
|
# NONAT or ACCEPT+ may not specify a destination interface
|
|
#
|
|
fatal_error "Invalid DEST ($dest) in $action rule" if $dest =~ /:/;
|
|
|
|
$origdest = '' unless $origdest and $origdest ne '-';
|
|
|
|
if ( $origdest eq 'detect' ) {
|
|
my $interfacesref = $sourceref->{interfaces};
|
|
my $interfaces = [ ( keys %$interfacesref ) ];
|
|
$origdest = $interfaces ? "detect:@$interfaces" : ALLIP;
|
|
}
|
|
|
|
my $tgt = 'RETURN';
|
|
|
|
my $nonat_chain;
|
|
|
|
my $chn;
|
|
|
|
if ( $inaction ) {
|
|
$nonat_chain = ensure_chain( 'nat', $chain );
|
|
} elsif ( $sourceref->{type} == FIREWALL ) {
|
|
$nonat_chain = $nat_table->{OUTPUT};
|
|
} else {
|
|
$nonat_chain = ensure_chain( 'nat', dnat_chain( $sourcezone ) );
|
|
|
|
my @interfaces = keys %{zone_interfaces $sourcezone};
|
|
|
|
for ( @interfaces ) {
|
|
my $ichain = input_chain $_;
|
|
|
|
if ( $nat_table->{$ichain} ) {
|
|
#
|
|
# Static NAT is defined on this interface
|
|
#
|
|
$chn = new_chain( 'nat', newnonatchain ) unless $chn;
|
|
add_ijump $chn, j => $nat_table->{$ichain}, @interfaces > 1 ? imatch_source_dev( $_ ) : ();
|
|
}
|
|
}
|
|
|
|
if ( $chn ) {
|
|
#
|
|
# Call expand_rule() to correctly handle logging. Because
|
|
# the 'logname' argument is passed, expand_rule() will
|
|
# not create a separate logging chain but will rather emit
|
|
# any logging rule in-line.
|
|
#
|
|
expand_rule( $chn,
|
|
PREROUTE_RESTRICT,
|
|
'', # Prerule
|
|
'', # Rule
|
|
'', # Source
|
|
'', # Dest
|
|
'', # Original dest
|
|
'ACCEPT',
|
|
$loglevel,
|
|
$log_action,
|
|
'',
|
|
'',
|
|
dnat_chain( $sourcezone ) )
|
|
unless unreachable_warning( $wildcard, $chn );
|
|
|
|
$loglevel = '';
|
|
$tgt = $chn->{name};
|
|
} else {
|
|
$tgt = 'ACCEPT';
|
|
}
|
|
}
|
|
|
|
expand_rule( $nonat_chain ,
|
|
PREROUTE_RESTRICT ,
|
|
'' ,
|
|
$rule ,
|
|
$source ,
|
|
$dest ,
|
|
$origdest ,
|
|
$tgt,
|
|
$loglevel ,
|
|
$log_action ,
|
|
'',
|
|
'',
|
|
)
|
|
unless unreachable_warning( $wildcard, $nonat_chain );
|
|
}
|
|
|
|
sub add_addresses () {
|
|
if ( @addresses_to_add ) {
|
|
my @addrs = @addresses_to_add;
|
|
my $arg = '';
|
|
my $addresses = 0;
|
|
|
|
while ( @addrs ) {
|
|
my $addr = shift @addrs;
|
|
my $interface = shift @addrs;
|
|
$arg = "$arg $addr $interface";
|
|
unless ( $config{RETAIN_ALIASES} ) {
|
|
emit '' unless $addresses++;
|
|
$interface =~ s/:.*//;
|
|
emit "del_ip_addr $addr $interface";
|
|
}
|
|
}
|
|
|
|
emit "\nadd_ip_aliases $arg";
|
|
}
|
|
}
|
|
|
|
1;
|