#!/bin/sh
#
# Shorewall 3.4 -- /usr/share/shorewall/lib.nat
#
#     This program is under GPL [http://www.gnu.org/copyleft/gpl.htm]
#
#     (c) 1999,2000,2001,2002,2003,2004,2005,2006,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., 675 Mass Ave, Cambridge, MA 02139, USA
#
# This library is loaded by /usr/share/shorewall/compiler when any of the following
# configuration files are non-empty: masq, nat, netmap; or when there are
# DNAT/REDIRECT rules in the /etc/shorewall/rules file.
#

#
# Set up Source NAT (including masquerading)
#
setup_masq()
{
    local comment=

    do_ipsec_options() {
	local options="$(separate_list $ipsec)" option
	[ -n "$ORIGINAL_POLICY_MATCH" ] || \
	    fatal_error "IPSEC options require policy match support in your kernel and iptables"
	policy="-m policy --pol ipsec --dir out"

	for option in $options; do
	    case $option in
		[Yy]es)         ;;
		strict)         policy="$policy --strict" ;;
		next)           policy="$policy --next" ;;
		reqid=*)        policy="$policy --reqid ${option#*=}" ;;
		spi=*)          policy="$policy --spi ${option#*=}" ;;
		proto=*)        policy="$policy --proto ${option#*=}" ;;
		mode=*)         policy="$policy --mode ${option#*=}" ;;
		tunnel-src=*)   policy="$policy --tunnel-src ${option#*=}" ;;
		tunnel-dst=*)   policy="$policy --tunnel-dst ${option#*=}" ;;
		reqid!=*)       policy="$policy ! --reqid ${option#*=}" ;;
		spi!=*)         policy="$policy ! --spi ${option#*=}" ;;
		proto!=*)       policy="$policy ! --proto ${option#*=}" ;;
		mode!=*)        policy="$policy ! --mode ${option#*=}" ;;
		tunnel-src!=*)  policy="$policy ! --tunnel-src ${option#*=}" ;;
		tunnel-dst!=*)  policy="$policy ! --tunnel-dst ${option#*=}" ;;
		*)              fatal_error "Invalid IPSEC option \"$option\"" ;;
	    esac
	done
    }

    setup_one() {
	local add_snat_aliases=$ADD_SNAT_ALIASES pre_nat= policy= destnets=

	[ "x$ipsec" = x- ] && ipsec=

	case $ipsec in
	    Yes|yes)
		[ -n "$ORIGINAL_POLICY_MATCH" ] || \
		    fatal_error "IPSEC=Yes requires policy match support in your kernel and iptables"
		policy="-m policy --pol ipsec --dir out"
		;;
	    No|no)
		[ -n "$ORIGINAL_POLICY_MATCH" ] || \
		    fatal_error "IPSEC=No requires policy match support in your kernel and iptables"
		policy="-m policy --pol none --dir out"
		;;
	    *)
		if [ -n "$ipsec" ]; then
		    do_ipsec_options
		elif [ -n "$POLICY_MATCH" ]; then
		    policy="-m policy --pol none --dir out"
		fi
		;;
	esac

	case $fullinterface in
	    +*)
		pre_nat=Yes
		fullinterface=${fullinterface#+}
		;;
	esac

	case $fullinterface in
	    *::*)
		add_snat_aliases=
		destnets="${fullinterface##*:}"
		fullinterface="${fullinterface%:*}"
		;;
	    *:*:*)
		# Both alias name and networks
		destnets="${fullinterface##*:}"
		fullinterface="${fullinterface%:*}"
		;;
	    *:)
		add_snat_aliases=
	        fullinterface=${fullinterface%:}
		;;
	    *:*)
		# Alias name OR networks
		case ${fullinterface#*:} in
		    *.*)
			# It's a networks
			destnets="${fullinterface#*:}"
			fullinterface="${fullinterface%:*}"
			;;
		    *)
			#it's an alias name
			;;
		esac
		;;
	    *)
		;;
	esac

	interface=${fullinterface%:*}

	if ! list_search $interface $ALL_INTERFACES; then
	    fatal_error "Unknown interface $interface"
	fi

	if [ "$networks" = "${networks%!*}" ]; then
	    nomasq=
	else
	    nomasq="${networks#*!}"
	    networks="${networks%!*}"
	fi

	source="${networks:=0.0.0.0/0}"

	detectinterface=

	case $source in
	    *.*.*|+*|!+*)
		;;
	    *)
		detectinterface=$networks
		networks=
		;;
	esac

	[ "x$proto" = x- ] && proto=
	[ "x$ports" = x- ] && ports=

	if [ -n "$proto" ]; then

	    displayproto="($proto)"

	    case $proto in
		tcp|TCP|udp|UDP|6|17)
		    if [ -n "$ports" ]; then
			displayproto="($proto $ports)"

			listcount=$(list_count $ports)

			if [ $listcount -gt 1 ]; then
			    case $ports in
				*:*)
				    if [ -n "$XMULTIPORT" ]; then
					if [ $(($listcount + $(list_count1 $(split $ports) ) )) -le 16 ]; then
					    ports="-m multiport --dports $ports"
					else
					    fatal_error "More than 15 entries in port list ($ports)"
					fi
				    else
					fatal_error "Port Range not allowed in list ($ports)"
				    fi
				    ;;
				*)
				    if [ -n "$MULTIPORT" ]; then
					[ $listcount -le 15 ] || fatal_error "More than 15 entries in port list ($ports)"
					ports="-m multiport --dports $ports"
				    else
					fatal_error "Port Ranges require multiport match support in your kernel ($ports)"
				    fi
				    ;;
			    esac
			else
			    ports="--dport $ports"
			fi
		    fi
		    ;;
		*)
		    [ -n "$ports" ] && fatal_error "Ports only allowed with UDP or TCP ($ports)"
		    ;;
	    esac

	    proto="-p $proto"
	else
	    displayproto="(all)"
	    [ -n "$ports" ] && fatal_error "Ports only allowed with UDP or TCP ($ports)"
	fi

	destination=${destnets:=0.0.0.0/0}

	[ -z "$pre_nat" ] && chain=$(masq_chain $interface) || chain=$(snat_chain $interface)

	ensurenatchain $chain

	case $destnets in
	    !*)
		destnets=${destnets#!}

		build_exclusion_chain newchain nat "$nomasq" "$destnets"

		if [ -n "$networks" ]; then
		    for s in $networks; do
			addnatrule $chain $(source_ip_range $s) $proto $ports $policy -j $newchain
		    done
		    networks=
		elif [ -n "$detectinterface" ]; then
		    indent >&3 << __EOF__

networks="\$(get_routed_networks $detectinterface)"

[ -z "\$networks" ] && fatal_error "Unable to determine the routes through interface \"$detectinterface\""

for network in \$networks; do
    run_iptables -t nat -A $chain -s \$network $proto $ports $policy -j $newchain
done

__EOF__
		else
		    addnatrule $chain -j $newchain
		fi

		chain=$newchain
		destnets=0.0.0.0/0
		proto=
		ports=
		policy=
		detectinterface=

		[ -n "$nomasq" ] && source="$source except $nomasq"
		;;
	    *)
		if [ -n "$nomasq" ]; then
		    build_exclusion_chain newchain nat $nomasq

		    if [ -n "$networks" ]; then
			for s in $networks; do
			    for destnet in $(separate_list $destnets); do
				addnatrule $chain $(both_ip_ranges $s $destnet) $proto $ports $policy -j $newchain
			    done
			done
		    elif [ -n "$detectinterface" ]; then
			indent >&3 << __EOF__

networks="\$(get_routed_networks $detectinterface)"

[ -z "\$networks" ] && fatal_error "Unable to determine the routes through interface \"$detectinterface\""

for network in \$networks; do
__EOF__
			for destnet in $(separate_list $destnets); do
			    indent >&3 << __EOF__
    run_iptables -t nat -A $chain -s \$network $(dest_ip_range $destnet) $proto $ports $policy -j $newchain
__EOF__
			done
			indent >&3 << __EOF__

done
__EOF__
		    else
			for destnet in $(separate_list $destnets); do
			    addnatrule $chain $(dest_ip_range $destnet) $proto $ports $policy -j $newchain
			done
		    fi

		    chain=$newchain
		    networks=
		    destnets=0.0.0.0/0
		    proto=
		    ports=
		    policy=
		    detectinterface=
		    source="$source except $nomasq"
		fi

		;;
	esac

	addrlist=
	target=MASQUERADE

	[ "x$addresses" = x- ] && addresses=

	if [ -n "$addresses" ]; then
	    case "$addresses" in
		SAME:nodst:*)
		    target="SAME --nodst"
		    addresses=${addresses#SAME:nodst:}
		    if [ "$addresses" = detect ]; then
			addrlist='$addrlist'
		    else
			for address in $(separate_list $addresses); do
			    addrlist="$addrlist --to $address";
			done
		    fi
		    ;;
		SAME:*)
		    target="SAME"
		    addresses=${addresses#SAME:}
		    if [ "$addresses" = detect ]; then
			addrlist='$addrlist'
		    else
			for address in $(separate_list $addresses); do
			    addrlist="$addrlist --to $address";
			done
		    fi
		    ;;
		detect)
		    target=SNAT
		    addrlist='$addrlist'
		    ;;
		*)
		    for address in $(separate_list $addresses); do
			case $address in
			    *.*.*.*)
				target=SNAT
				addrlist="$addrlist --to-source $address"
				;;
			    *)
				addrlist="$addrlist --to-ports ${address#:}"
				;;
			esac
		    done
		    ;;
	    esac

	    if [ "$addrlist" = '$addrlist' ]; then
		addresses='$(combine_list $addresses)'
		indent >&3 << __EOF__

addrlist=
addressses=\$(find_interface_addresses $interface)

if [ -n "\$addresses" ]; then
    for address in \$addresses; do
        addrlist="$addrlist --to-source $address"
    done
else
    fatal_error "Unable to determine the IP address(es) of $interface"
fi

__EOF__
	    elif [ -n "$add_snat_aliases" ]; then
		for address in $(separate_list $addresses); do
		    address=${address%:)}
		    if [ -n "$address" ]; then
			for addr in $(ip_range_explicit ${address%:*}) ; do
			    if ! list_search $addr $ALIASES_TO_ADD; then
				[ -n "$RETAIN_ALIASES" ] || save_command del_ip_addr $addr $interface
				ALIASES_TO_ADD="$ALIASES_TO_ADD $addr $fullinterface"
				case $fullinterface in
				    *:*)
					fullinterface=${fullinterface%:*}:$((${fullinterface#*:} + 1 ))
					;;
				esac
			    fi
			done
		    fi
		done
	    fi
	fi

	if [ -n "$networks" ]; then
	    for network in $networks; do
		for destnet in $(separate_list $destnets); do
		    addnatrule $chain $(both_ip_ranges $network $destnet) $proto $ports $policy -j $target $addrlist
		done

		if [ -n "$addresses" ]; then
		    progress_message_and_save "   To $destination $displayproto from $network through ${interface} using $addresses"
		else
		    progress_message_and_save "   To $destination $displayproto from $network through ${interface}"
		fi
	    done
	elif [ -n "$detectinterface" ]; then
	    indent >&3 << __EOF__

networks="\$(get_routed_networks $detectinterface)"

[ -z "\$networks" ] && fatal_error "Unable to determine the routes through interface \"$detectinterface\""

for network in \$networks; do
__EOF__
	    for destnet in $(separate_list $destnets); do
		indent >&3 << __EOF__
    run_iptables -t nat -A $chain -s \$network $(dest_ip_range $destnet) $proto $ports $policy -j $target $addrlist
__EOF__
	    done

	    if [ -n "$addresses" ]; then
		message="   To $destination $displayproto from \$network through ${interface} using $addresses"
	    else
		message="   To $destination $displayproto from \$network through ${interface}"
	    fi

	    indent >&3 << __EOF__
    progress_message "$message"
done

__EOF__

	else
	    for destnet in $(separate_list $destnets); do
		addnatrule $chain $(dest_ip_range $destnet) $proto $ports $policy -j $target $addrlist
	    done

	    if [ -n "$addresses" ]; then
		progress_message_and_save "   To $destination $displayproto from $source through ${interface} using $addresses"
	    else
		progress_message_and_save "   To $destination $displayproto from $source through ${interface}"
	    fi
	fi

    } #setup_one()

    if [ -s $TMP_DIR/masq ]; then
	progress_message2 "$DOING Masquerading/SNAT"
	save_progress_message "Setting up Masquerading/SNAT..."

	while read fullinterface networks addresses proto ports ipsec; do
	    if [ -n "$NAT_ENABLED" ]; then
		if [ "x$fullinterface" = xCOMMENT ]; then
		    if [ -n "$COMMENTS" ]; then
			comment=$(echo $networks $addresses $proto $ports $ipsec)
			save_command COMMENT=\"$comment\"
		    else
			error_message "COMMENT ignored --  requires comment support in iptables/Netfilter"
		    fi
		else
		    setup_one
		fi
	    else
		error_message "WARNING: NAT disabled; masq rule ignored"
	    fi
	done < $TMP_DIR/masq
        #
        # Just in case the file ended with a comment
        #
	if [ -n "$COMMENTS" ]; then
	    save_command
	    save_command COMMENT=
	    save_command
	fi
    fi
}

#
# Setup Static Network Address Translation (NAT)
#
setup_nat() {
    local external= interface= internal= allints= localnat= policyin= policyout= comment=

    validate_one() #1 = Variable Name, $2 = Column name, $3 = value
    {
	case $3 in
	    Yes|yes)
                ;;
	    No|no)
		eval ${1}=
		;;
	    *)
		[ -n "$3" ] && \
		    fatal_error "Invalid value ($3) for $2 in entry \"$external $interface $internal $allints $localnat\""
		;;
	esac
    }

    do_one_nat() {
	local add_ip_aliases=$ADD_IP_ALIASES iface=${interface%:*}

	if [ -n "$add_ip_aliases" ]; then
	    case $interface in
		*:)
		    interface=${interface%:}
		    add_ip_aliases=
		    ;;
		*)
		    [ -n "$RETAIN_ALIASES" ] || save_command del_ip_addr $external $iface
		    ;;
	    esac
	else
	    interface=${interface%:}
	fi

	validate_one allints  "ALL INTERFACES" $allints
	validate_one localnat "LOCAL"          $localnat

	if [ -n "$allints" ]; then
	    addnatrule nat_in  -d $external $policyin  -j DNAT --to-destination $internal
	    addnatrule nat_out -s $internal $policyout -j SNAT --to-source $external
	else
	    addnatrule $(input_chain $iface)  -d $external $policyin  -j DNAT --to-destination $internal
		addnatrule $(output_chain $iface) -s $internal $policyout -j SNAT --to-source $external
	fi

	[ -n "$localnat" ] && \
	    run_iptables2 -t nat -A OUTPUT -d $external $policyout -j DNAT --to-destination $internal

	if [ -n "$add_ip_aliases" ]; then
	    list_search $external $ALIASES_TO_ADD || \
		ALIASES_TO_ADD="$ALIASES_TO_ADD $external $interface"
	fi
    }
    #
    # At this point, we're just interested in the network translation
    #
    > $STATEDIR/nat

    if [ -n "$POLICY_MATCH" ]; then
	policyin="-m policy --pol none --dir in"
	policyout="-m policy --pol none --dir out"
    fi

    if [ -s $TMP_DIR/nat ]; then
	save_progress_message "Setting up one-to-one NAT..."

	while read external interface internal allints localnat; do

	    if [ "x$external" = xCOMMENT ]; then
		if [ -n "$COMMENTS" ]; then
		    comment=$(echo $interface $internal $allints $localnat)
		    save_command COMMENT=\"$comment\"
		else
		    error_message "COMMENT ignored --  requires comment support in iptables/Netfilter"
		fi
	    else
		do_one_nat
	    fi
	    progress_message_and_save "   Host $internal NAT $external on $interface"
	done < $TMP_DIR/nat

	if [ -n "$COMMENTS" ]; then
	    save_command
	    save_command COMMENT=
	    save_command
	fi
    fi

}

#
# Setup Network Mapping (NETMAP)
#
setup_netmap() {

    while read type net1 interface net2 ; do

	list_search $interface $ALL_INTERFACES || \
	    fatal_error "Unknown interface $interface in entry \"$type $net1 $interface $net2\""

	case $type in
	    DNAT)
		addnatrule $(input_chain $interface)  -d $net1 -j NETMAP --to $net2
		;;
	    SNAT)
		addnatrule $(output_chain $interface) -s $net1 -j NETMAP --to $net2
		;;
	    *)
		fatal_error "Invalid type $type in entry \"$type $net1 $interface $net2\""
		;;
	esac

	progress_message_and_save "   Network $net1 on $interface mapped to $net2 ($type)"

    done < $TMP_DIR/netmap
}

#
# Add a NAT rule - Helper function for the rules file processor
#
# The caller has established the following variables:
#    cli	    = Source IP, interface or MAC Specification
#    serv	    = Destination IP Specification
#    servport	    = Port the server is listening on
#    dest_interface = Destination Interface Specification
#    proto	    = Protocol Specification
#    addr	    = Original Destination Address
#    dports	    = Destination Port Specification. 'dports' may be changed
#		      by this function
#    cport	    = Source Port Specification
#    multiport	    = String to invoke multiport match if appropriate
#    ratelimit      = Optional rate limiting clause
#    userandgroup   = -m owner match to limit the rule to a particular user and/or group
#    logtag         = Log tag
#    excludesource  = Source Exclusion List
#
add_nat_rule() {
    local chain
    local excludedests=

    # Be sure we can NAT

    if [ -z "$NAT_ENABLED" ]; then
	fatal_error "Rule \"$rule\" requires NAT which is disabled"
    fi

    # Parse SNAT address if any

    if [ "$addr" != "${addr%:*}" ]; then
	fatal_error "SNAT may no longer be specified in a DNAT rule; use ${CONFDIR}/masq instead"
    fi

    # Set original destination address

    case $addr in
	all)
	    addr=
	    ;;
	detect)
	    eval interfaces=\$${source}_interfaces

	    if [ -n "$DETECT_DNAT_IPADDRS" -a "$source" != "$FW" ]; then

		save_command
		if [ $(list_count1 $interfaces) -eq 1 ]; then
		    save_command "addr=\$(find_first_interface_address $interfaces)"
		else
		    save_command "addr="
		    for interface in $interfaces; do
			    ident >&3 << __EOF__
addr="\$addr \$(find_first_interface_address $interface)"
__EOF__
		    done
		fi
	    else
		addr=
	    fi
	    ;;
	!*)
	    if [ $(list_count $addr) -gt 1 ]; then
		excludedests="${addr#\!}"
		addr=
	    fi
	    ;;
    esac

    addr=${addr:-0.0.0.0/0}

    # Select target

    if [ "$logtarget" = SAME ]; then
	[ -n "$servport" ] && fatal_error "Port mapping not allowed in SAME rules"
	serv1=
	for srv in $(separate_list $serv); do
	    serv1="$serv1 --to ${srv}"
	done
	target1="SAME $serv1"
    elif [ -n "$serv" ]; then
	servport="${servport:+:$servport}"
	serv1=
	for srv in $(separate_list $serv); do
	    serv1="$serv1 --to-destination ${srv}${servport}"
	done
	target1="DNAT $serv1"
    else
	target1="REDIRECT --to-port $servport"
    fi

    # Generate nat table rules

    if [ "$source" = "$FW" ]; then
	if [ -n "${excludesource}${excludedests}" ]; then
	    build_exclusion_chain chain nat "$excludesource" $excludedests

	    for adr in $(separate_list $addr); do
		run_iptables2 -t nat -A OUTPUT $cli $proto $userandgroup $multiport $sports $dports $(dest_ip_range $adr) -j $chain
	    done

	    if [ -n "$loglevel" ]; then
		log_rule_limit $loglevel $chain OUTPUT $logtarget "$ratelimit" "$logtag" -A -t nat
	    fi

	    addnatrule $chain $ratelimit $proto -j $target1 # Protocol is necessary for port redirection
	else
	    for adr in $(separate_list $addr); do
		if [ -n "$loglevel" ]; then
		    log_rule_limit $loglevel OUTPUT OUTPUT $logtarget "$ratelimit" "$logtag" -A -t nat \
			$(fix_bang $proto $cli $sports $userandgroup $(dest_ip_range $adr) $multiport $dports)
		fi

		run_iptables2 -t nat -A OUTPUT $ratelimit $proto $sports $userandgroup $(dest_ip_range $adr) $multiport $dports -j $target1
	    done
	fi
    else
	if [ -n "${excludesource}${excludedests}" ]; then
	    build_exclusion_chain chain nat "$excludesource" $excludedests

	    if [ $addr = detect ]; then
		ensurenatchain $(dnat_chain $source)
		#
		# The 'for loops' begun below are completed in add_a_rule() (in the compiler)
	        #
		indent >&3 << __EOF__

for adr in \$addr; do
    run_iptables -t nat -A $(fix_bang $(dnat_chain $source) $cli $proto $multiport $sports $dports) -d \$adr -j $chain
__EOF__
	    else
		for adr in $(separate_list $addr); do
		    addnatrule $(dnat_chain $source) $cli $proto $multiport $sports $dports $(dest_ip_range $adr) -j $chain
		done
	    fi

	    if [ -n "$loglevel" ]; then
		log_rule_limit $loglevel $chain $(dnat_chain $source) $logtarget "$ratelimit" "$logtag" -A -t nat
	    fi

	    addnatrule $chain $ratelimit $proto -j $target1 # Protocol is necessary for port redirection
	else
	    chain=$(dnat_chain $source)

	    if [ $addr = detect ]; then
		ensurenatchain $chain

		indent >&3 << __EOF__

for adr in \$addr; do
__EOF__
		if [ -n "$loglevel" ]; then
		    indent >&3 << __EOF__
    log_rule_limit $loglevel $chain $chain $logtarget "$ratelimit" "$logtag" -A -t nat $(fix_bang $proto $cli $sports $multiport $dports) -d \$adr
__EOF__
		fi

		indent >&3 << __EOF__
    run_iptables -t nat -A $chain $(fix_bang $proto $ratelimit $cli $sports $multiport $dports) -d \$adr -j $target1
__EOF__
	    else
		for adr in $(separate_list $addr); do
		    if [ -n "$loglevel" ]; then
			ensurenatchain $chain
			log_rule_limit $loglevel $chain $chain $logtarget "$ratelimit" "$logtag" -A -t nat \
			    $(fix_bang $proto $cli $sports $(dest_ip_range $adr) $multiport $dports)
		    fi

		    addnatrule $chain $proto $ratelimit $cli $sports \
			-d $adr $multiport $dports -j $target1
		done
	    fi
	fi
    fi

    # Replace destination port by the new destination port

    if [ -n "$servport" ]; then
	if [ -z "$multiport" ]; then
	    dports="--dport ${servport#*:}"
	else
	    dports="--dports ${servport#*:}"
	fi
    fi

    [ "x$addr" = "x0.0.0.0/0" ] && addr=
    ratelimit=
}
