#
# Shorewall 5.2 -- /usr/share/shorewall/Shorewall/Raw.pm
#
#     This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
#
#     (c) 2009-2019 - Tom Eastep (teastep@shorewall.net)
#
#       Complete documentation is available at https://shorewall.org
#
#       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 the code that handles the /etc/shorewall/conntrack file.
#
package Shorewall::Raw;
require Exporter;
use Shorewall::Config qw(:DEFAULT :internal);
use Shorewall::IPAddrs;
use Shorewall::Zones;
use Shorewall::Chains qw(:DEFAULT :internal);

use strict;

our @ISA = qw(Exporter);
our @EXPORT = qw( setup_conntrack );
our @EXPORT_OK = qw( handle_helper_rule );
our $VERSION = '5.2_2';

our %valid_ctevent = ( new        => 1,
		       related    => 1,
		       destroy    => 1,
		       reply      => 1,
		       assured    => 1,
		       protoinfo  => 1,
		       helper     => 1,
		       mark       => 1,
		       natseqinfo => 1,
		       secmark    => 1 );

our $family;

sub initialize($) {
    $family = shift;
}

#
# Notrack
#
sub process_conntrack_rule( $$$$$$$$$$ ) {

    my ($chainref, $zoneref, $action, $source, $dest, $proto, $ports, $sports, $user, $switch ) = @_;

    require_capability 'RAW_TABLE', 'conntrack rules', '';

    $proto  = ''    if $proto  eq 'any';
    $ports  = ''    if $ports  eq 'any' || $ports  eq 'all';
    $sports = ''    if $sports eq 'any' || $sports eq 'all';

    my $zone;
    my $restriction = PREROUTE_RESTRICT;
    my $raw_matches = get_inline_matches(0);
    my $prerule = '';

    if ( $raw_matches =~ /^s*+/ ) {
	$prerule = $raw_matches;
	$raw_matches = '';
    }

    if ( $chainref ) {
	$restriction = OUTPUT_RESTRICT if $chainref->{name} eq 'OUTPUT';
    } else {
	#
	# Entry in the conntrack file
	#
	if ( $zoneref ) {
	    $zone = $zoneref->{name};
	} else {
	    ($zone, $source) = split /:/, $source, 2;
	    $zoneref = find_zone ( $zone );
	}

	$chainref = ensure_raw_chain( notrack_chain $zone );
	$restriction = OUTPUT_RESTRICT if $zoneref->{type}  & (FIREWALL | VSERVER );
	fatal_error 'USER/GROUP is not allowed unless the SOURCE zone is $FW or a Vserver zone' if $user ne '-' && $restriction != OUTPUT_RESTRICT;
    }

    my $disposition = $action;
    my $exception_rule = '';

    my $level = '';

    if ( $action =~ /^(?:NFLOG|ULOG)/ ) {
	$action = join( ":" , 'LOG', $action );
    }

    my $usergenerated;

    if ( $action eq 'NOTRACK' ) {
	#
	# A patch that deimplements the NOTRACK target has been posted on the
	# Netfilter development list
	#
	if ( have_capability 'CT_TARGET' ) {
	    $action = 'CT --notrack';
	    $disposition = 'notrack';
	}
    } elsif ( $action =~ /^(DROP|LOG)(:(.+))?$/ ) {
	if ( $2 ) {
	    validate_level( $level = $3 );
	    $action      = $1;
	    $disposition = $1;
	}
    } elsif ( $action =~ /^IP(6)?TABLES\((.+)\)(:(.*))?$/ ) {
	if ( $family == F_IPV4 ) {
	    fatal_error 'Invalid conntrack ACTION (IP6TABLES)' if $1;
	} else {
	    fatal_error "Invalid conntrack ACTION (IPTABLES)" unless $1;
	}

	my ( $tgt, $options ) = split( ' ', $2, 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 raw table" unless $target_type & RAW_TABLE;
	$disposition = $tgt;
	$action      = $2;
	validate_level( $level = $4 ) if supplied $4;
    } else {
	(  $disposition, my ( $option, $args ), $level ) = split ':', $action, 4;

	fatal_error "Invalid conntrack ACTION ( $action )" if $disposition ne 'CT';

	validate_level( $level ) if supplied $level;

	require_capability 'CT_TARGET', 'CT entries in the conntrack file', '';

	if ( $proto ne '-' ) {
	    if ( $proto =~ s/:all$// ) {
		fatal_error '":all" may only be used with TCP' unless resolve_proto( $proto ) == TCP;
	    } else {
		$proto = TCP . ':syn' if $proto !~ /:syn/ && resolve_proto( $proto ) == TCP;
	    }
	}

	if ( $option eq 'notrack' ) {
	    fatal_error "Invalid conntrack ACTION ( $action )" if supplied $args;
	    $action = 'CT --notrack';
	    $disposition = 'notrack';
	} else {
	    fatal_error "Invalid or missing CT option and arguments" unless supplied $option && supplied $args;

	    if ( $option eq 'helper' ) {
		my $modifiers = '';

		$disposition = 'helper';

		if ( $args =~ /^([-\w.]+)\((.+)\)$/ ) {
		    $args      = $1;
		    $modifiers = $2;
		}

		fatal_error "Invalid helper' ($args)" if $args =~ /,/;
		validate_helper( $args, $proto );
		$action = "CT --helper $helpers_aliases{$args}";

		$exception_rule = do_proto( $proto, '-', '-' );
		
		for my $mod ( split_list1( $modifiers, 'ctevents' ) ) {
		    fatal_error "Invalid helper option ($mod)" unless $mod =~ /^(\w+)=(.+)$/;
		    $mod    = $1;
		    my $val = $2;
		    
		    if ( $mod eq 'ctevents' ) {
			for ( split_list( $val, 'ctevents' ) ) {
			    fatal_error "Invalid 'ctevents' event ($_)" unless $valid_ctevent{$_};
			}

			$action .= " --ctevents $val";
		    } elsif ( $mod eq 'expevents' ) {
			fatal_error "Invalid expevent argument ($val)" unless $val eq 'new';
			$action .= ' --expevents new';
		    } else {
			fatal_error "Invalid helper option ($mod)";
		    }
		}
	    } elsif ( $option eq 'ctevents' ) {
		$disposition = 'helper';

		for ( split_list( $args, 'ctevents' ) ) {
		    fatal_error "Invalid 'ctevents' event ($_)" unless $valid_ctevent{$_};
		}

		$action = "CT --ctevents $args";
	    } elsif ( $option eq 'expevents' ) {
		fatal_error "Invalid expevent argument ($args)" unless $args eq 'new';
		$action = 'CT --expevents new';
	    } else {
		fatal_error "Invalid CT option ($option)";
	    }
	}
    }

    expand_rule( $chainref ,
		 $restriction ,
		 $prerule,
		 do_proto( $proto, $ports, $sports ) . 
		 do_user ( $user ) . 
		 do_condition( $switch , $chainref->{name} ) .
		 $raw_matches ,
		 $source ,
		 $dest ,
		 '' ,
		 $action ,
		 $level || '' ,
		 $disposition ,
		 $exception_rule ,
		 $usergenerated && ! $level );

    progress_message "  Conntrack rule \"$currentline\" $done";
}

sub handle_helper_rule( $$$$$$$$$$$ ) {
    my ( $helper, $source, $dest, $proto, $ports, $sports, $sourceref, $action_target, $actionchain, $user, $rule ) = @_;

    if ( $helper ne '-' ) {
	fatal_error "A HELPER is not allowed with this ACTION" if $action_target;
	#
	# This means that an ACCEPT or NAT rule with a helper is being processed
	#
	process_conntrack_rule( $actionchain ? ensure_raw_chain( $actionchain ) : undef ,
				$sourceref ,
				"CT:helper:$helper",
				$source ,
				$dest ,
				$proto ,
				$ports ,
				$sports ,
				$user,
				'-',
			      );
    } else {
	assert( $action_target );
	#
	# The target is an action
	#
	if ( $actionchain ) {
	    #
	    # And the source is another action chain
	    #
	    expand_rule( ensure_raw_chain( $actionchain ) ,
			 PREROUTE_RESTRICT ,
			 '',
			 $rule ,
			 $source ,
			 $dest ,
			 '' ,
			 $action_target ,
			 '',
			 'CT' ,
			 '' ,
			 '' );
	} else {
	    expand_rule( ensure_raw_chain( notrack_chain( $sourceref->{name} ) ) ,
			 ( $sourceref->{type} == FIREWALL || $sourceref->{type} == VSERVER ?
			   OUTPUT_RESTRICT :
			   PREROUTE_RESTRICT ) ,
			 '' ,
			 $rule ,
			 $source ,
			 $dest ,
			 '' ,
			 $action_target ,
			 '' ,
			 'CT' ,
			 '' ,
			 '' );
	}
    }
}

sub process_format( $ ) {
    my $format = shift;

    fatal_error q(FORMAT must be '1', '2' or '3') unless $format =~ /^[123]$/;
    format_warning;

    $file_format = $format;
}

sub setup_conntrack($) {
    my $convert = shift;
    my $fn;
    my @files = $convert ? ( qw/notrack conntrack/ ) : ( 'conntrack' );

    for my $name ( @files ) {

	$fn = open_file( $name, 3 , 1 );

	if ( $fn ) {

	    my $action;

	    my $empty = 1;

	    first_entry( "$doing $fn..." );

	    while ( read_a_line( NORMAL_READ ) ) {
		my ( $source, $dest, $protos, $ports, $sports, $user, $switch );

		if ( $file_format == 1 ) {
		    ( $source, $dest, $protos, $ports, $sports, $user, $switch ) =
			split_line1( 'Conntrack File',
				     { source => 0, dest => 1, proto => 2, dport => 3, sport => 4, user => 5, switch => 6 } );
		    $action = 'NOTRACK';
		} else {
		    ( $action, $source, $dest, $protos, $ports, $sports, $user, $switch ) = split_line2( 'Conntrack File', { action => 0, source => 1, dest => 2, proto => 3, dport => 4, sport => 5, user => 6, switch => 7 }, undef, undef, 1 );
		}

		$empty = 0;

		for my $proto ( split_list $protos, 'Protocol' ) {
		    if ( $file_format < 3 ) {
			if ( $source =~ /^all(-)?(:(.+))?$/ ) {
			    fatal_error 'USER/GROUP is not allowed unless the SOURCE zone is $FW or a Vserver zone' if $user ne '-';
			    for my $zone ( $1 ? off_firewall_zones : all_zones ) {
				process_conntrack_rule( undef ,
							undef,
							$action,
							$zone . ( $2 || ''),
							$dest,
							$proto,
							$ports,
							$sports,
							$user ,
							$switch );
			    }
			} else {
			    process_conntrack_rule( undef, undef, $action, $source, $dest, $proto, $ports, $sports, $user, $switch );
			}
		    } elsif ( $action =~ s/:O$// ) {
			process_conntrack_rule( $raw_table->{OUTPUT}, undef, $action, $source, $dest, $proto, $ports, $sports, $user, $switch );
		    } elsif ( $action =~ s/:OP$// || $action =~ s/:PO// ) {
			process_conntrack_rule( $raw_table->{PREROUTING}, undef, $action, $source, $dest, $proto, $ports, $sports, $user, $switch );
			process_conntrack_rule( $raw_table->{OUTPUT},     undef, $action, $source, $dest, $proto, $ports, $sports, $user, $switch );
		    } else {
			$action =~ s/:P$//;
			process_conntrack_rule( $raw_table->{PREROUTING}, undef, $action, $source, $dest, $proto, $ports, $sports, $user, $switch );
		    }
		}
	    }

	    if ( $name eq 'notrack') {
		if ( $empty ) {
		    if ( unlink( $fn ) ) {
			warning_message "Empty notrack file ($fn) removed";
		    } else {
			warning_message "Unable to remove empty notrack file ($fn): $!";
		    }
		    $convert = undef;
		}
	    }
	} elsif ( $name eq 'notrack' ) {
	    $convert = undef;

	    if ( -f ( my $fn1 = find_file( $name ) ) ) {
		if ( unlink( $fn1 ) ) {
		    warning_message "Empty notrack file ($fn1) removed";
		} else {
		    warning_message "Unable to remove empty notrack file ($fn1): $!";
		}
	    }
	}
    }

    if ( $convert ) {
	my $conntrack;
	my $empty  = 1;
	my $date = compiletime;
	my $fn1 = find_writable_file 'conntrack';

	$fn = open_file( 'notrack' , 3, 1 ) || fatal_error "Unable to open the notrack file for conversion: $!";

	if ( -f $fn1 ) {
	    open $conntrack, '>>', $fn1 or fatal_error "Unable to open $fn for notrack conversion: $!";
	} else {
	    open $conntrack, '>' , $fn1 or fatal_error "Unable to open $fn for notrack conversion: $!";
	    #
	    # Transfer permissions from the existing notrack file
	    #
	    transfer_permissions( $fn, $fn1 );

	    print $conntrack <<'EOF';
#
# Shorewall version 5 - conntrack File
#
# For information about entries in this file, type "man shorewall-conntrack"
#
##############################################################################################################
EOF
	    print $conntrack '?' . "FORMAT 3\n";
	    
	    print $conntrack <<'EOF';
#ACTION                 SOURCE          DESTINATION     PROTO   DEST            SOURCE  USER/           SWITCH
#                                                               PORT(S)         PORT(S) GROUP
EOF
	}

	print( $conntrack
	       "#\n" ,
	       "# Rules generated from notrack file $fn by Shorewall $globals{VERSION} - $date\n" ,
	       "#\n" );

	while ( read_a_line( PLAIN_READ ) ) {
	    #
	    # Don't copy the header comments from the old notrack file
	    #
	    next if $empty && ( $currentline =~ /^\s*#/ || $currentline =~ /^\s*$/ );

	    if ( $empty ) {
		#
		# First non-commentary line
		#
		$empty = undef;

		print $conntrack '?' . "FORMAT 1\n" unless $currentline =~ /^\s*\??FORMAT/i;
	    }

	    print $conntrack "$currentline\n";
	}

	rename $fn, "$fn.bak" or fatal_error "Unable to rename $fn to $fn.bak: $!";
	progress_message2 "notrack file $fn saved in $fn.bak"
    }
}

1;
