#
# Shorewall-perl 4.1 -- /usr/share/shorewall-perl/Shorewall/Nat.pm
#
#     This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
#
#     (c) 2007 - Tom Eastep (teastep@shorewall.net)
#
#       Complete documentation is available at http://shorewall.net
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of Version 2 of the GNU General Public License
#       as published by the Free Software Foundation.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#   This 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::IPAddrs;
use Shorewall::Providers qw( lookup_provider );

use strict;

our @ISA = qw(Exporter);
our @EXPORT = qw( setup_masq setup_nat setup_netmap add_addresses );
our @EXPORT_OK = ();
our $VERSION = 4.1.5;

our @addresses_to_add;
our %addresses_to_add;

#
# Initialize globals -- we take this novel approach to globals initialization to allow
#                       the compiler to run multiple times in the same process. The
#                       initialize() function does globals initialization for this
#                       module and is called from an INIT block below. The function is
#                       also called by Shorewall::Compiler::compiler at the beginning of
#                       the second and subsequent calls to that function.
#

sub initialize() {
    @addresses_to_add = ();
    %addresses_to_add = ();
}

INIT {
    initialize;
}

#
# Handle IPSEC Options in a masq record
#
sub do_ipsec_options($)
{
    my %validoptions = ( strict       => NOTHING,
			 next         => NOTHING,
			 reqid        => NUMERIC,
			 spi          => NUMERIC,
			 proto        => IPSECPROTO,
			 mode         => IPSECMODE,
			 "tunnel-src" => NETWORK,
			 "tunnel-dst" => NETWORK,
		       );
    my $list=$_[0];
    my $options = '-m policy --pol ipsec --dir out ';
    my $fmt;

    for my $e ( split_list $list, 'option' ) {
	my $val    = undef;
	my $invert = '';

	if ( $e =~ /([\w-]+)!=(.+)/ ) {
	    $val    = $2;
	    $e      = $1;
	    $invert = '! ';
	} elsif ( $e =~ /([\w-]+)=(.+)/ ) {
	    $val = $2;
	    $e   = $1;
	}

	$fmt = $validoptions{$e};

	fatal_error "Invalid Option ($e)" unless $fmt;

	if ( $fmt eq NOTHING ) {
	    fatal_error "Option \"$e\" does not take a value" if defined $val;
	} else {
	    fatal_error "Missing value for option \"$e\""        unless defined $val;
	    fatal_error "Invalid value ($val) for option \"$e\"" unless $val =~ /^($fmt)$/;
	}

	$options .= $invert;
	$options .= "--$e ";
	$options .= "$val " if defined $val;
    }

    $options;
}

#
# Process a single rule from the the masq file
#
sub setup_one_masq($$$$$$$)
{
    my ($interfacelist, $networks, $addresses, $proto, $ports, $ipsec, $mark) = @_;

    my $pre_nat;
    my $add_snat_aliases = $config{ADD_SNAT_ALIASES};
    my $destnets = '';
    my $baserule = '';

    #
    # Leading '+'
    #
    $pre_nat = 1 if $interfacelist =~ s/^\+//;
    #
    # Parse the remaining part of the INTERFACE column
    #
    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 =~ /\./ ) {
	    $interfacelist = $one;
	    $destnets = $two;
	}
    }
    #
    # If there is no source or destination then allow all addresses
    #
    $networks = ALLIPv4 if $networks eq '-';
    $destnets = ALLIPv4 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 $globals{ORIGINAL_POLICY_MATCH};
	
	if ( $ipsec =~ /^yes$/i ) {
	    $baserule .= '-m policy --pol ipsec --dir out ';
	} elsif ( $ipsec =~ /^no$/i ) {
	    $baserule .= '-m policy --pol none --dir out ';
	} else {
	    $baserule .= do_ipsec_options $ipsec;
	}
    } elsif ( $capabilities{POLICY_MATCH} ) {
	$baserule .= '-m policy --pol none --dir out ';
    }

    #
    # Handle Protocol and Ports
    #
    $baserule .= do_proto $proto, $ports, '';

    #
    # Handle Mark
    #
    $baserule .= do_test( $mark, 0xFF) if $mark ne '-';
	
    for my $fullinterface (split_list $interfacelist, 'interface' ) {
	my $rule = '';
	my $target = '-j MASQUERADE ';
	#
	# Isolate and verify the interface part
	#
	( my $interface = $fullinterface ) =~ s/:.*//;

	if ( $interface =~ /(.*)[(](\w*)[)]$/ ) {
	    $interface = $1;
	    my $provider  = $2;
	    $fullinterface =~ s/[(]\w*[)]//;
	    my $realm = lookup_provider( $provider ) unless $provider =~ /^\d+$/;

	    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 );

	unless ( $interfaceref->{root} ) {
	    $rule .= "-o $interface ";
	    $interface = $interfaceref->{name};
	}

	my $chainref = ensure_chain('nat', $pre_nat ? snat_chain $interface : masq_chain $interface);

	my $detectaddress = 0;
	my $exceptionrule = '';
	my $randomize     = '';
	#
	# Parse the ADDRESSES column
	#
	if ( $addresses ne '-' ) {
	    if ( $addresses eq 'random' ) {
		$randomize = '--random ';
	    } else {
		$addresses =~ s/:random$// and $randomize = '--random ';
		
		if ( $addresses =~ /^SAME:nodst:/ ) {
		    fatal_error "':random' is not supported by the SAME target" if $randomize;
		    $target = '-j SAME --nodst ';
		    $addresses =~ s/.*://;
		    for my $addr ( split_list $addresses, 'address' ) {
			$target .= "--to $addr ";
		    }
		} elsif ( $addresses =~ /^SAME:/ ) {
		    fatal_error "':random' is not supported by the SAME target" if $randomize;
		    $target = '-j SAME ';
		    $addresses =~ s/.*://;
		    for my $addr ( split_list $addresses, 'address' ) {
			$target .= "--to $addr ";
		    }
		} elsif ( $addresses eq 'detect' ) {
		    my $variable = get_interface_address $interface;
		    $target = "-j 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;
		    }
		} else {
		    my $addrlist = '';
		    for my $addr ( split_list $addresses , 'address' ) {
			if ( $addr =~ /^.*\..*\..*\./ ) {
			    $target = '-j SNAT ';
			    $addrlist .= "--to-source $addr ";
			    $exceptionrule = do_proto( $proto, '', '' ) if $addr =~ /:/;
			} else {
			    $addr =~ s/^://;
			    $addrlist .= "--to-ports $addr ";
			    $exceptionrule = do_proto( $proto, '', '' );
			}
		    }

		    $target .= $addrlist;
		}
	    }

	    $target .= $randomize;
	} else {
	    $add_snat_aliases = 0;
	}
	#
	# And Generate the Rule(s)
	#
	expand_rule( $chainref ,
		     POSTROUTE_RESTRICT ,
		     $baserule . $rule ,
		     $networks ,
		     $destnets ,
		     '' ,
		     $target ,
		     '' ,
		     '' ,
		     $exceptionrule );
	
	if ( $detectaddress ) {
	    decr_cmd_level( $chainref );
	    add_command( $chainref , 'fi' );
	}
	
	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} ) {
			emit "del_ip_addr $addr $interface" unless $config{RETAIN_ALIASES};
			$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";

}

#
# Process the masq file
#
sub setup_masq()
{
    my $fn = open_file 'masq';

    first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'NAT_ENABLED' , 'a non-empty masq file' , 's'; } );
    
    while ( read_a_line ) {

	my ($fullinterface, $networks, $addresses, $proto, $ports, $ipsec, $mark ) = split_line1 2, 7, 'masq file';

	if ( $fullinterface eq 'COMMENT' ) {
	    process_comment;
	} else {
	    setup_one_masq $fullinterface, $networks, $addresses, $proto, $ports, $ipsec, $mark;
	}
    }

    clear_comment;

}

#
# 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 );

    unless ( $interfaceref->{root} ) {
	$rulein  = "-i $interface ";
	$ruleout = "-o $interface ";
	$interface = $interfaceref->{name};
    }

    if ( $capabilities{POLICY_MATCH} ) {
	$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 ) {
	if ( defined( $alias ) && $alias eq '' ) {
	    $add_ip_aliases = '';
	} else {
	    emit "del_ip_addr $external $interface" unless $config{RETAIN_ALIASES};
	}
    }

    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() {

    my $fn = open_file 'nat';

    first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'NAT_ENABLED' , 'a non-empty nat file' , 's'; } );
    
    while ( read_a_line ) {

	my ( $external, $interfacelist, $internal, $allints, $localnat ) = split_line1 3, 5, 'nat file';

	if ( $external eq 'COMMENT' ) {
	    process_comment;
	} else {
	    ( $interfacelist, my $digit ) = split /:/, $interfacelist;

	    $digit = defined $digit ? ":$digit" : '';

	    for my $interface ( split_list $interfacelist , 'interface' ) {
		fatal_error "Invalid Interface List ($interfacelist)" unless defined $interface && $interface ne '';
		do_one_nat $external, "${interface}${digit}", $internal, $allints, $localnat;
	    }

	    progress_message "   NAT entry \"$currentline\" $done";
	}

    }

    clear_comment;
}

#
# Setup Network Mapping
#
sub setup_netmap() {

    my $fn = open_file 'netmap';

    first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'NAT_ENABLED' , 'a non-empty netmap file' , 's'; } );

    while ( read_a_line ) {

	my ( $type, $net1, $interfacelist, $net2 ) = split_line 4, 4, 'netmap file';

	for my $interface ( split_list $interfacelist, 'interface' ) {
	
	    my $rulein = '';
	    my $ruleout = '';
	    my $iface = $interface;
	    
	    fatal_error "Unknown interface ($interface)" unless my $interfaceref = find_interface( $interface );
	
	    unless ( $interfaceref->{root} ) {
		$rulein  = "-i $interface ";
		$ruleout = "-o $interface ";
		$interface = $interfaceref->{name};
	    }

	    if ( $type eq 'DNAT' ) {
		add_rule ensure_chain( 'nat' , input_chain $interface )  , $rulein  . "-d $net1 -j NETMAP --to $net2";
	    } elsif ( $type eq 'SNAT' ) {
		add_rule ensure_chain( 'nat' , output_chain $interface ) , $ruleout . "-s $net1 -j NETMAP --to $net2";
	    } else {
		fatal_error "Invalid type ($type)";
	    }

	    progress_message "   Network $net1 on $iface mapped to $net2 ($type)";
	}
    }

}

sub add_addresses () {
    if ( @addresses_to_add ) {
	my $arg = '';

	while ( @addresses_to_add ) {
	    my $addr      = shift @addresses_to_add;
	    my $interface = shift @addresses_to_add;
	    $arg = "$arg $addr $interface";
	}

	emit "add_ip_aliases $arg";
    }
}

1;