wren-linux / bin / onion-site-session
#!/bin/bash
# onion-site-session SUBCOMMAND: written by Wren Linux extras/onion-site.sh
# Installed as /usr/local/libexec/onion-site-session. Your user may run exactly
# this, as the onion-site user, and nothing else (a rule in /etc/sudoers.d). The
# onion-site command calls it. It drives the site's VM (one qemu-system-x86_64
# process, via the bundled mockvps-kvm).
#
# After its first setup the VM runs with no network (QEMU's restrict=on): only
# two host-loopback ports lead in, the VM's SSH console (2243) and its nginx
# (18080, which Tor's onion service points at). "update" adds one more way, for
# apt: 10.0.2.100:9050 in the VM is relayed to the host's Tor.
set -euo pipefail
[ "$(id -un)" = onion-site ] || { echo "onion-site-session: only for the onion-site user." >&2; exit 1; }
# Run again without a way to reach XWayland's abstract socket, which any user
# can connect to (qemu and ssh below inherit this)
noabs=/usr/local/libexec/wren-no-abstract-sockets
if [ -z "${WREN_NOABS:-}" ]; then
[ -x "$noabs" ] || { echo "onion-site-session: $noabs is missing. Run: bash extras/onion-site.sh" >&2; exit 1; }
export WREN_NOABS=1
exec "$noabs" "$0" "$@"
fi
# Fixed configuration. The ports must match data/onion-site-firewall.nft and
# lib/onion-site.sh.
export HOME=/home/onion-site
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export LANG=${LANG:-C.UTF-8}
unset SUDO_USER
export USER=onion-site LOGNAME=onion-site
export VM_NAME=onion-site
export VM_USER=site
export SSH_HOST_IP=127.0.0.1
export SSH_HOST_PORT=2243
export WORKDIR=$HOME/vm
export WEB_TUNNEL=0
WEB=127.0.0.1:18080:80
SOCKS=10.0.2.100:9050
# QEMU's own syscall filter, as for claude-vm: strangers reach what runs in this VM, so a bug in
# QEMU is what a hacked site would aim for next, and this limits what that process could then do.
# The update's relay to Tor is a program QEMU starts for each connection, so that run may start one.
FILTER_UPDATE=on,obsolete=deny,elevateprivileges=deny,resourcecontrol=deny
FILTER=$FILTER_UPDATE,spawn=deny
MOCKVPS=/usr/local/libexec/onion-site-mockvps-kvm
PROVISION=$HOME/.config/onion-site/provision.sh
KEY_FILE=$HOME/.ssh/${VM_NAME}_id
die() { printf 'onion-site-session: %s\n' "$*" >&2; exit 1; }
# The firewall that keeps the VM off your computer and network, seen from this
# user (bin/sandbox-net-check): checked before anything starts a VM (the first
# install too, which has the internet), and watched while it runs: if the rule
# goes, the VM goes too.
NET_CHECK=/usr/local/libexec/wren-sandbox-net-check
net_ok() {
[ -x "$NET_CHECK" ] || die "$NET_CHECK is missing; run extras/onion-site.sh again."
"$NET_CHECK" || die "the firewall that keeps the VM off your network isn't active, so the VM wasn't started. Run: sudo systemctl restart wren-onion-site-firewall"
# (and while this runs: an install has the internet for minutes before QEMU's own watch begins)
"$NET_CHECK" --watch $$ </dev/null >/dev/null 2>&1 &
}
watch_vm() {
local pid
pid=$(head -n1 "$WORKDIR/run/$VM_NAME.pid" 2>/dev/null) || return 0
[[ $pid =~ ^[0-9]+$ ]] || return 0
setsid "$NET_CHECK" --watch "$pid" </dev/null >/dev/null 2>&1 &
}
mockvps() {
local rc=0 starts=false
[ -x "$MOCKVPS" ] || die "the VM engine is missing ($MOCKVPS); run extras/onion-site.sh again."
case " $* " in *" --uninstall "*) ;; *" --start "* | *" --yes "*) starts=true ;; esac
if $starts; then net_ok; fi
"$MOCKVPS" "$@" || rc=$?
if $starts; then watch_vm; fi
return "$rc"
}
# Non-interactive, key-only ssh into the VM, independent of ~/.ssh/config
guest_ssh() {
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
-o HostKeyAlias="$VM_NAME" -o IdentitiesOnly=yes -o LogLevel=ERROR \
-i "$KEY_FILE" -p "$SSH_HOST_PORT" "$VM_USER@$SSH_HOST_IP" "$@"
}
is_installed() { [ -s "$WORKDIR/$VM_NAME.qcow2" ]; }
running() { mockvps --status 2>/dev/null | grep -q Running; }
# boot: start the VM with no network, the site's port forwarded
boot() {
echo "Starting the site's VM; this takes about a minute."
net_ok
env NET_RESTRICT=1 HOST_FORWARDS="$WEB" QEMU_SANDBOX="$FILTER" "$MOCKVPS" --start 2>&1 |
sed -un 's/^==> / /p; /^\(ERROR\|WARNING\):/p' ||
die "could not start the VM (see $WORKDIR/${VM_NAME}-run.log)."
watch_vm
}
ensure_up() {
is_installed || die "no VM yet. Set it up first: wrenctl install onion-site"
running || boot
}
case "${1:-}" in
start) ensure_up; echo "The site is up." ;;
stop) mockvps --stop ;;
status) mockvps --status ;;
shell)
ensure_up
exec ssh -t -o StrictHostKeyChecking=accept-new -o HostKeyAlias="$VM_NAME" \
-o IdentitiesOnly=yes -i "$KEY_FILE" -p "$SSH_HOST_PORT" "$VM_USER@$SSH_HOST_IP" \
'cd /srv/site 2>/dev/null; exec bash -l'
;;
upload)
# upload static|app: a tar stream on standard input replaces that folder
# (there is no shared folder; the onion-site command sends it)
case ${2:-} in static | app) ;; *) die "upload what? static or app" ;; esac
ensure_up
guest_ssh "set -e; new=\$(mktemp -d /srv/site/.upload.XXXXXX); tar -x --no-same-owner -C \"\$new\"
rm -rf /srv/site/$2.old; mv /srv/site/$2 /srv/site/$2.old; mv \"\$new\" /srv/site/$2
chmod 755 /srv/site/$2; rm -rf /srv/site/$2.old
if [ $2 = app ] && systemctl cat site-app.service >/dev/null 2>&1; then sudo -n /usr/bin/systemctl restart site-app.service; fi"
echo "Uploaded to /srv/site/$2 in the VM."
;;
update)
# apt through the host's Tor: restart the VM with the relay, upgrade, and
# restart it without the relay. PROVISION_FILES runs the upgrade as root.
is_installed || die "no VM yet. Set it up first: wrenctl install onion-site"
up=$(mktemp)
trap 'rm -f "$up"' EXIT
printf '%s\n' '#!/bin/bash' 'set -e' 'export DEBIAN_FRONTEND=noninteractive' \
'apt-get update' 'apt-get -y upgrade' 'apt-get -y autoremove --purge' >"$up"
if running; then mockvps --stop >/dev/null; fi
echo "Updating the VM's packages through Tor (slow)."
rc=0
net_ok
env NET_RESTRICT=1 HOST_FORWARDS="$WEB" GUEST_SOCKS="$SOCKS" QEMU_SANDBOX="$FILTER_UPDATE" PROVISION_FILES="$up" "$MOCKVPS" --start || rc=$?
# Never left running with the relay, whether the update worked or not
mockvps --stop >/dev/null || true
boot
[ "$rc" -eq 0 ] || die "the update didn't finish (see above). The site runs again, without the relay to Tor."
echo "Updated, and running again without the relay to Tor."
;;
install)
# First build: the real user triggers this from extras/onion-site.sh. It
# never replaces a VM that is already there: rebuild is for that.
if is_installed; then
echo "The site's VM is already built; kept as it is. For a fresh one: onion-site rebuild"
exit 0
fi
[ -f "$PROVISION" ] || die "the provisioning script is missing ($PROVISION); run extras/onion-site.sh again."
# The install needs the network (Debian and nginx come from the
# internet); from the next start on, it has none.
PROVISION_FILES="$PROVISION" mockvps --yes
mockvps --stop >/dev/null
boot
;;
rebuild)
[ -f "$PROVISION" ] || die "the provisioning script is missing ($PROVISION); run extras/onion-site.sh again."
mockvps --uninstall --yes >/dev/null 2>&1 || true
PROVISION_FILES="$PROVISION" mockvps --yes
mockvps --stop >/dev/null
boot
;;
uninstall) mockvps --uninstall --yes ;;
*) die "unknown subcommand '${1:-}' (start, stop, status, shell, upload, update, install, rebuild, uninstall)." ;;
esac