#!/bin/sh
#
# A FreeBSD ng_nat backend for PFCFBP (Port Forwarding Configuration
# Frontend-Backend Protocol, draft)
#
# (c) Vadim Goncharov <vadim_nuclight@mail.ru>, 2008.
#
# It is supposed to be launched from inetd on Unix or TCP socket (uses
# stdin/stdout) with ng_nat config properly set up in /etc/rc.conf and/or
# /etc/rc.conf.d/ng_nat (reads nodes names and IP addresses), and then
# avtnatpmpd/miniupnpd/other frontend connecting to that socket.

. /etc/rc.conf
. /etc/rc.conf.d/ng_nat

ngc=/usr/sbin/ngctl
_IPADDR_EXPR_REGEXP='[0-9]\{1,3\}.[0-9]\{1,3\}.[0-9]\{1,3\}.[0-9]\{1,3\}$'

die()
{
	echo '#' ERROR: $*
	sleep 2	# if frontend will restart us via inetd, don't do this too often
	exit 1
}

# Iterate through list and for passed IP address (as $1) return node name, which is
# before address in the list of name-address pairs
ip2nodename()
{
	local ip _prev i

	ip=$1
	_prev=
	for i in $names_and_addrs; do
		if [ "$i" = "$ip" ]; then
			echo $_prev
			return 0
		fi
		_prev=$i
	done

	# not found, output nothing
	return 1
}

# <addrequest>  ::= "ADD" SPACE <proto> SPACE <localip> SPACE <localport> SPACE
#                   <publicip> SPACE <publicport> SPACE <remoteip> SPACE
#                   <remoteport> SPACE <description>
# <proto>       ::= "tcp" | "udp"
# <addreplyok>  ::= "ADDED" SPACE <identifier>
redirect_port()
{
	local id name proto _proto description
	local local_addr local_port alias_addr alias_port remote_addr remote_port

	# first check number of required args and fill variables
	if [ $# -lt 7 ]; then
		echo ERROR CMDSYNTAX
		return 1
	fi

	proto=$1
	local_addr=$2
	local_port=$3
	alias_addr=$4
	alias_port=$5
	remote_addr=$6
	remote_port=$7
	shift 7
	description="$*"

	# check and determine protocol
	if [ "udp" = "$proto" ]; then
		_proto=17
	elif [ "tcp" = "$proto" ]; then
		_proto=6
	else
		echo ERROR CMDSYNTAX
		return 1
	fi

	# check correct syntax of addresses and ports
	if [ `expr "$local_addr" : $_IPADDR_EXPR_REGEXP` -eq 0 -o	\
	     `expr "$alias_addr" : $_IPADDR_EXPR_REGEXP` -eq 0 -o	\
	     `expr "$remote_addr" : $_IPADDR_EXPR_REGEXP` -eq 0 -o	\
	     `expr "$local_port" : "[0-9]\{1,5\}$"` -eq 0 -o		\
	     `expr "$alias_port" : "[0-9]\{1,5\}$"` -eq 0 -o		\
	     `expr "$remote_port" : "[0-9]\{1,5\}$"` -eq 0 ]; then
		echo ERROR CMDSYNTAX
		return 1
	fi

	# Find node name for passed addr - we don't support default
	# address yet
	name=`ip2nodename $alias_addr`
	if [ -z "$name" ]; then
		echo ERROR NOTFOUND
		return 1
	fi

	# Trim too long description or netgraph will complain on otherwise
	# correct redirection
	if [ ${#description} -gt 63 ]; then
		description=`echo $description | /usr/bin/cut -c 1-63`
	fi

	# Add redirection and obtain it's ID
	id=`$ngc msg $name: redirectport \{ proto=$_proto		\
		local_addr=$local_addr local_port=$local_port		\
		alias_addr=$alias_addr alias_port=$alias_port 		\
		remote_addr=$remote_addr remote_port=$remote_port	\
		description=\"$description\" \} | sed -E '1d;s/^Args:[[:space:]]*([0-9]+)$/\1/g'`

	if [ -z "$id" ]; then
		echo ERROR OPFAILED
		return 1
	else
		echo ADDED ${name}_$id
	fi
}

# <deleterequest> ::= "DELETE" SPACE <identifier>
# <deletereply>   ::= "DELETED" SPACE <identifier>
delete_redirect()
{
	local node_name id

	# check that identifier passed in argument is correct for us
	if [ `expr "$1" : "[A-Za-z_][A-Za-z0-9_]\{0,29\}_[0-9]\{1,8\}"` -eq 0 ]; then
		echo ERROR CMDSYNTAX
		return 1
	fi

	# we use identifiers as NAME_ID, but name could contain another '_',
	# while our IDs can't
	id=${1##*_}
	node_name=${1%_*}

	$ngc msg $node_name: redirectdelete $id >/dev/null 2>&1

	if [ $? -eq 0 ]; then
		echo DELETED $1
	else
		echo ERROR NOTFOUND
	fi
}

# <listreplyentry> ::= "LIST" SPACE <identifier> SPACE <proto> SPACE
#                      <localip> SPACE <localport> SPACE <publicip> SPACE
#                      <publicport> SPACE <remoteip> SPACE <remoteport> SPACE
#                      <description>
list_redirects()
{
	local local_addr local_port alias_addr alias_port remote_addr lsnat
	local node_name id proto description total_count _line remote_port

	for node_name in ${ng_nat_nodes}; do
		$ngc msg $node_name: listredirects | tr -s '{}[]' ' \n\n ' | \
		sed -E '1,2d;s/Args.*$//g;/^$/d' | \
		while read _line; do
			[ -z "$_line" ] && continue	# suppress empty lines
			id=0
			local_addr=0.0.0.0
			local_port=0
			alias_addr=0.0.0.0
			alias_port=0
			remote_addr=0.0.0.0
			remote_port=0
			proto=0
			lsnat=0
			description=

			eval "`echo $_line | sed -E 's/=[0-9]+ /&\
/g'`"
			# Frontend knows only TCP and UDP, skip others
			if [ "$proto" -eq 6 ]; then
				proto=tcp
			elif [ "$proto" -eq 17 ]; then
				proto=udp
			else
				continue
			fi

			# finally output current entry for this node
			echo LIST ${node_name}_$id "$proto" "$local_addr" "$local_port"	\
				"$alias_addr" "$alias_port" "$remote_addr" "$remote_port" \
				"$description"
		done
	done

	echo ENDLIST
}

get_ip_list()
{
	local ip flag

	ip=$1
	flag=0
	for ip in $names_and_addrs; do
		if [ "$flag" -eq 0 ]; then
			flag=1
		else
			flag=0
		fi
		[ "$flag" -eq 0 ] && echo IPLIST $ip
	done

	echo ENDIPLIST
	return 0
}

### Main script start, prepare...

if [ -z "${ng_nat_nodes}" ]; then
	die '$ng_nat_nodes is not set'
fi

# Set mapping variables for public IPs to node names
for node_name in ${ng_nat_nodes}; do
	eval iface=\"\$ng_nat_${node_name}_interface\"
	if [ -z "${iface}" ]; then
		eval die \'\$ng_nat_${node_name}_interface is not set\'
	fi

	if [ `expr "${iface}" : $_IPADDR_EXPR_REGEXP` -ne 0 ]; then
		alias_addr=${iface}
		eval ng_nat_${node_name}_alias_addr=${iface}
	else
		alias_addr=$(ifconfig ${iface} | sed -Ene \
			'/^[[:space:]]*inet ([0-9.]*) .*$/{s//\1/;p;q' -e '}')
	fi

	if [ `expr "${alias_addr}" : $_IPADDR_EXPR_REGEXP` -eq 0 ]; then
		die "could not determine alias address for node ${node_name}"
		return 1
	fi

	# append pair to list
	names_and_addrs="$names_and_addrs $node_name $alias_addr"
done

### Ready to serve, start main loop

CR=`echo -e \\\r` # for script to be transferable, we don't store literal CR
while read cmd cmdargs; do
	# strip trailing CR LF from command and/or arguments; in fact, LF was
	# already stripped by shell, so we strip only possible CR character
	cmd=${cmd%$CR}
	[ -n "$cmdargs" ] && cmdargs=${cmdargs%$CR}
	[ -z "$cmd" ] && continue	# ignore empty lines
	
	case $cmd in
	'#'*)		# ignore comments
		;;
	ADD)
		redirect_port $cmdargs
		;;
	DELETE)
		delete_redirect $cmdargs
		;;
	LIST)
		list_redirects
		;;
	CAPABILITIES)
		echo CAPABILITIES GETIPLIST
		;;
	GETIPLIST)
		get_ip_list
		;;
	*)
		echo ERROR CMDSYNTAX
		;;
	esac
done

