# Tiny Cloud - Network Config Functions
# vim:set filetype=sh:
# shellcheck shell=sh

network_config_get() {
	local file="$1"
	shift
	yx -f "$file" "$@" 2>/dev/null
}

network_config_warn() {
	local source="$1"
	shift
	log -i -t "$phase" warning "$ACTION: $source $*"
}

network_config_has_dhcpcd() {
	command -v dhcpcd >/dev/null 2>&1
}

network_config_can_rename() {
	case "$(init_system)" in
		openrc) [ -e "$ETC/init.d/macifrename" ] ;;
		systemd) command -v udevadm >/dev/null 2>&1 ;;
	esac
}

network_config_interface_for_mac() {
	local wanted="$(printf '%s' "$1" | lower)" address
	for address in "$SYS"/class/net/*/address; do
		[ -f "$address" ] || continue
		if [ "$(lower < "$address")" = "$wanted" ]; then
			echo "${address%/address}"
			return 0
		fi
	done
	return 1
}

network_config_apply_systemd_renames() {
	local map="$1" source="$2" plan applied target mac path current
	[ -s "$map" ] || return 0

	plan="$(mktemp "$TINY_CLOUD_VAR/rename-plan.XXXXXX")"
	applied="$(mktemp "$TINY_CLOUD_VAR/renamed-interfaces.XXXXXX")"
	while read -r target mac; do
		[ -n "$target" ] || continue
		if [ -e "$SYS/class/net/$target" ]; then
			if [ "$(lower < "$SYS/class/net/$target/address")" = \
				"$(printf '%s' "$mac" | lower)" ]; then
				continue
			fi
			network_config_warn "$source" \
				"cannot rename interface to existing name $target"
			rm -f "$plan" "$applied"
			return 1
		fi
		if ! path="$(network_config_interface_for_mac "$mac")"; then
			network_config_warn "$source" \
				"cannot find interface with MAC address $mac"
			rm -f "$plan" "$applied"
			return 1
		fi
		current="${path##*/}"
		printf '%s %s\n' "$current" "$target" >> "$plan"
	done < "$map"

	while read -r current target; do
		[ -n "$current" ] || continue
		$MOCK ip link set dev "$current" down
		if ! $MOCK ip link set dev "$current" name "$target"; then
			while read -r target current; do
				$MOCK ip link set dev "$target" name "$current" || :
			done < "$applied"
			rm -f "$plan" "$applied"
			return 1
		fi
		printf '%s %s\n' "$target" "$current" >> "$applied"
	done < "$plan"
	rm -f "$plan" "$applied"
}

network_config_prepare_interface_names() {
	local map="$1" source="$2"
	case "$(init_system)" in
		openrc) return 0 ;;
		systemd) network_config_apply_systemd_renames "$map" "$source" ;;
	esac
}

network_config_collect() {
	local file="$1" output="$2" label="$3" indexes index value
	shift 3
	indexes="$(network_config_get "$file" "$@")"
	for index in $indexes; do
		value="$(network_config_get "$file" "$@" "$index")"
		[ -n "$value" ] && printf '%s %s\n' "$label" "$value" >> "$output"
	done
}

network_config_netmask_prefix() {
	local mask="$1" octet bits prefix=0 zero_seen='' IFS=.
	case "$mask" in *[!0-9.]*) return 1;; esac
	set -- $mask
	[ "$#" -eq 4 ] || return 1
	for octet; do
		case "$octet" in
			255) bits=8;; 254) bits=7;; 252) bits=6;;
			248) bits=5;; 240) bits=4;; 224) bits=3;;
			192) bits=2;; 128) bits=1;; 0) bits=0;;
			*) return 1;;
		esac
		[ -n "$zero_seen" ] && [ "$bits" -ne 0 ] && return 1
		[ "$bits" -lt 8 ] && zero_seen=1
		prefix=$((prefix + bits))
	done
	echo "$prefix"
}

network_config_cleanup() {
	rm -f "$1" "$2" "$3"
}

network_config_commit() {
	local interfaces_tmp="$1" iftab_tmp="$2" nameservers_tmp="$3"
	local value searches=''

	mv "$interfaces_tmp" "$ETC/network/interfaces"
	mkdir -p "$ETC/tiny-cloud"
	if [ -s "$iftab_tmp" ]; then
		mv "$iftab_tmp" "$ETC/tiny-cloud/iftab"
	else
		rm -f "$iftab_tmp" "$ETC/tiny-cloud/iftab"
	fi
	while IFS= read -r value; do
		case "$value" in
			search\ *) searches="${searches:+$searches }${value#search }" ;;
			*) add_once "$ETC/resolv.conf" "$value" ;;
		esac
	done < "$nameservers_tmp"
	[ -n "$searches" ] && add_once "$ETC/resolv.conf" "search $searches"
	rm -f "$nameservers_tmp"
}

network_config_apply_v1() {
	local file="$1" source="$2"
	local interfaces_tmp iftab_tmp nameservers_tmp entries entry configured='' has_dhcpcd=''
	network_config_has_dhcpcd && has_dhcpcd=1

	entries="$(network_config_get "$file" config)"
	if [ -z "$entries" ]; then
		network_config_warn "$source" "v1 has no supported config entries"
		return 1
	fi

	interfaces_tmp="$(mktemp "$TINY_CLOUD_VAR/network-interfaces.XXXXXX")"
	iftab_tmp="$(mktemp "$TINY_CLOUD_VAR/iftab.XXXXXX")"
	nameservers_tmp="$(mktemp "$TINY_CLOUD_VAR/nameservers.XXXXXX")"
	printf "%s\n%s\n\n" "auto lo" "iface lo inet loopback" > "$interfaces_tmp"

	for entry in $entries; do
		local type name macaddress mtu stanza='' subnets subnet subtype address netmask prefix gateway
		local dhcp4='' dhcp6='' dhcp4_metric='' dhcp6_metric=''
		local dhcp_program='' dhcp_opts='' accept_ra='' current_mac='' subnet_usable=''
		local dns_subnets=''
		type="$(network_config_get "$file" config "$entry" type)"
		case "$type" in
			physical)
				name="$(network_config_get "$file" config "$entry" name)"
				macaddress="$(network_config_get "$file" config "$entry" mac_address)"
				mtu="$(network_config_get "$file" config "$entry" mtu)"
				accept_ra="$(network_config_get "$file" config "$entry" accept-ra)"
				case "$name" in
					''|*[!A-Za-z0-9_.:-]*)
					network_config_warn "$source" "v1 physical entry has invalid name: ${name:-empty}"
					continue
					;;
				esac
				if [ -n "$macaddress" ] && [ -s "$SYS/class/net/$name/address" ]; then
					current_mac="$(lower < "$SYS/class/net/$name/address")"
					if [ "$current_mac" != "$(printf '%s' "$macaddress" | lower)" ]; then
						network_config_warn "$source" "v1 MAC does not match existing interface $name"
						continue
					fi
				fi
				if [ -n "$macaddress" ] && ! [ -e "$SYS/class/net/$name" ]; then
					if ! network_config_can_rename; then
						network_config_warn "$source" \
							"cannot rename interface to $name on this system"
						continue
					fi
					printf '%s %s\n' "$name" "$macaddress" >> "$iftab_tmp"
				fi

				subnets="$(network_config_get "$file" config "$entry" subnets)"
				for subnet in $subnets; do
					subnet_usable=''
					subtype="$(network_config_get "$file" config "$entry" subnets "$subnet" type)"
					case "$subtype" in
						dhcp|dhcp4)
							dhcp4=1
							subnet_usable=1
							dhcp4_metric="$(network_config_get "$file" config "$entry" subnets "$subnet" metric)"
							;;
						dhcp6|ipv6_dhcpv6-stateful)
							dhcp6=1
							subnet_usable=1
							dhcp6_metric="$(network_config_get "$file" config "$entry" subnets "$subnet" metric)"
							;;
						ipv6_slaac)
							accept_ra=yes
							subnet_usable=1
							;;
						ipv6_dhcpv6-stateless)
							dhcp6=1
							accept_ra=yes
							subnet_usable=1
							dhcp6_metric="$(network_config_get "$file" config "$entry" subnets "$subnet" metric)"
							;;
						static|static6)
							address="$(network_config_get "$file" config "$entry" subnets "$subnet" address)"
							netmask="$(network_config_get "$file" config "$entry" subnets "$subnet" netmask)"
							if [ -z "$address" ]; then
								network_config_warn "$source" "v1 static subnet for $name has no address"
								continue
							fi
							case "$address" in
								*/*) ;;
								*)
									if [ -n "$netmask" ]; then
										if ! prefix="$(network_config_netmask_prefix "$netmask")"; then
											network_config_warn "$source" \
												"v1 static subnet for $name has invalid netmask: $netmask"
											continue
										fi
										address="$address/$prefix"
									fi
									;;
							esac
							stanza="$stanza\taddress $address\n"
							gateway="$(network_config_get "$file" config "$entry" subnets "$subnet" gateway)"
							[ -n "$gateway" ] && stanza="$stanza\tgateway $gateway\n"
							subnet_usable=1
							;;
						'') network_config_warn "$source" "v1 subnet for $name has no type" ;;
						*) network_config_warn "$source" "v1 has unsupported subnet type: $subtype" ;;
					esac
					if [ -n "$subnet_usable" ]; then
						case "$subtype" in
							dhcp6|ipv6_dhcpv6-stateful)
								[ -n "$has_dhcpcd" ] && dns_subnets="$dns_subnets $subnet"
								;;
							*) dns_subnets="$dns_subnets $subnet" ;;
						esac
					fi
				done
				case "$mtu" in
					'') ;;
					*[!0-9]*) network_config_warn "$source" "v1 physical entry $name has invalid mtu: $mtu" ;;
					*) stanza="$stanza\tmtu $mtu\n" ;;
				esac
				if [ -n "$dhcp6" ] && [ -z "$has_dhcpcd" ]; then
					network_config_warn "$source" \
						"v1 DHCPv6 for $name requires dhcpcd; ignoring DHCPv6"
					dhcp6=''
					dhcp6_metric=''
				fi
				if [ -n "$dhcp4_metric" ] && [ -z "$has_dhcpcd" ]; then
					network_config_warn "$source" \
						"v1 DHCP metric for $name requires dhcpcd; ignoring metric"
					dhcp4_metric=''
				fi
				if [ -n "$dhcp4$dhcp6" ]; then
					stanza="$stanza\tuse dhcp\n"
				fi
				if [ -n "$dhcp6" ]; then
					dhcp_program=dhcpcd
					[ -z "$dhcp4" ] && dhcp_opts="$dhcp_opts -6"
				fi
				if [ -n "$dhcp4_metric" ]; then
					dhcp_program=dhcpcd
					dhcp_opts="$dhcp_opts -m $dhcp4_metric"
				fi
				[ -n "$dhcp6_metric" ] && dhcp_opts="$dhcp_opts -m $dhcp6_metric"
				case "$accept_ra" in
					true|yes) stanza="$stanza\tuse ipv6-ra\n" ;;
				esac
				[ -n "$dhcp_program" ] && stanza="$stanza\tdhcp-program $dhcp_program\n"
				if [ -n "$dhcp_opts" ]; then
					stanza="$stanza\tdhcp-opts${dhcp_opts}\n"
				fi
				[ -n "$stanza" ] || continue
				for subnet in $dns_subnets; do
					network_config_collect "$file" "$nameservers_tmp" nameserver \
						config "$entry" subnets "$subnet" dns_nameservers
					network_config_collect "$file" "$nameservers_tmp" search \
						config "$entry" subnets "$subnet" dns_search
				done
				printf "%s\n%s\n%b\n" "auto $name" "iface $name" "$stanza" >> "$interfaces_tmp"
				configured=1
				;;
			nameserver)
				network_config_collect "$file" "$nameservers_tmp" nameserver config "$entry" address
				network_config_collect "$file" "$nameservers_tmp" search config "$entry" search
				;;
			'') network_config_warn "$source" "v1 config entry has no type" ;;
			*) network_config_warn "$source" "v1 has unsupported entry type: $type" ;;
		esac
	done

	if [ -z "$configured" ]; then
		network_config_cleanup "$interfaces_tmp" "$iftab_tmp" "$nameservers_tmp"
		network_config_warn "$source" "v1 has no usable physical entries"
		return 1
	fi

	if ! network_config_prepare_interface_names "$iftab_tmp" "$source"; then
		network_config_cleanup "$interfaces_tmp" "$iftab_tmp" "$nameservers_tmp"
		return 1
	fi
	network_config_commit "$interfaces_tmp" "$iftab_tmp" "$nameservers_tmp"
}

network_config_apply_v2() {
	local file="$1" source="$2"
	local interfaces_tmp iftab_tmp nameservers_tmp has_dhcpcd=''
	network_config_has_dhcpcd && has_dhcpcd=1

	local ethernets="$(network_config_get "$file" ethernets)"
	if [ -z "$ethernets" ]; then
		network_config_warn "$source" "v2 has no supported ethernets entries"
		return 1
	fi

	interfaces_tmp="$(mktemp "$TINY_CLOUD_VAR/network-interfaces.XXXXXX")"
	iftab_tmp="$(mktemp "$TINY_CLOUD_VAR/iftab.XXXXXX")"
	nameservers_tmp="$(mktemp "$TINY_CLOUD_VAR/nameservers.XXXXXX")"
	printf "%s\n%s\n\n" \
		"auto lo" \
		"iface lo inet loopback" \
		> "$interfaces_tmp"

	local iface configured='' target_name macaddress set_name
	for iface in $ethernets; do
		local stanza='' dhcp4='' dhcp6='' accept_ra='' mtu='' dhcp_program='' dhcp_opts=''
		local addresses='' address='' has_address='' gateways='' gateway=''
		local dhcp4_metric='' dhcp6_metric=''

		set_name="$(network_config_get "$file" ethernets "$iface" set-name)"
		macaddress="$(network_config_get "$file" ethernets "$iface" match macaddress)"
		target_name="${set_name:-$iface}"
		if [ -n "$set_name" ] && [ "$set_name" != "$iface" ]; then
			if [ -z "$macaddress" ] ||
				! network_config_can_rename; then
				network_config_warn "$source" \
					"cannot apply set-name for $iface without match.macaddress and rename support"
				continue
			fi
		fi

		dhcp4="$(network_config_get "$file" ethernets "$iface" dhcp4)"
		dhcp6="$(network_config_get "$file" ethernets "$iface" dhcp6)"
		accept_ra="$(network_config_get "$file" ethernets "$iface" accept-ra)"
		mtu="$(network_config_get "$file" ethernets "$iface" mtu)"

		case "$dhcp4" in
			true|yes)
				stanza="$stanza\tuse dhcp\n"
				dhcp4_metric="$(network_config_get "$file" ethernets "$iface" dhcp4-overrides route-metric)"
				if [ -n "$dhcp4_metric" ] && [ -z "$has_dhcpcd" ]; then
					network_config_warn "$source" \
						"v2 DHCP metric for $iface requires dhcpcd; ignoring metric"
					dhcp4_metric=''
				fi
				if [ -n "$dhcp4_metric" ]; then
					dhcp_program="dhcpcd"
					dhcp_opts="$dhcp_opts -m $dhcp4_metric"
				fi
				;;
		esac

		case "$dhcp6" in
			true|yes)
				dhcp6_metric="$(network_config_get "$file" ethernets "$iface" dhcp6-overrides route-metric)"
				if [ -z "$has_dhcpcd" ]; then
					network_config_warn "$source" \
						"v2 DHCPv6 for $iface requires dhcpcd; ignoring DHCPv6"
					dhcp6=''
					dhcp6_metric=''
				else
					dhcp_program="dhcpcd"
					case "$dhcp4" in
						true|yes) ;;
						*) stanza="$stanza\tuse dhcp\n"; dhcp_opts="$dhcp_opts -6" ;;
					esac
					[ -n "$dhcp6_metric" ] && dhcp_opts="$dhcp_opts -m $dhcp6_metric"
				fi
				;;
		esac

		case "$accept_ra" in
			true|yes) stanza="$stanza\tuse ipv6-ra\n" ;;
		esac
		case "$mtu" in
			'') ;;
			*[!0-9]*) network_config_warn "$source" "v2 ethernet $iface has invalid mtu: $mtu" ;;
			*) stanza="$stanza\tmtu $mtu\n" ;;
		esac

		addresses="$(network_config_get "$file" ethernets "$iface" addresses)"
		for address in $addresses; do
			address="$(network_config_get "$file" ethernets "$iface" addresses "$address")"
			if [ -n "$address" ]; then
				stanza="$stanza\taddress $address\n"
				has_address=1
			fi
		done

		for gateway in gateway4 gateway6; do
			gateway="$(network_config_get "$file" ethernets "$iface" "$gateway")"
			[ -n "$gateway" ] && gateways="$gateways\tgateway $gateway\n"
		done
		[ -n "$has_address" ] && stanza="$stanza$gateways"

		if [ -n "$dhcp_program" ]; then
			stanza="$stanza\tdhcp-program $dhcp_program\n"
		fi
		if [ -n "$dhcp_opts" ]; then
			stanza="$stanza\tdhcp-opts${dhcp_opts}\n"
		fi

		[ -n "$stanza" ] || continue
		network_config_collect "$file" "$nameservers_tmp" nameserver \
			ethernets "$iface" nameservers addresses
		network_config_collect "$file" "$nameservers_tmp" search \
			ethernets "$iface" nameservers search
		printf "%s\n%s\n%b\n" \
			"auto $target_name" \
			"iface $target_name" \
			"$stanza" >> "$interfaces_tmp"
		if [ -n "$set_name" ] && [ -n "$macaddress" ]; then
			printf "%s %s\n" "$target_name" "$macaddress" >> "$iftab_tmp"
		fi
		configured=1
	done

	if [ -z "$configured" ]; then
		network_config_cleanup "$interfaces_tmp" "$iftab_tmp" "$nameservers_tmp"
		network_config_warn "$source" "v2 has no usable ethernets entries"
		return 1
	fi

	if ! network_config_prepare_interface_names "$iftab_tmp" "$source"; then
		network_config_cleanup "$interfaces_tmp" "$iftab_tmp" "$nameservers_tmp"
		return 1
	fi
	network_config_commit "$interfaces_tmp" "$iftab_tmp" "$nameservers_tmp"
}

network_config_apply() {
	local file="$1" source="${2:-network-config}" version

	[ -s "$file" ] || return 1
	if ! version="$(network_config_get "$file" version)"; then
		network_config_warn "$source" "is malformed"
		return 1
	fi
	case "$version" in
		1) network_config_apply_v1 "$file" "$source" ;;
		2) network_config_apply_v2 "$file" "$source" ;;
		*)
			network_config_warn "$source" "has unsupported version: ${version:-unknown}"
			return 1
			;;
	esac
}

network_config_setup_macifrename_openrc() {
	local source="${1:-network-config}" iftab="$ETC/tiny-cloud/iftab"
	if ! [ -s "$iftab" ]; then
		if [ -f "$TINY_CLOUD_VAR/.macifrename-enabled" ]; then
			if [ "$(init_system)" = openrc ]; then
				service_disable macifrename boot
			fi
			rm -f "$TINY_CLOUD_VAR/.macifrename-enabled"
		fi
		if [ -f "$ETC/conf.d/macifrename" ]; then
			sed -i -e '\|^MACIFRENAME_OPTS="/etc/tiny-cloud/iftab"$|d' \
				"$ETC/conf.d/macifrename"
		fi
		return 0
	fi
	if ! [ -e "$ETC/init.d/macifrename" ]; then
		network_config_warn "$source" "requires the macifrename service for interface renames"
		return 0
	fi

	mkdir -p "$ETC/conf.d"
	if [ -f "$ETC/conf.d/macifrename" ]; then
		if grep -q '^MACIFRENAME_OPTS=' "$ETC/conf.d/macifrename"; then
			sed -i -e 's|^MACIFRENAME_OPTS=.*|MACIFRENAME_OPTS="/etc/tiny-cloud/iftab"|' \
				"$ETC/conf.d/macifrename"
		else
			printf '%s\n' 'MACIFRENAME_OPTS="/etc/tiny-cloud/iftab"' >> "$ETC/conf.d/macifrename"
		fi
	else
		printf '%s\n' 'MACIFRENAME_OPTS="/etc/tiny-cloud/iftab"' > "$ETC/conf.d/macifrename"
	fi

	# tiny-cloud-boot runs before net; apply renames now, before OpenRC starts net.
	if ! [ -e "$ETC/runlevels/boot/macifrename" ]; then
		service_enable macifrename boot
		touch "$TINY_CLOUD_VAR/.macifrename-enabled"
	fi
	service_start macifrename
}

network_config_setup_systemd_links() {
	local map="$ETC/tiny-cloud/iftab"
	local link_dir="$ETC/systemd/network"
	local tmpdir link target mac index=0

	mkdir -p "$link_dir"
	tmpdir="$(mktemp -d "$TINY_CLOUD_VAR/systemd-links.XXXXXX")"
	if [ -s "$map" ]; then
		while read -r target mac; do
			[ -n "$target" ] || continue
			index=$((index + 1))
			printf '%s\n' \
				'[Match]' \
				"MACAddress=$mac" \
				'' \
				'[Link]' \
				'NamePolicy=' \
				"Name=$target" \
				> "$tmpdir/10-tiny-cloud-$index.link"
		done < "$map"
	fi

	rm -f "$link_dir"/10-tiny-cloud-*.link
	for link in "$tmpdir"/*.link; do
		[ -f "$link" ] || continue
		mv "$link" "$link_dir/${link##*/}"
	done
	rmdir "$tmpdir"
	$MOCK udevadm control --reload
}

network_config_setup_interface_names() {
	local source="${1:-network-config}"
	case "$(init_system)" in
		openrc) network_config_setup_macifrename_openrc "$source" ;;
		systemd) network_config_setup_systemd_links ;;
	esac
}
