Initial commit

This commit is contained in:
Martin 2021-11-07 12:30:57 +01:00
commit da4ce0789d
Signed by: mawalu
GPG Key ID: BF556F989760A7C8
6 changed files with 304 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
ssh/*
image/*
!**/.gitkeep

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# qemu-sandbox
PoC shell sandboxing using QEMU and virtiofsd.
## Installation
Clone the repo and link `qsandbox` somewhere in your path. The script currently expects the `image` and `ssh` folder next to its location on disk.
## Setup
You'll need a few things for the script to work:
* A ssh key pair in `ssh/qemu_ssh` & `ssh/qemu_ssh.pub`. You can link your default key pair or use the chance to generate one without a passphrase.
* `image/image.qcow2`, `image/vmlinuz-linux`, `image/initramfs-linux-custom.img`. The `build.sh` script can build these based on arch
These requirements are currently hard coded but should be configurable in the future.
## Usage
```
Usage:
qsandbox run [dir] - start sandbox and mount current working dir
qsandbox list - list running sandboxes
qsandbox enter - open ssh connection to a sandbox
qsandbox qemu - start the qemu process for a new sandbox, used by run
```
### `qsandbox run`
Starts a new vm using `systemd-run` and `qsandbox qemu`, mounts the current working dir or the specified directory and opens an ssh session.
### `qsandbox list`
Lists all running sandboxes and their ssh ports.
### `qsandbox enter`
A wrapper around `ssh`. Takes port as only argument but defaults to `5555`.
### `qsandbox qemu`
Starts the actual sandbox.
## Accessing the sandbox
By default, QEMU exposes to ports for each sandbox. An ssh port (starting at `5555`) and an "app port" that can be used by some app in the vm (starting at `8000`). Ports should be configurable in the future.
## Tips for custom images
Mount the default share automatically:
```
echo -e "share.1\t/mnt\tvirtiofs\trw,_netdev\t0\t0" >> /etc/fstab
```
Disable auth on the QEMU serial console:
```
mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d
echo -e "[Service]\nExecStart=\nExecStart=-/usr/bin/agetty --autologin root -s %I 115200,38400,9600 vt102" > /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf
````
# License
MIT

75
build.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/bash
# Based on https://blog.stefan-koch.name/2020/05/31/automation-archlinux-qemu-installation
src=https://ftp.halifax.rwth-aachen.de/archlinux/iso/2021.11.01/archlinux-bootstrap-2021.11.01-x86_64.tar.gz
archive=image/archlinux.tar.gz
image=image/image.raw
mountpoint=image/arch
if [[ ! -f $archive ]]; then
wget $src -O $archive
fi
mkdir -p $mountpoint
mkdir -p ssh
qemu-img create -f raw $image 20G
loop="$(sudo losetup --show -f -P $image)"
sudo mkfs.ext4 "$loop"
sudo mount "$loop" "$mountpoint"
sudo tar zxf "$archive" -C "$mountpoint" --strip-components 1
key="$(cat ssh/qemu_ssh.pub)"
sudo "$mountpoint/bin/arch-chroot" "$mountpoint" /bin/bash <<EOL
set -v
echo 'Server = http://ftp.uni-bayreuth.de/linux/archlinux/\$repo/os/\$arch' >> /etc/pacman.d/mirrorlist
pacman-key --init
pacman-key --populate archlinux
pacman -Syu --noconfirm
pacman -S --noconfirm base linux linux-firmware mkinitcpio dhcpcd dropbear kitty-terminfo
systemctl enable dhcpcd dropbear
dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key
# Standard Archlinux Setup
ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime
hwclock --systohc
echo en_US.UTF-8 UTF-8 >> /etc/locale.gen
locale-gen
echo LANG=en_US.UTF-8 > /etc/locale.conf
echo arch-qemu > /etc/hostname
echo -e '127.0.0.1 localhost\n::1 localhost' >> /etc/hosts
mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d
echo -e "[Service]\nExecStart=\nExecStart=-/usr/bin/agetty --autologin root -s %I 115200,38400,9600 vt102" > /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf
echo -e "share.1\t/mnt\tvirtiofs\trw,_netdev\t0\t0" >> /etc/fstab
mkdir /root/.ssh
echo "$key" > /root/.ssh/authorized_keys
# Create an initramfs without autodetect, because this breaks with the
# combination host/chroot/qemu
linux_version=\$(ls /lib/modules/ | sort -V | tail -n 1)
mkinitcpio -c /etc/mkinitcpio.conf -S autodetect --kernel \$linux_version -g /boot/initramfs-linux-custom.img
echo 'root:root' | chpasswd
EOL
cp "$mountpoint/boot/vmlinuz-linux" image/
cp "$mountpoint/boot/initramfs-linux-custom.img" image/
sudo "$mountpoint/bin/arch-chroot" "$mountpoint" /bin/bash <<EOL
pacman -Rs --noconfirm linux linux-firmware mkinitcpio
rm -r /var/cache/pacman/pkg
rm -r /boot
EOL
sudo umount "$mountpoint"
sudo losetup -d "$loop"
qemu-img convert -f raw -O qcow2 "$image" image/image.qcow2
rm image/image.raw

0
image/.gitkeep Normal file
View File

161
qsandbox Executable file
View File

@ -0,0 +1,161 @@
#!/bin/bash
script_dir="$( cd -- "$( dirname -- "$(readlink -f "${BASH_SOURCE[0]}" )" )" &> /dev/null && pwd )"
qemu_args=()
function run_virtiofsd {
local shares=("$@")
local share_count=1
{
echo "mount -t tmpfs swap /var/run"
for m in "${shares[@]}"; do
local share_name="share.${share_count}"
local socket="$tmp_dir/$share_name"
echo "/usr/lib/qemu/virtiofsd --socket-path=$socket -o source=$m &"
echo "echo starting $m $socket"
share_count=$((share_count+1))
done
} | (unshare --user --map-root-user --mount bash)&
local mounts=${#shares[@]}
for i in $(seq "$mounts"); do
qemu_args+=("-chardev" "socket,id=share.$i,path=$tmp_dir/share.$i")
qemu_args+=("-device" "vhost-user-fs-pci,queue-size=1024,chardev=share.$i,tag=share.$i")
done
}
function start_qemu {
if [[ $# -lt 3 ]]; then
echo "Usage $(basename "$0") qemu <ssh_port> <app_port> <tmp_dir> [mount]"
exit 1
fi
trap finish EXIT
local ssh_port="$1"
local app_port="$2"
tmp_dir="$3"
local image
image="$tmp_dir/$(openssl rand -hex 12).qcow2"
qemu-img create -b "$script_dir/image/image.qcow2" -F qcow2 -f qcow2 "$image"
run_virtiofsd "${4:-$PWD}"
qemu-system-x86_64 \
-enable-kvm -cpu host -m 512m -smp 2 \
-kernel "$script_dir/image/vmlinuz-linux" -append "earlyprintk=ttyS0 console=ttyS0 root=/dev/vda rw quiet" \
-initrd "$script_dir/image/initramfs-linux-custom.img" \
-m 4G -object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on -numa node,memdev=mem \
-device virtio-rng-pci \
-bios /usr/share/qemu/qboot.rom \
-drive if=virtio,file="$image",format=qcow2 \
-netdev user,id=net0,hostfwd=tcp::"$ssh_port"-:22,hostfwd=tcp::"$app_port"-:8000 \
-device virtio-net-pci,netdev=net0 \
-nodefaults -no-user-config -nographic \
-serial stdio "${qemu_args[@]}"
}
function get_port {
local port="$1"
if netstat -tulen | grep -q "$port"; then
get_port "$((port+1))"
else
echo "$port"
fi
}
function run {
local ssh_port
ssh_port="$(get_port 5555)"
local app_port
app_port="$(get_port 8000)"
local work_dir="${1:-$PWD}"
tmp_dir="$(mktemp -d --suffix=.qemu)"
echo "$work_dir"
echo "$ssh_port" > "$tmp_dir/ssh"
echo "$ssh_port" > "$tmp_dir/app"
echo "$work_dir" > "$tmp_dir/work_dir"
systemd-run --user -d "$0" qemu "$ssh_port" "$app_port" "$tmp_dir" "$work_dir"
echo "[ ] SSH Port: $ssh_port"
echo "[ ] App Port: $app_port"
sleep 1
enter "$ssh_port"
}
function finish {
rm -rf "$tmp_dir"
}
function list {
for d in /tmp/*.qemu; do
if [[ -f "$d/ssh" ]]; then
echo "$(cat "$d/ssh") - $(cat "$d/work_dir")"
fi
done
}
function enter {
if [[ "$#" -eq 1 ]]; then
local port="$1"
else
echo "Using default port 5555"
local port="5555"
fi
ssh -i "$script_dir/ssh/qemu_ssh" -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@localhost -p "$port"
}
function usage {
local bin
bin="$(basename "$0")"
echo "Usage:"
echo " $bin run [dir] - start sandbox and mount current working dir"
echo " $bin list - list running sandboxes"
echo " $bin enter - open ssh connection to a sandbox"
echo " $bin qemu - start the qemu process for a new sandbox, used by run"
}
function main {
if [[ "$#" -lt 1 ]]; then
usage "$@"
exit 1
fi
local cmd="$1"
shift
if [[ "$cmd" == "qemu" ]]; then
start_qemu "$@"
fi
if [[ "$cmd" == "list" ]]; then
list
fi
if [[ "$cmd" == "run" ]]; then
run "$@"
fi
if [[ "$cmd" == "enter" ]]; then
enter "$@"
fi
}
main "$@"

0
ssh/.gitkeep Normal file
View File