wren-linux / bin / incognito-session
#!/bin/bash
# incognito-session: written by Wren Linux parts/incognito.sh
# Runs as the user "incognito", or "wifi-signin" for the Wi-Fi sign-in window
# (the incognito command starts it through sudo; it is the only thing your
# user may run as either). Each user runs only its own kind of window: the
# sign-in one, whose firewall rule lets it reach the local network, never a
# normal one. Opens Firefox
# inside cage, a tiny Wayland compositor shown as one window in your sway.
# Home folder, profile, cache and downloads all live in /dev/shm (memory)
# and are deleted when Firefox closes.
# Sound: its own PipeWire (no sound card, no devices) whose only output is
# a pipe into your user's player; see the incognito command.
set -euo pipefail
umask 077
export PATH=/usr/local/bin:/usr/bin:/bin
die() { printf 'incognito: %s\n' "$*" >&2; exit 1; }
# Keeps it off XWayland's abstract socket, which any user can connect to
noabs=/usr/local/libexec/wren-no-abstract-sockets
[ -x "$noabs" ] || die "$noabs is missing. Run: wrenctl part incognito"
me=$(id -un)
[ "$me" = incognito ] || [ "$me" = wifi-signin ] || die "this runs as the user incognito or wifi-signin; use the incognito command."
# The firewall that keeps it off your computer (and, for incognito, your network),
# seen from this user: no rule, no window (bin/sandbox-net-check); and while it runs, it is watched
netcheck=/usr/local/libexec/wren-sandbox-net-check
[ -x "$netcheck" ] || die "$netcheck is missing. Run: wrenctl part incognito"
"$netcheck" || die "the firewall that keeps it off your network isn't active, so it wasn't started. Run: sudo systemctl restart wren-incognito-firewall"
[ $# -ge 5 ] || die "usage: incognito-session SOCKET_DIR WAYLAND_DISPLAY AUDIO THEME normal|signin [URL...]"
host_dir=$1 host_wl=$2 audio=$3 theme=$4 kind=$5
shift 5
# These come from the user who called us, so nothing else is accepted
[[ $host_dir =~ ^/dev/shm/wren-$me\.[A-Za-z0-9]+$ ]] || die "unexpected socket folder."
[[ $host_wl =~ ^[A-Za-z0-9._-]+$ ]] || die "unexpected Wayland display name."
[ -S "$host_dir/$host_wl" ] || die "can't reach the Wayland socket."
[[ $audio == - || $audio == incognito-audio.fifo ]] || die "unexpected sound pipe name."
[[ $theme == light || $theme == dark ]] || die "unexpected theme."
[[ $kind == normal || $kind == signin ]] || die "unexpected window kind."
{ [ "$me" = incognito ] && [ "$kind" = normal ]; } || { [ "$me" = wifi-signin ] && [ "$kind" = signin ]; } ||
die "the user $me doesn't run the $kind window."
if [ "$audio" != - ]; then
[ -p "$host_dir/$audio" ] || audio=-
for c in pipewire wireplumber pipewire-pulse; do
command -v "$c" >/dev/null || audio=-
done
fi
for a in "$@"; do
[[ $a =~ ^https?://[^[:space:]]+$ ]] || die "only http:// and https:// addresses can be opened."
done
[ -d /dev/shm ] || die "/dev/shm (memory) isn't available."
# Left behind by a crash or a power cut
find /dev/shm -maxdepth 1 -name "$me.*" -user "$me" -exec rm -rf {} + 2>/dev/null || true
base=$(mktemp -d "/dev/shm/$me.XXXXXXXX")
mkdir "$base/home" "$base/run" "$base/profile" "$base/tmp"
pid=
cleanup() {
trap - EXIT INT TERM HUP
if [ -n "$pid" ]; then
kill -TERM -- "-$pid" 2>/dev/null || true
sleep 1
kill -KILL -- "-$pid" 2>/dev/null || true
fi
rm -rf "$base"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
cp /usr/local/share/wren/incognito-user.js "$base/profile/user.js"
# Downloads go to memory too, and are gone when the window closes
{
echo 'user_pref("browser.download.folderList", 2);'
echo "user_pref(\"browser.download.dir\", \"$base/tmp\");"
echo 'user_pref("browser.download.useDownloadDir", true);'
# light or dark (bin/theme): Firefox's own look (websites are told light
# anyway: fingerprinting resistance)
if [ "$theme" = light ]; then
echo 'user_pref("extensions.activeThemeID", "firefox-compact-light@mozilla.org");'
else
echo 'user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");'
fi
} >>"$base/profile/user.js"
# doh_prefs normal|signin: Firefox's own DNS, set at every start. While secure
# DNS is on (parts/secure-dns.sh; /etc/resolv.conf says so) Firefox uses the
# system's, which is encrypted; while it is off, its own DNS over HTTPS to
# Quad9: strict (never plain DNS, so a hostile network can't push it back to
# plain DNS by blocking the encrypted kind) unless fallback was chosen at the
# sandboxed Firefox's install. The Wi-Fi sign-in window (signin) uses the
# network's own DNS: that is what it is for. (The same in firefox-session and
# incognito-session; a test checks.)
doh_prefs() {
local mode=3
if [ "$1" = signin ] ||
{ [ ! -L /etc/resolv.conf ] && grep -qsE '^# Written by Wren Linux (parts|extras)/secure-dns\.sh' /etc/resolv.conf; }; then
mode=5
elif [ "$(sed -n 's/^dns=//p' /etc/wren/firefox.conf 2>/dev/null | head -1)" = fallback ]; then
mode=2
fi
echo "user_pref(\"network.trr.mode\", $mode);"
echo 'user_pref("network.trr.uri", "https://dns.quad9.net/dns-query");'
echo 'user_pref("network.trr.custom_uri", "https://dns.quad9.net/dns-query");'
}
doh_prefs "$kind" >>"$base/profile/user.js"
# The Wi-Fi sign-in window: sign-in pages are plain http (HTTPS-Only would
# stop at a warning first)
if [ "$kind" = signin ]; then
echo 'user_pref("dom.security.https_only_mode", false);' >>"$base/profile/user.js"
fi
# Its own sound server. The only sink it has writes into the pipe; there
# are no sound cards and no microphone to reach. WirePlumber's "policy"
# profile does the linking without looking for any hardware.
if [ "$audio" != - ]; then
mkdir -p "$base/home/.config/pipewire/pipewire.conf.d"
cat >"$base/home/.config/pipewire/pipewire.conf.d/10-incognito.conf" <<EOF
context.modules = [
{ name = libpipewire-module-pipe-tunnel
args = {
tunnel.mode = sink
pipe.filename = "$host_dir/$audio"
audio.format = S16LE
audio.rate = 48000
audio.channels = 2
audio.position = [ FL FR ]
node.name = incognito-out
node.description = "incognito"
}
}
]
EOF
fi
# cage connects to your sway (the restricted socket from wayland-restrict,
# by absolute WAYLAND_DISPLAY) and makes
# its own socket in XDG_RUNTIME_DIR for Firefox. Software rendering; the
# render group lets cage open the GPU's render node, which it needs to start. Everything below runs in one
# session, so closing it all is one signal to the group.
# shellcheck disable=SC2016 # expanded by the inner bash, on purpose
start='
waitfor() { for _ in $(seq 50); do [ -S "$1" ] && return 0; sleep 0.1; done; return 1; }
if [ -n "$INCOGNITO_AUDIO" ]; then
pipewire &
if waitfor "$XDG_RUNTIME_DIR/pipewire-0"; then
wireplumber -p policy &
pipewire-pulse &
waitfor "$XDG_RUNTIME_DIR/pulse/native" || true
fi
fi
exec cage -d -- /usr/lib/firefox-esr/firefox-esr --no-remote --profile "$INCOGNITO_PROFILE" "$@"'
setsid env -i \
PATH="$PATH" LANG=C.UTF-8 \
HOME="$base/home" TMPDIR="$base/tmp" XDG_RUNTIME_DIR="$base/run" \
XDG_CONFIG_HOME="$base/home/.config" XDG_CACHE_HOME="$base/home/.cache" \
XDG_DATA_HOME="$base/home/.local/share" \
WAYLAND_DISPLAY="$host_dir/$host_wl" WLR_BACKENDS=wayland WLR_RENDERER=pixman \
LIBGL_ALWAYS_SOFTWARE=1 MOZ_ENABLE_WAYLAND=1 MOZ_DBUS_REMOTE=0 GTK_USE_PORTAL=0 \
DBUS_SESSION_BUS_ADDRESS=disabled: \
PULSE_SERVER="unix:$base/run/pulse/native" \
INCOGNITO_AUDIO="$([ "$audio" != - ] && echo 1)" INCOGNITO_PROFILE="$base/profile" \
"$noabs" bash -c "$start" incognito-start "$@" &
pid=$!
"$netcheck" --watch "$pid" &
wait "$pid" || true