#!/usr/local/bin/cbsd
#v13.0.11
MYARG=""
MYOPTARG="in inaddr jname mode out outaddr proto"
MYDESC="Exposing a port (port forwarding) to env via IPFW or PF"
ADDHELP="

${H3_COLOR}Description${N0_COLOR}:

 By command 'cbsd expose' you can create forward rule for tcp/udp
 port from external IP (default with 'nodeip' from 'cbsd initenv-tui'
 to bhyve or jail.
 The RDR/FWD rules are generated for NAT framework, 
 which you chose through 'cbsd natcfg'. For 'pf', nat rules file
 located in ~cbsd/etc/pf.conf. For 'ipfw' rules applied in runtime
 and have a comment 'Setup by CBSD expose.'. Each '[jb]stop' or '[jb]start'
 sequence trigger 'expose' script every time (when the environment has rules).

${H3_COLOR}Options${N0_COLOR}:

 ${N2_COLOR}in${N0_COLOR}      - master port for incoming connection.
 ${N2_COLOR}inaddr${N0_COLOR}  - use IP as nodeip (for incoming connection),
           default is '0' - inherit \${nodeip} ( 'cbsd initenv-tui' to change ).
           This is useful when the env is migrating between different hosts 
           (with different nodes). Or set a fixed IPv4 address.
 ${N2_COLOR}jname${N0_COLOR}   - environment name.
 ${N2_COLOR}mode${N0_COLOR}    - add,delete,apply,clear,flush,list:
  - add    : add and apply one rule, e.g: in=222 out=22 proto=tcp;
  - delete : delete and clear one rule, e.g: in=222 out=22;
  - apply  : apply all rules from database;
  - clear  : clear all rules from datatase;
  - flush  : clear and remove all rules;
 ${N2_COLOR}out${N0_COLOR}     - (optional) destination port inside jail.
 ${N2_COLOR}outaddr${N0_COLOR} - use IP as destination address, do not
           use jail/bhyve IPs.
 ${N2_COLOR}proto${N0_COLOR}   - udp, tcp. default: tcp.

${H3_COLOR}Examples${N0_COLOR}:

 Forward all incoming traffic to \$nodeip:2233 to foo:22 jail:

 # cbsd expose mode=add in=2233 out=22 jname=foo

  Or via CBSDfile (the 'jname=' args can be ommited):
--
jail_foo()
{
}

postcreate_foo()
{
    expose mode=add in=2233 out=22
}
--

"
CBSDMODULE="bhyve,jail"
EXTHELP="wf_expose"


. ${subrdir}/nc.subr
cloud_api=0
. ${cbsdinit}

if [ -z "${jname}" -a -n "${ojname}" ]; then
	# inherit jname env
	jname="${ojname}"
fi

# check for cloud function when CBSDfile exist
Makefile="${CBSD_PWD}/CBSDfile"
if [ -r ${Makefile} ]; then
	[ -z "${CBSDFILE_RECURSIVE}" ] && ${ECHO} "${N1_COLOR}found CBSDfile: ${N2_COLOR}${Makefile}${N0_COLOR}" 1>&2
	. ${Makefile}
	all_jail_list=$( ${EGREP_CMD} '^jail_[a-zA-Z0-9_@%:][-a-zA-Z0-9_@%:]*\(\)$' ${Makefile} | ${XARGS_CMD} | ${TR_CMD} -d "()" | ${SED_CMD} s#jail_##g )
	all_bhyve_list=$( ${EGREP_CMD} '^bhyve_[a-zA-Z0-9_@%:][-a-zA-Z0-9_@%:]*\(\)$' ${Makefile} | ${XARGS_CMD} | ${TR_CMD} -d "()" | ${SED_CMD} s#bhyve_##g )

	if [ -n "${CLOUD_URL}" -a -n "${CLOUD_KEY}" ]; then
		cbsd_api=1
	else
		cbsd_api=0
	fi

	if [ -n "${jname}" ]; then
		found=0
		for i in ${all_jail_list} ${all_bhyve_list}; do
		if [ "${i}" = "${jname}" ]; then
			found=1
			break
		fi
		done
		[ ${found} -eq 0 ] && err 1 "${N1_COLOR}${CBSD_APP}: no such env: ${N2_COLOR}${jname}${N0_COLOR}"
	fi
else
	#jname="${all_jail_list}"
	cbsd_api=0
fi

if [ ${cbsd_api} -eq 1 ]; then
	err 0 "${N1_COLOR}${CBSD_APP}: expose for API/remote not supported yet, skip for expose rule: ${N2_COLOR}mode=${mode} in=${in}${N0_COLOR}"
fi

show_all_expose()
{
	local _jname _file

	cbsdsqlro local "SELECT jname FROM jails ASC" | while read _jname; do
		_file="${jailsysdir}/${_jname}/expose.sqlite"
		[ ! -r ${_file} ] && continue
		${ECHO} "${N1_COLOR}Expose for ${N2_COLOR}${_jname}${N1_COLOR}:${N0_COLOR}" 1>&2
		fw_expose_list ${_file}
	done
}

get_first_ip()
{
	local IFS=","
	local ip IWM

	for ip in ${ip4_addr}; do
		ipwmask "${ip}"
		if [ -n "${IWM}" ]; then
			echo "${IWM}"
			return 0
		fi
	done
}

get_first_fwnum()
{
	local tmp
	unset fwnum

	tmp=$( for i in $( ${SEQ_CMD} ${fwexpose_st} ${fwexpose_end} ); do
		${IPFW_CMD} list ${i} > /dev/null 2>&1
		[ $? -eq 0 ] && continue
		echo ${i}
		break
	done )

	[ -z "${tmp}" ] && err 1 "${N1_COLOR}Unable to determine first fwnum for expose${N0_COLOR}"
	[ ${tmp} -eq ${fwexpose_end} ] && err 1 "${N1_COLOR}No free ipfw num for expose in expose range: ${N2_COLOR}${fwexpose_st} - ${fwexpose_end}${N0_COLOR}"

	fwnum="${tmp}"
}

pf_del()
{
	[ -z "${COMMENT}" ] && ${ECHO} "${N1_COLOR}No comment in pf_del${N0_COLOR}" && return 1

	if [ -f "${etcdir}/pfnat.conf" ]; then
		if ${GREP_CMD} "${COMMENT}" ${etcdir}/pfnat.conf >/dev/null 2>&1; then
			${CP_CMD} -a ${etcdir}/pfnat.conf ${tmpdir}/pfnat.conf
			${GREP_CMD} -v "${COMMENT}" ${tmpdir}/pfnat.conf | ${GREP_CMD} "." > ${etcdir}/pfnat.conf
			${RM_CMD} -f ${tmpdir}/pfnat.conf
		fi
	fi
}

fw_expose_add()
{
	local _ret _inaddr

	[ "${inaddr}" = "nodeip" ] && inaddr="0"

	if [ -z "${inaddr}" ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP}: inaddr not IPv4 address, skip: ${N2_COLOR}${inaddr}${N0_COLOR}" 1>&2
		return 1
	fi

	if [ "${inaddr}" = "0" ]; then
		_inaddr="${nodeip}"
	else
		_inaddr="${inaddr}"
	fi

	ipwmask ${_inaddr}
	iptype ${IWM}
	_ret=$?

	if [ ${_ret} -ne 1 ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP}: inaddr not IPv4 address, skip: ${N2_COLOR}${_inaddr}${N0_COLOR}" 1>&2
		return 1
	fi

	if [ -z "${jip}" -o "${jip}" = "0" ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP}: outaddr not IPv4 address, skip: ${N2_COLOR}${jip}${N0_COLOR}" 1>&2
		return 1
	fi

	ipwmask ${jip}
	iptype ${IWM}
	_ret=$?

	if [ ${_ret} -ne 1 ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP}: outaddr not IPv4 address, skip: ${N2_COLOR}${jip}${N0_COLOR}" 1>&2
		return 1
	fi

	res=$( ${NC_CMD} -w1 -z ${_inaddr} ${in} 2>/dev/null )

	if [ $? -eq 0 ]; then
		${ECHO} "${N1_COLOR}port already in use on ${_inaddr}: ${N2_COLOR}${in}${N0_COLOR}"
		return 1
	fi

	[ -f ${ftmpdir}/${jname}-expose_fwnum ] && fwnum=$( ${CAT_CMD} ${ftmpdir}/${jname}-expose_fwnum )

	${ECHO} "${N1_COLOR}CBSD Expose for ${jname}: ${N2_COLOR}${in} -> ${out} (${proto})${N0_COLOR}"
	cbsdlogger NOTICE ${CBSD_APP}: ${jname}: proto: ${proto}: ${in}:${out}

	case "${nat_enable}" in
		pf)
			pf_del
			${CAT_CMD} >> ${etcdir}/pfnat.conf << EOF
rdr pass proto ${proto} from any to ${_inaddr} port ${in} -> ${jip} port ${out} # ${COMMENT}
EOF
			# reload rule
			naton
			;;
		*)
			if [ ${freebsdhostversion} -gt 1100120 ]; then
				${IPFW_CMD} add ${fwnum} fwd ${jip},${out} ${proto} from any to ${_inaddr} ${in} in ${COMMENT}
			else
				${IPFW_CMD} add ${fwnum} fwd ${jip},${out} ${proto} from any to ${_inaddr} ${in} in
			fi
			echo "${fwnum}" >"${ftmpdir}/${jname}-expose_fwnum"
			;;
	esac

	return 0
}

fw_expose_apply()
{
	cbsdsqlro ${exposefile} SELECT pin,pout,proto,inaddr FROM expose | ${TR_CMD} "|" " " | while read in out proto inaddr; do
		COMMENT="// Setup by CBSD expose: ${proto}-${out}-${jname}"
		fw_expose_add
	done
}

# if $1 than use it as exposefile
fw_expose_list()
{
	[ -n "${1}" ] && exposefile="${1}"

	[ ! -r ${exposefile} ] && return 1

	cbsdsqlro ${exposefile} SELECT pin,pout,proto,inaddr FROM expose | ${TR_CMD} "|" " " | while read in out proto inaddr; do
		echo "${in} -> ${out} (via ${inaddr} ${proto})"
	done
}

fw_expose_clear()
{
	case "${nat_enable}" in
		pf)
			[ ! -f ${etcdir}/pfnat.conf ] && return 0
			if ${GREP_CMD} -E "([tcp][udp])-([[:digit:]]{1,5})-${jname}"$ ${etcdir}/pfnat.conf 2>&1; then
				${CP_CMD} -a ${etcdir}/pfnat.conf ${tmpdir}/pfnat.conf
				cbsdlogger NOTICE ${CBSD_APP}: fw_expose_clear for ${jname}: via pfnat
				${GREP_CMD} -E -v "([tcp][udp])-([[:digit:]]{1,5})-${jname}"$ ${tmpdir}/pfnat.conf | ${GREP_CMD} "." > ${etcdir}/pfnat.conf
				naton
			fi
			;;
		ipfw)
			if [ ! -f ${ftmpdir}/${jname}-expose_fwnum ]; then
				return 0
			else
				fwnum=$( ${CAT_CMD} ${ftmpdir}/${jname}-expose_fwnum )
			fi
			cbsdlogger NOTICE ${CBSD_APP}: fw_expose_clear for ${jname}: delete ipfw rule ${fwnum}
			${IPFW_CMD} delete ${fwnum}
			;;
	esac
}

fw_expose_delete()
{
	[ "${inaddr}" = "nodeip" ] && inaddr="${nodeip}"

	cbsdsqlrw ${exposefile} "DELETE FROM expose WHERE pin=${in} AND pout=${out} AND proto=\"${proto}\" AND inaddr=\"${inaddr}\" AND outaddr=\"${outaddr}\""
	cbsdlogger NOTICE ${CBSD_APP}: fw_expose_delete: delete config for: ${jname}

	case "${nat_enable}" in
		pf)
			pf_del
			# reload pf
			naton
			;;
		*)
			if [ ! -f ${ftmpdir}/${jname}-expose_fwnum ]; then
				${ECHO} "${N1_COLOR}No ${ftmpdir}/${jname}-expose_fwnum: skip for deletion expose rule${N0_COLOR}"
				return 0
			else
				fwnum=$( ${CAT_CMD} ${ftmpdir}/${jname}-expose_fwnum )
			fi
			${IPFW_CMD} delete ${fwnum}
			;;
	esac
}


# MAIN
if [ -z "$1" ]; then
	show_all_expose
	exit 0
fi

[ -z "${jname}" ] && err 1 "${N1_COLOR}${CBSD_APP}: give me jname${N0_COLOR}"

. ${subrdir}/rcconf.subr
[ $? -eq 1 ] && err 1 "${N1_COLOR}no such jail: ${N2_COLOR}${jname}${N0_COLOR}"

[ -z "${proto}" ] && proto="tcp"
[ -z "${inaddr}" ] && inaddr="nodeip"  # "nodeip"  - reserverd word for $nodeip variable
if [ "${nat_enable}" = "ipfw" ]; then
	[ "$( ${SYSCTL_CMD} -qn net.inet.ip.fw.enable 2>/dev/null )" != "1" ] && err 1 "${N1_COLOR}IPFW is not enabled${N0_COLOR}"
fi
# init ipfw number
get_first_fwnum
[ -z "${fwnum}" ] && err 1 "${N1_COLOR}Empty fwnum variable${N0_COLOR}"

if [ -z "${outaddr}" ]; then
	jip=$( get_first_ip )
else
	jip="${outaddr}"
fi

[ -z "${jip}" ] && err 1 "${N1_COLOR}${CBSD_APP}: unable to determine jail ip: ${N2_COLOR}${jname}${N0_COLOR}"

exposefile="${jailsysdir}/${jname}/expose.sqlite"

[ ! -r "${exposefile}" ] && /usr/local/bin/cbsd ${miscdir}/updatesql ${exposefile} ${distdir}/share/system-expose.schema expose

[ -z "${in}" -a -n "${out}" ] && in="${out}"
[ -z "${out}" -a -n "${in}" ] && out="${in}"

case "${mode}" in
	list)
		fw_expose_list
		exit 0
		;;
	apply)
		fw_expose_apply
		exit 0
		;;
	clear)
		fw_expose_clear
		[ -f ${ftmpdir}/${jname}-expose_fwnum ] && ${RM_CMD} -f ${ftmpdir}/${jname}-expose_fwnum
		exit 0
		;;
	flush)
		fw_expose_clear
		${RM_CMD} -f ${exposefile}
		[ -f ${ftmpdir}/${jname}-expose_fwnum ] && ${RM_CMD} -f ${ftmpdir}/${jname}-expose_fwnum
		exit 0
		;;
esac

[ -z "${in}" ] && err 1 "${N1_COLOR}Empty ${N2_COLOR}in${N0_COLOR}"
[ -z "${out}" ] && err 1 "${N1_COLOR}Empty ${N2_COLOR}out${N0_COLOR}"

COMMENT="// Setup by CBSD expose: ${proto}-${out}-${jname}"

case "${mode}" in
	add)
		fw_expose_add
		ret=$?
		if [ ${ret} -eq 0 ]; then
			# check for dup
			_res=$( cbsdsqlro ${exposefile} "SELECT pin FROM expose WHERE pin=${pin} AND pout=${pout} AND proto=\"${proto}\" AND inaddr=\"${inaddr}\" AND outaddr=\"${outaddr}\"" | ${HEAD_CMD} -n 1 )
			[ -n "${_res}" ] && err 1 "${N1_COLOR}${CBSD_APP}: already exist in DB: pin=${pin} AND pout=${pout} AND proto=\"${proto}\" AND inaddr=\"${inaddr}\" AND outaddr=\"${outaddr}\""
			cbsdsqlrw ${exposefile} "INSERT INTO expose ( pin, pout, proto, inaddr, outaddr ) VALUES ( ${in}, ${out}, \"${proto}\", \"${inaddr}\", \"${outaddr}\" )"
		else
			err 1 "${N1_COLOR}${CBSD_APP}: fw_expose_add error: ${ret}${N0_COLOR}"
		fi
		;;
	delete)
		fw_expose_delete
		fw_expose_apply
		;;
	*)
		err 1 "${N1_COLOR}Unknown mode (valid: add,delete,apply,clear,flush,list): ${N2_COLOR}${mode}${N0_COLOR}"
		;;
esac

exit 0
