#!/bin/sh
# James Budiono 2011, 2013, 2015, 2017, 2018, 2019
# fatdog interactive secure sandbox
# this version uses tmpfs instead of an rw image.
# you can also choose which SFS to use.
# run this from terminal.
#
# version 1 - forked from version 9 of sandbox.sh
# version 2 - support user namespaces when available
# version 3 - updated support for lxc 1.0.4 (unprivileged container)
# version 4 - proper termination if accidentally closed
# version 5 - support command in arguments
# version 6 - support network and replace insecure desktop emulation with remote Xephyr
# version 7 - added NAT support
#
# env: IDMAP=yes - enable use of user namespaces
# env: HOST_IP & LXC_IP & WINDOW_SIZE (if blank auto-calculate)

# 0. directory locations
. $BOOTSTATE_PATH # AUFS_ROOT_ID, BASELINE_MOUNT
[ "$NOT_USING_AUFS" ] && exit 1
XTERM="defaultterm"
SANDBOX_ROOT=/mnt/sb
FAKEROOT=$SANDBOX_ROOT/fakeroot   # mounted chroot location of sandbox - ie, the fake root
SANDBOX_TMPFS=$SANDBOX_ROOT/sandbox # mounted rw location of tmpfs used for sandbox
SANDBOX_ID=
LXC_ID=sb-lxc	# LXC container ID
USE_DESKTOP=	# use desktop emulation?
USE_NETWORK=${USE_NETWORK-ask} # blank (none), ask, proxyarp or nat
WINDOW_SIZE=${WINDOW_SIZE-1024x600}
#HOST_IP and LXC_IP # auto-calculate if blank
TMPFILE=$(mktemp -p /tmp)

# user namespaces settings
#IDMAP=			# enable IDMAP=yes on env to use user namespaces
LXC_UID=${LXC_UID:-100000}          # UID and GID when running under user namespaces
LXC_GID=${LXC_GID:-100000}
LXC_IDRANGE=${LXC_IDRANGE:-65536}   # no of ids to map under user name spaces


### trap handler
# umount all and stop lxc if we are accidentally killed
trap '[ $CONSOLE_PID] && kill $CONSOLE_PID; 
[ ${LXC_ID} ] && lxc-stop -n ${LXC_ID}; umountall' HUP
umountall() {
	{
	[ $XSERVER_PID ] && kill $XSERVER_PID
	lxc-wait -n $LXC_ID -s STOPPED
	umount -l $FAKEROOT/$SANDBOX_TMPFS
	if [ "$USE_DESKTOP" ]; then
		umount -l $FAKEROOT/tmp
		xauth remove ${LXC_ID}/unix:0
	fi

	stop_network
	umount -l $FAKEROOT
	umount -l $SANDBOX_TMPFS
	rmdir $FAKEROOT
	rmdir $SANDBOX_TMPFS
	rm ${FAKEROOT}.hold
	} 2> /dev/null
}

### general helpers

die() {
	echo "$1"
	exit 1
}

# get random from $1 to $2 (inclusive)
get_random() {
	awk -v min=$1 -v max=$2 'BEGIN{srand(); print int(min+rand()*(max-min+1))}'
}

# $1-ip, $2-which octet, $3-by how much
adjust_ipv4() {
	awk -F. -v ip=$1 -v octet=$2 -v howmuch=$3 \
'BEGIN {
	$0=ip
	$octet=($octet + howmuch) % 256
	printf("%d.%d.%d.%d", $1,$2,$3,$4);
}'
}

### network setup handlers

prepare_network() {
	# compute common parameters
	if [ "$HOST_IP" ]; then
		ipcidr=${HOST_IP}
	else
		ipcidr=$(busybox ipaddr | awk '$1=="inet" && $2 !~ /127.0.0.1/ {print $2}' | head -n1)
		HOST_IP=${ipcidr%/*}
	fi
	HOST_NET=$(busybox ipcalc -n $ipcidr); HOST_NET=${HOST_NET#*=}
	HOST_PREFIX=$(busybox ipcalc -p $ipcidr); HOST_PREFIX=${HOST_PREFIX#*=}
	HOST_GW=$(busybox iproute | awk '$1=="default" { print $3}' | head -n1)
	HOST_GW_DEV=$(busybox iproute | awk '$1=="default" { print $5}' | head -n1)

	case "$USE_NETWORK" in
		proxyarp)
			if [ -z "$LXC_IP" ]; then
				# replace 4th octet
				LXC_IP=$(adjust_ipv4 $HOST_IP 4 $(get_random 1 255))
			fi
			#    type         guest-ip guest-net guest-prefix gw       host-ip
			echo $USE_NETWORK $LXC_IP  $HOST_NET $HOST_PREFIX $HOST_GW $HOST_IP > $FAKEROOT/etc/netparams
			;;

		nat)
			if [ -z "$LXC_IP" ]; then
				# replace 3rd octet
				LXC_IP=$(adjust_ipv4 $HOST_IP 3 $(get_random 1 255))
			fi
			GUEST_NET=$(busybox ipcalc -n $LXC_IP); GUEST_NET=${GUEST_NET#*=}
			GUEST_PREFIX=$(busybox ipcalc -p $LXC_IP); GUEST_PREFIX=${GUEST_PREFIX#*=}
			GUEST_GW=$(adjust_ipv4 $LXC_IP 4 $(get_random 1 255))
			#    type         guest-ip guest-net  guest-prefix  gw        host-ip
			echo $USE_NETWORK $LXC_IP  $GUEST_NET $GUEST_PREFIX $GUEST_GW $HOST_IP > $FAKEROOT/etc/netparams
			;;
		
		"") rm -f $FAKEROOT/etc/netparams ;;
	esac
	
	# create guest network init script
	! [ -e $FAKEROOT/etc/rc.d/rc.network.lxc ] && cat > $FAKEROOT/etc/rc.d/rc.network.lxc << "EOF" &&
#!/bin/sh
[ -e /etc/netparams ] || exit

read type ip net prefix gw host < /etc/netparams
#echo $type $ip $net/$prefix $gw $host

[ $type = proxyarp ] &&
echo 1 > /proc/sys/net/ipv4/conf/all/proxy_arp ||
echo 0 > /proc/sys/net/ipv4/conf/all/proxy_arp

if type busybox > /dev/null && busybox ipaddr > /dev/null 2>&1; then
busybox iplink set lo up; iplink set eth0 up # bring interfaces up
busybox ipaddr add $ip dev eth0              # add own ip address
busybox iproute add $net/$prefix dev eth0    # add own route
busybox iproute add default via $gw dev eth0 # add default gw
else # if busybox
ip link set lo up; ip link set eth0 up # bring interfaces up
ip addr add $ip dev eth0               # add own ip address
ip route add $net/$prefix dev eth0     # add own route
ip route add default via $gw dev eth0  # add default gw
fi  # if busybox
EOF
	chmod +x $FAKEROOT/etc/rc.d/rc.network.lxc
}

start_network() {
	case "$USE_NETWORK" in
		proxyarp)
			echo 1 > /proc/sys/net/ipv4/ip_forward         # enable ip forwarding
			echo 1 > /proc/sys/net/ipv4/conf/all/proxy_arp # enable proxy arp
			busybox iplink set $LXC_ID up           # enable interface
			busybox ipaddr add $HOST_IP dev $LXC_ID # assign ip address, same as host
			busybox iproute add $LXC_IP dev $LXC_ID # add routing to sandbox
			;;
		
		nat)
			echo 1 > /proc/sys/net/ipv4/ip_forward # enable ip forwarding
			busybox iplink set $LXC_ID up             # enable interface
			busybox ipaddr add $GUEST_GW dev $LXC_ID  # assign guest gw ip address
			busybox iproute add $GUEST_NET/$GUEST_PREFIX dev $LXC_ID # add guest net routing
			iptables -t nat -A POSTROUTING -o $HOST_GW_DEV -j MASQUERADE
			;;
	esac
}

stop_network() {
	case "$USE_NETWORK" in
		proxyarp) ;; # nothing to do
		nat) iptables -t nat -D POSTROUTING -o $HOST_GW_DEV -j MASQUERADE ;;
	esac
}




############# main ###############

# 0.1 must be root
[ $(id -u) -ne 0 ] && die "You must be root to use sandbox."

# 0.2 cannot launch sandbox within sandbox
grep -q LXC_ID $BOOTSTATE_PATH &&
die "Cannot launch sandbox within sandbox."

# 0.3 help
case "$1" in
	--help|-h)
	echo "Usage: ${0##*/} [cmd [args]]"
	echo "Starts an in-memory (throwaway) sandbox using lxc. Type 'poweroff' to leave."
	exit
esac

# 0.4 - if not running from terminal but in Xorg, then launch via terminal
! [ -t 0 ] && [ -n "$DISPLAY" ] && exec $XTERM -e "$0" "$@"
! [ -t 0 ] && exit

# 0.5 is this the first sandbox? If not, then create another name for mountpoints
if grep -q $FAKEROOT /proc/mounts; then
	FAKEROOT=$(mktemp -d -p $SANDBOX_ROOT ${FAKEROOT##*/}-XXXXXXX)
	SANDBOX_ID="-${FAKEROOT##*-}"
	SANDBOX_TMPFS=$SANDBOX_ROOT/${SANDBOX_TMPFS##*/}${SANDBOX_ID}
	LXC_ID=${LXC_ID}${SANDBOX_ID}
	rmdir $FAKEROOT
fi

# 0.6 check for lxc
if ! type lxc-start > /dev/null 2>&1; then
	die "LXC not found, please use 'sandbox.sh' instead."
fi

# 1. get aufs system-id for the root filesystem
[ -z "$AUFS_ROOT_ID" ] && AUFS_ROOT_ID=$(
awk '{ if ($2 == "/" && $3 == "aufs") { match($4,/si=[0-9a-f]*/); print "si_" substr($4,RSTART+3,RLENGTH-3) } }' /proc/mounts
)

# 2. get branches, then map branches to mount types or loop devices 
items=$(
{ echo ==mount==; cat /proc/mounts; 
  echo ==losetup==; losetup-FULL -a; 
  echo ==branches==; ls -v /sys/fs/aufs/$AUFS_ROOT_ID/br[0-9]* | xargs sed 's/=.*//'; } | awk '
  /==mount==/ { mode=1 }
  /==losetup==/ { mode=2 }
  /==branches==/ { mode=3 }
  {
	if (mode == 1) {
		# get list of mount points, types, and devices - index is $3 (mount points)
		mountdev[$2]=$1
		mounttypes[$2]=$3
	} else if (mode == 2) {
		# get list of loop devices and files - index is $1 (loop devs)
		sub(/:/,"",$1)
		sub(/.*\//,"",$3); sub(/)/,"",$3)
		loopdev[$1]=$3
	} else if (mode == 3) {
		# map mount types to loop files if mount devices is a loop
		for (m in mountdev) {
			if ( loopdev[mountdev[m]] != "" ) mounttypes[m]=loopdev[mountdev[m]]
		}
		# for (m in mountdev) print m " on " mountdev[m] " type " mounttypes[m]
		mode=4
	} else if (mode == 4) {
		# print the branches and its mappings
		if (mounttypes[$0])
			print $0, mounttypes[$0], "on"
		else
			print $0, "unknown", "on"
	}
  }  
'
)
# '
# 3. Ask user to choose the SFS
[ -z "$items" ] && die "No layered filesystem found."
dialog --separate-output --backtitle "tmpfs sandbox" --title "sandbox config" \
	--checklist "Choose which SFS you want to use" 0 60 10 $items 2> $TMPFILE

chosen="$(cat $TMPFILE)"
clear
[ -z "$chosen" ] && die "Cancelled or no SFS is chosen, exiting."

# 3.1 lxc - enable network?
[ "$USE_NETWORK" = ask ] && dialog --backtitle "tmpfs sandbox" \
	--title "Network Configuration" --no-cancel --no-tags \
	--radiolist "Choose how to configure network (required for desktop emulation)" 0 60 0 \
	"" "None" on proxyarp "Proxy ARP" off nat "NAT" off 2> $TMPFILE-net
read USE_NETWORK < $TMPFILE-net
rm -f $TMPFILE-net
clear
#echo "-->$USE_NETWORK<--"

# 3.2 lxc - enable desktop emulation? (only if we use network)
[ "$USE_NETWORK" ] && dialog --backtitle "tmpfs sandbox" --title "use xorg" \
       --defaultno --yesno "Enable desktop emulation?" 0 0 && USE_DESKTOP=yes
clear

# 4. convert chosen SFS to robranches
robranches=""
for a in $(cat $TMPFILE) ; do
	robranches=$robranches:$a=ro
done
rm $TMPFILE

# 5. make the mountpoints if not exist  yet
mkdir -p $FAKEROOT $SANDBOX_TMPFS

# 6. do the magic - mount the tmpfs first, and then the rest with aufs
if mount -t tmpfs none $SANDBOX_TMPFS; then
	chmod 0777 $SANDBOX_TMPFS
	if mount -t aufs -o "br:$SANDBOX_TMPFS=rw$robranches" aufs $FAKEROOT; then
		# 7. record our new aufs-root-id so tools don't hack real filesystem
		SANDBOX_AUFS_ID=$(grep $FAKEROOT /proc/mounts | sed 's/.*si=/si_/; s/ .*//') #'
		sed -i -e '/AUFS_ROOT_ID/ d; /LXC_ID/ d' $FAKEROOT/$BOOTSTATE_PATH 2> /dev/null
		echo AUFS_ROOT_ID=$SANDBOX_AUFS_ID >> $FAKEROOT/$BOOTSTATE_PATH
		echo LXC_ID=$LXC_ID >> $FAKEROOT/$BOOTSTATE_PATH
		
		# 8. sandbox is ready, now just need to mount other supports - pts, proc, sysfs, usb and tmp
		mkdir -p $FAKEROOT/$SANDBOX_TMPFS
		mount -o bind $SANDBOX_TMPFS $FAKEROOT/$SANDBOX_TMPFS	# so we can access it within sandbox

		# old, insecure desktop emulation, no longer use this
		# 9. desktop emulation preparation. We copy files regardless because /etc/profile always tries to start xwin
		#cp /usr/share/sandbox/lxc/xwin $FAKEROOT/usr/bin
		#cp /usr/share/sandbox/lxc/wmexit $FAKEROOT/usr/bin
		#LXC_SHARE_IPC=""
		#if [ "$USE_DESKTOP" ]; then
		#	LXC_SHARE_IPC="--share-ipc=1" # share ipc from root container
		#	mount -o bind /tmp $FAKEROOT/tmp		
		#	xauth add ${LXC_ID}/unix:0 $(xauth list 2> /dev/null | awk "/^$(hostname)\/unix/"' {print $2, $3; exit}') 2> /dev/null
		#	echo "[ -z \"\$DISPLAY\" ] && export DISPLAY=$DISPLAY" >> $FAKEROOT/etc/shinit
		#	rm $FAKEROOT/etc/.XLOADED 2> /dev/null	# hack because of Xephyr always crash the first time			
		#fi
		
		# 10. make sure we identify ourself as in sandbox
		echo -e '\nexport PS1="sandbox-lxc'${SANDBOX_ID}'# "' >> $FAKEROOT/etc/shinit #fatdog 600
		sed -i -e '/^PS1/ s/^.*$/PS1="sandbox-lxc'${SANDBOX_ID}'# "/' $FAKEROOT/etc/profile # other puppies
		echo "export TERM=$TERM" >> $FAKEROOT/etc/shinit  # default TERM is screwed, so use host's 
		# hostname
		sed -i -e "/${LXC_ID}/ d" $FAKEROOT/etc/hosts
		echo "127.0.0.1 ${LXC_ID}" >> $FAKEROOT/etc/hosts
		echo "${LXC_ID}" > $FAKEROOT/etc/hostname
		# make sure we can shutdown, remove the acpi_poweroff tests
		sed -i -e '/acpi_poweroff/d' $FAKEROOT/sbin/poweroff $FAKEROOT/sbin/reboot

		# 11. prepare init config
		rm $FAKEROOT/sbin/init; cp $BASELINE_MOUNT/sbin/init $FAKEROOT/sbin/init
		cp /usr/share/sandbox/lxc/inittab $FAKEROOT/etc/inittab 

		# 12. make minimal /dev
		rm -rf $FAKEROOT/dev; mkdir $FAKEROOT/dev
		mknod $FAKEROOT/dev/tty c 5 0; mknod $FAKEROOT/dev/console c 5 1; mkdir $FAKEROOT/dev/pts
		for dev in "null-3" "zero-5" "full-7" "random-8" "urandom-9"; do
			mknod $FAKEROOT/dev/${dev%-*} c 1 ${dev#*-}
		done
		
		# 13. setup when running with user namespaces 
		if [ "$IDMAP" ] && zcat /proc/config.gz | grep -q CONFIG_USER_NS=y; then
			echo User namespaces enabled, running as uid/gid $LXC_UID/$LXC_GID
			LXC_AUTO_MOUNT="-s lxc.mount.auto='proc cgroup sys'"
			LXC_UID_MAP="-s lxc.id_map='u 0 $LXC_UID $LXC_IDRANGE'"
			LXC_GID_MAP="-s lxc.id_map='g 0 $LXC_GID $LXC_IDRANGE'"
			LXC_DROP_CAP=
			chown -Rh $LXC_UID:$LXC_GID $FAKEROOT/dev
			#rm -rf $FAKEROOT/root; cp -a /root $FAKEROOT
			chown     $LXC_UID:$LXC_GID $FAKEROOT/
			chown -Rh $LXC_UID:$LXC_GID $FAKEROOT/etc $FAKEROOT/root
		else
			echo *Not* using user namespaces, dropping capabilities
			LXC_AUTO_MOUNT="-s lxc.mount.auto='proc cgroup sys'"
			LXC_UID_MAP= 
			LXC_GID_MAP=
			LXC_DROP_CAP="-s lxc.cap.drop='sys_module sys_time sys_rawio syslog'"
		fi

		# 13.1 network connection
		prepare_network

		# 13.2 post-process if we use desktop
		if [ $USE_DESKTOP ]; then
			XSERVER_PID=
			# get free display number
			for disp in $(seq 1 10); do
				[ ! -e /tmp/.X${disp}-lock ] && break
			done
			Xephyr :$disp -listen tcp -ac -host-cursor -screen $WINDOW_SIZE -keybd ephyr,,xkbmodel=evdev,xkbrules=evdev > /dev/null 2>&1 &
			XSERVER_PID=$!
			cp /usr/bin/xwin $FAKEROOT/usr/bin
			echo "DISPLAY='$HOST_IP:$disp'" > $FAKEROOT/etc/X11/host-x11 # save DISPLAY from host
			sed -i -e '\_xinit $HOME/.xinitrc_ s|xinit|. /etc/X11/host-x11; export DISPLAY;|' $FAKEROOT/usr/bin/xwin
			cp /usr/lib64/uml/wmexit $FAKEROOT/usr/bin # we use UML's wmexit
		else
			> $FAKEROOT/usr/bin/xwin
			> $FAKEROOT/usr/bin/wmexit
		fi

		# 14. adjust sysinit/shutdown commands
		sed -i -e '/rc.sysinit.lxc/d; /rc.shutdown.lxc/d' $FAKEROOT/etc/inittab

		# 14.1. append oneshot commands
		# TODO: Would it be better to use lxc.init.cmd?
		if [ $# -gt 0 ]; then
			{	echo "#!/bin/sh" 
				for p in "$@"; do printf "'%s' " "$p"; done
				echo
			} > $FAKEROOT/etc/rc.d/rc.oneshot
			chmod +x $FAKEROOT/etc/rc.d/rc.oneshot
		else
			rm -f $FAKEROOT/etc/rc.d/rc.oneshot
			sed -i -e '/rc.oneshot/d' $FAKEROOT/etc/inittab 
		fi

		#  We're good to go!
		echo "Starting sandbox (id: ${LXC_ID}) now."
		# TODO: consider to use lxc.environment to pass "boot parameter" to init
		eval lxc-start -d -f /etc/lxc/default.conf -n $LXC_ID  \
					-s lxc.devttydir=lxc -s lxc.pts=10 -s lxc.tty=4 \
					-s lxc.rootfs=$FAKEROOT \
					-s lxc.utsname=${LXC_ID} -s lxc.network.type=veth -s lxc.network.veth.pair=${LXC_ID} \
					-s lxc.cgroup.devices.deny=a -s lxc.cgroup.devices.allow=\"c *:* rwm\" \
					-s lxc.autodev=0 $LXC_AUTO_MOUNT \
					$LXC_SHARE_IPC $LXC_DROP_CAP $LXC_UID_MAP $LXC_GID_MAP # -o /tmp/lxc-err.log -l TRACE
					#-- busybox getty -n -l /bin/autologin 38400 tty1
		sleep 1		# allow some time for container to start

		# Post-process if we use network
		if [ "$USE_NETWORK" ]; then
			# wait for the thing to show up
			while ! iplink show $LXC_ID > /dev/null 2>&1; do
				sleep 0.5
			done
			start_network
		fi

		# start console and restart until container is terminated
		set -m # we need console's pid so we can kill it later		
		while true; do
			lxc-console -q -e g -n "$LXC_ID" -t 1 &
			CONSOLE_PID=$!
			fg %%
			lxc-info -n $LXC_ID >/dev/null 2>&1 || break
		done

		# 14. done - clean up everything 
		umountall
		echo "Leaving sandbox."
	else
		echo "Unable to mount aufs br:$SANDBOX_TMPFS=rw$robranches"
		umount -l $SANDBOX_TMPFS		
	fi
else
	echo "unable to mount tmpfs."
fi
