#!/bin/sh

FAKE_IP_STATE="${FAKE_NETWORK_STATE}/ip-state"
mkdir -p "$FAKE_IP_STATE"

promote_secondaries=true

not_implemented()
{
	echo "ip stub command: \"$1\" not implemented"
	exit 127
}

######################################################################

ip_link()
{
	case "$1" in
	set)
		shift
		# iface="$1"
		case "$2" in
		up) ip_link_set_up "$1" ;;
		down) ip_link_down_up "$1" ;;
		*) not_implemented "\"$2\" in \"$orig_args\"" ;;
		esac
		;;
	show)
		shift
		ip_link_show "$@"
		;;
	add*)
		shift
		ip_link_add "$@"
		;;
	del*)
		shift
		ip_link_delete "$@"
		;;
	*) not_implemented "$*" ;;
	esac
}

ip_link_add()
{
	_link=""
	_name=""
	_type=""

	while [ -n "$1" ]; do
		case "$1" in
		link)
			_link="$2"
			shift 2
			;;
		name)
			_name="$2"
			shift 2
			;;
		type)
			if [ "$2" != "vlan" ]; then
				not_implemented "link type $1"
			fi
			_type="$2"
			shift 2
			;;
		id) shift 2 ;;
		*) not_implemented "$1" ;;
		esac
	done

	case "$_type" in
	vlan)
		if [ -z "$_name" ] || [ -z "$_link" ]; then
			not_implemented "ip link add with null name or link"
		fi

		mkdir -p "${FAKE_IP_STATE}/interfaces-vlan"
		echo "$_link" >"${FAKE_IP_STATE}/interfaces-vlan/${_name}"
		ip_link_set_down "$_name"
		;;
	esac
}

ip_link_delete()
{
	mkdir -p "${FAKE_IP_STATE}/interfaces-deleted"
	touch "${FAKE_IP_STATE}/interfaces-deleted/$1"
	rm -f "${FAKE_IP_STATE}/interfaces-vlan/$1"
}

ip_link_set_up()
{
	rm -f "${FAKE_IP_STATE}/interfaces-down/$1"
	rm -f "${FAKE_IP_STATE}/interfaces-deleted/$1"
}

ip_link_set_down()
{
	rm -f "${FAKE_IP_STATE}/interfaces-deleted/$1"
	mkdir -p "${FAKE_IP_STATE}/interfaces-down"
	touch "${FAKE_IP_STATE}/interfaces-down/$1"
}

ip_link_show()
{
	dev="$1"
	if [ "$dev" = "dev" ] && [ -n "$2" ]; then
		dev="$2"
	fi

	if [ -e "${FAKE_IP_STATE}/interfaces-deleted/$dev" ]; then
		echo "Device \"${dev}\" does not exist." >&2
		exit 255
	fi

	if [ -r "${FAKE_IP_STATE}/interfaces-vlan/${dev}" ]; then
		read -r _link <"${FAKE_IP_STATE}/interfaces-vlan/${dev}"
		dev="${dev}@${_link}"
	fi

	_state="UP"
	_flags=",UP,LOWER_UP"
	if [ -e "${FAKE_IP_STATE}/interfaces-down/$dev" ]; then
		_state="DOWN"
		_flags=""
	fi
	case "$dev" in
	lo)
		_mac="00:00:00:00:00:00"
		_brd="00:00:00:00:00:00"
		_type="loopback"
		_state="UNKNOWN"
		_status="<LOOPBACK${_flags}>"
		_opts="mtu 65536 qdisc noqueue state ${_state}"
		;;
	*)
		_mac=$(echo "$dev" | cksum | sed -r -e 's@(..)(..)(..).*@fe:fe:fe:\1:\2:\3@')
		_brd="ff:ff:ff:ff:ff:ff"
		_type="ether"
		_status="<BROADCAST,MULTICAST${_flags}>"
		_opts="mtu 1500 qdisc pfifo_fast state ${_state} qlen 1000"
		;;
	esac

	if $brief; then
		printf '%-16s %-14s %-17s %s\n' \
			"$dev" "$_status" "$_mac" "$_status"
	else
		echo "${n:-42}: ${dev}: ${_status} ${_opts}"
		echo "    link/${_type} ${_mac} brd ${_brd}"
	fi
}

# This is incomplete because it doesn't actually look up table ids in
# /etc/iproute2/rt_tables.  The rules/routes are actually associated
# with the name instead of the number.  However, we include a variable
# to fake a bad table id.
[ -n "$IP_ROUTE_BAD_TABLE_ID" ] || IP_ROUTE_BAD_TABLE_ID=false

ip_check_table()
{
	_cmd="$1"

	if [ "$_cmd" = "route" ] && [ -z "$_table" ]; then
		_table="main"
	fi

	[ -n "$_table" ] || not_implemented "ip rule/route without \"table\""

	# Only allow tables names from 13.per_ip_routing and "main".  This
	# is a cheap way of avoiding implementing the default/local
	# tables.
	case "$_table" in
	ctdb.* | main)
		if $IP_ROUTE_BAD_TABLE_ID; then
			# Ouch.  Simulate inconsistent errors from ip.  :-(
			case "$_cmd" in
			route)
				echo "Error: argument \"${_table}\" is wrong: table id value is invalid" >&2

				;;
			*)
				echo "Error: argument \"${_table}\" is wrong: invalid table ID" >&2
				;;
			esac
			exit 255
		fi
		;;
	*) not_implemented "table=${_table} ${orig_args}" ;;
	esac
}

######################################################################

ip_addr()
{
	case "$1" in
	show | list | "")
		shift
		ip_addr_show "$@"
		;;
	add*)
		shift
		ip_addr_add "$@"
		;;
	del*)
		shift
		ip_addr_del "$@"
		;;
	*) not_implemented "\"$1\" in \"$orig_args\"" ;;
	esac
}

ip_addr_show()
{
	dev=""
	primary=true
	secondary=true
	_to=""

	if $brief; then
		not_implemented "ip -br addr show in \"$orig_args\""
	fi

	while [ -n "$1" ]; do
		case "$1" in
		dev)
			dev="$2"
			shift 2
			;;
			# Do stupid things and stupid things will happen!
		primary)
			primary=true
			secondary=false
			shift
			;;
		secondary)
			secondary=true
			primary=false
			shift
			;;
		to)
			_to="$2"
			shift 2
			;;
		*)
			# Assume an interface name
			dev="$1"
			shift 1
			;;
		esac
	done
	devices="$dev"
	if [ -z "$devices" ]; then
		# No device specified?  Get all the primaries...
		devices=$(find "${FAKE_IP_STATE}/addresses" -name "*-primary" |
			sed -e 's@.*/@@' -e 's@-.*-primary$@@' |
			sort -u)
	fi
	calc_brd()
	{
		case "${local#*/}" in
		24) brd="${local%.*}.255" ;;
		32) brd="" ;;
		*) not_implemented "list ... fake bits other than 24/32: ${local#*/}" ;;
		esac
	}
	show_iface()
	{
		ip_link_show "$dev"

		nets=$(find "${FAKE_IP_STATE}/addresses" -name "${dev}-*-primary" |
			sed -e 's@.*/@@' -e "s@${dev}-\(.*\)-primary\$@\1@")

		for net in $nets; do
			pf="${FAKE_IP_STATE}/addresses/${dev}-${net}-primary"
			sf="${FAKE_IP_STATE}/addresses/${dev}-${net}-secondary"
			if $primary && [ -r "$pf" ]; then
				read -r local scope <"$pf"
				if [ -z "$_to" ] || [ "${_to%/*}" = "${local%/*}" ]; then
					calc_brd
					echo "    inet ${local} ${brd:+brd ${brd} }scope ${scope} ${dev}"
				fi
			fi
			if $secondary && [ -r "$sf" ]; then
				while read -r local scope; do
					if [ -z "$_to" ] || [ "${_to%/*}" = "${local%/*}" ]; then
						calc_brd
						echo "    inet ${local} ${brd:+brd }${brd} scope ${scope} secondary ${dev}"
					fi
				done <"$sf"
			fi
			if [ -z "$_to" ]; then
				echo "       valid_lft forever preferred_lft forever"
			fi
		done
	}
	n=1
	for dev in $devices; do
		if [ -z "$_to" ] ||
			grep -F "${_to%/*}/" "${FAKE_IP_STATE}/addresses/${dev}-"* >/dev/null; then
			show_iface
		fi
		n=$((n + 1))
	done
}

# Copied from 13.per_ip_routing for now... so this is lazy testing  :-(
ipv4_host_addr_to_net()
{
	_addr="$1"

	_host="${_addr%/*}"
	_maskbits="${_addr#*/}"

	# Convert the host address to an unsigned long by splitting out
	# the octets and doing the math.
	_host_ul=0
	# Want word splitting here
	# shellcheck disable=SC2086
	for _o in $(
		export IFS="."
		echo $_host
	); do
		_host_ul=$(((_host_ul << 8) + _o)) # work around Emacs color bug
	done

	# Calculate the mask and apply it.
	_mask_ul=$((0xffffffff << (32 - _maskbits)))
	_net_ul=$((_host_ul & _mask_ul))

	# Now convert to a network address one byte at a time.
	_net=""
	for _o in $(seq 1 4); do
		_net="$((_net_ul & 255))${_net:+.}${_net}"
		_net_ul=$((_net_ul >> 8))
	done

	echo "${_net}/${_maskbits}"
}

ip_addr_add()
{
	local=""
	dev=""
	brd=""
	scope="global"
	while [ -n "$1" ]; do
		case "$1" in
		*.*.*.*/*)
			local="$1"
			shift
			;;
		local)
			local="$2"
			shift 2
			;;
		broadcast | brd)
			# For now assume this is always '+'.
			if [ "$2" != "+" ]; then
				not_implemented "addr add ... brd $2 ..."
			fi
			shift 2
			;;
		dev)
			dev="$2"
			shift 2
			;;
		scope)
			scope="$2"
			shift 2
			;;
		*)
			not_implemented "$@"
			;;
		esac
	done
	if [ -z "$dev" ]; then
		not_implemented "addr add (without dev)"
	fi
	mkdir -p "${FAKE_IP_STATE}/addresses"
	net_str=$(ipv4_host_addr_to_net "$local")
	net_str=$(echo "$net_str" | sed -e 's@/@_@')
	pf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-primary"
	sf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-secondary"
	# We could lock here... but we should be the only ones playing
	# around here with these stubs.
	if [ ! -f "$pf" ]; then
		echo "$local $scope" >"$pf"
	elif grep -Fq "$local" "$pf"; then
		echo "RTNETLINK answers: File exists" >&2
		exit 254
	elif [ -f "$sf" ] && grep -Fq "$local" "$sf"; then
		echo "RTNETLINK answers: File exists" >&2
		exit 254
	else
		echo "$local $scope" >>"$sf"
	fi
}

ip_addr_del()
{
	local=""
	dev=""
	while [ -n "$1" ]; do
		case "$1" in
		*.*.*.*/*)
			local="$1"
			shift
			;;
		local)
			local="$2"
			shift 2
			;;
		dev)
			dev="$2"
			shift 2
			;;
		*)
			not_implemented "addr del ... $1 ..."
			;;
		esac
	done
	if [ -z "$dev" ]; then
		not_implemented "addr del (without dev)"
	fi
	mkdir -p "${FAKE_IP_STATE}/addresses"
	net_str=$(ipv4_host_addr_to_net "$local")
	net_str=$(echo "$net_str" | sed -e 's@/@_@')
	pf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-primary"
	sf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-secondary"
	# We could lock here... but we should be the only ones playing
	# around here with these stubs.
	if [ ! -f "$pf" ]; then
		echo "RTNETLINK answers: Cannot assign requested address" >&2
		exit 254
	elif grep -Fq "$local" "$pf"; then
		if $promote_secondaries && [ -s "$sf" ]; then
			head -n 1 "$sf" >"$pf"
			sed -i -e '1d' "$sf"
		else
			# Remove primaries AND SECONDARIES.
			rm -f "$pf" "$sf"
		fi
	elif [ -f "$sf" ] && grep -Fq "$local" "$sf"; then
		grep -Fv "$local" "$sf" >"${sf}.new"
		mv "${sf}.new" "$sf"
	else
		echo "RTNETLINK answers: Cannot assign requested address" >&2
		exit 254
	fi
}

######################################################################

ip_rule()
{
	case "$1" in
	show | list | "")
		shift
		ip_rule_show "$@"
		;;
	add)
		shift
		ip_rule_add "$@"
		;;
	del*)
		shift
		ip_rule_del "$@"
		;;
	*) not_implemented "$1 in \"$orig_args\"" ;;
	esac

}

# All non-default rules are in $FAKE_IP_STATE_RULES/rules.  As with
# the real version, rules can be repeated.  Deleting just deletes the
# 1st match.

ip_rule_show()
{
	if $brief; then
		not_implemented "ip -br rule show in \"$orig_args\""
	fi

	ip_rule_show_1()
	{
		_pre="$1"
		_table="$2"
		_selectors="$3"
		# potentially more options

		printf "%d:\t%s lookup %s \n" "$_pre" "$_selectors" "$_table"
	}

	ip_rule_show_some()
	{
		_min="$1"
		_max="$2"

		[ -f "${FAKE_IP_STATE}/rules" ] || return

		while read -r _pre _table _selectors; do
			# Only print those in range
			if [ "$_min" -le "$_pre" ] &&
				[ "$_pre" -le "$_max" ]; then
				ip_rule_show_1 "$_pre" "$_table" "$_selectors"
			fi
		done <"${FAKE_IP_STATE}/rules"
	}

	ip_rule_show_1 0 "local" "from all"

	ip_rule_show_some 1 32765

	ip_rule_show_1 32766 "main" "from all"
	ip_rule_show_1 32767 "default" "from all"

	ip_rule_show_some 32768 2147483648
}

ip_rule_common()
{
	_from=""
	_pre=""
	_table=""
	while [ -n "$1" ]; do
		case "$1" in
		from)
			_from="$2"
			shift 2
			;;
		pref)
			_pre="$2"
			shift 2
			;;
		table)
			_table="$2"
			shift 2
			;;
		*) not_implemented "$1 in \"$orig_args\"" ;;
		esac
	done

	[ -n "$_pre" ] || not_implemented "ip rule without \"pref\""
	ip_check_table "rule"
	# Relax this if more selectors added later...
	[ -n "$_from" ] || not_implemented "ip rule without \"from\""
}

ip_rule_add()
{
	ip_rule_common "$@"

	_f="${FAKE_IP_STATE}/rules"
	touch "$_f"
	(
		flock 0
		# Filter order must be consistent with the comparison in ip_rule_del()
		echo "$_pre $_table${_from:+ from }$_from" >>"$_f"
	) <"$_f"
}

ip_rule_del()
{
	ip_rule_common "$@"

	_f="${FAKE_IP_STATE}/rules"
	touch "$_f"
	# ShellCheck doesn't understand this flock pattern
	# shellcheck disable=SC2094
	(
		flock 0
		_tmp="${_f}.new"
		: >"$_tmp"
		_found=false
		while read -r _p _t _s; do
			if ! $_found &&
				[ "$_p" = "$_pre" ] && [ "$_t" = "$_table" ] &&
				[ "$_s" = "${_from:+from }$_from" ]; then
				# Found.  Skip this one but not future ones.
				_found=true
			else
				echo "$_p $_t $_s" >>"$_tmp"
			fi
		done
		if cmp -s "$_tmp" "$_f"; then
			# No changes, must not have found what we wanted to delete
			echo "RTNETLINK answers: No such file or directory" >&2
			rm -f "$_tmp"
			exit 2
		else
			mv "$_tmp" "$_f"
		fi
	) <"$_f" || exit $?
}

######################################################################

ip_route()
{
	case "$1" in
	show | list)
		shift
		ip_route_show "$@"
		;;
	flush)
		shift
		ip_route_flush "$@"
		;;
	add)
		shift
		ip_route_add "$@"
		;;
	del*)
		shift
		ip_route_del "$@"
		;;
	*) not_implemented "$1 in \"ip route\"" ;;
	esac
}

ip_route_common()
{
	if [ "$1" = table ]; then
		_table="$2"
		shift 2
	fi

	ip_check_table "route"
}

# Routes are in a file per table in the directory
# $FAKE_IP_STATE/routes.  These routes just use the table ID
# that is passed and don't do any lookup.  This could be "improved" if
# necessary.

ip_route_show()
{
	ip_route_common "$@"

	# Missing file is just an empty table
	sort "$FAKE_IP_STATE/routes/${_table}" 2>/dev/null || true
}

ip_route_flush()
{
	ip_route_common "$@"

	rm -f "$FAKE_IP_STATE/routes/${_table}"
}

ip_route_add()
{
	_prefix=""
	_dev=""
	_gw=""
	_table=""
	_metric=""

	while [ -n "$1" ]; do
		case "$1" in
		*.*.*.*/* | *.*.*.*)
			_prefix="$1"
			shift 1
			;;
		local)
			_prefix="$2"
			shift 2
			;;
		dev)
			_dev="$2"
			shift 2
			;;
		via)
			_gw="$2"
			shift 2
			;;
		table)
			_table="$2"
			shift 2
			;;
		metric)
			_metric="$2"
			shift 2
			;;
		*) not_implemented "$1 in \"$orig_args\"" ;;
		esac
	done

	ip_check_table "route"
	[ -n "$_prefix" ] || not_implemented "ip route without inet prefix in \"$orig_args\""
	# This can't be easily deduced, so print some garbage.
	[ -n "$_dev" ] || _dev="ethXXX"

	# Alias or add missing bits
	case "$_prefix" in
	0.0.0.0/0) _prefix="default" ;;
	*/*) : ;;
	*) _prefix="${_prefix}/32" ;;
	esac

	_f="$FAKE_IP_STATE/routes/${_table}"
	mkdir -p "$FAKE_IP_STATE/routes"
	touch "$_f"

	# Check for duplicate
	_prefix_regexp=$(echo "^${_prefix}" | sed -e 's@\.@\\.@g')
	if [ -n "$_metric" ]; then
		_prefix_regexp="${_prefix_regexp} .*metric ${_metric} "
	fi
	if grep -q "$_prefix_regexp" "$_f"; then
		echo "RTNETLINK answers: File exists" >&2
		exit 1
	fi

	(
		flock 0

		_out="${_prefix} "
		[ -z "$_gw" ] || _out="${_out}via ${_gw} "
		[ -z "$_dev" ] || _out="${_out}dev ${_dev} "
		[ -n "$_gw" ] || _out="${_out} scope link "
		[ -z "$_metric" ] || _out="${_out} metric ${_metric} "
		echo "$_out" >>"$_f"
	) <"$_f"
}

ip_route_del()
{
	_prefix=""
	_dev=""
	_gw=""
	_table=""
	_metric=""

	while [ -n "$1" ]; do
		case "$1" in
		*.*.*.*/* | *.*.*.*)
			_prefix="$1"
			shift 1
			;;
		local)
			_prefix="$2"
			shift 2
			;;
		dev)
			_dev="$2"
			shift 2
			;;
		via)
			_gw="$2"
			shift 2
			;;
		table)
			_table="$2"
			shift 2
			;;
		metric)
			_metric="$2"
			shift 2
			;;
		*) not_implemented "$1 in \"$orig_args\"" ;;
		esac
	done

	ip_check_table "route"
	[ -n "$_prefix" ] || not_implemented "ip route without inet prefix in \"$orig_args\""
	# This can't be easily deduced, so print some garbage.
	[ -n "$_dev" ] || _dev="ethXXX"

	# Alias or add missing bits
	case "$_prefix" in
	0.0.0.0/0) _prefix="default" ;;
	*/*) : ;;
	*) _prefix="${_prefix}/32" ;;
	esac

	_f="$FAKE_IP_STATE/routes/${_table}"
	mkdir -p "$FAKE_IP_STATE/routes"
	touch "$_f"

	# ShellCheck doesn't understand this flock pattern
	# shellcheck disable=SC2094
	(
		flock 0

		# Escape some dots
		[ -z "$_gw" ] || _gw=$(echo "$_gw" | sed -e 's@\.@\\.@g')
		_prefix=$(echo "$_prefix" | sed -e 's@\.@\\.@g' -e 's@/@\\/@')

		_re="^${_prefix}\>.*"
		[ -z "$_gw" ] || _re="${_re}\<via ${_gw}\>.*"
		[ -z "$_dev" ] || _re="${_re}\<dev ${_dev}\>.*"
		[ -z "$_metric" ] || _re="${_re}.*\<metric ${_metric}\>.*"
		sed -i -e "/${_re}/d" "$_f"
	) <"$_f"
}

######################################################################

orig_args="$*"

brief=false
case "$1" in
-br*)
	brief=true
	shift
	;;
esac

case "$1" in
link)
	shift
	ip_link "$@"
	;;
addr*)
	shift
	ip_addr "$@"
	;;
rule)
	shift
	ip_rule "$@"
	;;
route)
	shift
	ip_route "$@"
	;;
*) not_implemented "$1" ;;
esac

exit 0
