#!/bin/sh
#
# Shorewall 4.1 -- /usr/share/shorewall/lib.actions
#
#     This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
#
#     (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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# This library is loaded by /usr/share/shorewall/compiler when USE_ACTIONS=Yes
# (either explicitly specified or defaulted).
#

#
# Add one Filter Rule from an action -- Helper function for the action file processor
#
# The caller has established the following variables:
#    COMMAND      = current command.
#    client	  = SOURCE IP or MAC
#    server	  = DESTINATION IP or interface
#    protocol	  = Protocol
#    address	  = Original Destination Address
#    port	  = Destination Port
#    cport	  = Source Port
#    multioption  = String to invoke multiport match if appropriate
#    action	  = The chain for this rule
#    ratelimit    = Optional rate limiting clause
#    userandgroup = owner match clause
#    logtag       = Log tag
#
add_an_action()
{
    local chain1

    do_ports() {
	if [ -n "$port" ]; then
	    dports="--dport"
	    if [ -n "$multioption" -a "$port" != "${port%,*}" ]; then
		multiport="$multioption"
		dports="--dports"
	    fi
	    dports="$dports $port"
	fi

	if [ -n "$cport" ]; then
	    sports="--sport"
	    if [ -n "$multioption" -a "$cport" != "${cport%,*}" ]; then
		multiport="$multioption"
		sports="--sports"
	    fi
	    sports="$sports $cport"
	fi
    }

    interface_error()
    {
	fatal_error "Unknown interface $1 in rule: \"$rule\""
    }

    action_interface_verify()
    {
	verify_interface $1 || interface_error $1
    }

    handle_exclusion()
    {
	build_exclusion_chain chain1 filter "$excludesource" "$excludedest"

	run_iptables -A $chain $(fix_bang $cli $proto $multiport $sports $dports) $user -j $chain1

	cli=
	proto=
	sports=
	multiport=
	dports=
	user=
    }

    do_ipp2p() {
	[ -n "$IPP2P_MATCH" ] || fatal_error "Your kernel and/or iptables does not have IPP2P match support. Rule: \"$rule\""

	dports="-m ipp2p --${port:-ipp2p}"

	case $proto in
	    ipp2p|IPP2P)
		proto=tcp
		port=
		do_ports
		;;
	    ipp2p:udp|IPP2P:UDP)
		proto=udp
		port=
		do_ports
		;;
	    ipp2p:all|IPP2P:ALL)
		proto=all
		;;
	esac
    }

    # Set source variables. The 'cli' variable will hold the client match predicate(s).

    cli=

    case "$client" in
    -)
	;;
    *:*)
	action_interface_verify ${client%:*}
	cli="$(match_source_dev ${client%:*}) $(source_ip_range ${client#*:})"
	;;
    *.*.*|+*|!+*)
	cli="$(source_ip_range $client)"
	;;
    ~*|!~*)
	cli=$(mac_match $client)
	;;
    *)
	if [ -n "$client" ]; then
	    action_interface_verify $client
	    cli="$(match_source_dev $client)"
	fi
	;;
    esac

    # Set destination variables - 'serv' and 'dest_interface' hold the server match predicate(s).

    dest_interface=
    serv=

    case "$server" in
    -)
	;;
    *:*)
	action_interface_verify ${server%:*}
	dest_interface=$(match_dest_dev ${server%:*})
	serv=${server#*:}
	;;
    *.*.*|+*|!+*)
	serv=$server
	;;
    ~*|!~*)
	fatal_error "Rule \"$rule\" - Destination may not be specified by MAC Address"
	;;
    *)
	if [ -n "$server" ]; then
	    action_interface_verify $server
	    dest_interface="$(match_dest_dev $server)"
	fi
	;;
    esac

    # Setup protocol and port variables

    sports=
    dports=
    proto=$protocol
    servport=$serverport
    multiport=
    chain1=$chain
    user="$userandgroup"

    [ x$port  = x- ] && port=
    [ x$cport = x- ] && cport=

    case $proto in
	tcp|TCP|6)
	    do_ports
	    ;;
	tcp:syn)
	    proto="$proto --syn"
	    do_ports
	    ;;
	udp|UDP|17)
	    do_ports
	    ;;
	icmp|ICMP|1)
	    [ -n "$port" ]  && 	dports="--icmp-type $port"
	    ;;
	ipp2p|IPP2P|ipp2p:*|IPP2P:*)
	    do_ipp2p
	    ;;
	*)
	    [ -n "$port" ] && \
		fatal_error "Port number not allowed with protocol \"$proto\"; rule: \"$rule\""
	    ;;
    esac

    proto="${proto:+-p $proto}"

    # Some misc. setup

    case "$logtarget" in
	LOG)
	    [ -z "$loglevel" ] && fatal_error "LOG requires log level"
	    ;;
    esac

    if [ -n "${excludesource}${excludedest}" ]; then
	handle_exclusion
    fi

    if [ -n "${serv}" ]; then
	for serv1 in $(separate_list $serv); do
	    for srv in $(firewall_ip_range $serv1); do
		if [ -n "$loglevel" ]; then
		    log_rule_limit $loglevel $chain1 $action $logtarget "$ratelimit" "$logtag" -A $user \
			$(fix_bang $proto $multiport $sports $cli $(dest_ip_range $srv) $dest_interface $dports)
		fi

		run_iptables2 -A $chain1 $proto $multiport $cli $sports \
		    $(dest_ip_range $srv) $dest_interface $dports $ratelimit $user -j $target
	    done
	done
    else
	if [ -n "$loglevel" ]; then
	    log_rule_limit $loglevel $chain1 $action $logtarget "$ratelimit" "$logtag" -A $user \
		$(fix_bang $proto $multiport $sports $cli $dest_interface $dports)
	fi

	run_iptables2 -A $chain1 $proto $multiport $cli $dest_interface $sports \
	    $dports $ratelimit $user -j $target
    fi
}

#
# Process a record from an action file
#
process_action() # $1 = chain    (Chain to add the rules to)
                 # $2 = action   (The action name for logging purposes)
                 # $3 = target   (The (possibly modified) contents of the TARGET column)
                 # $4 = clients
                 # $5 = servers
                 # $6 = protocol
                 # $7 = ports
                 # $8 = cports
                 # $9 = ratelimit
                 # $10 = userspec
                 # $11 = mark
{
    local chain="$1"
    local action="$2"
    local target="$3"
    local clients="$4"
    local servers="$5"
    local protocol="$6"
    local ports="$7"
    local cports="$8"
    local ratelimit="$9"
    local userspec="${10}"
    local mark="${11}"
    local userandgroup=
    local logtag=

    if [ -n "$ratelimit" ]; then
	case $ratelimit in
	    -)
		ratelimit=
		;;
	    *:*)
		ratelimit="-m limit --limit ${ratelimit%:*} --limit-burst ${ratelimit#*:}"
		;;
	    *)
		ratelimit="-m limit --limit $ratelimit"
		;;
	esac
    fi

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

    if [ -n "$userspec" ]; then
	userandgroup="-m owner"

	case "$userspec" in
	    !*+*)
		if [ -n "${userspec#*+}" ]; then
		    userandgroup="$userandgroup ! --cmd-owner ${userspec#*+}"
		fi
		userspec=${userspec%+*}
		;;
	    *+*)
		if [ -n "${userspec#*+}" ]; then
		    userandgroup="$userandgroup --cmd-owner ${userspec#*+}"
		fi
		userspec=${userspec%+*}
		;;
	esac

	case "$userspec" in
	    !*:*)
		if [ "$userspec" != "!:" ]; then
		    temp="${userspec#!}"
		    temp="${temp%:*}"
		    [ -n "$temp" ] && userandgroup="$userandgroup ! --uid-owner $temp"
		    temp="${userspec#*:}"
		    [ -n "$temp" ] && userandgroup="$userandgroup ! --gid-owner $temp"
		fi
		;;
	    *:*)
		if [ "$userspec" != ":" ]; then
		    temp="${userspec%:*}"
		    [ -n "$temp" ] && userandgroup="$userandgroup --uid-owner $temp"
		    temp="${userspec#*:}"
		    [ -n "$temp" ] && userandgroup="$userandgroup --gid-owner $temp"
		fi
		;;
	    !*)
		[ "$userspec" != "!" ] && userandgroup="$userandgroup ! --uid-owner ${userspec#!}"
		;;
	    *)
		[ -n "$userspec" ] && userandgroup="$userandgroup --uid-owner $userspec"
		;;
	esac

	[ "$userandgroup" = "-m owner" ] && userandgroup=
    fi

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

    if [ -n "$mark" ]; then
        if [ "$mark" = "${mark%!*}" ]; then
            mark="-m mark --mark $mark"
        else
            mark="-m mark ! --mark ${mark#*!}"
        fi
    fi

    # Isolate log level

    if [ "$target" = "${target%:*}" ]; then
	loglevel=
    else
	loglevel="${target#*:}"
	target="${target%%:*}"
	if [ "$loglevel" != "${loglevel%:*}" ]; then
	    logtag="${loglevel#*:}"
	    loglevel="${loglevel%:*}"
	fi

	case $loglevel in
	    none*)
		loglevel=
		[ $target = LOG ] && return
		;;
	esac

	loglevel=${loglevel%\!}
    fi

    logtarget="$target"

    case $target in
	REJECT)
	    target=reject
	    ;;
	CONTINUE)
	    target=RETURN
	    ;;
	*)
	    ;;
	esac

    excludesource=

    case ${clients:=-} in
	*!*!*)
	    fatal_error "Invalid SOURCE in rule \"$rule\""
	    ;;
	!*)
	    if [ $(list_count $clients) -gt 1 ]; then
		excludesource=${clients#!}
		clients=
	    fi
	    ;;
	*!*)
	    excludesource=${clients#*!}
	    clients=${clients%!*}
	    ;;
    esac

    excludedest=

    case ${servers:=-} in
	*!*!*)
	    fatal_error "Invalid DEST in rule \"$rule\""
	    ;;
	!*)
	    if [ $(list_count $servers) -gt 1 ]; then
		excludedest=${servers#*!}
		servers=
	    fi
	    ;;
	*!*)
	    excludedest=${servers#*!}
	    servers=${servers%!*}
	    ;;
    esac

    # Generate Netfilter rule(s)

    [ "x$protocol" = "x-" ] && protocol=all || protocol=${protocol:=all}

    if [ -n "$XMULTIPORT" ] && \
	! list_search $protocol "icmp" "ICMP" "1" && \
	[ $(( $(list_count $ports)  + $(list_count1 $(split $ports ) ) ))  -le 16 -a \
	  $(( $(list_count $cports) + $(list_count1 $(split $cports ) ) )) -le 16 ]
	then
	#
	# Extended MULTIPORT is enabled, and less than
	# 16 ports are listed (port ranges count as two ports) - use multiport match.
	#
	multioption="-m multiport"
	for client in $(separate_list $clients); do
	    for server in $(separate_list $servers); do
		#
		# add_an_action() modifies these so we must set their values each time
		#
		port=${ports:=-}
		cport=${cports:=-}
		add_an_action
	    done
	done
    elif [ -n "$MULTIPORT" ] && \
	! list_search $protocol "icmp" "ICMP" "1" && \
	[ "$ports" = "${ports%:*}" -a \
	"$cports" = "${cports%:*}" -a \
	$(list_count $ports) -le 15 -a \
	$(list_count $cports) -le 15 ]
	then
	#
	# MULTIPORT is enabled, there are no port ranges in the rule and less than
	# 16 ports are listed - use multiport match.
	#
	multioption="-m multiport"
	for client in $(separate_list $clients); do
	    for server in $(separate_list $servers); do
		#
		# add_an_action() modifies these so we must set their values each time
		#
		port=${ports:=-}
		cport=${cports:=-}
		add_an_action
	    done
	done
    else
	#
	# MULTIPORT is disabled or the rule isn't compatible with multiport match
	#
	multioption=
	for client in $(separate_list $clients); do
	    for server in $(separate_list $servers); do
		for port in $(separate_list ${ports:=-}); do
		    for cport in $(separate_list ${cports:=-}); do
			add_an_action
		    done
		done
	    done
	done
    fi
    #
    # Report Result
    #
    progress_message "   Rule \"$rule\" $DONE."
    save_progress_message_short "   Rule \\\"$rule\\\" added."
}

#
# This function determines the logging for a subordinate action or a rule within a subordinate action
#
merge_levels() # $1=level at which superior action is called, $2=level at which the subordinate rule is called
{
    local superior=$1 subordinate=$2

    set -- $(split $1)

    case $superior in
	*:*:*)
	    case $2 in
		'none!')
		    echo ${subordinate%%:*}:'none!':$3
		    return
		    ;;
		*'!')
                    echo ${subordinate%%:*}:$2:$3
		    return
		    ;;
		*)
		    case $subordinate in
			*:*:*)
			    echo $subordinate
			    return
			    ;;
			*:*)
			    echo $subordinate:$3
			    return
			    ;;
			*)
			    echo ${subordinate%%:*}:$2:$3
			    return
			    ;;
		    esac
		    ;;
	    esac
	    ;;
	*:*)
	    case $2 in
		'none!')
		    echo ${subordinate%%:*}:'none!'
		    return
		    ;;
		*'!')
                    echo ${subordinate%%:*}:$2
		    return
		    ;;
		*)
		    case $subordinate in
			*:*)
			    echo $subordinate
			    return
			    ;;
			*)
			    echo ${subordinate%%:*}:$2
			    return
			    ;;
		    esac
		    ;;
	    esac
	    ;;
	*)
	    echo $subordinate
	    ;;
    esac
}

#
# The next three functions implement the three phases of action processing.
#
# The first phase (process_actions1) occurs before the rules file is processed. ${SHAREDIR}/actions.std
# and ${CONFDIR}/actions are scanned (in that order) and for each action:
#
#      a) The related action definition file is located and scanned.
#      b) Forward and unresolved action references are trapped as errors.
#      c) A dependency graph is created. For each <action>, the variable 'requiredby_<action>' lists the
#         action[:level[:tag]] of each action invoked by <action>.
#      d) All actions are listed in the global variable ACTIONS.
#
# As the rules file is scanned, each action[:level[:tag]] is merged onto the USEDACTIONS list. When an <action>
# is merged onto this list, its action chain is created. Where logging is specified, a chain with the name
# %<action>n is used where the <action> name is truncated on the right where necessary to ensure that the total
# length of the chain name does not exceed 30 characters.
#
# The second phase (process_actions2) occurs after the rules file is scanned. The transitive closure of
# USEDACTIONS is generated; again, as new actions are merged onto this list, their action chains are created.
#
# The final phase (process_actions3) is to traverse the USEDACTIONS list populating each chain appropriately
# by reading the action definition files and creating rules. Note that a given action definition file is
# processed once for each unique [:level[:tag]] applied to an invocation of the action.
#
process_actions1() {

    for inputfile in actions.std actions; do
	while read xaction rest; do
	    [ "x$rest" = x ] || fatal_error "Invalid Action: $xaction $rest"

	    case $xaction in
		*:*)
		    error_message "WARNING: Default Actions are now specified in /etc/shorewall/shorewall.conf"
		    xaction=${xaction%:*}
		    ;;
	    esac

	    [ -z "$xaction" ] && continue

	    [ "$xaction" = "$(chain_base $xaction)" ] || fatal_error "Invalid Action Name: $xaction"

	    if ! list_search $xaction $ACTIONS; then
		f=action.$xaction
		fn=$(find_file $f)

		eval requiredby_${action}=

		if [ -f $fn ]; then
		    progress_message2 "   Pre-processing $fn..."
		    strip_file $f $fn
		    while read xtarget xclients xservers xprotocol xports xcports xratelimit $xuserspec $xmark; do
			temp="${xtarget%%:*}"
			case "$temp" in
			    ACCEPT|DROP|REJECT|LOG|QUEUE|CONTINUE)
				;;
			    COMMENT)
				if [ "$temp" != "$xtarget" ]; then
				    rule="$xtarget $xclients $xservers $xprotocol $xports $xcports $xratelimit $xuserspec $xmark"
				    fatal_error "Invalid TARGET in rule \"$rule\""
				fi
				;;
			    *)
				if list_search $temp $ACTIONS; then
				    eval requiredby=\"\$requiredby_${xaction}\"
				    list_search $xtarget $requiredby || eval requiredby_${xaction}=\"$requiredby $xtarget\"
				else
				    temp=$(map_old_action $temp)

				    case $temp in
					*/*)
					    param=${temp#*/}
					    case $param in
						ACCEPT|DROP|REJECT|LOG|QUEUE|CONTINUE)
						    ;;
						*)
						    rule="$xtarget $xclients $xservers $xprotocol $xports $xcports $xratelimit $xuserspec $xmark"
						    fatal_error "Invalid Macro Parameter in rule \"$rule\""
						    ;;
					    esac
					    temp=${temp%%/*}
					    ;;
				    esac

				    f1=macro.${temp}
				    fn=$(find_file $f1)

				    if [ ! -f $TMP_DIR/$f1 ]; then
					#
					# We must only verify macros once to ensure that they don't invoke any non-standard actions
					#
					if [ -f $fn ]; then
					    strip_file $f1 $fn

					    progress_message "   ..Expanding Macro $fn..."

					    while read mtarget mclients mservers mprotocol mports mcports mratelimit muserspec; do

						[ $mtarget = COMMENT ] && continue

						temp="${mtarget%%:*}"
						case "$temp" in
						    ACCEPT|DROP|REJECT|LOG|QUEUE|CONTINUE|PARAM)
							;;
						    *)
							rule="$mtarget $mclients $mservers $mprotocol $mports $mcports $mratelimit $muserspec"
							fatal_error "Invalid TARGET in rule \"$rule\""
						esac
					    done < $TMP_DIR/$f1

					    progress_message "   ..End Macro"
					else
					    rule="$xtarget $xclients $xservers $xprotocol $xports $xcports $xratelimit $xuserspec $xmark"
					    fatal_error "Invalid TARGET in rule \"$rule\""
					fi
				    fi
				fi
				;;

			esac
		    done < $TMP_DIR/$f
		else
		    fatal_error "Missing Action File: $f"
		fi

		ACTIONS="$ACTIONS $xaction"
	    fi
	done < $TMP_DIR/$inputfile
    done

    for action in $DROP_DEFAULT $REJECT_DEFAULT; do
	case $action in
	    none)
		;;
	    *)
		if list_search $action $ACTIONS; then
		    list_search $action $USEDACTIONS || USEDACTIONS="$USEDACTIONS $action"
		fi
		;;
	esac
    done
}

process_actions2() {

    local interfaces="$(find_interfaces_by_option upnp)"

    if [ -n "$interfaces" ]; then
	if ! list_search forwardUPnP $USEDACTIONS; then
	    error_message "WARNING:Missing forwardUPnP rule (required by 'upnp' interface option on $interfaces)"
	fi
    fi

    progress_message "   Generating Transitive Closure of Used-action List..."

    changed=Yes

    while [ -n "$changed" ]; do
	changed=
	for xaction in $USEDACTIONS; do

	    eval required=\"\$requiredby_${xaction%%:*}\"

	    for xaction1 in $required; do
		#
		# Generate the action that will be passed to process_action by merging the
		# logging specified when the action was invoked with the logging in the
		# invocation of the subordinate action (usually no logging)
		#
		xaction2=$(merge_levels $xaction $xaction1)

		if ! list_search $xaction2 $USEDACTIONS; then
		    #
		    # We haven't seen this one before -- create and record a chain to handle it
		    #
		    USEDACTIONS="$USEDACTIONS $xaction2"
		    createactionchain $xaction2
		    changed=Yes
		fi
	    done
	done
    done
}

#
# process_actions3() is in the compiler. What follows is called from that function when the action
#                    being processed is not a builtin.

process_action3() {

    local f=action.$xaction1 comment=

    progress_message2 "$DOING $(find_file $f) for Chain $xchain..."

    while read xtarget xclients xservers xprotocol xports xcports xratelimit xuserspec xmark; do
	#
	# Generate the target:level:tag to pass to process_action()
	#
	xaction2=$(merge_levels $xaction $xtarget)

	is_macro=
	param=

	xtarget1=${xaction2%%:*}

	case $xtarget1 in
	    ACCEPT|DROP|REJECT|LOG|QUEUE|CONTINUE)
		#
		# Builtin target -- Nothing to do
		#
		;;
	    COMMENT)
		if [ -n "$COMMENTS" ]; then
		    comment=$(echo $xclients $xservers $xprotocol $xports $xcports $xaddress $xratelimit $xuserspec $xmark)
		    save_command COMMENT=\"$comment\"
		else
		    error_message "COMMENT ignored --  requires comment support in iptables/Netfilter"
		fi
		continue
		;;
	    *)
		if list_search $xtarget1 $ACTIONS ; then
		    #
		    # An Action             -- Replace the target from the file
		    #                       -- with the one generated above
		    xtarget=$xaction2
		    #
		    # And locate the chain for that action:level:tag
		    #
		    xaction2=$(find_logactionchain $xtarget)
		else
		    is_macro=yes
		fi
		;;
	esac

	if [ -n "$is_macro" ]; then

	    xtarget1=$(map_old_action $xtarget1)

	    case $xtarget1 in
		*/*)
		    param=${xtarget1#*/}
		    xtarget1=${xtarget1%%/*}
		    ;;
	    esac

	    progress_message "..Expanding Macro $(find_file macro.$xtarget1)..."

	    while read mtarget mclients mservers mprotocol mports mcports mratelimit muserspec; do

		[ $mtarget = COMMENT ] && continue

		mtarget=$(merge_levels $xaction2 $mtarget)

		case $mtarget in
		    PARAM|PARAM:*)
			[ -n "$param" ] && mtarget=$(substitute_action $param $mtarget) || fatal_error "PARAM requires that a parameter be supplied in macro invocation"
			;;
		esac

		if [ -n "$mclients" ]; then
		    case $mclients in
			-|SOURCE)
			    mclients=${xclients}
			    ;;
			DEST)
			    mclients=${xservers}
			    ;;
			*)
			    mclients=$(merge_macro_source_dest $mclients $xclients)
			    ;;
		    esac
		else
		    mclients=${xclients}
		fi

		if [ -n "$mservers" ]; then
		    case $mservers in
			-|DEST)
			    mservers=${xservers}
			    ;;
			SOURCE)
			    mservers=${xclients}
			    ;;
			*)
			    mservers=$(merge_macro_source_dest $mservers $xservers)
			    ;;
		    esac
		else
		    mservers=${xserverss}
		fi

		[ -n "$xprotocol" ]  && [ "x${xprotocol}" != x- ]  && mprotocol=$xprotocol
		[ -n "$xports" ]     && [ "x${xports}" != x- ]     && mports=$xports
		[ -n "$xcports" ]    && [ "x${xcports}" != x- ]    && mcports=$xcports
		[ -n "$xratelimit" ] && [ "x${xratelimit}" != x- ] && mratelimit=$xratelimit
		[ -n "$xuserspec" ]  && [ "x${xuserspec}" != x- ]  && muserspec=$xuserspec

		rule="$mtarget ${mclients:=-} ${mservers:=-} ${mprotocol:=-} ${mports:=-} ${mcports:=-} ${mratelimit:-} ${muserspec:=-} $xmark"
		process_action $xchain $xaction1 $mtarget $mclients $mservers $mprotocol $mports $mcports $mratelimit $muserspec $xmark
	    done < $TMP_DIR/macro.$xtarget1
	    progress_message "..End Macro"
	else
	    rule="$xtarget $xclients $xservers $xprotocol $xports $xcports $xratelimit $xuserspec $xmark"
	    process_action $xchain $xaction1 $xaction2 $xclients $xservers $xprotocol $xports $xcports $xratelimit $xuserspec $xmark
	fi
    done < $TMP_DIR/$f

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

}