#!/usr/bin/env bash

SELF="$0"

# Linux bridge for connecting lan and wan network of guest machines
BR_LAN="${BR_LAN:-br-lan}"
BR_WAN="${BR_WAN:-br-wan}"

# Host network interface providing internet access for guest machines
IF_INET="${IF_INET:-eth0}"

# qemu-bridge-helper does two things here
#
# - create tap interface
# - add the tap interface to bridge
#
# as such it requires CAP_NET_ADMIN to do its job.  It will be convenient to
# have it as a root setuid program.  Be aware of the security risks implied
#
# the helper has an acl list which defaults to deny all bridge.  we need to add
# $BR_LAN and $BR_WAN to its allow list
#
#	# sudo vim /etc/qemu/bridge.conf
#	allow br-lan
#	allow br-wan
#
# Other allowed directives can be 'allow all', 'deny all', 'include xxx',  See
# qemu-bridge-helper.c of qemu source code for details.
#
# The helper can be provided by package qemu-system-common on debian, or
# qemu-kvm-common on rhel
#
HELPER="${HELPER:-/usr/libexec/qemu-bridge-helper}"

### end of global settings

__errmsg() {
	echo "$*" >&2
}

do_setup() {
	# setup bridge for LAN network
	sudo ip link add dev "$BR_LAN" type bridge
	sudo ip link set dev "$BR_LAN" up
	sudo ip addr add 192.168.1.3/24 dev "$BR_LAN"

	# setup bridge for WAN network
	#
	# minimal dnsmasq config for configuring guest wan network with dhcp
	#
	#	# sudo apt-get install dnsmasq
	#	# sudo vi /etc/dnsmasq.conf
	#	interface=br-wan
	#	dhcp-range=192.168.7.50,192.168.7.150,255.255.255.0,30m
	#
	sudo ip link add dev "$BR_WAN" type bridge
	sudo ip link set dev "$BR_WAN" up
	sudo ip addr add 192.168.7.1/24 dev "$BR_WAN"

	# guest internet access
	sudo sysctl -w "net.ipv4.ip_forward=1"
	sudo sysctl -w "net.ipv4.conf.$BR_WAN.proxy_arp=1"
	while sudo iptables -t nat -D POSTROUTING -o "$IF_INET" -j MASQUERADE 2>/dev/null; do true; done
	      sudo iptables -t nat -A POSTROUTING -o "$IF_INET" -j MASQUERADE
}

check_setup_() {
	ip link show "$BR_LAN" >/dev/null || return 1
	ip link show "$BR_WAN" >/dev/null || return 1
	[ -x "$HELPER" ] || {
		__errmsg "helper $HELPER is not an executable"
		return 1
	}
}

check_setup() {
	[ -n "$o_network" ] || return 0
	[ -z "$o_user" ] || return 0
	check_setup_ || {
		__errmsg "please check the script content to see the environment requirement"
		return 1
	}
}

# Append user-mode networking args to o_qemu_extra.  $1 is the qemu NIC device
# (e.g. virtio-net-pci, e1000).  The LAN netdev shares OpenWrt's default LAN
# subnet so the guest's static 192.168.1.1 is reachable through SLIRP; the WAN
# netdev uses SLIRP defaults (10.0.2.0/24) to provide internet via the host.
add_user_netdev() {
	local nic="$1"
	local ssh="${o_ssh_port:-2222}"
	local http="${o_http_port:-8080}"
	local https="${o_https_port:-8443}"
	__errmsg "user-mode networking: ssh on 127.0.0.1:$ssh, http on 127.0.0.1:$http, https on 127.0.0.1:$https"

	local lan="user,id=lan,net=192.168.1.0/24,host=192.168.1.2"
	lan+=",hostfwd=tcp:127.0.0.1:$ssh-192.168.1.1:22"
	lan+=",hostfwd=tcp:127.0.0.1:$http-192.168.1.1:80"
	lan+=",hostfwd=tcp:127.0.0.1:$https-192.168.1.1:443"

	o_qemu_extra+=(
		-netdev "$lan"
		-device "$nic,id=devlan,netdev=lan,mac=$MAC_LAN"
		-netdev "user,id=wan"
		-device "$nic,id=devwan,netdev=wan,mac=$MAC_WAN"
	)
}
#do_setup; check_setup; exit $?

usage() {
	cat >&2 <<EOF
Usage: $SELF [-h|--help]
       $SELF <target>
         [<subtarget> [<extra-qemu-options>]]
         [--kernel <kernel>]
         [--rootfs <rootfs>]
         [--machine <machine>]
         [-n|--network]
         [-u|--user-network [--ssh-port <port>] [--http-port <port>] [--https-port <port>]]

<subtarget> will default to "generic" and must be specified if
<extra-qemu-options> are present

e.g. <subtarget> for malta can be le, be, le64, be64, le-glibc, le64-glibc, etc

<kernel>, <rootfs> can be required or optional arguments to qemu depending on
the actual <target> in use.  They will default to files under bin/targets/

Networking modes:
  -n / --network        use bridges \$BR_LAN and \$BR_WAN (needs sudo + helper
                        setup)
  -u / --user-network   use rootless qemu user-mode networking; the guest LAN
                        interface (static 192.168.1.1) is reachable from the
                        host via 127.0.0.1:<ssh-port> (default 2222),
                        127.0.0.1:<http-port> (default 8080) and
                        127.0.0.1:<https-port> (default 8443); the guest WAN
                        gets internet via SLIRP
  --ssh-port <p>        override host-side ssh forward port (default 2222)
  --http-port <p>       override host-side http forward port (default 8080)
  --https-port <p>      override host-side https forward port (default 8443)

Examples

  $SELF x86 64
  $SELF x86 64 --user-network
  $SELF x86 64 --user-network --ssh-port 2200 --http-port 8000 --https-port 8001
  $SELF x86 64 --machine q35,accel=kvm -device virtio-balloon-pci
  $SELF x86 64 -incoming tcp:0:4444
  $SELF x86 64-glibc
  $SELF malta be -m 64
  $SELF malta le64
  $SELF malta be-glibc
  $SELF armsr armv8 --user-network
  $SELF armsr armv7 \\
                --machine virt,highmem=off \\
                --kernel bin/targets/armsr/armv7/openwrt-armsr-armv7-generic-kernel.bin \\
                --rootfs bin/targets/armsr/armv7/openwrt-armsr-armv7-generic-ext4-rootfs.img
EOF
}

rand_mac() {
	hexdump -n 3 -e '"52:54:00" 3/1 ":%02x"' /dev/urandom
}

parse_args() {
	o_network=
	o_user=
	o_qemu_extra=()
	while [ "$#" -gt 0 ]; do
		# Cmdline options for the script itself SHOULD try to be
		# prefixed with two dashes to distinguish them from those for
		# qemu executables.
		#
		# Also note that qemu accepts both --opt and -opt
		case "$1" in
			--kernel) o_kernel="$2"; shift 2 ;;
			--rootfs) o_rootfs="$2"; shift 2 ;;
			--machine|-machine|-M) o_mach="$2"; shift 2 ;;
			--network|-n) o_network=1; shift ;;
			--user-network|-u) o_user=1; o_network=1; shift ;;
			--ssh-port) o_ssh_port="$2"; shift 2 ;;
			--http-port) o_http_port="$2"; shift 2 ;;
			--https-port) o_https_port="$2"; shift 2 ;;
			--help|-h)
				usage
				exit 0
				;;
			*)
				if [ -z "$o_target" ]; then
					o_target="$1"
				elif [ -z "$o_subtarget" ]; then
					o_subtarget="$1"
				else
					o_qemu_extra+=("$1")
				fi
				shift
				;;
		esac
	done

	MAC_LAN="$(rand_mac)"
	MAC_WAN="$(rand_mac)"
	[ -n "$o_target" ] || {
		usage
		return 1
	}
	[ -n "$o_subtarget" ] || o_subtarget="generic"
	eval "$(grep ^CONFIG_BINARY_FOLDER= .config 2>/dev/null)"
	o_bindir="${CONFIG_BINARY_FOLDER:-bin}/targets/$o_target/$o_subtarget"
}

start_qemu_armsr() {
	local kernel="$o_kernel"
	local rootfs="$o_rootfs"
	local mach="${o_mach:-virt}"
	local cpu
	local qemu_exe

	case "${o_subtarget%-*}" in
		armv7)
			qemu_exe="qemu-system-arm"
			cpu="cortex-a15"
			[ -n "$kernel" ] || kernel="$o_bindir/openwrt-$o_target-${o_subtarget%-*}-generic-initramfs-kernel.bin"
			;;
		armv8)
			qemu_exe="qemu-system-aarch64"
			cpu="cortex-a57"
			[ -n "$kernel" ] || kernel="$o_bindir/openwrt-$o_target-${o_subtarget%-*}-generic-initramfs-kernel.bin"
			;;
		*)
			__errmsg "target $o_target: unknown subtarget $o_subtarget"
			return 1
			;;
	esac
	[ -z "$rootfs" ] || {
		if [ ! -f "$rootfs" -a -s "$rootfs.gz" ]; then
			gunzip "$rootfs.gz"
		fi
		o_qemu_extra+=( \
			"-drive" "file=$rootfs,format=raw,if=virtio" \
			"-append" "root=/dev/vda rootwait" \
		)
	}

	[ -z "$o_network" ] || {
		if [ -n "$o_user" ]; then
			add_user_netdev virtio-net-pci
		else
			o_qemu_extra+=( \
				"-netdev" "bridge,id=lan,br=$BR_LAN,helper=$HELPER" \
				    "-device" "virtio-net-pci,id=devlan,netdev=lan,mac=$MAC_LAN" \
				"-netdev" "bridge,id=wan,br=$BR_WAN,helper=$HELPER" "-device" \
				    "virtio-net-pci,id=devwan,netdev=wan,mac=$MAC_WAN" \
			)
		fi
	}

	"$qemu_exe" -machine "$mach" -cpu "$cpu" -nographic \
		-kernel "$kernel" \
		"${o_qemu_extra[@]}"
}

start_qemu_malta() {
	local is64
	local isel
	local qemu_exe
	local cpu
	local rootfs="$o_rootfs"
	local kernel="$o_kernel"
	local mach="${o_mach:-malta}"

	# o_subtarget can be le, be, le64, be64, le-glibc, le64-glibc, etc..
	is64="$(echo $o_subtarget | grep -o 64)"
	[ "$(echo "$o_subtarget" | grep -o '^..')" = "le" ] && isel="el"
	qemu_exe="qemu-system-mips$is64$isel"
	[ -n "$is64" ] && cpu="MIPS64R2-generic" || cpu="24Kc"

	[ -n "$kernel" ] || kernel="$o_bindir/openwrt-malta-${o_subtarget%-*}-generic-initramfs-kernel.bin"

	[ -z "$rootfs" ] || {
		if [ ! -f "$rootfs" -a -s "$rootfs.gz" ]; then
			gunzip "$rootfs.gz"
		fi
		o_qemu_extra+=( \
			"-drive" "file=$rootfs,format=raw" \
			"-append" "root=/dev/sda rootwait" \
		)
	}

	# NOTE: order of -device arguments matters: the first -device becomes eth0
	# in the guest, which OpenWrt's 99-default_network assigns as LAN; the
	# second becomes eth1 / WAN.  Keep LAN first to match that mapping.
	[ -z "$o_network" ] || {
		if [ -n "$o_user" ]; then
			add_user_netdev pcnet
		else
			o_qemu_extra+=(
				-netdev bridge,id=lan,br="$BR_LAN,helper=$HELPER" -device pcnet,netdev=lan,mac="$MAC_LAN"
				-netdev bridge,id=wan,br="$BR_WAN,helper=$HELPER" -device pcnet,netdev=wan,mac="$MAC_WAN"
			)
		fi
	}

	"$qemu_exe" -machine "$mach" -cpu "$cpu" -nographic \
		-kernel "$kernel" \
		"${o_qemu_extra[@]}"
}

start_qemu_x86() {
	local qemu_exe
	local kernel="$o_kernel"
	local rootfs="$o_rootfs"
	local mach="${o_mach:-pc}"

	[ -n "$rootfs" ] || {
		rootfs="$o_bindir/openwrt-$o_target-${o_subtarget%-*}-generic-squashfs-combined.img"
		if [ ! -f "$rootfs" -a -s "$rootfs.gz" ]; then
			gunzip "$rootfs.gz"
		fi
	}
	#
	# generic: 32-bit, pentium4 (CONFIG_MPENTIUM4), kvm guest, virtio
	# legacy: 32-bit, i486 (CONFIG_M486)
	# 64: 64-bit, kvm guest, virtio
	#
	case "${o_subtarget%-*}" in
		legacy)			qemu_exe="qemu-system-i386"	;;
		generic|64)		qemu_exe="qemu-system-x86_64"	;;
		*)
			__errmsg "target $o_target: unknown subtarget $o_subtarget"
			return 1
			;;
	esac

	[ -n "$kernel" ] && {
	    o_qemu_extra+=( \
		"-kernel" "$kernel" \
		"-append" "root=/dev/vda console=ttyS0 rootwait" \
	    )
	}

	[ -z "$o_network" ] || {
		case "${o_subtarget%-*}" in
			legacy)
				if [ -n "$o_user" ]; then
					add_user_netdev e1000
				else
					o_qemu_extra+=(
						-netdev "bridge,id=lan,br=$BR_LAN,helper=$HELPER" -device "e1000,id=devlan,netdev=lan,mac=$MAC_LAN"
						-netdev "bridge,id=wan,br=$BR_WAN,helper=$HELPER" -device "e1000,id=devwan,netdev=wan,mac=$MAC_WAN"
					)
				fi
				;;
			generic|64)
				if [ -n "$o_user" ]; then
					add_user_netdev virtio-net-pci
				else
					o_qemu_extra+=(
						-netdev "bridge,id=lan,br=$BR_LAN,helper=$HELPER" -device "virtio-net-pci,id=devlan,netdev=lan,mac=$MAC_LAN"
						-netdev "bridge,id=wan,br=$BR_WAN,helper=$HELPER" -device "virtio-net-pci,id=devwan,netdev=wan,mac=$MAC_WAN"
					)
				fi
				;;
		esac
	}

	case "${o_subtarget%-*}" in
		legacy)
			# use IDE (PATA) disk instead of AHCI (SATA).  Refer to link
			# [1] for related discussions
			#
			# To use AHCI interface
			#
			#	-device ich9-ahci,id=ahci \
			#	-device ide-hd,drive=drv0,bus=ahci.0 \
			#	-drive "file=$rootfs,format=raw,id=drv0,if=none" \
			#
			# [1] https://dev.openwrt.org/ticket/17947
			"$qemu_exe" -machine "$mach" -nographic \
				-device ide-hd,drive=drv0 \
				-drive "file=$rootfs,format=raw,id=drv0,if=none" \
				"${o_qemu_extra[@]}"
			;;
		generic|64)
			"$qemu_exe" -machine "$mach" -nographic \
				-drive "file=$rootfs,format=raw,if=virtio" \
				"${o_qemu_extra[@]}"
			;;
	esac
}

start_qemu() {
	case "$o_target" in
		armsr)		start_qemu_armsr	;;
		malta)		start_qemu_malta	;;
		x86)		start_qemu_x86		;;
		*)
			__errmsg "target $o_target is not supported yet"
			return 1
			;;
	esac
}

parse_args "$@" \
	&& check_setup \
	&& start_qemu
