#!/bin/sh
#
# ip-brctl: Implementation of brctl from bridge-utils as iproute2 wrapper
#
# Copyright (c) 2019 Red Hat, Inc.
# Author: Stefano Brivio <sbrivio@redhat.com>
# https://patchwork.ozlabs.org/patch/1027627/
#
# SPDX-License-Identifier: GPL-2.0

# Global variables #############################################################

# These will be replaced by Makefile on install with ip and bridge install paths
IP="/sbin/ip"
BRIDGE="/usr/sbin/bridge"

usage_text="Usage: brctl [commands]
commands:
	addbr     	<bridge>		add bridge
	delbr     	<bridge>		delete bridge
	addif     	<bridge> <device>	add interface to bridge
	delif     	<bridge> <device>	delete interface from bridge
	hairpin   	<bridge> <port> {on|off}	turn hairpin on/off
	setageing 	<bridge> <time>		set ageing time
	setbridgeprio	<bridge> <prio>		set bridge priority
	setfd     	<bridge> <time>		set bridge forward delay
	sethello  	<bridge> <time>		set hello time
	setmaxage 	<bridge> <time>		set max message age
	setpathcost	<bridge> <port> <cost>	set path cost
	setportprio	<bridge> <port> <prio>	set port priority
	show      	[ <bridge> ]		show a list of bridges
	showmacs  	<bridge>		show a list of mac addrs
	showstp   	<bridge>		show bridge stp info
	stp       	<bridge> {on|off}	turn stp on/off"

# List of commands prefixed by minimum number of arguments
commands="1 addbr 1 delbr 2 addif 2 delif 3 hairpin 2 setageing 2 setbridgeprio
	  2 setfd 2 sethello 2 setmaxage 3 setpathcost 3 setportprio 0 show
	  1 showmacs 1 showstp 2 stp"


# Helper functions #############################################################

usage() {
	echo "${usage_text}"
	exit "${1:-1}"
}

# Print a single line from usage for given command
usage_one() {
	command="${1}"

	ifs="${IFS}"
	IFS='
'
	for line in ${usage_text}; do
		case ${line} in
		"	${command}"*)
			line="${line#	${command}*}"
			while [ "${line}" != "${line#[[:blank:]]*}" ]; do
				line="${line#[[:blank:]]*}"
			done
			echo "Usage: brctl ${command} ${line}"
			break
			;;
		esac
	done
	IFS="${ifs}"

	exit 1
}

# Print to standard error and exit
err() {
	echo "${@}" > /dev/stderr
	exit 1
}

# Output token following the given one, from a space-separated string
parse_next() {
	needle=${1}
	str=${2}

	next=0
	ifs="${IFS}"
	IFS=' '
	for token in ${str}; do
		[ ${next} -eq 1 ] && echo "${token}" && break
		[ "${token}" = "${needle}" ] && next=1
	done
	IFS="${ifs}"
}

# Output value for given ip-link attribute, for the given device
parse_iplink() {
	attr="${1}"
	dev="${2}"

	out="$(${IP} -d link show dev "${dev}" 2>/dev/null)"
	parse_next "${attr}" "${out}"
}

# Once starting token is matched, for each token x with index n = 2 * k, assign
# value of token with index n + 1 to a variable named by the value of x, using
# state variables parse_assign_start and parse_assign_prev. Pass one token at
# a time.
parse_assign() {
	token="${1}"
	start_token="${2}"

	if [ "${token}" = "${start_token}" ]; then
		parse_assign_start=1
		parse_assign_prev=
	elif [ ${parse_assign_start} -eq 0 ]; then
		:
	elif [ -z "${parse_assign_prev}" ]; then
		parse_assign_prev="${token}"
	else
		eval "${parse_assign_prev}"="${token}"
		parse_assign_prev=
	fi
}

# Execute ip-link command with given arguments. On failure, print returned error
# message prefixed by given string and exit.
exec_iplink() {
	err_prefix="${1}"
	shift

	err_out="$("${IP}" link "$@" 2>&1)" || err "${err_prefix}: ${err_out#*:}"
}

# Print passed error string and exit if the given device does or does not exist,
# depending on the condition given. Device type matching is optional.
err_dev_exists() {
	cond="${1}"
	dev="${2}"
	err_string="${3}"
	opt_type="${4}"

	[ -n "${opt_type}" ] && opt_type="type ${opt_type}" || opt_type=

	if [ -n "$(${IP} link show dev "${dev}" ${opt_type} 2>/dev/null)" ]; then
		[ "${cond}" = "y" ] && err "${err_string}"
	else
		[ "${cond}" = "n" ] && err "${err_string}"
	fi
}


# Type checks and conversions ##################################################

# Don't attempt operations on values exceeding limits for a signed long. From
# POSIX.1-2017, XCU, par. 2.6.4 Arithmetic Expansion:
#
#	Only signed long integer arithmetic is required.
#
# On failure, print returned error message prefixed by given string and exit.
check_long() {
	in="${1}"
	err_prefix="${2}"
	int="${in%%.*}"

	# Compare the integer part against signed long max without arithmetic
	# expansion, which may itself overflow on large values.
	long_max=2147483647
	if [ ${#int} -gt ${#long_max} ] || \
	   { [ ${#int} -eq ${#long_max} ] && [ "${int}" -gt "${long_max}" ]; }; then
		err "${err_prefix}: Numerical result out of range"
	fi
}

# On failure, print passed error string and exit
check_float() {
	in="${1}"
	err_string="${2}"

	case ${in} in
	""|.*|*[!0-9.]*) err "${err_string}" ;;
	*) ;;
	esac
}

# Convert values allowed as boolean to given strings for false and true values
make_bool() {
	in="${1}"
	str_false="${2}"
	str_true="${3}"

	case ${in} in
	off|no|0) echo "${str_false}" ;;
	on|yes|1) echo "${str_true}" ;;
	*) err "expect on/off for argument" ;;
	esac
}

# Divide by 100, output number with two fractional digits
make_float() {
	in="${1}"

	int=$((in / 100))
	frac=$((in % 100))
	[ ${#frac} -eq 1 ] && frac="0${frac}"
	echo "${int}.${frac}"
}

# Multiply given float by 100. Multiple decimal separators are allowed, anything
# after the second one is discarded.
brctl_timeval() {
	in="${1}"

	# Drop second decimal separator and following digits, if any
	drop=${in#[0-9]*.[0-9]*.}
	time=${in%%.${drop}}

	int=${time%.*}

	# Take up to two digits from fractional part
	frac=${time#*.}
	drop=${frac#[0-9][0-9]*}
	frac=${frac%%${drop}}
	frac=${frac:-0}
	[ "${frac}" -lt 10 ] && frac=$((frac * 10))

	echo "$((int * 100 + frac))"
}

# Strip colons from MAC address in bridge identifiers, pad bytes with 0
fixup_id() {
	in="${1}"

	ifs="${IFS}"
	IFS=':'
	out=
	for byte in ${in}; do
		[ ${#byte} -eq 1 ] && byte="0${byte}"
		out="${out}${byte}"
	done
	IFS="${ifs}"
	echo "${out}"
}


# Display functions ############################################################

# Pad string to given width -- if not given, width is 7 if float, 4 otherwise
pad_width() {
	str="${1}"
	[ "${str}" != "${str%.*}" ] && width=${2:-7} || width=${2:-4}

	len=${#str}
	while [ "${len}" -lt "${width}" ]; do
		str=" ${str}"
		len=$((len + 1))
	done
	echo "${str}"
}

# Dump bridge information from set variables in 'brctl showstp' format
#
# shellcheck disable=SC2154 # shellcheck won't see variables we set under eval
dump_bridge() {
	name="${1}"

	for i in bridge_id designated_root; do
		eval ${i}="\$(fixup_id \$${i})"
	done
	for i in max_age hello_time forward_delay ageing_time; do
		eval ${i}="\$(make_float \$${i})"
	done
	for i in root_port root_path_cost max_age hello_time forward_delay \
		 ageing_time hello_timer tcn_timer topology_change_timer \
		 gc_timer; do
		eval ${i}=\""\$(pad_width \$${i})"\"
	done
	flags=
	[ "${topology_change}" != "0" ] && flags="TOPOLOGY_CHANGE "
	[ "${topology_change_detected}" -ne "0" ] && flags="${flags}TOPOLOGY_CHANGE_DETECTED "

	echo "${name}"
	echo " bridge id		${bridge_id}"
	echo " designated root	${designated_root}"
	echo " root port		${root_port}			path cost		${root_path_cost}"
	echo " max age		${max_age}			bridge max age		${max_age}"
	echo " hello time		${hello_time}			bridge hello time	${hello_time}"
	echo " forward delay		${forward_delay}			bridge forward delay	${forward_delay}"
	echo " ageing time		${ageing_time}"
	echo " hello timer		${hello_timer}			tcn timer		${tcn_timer}"
	echo " topology change timer	${topology_change_timer}			gc timer		${gc_timer}"
	echo " flags			${flags}"
	echo
	echo
}

# Dump port information from set variables in 'brctl showstp' format
#
# shellcheck disable=SC2154 # shellcheck won't see variables we set under eval
dump_port() {
	for i in designated_root designated_bridge; do
		eval ${i}="\$(fixup_id \$${i})"
	done

	for i in message_age_timer cost message_age_timer forward_delay_timer \
		 designated_cost hold_timer; do
		eval ${i}=\""\$(pad_width \$${i})"\"
	done

	flags=
	[ "${config_pending}" != "0" ] && flags="CONFIG_PENDING "
	[ "${topology_change_ack}" -ne "0" ] && flags="${flags}TOPOLOGY_CHANGE_ACK "

	echo "${port_name%*:} (${port_no#0x*})"
	echo " port id		${port_id#0x*}			state		$(pad_width "${state}" 15)"
	echo " designated root	${designated_root}	path cost		${cost}"
	echo " designated bridge	${designated_bridge}	message age timer	${message_age_timer}"
	echo " designated port	$((8000 + designated_port % 32768))			forward delay timer	${forward_delay_timer}"
	echo " designated cost	${designated_cost}			hold timer		${hold_timer}"
	echo " flags ${flags}"
	[ "${hairpin}" != "off" ] && echo " hairpin mode		$(pad_width 1)"
	echo
}


# Commands #####################################################################

cmd_addbr() {
	err_dev_exists y "${1}" "device ${1} already exists; can't create bridge with the same name"

	exec_iplink "add bridge failed" add "${1}" type bridge
}

cmd_delbr() {
	err_dev_exists n "${1}" "bridge ${1} doesn't exist; can't delete it"
	if [ "$(parse_iplink state "${1}")" != "DOWN" ]; then
		err "bridge ${1} is still up; can't delete it"
	fi

	exec_iplink "can't delete bridge ${1}" del "${1}"
}

cmd_addif() {
	err_dev_exists n "${1}" "bridge ${1} does not exist!"
	err_dev_exists n "${2}" "interface ${2} does not exist!"
	if [ -n "$(parse_iplink master "${2}")" ]; then
		err "device ${2} is already a member of a bridge; can't enslave it to bridge ${1}."
	fi
	err_dev_exists y "${2}" "device ${2} is a bridge device itself; can't enslave a bridge device to a bridge device." bridge

	exec_iplink "can't add ${2} to bridge ${1}" set "${2}" master "${1}"
}

cmd_delif() {
	err_dev_exists n "${1}" "bridge ${1} does not exist!"
	err_dev_exists n "${2}" "interface ${2} does not exist!"
	if [ "$(parse_iplink master "${2}")" != "${1}" ]; then
		err "device ${2} is not a slave of ${1}"
	fi

	exec_iplink "can't delete ${2} from ${1}" set "${2}" nomaster
}

cmd_hairpin() {
	hairpin="$(make_bool "${3}" off on)"
	[ -z "${hairpin}" ] && exit 1
	err_dev_exists n "${2}" "interface ${2} does not exist!"
	err_dev_exists n "${1}" "bridge ${1} does not exist!"

	exec_iplink "can't set ${2} to hairpin on bridge ${1}" \
		set "${2}" type bridge_slave hairpin "${hairpin}"
}

cmd_setageing() {
	check_long "${2}" "set ageing time failed"
	check_float "${2}" "bad ageing time value"
	err_dev_exists n "${1}" "set ageing time failed: No such device"

	exec_iplink "set ageing time failed" \
		set "${1}" type bridge ageing_time "$(brctl_timeval "${2}")"
}

cmd_setbridgeprio() {
	check_long "${2}" "set bridge priority failed"
	check_float "${2}" "bad priority"
	err_dev_exists n "${1}" "set bridge priority failed: No such device"

	prio=${2%%.*}
	prio=$((prio % 65536))

	exec_iplink "set bridge priority failed" \
		set "${1}" type bridge priority "${prio}"
}

cmd_setfd() {
	check_long "${2}" "set forward delay failed"
	check_float "${2}" "bad forward delay value"
	err_dev_exists n "${1}" "set forward delay failed: No such device"

	exec_iplink "set forward delay failed" \
		set "${1}" type bridge forward_delay "$(brctl_timeval "${2}")"
}

cmd_sethello() {
	check_long "${2}" "set hello timer failed"
	check_float "${2}" "bad hello timer value"
	err_dev_exists n "${1}" "set hello timer failed: No such device"

	exec_iplink "set hello timer failed" \
		set "${1}" type bridge hello_time "$(brctl_timeval "${2}")"
}

cmd_setmaxage() {
	check_long "${2}" "set max age failed"
	check_float "${2}" "bad max age value"
	err_dev_exists n "${1}" "set max age failed: No such device"

	exec_iplink "set max age failed" \
		set "${1}" type bridge max_age "$(brctl_timeval "${2}")"
}

cmd_setpathcost() {
	check_long "${3}" "set path cost failed"
	check_float "${3}" "bad path cost value"
	err_dev_exists n "${2}" "set path cost failed: No such device"

	cost=${3%%.*}

	exec_iplink "set path cost failed" \
		set "${2}" type bridge_slave cost "${cost}"
}

cmd_setportprio() {
	check_long "${3}" "set port priority failed"
	check_float "${3}" "bad path priority value"
	err_dev_exists n "${2}" "set port priority failed: No such device"

	prio=${3%%.*}
	[ "${prio}" -ge 64 ] && err "set port priority failed: Numerical result out of range"

	exec_iplink "set port priority failed" \
		set "${2}" type bridge_slave priority "${prio}"
}

cmd_stp() {
	stp="$(make_bool "${2}" 0 1)"
	[ -z "${stp}" ] && exit 1
	err_dev_exists n "${1}" "set stp status failed: No such device"

	exec_iplink "set stp status failed" \
		set "${1}" type bridge stp_state "${stp}"
}

cmd_showstp() {
	parse_assign_start=0
	for t in $(${IP} -d link show "${1}" 2>/dev/null); do
		parse_assign "${t}" bridge
	done
	[ ${parse_assign_start} -eq 0 ] && err "${1}: can't get info No such device"

	dump_bridge "${1}"

	parse_assign_start=0
	for t in $(${IP} -d link show type bridge_slave master "${1}" 2>/dev/null); do
		case ${t} in
		[0-9]*:)
			[ -n "${port_name}" ] && dump_port
			port_name=
			parse_assign_start=0
			continue
			;;
		esac
		[ -z "${port_name}" ] && port_name="${t}"

		parse_assign "${t}" bridge_slave
	done

	[ -n "${port_name}" ] && dump_port
}

cmd_show() {
	dev="${1}"

	if [ -n "${dev}" ]; then
		err_dev_exists n "${dev}" "bridge ${dev} does not exist!"
		err_dev_exists n "${dev}" "device ${dev} is not a bridge!" bridge
	fi

	echo "bridge name	bridge id		STP enabled	interfaces"
	ifs="${IFS}"
	IFS='
'
	for line in $(${IP} -br link show type bridge ${dev:+"${dev}"} 2>/dev/null); do
		name="${line%% *}"
		id="$(fixup_id "$(parse_iplink bridge_id "${name}")")"
		[ "$(parse_iplink stp_state "${name}")" = "0" ] && stp="no" || stp="yes"
		first=1
		for s in $(${IP} -br link show type bridge_slave master "${name}"); do
			if [ ${first} -eq 1 ]; then
				echo "${name}		${id}	${stp}		${s%% *}"
				first=0
			else
				echo "							${s%% *}"
			fi
		done
		[ ${first} -eq 1 ] && echo "${name}		${id}	${stp}"
	done
	IFS="${ifs}"
}

cmd_showmacs() {
	err_dev_exists n "${1}" "read of forward table failed: No such device"

	echo "port no	mac addr		is local?	ageing timer"
	ifs="${IFS}"
	IFS='
'
	for s in $(${IP} -br link show type bridge_slave master "${1}"); do
		for fdb_entry in $(${BRIDGE} -s fdb show dev "${s%% *}"); do
			case ${fdb_entry} in
			*" ${1} "*) ;;
			*) continue ;;
			esac

			case ${fdb_entry} in
			*" permanent"*)
				is_local="yes"
				timer="$(pad_width "0.00")"
				;;
			*)
				is_local="no"
				timer="$(parse_next used "${fdb_entry}")"
				timer="$(pad_width "${timer#*/}.00")"
				;;
			esac

			port_no="$(parse_iplink port_no "${s%% *}")"
			port_no="${port_no#0x*}"

			echo "$(pad_width "${port_no}" 3)	${fdb_entry%% *}	${is_local}		${timer}"
		done
	done
	IFS="${ifs}"
}


# Start here ###################################################################

cmdname="${1}"
[ -z "${cmdname}" ] && usage

for arg do
	case ${arg} in
	"-V"|"--v"*)
		echo "ip-link wrapper, compatible with bridge-utils, 1.6"
		exit 0
		;;
	"-h"|"--h"*)
		usage 0
		;;
	"--")
		usage
		;;
	"-")
		break
		;;
	"-"*)
		echo "${0}: unrecognized option '${arg}'" >/dev/stderr
		echo "Unknown option '?'" >/dev/stderr
		usage
		;;
	esac
done

found=0
min_arg=
for cmd in ${commands}; do
	[ -n "${min_arg}" ] && [ "${cmd}" = "${cmdname}" ] && found=1 && break
	min_arg="${cmd}"
done
[ ${found} -eq 0 ] && echo "never heard of command [${cmdname}]" && usage
[ ${#} -le "${min_arg}" ] && echo "Incorrect number of arguments for command" && usage_one "${cmd}"

shift
eval "cmd_${cmdname}" "${@}"

exit 0
