#!/bin/bash
# etcnet firewall handler

# Known issues:
# 1. When interface is 'all' we can't guess correct interfaces processing order
#    like etcnet does so process it by bash sort order
# 2. Guessing may not work correct

# default values
NAME=default
TABLE=filter
CHAIN=
MYIFACEDIR=
TABLES="mangle filter nat"
ACTIONS="start stop restart load unload reload flush show list count counters rule new create remove delete zero policy rename"

usage()
{
	echo '/etc/net FireWall handler'
	echo "Usage: $0 [iface] [table] [chain] <action> [iptables rule or action options]" >&2
	echo ""
	echo " iface    - 'default' (by default), real interface name or 'all' for all interfaces"
	echo " table    - 'mangle', 'filter' (by default), 'nat' or 'all' for all tables"
	echo " chain    -  system or user defined chain name (case sensitive!) or 'all' for all chains"
	echo " action   - 'start','stop','restart','load','unload','reload','flush','show|list','count|counters','rule','new|create','remove|delete','zero','policy','rename'"
	echo ""
	echo " start    - process all tables and chains for given interface (even when chain or table is defined)"
	echo " stop     - process all tables and chains for given interface (even when chain or table is defined)"
	echo " restart  - equivalent to 'stop' then 'start'"
	echo " load     - load rules for given interface, table and chain"
	echo " unload   - unload rules for given interface, table and chain"
	echo " reload   - equivalent to 'unload' then 'load'"
	echo " flush    - flush rules for given interface, table and chain"
	echo " show     - list rules for given interface, table and chain"
	echo " list     - same as 'show'"
	echo " count    - show counters for given table and chains"
	echo " counters - same as 'count'"
	echo " rule     - parse rule and pass it to iptables"
	echo " new      - create new chain"
	echo " create   - same as 'new'"
	echo " remove   - remove chain"
	echo " delete   - same as 'remove'"
	echo " zero     - zero packet and byte counters in chain"
	echo " policy   - set default policy for chain"
	echo " rename   - rename chain"
	echo ""
	echo "Options for action 'show' or 'list':"
	echo " -n or numeric - numeric output for IP addresses, ports and services"
	echo " -v or verbose - verbose output of rules"
	echo " -x or exact   - expand numbers instead of rounded numbers"
	echo " --line-numbers or lines - display line number for each rule" 
	echo ""
	exit 1
}



guess_options()
{
	# Four options. Get interface name and shift
	[ $# -ge 4 ] && 
	{
		NAME="$1"
		[ "$NAME" != "default" ] && [ "$NAME" != "all" ] && 
		{
			[ -d $IFACEDIR/$NAME@$NETHOST ] && MYIFACEDIR=$IFACEDIR/$NAME@$NETHOST || MYIFACEDIR=$IFACEDIR/$NAME
			[ -d "$MYIFACEDIR" ] || {
				print_error "interface configuration directory $MYIFACEDIR not found"
				exit 1
			}
		}
		[ "$2" = "all" ] || egrep -q "([^-]\b|^)$2(\b[^-]|$)" < <(echo "$TABLES") && TABLE="$2" ||
		{
			print_error "Unknown firewall table $2"
			exit 1
		}
		CHAIN="$3"
		egrep -q "([^-]\b|^)$4(\b[^-]|$)" < <(echo "$ACTIONS") && ACTION="$4" ||
		{
			print_error "Unknown firewall action $4"
			exit 1
		}
		return
	}

	# Three options. Guess first and shift
	[ $# -eq 3 ] && 
	{
			[ "$1" != "default" ] && [ "$1" != "all" ]  && 
			{
				# May be it's and interface name
				[ -d $IFACEDIR/$1@$NETHOST ] && MYIFACEDIR=$IFACEDIR/$1@$NETHOST || MYIFACEDIR=$IFACEDIR/$1
				[ -d "$MYIFACEDIR" ] &&
				{
					# Yes, it's an interface name
					NAME="$1"
					{
						[ "$2" = "all" ] || egrep -q "([^-]\b|^)$2(\b[^-]|$)" < <(echo "$TABLES") && TABLE="$2"
					} || CHAIN="$2"
				} ||
				{
					# No, it's not an interface name
					MYIFACEDIR=
					[ "$1" = "all" } || egrep -q "([^-]\b|^)$1(\b[^-]|$)" < <(echo "$TABLES") && 
					{
						TABLE="$1"
						CHAIN="$2"
					} || 
					{
						print_error "Unknown firewall table $1"
						exit 1
					}
				}
			} ||
			{
				# Yes, it's an interface name or 'all'
				NAME="$1"
				{
					[ "$2" = "all" ] || egrep -q "([^-]\b|^)$2(\b[^-]|$)" < <(echo "$TABLES") && TABLE="$2"
				} || CHAIN="$2"
			}
			shift 2
	}

	# Two options. Guest first and shift
	[ $# -eq 2 ] && 
	{
		[ "$1" != "default" ] && [ "$1" != "all" ] && 
			{
				# May be it's and interface name
				[ -d $IFACEDIR/$1@$NETHOST ] && MYIFACEDIR=$IFACEDIR/$1@$NETHOST || MYIFACEDIR=$IFACEDIR/$1
				[ -d "$MYIFACEDIR" ] &&
				{
					# Yes, it's an interface name
					NAME="$1"
				} ||
				{
					# No
					MYIFACEDIR=
					{
						[ "$1" = "all" ] || egrep -q "([^-]\b|^)$1(\b[^-]|$)" < <(echo "$TABLES") && TABLE="$1"
					} || CHAIN="$1"
				}
			} ||
			{
				# Yes, it's an interface name or 'all'
				NAME="$1"
			}
			shift
	
		}
	# One option
	[ $# -eq 1 ] &&
	{
		egrep -q "([^-]\b|^)$1(\b[^-]|$)" < <(echo "$ACTIONS") && ACTION=$1 ||
		{
			print_error "Unknown firewall action $1"
			exit 1
		}
	}
}


iptables_local_rules_from_file()
{
	[ "$TABLE" = "all" ] && TABLE="*"
	[ "$CHAIN" = "all" ] && CHAIN="*"
	
	for CTABLE in $TABLE; do
		[ -d "$CTABLE" ] || continue
		for CCHAIN in "$CTABLE"/$CHAIN; do
			CCHAIN=`basename $CCHAIN`
			[ "$CTABLE/$CCHAIN" = "$CTABLE/loadorder" ] || [ ! -f "$CTABLE/$CCHAIN" ] || [ ! -s "$CTABLE/$CCHAIN" ] && continue
			[ "$1" = "unload" ] && iptables_flush_rules_from_file "$CTABLE" "$CCHAIN" "$2" \
			|| iptables_load_rules_from_file "$CTABLE" "$CCHAIN"
		done
	done
}

iptables_local_rule()
{
	local RULE
	[ "$TABLE" = "all" ] && TABLE="*"
	[ "$CHAIN" = "all" ] && CHAIN="*"
	RULE=`iptables_expand_string $@`
	[ -z "$RULE" ] && return
	for CTABLE in $TABLE; do
		[ -d "$CTABLE" ] || continue
		for CCHAIN in "$CTABLE"/$CHAIN; do
			CCHAIN=`basename $CCHAIN`
			[ "$CTABLE/$CCHAIN" = "$CTABLE/loadorder" ] && continue
			read FIRST SECOND REST < <(echo $RULE)
		    	case $FIRST in
					"-A"|"-D")
		        		iptables_push_rule $FIRST $CCHAIN -t $CTABLE $SECOND $REST
						;;
					"-P")
		 	      		iptables_chain_policy `echo $CCHAIN $SECOND $REST -t $CTABLE|sed -e 's/-j//g'`
						;;
					"-I")
						case $SECOND in
						    [0-9]*)
			        			iptables_push_rule $FIRST $CCHAIN $SECOND -t $CTABLE $REST
							    ;;
							*)
			        			iptables_push_rule $FIRST $CCHAIN -t $CTABLE $SECOND $REST
								;;
						esac
						;;					    
					*)
		        		iptables_push_rule $CCHAIN -t $CTABLE $FIRST $SECOND $REST
						;;
				esac
		done
	done
}

iptables_local_create_chain()
{
	[ "$TABLE" = "all" ] && TABLE=$TABLES
	for CTABLE in $TABLE; do
		iptables_create_chain $CHAIN -t $CTABLE
	done
}

iptables_local_delete_chain()
{
	[ "$TABLE" = "all" ] && TABLE=$TABLES
	for CTABLE in $TABLE; do
		# We can delete only user-defined chains so just don't pass chain name
		# in case of 'all' chain
		[ "$CHAIN" = "all" ] && iptables_delete_chain -t $CTABLE || iptables_delete_chain $CHAIN -t $CTABLE
	done
}

iptables_local_list_chain()
{
	local LIST_ARGS
	[ $# -eq 0 ] || 
	{
		iptables_preload
		LIST_ARGS=`iptables_expand_string $@`
	}
	[ "$TABLE" = "all" ] && TABLE=$TABLES
	for CTABLE in $TABLE; do
		printf "\nTable $CTABLE\n\n"
		[ "$CHAIN" = "all" ] && iptables_list_chain -t $CTABLE $LIST_ARGS || iptables_list_chain $CHAIN -t $CTABLE $LIST_ARGS
	done
}

iptables_local_zero_chain()
{
	[ "$TABLE" = "all" ] && TABLE=$TABLES
	for CTABLE in $TABLE; do
		[ "$CHAIN" = "all" ] && iptables_zero_chain -t $CTABLE || iptables_zero_chain $CHAIN -t $CTABLE
	done
}

iptables_local_rename_chain()
{
	[ "$TABLE" = "all" ] && TABLE=$TABLES
	for CTABLE in $TABLE; do
		iptables_rename_chain $CHAIN $@ -t $CTABLE
	done
}

# Be verbose in any way
export VERBOSE=on
export PROGRESS=on

if [ ! -s ${SCRIPTDIR:=/etc/net/scripts}/functions ]; then
        echo "Error: $SCRIPTDIR/functions not found!"
		exit 1
fi

. ${SCRIPTDIR:=/etc/net/scripts}/functions
pickup_defaults

if [ ! -s ${SCRIPTDIR:=/etc/net/scripts}/functions-fw ]; then
        echo "Error: $SCRIPTDIR/functions-fw not found!"
		exit 1
fi

. ${SCRIPTDIR:=/etc/net/scripts}/functions-fw

[ -z "$NETPROFILE" ] && init_netprofile

[ -z "$CONFIG_FW" ] && {
	SourceIfNotEmpty `profiled_filename /etc/net/ifaces/default/options`
	SourceIfNotEmpty `profiled_filename /etc/net/ifaces/default/fw/options`
}

if  ! is_yes "$CONFIG_FW";  then
	print_message "Firewall is disabled"
	exit 1
fi

[ $# -lt 1 ] && usage || guess_options $@

echo "interface is $NAME"
echo "table is $TABLE"
echo "chain is $CHAIN"
echo "action is $ACTION"
echo ""

[ "$NAME" = "all" ] && NAME="*"

for IFACE in "$IFACEDIR"/$NAME; do
	[ -d "$IFACE" ] || continue
	NAME=`basename $IFACE`
	[ -z "$MYIFACEDIR" ] && {
		[ -d $IFACEDIR/$NAME@$NETHOST ] && MYIFACEDIR=$IFACEDIR/$NAME@$NETHOST || MYIFACEDIR=$IFACEDIR/$NAME
	}
	SourceIfNotEmpty "$MYIFACEDIR/fw/options"
	case "$FW_TYPE" in
		iptables)
		 	[ ! -d "$MYIFACEDIR/fw/$FW_TYPE" ] &&
			{
				 MYIFACEDIR=""
				 continue
			}
			# Load own interface syntax if exists
			[ "$NAME" != "default" ] && is_yes "$IPTABLES_HUMAN_SYNTAX" && {
				[ -f "$MYIFACEDIR/fw/$FW_TYPE/syntax" ] && [ -s "$MYIFACEDIR/fw/$FW_TYPE/syntax" ] && {
					export IPTABLES_SYNTAX_DIR="$MYIFACEDIR/fw/$FW_TYPE"
					export IPTABLES_SYNTAX=
					export IPTABLES_SED_RULES=
				}
			}
			case "$ACTION" in
				start)
					iptables_preload
					iptables_start "$NAME"
					;;
				stop)
					iptables_preload
					iptables_stop "$NAME"
					;;
				restart)
					iptables_preload
					iptables_stop "$NAME"
					iptables_start "$NAME"
					;;
				load)
					iptables_preload
					cd $MYIFACEDIR/fw/$FW_TYPE/
					iptables_local_rules_from_file
					;;
				unload)
					iptables_preload
					cd $MYIFACEDIR/fw/$FW_TYPE/
					iptables_local_rules_from_file unload
					;;
				reload)
					iptables_preload
					cd $MYIFACEDIR/fw/$FW_TYPE/
					iptables_local_rules_from_file unload
					iptables_local_rules_from_file
					;;
				flush)
					iptables_preload
					cd $MYIFACEDIR/fw/$FW_TYPE/
					iptables_local_rules_from_file unload flush
					;;
				show|list)
					case $# in
						1)
							shift 1
							;;
						2)
							shift 2
							;;
						3)
							shift 3
							;;
						*)
							shift 4
							;;
					esac
					iptables_local_list_chain $@
					;;
				count|counters)
					echo "Not implemented yet"
					exit 1
					;;
				rule)
					case $# in
						1)
							shift 1
							;;
						2)
							shift 2
							;;
						3)
							shift 3
							;;
						*)
							shift 4
							;;
					esac
					iptables_preload
					cd $MYIFACEDIR/fw/$FW_TYPE/
					iptables_local_rule "$@"
					;;
				new|create)
					iptables_local_create_chain
					;;
				remove|delete)
					iptables_local_delete_chain
					;;
				zero)
					iptables_local_zero_chain
					;;
				policy)
					case $# in
						1)
							shift 1
							;;
						2)
							shift 2
							;;
						3)
							shift 3
							;;
						*)
							shift 4
							;;
					esac
					iptables_preload
					cd $MYIFACEDIR/fw/$FW_TYPE/
					iptables_local_rule "-P $@"
					;;
				rename)
					case $# in
						1)
							shift 1
							;;
						2)
							shift 2
							;;
						3)
							shift 3
							;;
						*)
							shift 4
							;;
					esac
					iptables_local_rename_chain $@
					;;
				*)
					usage
					;;
			esac
			MYIFACEDIR=
			;;
		*)
			echo "Firewall type $FW_TYPE isn't supported"
	esac
done
