#!/bin/sh

# PROVIDE:	ngbuddy
# REQUIRE:	FILESYSTEMS
# BEFORE:	NETWORKING

# See ngbuddy(8) for configuration details. For manual configuration,
# 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.

. /etc/rc.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_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 given an eiface
ngbuddy_enable() {
	sysrc ngbuddy_enable=YES
	if [ -z "$ngbuddy_bridges" ]
	then
		echo Adding default public and private bridges.
		local public_if="$(netstat -rn | awk '/default/{gsub("\\.","_",$4);print $4}')"
		[ -n "$public_if" ] && sysrc ngbuddy_public_if="$public_if"
		sysrc ngbuddy_private_if="nghost0"
	fi
}

# Set LNUM to next available link
ngbuddy_linkpp() {
	if [ -n "$LNUM" ]
	then
		LNUM=$((LNUM + 1))
	else
		# If there are no links (x), return link0, otherwise increment (y)
		LNUM=$(ngctl list -l | awk '
			$1 ~ /^link/ { x = int(substr($1, 5)); if (x > y) y = x }
			END { print (x != "") ? (++y) : 0}')
	fi
}

# Make sure netgraph is loaded and count its links.
ngbuddy_init() {
	kldload -nq ng_ether ng_bridge
	[ -z "$LNUM" ] && ngbuddy_linkpp
}

# 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() {
	ngbuddy_init
	if_name="$1"
	bridge="${2:-$(echo $ngbuddy_bridges | awk '{print $1}')}"
	if ifconfig "$if_name" > /dev/null 2>&1; then
		echo "$if_name" already exists.
		return
	fi
	ngctl mkpeer "$bridge:" eiface "link${LNUM}" ether
	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_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"
}

# 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
}

# Enable a public bridge and attach an existing NIC
ngbuddy_public() {
	bridge=$1
	eval if_name=\$ngbuddy_${bridge}_if
	ngctl msg $if_name: setpromisc 1
	ngctl msg $if_name: setautosrc 0
	ngctl mkpeer $if_name: bridge lower link${LNUM}
	ngbuddy_linkpp
	ngctl name  $if_name:lower $bridge
	ngctl connect $if_name: $bridge: upper link${LNUM}
	ifconfig $if_name lro -tso4 -tso6 -vlanhwfilter -vlanhwtso
}

ngbuddy_bridge() {
	bridge=$1
	bridgeif=$2
	ngbuddy_init

	# 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=`echo $if_name|tr _ .`
	if ifconfig -l|grep -qw "$linkmatch"
	then
		ngbuddy_public $bridge
	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_linkpp
		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
		iftype=`ngctl list|grep -w $bridgeif|cut -wf5`
		if [ "$iftype" == "eiface" ]
		then
			echo "Shutdown $bridgeif"
			ngctl shutdown "${bridgeif}:"
		else
		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
	local bridge_count
	echo Starting $name.
	if [ "$LNUM" -gt 0 ]
	then
		echo Netgraph already initialized with $LNUM links.
		exit 1
	fi
	for bridge in $ngbuddy_bridges
	do
		[ -n "$bridge_count" ] && ngbuddy_linkpp
		ngbuddy_bridge $bridge
		bridge_count=$(($bridge_count + 1))
	done
	echo Created $(($LNUM + 1)) links.
}

# 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"
