#!/bin/sh

# PROVIDE:	ngbuddy
# REQUIRE:	netif
# BEFORE:	routing

# ngbuddy 1.1
#
# See ngbuddy(8) for configuration details. For configuration, use
# `service ngbuddy enable` or add the following to /etc/rc.conf[.local]:
#
# ngbuddy_enable="YES"
# ngbuddy_BRIDGE_if="IF"
#	Link a new BRIDGE to interface IF.
#	If IF does not exist, create an ng_eiface device.
#
# ngbuddy_BRIDGE_list="IF1 IF2 ..."
#	Create additional ng_eiface devices attached to BRIDGE.
#
# See documentation for additional jail and vm-bhyve helpers.

. /etc/rc.subr
. /etc/network.subr

name="ngbuddy"
desc="configures netgraph bridge and eiface devices"
rcvar="ngbuddy_enable"

enable_cmd="${name}_enable"
start_cmd="${name}_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"
bridge_cmd="${name}_bridge"
unbridge_cmd="${name}_shutdown_bridge"
create_cmd="${name}_create_eiface_permanent"
destroy_cmd="${name}_shutdown_eiface_permanent"
jail_cmd="${name}_create_eiface_temporary"
unjail_cmd="${name}_unjail"
vmconf_cmd="${name}_vmconf"
vmname_cmd="${name}_vmname"
extra_commands="bridge create destroy jail unbridge unjail vmconf vmname"

load_rc_config "$ngbuddy"
: ${ngbuddy_bridges:="`list_vars 'ngbuddy_*_if'|sed -E 's/^ngbuddy_|_if$//g'|xargs`"}
: ${ngbuddy_max_retries:=1024}
: ${ngbuddy_public_hooks:=uplink}
: ${ngbuddy_share_dir:="/usr/local/share/ngbuddy"}

# Enable Netgraph Buddy and add default bridges if none are defined
# "public" is associated with the default route, and "private" is a new eiface
ngbuddy_enable() {
	sysrc ngbuddy_enable=YES
	if [ -z "$ngbuddy_bridges" ]
	then
		echo Adding default public and private bridges.
		# Keep ifconfig name (dots); convert to underscores only for ngctl.
		local public_if="$(netstat -rn | awk '/default/{print $4;exit}')"
		[ -n "$public_if" ] && sysrc ngbuddy_public_if="$public_if"
		sysrc ngbuddy_private_if="nghost0"
	fi
}

# Netgraph node names cannot contain '.'; ng_ether converts them to '_'.
ngbuddy_ng_name() {
	echo "$1" | tr . _
}

# Load netgraph and count existing bridge hooks for the start-time guard.
ngbuddy_init() {
	kldload -nq ng_ether ng_bridge
	NGHOOKS=$(ngctl list -l 2>/dev/null | awk \
		'$1 ~ /^(up)?link[0-9]+$/ { hooks++ } END { print hooks + 0 }')
}

# Set LNUM to the next available ordinary link on a specific bridge.
ngbuddy_linkpp() {
	kldload -nq ng_ether ng_bridge
	LNUM=$(ngctl show "$1:" 2>/dev/null | awk '
		$1 ~ /^link[0-9]+$/ {
			n = int(substr($1, 5))
			if (n >= nextlink) nextlink = n + 1
		}
		END { print nextlink + 0 }')
}

# Set a MAC address
ngbuddy_ifconfig_ether() {
	local seed mac_len mac
	case "${ngbuddy_set_mac}" in
		[Nn][Oo] | '')	return ;;
		[Yy][Ee][Ss])	seed="$if_name" ;;
		*)		seed="$if_name$ngbuddy_set_mac" ;;
	esac
	: ${ngbuddy_set_mac_prefix:="589cfc"}
	: ${ngbuddy_set_mac_hash:="sha1"}
	local mac_len="$((12-${#ngbuddy_set_mac_prefix}))"
	local mac="$(echo -n "$seed" | $ngbuddy_set_mac_hash | cut -c1-$mac_len)"
	mac="${ngbuddy_set_mac_prefix}${mac}"
	ifconfig "$if_name" ether "$mac"
}

# Attach a new eiface to a bridge
ngbuddy_create_eiface() {
	if_name="$1"
	bridge="${2:-$(echo $ngbuddy_bridges | awk '{print $1}')}"
	ngbuddy_linkpp "$bridge"
	local error retries=0
	if ifconfig "$if_name" > /dev/null 2>&1; then
		echo "$if_name" already exists.
		return
	fi
	while ! error="$(ngctl mkpeer "$bridge:" eiface "link${LNUM}" ether 2>&1)"
	do
		if ngctl show "$bridge:link${LNUM}" > /dev/null 2>&1
		then
			retries=$((retries + 1))
			if [ "$retries" -gt "$ngbuddy_max_retries" ]
			then
				echo "Unable to claim a link on $bridge after $ngbuddy_max_retries retries." >&2
				return 1
			fi
			LNUM=$((LNUM + 1))
		else
			echo "$error" >&2
			return 1
		fi
	done
	local ng_if="$(ngctl show -n "$bridge:link${LNUM}" |awk '{print $2}')"
	ngctl name "$ng_if:" "$if_name"
	ifconfig "$ng_if" name "$if_name" >/dev/null
	ngbuddy_ifconfig_ether
	ngbuddy_bridge_seed_mac "$bridge" "link${LNUM}" "$if_name"
	ifn_start "$if_name"
}

ngbuddy_create_eiface_permanent() {
	ngbuddy_create_eiface "$@"
	if [ -n "$if_name" -a -n "$bridge" ]
	then
		sysrc ngbuddy_${bridge}_list+="$if_name"
	fi
}

ngbuddy_create_eiface_temporary() {
	ngbuddy_create_eiface "$@"
}

# service ngbuddy destroy: remove an eiface
ngbuddy_shutdown_eiface() {
	if_name="$1"
	[ -z "$if_name" ] && return
	bridge=$(ngctl list -l | awk -v myif="$if_name" \
		'($3 == "bridge"){bridge[$5] = $2} \
		($3 == "eiface"){eiface[$2] = $1} \
		END{print bridge[eiface[myif]]}')
	local bridge_if=$(eval echo \$ngbuddy_${bridge}_if)
	if [ "$bridge_if" = "$if_name" ]
	then
		echo Interface  $if_name is associated with bridge $bridge.
		echo Use the subcommand \"unbridge $bridge\" to remove it.
	elif [ -n "$bridge" ]
	then
		echo Removing $if_name from $bridge
	else
		echo Removing orphaned interface $if_name
	fi
	ngctl shutdown "$if_name":
}

# Subcommand: destroy
ngbuddy_shutdown_eiface_permanent() {
	ngbuddy_shutdown_eiface "$@"
	sysrc ngbuddy_${bridge}_list-="$if_name"
}

# Remove a temporary interface from a jail and shut it down
# Subcommand: unjail
ngbuddy_unjail() {
	local if_name="$1"
	local jail_name="${2:-$1}"
	ifconfig "$if_name" -vnet "$jail_name"
	ngbuddy_shutdown_eiface "$if_name"
}

# Record an eiface MAC on its bridge hook before VNET handoff for
# FreeBSD 14 bug workaround.
ngbuddy_bridge_seed_mac() {
	local bridge="$1" hook="$2" if_name="$3" mac
	mac="$(ifconfig "$if_name" | awk '/ether / { print $2; exit }')"
	[ -n "$mac" ] || return 0
	ngctl msg "${bridge}:" movehost "{ addr=${mac} hook=\"${hook}\" }" >/dev/null
}

# Prefer long-lived L2 entries so pre-seeded jail MACs are not (likely) forgotten.
ngbuddy_bridge_set_staleness() {
	ngctl msg "$1:" setconfig \
		"{ debugLevel=1 loopTimeout=60 maxStaleness=65535 minStableAge=1 }" \
		>/dev/null
}

# Enable a private bridge and attach a new eiface.
ngbuddy_private() {
	bridge=$1
	eval if_name=\$ngbuddy_${bridge}_if
	ngctl mkpeer eiface ether ether
	ngeth=`ngctl list|cut -wf3|grep '^ngeth'|sort -n|tail -1`
	ngctl name $ngeth: $if_name
	ifconfig $ngeth name $if_name up > /dev/null
	ngbuddy_ifconfig_ether
	ngctl mkpeer $if_name: bridge ether link${LNUM}
	ngctl name $if_name:ether $bridge
	ngbuddy_bridge_set_staleness "$bridge"
	ngbuddy_bridge_seed_mac "$bridge" "link${LNUM}" "$if_name"
	ifn_start "$if_name"
}

# Enable a public bridge and attach an existing NIC, following the ng_bridge(4) recommendations.
ngbuddy_public() {
	bridge=$1
	if_name=$2
	local ng_name lower_hook upper_hook
	ng_name="$(ngbuddy_ng_name "$if_name")"
	case "$ngbuddy_public_hooks" in
		[Uu][Pp][Ll][Ii][Nn][Kk]|'')
			lower_hook=uplink1
			upper_hook=link0
			;;
		[Ll][Ii][Nn][Kk])
			lower_hook=link0
			upper_hook=link1
			;;
		*)
			echo "ngbuddy_public_hooks must be link or uplink." >&2
			return 1
			;;
	esac
	ngctl msg $ng_name: setpromisc 1
	ngctl msg $ng_name: setautosrc 0
	ngctl mkpeer $ng_name: bridge lower $lower_hook
	ngctl name  $ng_name:lower $bridge
	ngctl connect $ng_name: $bridge: upper $upper_hook
	ngbuddy_bridge_set_staleness "$bridge"
	ifconfig $if_name -lro -tso
}

ngbuddy_bridge() {
	bridge=$1
	bridgeif=$2
	ngbuddy_linkpp "$bridge"

	# Check for bridge name conflicts
	brmatch=`echo $bridge|tr _ .`
	if ifconfig -l|grep -wo "$brmatch">/dev/null
	then
		echo Bridge name $bridge conflicts with an existing interface.
		return 1
	fi

	# If we get a second parameter, update the bridge rc var
	if [ -n "$bridgeif" ]
	then
		eval ngbuddy_${bridge}_if=${bridgeif}
		sysrc ngbuddy_${bridge}_if=${bridgeif}
	fi

	# If given an existing interface, bridge to it, otherwise create an eiface
	eval if_name=\$ngbuddy_${bridge}_if
	if [ -z "$if_name" ]
	then
		echo No interface given for bridge $bridge.
		return 1
	fi
	linkmatch="$if_name"
	if ! ifconfig "$linkmatch" > /dev/null 2>&1
	then
		linkmatch=`echo "$if_name"|tr _ .`
	fi
	if ifconfig "$linkmatch" > /dev/null 2>&1
	then
		ngbuddy_public "$bridge" "$linkmatch" || return 1
	else
		ngbuddy_private "$bridge"
	fi
	eval linklist=\"\$ngbuddy_${bridge}_list\"
	[ -z "$linklist" ] && return
	echo -n "Creating bridge $bridge eiface nodes:"
	for newif in $linklist
	do
		echo -n " $newif"
		ngbuddy_create_eiface "$newif" "$bridge"
	done
	echo
}

# TO-DO: Show a complete list of bridged connections if applicable.
ngbuddy_shutdown_bridge() {
	bridge="$1"
	# TO-DO: We also need to check for bridges not recorded in rc.conf.
	eval node_list=\$ngbuddy_${bridge}_list
	if [ -n "$node_list" ]
	then
		echo -n $bridge is a bridge. First destroy:\
		echo $node_list
		return 1
	fi
	eval bridgeif=\$ngbuddy_${bridge}_if
	if [ -n "$bridgeif" ]
	then
		local ng_bridgeif
		ng_bridgeif="$(ngbuddy_ng_name "$bridgeif")"
		iftype=`ngctl list|awk -v n="$ng_bridgeif" '$2==n {print $4}'`
		if [ "$iftype" = "eiface" ]
		then
			echo "Shutdown $bridgeif"
			ngctl shutdown "${ng_bridgeif}:"
		fi
	fi
	echo Shutdown bridge $bridge
	if ngctl list|grep -wq $bridge
	then
		ngctl shutdown "$bridge":
	fi
	sysrc -x "ngbuddy_${bridge}_if"
}

# Create nodes and ifaces defined in rc.conf
ngbuddy_start() {
	ngbuddy_init
	echo Starting $name.
	if [ "${NGHOOKS:-0}" -gt 0 ]
	then
		echo Netgraph already initialized with $NGHOOKS bridge hooks.
		exit 1
	fi
	for bridge in $ngbuddy_bridges
	do
		ngbuddy_bridge "$bridge" || return 1
	done
	local created
	created=$(ngctl list -l | awk '$1 ~ /^(up)?link[0-9]+$/ { n++ } END { print n+0 }')
	echo Created "$created" bridge hooks.
}

# Return a netgraph link count
ngbuddy_status() {
	ngbuddy_vmname
	awk -f "${ngbuddy_share_dir}/ngbuddy-status.awk"
}

# Blindly lay waste to all netgraph nodes
ngbuddy_stop() {
	local shutdown_nodes=$( ngctl list | \
		awk '$4 ~ /eiface|bridge/ { print $2 }'| \
		xargs -tn1 -I% ngctl shutdown %: 2>&1 | \
		awk 'END { print NR }')
	echo Shut down "$shutdown_nodes" nodes.
}

# Configure vm-bhyve with ngbuddy bridges (version 1.5+ required)
ngbuddy_vmname() {
	case "$vm_dir" in
		zfs:*)	vm_dir="`echo "$vm_dir"|cut -d: -f2`"
			vm_conf="`zfs get -H mountpoint "$vm_dir"|cut -wf3`" ;;
		?*)	vm_conf="$vm_dir" ;;
		*)	return ;;
	esac
	running_vms=`ps axww|awk '/bhyve[:] /{print $6}'`
	for this_vm in $running_vms; do
		vm_socket_name=`echo $this_vm|tr ". " _`
		vm_if_conf=`tail -r "$vm_conf/$this_vm/vm-bhyve.log" |\
			grep -Eom1 'netgraph,path=.*,peerhook=[^,]+'`
		path=`echo $vm_if_conf|cut -d, -f2|cut -d= -f2`
		peerhook=`echo $vm_if_conf|cut -d, -f3|cut -d= -f2`
		ngctl name $path$peerhook $vm_socket_name
	done
}

ngbuddy_vmconf() {
	case "$vm_dir" in
		zfs:*)	vm_dir="`echo "$vm_dir"|cut -d: -f2`"
			vm_conf="`zfs get -H mountpoint "$vm_dir"|cut -wf3`" ;;
		?*)	vm_conf="$vm_dir" ;;
		*)	return ;;
	esac
	vm_bak="$vm_conf/.config/.system.conf.bak"
	vm_conf="$vm_conf/.config/system.conf"
	cp -p "$vm_conf" "$vm_bak"
	echo Configuring $vm_conf:
	sysrc -f "$vm_conf" switch_list+="$ngbuddy_bridges"
	for ngbuddy_bridge in $ngbuddy_bridges
	do
		sysrc -f "$vm_conf" type_$ngbuddy_bridge=netgraph
	done
}

run_rc_command "$1" "$2" "$3"
