#!/usr/local/bin/cbsd
#v12.1.6
# Detect first available IPv6 from ippool's
MYARG=""
MYOPTARG="cleanup dhcpd_helper ip6pool lease_time lock pass"
MYDESC="Detect first available IPv6 from pools"
ADDHELP="

${H3_COLOR}Description${N0_COLOR}:

This script consistently suggests free IP addresses if the user does not specify
a static address. Can pass a request for any external script (see dhcpdv6.conf config).
Works with 'nodeip6pool' variable ranges by default (see 'cbsd initenv-tui').

 ${UNDERLINE}Return codes${N0_COLOR}:

   0 - IP found
   1 - Unknown error
   2 - All pools are exhausted

${H3_COLOR}Options${N0_COLOR}:

 ${N2_COLOR}cleanup=${N0_COLOR}      - flush leasetime for cleanup= list, e.g:
                 cleanup=\"fe80::222:56ff:febb:4a7f fe80::333:22ff:beff:1b4a\";
 ${N2_COLOR}dhcpd_helper=${N0_COLOR} - <path_to_executable> overwrite dhcpd_helper settings
                 from dhcpdv6.conf;
 ${N2_COLOR}ip6pool=${N0_COLOR}      - use alternative pool, comma-separated if multiple
                 valid value sample:
                   ip4pool=\"2a01:4f8:140:918b::/64\";
 ${N2_COLOR}lease_time=${N0_COLOR}   - lock/lease for X seconds to avoid race/collisions
                 on concurrent request, default is: 30;
 ${N2_COLOR}lock=${N0_COLOR}         - set to '0' to prevent recursive/lock
                 (race/collisions protection);

${H3_COLOR}Examples${N0_COLOR}:

 # cbsd dhcpd
 # cbsd dhcpd ip4pool=\"2a01:4f8:140:918b::/64\"
 # cbsd dhcpd dhcpd_helper=\"/root/bin/myhelper6\"

${H3_COLOR}See also${N0_COLOR}:

 # cbsd initenv-tui --help
 # cat ~cbsd/etc/defaults/dhcpdv6.conf
 External helper sample: https://www.convectix.com/en/13.0.x/wf_ipam_ssi.html

"

. ${subrdir}/nc.subr
lock=1
pass=
lease_time=30
cleanup=
dhcpd_helper=
. ${cbsdinit}

[ -n "${dhcpd_helper}" ] && odhcpd_helper="${dhcpd_helper}"

# dhcpd_helper?
readconf dhcpdv6.conf

LOCKFILE="${ftmpdir}/dhcpdv6.lock"
LEASE_FILE="${tmpdir}/dhcpdv6.lease"
# list of locked/skip IPS
LOCKFILE_SKIPLIST=

[ -n "${odhcpd_helper}" ] && dhcpd_helper="${odhcpd_helper}"

if [ "${dhcpd_helper}" != "internal" ]; then
	cbsdlogger NOTICE ${CBSD_APP}: use external dhcpd_helper: ${dhcpd_helper}
	[ ! -x "${dhcpd_helper}" ] && log_err 1 "${N1_COLOR}${CBSD_APP}: external helper not executable: ${N2_COLOR}${dhcpd_helper}${N0_COLOR}"

	# rebuild arg list ( + add pass )
	# Pass '"' as \" in cmd
	INIT_IFS="${IFS}"
	IFS="~"
	cmd="$@"
	IFS="${INIT_IFS}"
	cmd=$( while [ -n "${1}" ]; do
		IFS="~"
		strpos --str="${1}" --search="="
		_pos=$?
		if [ ${_pos} -eq 0 ]; then
			# not params=value form
			#printf "${1} "         # (printf handles -args (with dashes)
			#echo -n "${1} "
			shift
			continue
		fi
		_arg_len=$( strlen ${1} )
		_pref=$(( _arg_len - _pos ))
		ARG=$( substr --pos=0 --len=${_pos} --str="${1}" )
		VAL=$( substr --pos=$(( ${_pos} +2 )) --len=${_pref} --str="${1}" )
		if [ -z "${ARG}" -o -z "${VAL}" ]; then
			shift
			continue
		fi
		printf "${ARG}='${VAL}' "
		shift
	done )

	cbsd_lock 60 ${LOCKFILE} ${dhcpd_helper} ${cmd}
	/usr/local/bin/cbsd freejname ${cmd} pass=1
	ret=$?
	# should never happen
	exit ${ret}
fi

if [ -n "${cleanup}" ]; then
	[ ! -r ${LEASE_FILE} ] && exit 0
	for i in ${cleanup}; do
		# ndp
		#${ARP_CMD} -nd ${i} > /dev/null 2>&1
		${SED_CMD} -i${SED_DELIMER}'' "/${i}|/d" ${LEASE_FILE}
	done
	exit 0
fi

[ "${nodeip6pool}" = "0" -o "${nodeip6pool}" = "(null)" ] && err 1 "${N1_COLOR}no nodeip6pool${N0_COLOR}"

case "${platform}" in
	FreeBSD)
		[ -z "${NDP_CMD}" ] && err 1 "${N1_COLOR}no such command: ndp${N0_COLOR}"
		;;
	*)
esac

# use args network instead of global CBSD settings/variable
if [ -n "${ip6pool}" ]; then
	# we need the atomicity of the operation to exclude
	# the simultaneous selection of the same free IP
	# use file as lock and temp database in
	# <ip>:<end_lease_time>
	# <ip>:<end_lease_time>
	if [ -z "${pass}" ]; then
		if [ "${lock}" = "1" ]; then
			# rebuild arg list ( + add pass )
			# Pass '"' as \" in cmd
			INIT_IFS="${IFS}"
			IFS="~"
			cmd="$@"
			IFS="${INIT_IFS}"
			while [ -n "${1}" ]; do
				IFS="~"
				strpos --str="${1}" --search="="
				_pos=$?
				if [ ${_pos} -eq 0 ]; then
					# not params=value form
					#printf "${1} "         # (printf handles -args (with dashes)
					#echo -n "${1} "
					shift
					continue
				fi
				_arg_len=$( strlen ${1} )
				_pref=$(( _arg_len - _pos ))
				ARG=$( substr --pos=0 --len=${_pos} --str="${1}" )
				VAL=$( substr --pos=$(( ${_pos} +2 )) --len=${_pref} --str="${1}" )
				if [ -z "${ARG}" -o -z "${VAL}" ]; then
					shift
					continue
				fi
				#printf "${ARG}='${VAL}' "
				shift
			done

			cbsd_lock 60 ${LOCKFILE} /usr/local/bin/cbsd dhcpdv6 ${cmd} pass=1
			ret=$?
			# should never happen
			exit ${ret}
	fi
fi
	nodeip6pool=$( echo ${ip6pool} | ${TR_CMD} "," " " )
fi

[ -z "${nodeip6pool}" ] && err 1 "${N1_COLOR}no nodeip6pool${N0_COLOR}"

iptype ${nodeip6pool}
ret=$?

case ${ret} in
	2)
		;;
	*)
		err 1 "${N1_COLOR}not IPv6?: ${N2_COLOR}${nodeip6pool}${N0_COLOR}"
		;;
esac

# prune/purge old records
if [ -r ${LEASE_FILE} ]; then
	${TRUNCATE_CMD} -s0 ${LEASE_FILE}.swap
	cur_time=$( ${DATE_CMD} +%s )
	eval $( ${CAT_CMD} ${LEASE_FILE} | while read items; do
		p1=${items%%|*}
		p2=${items##*|}
		[ -z "${p1}" -o -z "${p2}" ] && continue
		if is_number "${p2}"; then
			continue
		fi
		if [ ${p2} -gt ${cur_time} ]; then
			# still valid
			echo "${items}" >> ${LEASE_FILE}.swap
			if [ -z "${LOCKFILE_SKIPLIST}" ]; then
				LOCKFILE_SKIPLIST="${p1}"
			else
				LOCKFILE_SKIPLIST="${LOCKFILE_SKIPLIST} ${p1}"
			fi
		fi
	echo "LOCKFILE_SKIPLIST=\"${LOCKFILE_SKIPLIST}\""
	done )
	${MV_CMD} ${LEASE_FILE}.swap ${LEASE_FILE}
fi

eval $( ${miscdir}/sipcalc ${nodeip6pool} )

[ $? -ne 0 ] && err 1 "${N1_COLOR}sipcalc: error for: ${N2_COLOR}${nodeip6pool}${N0_COLOR}"
[ -z "${_prefix_ipv6_range_start}" -o -z "${_prefix_ipv6_range_end}" ] && 1 "${N1_COLOR}sipcalc: no range for: ${N2_COLOR}${nodeip6pool}${N0_COLOR}"

# normalize range from network/broadcast to first/latest usable IP
# todo: implement in sipcalc!
sqllistdelimer=":"
sqllist "${_prefix_ipv6_range_start}" s1 s2 s3 s4 s5 s6 s7 s8
sqllist "${_prefix_ipv6_range_end}" e1 e2 e3 e4 e5 e6 e7 e8
unset sqllistdelimer
if [ "${s8}" = "0000" ]; then
	if [ "${s7}" = "0000" ]; then
		if [ "${s6}" = "0000" ]; then
			if [ "${s5}" = "0000" ]; then
				_prefix_ipv6_range_start="${s1}:${s2}:${s3}:${s4}:${s5}:${s6}:${s7}:0001"
			fi
		fi
	fi
fi
[ "${e7}" = "ffff" ] && _prefix_ipv6_range_end="${e1}:${e2}:${e3}:${e4}:${e5}:${e6}:${e7}:fffc"

# check jail&bhyve first

skip_ip="${nodeip6}"

existing_ipjail=$( cbsdsqlro local SELECT DISTINCT ip4_addr FROM jails WHERE ip4_addr != \'0\' AND ip4_addr != \'DHCP\' | ${TR_CMD} "," " " | ${XARGS_CMD} )

# prepare skip ip list
for i in ${existing_ipjail}; do
	ipwmask ${i}
	[ -z "${IWM}" ] && continue
	iptype ${IWM}
	[ $? -ne 2 ] && continue
	skip_ip="${skip_ip} ${IWM}"
done

ip=$( ${miscdir}/ipv6range ${_prefix_ipv6_range_start} ${_prefix_ipv6_range_end} | while read _ip; do

	skip=0
	for n in ${skip_ip} ${LOCKFILE_SKIPLIST}; do
		[ "${n}" = "${_ip}" ] && skip=1
	done
	[ ${skip} -eq 1 ] && continue

	if [ ${freebsdhostversion} -gt 1300131 ]; then
		# https://svnweb.freebsd.org/base?view=revision&revision=r368045
		${PING_CMD} -6 -n -t1 -i0.3 -W 300 -c2 -q ${_ip} > /dev/null 2>&1
		ret=$?
	else
		${PING6_CMD} -n -t1 -i0.3 -W 300 -c2 -q ${_ip} > /dev/null 2>&1
		ret=$?
	fi
	if [ ${ret} -eq 0 ]; then
		# second check via NDP
		${NDP_CMD} ${_ip} > /dev/null 2>&1
		ret=$?
	fi
	[ ${ret} -eq 0 ] && continue

	# normalize IPv6 to compressed form
	eval $( ${miscdir}/sipcalc ${_ip} )
	_pureip="${_compressed_ipv6_address}"

	printf "${_pureip}" && exit 0
done )

cur_time=$( ${DATE_CMD} +%s )
lease_time_end=$(( cur_time + lease_time ))
echo "${ip}|${lease_time_end}" >> ${LEASE_FILE}

echo "${ip}"

exit 0
