#!/bin/sh -euf
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# PASST - Plug A Simple Socket Transport
#  for qemu/UNIX domain socket mode
#
# PASTA - Pack A Subtle Tap Abstraction
#  for network namespace/tap device mode
#
# slirp4netns.sh - Compatibility wrapper for pasta, behaving like slirp4netns(1)
#
# WARNING: Draft quality, not really tested
#
# Copyright (c) 2021-2022 Red Hat GmbH
# Author: Stefano Brivio <sbrivio@redhat.com>

PASTA_PID="$(mktemp)"
PASTA_OPTS="-q --ipv4-only -a 10.0.2.0 -n 24 -g 10.0.2.2 --dns-forward 10.0.2.3 -m 1500 --no-ndp --no-dhcpv6 --no-dhcp -P ${PASTA_PID}"
PASTA="$(command -v ./pasta || command -v pasta || :)"

API_SOCKET=
API_DIR="$(mktemp -d)"
PORTS_DIR="${API_DIR}/ports"
FIFO_REQ="${API_DIR}/req.fifo"
FIFO_RESP="${API_DIR}/resp.fifo"
PORT_ARGS=

USAGE_RET=1
NOTFOUND_RET=127

# add() - Add single option to $PASTA_OPTS
# $1:	Option name, with or without argument
add() {
	PASTA_OPTS="${PASTA_OPTS} ${1}"
}

# drop() - Drop one option (without argument) from $PASTA_OPTS
# $1:	Option name
drop() {
	old_opts="${PASTA_OPTS}"; PASTA_OPTS=
	for o in ${old_opts}; do [ "${o}" != "${1}" ] && add "${o}"; done
}

# sub() - Substitute option in $PASTA_OPTS, with or without argument
# $1:	Option name
# $2:	Option argument, can be empty
sub() {
	old_opts="${PASTA_OPTS}"; PASTA_OPTS=
	next=0
	for o in ${old_opts}; do
		if [ ${next} -eq 1 ]; then
			next=0; add "${1} ${2}"; shift; shift; continue
		fi

		for r in ${@}; do [ "${o}" = "${r}" ] && next=1 && break; done
		[ "${next}" -eq 0 ] && add "${o}"
	done
}

# xorshift() - pseudorandom permutation of 16-bit group
# $1:	16-bit value to shuffle
xorshift() {
	# Adaptation of Xorshift algorithm from:
	#   Marsaglia, G. (2003). Xorshift RNGs.
	#   Journal of Statistical Software, 8(14), 1 - 6.
	#   doi:http://dx.doi.org/10.18637/jss.v008.i14
	# with triplet (5, 3, 1), suitable for 16-bit ranges.
	n=${1}
	: $((n ^= n << 5))
	: $((n ^= n >> 3))
	: $((n ^= n << 1))
	echo ${n}
}

# opt() - Validate single option from getopts
# $1:	Option type
# $@:	Variable names to assign to
opt() {
	case "${1}" in
	u32)
		if ! printf "%i" "${OPTARG}" >/dev/null 2>&1 || \
		   [ "${OPTARG}" -lt 0 ]; then
			echo "${OPT} must be a non-negative integer"
			usage
		fi
		eval ${2}="${OPTARG}"
		;;
	mtu)
		if ! printf "%i" "${OPTARG}" >/dev/null 2>&1 || \
		   [ "${OPTARG}" -lt 0 ] || [ "${OPTARG}" -ge 65522 ]; then
			echo "MTU must be a positive integer (< 65522)"
			usage
		fi
		eval ${2}="${OPTARG}"
		;;
	str)
		eval ${2}="${OPTARG}"
		;;
	net4)
		addr="${OPTARG%/*}"
		mask="${OPTARG##*/}"

		{ [ -z "${mask}" ] || !printf "%i" "${mask}" >/dev/null 2>&1 \
		  || [ ${mask} -gt 32 ] || ${mask} -le 0 ]; } && usage

		expr "${addr}" :					      \
			'[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' \
			>/dev/null
		[ $? -ne 0 ] && usage

		ifs="${IFS}"; IFS='.'
		for q in ${addr}; do [ ${q} -gt 255 ] && usage; done
		IFS="${ifs}"

		eval ${2}="${addr}"
		eval ${3}="${mask}"
		;;
	esac
}

# start() - Start pasta
start() {
	${PASTA} ${PASTA_OPTS} ${PORT_ARGS} --netns ${ns_spec}
	[ ${RFD} -ne 0 ] && echo "1" >&${RFD} || :
}

# start() - Terminate pasta process
stop() {
	kill $(cat ${PASTA_PID})
}

# api_insert() - Handle add_hostfwd request, update PORT_ARGS
# $1:	Protocol, "tcp" or "udp"
# $2:	Host port
# $3:	Guest port
api_insert() {
	__id=
	__next_id=1	# slirp4netns starts from ID 1
	PORT_ARGS=

	for __entry in $(ls ${PORTS_DIR}); do
		PORT_ARGS="${PORT_ARGS} $(cat "${PORTS_DIR}/${__entry}")"

		if [ -z "${__id}" ] && [ ${__entry} -ne ${__next_id} ]; then
			__id=${__next_id}
		fi

		__next_id=$((__next_id + 1))
	done
	[ -z "${__id}" ] && __id=${__next_id}

	# Invalid ports are accepted by slirp4netns, store them as empty files.
	# Unknown protocols aren't.

	case ${1} in
	"tcp") opt="-t" ;;
	"udp") opt="-u" ;;
	*)
		echo '{"error":{"desc":"bad request: add_hostfwd: bad arguments.proto"}}'
		return
		;;
	esac

	if [ ${2} -ge 0 ] && [ ${2} -le 65535 ] && \
	   [ ${3} -ge 0 ] && [ ${3} -le 65535 ]; then
		echo "${opt} ${2}:${3}" > "${PORTS_DIR}/${__id}"
		PORT_ARGS="${PORT_ARGS} ${opt} ${2}:${3}"
	else
		:> "${PORTS_DIR}/${__id}"
	fi

	echo "{ \"return\": {\"id\": ${__id}}}"

	NEED_RESTART=1
}

# api_list_one() - Print a single port forwarding entry in JSON
# $1:	ID
# $2:	protocol option, -t or -u
# $3:	host port
# $4:	guest port
api_list_one() {
	[ "${2}" = "-t" ] && __proto="tcp" || __proto="udp"

	printf '{"id": %i, "proto": "%s", "host_addr": "0.0.0.0", "host_port": %i, "guest_addr": "%s", "guest_port": %i}' \
		"${1}" "${__proto}" "${3}" "${A4}" "${4}"
}

# api_list() - Handle list_hostfwd request: list port forwarding entries in JSON
api_list() {
	printf '{ "return": {"entries": ['

	__first=1
	for __entry in $(ls "${PORTS_DIR}"); do
		[ ${__first} -eq 0 ] && printf ", " || __first=0
		IFS=' :'
		api_list_one ${__entry} $(cat ${PORTS_DIR}/${__entry})
		unset IFS
	done

	printf ']}}'
}

# api_delete() - Handle remove_hostfwd request: delete entry, update PORT_ARGS
# $1:	Entry ID -- caller *must* ensure it's a number
api_delete() {
	if [ ! -f "${PORTS_DIR}/${1}" ]; then
		printf '{"error":{"desc":"bad request: remove_hostfwd: bad arguments.id"}}'
		return
	fi

	rm "${PORTS_DIR}/${1}"

	PORT_ARGS=
	for __entry in $(ls ${PORTS_DIR}); do
		PORT_ARGS="${PORT_ARGS} $(cat "${PORTS_DIR}/${__entry}")"
	done

	printf '{"return":{}}'

	NEED_RESTART=1
}

# api_error() - Print generic error in JSON
api_error() {
	printf '{"error":{"desc":"bad request"}}'
}

# api_handler() - Entry point for slirp4netns-like API socket handler
api_handler() {
	trap 'exit 0' INT QUIT TERM
	mkdir "${PORTS_DIR}"

	while true; do
		mkfifo "${FIFO_REQ}" "${FIFO_RESP}"

		cat "${FIFO_RESP}" | nc -l -U "${API_SOCKET}" | \
			tee /dev/null >"${FIFO_REQ}" & READER_PID=${!}

		__req="$(dd count=1 2>/dev/null <${FIFO_REQ})"

		>&2 echo "apifd event"
		>&2 echo "api_handler: got request: ${__req}"

		eval $(echo "${__req}" |
			(jq -r 'to_entries | .[0] |
			 .key + "=" + (.value | @sh)' ||
			 printf 'execute=ERR'))

		if [ "${execute}" != "list_hostfwd" ]; then
			eval $(echo "${__req}" |
				(jq -r '.arguments | to_entries | .[] |
				 .key + "=" + (.value | @sh)' ||
				 printf 'execute=ERR'))
		fi

		NEED_RESTART=0
		case ${execute} in
		"add_hostfwd")
			api_insert "${proto}" "${host_port}" "${guest_port}"
			__restart=1
			;;
		"list_hostfwd")
			api_list
			;;
		"remove_hostfwd")
			case ${id} in
			''|*[!0-9]*)	api_error ;;
			*)		api_delete "${id}"; __restart=1 ;;
			esac
			;;
		*)
			api_error
			;;
		esac >"${FIFO_RESP}"

		kill ${READER_PID}

		rm "${FIFO_REQ}" "${FIFO_RESP}"

		[ ${NEED_RESTART} -eq 1 ] && { stop; start; }
	done

	exit 0
}

# usage() - Print slirpnetns(1) usage and exit indicating failure
# $1:	Invalid option name, if any
usage() {
	[ ${#} -eq 1 ] && printf "%s: invalid option -- '%s'\n" "${0}" "${1}"
	cat << EOF
Usage: ${0} [OPTION]... PID|PATH TAPNAME
User-mode networking for unprivileged network namespaces.

-c, --configure          bring up the interface
-e, --exit-fd=FD         specify the FD for terminating slirp4netns
-r, --ready-fd=FD        specify the FD to write to when the network is configured
-m, --mtu=MTU            specify MTU (default=1500, max=65521)
-6, --enable-ipv6        enable IPv6 (experimental)
-a, --api-socket=PATH    specify API socket path
--cidr=CIDR              specify network address CIDR (default=10.0.2.0/24)
--disable-host-loopback  prohibit connecting to 127.0.0.1:* on the host namespace
--netns-type=TYPE 	 specify network namespace type ([path|pid], default=pid)
--userns-path=PATH	 specify user namespace path
--enable-sandbox         create a new mount namespace (and drop all caps except CAP_NET_BIND_SERVICE if running as the root)
--enable-seccomp         enable seccomp to limit syscalls (experimental)
-h, --help               show this help and exit
-v, --version            show version and exit
EOF
	exit ${USAGE_RET}
}

# version() - Print version
version() {
	echo "slirp4netns-like wrapper for pasta"
	exit 0
}

# gen_addr6() - Generate pseudorandom IPv6 address, changes every second
gen_addr6() {
	printf "fd00"
	n=$(($(xorshift $(date +%S)) % 65536))
	for i in $(seq 2 8); do
		printf ":%04x" ${n}
		n=$(($(xorshift ${n}) % 65536))
	done
}

# Default options
v6=0
get_pid=0
MTU=1500
A4="10.0.2.0"
M4="255.255.255.0"
no_map_gw=0
EFD=0
RFD=0

[ -z "${PASTA}" ] && echo "pasta command not found" && exit ${NOTFOUND_RET}

while getopts ce:r:m:6a:hv-: OPT 2>/dev/null; do
	if [ "${OPT}" = "-" ]; then
		OPT="${OPTARG%%[= ]*}"
		OPTARG="${OPTARG#${OPT}[= ]}"
	fi
	case "${OPT}" in
	c | configure)		add "--config-net"			      ;;
	e | exit-fd)		opt u32 EFD				      ;;
	r | ready-fd)		opt u32 RFD				      ;;
	m | mtu)		opt mtu MTU && sub -m ${MTU}		      ;;
	6 | enable-ipv6)	V6=1					      ;;
	a | api-socket)		opt str API_SOCKET			      ;;
	cidr)			opt net4 A4 M4 && sub -a ${A4} -n ${M4}	      ;;
	disable-host-loopback)	add "--no-map-gw" && no_map_gw=1	      ;;
	netns-type)		: Autodetected				      ;;
	userns-path)		opt_str USERNS_NAME "${OPTARG}"		      ;;
	enable-sandbox) 	: Not supported yet			      ;;
	enable-seccomp)		: Cannot be disabled			      ;;
	h | help)		USAGE_RET=0 && usage			      ;;
	v | version)		version					      ;;
	??*)			usage "${OPT}"				      ;;
	?)			usage "${OPT}"				      ;;
	esac
done

shift $((OPTIND - 1))
[ ${#} -ne 2 ] && usage
ns_spec="${1}"

ifname="${2}"
add "-I ${ifname}"

if [ ${v6} -eq 1 ]; then
	drop "--ipv4-only"
	add "-a $(gen_addr6) -g fd00::2 -D fd00::3"
fi

start
[ -n "${API_SOCKET}" ] && api_handler </dev/null &
trap "stop; rm -rf ${API_DIR}; rm -f ${API_SOCKET}; rm ${PASTA_PID}" EXIT
trap 'exit 0' INT QUIT TERM

>&2 echo "sent tapfd=5 for ${ifname}"
>&2 echo "received tapfd=5"

cat << EOF
Starting slirp
* MTU:             ${MTU}
* Network:         ${A4}
* Netmask:         ${M4}
* Gateway:         10.0.2.2
* DNS:             10.0.2.3
* Recommended IP:  10.0.2.100
EOF
[ -n "${API_SOCKET}" ] && echo "* API socket:      ${API_SOCKET}"

if [ ${no_map_gw} -eq 0 ]; then
	echo "WARNING: 127.0.0.1:* on the host is accessible as 10.0.2.2 (set --disable-host-loopback to prohibit connecting to 127.0.0.1:*)"
fi

if [ ${EFD} -ne 0 ]; then
	dd count=1 of=/dev/null 2>/dev/null <&${EFD}
else
	while read a; do :; done
fi

exit 0
