#!/sbin/runscript
# Copyright (c) 2007-2009 Roy Marples <roy@marples.name>
# Released under the 2-clause BSD license.

MODULESDIR="/lib/netifrc/net"
MODULESLIST="${RC_SVCDIR}/nettree"
_config_vars="config routes"

[ -z "${IN_BACKGROUND}" ] && IN_BACKGROUND="NO"

description="Configures network interfaces."

# Handy var so we don't have to embed new lines everywhere for array splitting
__IFS="
"
depend()
{
	local IFACE=${RC_SVCNAME#*.}
	local IFVAR=$(shell_var "${IFACE}")

	need localmount
	if [ "$RC_UNAME" = Linux -a "$IFACE" != lo ]; then
		need sysfs
	fi
	after bootmisc
	keyword -jail -prefix -vserver

	case "${IFACE}" in
		lo|lo0) ;;
		*)
			after net.lo net.lo0 dbus
			provide net
			;;
	esac

	if [ "$(command -v "depend_${IFVAR}")" = "depend_${IFVAR}" ]; then
		depend_${IFVAR}
	fi

	local dep= prov=
	for dep in need use before after provide keyword; do
		eval prov=\$rc_${dep}_${IFVAR}
		if [ -n "${prov}" ]; then
			${dep} ${prov}
			ewarn "rc_${dep}_${IFVAR} is deprecated."
			ewarn "Please use rc_net_${IFVAR}_${dep} instead."
		fi
	done
}

# Support bash arrays - sigh
_array_helper()
{
	local _a=

	eval _a=\$$1
	_a=$(echo "${_a}" | sed -e 's:^[[:space:]]*::' -e 's:[[:space:]]*$::' -e '/^$/d' -e 's:[[:space:]]\{1,\}: :g')

	[ -n "${_a}" ] && printf "%s\n" "${_a}"
}

_get_array()
{
	local _a=
	if [ -n "${BASH}" ]; then
		case "$(declare -p "$1" 2>/dev/null)" in
			"declare -a "*)
				ewarn "You are using a bash array for $1."
				ewarn "This feature will be removed in the future."
				ewarn "Please see net.example for the correct format for $1."
				eval "set -- \"\${$1[@]}\""
				for _a; do
					printf "%s\n" "${_a}"
				done
				return 0
				;;
		esac
	fi

	_array_helper $1
}

# Flatten bash arrays to simple strings
_flatten_array()
{
	if [ -n "${BASH}" ]; then
		case "$(declare -p "$1" 2>/dev/null)" in
			"declare -a "*)
				ewarn "You are using a bash array for $1."
				ewarn "This feature will be removed in the future."
				ewarn "Please see net.example for the correct format for $1."
				eval "set -- \"\${$1[@]}\""
				for x; do
					printf "'%s' " "$(printf "$x" | sed "s:':'\\\'':g")"
				done
				return 0
				;;
		esac
	fi

	_array_helper $1
}

_wait_for_carrier()
{
	local timeout= efunc=einfon

	_has_carrier  && return 0

	eval timeout=\$carrier_timeout_${IFVAR}
	timeout=${timeout:-${carrier_timeout:-0}}

	# Incase users don't want this nice feature ...
	[ ${timeout} -le 0 ] && return 0

	yesno ${RC_PARALLEL} && efunc=einfo
	${efunc} "Waiting for carrier (${timeout} seconds) "
	while [ ${timeout} -gt 0 ]; do
		if _has_carrier; then
			[ "${efunc}" = "einfon" ] && echo
			eend 0
			return 0
		fi
		sleep 1
		: $(( timeout -= 1 ))
		[ "${efunc}" = "einfon" ] && printf "."
	done

	[ "${efunc}" = "einfon" ] && echo
	eend 1
	return 1
}

_netmask2cidr()
{
	# Some shells cannot handle hex arithmetic, so we massage it slightly
	# Buggy shells include FreeBSD sh, dash and busybox.
	# bash and NetBSD sh don't need this.
	case $1 in
		0x*)
		local hex=${1#0x*} quad=
		while [ -n "${hex}" ]; do
			local lastbut2=${hex#??*}
			quad=${quad}${quad:+.}0x${hex%${lastbut2}*}
			hex=${lastbut2}
		done
		set -- ${quad}
		;;
	esac

	local i= len=
	local IFS=.
	for i in $1; do
		case $i in
			0x*)	i=$((i)) ;;
		esac
		while [ ${i} -ne 0 ]; do
			: $(( len += i % 2 ))
			: $(( i >>= 1 ))
		done
	done

	echo "${len}"
}

_configure_variables()
{
	local var= v= t=

	for var in ${_config_vars}; do
		local v=
		for t; do
			eval v=\$${var}_${t}
			if [ -n "${v}" ]; then
				eval ${var}_${IFVAR}=\$${var}_${t}
				continue 2
			fi
		done
	done
}

_which()
{
	local i OIFS
	# Empty
	[ -z "$1" ] && return
	# check paths
	OIFS="$IFS"
	IFS=:
	for i in $PATH ; do
		[ -x $i/$1 ] && echo $i/$1 && break
	done
	IFS=$OIFS
}

# Like _which, but also consider shell builtins, and multiple alternatives
_program_available()
{
	[ -z "$1" ] && return 0
	local x=
	for x; do
		case "${x}" in
			/*) [ -x "${x}" ] && break;;
			*) type "${x}" >/dev/null 2>&1 && break;;
		esac
		unset x
	done
	[ -n "${x}" ] && echo $x && return 0
	return 1
}

_show_address()
{
	einfo "received address $(_get_inet_address "${IFACE}")"
}

# Basically sorts our modules into order and saves the list
_gen_module_list()
{
	local x= f= force=$1
	if ! ${force} && [ -s "${MODULESLIST}" -a "${MODULESLIST}" -nt "${MODULESDIR}" ]; then
		local update=false
		for x in "${MODULESDIR}"/*.sh; do
			[ -e "${x}" ] || continue
			if [ "${x}" -nt "${MODULESLIST}" ]; then
				update=true
				break
			fi
		done
		${update} || return 0
	fi

	einfo "Caching network module dependencies"
	# Run in a subshell to protect the main script
	(
	after() {
		eval ${MODULE}_after="\"\${${MODULE}_after}\${${MODULE}_after:+ }$*\""
	}

	before() {
		local mod=${MODULE}
		local MODULE=
		for MODULE; do
			after "${mod}"
		done
	}

	program() {
		if [ "$1" = "start" -o "$1" = "stop" ]; then
			local s="$1"
			shift
			eval ${MODULE}_program_${s}="\"\${${MODULE}_program_${s}}\${${MODULE}_program_${s}:+ }$*\""
		else
			eval ${MODULE}_program="\"\${${MODULE}_program}\${${MODULE}_program:+ }$*\""
		fi
	}

	provide() {
		eval ${MODULE}_provide="\"\${${MODULE}_provide}\${${MODULE}_provide:+ }$*\""
		local x
		for x in $*; do
			eval ${x}_providedby="\"\${${MODULE}_providedby}\${${MODULE}_providedby:+ }${MODULE}\""
		done
	}

	for MODULE in "${MODULESDIR}"/*.sh; do
		sh -n "${MODULE}" || continue
		. "${MODULE}" || continue
		MODULE=${MODULE#${MODULESDIR}/}
		MODULE=${MODULE%.sh}
		eval ${MODULE}_depend
		MODULES="${MODULES} ${MODULE}"
	done

	VISITED=
	SORTED=
	visit() {
		case " ${VISITED} " in
			*" $1 "*) return;;
		esac
		VISITED="${VISITED} $1"

		eval AFTER=\$${1}_after
		for MODULE in ${AFTER}; do
			eval PROVIDEDBY=\$${MODULE}_providedby
			if [ -n "${PROVIDEDBY}" ]; then
				for MODULE in ${PROVIDEDBY}; do
					visit "${MODULE}"
				done
			else
				visit "${MODULE}"
			fi
		done

		eval PROVIDE=\$${1}_provide
		for MODULE in ${PROVIDE}; do
			visit "${MODULE}"
		done

		eval PROVIDEDBY=\$${1}_providedby
		[ -z "${PROVIDEDBY}" ] && SORTED="${SORTED} $1"
	}

	for MODULE in ${MODULES}; do
		visit "${MODULE}"
	done

	printf "" > "${MODULESLIST}"
	i=0
	for MODULE in ${SORTED}; do
		eval PROGRAM=\$${MODULE}_program
		eval PROGRAM_START=\$${MODULE}_program_start
		eval PROGRAM_STOP=\$${MODULE}_program_stop
		eval PROVIDE=\$${MODULE}_provide
		echo "module_${i}='${MODULE}'" >> "${MODULESLIST}"
		echo "module_${i}_program='${PROGRAM}'" >> "${MODULESLIST}"
		echo "module_${i}_program_start='${PROGRAM_START}'" >> "${MODULESLIST}"
		echo "module_${i}_program_stop='${PROGRAM_STOP}'" >> "${MODULESLIST}"
		echo "module_${i}_provide='${PROVIDE}'" >> "${MODULESLIST}"
		: $(( i += 1 ))
	done
	echo "module_${i}=" >> "${MODULESLIST}"
	)

	return 0
}

_load_modules()
{
	local starting=$1 mymods=

	# Ensure our list is up to date
	_gen_module_list false
	if ! . "${MODULESLIST}"; then
		_gen_module_list true
		. "${MODULESLIST}"
	fi

	MODULES=
	if [ "${IFACE}" != "lo" -a "${IFACE}" != "lo0" ]; then
		eval mymods=\$modules_${IFVAR}
		[ -z "${mymods}" ] && mymods=${modules}
	fi

	local i=-1 x= mod= f= provides=
	while true; do
		: $(( i += 1 ))
		eval mod=\$module_${i}
		[ -z "${mod}" ] && break
		[ -e "${MODULESDIR}/${mod}.sh" ] || continue

		eval set -- \$module_${i}_program
		if [ -n "$1" ]; then
			if ! _program_available "$@" >/dev/null; then
				vewarn "Skipping module $mod due to missing program: $@"
				continue
			fi
		fi
		if ${starting}; then
			eval set -- \$module_${i}_program_start
		else
			eval set -- \$module_${i}_program_stop
		fi
		if [ -n "$1" ]; then
			if ! _program_available "$@" >/dev/null; then
				vewarn "Skipping module $mod due to missing program: $@"
				continue
			fi
		fi

		eval provides=\$module_${i}_provide
		if ${starting}; then
			case " ${mymods} " in
				*" !${mod} "*) continue;;
				*" !${provides} "*) [ -n "${provides}" ] && continue;;
			esac
		fi
		MODULES="${MODULES}${MODULES:+ }${mod}"

		# Now load and wrap our functions
		if ! . "${MODULESDIR}/${mod}.sh"; then
			eend 1 "${RC_SVCNAME}: error loading module \`${mod}'"
			exit 1
		fi

		[ -z "${provides}" ] && continue

		# Wrap our provides
		local f=
		for f in pre_start start post_start; do
			eval "${provides}_${f}() { [ "$(command -v "${mod}_${f}")" = "${mod}_${f}" ] || return 0; ${mod}_${f} \"\$@\"; }"
		done

		eval module_${mod}_provides="${provides}"
		eval module_${provides}_providedby="${mod}"
	done

	# Wrap our preferred modules
	for mod in ${mymods}; do
		case " ${MODULES} " in
			*" ${mod} "*)
			eval x=\$module_${mod}_provides
			[ -z "${x}" ] && continue
			for f in pre_start start post_start; do
				eval "${x}_${f}() { [ "$(command -v "${mod}_${f}")" = "${mod}_${f}" ] || return 0; ${mod}_${f} \"\$@\"; }"
			done
			eval module_${x}_providedby="${mod}"
			;;
		esac
	done

	# Finally remove any duplicated provides from our list if we're starting
	# Otherwise reverse the list
	local LIST="${MODULES}" p=
	MODULES=
	if ${starting}; then
		for mod in ${LIST}; do
			eval x=\$module_${mod}_provides
			if [ -n "${x}" ]; then
				eval p=\$module_${x}_providedby
				[ "${mod}" != "${p}" ] && continue
			fi
			MODULES="${MODULES}${MODULES:+ }${mod}"
		done
	else
		for mod in ${LIST}; do
			MODULES="${mod}${MODULES:+ }${MODULES}"
		done
	fi

	veinfo "Loaded modules: ${MODULES}"
}

_load_config()
{
	local config="$(_get_array "config_${IFVAR}")"
	local fallback="$(_get_array fallback_${IFVAR})"

	config_index=0
	local IFS="$__IFS"
	set -- ${config}

	# We should support a space separated array for cidr configs
	# But only as long as they do not contain other parameters for the address
	if [ $# = 1 ]; then
		unset IFS
		set -- ${config}
		# Of course, we may have a single address added old style.
		# If the NEXT argument is a v4 or v6 address, it's the next config.
		# Otherwise, it's arguments to the first config...
		if [ "${2#*.*}" = "${2}" -a "${2#*:*}" = "${2}" ]; then
			# Not an IPv4/IPv6
			local IFS="$__IFS"
			set -- ${config}
		fi
	fi

	# Ensure that loopback has the correct address
	if [ "${IFACE}" = "lo" -o "${IFACE}" = "lo0" ]; then
		if [ "$1" != "null" ]; then
		   	config_0="127.0.0.1/8"
			config_index=1
		fi
	else
		if [ -z "$1" ]; then
			ewarn "config_${IFVAR} not specified; defaulting to DHCP"
			config_0="dhcp"
			config_index=1
		fi
	fi


	# We store our config in an array like vars
	# so modules can influence it
	for cmd; do
		eval config_${config_index}="'${cmd}'"
		: $(( config_index += 1 ))
	done
	# Terminate the list
	eval config_${config_index}=

	config_index=0
	for cmd in ${fallback}; do
		eval fallback_${config_index}="'${cmd}'"
		: $(( config_index += 1 ))
	done
	# Terminate the list
	eval fallback_${config_index}=

	# Don't set to zero, so any net modules don't have to do anything extra
	config_index=-1
}

# Support functions
_run_if()
{
	local cmd=$1 iface=$2 ifr=${IFACE} ifv=${IFVAR}
	# Ensure that we don't stamp on real values
	local IFACE= IFVAR=
	shift
	if [ -n "${iface}" ]; then
		IFACE="${iface}"
		[ "${iface}" != "${ifr}" ] && IFVAR=$(shell_var "${IFACE}")
	else
		IFACE=${ifr}
		IFVAR=${ifv}
	fi
	${cmd}
}
interface_exists()
{
	_run_if _exists "$@"
}
interface_up()
{
	_run_if _up "$@"
}
interface_down()
{
	_run_if _down "$@"
}

start()
{
	local IFACE=${RC_SVCNAME#*.} oneworked=false fallback=false module=
	local IFVAR=$(shell_var "${IFACE}") cmd= our_metric=
	local metric=0 _up_before_preup
	eval _up_before_preup="\$up_before_preup_${IFVAR}"
	[ -z "${_up_before_preup}" ] && _up_before_preup=$up_before_preup

	einfo "Bringing up interface ${IFACE}"
	eindent

	if [ -z "${MODULES}" ]; then
		local MODULES=
		_load_modules true
	fi

	# We up the iface twice if we have a preup to ensure it's up if
	# available in preup and afterwards incase the user inadvertently
	# brings it down
	if [ "$(command -v preup)" = "preup" ]; then
		yesno "${_up_before_preup:-yes}" && _up 2>/dev/null
		ebegin "Running preup"
		eindent
		preup || return 1
		eoutdent
	fi

	_up 2>/dev/null

	for module in ${MODULES}; do
		if [ "$(command -v "${module}_pre_start")" = "${module}_pre_start" ]; then
			${module}_pre_start || exit $?
		fi
	done

	if ! _exists; then
		eerror "ERROR: interface ${IFACE} does not exist"
		eerror "Ensure that you have loaded the correct kernel module for your hardware"
		return 1
	fi

	if ! _wait_for_carrier; then
		if service_started devd; then
			ewarn "no carrier, but devd will start us when we have one"
			mark_service_inactive "${RC_SVCNAME}"
		else
			eerror "no carrier"
		fi
		return 1
	fi

	local config= config_index=
	_load_config
	config_index=0

	eval our_metric=\$metric_${IFVAR}
	if [ -n "${our_metric}" ]; then
		metric=${our_metric}
	elif [ "${IFACE}" != "lo" -a "${IFACE}" != "lo0" ]; then
		: $(( metric += $(_ifindex) ))
	fi

	while true; do
		eval config=\$config_${config_index}
		[ -z "${config}" ] && break

		set -- ${config}
		if [ "$1" != "null" -a "$1" != "noop" ]; then
			ebegin "$1"
		fi
		eindent
		case "$1" in
			noop)
				if [ -n "$(_get_inet_address)" ]; then
					oneworked=true
					break
				fi
				;;
			null) :;;
			[0-9]*|*:*) _add_address ${config};;
			*)
				if [ "$(command -v "${config}_start")" = "${config}_start" ]; then
					"${config}"_start
				else
					eerror "nothing provides \`${config}'"
				fi
				;;
		esac
		if eend $?; then
			oneworked=true
		else
			eval config=\$fallback_${config_index}
			if [ -n "${config}" ]; then
				fallback=true
				eoutdent
				ewarn "Trying fallback configuration ${config}"
				eindent
				eval config_${config_index}=\$config
				unset fallback_${config_index}
				: $(( config_index -= 1 ))
			fi
		fi
		eoutdent
		: $(( config_index += 1 ))
	done

	if ! ${oneworked}; then
		if [ "$(command -v failup)" = "failup" ]; then
			ebegin "Running failup"
			eindent
			failup
			eoutdent
		fi
		return 1
	fi

	local hideFirstroute=false first=true routes=
	if ${fallback}; then
		routes="$(_get_array "fallback_routes_${IFVAR}")"
	fi
	if [ -z "${routes}" ]; then
		routes="$(_get_array "routes_${IFVAR}")"
	fi
	if [ "${IFACE}" = "lo" -o "${IFACE}" = "lo0" ]; then
		if [ "${config_0}" != "null" ]; then
			routes="127.0.0.0/8 via 127.0.0.1
${routes}"
			hideFirstroute=true
		fi
	fi

	local OIFS="${IFS}" SIFS="${IFS-y}"
	local IFS="$__IFS"
	local fam
	for cmd in ${routes}; do
		unset IFS
		if ${first}; then
			first=false
			einfo "Adding routes"
		fi

		case ${cmd} in
			-6" "*) fam="-6"; cmd=${cmd#-6 };;
			-4" "*) fam="-4"; cmd=${cmd#-4 };;
		esac

		eindent
		ebegin ${cmd}
		# Work out if we're a host or a net if not told
		case ${cmd} in
			-net" "*|-host" "*);;
			*" "netmask" "*)                   cmd="-net ${cmd}";;
			*.*.*.*/32*)                       cmd="-host ${cmd}";;
			*.*.*.*/*|0.0.0.0|0.0.0.0" "*)     cmd="-net ${cmd}";;
			default|default" "*)               cmd="-net ${cmd}";;
			*:*/128*)                          cmd="-host ${cmd}";;
			*:*/*)                             cmd="-net ${cmd}";;
			*)                                 cmd="-host ${cmd}";;
		esac
		if ${hideFirstroute}; then
			_add_route ${fam} ${cmd} >/dev/null 2>&1
			hideFirstroute=false
		else
			_add_route ${fam} ${cmd} >/dev/null
		fi
		eend $?
		eoutdent
	done
	if [ "${SIFS}" = "y" ]; then
		unset IFS
	else
		IFS="${OIFS}"
	fi

	for module in ${MODULES}; do
		if [ "$(command -v "${module}_post_start")" = "${module}_post_start" ]; then
			${module}_post_start || exit $?
		fi
	done

	if [ "$(command -v postup)" = "postup" ]; then
		ebegin "Running postup"
		eindent
		postup
		eoutdent
	fi

	return 0
}

stop()
{
	# Don't stop the network at shutdown.
 	# We don't use the noshutdown keyword so that we are started again
	# correctly if we go back to multiuser.
	yesno ${keep_network:-YES} && yesno $RC_GOINGDOWN && return 0

	local IFACE=${RC_SVCNAME#*.} module=
	local IFVAR=$(shell_var "${IFACE}") opts=

	einfo "Bringing down interface ${IFACE}"
	eindent

	if [ -z "${MODULES}" ]; then
		local MODULES=
		_load_modules false
	fi

	if [ "$(command -v predown)" = "predown" ]; then
		ebegin "Running predown"
		eindent
		predown || return 1
		eoutdent
	else
		if is_net_fs /; then
			eerror "root filesystem is network mounted -- can't stop ${IFACE}"
			return 1
		fi
	fi

	for module in ${MODULES}; do
		if [ "$(command -v "${module}_pre_stop")" = "${module}_pre_stop" ]; then
			${module}_pre_stop || exit $?
		fi
	done

	for module in ${MODULES}; do
		if [ "$(command -v "${module}_stop")" = "${module}_stop" ]; then
			${module}_stop
		fi
	done

	# Only delete addresses for interfaces that exist
	if _exists; then
		# PPP can manage it's own addresses when IN_BACKGROUND
		# Important in case "demand" set on the ppp link
		if ! (yesno ${IN_BACKGROUND} && is_ppp) ; then
			_delete_addresses "${IFACE}"
		fi
	fi

	for module in ${MODULES}; do
		if [ "$(command -v "${module}_post_stop")" = "${module}_post_stop" ]; then
			${module}_post_stop
		fi
	done

	# If not in background, and not loopback then bring the interface down
	# unless overridden.
	if ! yesno ${IN_BACKGROUND} && \
	[ "${IFACE}" != "lo" -a "${IFACE}" != "lo0" ]; then
		eval module=\$ifdown_${IFVAR}
		module=${module:-${ifdown:-YES}}
		yesno ${module} && _down 2>/dev/null
	fi

	type resolvconf >/dev/null 2>&1 && resolvconf -d "${IFACE}" 2>/dev/null

	if [ "$(command -v "postdown")" = "postdown" ]; then
		ebegin "Running postdown"
		eindent
		postdown
		eoutdent
	fi

	return 0
}
