# Copyright (c) 2007-2008 Roy Marples <roy@marples.name>
# Released under the 2-clause BSD license.
# shellcheck shell=sh disable=SC1008

iproute2_depend()
{
	program ip
	provide interface
	after ifconfig
}

_up()
{
	_ip -v link set dev "${IFACE}" up
}

_down()
{
	_ip -v link set dev "${IFACE}" down
}

_exists()
{
	_netns [ -e /sys/class/net/"$IFACE" ]
}

_set_netns()
{
	eval netns="\$netns_${IFVAR}"
}

_ip()
{
	local v
	if [ "${1}" = -v ]; then
		v=1
		shift
	fi

	local ip
	if ! ip=$(command -v ip 2>/dev/null) || [ ! -x "${ip}" ]; then
		eerror "Please make sure that iproute2 is installed"
		exit 1
	fi

	local netns
	_set_netns
	# make sure the netns exists
	[ -n "${netns}" ] && [ -e /run/netns/"${netns}" ] && set -- -n "${netns}" "${@}"
	[ -n "${v}" ] && veinfo ip "${@}"
	"${ip}" "${@}"
}

_ifindex()
{
	local index=-1
	local f v
	if _netns [ -e /sys/class/net/"${IFACE}"/ifindex ]; then
		index=$(_netns cat /sys/class/net/"${IFACE}"/ifindex)
	else
		for f in $(_netns glob /sys/class/net/\*/ifindex); do
			v=$(cat $f)
			[ $v -gt $index ] && index=$v
		done
		: $(( index += 1 ))
	fi
	echo "${index}"
	return 0
}

_is_wireless()
{
	# Support new sysfs layout
	_netns [ -d /sys/class/net/"${IFACE}"/wireless -o \
		-d /sys/class/net/"${IFACE}"/phy80211 ] && return 0

	_netns [ ! -e /proc/net/wireless ] && return 1
	_netns grep -Eq "^[[:space:]]*${IFACE}:" /proc/net/wireless
}

_set_flag()
{
	local flag=$1 opt="on"
	if [ "${flag#-}" != "${flag}" ]; then
		flag=${flag#-}
		opt="off"
	fi
	_ip -v link set dev "${IFACE}" "${flag}" "${opt}"
}

_get_mac_address()
{
	local mac=
	mac=$(_netns sed -e 'y/abcdef/ABCDEF/;q' /sys/class/net/"${IFACE}"/address) || return

	case "${mac}" in
		'')                false ;;
		00:00:00:00:00:00) false ;;
		44:44:44:44:44:44) false ;;
		FF:FF:FF:FF:FF:FF) false ;;
	esac &&
	printf '%s\n' "${mac}"
}

_set_mac_address()
{
	_ip -v link set dev "${IFACE}" address "$1"
}

_get_inet_addresses()
{
	local family="$1";
	if [ -z "$family" ]; then
		family="inet"
	fi
	LC_ALL=C _ip -family $family addr show "${IFACE}" | \
	sed -n -e 's/.*inet6\? \([^ ]*\).*/\1/p'
}

_get_inet6_addresses() {
	_get_inet_addresses "inet6"
}

_get_inet_address()
{
	set -- $(_get_inet_addresses)
	[ $# = "0" ] && return 1
	echo "$1"
}

_get_inet6_address()
{
	set -- $(_get_inet6_addresses)
	[ $# = "0" ] && return 1
	echo "$1"
}

_add_address()
{
	if [ "$1" = "127.0.0.1/8" -a "${IFACE}" = "lo" ]; then
		_ip -v addr add "$@" dev "${IFACE}" 2>/dev/null
		return 0
	fi
	local x
	local address netmask broadcast peer anycast label scope
	local valid_lft preferred_lft home nodad
	local confflaglist family raw_address family_maxnetmask
	raw_address="$1" ; shift
	# Extract the netmask on address if present.
	if [ "${raw_address%\/*}" != "${raw_address}" ]; then
		address="${raw_address%\/*}"
		netmask="${raw_address#*\/}"
	else
		address="$raw_address"
	fi

	# Some options are not valid for one family or the other.
	case ${address} in
		*:*) family=6 family_maxnetmask=128 ;;
		*) family=4 family_maxnetmask=32 ;;
	esac

	while [ -n "$*" ]; do
		x=$1 ; shift
		case "$x" in
			netmask|ne*)
				if [ -n "${netmask}" ]; then
					eerror "Too many netmasks: $raw_address netmask $1"
					return 1
				fi
				netmask="$(_netmask2cidr "$1")"
				shift ;;
			broadcast|brd|br*)
				broadcast="$1" ; shift ;;
			pointopoint|pointtopoint|peer|po*|pe*)
				peer="$1" ; shift ;;
			anycast|label|scope|valid_lft|preferred_lft|a*|l*|s*|v*|pr*)
				case $x in
					a*) x=anycast ;;
					l*) x=label ;;
					s*) x=scope ;;
					v*) x=valid_lft ;;
					pr*) x=preferred_lft ;;
				esac
				eval "$x=$1" ; shift ;;
			home|mngtmpaddr|nodad|noprefixroute|h*|mng*|no*)
				case $x in
					h*) x=home ;;
					m*) x=mngtmpaddr ;;
					nop*) x=noprefixroute ;;
					n*) x=nodad ;;
				esac
				# FIXME: If we need to reorder these, this will take more code
				confflaglist="${confflaglist} $x" ; ;;
			*)
				ewarn "Unknown argument to config_$IFACE: $x"
		esac
	done

	# figure out the broadcast address if it is not specified
	# This must NOT be set for IPv6 addresses
	case $family in
		4) [ -z "$broadcast" ] && broadcast="+" ;;
		6) [ -n "$broadcast" ] && eerror "Broadcast keywords are not valid with IPv6 addresses" && return 1 ;;
	esac

	# Always have a netmask
	[ -z "$netmask" ] && netmask=$family_maxnetmask

	# Check for address already existing:
	_ip addr show to "${address}/${family_maxnetmask}" dev "${IFACE}" 2>/dev/null | \
		grep -Fsq "${address}"
	address_already_exists=$?

	# This must appear on a single line, continuations cannot be used
	set -- "${address}${netmask:+/}${netmask}" ${peer:+peer} ${peer} ${broadcast:+broadcast} ${broadcast} ${anycast:+anycast} ${anycast} ${label:+label} ${label} ${scope:+scope} ${scope} dev "${IFACE}" ${valid_lft:+valid_lft} $valid_lft ${preferred_lft:+preferred_lft} $preferred_lft $confflaglist
	_ip -v addr add "$@"
	rc=$?
	# Check return code in some cases
	if [ $rc -ne 0 ]; then
		# If the address already exists, our default behavior is to WARN but continue.
		# You can completely silence this with: errh_IFVAR_address_EEXIST=continue
		if [ $address_already_exists -eq 0 ]; then
			eh_behavior=$(_get_errorhandler_behavior "$IFVAR" "address" "EEXIST" "warn")
			case $eh_behavior in
				continue) msgfunc=true rc=0 ;;
				info) msgfunc=einfo rc=0 ;;
				warn) msgfunc=ewarn rc=0 ;;
				error|fatal) msgfunc=eerror rc=1;;
				*) msgfunc=eerror rc=1 ; eerror "Unknown error behavior: $eh_behavior" ;;
			esac
			eval $msgfunc "Address ${address}${netmask:+/}${netmask} already existed!"
			eval $msgfunc \"$(_ip addr show to "${address}/${family_maxnetmask}" dev "${IFACE}" 2>&1)\"
		else
			: # TODO: Handle other errors
		fi
	fi
	return $rc
}

_add_route()
{
	local family=

	if [ "$1" = "-A" -o "$1" = "-f" -o "$1" = "-family" ]; then
		family="-f $2"
		shift; shift
	elif [ "$1" = "-4" ]; then
	    family="-f inet"
		shift
	elif [ "$1" = "-6" ]; then
	    family="-f inet6"
		shift
	fi

	local rtype=

	# Check if route type is provided that does not allow to use dev
	# Route type must come first, before the prefix, also it cannot be used to list routes
	case "$1" in
		blackhole|prohibit|throw|unreachable) rtype="$1" ; shift ;;
	esac

	if [ $# -eq 3 ]; then
		set -- "$1" "$2" via "$3"
	elif [ "$3" = "gw" ]; then
		local one=$1 two=$2
		shift; shift; shift
		set -- "${one}" "${two}" via "$@"
	fi

	local cmd= cmd_nometric= have_metric=false
	while [ -n "$1" ]; do
		case "$1" in
			metric) metric=$2 ; cmd="${cmd} metric $2" ; shift ; have_metric=true ;;
			netmask) x="/$(_netmask2cidr "$2")" ; cmd="${cmd}${x}" ; cmd_nometric="${cmd}${x}" ; shift;;
			-host|-net);;
			*) cmd="${cmd} ${1}" ; cmd_nometric="${cmd_nometric} ${1}" ;;
		esac
		shift
	done

	# We cannot use a metric if we're using a nexthop
	if ! ${have_metric} && \
		[ -n "${metric}" -a \
			"${cmd##* nexthop }" = "$cmd" ]
	then
		cmd="${cmd} metric ${metric}"
	fi

	# Process dev vs nodev routes
	# Positional parameters are used for correct array handling
	if [ -n "${rtype}" ]; then
		local nodev_routes="$(service_get_value "nodev_routes")"
		service_set_value "nodev_routes" "${nodev_routes}
${family} route del ${rtype} ${cmd}"
		set --
	else
		set -- dev "${IFACE}"
	fi

	# Check for route already existing:
	_ip ${family} route show ${cmd_nometric} "$@" 2>/dev/null | \
		grep -Fsq "${cmd%% *}"
	route_already_exists=$?

	_ip -v ${family} route append ${rtype} ${cmd} "$@"
	rc=$?

	# Check return code in some cases
	if [ $rc -ne 0 ]; then
		# If the route already exists, our default behavior is to WARN but continue.
		# You can completely silence this with: errh_IFVAR_route_EEXIST=continue
		if [ $route_already_exists -eq 0 ]; then
			eh_behavior=$(_get_errorhandler_behavior "$IFVAR" "route" "EEXIST" "warn")
			case $eh_behavior in
				continue) msgfunc=true rc=0 ;;
				info) msgfunc=einfo rc=0 ;;
				warn) msgfunc=ewarn rc=0 ;;
				error|fatal) msgfunc=eerror rc=1;;
				*) msgfunc=eerror rc=1 ; eerror "Unknown error behavior: $eh_behavior" ;;
			esac
			eval $msgfunc "Route '$cmd_nometric' already existed:"
			eval $msgfunc \"$(_ip $family route show ${cmd_nometric} \"$@\" 2>&1)\"
		else
			: # TODO: Handle other errors
		fi
	fi
	eend $rc
}

_delete_addresses()
{
	_ip -v addr flush dev "${IFACE}" scope global 2>/dev/null
	_ip -v addr flush dev "${IFACE}" scope site 2>/dev/null
	if [ "${IFACE}" != "lo" ]; then
		_ip -v addr flush dev "${IFACE}" scope host 2>/dev/null
	fi
	return 0
}

_has_carrier()
{
	LC_ALL=C _ip link show dev "${IFACE}" | grep -Fq "LOWER_UP"
}

# Used by iproute2, ip6rd & ip6to4
_tunnel()
{
	_ip $FAMILY tunnel "$@"
	rc=$?
	# TODO: check return code in some cases
	return $rc
}

# This is just to trim whitespace, do not add any quoting!
_trim() {
	echo $*
}

# This is our interface to Routing Policy Database RPDB
# This allows for advanced routing tricks
_ip_rule_runner() {
	local cmd rules OIFS="${IFS}" family
	local ru ruN
	if [ "$1" = "-4" -o "$1" = "-6" ]; then
		family="$1"
		shift
	else
		family="-4"
	fi
	cmd="$1"
	rules="$2"
	veindent
	local IFS="$__IFS"
	for ru in $rules ; do
		unset IFS
		ruN="$(_trim "${ru}")"
		[ -z "${ruN}" ] && continue
		vebegin "${cmd} ${ruN}"
		_ip $family rule ${cmd} ${ru}
		veend $?
		local IFS="$__IFS"
	done
	IFS="${OIFS}"
	veoutdent
}

_iproute2_policy_routing()
{
	# Kernel may not have IP built in
	if _netns [ -e /proc/net/route ]; then
		local rules="$(_get_array "rules_${IFVAR}")"
		if [ -n "${rules}" ]; then
			if ! _ip -4 rule list | grep -q "^"; then
				eerror "IP Policy Routing (CONFIG_IP_MULTIPLE_TABLES) needed for ip rule"
			else
				service_set_value "ip_rule" "${rules}"
				einfo "Adding IPv4 RPDB rules"
				_ip_rule_runner -4 add "${rules}"
			fi
		fi
		FAMILY="-4" IFACE="${IFACE}" _iproute2_route_flush
	fi

	# Kernel may not have IPv6 built in
	if [ -e /proc/net/ipv6_route ]; then
		local rules="$(_get_array "rules6_${IFVAR}")"
		if [ -n "${rules}" ]; then
			if ! _ip -6 rule list | grep -q "^"; then
				eerror "IPv6 Policy Routing (CONFIG_IPV6_MULTIPLE_TABLES) needed for ip rule"
			else
				service_set_value "ip6_rule" "${rules}"
				einfo "Adding IPv6 RPDB rules"
				_ip_rule_runner -6 add "${rules}"
			fi
		fi
		FAMILY="-6" IFACE="${IFACE}" _iproute2_route_flush
	fi
}

iproute2_pre_up()
{
	local netns
	_set_netns
	if [ -n "${netns}" ] && [ -e /sys/class/net/"${IFACE}" ] ; then
		local ip
		ip=$(command -v ip 2>/dev/null)

		if [ ! -e /run/netns/"${netns}" ]; then
			if ! "${ip}" netns add "${netns}"; then
				eerror "Could not create netns ${netns} for ${iface}"
				return 1
			fi

			if ! _ip link set up lo; then
				eerror "Could not enable lo interface in netns ${netns} for ${iface}"
				return 1
			fi
			if ! _ip route add 127.0.0.0/8 via 127.0.0.1 dev lo; then
				eerror "Could not enable lo interface in netns ${netns} for ${iface}"
				return 1
			fi
		fi

		if ! "${ip}" link set netns "${netns}" dev "${IFACE}"; then
			eerror "Could not move ${IFACE} to netns ${netns}"
			return 1
		fi
	fi
}

iproute2_pre_start()
{
	local tunnel=
	eval tunnel=\$iptunnel_${IFVAR}
	if [ -n "${tunnel}" ]; then
		# Set our base metric to 1000
		metric=1000
		# Bug#347657: If the mode is ipip6, ip6ip6, ip6gre or any, the -6 must
		# be passed to iproute2 during tunnel creation.
		# state 'any' does not exist for IPv4
		local ipproto=''
		case $tunnel in
			*mode\ ipip6*) ipproto='-6' ;;
			*mode\ ip6*) ipproto='-6' ;;
			*mode\ any*) ipproto='-6' ;;
		esac

		ebegin "Creating tunnel ${IFVAR}"
		_ip -v ${ipproto} tunnel add ${tunnel} name "${IFACE}"
		eend $? || return 1
		_up
	fi
	local link=
	eval link=\$iplink_${IFVAR}
	if [ -n "${link}" ]; then
		local ipproto=''
		case $tunnel in
			*mode\ ipip6*) ipproto='-6' ;;
			*mode\ ip6*) ipproto='-6' ;;
			*mode\ any*) ipproto='-6' ;;
		esac

		ebegin "Creating interface ${IFVAR}"
		_ip -v ${ipproto} link add "${IFACE}" ${link}
		eend $? || return 1
		_up
	fi

	# MTU support
	local mtu=
	eval mtu=\$mtu_${IFVAR}
	if [ -n "${mtu}" ]; then
		_ip -v link set dev "${IFACE}" mtu "${mtu}"
	fi

	# TX Queue Length support
	local len=
	eval len=\$txqueuelen_${IFVAR}
	if [ -n "${len}" ]; then
		_ip -v link set dev "${IFACE}" txqueuelen "${len}"
	fi

	local policyroute_order=
	eval policyroute_order=\$policy_rules_before_routes_${IFVAR}
	[ -z "$policyroute_order" ] && policyroute_order=${policy_rules_before_routes:-no}
	yesno "$policyroute_order" && _iproute2_policy_routing

	return 0
}

_iproute2_tunnel_delete() {
	ebegin "Destroying tunnel $1"
	_tunnel del "$1"
	eend $?
}

_iproute2_link_delete() {
	ebegin "Destroying interface $1"
	_ip -v $FAMILY link del dev "$1"
	eend $?
}

_iproute2_route_flush_supported() {
	case $FAMILY in
		-6) p='/proc/sys/net/ipv6/route/flush' ;;
		-4|*) p='/proc/sys/net/ipv4/route/flush' ;;
	esac
	test -f $p
}

_iproute2_route_flush() {
	if _iproute2_route_flush_supported; then
		_ip -v $FAMILY route flush table cache dev "${IFACE}"
	fi
}

_iproute2_route_undo() {
	local OIFS="${IFS}"
	local cmd cmdN
	local routes="$(service_get_value "nodev_routes")"

	veindent
	local IFS="$__IFS"
	for cmd in $routes ; do
		unset IFS
		cmdN="$(_trim "${cmd}")"
		[ -z "${cmdN}" ] && continue
		_ip -v ${cmd}
		local IFS="$__IFS"
	done
	IFS="${OIFS}"
	veoutdent
}

_iproute2_ipv6_tentative_output() {
	LC_ALL=C _ip -family inet6 addr show dev ${IFACE} tentative
}

_iproute2_ipv6_tentative()
{
	[ -n "$(_iproute2_ipv6_tentative_output)" ] && return 0

	return 1
}

iproute2_post_start()
{
	local _dad_timeout=

	eval _dad_timeout=\$dad_timeout_${IFVAR}
	_dad_timeout=${_dad_timeout:-${dad_timeout:-5}}

	local policyroute_order=
	eval policyroute_order=\$policy_rules_before_routes_${IFVAR}
	[ -z "$policyroute_order" ] && policyroute_order=${policy_rules_before_routes:-no}
	yesno "$policyroute_order" || _iproute2_policy_routing

	# This block must be non-fatal, otherwise the interface will not be
	# recorded as starting, and later services may be blocked.
	_iproute2_ipv6_tentative ; ret_tentative=$?
	if [ $ret_tentative -eq 0 -a "$EINFO_VERBOSE" = "yes" ]; then
		veinfo "Found tentative addresses:"
		_iproute2_ipv6_tentative_output
	fi
	# This could be nested, but the conditionals are very deep at that point.
	if [ $ret_tentative -eq 0 -a $_dad_timeout -le 0 ]; then
		einfo "Not waiting for DAD timeout on tentative IPv6 addresses (per conf.d/net dad_timeout)"
	elif [ $ret_tentative -eq 0 ]; then
		einfon "Waiting for tentative IPv6 addresses to complete DAD (${_dad_timeout} seconds) "
		while [ $_dad_timeout -gt 0 ]; do
			_iproute2_ipv6_tentative || break
			sleep 1
			: $(( _dad_timeout -= 1 ))
			printf "."
		done

		echo ""

		if [ $_dad_timeout -le 0 ]; then
			eend 1
		else
			eend 0
		fi
	fi

	return 0
}

iproute2_post_stop()
{
	# Remove routes added without dev
	_iproute2_route_undo

	# Kernel may not have IP built in
	if [ -e /proc/net/route ]; then
		local rules="$(service_get_value "ip_rule")"
		if [ -n "${rules}" ]; then
			einfo "Removing IPv4 RPDB rules"
			_ip_rule_runner -4 del "${rules}"
		fi

		# Only do something if the interface actually exist
		if _exists; then
			FAMILY="-4" IFACE="${IFACE}" _iproute2_route_flush
		fi
	fi

	# Kernel may not have IPv6 built in
	if [ -e /proc/net/ipv6_route ]; then
		local rules="$(service_get_value "ip6_rule")"
		if [ -n "${rules}" ]; then
			einfo "Removing IPv6 RPDB rules"
			_ip_rule_runner -6 del "${rules}"
		fi

		# Only do something if the interface actually exist
		if _exists; then
			FAMILY="-6" IFACE="${IFACE}" _iproute2_route_flush
		fi
	fi

	# Don't delete sit0 as it's a special tunnel
	if [ "${IFACE}" != "sit0" ]; then
		if [ -n "$(_ip tunnel show "${IFACE}" 2>/dev/null)" ]; then
			_iproute2_tunnel_delete "${IFACE}"
		elif  [ -n "$(_ip -6 tunnel show "${IFACE}" 2>/dev/null)" ]; then
			FAMILY=-6 _iproute2_tunnel_delete "${IFACE}"
		fi

		[ -n "$(_ip link show "${IFACE}" type gretap 2>/dev/null)" ] && _iproute2_link_delete "${IFACE}"
		[ -n "$(_ip link show "${IFACE}" type vxlan 2>/dev/null)" ] && _iproute2_link_delete "${IFACE}"
	fi
}

# Is the interface administratively/operationally up?
# The 'UP' status in ifconfig/iproute2 is the administrative status
# Operational state is available in iproute2 output as 'state UP', or the
# operstate sysfs variable.
# 0: up
# 1: down
# 2: invalid arguments
is_admin_up()
{
	local iface="$1"
	[ -z "$iface" ] && iface="$IFACE"
	_ip link show dev $iface | \
	sed -n '1,1{ /[<,]UP[,>]/{ q 0 }}; q 1; '
}

is_oper_up()
{
	local iface="$1"
	[ -z "$iface" ] && iface="$IFACE"
	read state </sys/class/net/"${iface}"/operstate
	[ "x$state" = "up" ]
}
