#!/bin/sh /etc/rc.common
#Chain Position: Mangle(PREROUTING,OUTPUT) and NAT(POSTROUTING)

############# Failover Process Table
#	WAN case		Failover		No Failover
#(1)WAN disable	man:	-u				mark
#				nat:	Nop				Nop
#(2)WAN down	man:	-u				mark
#				nat:	Nop				Nop
#(3)WAN up		man:	mark			mark
#				nat:	SNAT			SNAT

####### Error code table
#101: Selected WAN profile is not available
#102: IP alias can not be empty
#103: Target WAN IP is not available

START=51

EXTRA_COMMANDS='do_auto_failover'
EXTRA_HELP='	do_auto_failover Handle auto failover when WAN interface down/up event occurs'
IPTABLES=iptables
IPTABLES_CHAIN="nat_addressmap"
UCI_CONFIG="addr_mapv2"
UCI_TMP_PATH="/tmp/.uci/$UCI_CONFIG"
PROFILE_BACKUP="/tmp/profile_backup/addr_map"
CGI_ERROR_MSG="/tmp/cgi_error_msg"

HASHTABLE="$UCI_CONFIG"
BOOT_FILE_MANGLE="/tmp/profile_backup/$UCI_CONFIG/boot_file_mangle"
BOOT_FILE_NAT="/tmp/profile_backup/$UCI_CONFIG/boot_file_nat"
DEBUG_LOG="/tmp/profile_backup/$UCI_CONFIG/debug_log"
FAILOVER_MANGLE="/tmp/profile_backup/$UCI_CONFIG/failover_mangle"
FAILOVER_NAT="/tmp/profile_backup/$UCI_CONFIG/failover_nat"

SCRIPT_LOCK="/tmp/web_apply_lock/addr_map"
BOOT_COMPLETE_LOCK="/tmp/profile_backup/$UCI_CONFIG/BOOT_COMPLETE_LOCK"

pre_rule=0

source /etc/func_htable.sh
#remove blanks in variable
trim() { echo $1;}

setup_parameters() {
	is_legal_rule="yes"
	if [ "$2" = "boot" ] ;then
		config_get status $1 status
		config_get dest $1 dest
		config_get addr_type $1 addr_type
		config_get src_ip_obj $1 src_ip_obj
		config_get src_ip_grp $1 src_ip_grp
		config_get src_ip $1 src_ip
		config_get mask $1 mask
		config_get proto $1 proto
		config_get use_ipalias $1 use_ipalias
		config_get dest_ip $1 dest_ip
		config_get auto_failover $1 auto_failover
		config_get fast_recover $1 fast_recover
	elif [ "$2" = "apply" ] ;then
		status=$(uci get $UCI_CONFIG.$1.status)
		dest=$(uci get $UCI_CONFIG.$1.dest)
		addr_type=$(uci get $UCI_CONFIG.$1.addr_type)
		src_ip_obj=$(uci get $UCI_CONFIG.$1.src_ip_obj)
		src_ip_grp=$(uci get $UCI_CONFIG.$1.src_ip_grp)
		src_ip=$(uci get $UCI_CONFIG.$1.src_ip)
		mask=$(uci get $UCI_CONFIG.$1.mask)
		proto=$(uci get $UCI_CONFIG.$1.proto)
		use_ipalias=$(uci get $UCI_CONFIG.$1.use_ipalias)
		dest_ip=$(uci get $UCI_CONFIG.$1.dest_ip)
		auto_failover=$(uci get $UCI_CONFIG.$1.auto_failover)
		fast_recover=$(uci get $UCI_CONFIG.$1.fast_recover)
	fi
	
	######## UI version upgrade:(object-addr_type, auto-failover, fast-recovery)
	[ -z "$addr_type" ] && {
		addr_type="subnet"
		uci set $UCI_CONFIG.$1.addr_type="subnet"
		ver_upgrade=1
	}
	[ -z "$auto_failover" ] && {
		auto_failover="enable"
		uci set $UCI_CONFIG.$1.auto_failover="enable"
		ver_upgrade=1
	}
	[ -z "$fast_recover" ] && {
		fast_recover="disable"
		uci set $UCI_CONFIG.$1.fast_recover="disable"
		ver_upgrade=1
	}
	
	#Check if route table id exist?
	rt_table_id=`json get policy_rt.table_map.$dest`
	if [ -z "$rt_table_id" ] ;then
		uci set $UCI_CONFIG.$1.status="disable"
		echo -n "Selected WAN profile is not available" >$CGI_ERROR_MSG
		is_legal_rule=101
	else
		#for examing profile is disabled or not
		pool_prof_status=`uci get network.$dest.status`
	fi
	
	flag=""
	if [ "$status" = "disable" ] ;then
		#echo "addr_map: profile($1) is disabled, NOP the rule" >/dev/console
		flag="-u"
		uci set $UCI_CONFIG.$1.failovering=""
	else
		######## G42248 auto-failover: decide whether this rule should be NOP according to the WAN status
		do_failover=0
		#check if target WAN is down
		if [ -n "$rt_table_id" ] ;then
			iface_type=`uci get network.$dest`
			if [ -n "$iface_type" ] ;then
				#iface is a single interface, get it online status (lb_pool is not used in address map)
				iface_status=$(json get network.$dest.connection)
			fi
			#For Failover:If auto_failover is enable and WAN interface is down or disabled => NOP rule
			[ "$auto_failover" = "enable" ] && {
				[ "$pool_prof_status" = "disable" -o "$iface_status" = "down" ] && do_failover=1
			}
			#For Failback:If failback is enable and target WAN is up, set up faliback flag for route(donw->up) for later use
			[ "$auto_failover" = "enable" -a "$iface_status" = "up" -a "$fast_recover" = "enable" ] && fast_recover_flag=1
			
			if [ "$auto_failover" = "enable" ] ;then
				if [ "$do_failover" = "1" ] ;then
					#echo "addr_map(failover): profile($1) WAN($dest) is down, NOP the rule" >/dev/console
					uci set $UCI_CONFIG.$1.failovering="en_active"
					flag="-u"
				else
					uci set $UCI_CONFIG.$1.failovering="en_inactive"
				fi
			else
				uci set $UCI_CONFIG.$1.failovering="disable"
			fi
		fi
	fi
	
	ifname="wan-$dest"
	######## CHECK IP ALIAS ########
	wan_failed=""
	if [ "$pool_prof_status" = "disable" -o "$iface_status" = "down" ] ;then
		wan_failed="true"
	else
		if [ "$use_ipalias" = "disable" ] ;then
			dip=$(json get network.$dest.ipaddr)
			[ -z "$dip" ] && {
				uci set $UCI_CONFIG.$1.status="disable"
				echo -n "Target WAN IP is not available" >$CGI_ERROR_MSG
				is_legal_rule=103
			}
			uci set $UCI_CONFIG.$1.dest_ip=
		else
			if [ -z "$dest_ip" ] ;then
				uci set $UCI_CONFIG.$1.status="disable"
				echo -n "IP alias can not be empty" >$CGI_ERROR_MSG
				is_legal_rule=102
			else
				dip=$(echo $dest_ip | cut -d '/' -f 1)
			fi
		fi
	fi
	
	######## SETUP PROTOCOL ########
	if [ "$proto" = "both" ];then
		protocol="-m service --service-flag tcp,udp"
	else
		protocol=" -p $proto"
	fi
	
	######## CHECK ADDR_TYPE ########
	ipo_set=""
	ipg_set=""
	src_set=""
	if [ "$addr_type" = "subnet" ] ;then
		[ -z "$src_ip" ] || {
			src_set="-s $src_ip/$mask"
		}
		uci set $UCI_CONFIG.$1.src_ip_obj=
		uci set $UCI_CONFIG.$1.src_ip_grp=
	elif [ "$addr_type" = "object" ] ;then
		if [ -n "$src_ip_obj" ] ;then
			for item in $src_ip_obj; do
				ipset -L ipo_$item >/dev/null 2>/dev/null
				[ "$?" = 0 ] && {
					ipo_set=$ipo_set"--set2 ipo_$item src "
				}
			done
		fi
		if [ -n "$src_ip_grp" ] ;then
			for item in $src_ip_grp; do
				ipset -L ipo_$item >/dev/null 2>/dev/null
				[ "$?" = 0 ] && {
					ipg_set=$ipg_set"--set2 ipo_$item src "
				}
			done
		fi
		if [ -n "$ipo_set" -o -n "$ipg_set" ] ;then
			src_set="-m mset2 $ipo_set $ipg_set"
		fi
		uci set $UCI_CONFIG.$1.src_ip=
	fi
}

add_rule() {
	setup_parameters $1 apply
	#echo "add_rule: $uciid, is_legal_rule=$is_legal_rule" >/dev/console
	if [ "$is_legal_rule" = "yes" ] ;then
		$IPTABLES -t mangle -A $IPTABLES_CHAIN $flag $protocol $src_set -j MARK --set-mark $rt_table_id
		if [ "$wan_failed" = "true" ] ;then
			$IPTABLES -t nat -A $IPTABLES_CHAIN -u -j RETURN
		else
			$IPTABLES -t nat -A $IPTABLES_CHAIN $flag $protocol $src_set -o $ifname -m mset ! --set exception_subnet_set dst -j SNAT --to-source $dip
		fi
		[ "$?" = 0 ] && {
			ht_put $HASHTABLE $1 IDX $uciid
			ht_put $HASHTABLE $1 WANRTID $rt_table_id
		}
	else
		$IPTABLES -t mangle -A $IPTABLES_CHAIN -u -j RETURN
		$IPTABLES -t nat -A $IPTABLES_CHAIN -u -j RETURN
		[ "$?" = 0 ] && {
			ht_put $HASHTABLE $1 WANRTID dummy
			ht_put $HASHTABLE $1 IDX $uciid
		}
	fi
}
swap_rule() {
	setup_parameters $1 apply
	#echo "swap_rule: uciid=$uciid, is_legal_rule=$is_legal_rule" >/dev/console
	orig_pos=`ht_get $HASHTABLE $1 IDX`
	#echo "swap_rule: src_set=$src_set, rt_table_id=$rt_table_id" >/dev/console
	#kill old position then insert to new position by the uciid
	if [ "$is_legal_rule" = "yes" ] ;then
		$IPTABLES -t mangle -D $IPTABLES_CHAIN $orig_pos
		$IPTABLES -t nat -D $IPTABLES_CHAIN $orig_pos
		$IPTABLES -t mangle -I $IPTABLES_CHAIN $uciid $flag $protocol $src_set -j MARK --set-mark $rt_table_id
		if [ "$wan_failed" = "true" ] ;then
			$IPTABLES -t nat -I $IPTABLES_CHAIN $uciid -u -j RETURN
		else
			$IPTABLES -t nat -I $IPTABLES_CHAIN $uciid $flag $protocol $src_set -o $ifname -m mset ! --set exception_subnet_set dst -j SNAT --to-source $dip
		fi
	else
		$IPTABLES -t mangle -D $IPTABLES_CHAIN $orig_pos
		$IPTABLES -t nat -D $IPTABLES_CHAIN $orig_pos
		$IPTABLES -t mangle -I $IPTABLES_CHAIN $uciid -u -j RETURN
		$IPTABLES -t nat -I $IPTABLES_CHAIN $uciid -u -j RETURN
	fi
	#find which profile's IDX entry is my new position, swap IDX with it
	target_profile=$(ht_find_word_in_profiles $HASHTABLE IDX $uciid)
	if [ -n "$target_profile" ] ;then
		#target_profile should be only one
		test=`echo "$target_profile" |awk -F" " '{print $2}'`
		if [ -z "$test" ] ;then
			if [ "$target_profile" != "$1" ] ;then
				ht_replace_entry $HASHTABLE $target_profile IDX $orig_pos
				ht_replace_entry $HASHTABLE $1 IDX $uciid
			fi
		fi
	fi
}
rename_rule() {
	ht_rename_file $HASHTABLE $1 $2
}
replace_rule() {
	setup_parameters $1 apply
	#echo "replace_rule: $uciid, is_legal_rule=$is_legal_rule" >/dev/console
	if [ "$is_legal_rule" = "yes" ] ;then
		$IPTABLES -t mangle -R $IPTABLES_CHAIN $uciid $flag $protocol $src_set -j MARK --set-mark $rt_table_id
		if [ "$wan_failed" = "true" ] ;then
			$IPTABLES -t nat -R $IPTABLES_CHAIN $uciid -u -j RETURN
		else
			$IPTABLES -t nat -R $IPTABLES_CHAIN $uciid $flag $protocol $src_set -o $ifname -m mset ! --set exception_subnet_set dst -j SNAT --to-source $dip
		fi
		[ "$?" = 0 ] && ht_replace_entry $HASHTABLE $1 WANRTID $rt_table_id
	else
		$IPTABLES -t mangle -R $IPTABLES_CHAIN $uciid -u -j RETURN
		$IPTABLES -t nat -R $IPTABLES_CHAIN $uciid -u -j RETURN
		[ "$?" = 0 ] && ht_replace_entry $HASHTABLE $1 WANRTID dummy
	fi
}
delete_rule() {
	#echo "delete_rule: $1" >/dev/console
	pos=`ht_get $HASHTABLE $1 IDX`
	ht_rm_file $HASHTABLE $1 2>/dev/null
	#echo "pos=$pos" >/dev/console
	while true ;do
		target_profile=`ht_find_word_in_profiles $HASHTABLE IDX $(($pos+1))`
		if [ -n "$target_profile" ] ;then
			#echo "position ($pos+1) found=> replace $target_profile IDX" >/dev/console
			ht_replace_entry $HASHTABLE $target_profile IDX $pos
			pos=$(($pos+1))
		else
			#echo "position ($pos+1) not found=> break" >/dev/console
			break
		fi
	done
	$IPTABLES -t mangle -D $IPTABLES_CHAIN $uciid
	$IPTABLES -t nat -D $IPTABLES_CHAIN $uciid
}
apply_rule() {
	config_get uciaction $1 uciaction
	config_get uciid $1 uciid
	#rule index offset for the 1st rule
	uciid=$(($uciid+$pre_rule))
	
	case $uciaction in
		add)
			add_rule $1
			;;
		modify)
			#inspect what kind of modification is: position swapping or attribute change
			action_type=`cat $UCI_TMP_PATH`
			echo $action_type |grep '*' >/dev/null
			if [ "$?" = 0 ] ;then
				#a position swapping action
				swap_rule $1
			elif [ $(echo $action_type |grep '@') ] ;then
				#a profile renaming action
				old=`echo $action_type|sed 's/.*\.//'|awk '{FS="="; print $1}'`
				rename_rule $old $1 
			else
				#a normal attribute changing action
				replace_rule $1
			fi
			;;
		delete)
			delete_rule $1
			;;
	esac
}

add_rule_boot() {
	setup_parameters $1 boot
	if [ "$is_legal_rule" = "yes" ] ;then
		if [ -f "$BOOT_FILE_MANGLE" -a -f "$BOOT_FILE_NAT" -a "$fastboot" = 1 ] ;then
			echo "-A $IPTABLES_CHAIN $flag $protocol $src_set -j MARK --set-mark $rt_table_id" >> $BOOT_FILE_MANGLE
			if [ "$wan_failed" = "true" ] ;then
				echo "-A $IPTABLES_CHAIN -u -j RETURN" >> $BOOT_FILE_NAT
			else
				echo "-A $IPTABLES_CHAIN $flag $protocol $src_set -o $ifname -m mset ! --set exception_subnet_set dst -j SNAT --to-source $dip" >> $BOOT_FILE_NAT
			fi
		else
			$IPTABLES -t mangle -A $IPTABLES_CHAIN $flag $protocol $src_set -j MARK --set-mark $rt_table_id
			if [ "$wan_failed" = "true" ] ;then
				$IPTABLES -t nat -A $IPTABLES_CHAIN -u -j RETURN
			else
				$IPTABLES -t nat -A $IPTABLES_CHAIN $flag $protocol $src_set -o $ifname -m mset ! --set exception_subnet_set dst -j SNAT --to-source $dip
			fi
		fi
	else
		#use a dummy rule if parameter is illegal
		if [ -f "$BOOT_FILE_MANGLE" -a -f "$BOOT_FILE_NAT" -a "$fastboot" = 1 ] ;then
			echo "-A $IPTABLES_CHAIN -u -j RETURN" >> $BOOT_FILE_MANGLE
			echo "-A $IPTABLES_CHAIN -u -j RETURN" >> $BOOT_FILE_NAT
		else
			$IPTABLES -t mangle -A $IPTABLES_CHAIN -u -j RETURN
			$IPTABLES -t nat -A $IPTABLES_CHAIN -u -j RETURN
		fi
	fi
	#clean the profile_backup files for regular boot if fastboot is failed
	ht_rm_file $HASHTABLE $1 2>/dev/null
	if [ "$is_legal_rule" = "yes" ] ;then
		ht_put $HASHTABLE $1 IDX $boot_uciid_counter
		ht_put $HASHTABLE $1 WANRTID $rt_table_id
	else
		ht_put $HASHTABLE $1 IDX $boot_uciid_counter
		ht_put $HASHTABLE $1 WANRTID dummy
	fi
	boot_uciid_counter=$(($boot_uciid_counter+1))
}
boot() {
	start
}

start() {
	ht_init $HASHTABLE
	if [ -f $BOOT_FILE_MANGLE -o -f $FAILOVER_MANGLE ] ;then
		rm -f $BOOT_FILE_MANGLE 2>/dev/null
		rm -f $BOOT_FILE_NAT 2>/dev/null
		rm -f $FAILOVER_MANGLE 2>/dev/null
		rm -f $FAILOVER_NAT 2>/dev/null
	fi
	touch $BOOT_FILE_MANGLE
	touch $BOOT_FILE_NAT
	touch $FAILOVER_MANGLE
	touch $FAILOVER_NAT
	
	#pre_rule:
	#$IPTABLES -t mangle -I $IPTABLES_CHAIN -m state --state ESTABLISHED,RELATED -j RETURN
	#$IPTABLES -t nat -I $IPTABLES_CHAIN -m state --state ESTABLISHED,RELATED -j RETURN
	
	fastboot=1
	ver_upgrade=0
	config_load $UCI_CONFIG
	##create rules in mangle and nat
	echo "*mangle" > $BOOT_FILE_MANGLE
	echo "*nat" > $BOOT_FILE_NAT
		boot_uciid_counter=$(($pre_rule+1))
		config_foreach add_rule_boot
	echo "COMMIT" >> $BOOT_FILE_MANGLE
	echo "COMMIT" >> $BOOT_FILE_NAT

	iptables-restore -n < $BOOT_FILE_MANGLE 2>/dev/null
	fastboot_ret1="$?"
	iptables-restore -n < $BOOT_FILE_NAT 2>/dev/null
	fastboot_ret2="$?"
	
	#If fastboot fails, go back to regular boot
	if [ "$fastboot_ret1" != 0 -o "$fastboot_ret2" != 0 ] ;then
		echo "address_map: fast boot failed, use regular boot" >/dev/console
		logger "address_map: fast boot failed, use regular boot"
		fastboot=0
		boot_uciid_counter=$(($pre_rule+1))
		config_foreach add_rule_boot
	fi
	[ "$ver_upgrade" = 1 ] && uci commit $UCI_CONFIG
	[ -f "$BOOT_COMPLETE_LOCK" ] || touch $BOOT_COMPLETE_LOCK
}

stop() {	#restart() reviewed ok (rev.1709)
	$IPTABLES -t mangle -F $IPTABLES_CHAIN 2>/dev/dull
	$IPTABLES -t nat -F $IPTABLES_CHAIN 2>/dev/dull
	#echo "Execute address_mapping restart" >/dev/console
}

apply() {
	lock $SCRIPT_LOCK
	tmpcfg=${UCI_CONFIG}_`cat /proc/uptime | cut -d. -f 1`
	uci fchanges export $UCI_CONFIG > /etc/config/$tmpcfg
	config_load $tmpcfg
	rm -f /etc/config/$tmpcfg
	is_change=`uci fchanges all $UCI_CONFIG`
	
	config_foreach apply_rule
	
	[ -n "$is_change" ] && {
		#Notice: Here flush ALL route cache and conntrack when everytime web do apply
		/usr/sbin/flush_route_cache.sh "address_map"
		conntrack -F >/dev/null 2 >/dev/null
		uci commit $UCI_CONFIG
	}
	lock -u $SCRIPT_LOCK
	
	#Return error code
	if [ "$is_legal_rule" != "yes" ] ;then
		exit $is_legal_rule
	fi
}

######### auto failover ##########
failover_replace_rule() {
	setup_parameters $1 apply
	if [ "$is_legal_rule" = "yes" ] ;then
		if [ "$flag" != "-u" ] ;then
			echo "-R $IPTABLES_CHAIN $uciid $protocol $src_set -j MARK --set-mark $rt_table_id" >> $FAILOVER_MANGLE
			if [ "$wan_failed" = "true" ] ;then
				echo "-R $IPTABLES_CHAIN $uciid -u -j RETURN" >> $FAILOVER_NAT
			else
				echo "-R $IPTABLES_CHAIN $uciid $protocol $src_set -o $ifname -m mset ! --set exception_subnet_set dst -j SNAT --to-source $dip" >> $FAILOVER_NAT
			fi
		else
			echo "-R $IPTABLES_CHAIN $uciid -u -j RETURN" >> $FAILOVER_MANGLE
			echo "-R $IPTABLES_CHAIN $uciid -u -j RETURN" >> $FAILOVER_NAT
		fi
		[ "$?" = 0 ] && ht_replace_entry $HASHTABLE $1 WANRTID $rt_table_id
	else
		echo "-R $IPTABLES_CHAIN $uciid -u -j RETURN" >> $FAILOVER_MANGLE
		echo "-R $IPTABLES_CHAIN $uciid -u -j RETURN" >> $FAILOVER_NAT
		[ "$?" = 0 ] && ht_replace_entry $HASHTABLE $1 WANRTID dummy
	fi
}
do_fast_recovery() {
	if [ "$fast_recover_flag" = "1" -a "$action" = "UP" ] ;then
		#for addressm_ap, it must be a single WAN interface
		default_route_failover_status=`uci get network.default_route.auto_lb`
		if [ "$default_route_failover_status" = "enable" ] ;then
			/usr/sbin/flush_route_cache.sh "address_map"
		else
			default_route=`uci get network.default_route.default`
			if_name=`json get network.$default_route.ifname`
			/usr/sbin/flush_route_cache.sh "address_map" "$if_name"
		fi
		#echo "$UCI_CONFIG(failover): flush all conntrack" >/dev/console
		conntrack -F >/dev/null 2>/dev/null
	fi
}
do_auto_failover() {
	[ -f "$BOOT_COMPLETE_LOCK" ] || return
	lock $SCRIPT_LOCK
	rt_id=$3
	action=$4
	fast_recover_flag=0
	#echo "$UCI_CONFIG(failover): enter (rt_id=$3, action=$4)" >/dev/console
	#find which profile's WANRTID entry contains the target route table id
	target_profile=$(ht_find_word_in_profiles $HASHTABLE WANRTID $rt_id)
	if [ -n "$target_profile" ] ;then
		#echo "$UCI_CONFIG(failover): found matched profile list:$target_profile" >/dev/console
		echo "*mangle" > $FAILOVER_MANGLE
		echo "*nat" > $FAILOVER_NAT
		for pf_name in $target_profile ;do
			uciid=`ht_get $HASHTABLE $pf_name IDX`
			#echo "$UCI_CONFIG(failover): pf_name=$pf_name, uciid=$uciid" >/dev/console
			failover_replace_rule $pf_name
		done
		echo "COMMIT" >> $FAILOVER_MANGLE
		echo "COMMIT" >> $FAILOVER_NAT
		iptables-restore -n < $FAILOVER_MANGLE 2>/dev/null
		iptables-restore -n < $FAILOVER_NAT 2>/dev/null
	fi
	do_fast_recovery
	lock -u $SCRIPT_LOCK
}