Wren Linux

wren-linux / docs / extras / whonix.md (as plain text)

Whonix (extras/whonix.sh)

SYNOPSIS

wrenctl install whonix                     download (if needed), check and install both VMs (asks about a bridge)
wrenctl install whonix --bridges           installed already: choose the Gateway's bridge again
wrenctl install whonix --fetch             only download and check it, for later
wrenctl remove whonix                      remove the VMs (asks before deleting their disks)
wrenctl manual whonix                      print the manual steps below

whonix             start both VMs and open both, each on an empty workspace (or Super+d)
whonix gateway     start the Gateway and open it (Tor settings, bridges)
whonix stop        shut both down, right away and cleanly: save your work inside first
whonix stop --force   the same, and one still running after 60 seconds is switched off
whonix live        the same as whonix, but nothing done in the Workstation is kept after it shuts down
whonix update      update both VMs and Tor Browser, keeping your bookmarks; they power off when done

Whonix runs Tor in one virtual machine, the Gateway, and your programs in another, the Workstation, whose only network is the Gateway, so even a hacked browser in the Workstation can't learn your real address.

Everything runs as your user under KVM (libvirt's qemu:///session), with no root daemon or host network setup. The download (about 1.9 GB) is checked against Whonix's signing key, pinned in pins.sh (WHONIX_FPR), and kept in offline/whonix.

Before the first boot, Wren Linux writes into the images, without mounting them (with qemu-img and debugfs): in both, a rule for acpid so that the power button, which whonix stop presses, shuts the VM down cleanly (left to Whonix's desktop, it opens a dialog); a bridge for the Gateway if you need one (obfs4 or webtunnel lines you paste, or Snowflake), so its very first connection uses it, or Tor kept off until you turn it on (--bridges chooses again later); and in the Workstation, Tor Browser at Safest with JavaScript off (data/tor-browser-safest.js), the level locked by the browser's own policy file (data/tor-browser-policies-safest.json: the slider won't move, about:config is blocked), and the Feather wallet for Monero: Feather's own build for Tails and Whonix, which has no Tor of its own and uses the Gateway's, checked against the hash list signed by Feather's key (FEATHER_FPR), as the Feather extra checks it.

Electrum, for Bitcoin, comes with Whonix. If Feather can't be downloaded or checked, Whonix is installed without it.

For wallets, Whonix is the safest choice Wren Linux has: a wallet in the Workstation can't reach this computer's files or its real address, even if it is hacked.

The Gateway's way out is a network card of Wren Linux's, joined to a passt that whonix starts before the Gateway.

Whonix's own file gives it libvirt's, and libvirt can't give passt any option: with passt's defaults the Gateway's address for your router leads to this computer's own loopback instead (every port of it: Tor's port, the VMs' consoles, a local server), and nothing answers DNS, so Snowflake never gets past "broker failure" at 10%.

Wren Linux's passt has Whonix's fixed addresses (the Gateway 10.0.2.15, its gateway 10.0.2.2), IPv4 only, no DHCP, nothing of this computer mapped in (--no-map-gw), and one address, 10.0.2.3, where only DNS goes on, to the resolver this computer uses (Unbound with secure DNS on, so encrypted).

The Gateway's own DNS stays off, as Whonix ships it, except for a bridge that has to look names up (Snowflake, webtunnel): then its /etc/resolv.conf.whonix gets nameserver 10.0.2.3. webtunnel is experimental here: Whonix ships no program for it, so Wren Linux installs lyrebird on this computer from the Tor Project's repository (its key pinned, TORPROJECT_FPR, and an apt pin that lets nothing else come from there), where its updates install themselves, and copies it into the Gateway as /usr/bin/lyrebird; Tor's AppArmor profile in Whonix 18 already lets Tor run it.

whonix update updates everything in one go, over Tor, with both VMs shut down first. It starts the Gateway, which updates and powers off; then starts it as usual, for the Workstation to update its packages and Tor Browser. You see it in their windows, and both end up off.

Nothing starts by itself: it runs when you run it. A week after the last update that worked, the bar shows whonix update (a click runs it), and from then on whonix stop asks whether to update before shutting down. Only an update that worked starts the week again.

How the VMs know: whonix update starts each with a note in its firmware (an SMBIOS OEM string, for a service written into the images at install, data/whonix-update.service). The VM's saved settings aren't changed, so with no update running a VM boots as ever, and the service does nothing.

Whonix's update-torbrowser is a downloader rather than an updater: it deletes the browser's folder and puts a new one there, which would lose your bookmarks and downloads. So data/whonix-update-browser, running as the user and never as root, moves the old folder aside, runs it, and gives the new one your downloads as they are and your bookmarks rebuilt, nothing else of the old profile: a hacked page can write to anything in a profile (settings, add-ons, a changed database), and a profile carried over would keep that. The bookmarks are read from a copy of the old database and written again as a plain bookmarks.html of checked web addresses and titles (javascript:, data: and place: entries, tags and anything with a quote or markup in the address are left out); Firefox imports it at its first start. Once the bookmarks are safely out, the rest of the old profile (history, cookies, settings, whatever a hacked page wrote) is deleted; only if the bookmarks couldn't be read is the old profile kept, in ~/.tb/left-from-browser-VERSION/profile, out of the folder that runs, so they can still be got at. Lost on purpose: per-site NoScript choices, saved onion keys and any other setting. The new browser is put at the Safest level with JavaScript off by the update itself, written into its own profile (user.js, from the settings Wren Linux put in the image), and the level is locked by the browser's policy file (policies.json, likewise), not left to Whonix's start-up; if those files are missing from the image, the update says so plainly and you set Safest by hand. It runs it only for a newer version: with no one to ask, it would install an older one too, which could be an attack. If anything fails, the old folder goes back whole, and a power cut is put right by the next update. Anything else in the old folder that the new one doesn't have, such as a page you saved beside Downloads, is kept in ~/.tb/left-from-browser-VERSION. A download cut off (update-torbrowser status 17, which a bridge's connection can cause) is tried again, up to four times in all, with `update-torbrowser --resume; the flag is Whonix's own advice and has been tried only with a pretendupdate-torbrowser`. Any other failure ends the update and puts the old browser back.

The maintainer's config files replace ones that were changed (--force-confnew, as Whonix's own apt-get-noninteractive does): a security fix in a config file reaches you, where keeping the old one would have kept it out without a word. A file you edited by hand in the VM is replaced too; dpkg keeps yours beside it as .dpkg-old. Put your own settings in /usr/local/etc, where Whonix looks for them and an update leaves them alone. The one packaged file Wren Linux edits, the Gateway's /etc/resolv.conf.whonix (its nameserver line, for a bridge given as a name), is put back after the upgrade, if it was on before; if that ever fails, Tor can't resolve a bridge's name and doesn't connect, which is the safe way to fail (wrenctl install whonix --bridges sets it again).

Installed before this? Running wrenctl install whonix again (with the Gateway shut down) gives it the new card; --bridges then sets the DNS for the bridge you choose.

LIMITS

MANUAL INSTALL

# KVM must be on: this must list /dev/kvm (if not, turn on VT-x / AMD-V in the firmware settings)
ls -l /dev/kvm
# KVM, libvirt and virt-manager (from whonix.org/wiki/KVM, for Debian trixie),
# and the tools to unpack and edit the images
sudo apt install --no-install-recommends qemu-system-x86 qemu-system-gui qemu-system-modules-spice qemu-utils \
    libvirt-daemon libvirt-daemon-driver-qemu libvirt-daemon-driver-storage libvirt-daemon-driver-nodedev \
    libvirt-clients virt-manager gir1.2-spiceclientgtk-3.0 passt ovmf swtpm swtpm-tools \
    xz-utils e2fsprogs fdisk acl
# Your user may use KVM and libvirt (in full from your next login)
sudo adduser "$(id -un)" kvm
sudo adduser "$(id -un)" libvirt
# No core dumps, and no limits libvirt can't set as a normal user
mkdir -p ~/.config/libvirt
printf 'max_core = 0\nmax_processes = 0\nmax_files = 0\n' >~/.config/libvirt/qemu.conf

# The newest version: the highest number on whonix.org's KVM page
wget -qO- https://www.whonix.org/wiki/KVM | grep -o 'download/libvirt/[0-9.]*/Whonix-LXQt-[0-9.]*\.Intel_AMD64' | sort -uV | tail -1
V=18.2.4.2
F=Whonix-LXQt-$V.Intel_AMD64.qcow2.libvirt.xz
mkdir -p ~/whonix-download && cd ~/whonix-download
# The image (about 1.9 GB), its signature and the signing key
wget -c https://www.whonix.org/download/libvirt/$V/$F https://www.whonix.org/download/libvirt/$V/$F.asc
wget -O derivative.asc @WHONIX_KEY_URL@
# The key, in a keyring of its own. This must show the fingerprint @WHONIX_FPR@
mkdir -m 700 gpg
gpg --homedir gpg --import derivative.asc
gpg --homedir gpg --fingerprint @WHONIX_FPR@
# The image is signed by it: this must say "Good signature" with
# "Primary key fingerprint" @WHONIX_FPR@. Stop here if not.
gpg --homedir gpg --verify $F.asc $F

# Unpack it, and read Whonix's license and disclaimer before going on
tar -xSf $F
less WHONIX_BINARY_LICENSE_AGREEMENT WHONIX_DISCLAIMER
mv Whonix-Gateway-*.qcow2 gw.qcow2 && mv Whonix-Workstation-*.qcow2 ws.qcow2

# Files to put into the images before their first boot:
# the Workstation's Tor Browser at Safest, JavaScript off, the level locked by its policy file
cp @WREN_DIR@/data/tor-browser-safest.js user.js
cp @WREN_DIR@/data/whonix-torbrowser.conf 50_wren.conf
cp @WREN_DIR@/data/tor-browser-policies-safest.json policies.json
# Feather, its build for Tails and Whonix (the newest version: the tag in the
# "Location:" line, e.g. 2.9.1), checked as extras/feather-wallet.sh did
wget --max-redirect=0 -S -O /dev/null https://github.com/feather-wallet/feather/releases/latest 2>&1 | grep -i location
VER=2.9.1
wget https://featherwallet.org/files/releases/hashes-$VER.txt https://featherwallet.org/files/releases/linux-appimage-a/feather-$VER-a.AppImage
mkdir -m 700 fgpg
wget -O feather.asc @FEATHER_KEY_URL@
gpg --homedir fgpg --import feather.asc
# This must say "Good signature" with "Primary key fingerprint"
# @FEATHER_FPR@. Stop here if not.
gpg --homedir fgpg --output sums.txt --decrypt hashes-$VER.txt
# This must print "OK"
awk -v n="feather-$VER-a.AppImage" '$2 == n || $2 == "*" n { print $1 "  " n }' sums.txt | sha256sum -c
mv feather-$VER-a.AppImage feather.AppImage
cp @WREN_DIR@/data/whonix-feather @WREN_DIR@/data/whonix-feather.desktop .
# A bridge for the Gateway, if you need one, in 50_user.conf: obfs4 lines
# from bridges.torproject.org (for Snowflake, put "ClientTransportPlugin
# snowflake exec /usr/bin/snowflake-client" and the Snowflake "Bridge" lines
# from data/snowflake-bridges.txt instead)
printf '%s\n' 'UseBridges 1' 'ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy' \
    'Bridge obfs4 1.2.3.4:443 FINGERPRINT cert=... iat-mode=0' >50_user.conf
# Only for Snowflake: the Gateway's own DNS, at 10.0.2.3 (passt hands it to this
# computer's resolver). Take Whonix's file out of the image first: copy it from
# "debugfs -R 'cat /etc/resolv.conf.whonix'" (see put below), then
#   sed -i 's/^#nameserver 10\.0\.2\.3$/nameserver 10.0.2.3/' resolv.conf.whonix
# and put it back as /etc/resolv.conf.whonix (for webtunnel too)
# Only for webtunnel: lyrebird, from the Tor Project's repository (as
# extras/apt-tor.sh adds it: key @TORPROJECT_FPR@, pinned to lyrebird only),
# and in 50_user.conf "ClientTransportPlugin webtunnel exec /usr/bin/lyrebird"
# with your "Bridge webtunnel ..." lines; then, with put below:
#   put gw.qcow2 /usr/bin/lyrebird /usr/bin/lyrebird 755

# put IMAGE FILE DEST [MODE]: write FILE into the image's root file system as
# DEST (root's, mode 0644 or MODE), checked, without mounting it
put() {
    qemu-img convert -O raw "$1" "$1.raw"
    start=$(sfdisk -d "$1.raw" | sed -n 's/.*start= *\([0-9]*\),.*name="primary".*/\1/p')
    fs="$1.raw?offset=$((start * 512))"
    dir=$(dirname "$3")
    debugfs -w -R "mkdir $dir" "$fs"
    debugfs -w -f - "$fs" <<EOF
sif $dir uid 0
sif $dir gid 0
sif $dir mode 040755
rm $3
write $2 $3
sif $3 uid 0
sif $3 gid 0
sif $3 mode 0100${4:-644}
EOF
    # It must be there, as written, and the file system must check out;
    # if not, the image is left as it was
    if ! debugfs -R "cat $3" "$fs" | cmp -s - "$2" || ! e2fsck -fn "$fs"; then
        echo "Couldn't write $3 into $1; it is unchanged." >&2
        rm "$1.raw"
        return 1
    fi
    # (a write that fails, on a full disk, must never take the image's place)
    qemu-img convert -O qcow2 -o cluster_size=2M,extended_l2=on "$1.raw" "$1.new" ||
        { echo "Couldn't write $1 back; it is unchanged." >&2; rm -f "$1.raw" "$1.new"; return 1; }
    rm "$1.raw" && mv "$1.new" "$1"
}
put ws.qcow2 @WREN_DIR@/data/whonix-power-button /etc/acpi/events/wren-power-button
put gw.qcow2 @WREN_DIR@/data/whonix-power-button /etc/acpi/events/wren-power-button
put ws.qcow2 user.js /usr/local/etc/torbrowser.d/user.js
put ws.qcow2 50_wren.conf /usr/local/etc/torbrowser.d/50_wren.conf
put ws.qcow2 policies.json /usr/local/etc/torbrowser.d/policies.json
put ws.qcow2 feather.AppImage /usr/local/lib/feather/feather.AppImage 755
put ws.qcow2 whonix-feather /usr/local/bin/feather 755
put ws.qcow2 whonix-feather.desktop /usr/local/share/applications/feather.desktop
put gw.qcow2 50_user.conf /usr/local/etc/torrc.d/50_user.conf
# What whonix update runs, in both (the service does nothing without the host's note)
for i in gw.qcow2 ws.qcow2; do
    put $i @WREN_DIR@/data/whonix-update /usr/local/libexec/wren-update 755
    put $i @WREN_DIR@/data/whonix-update.service /etc/systemd/system/wren-update.service
    put $i @WREN_DIR@/data/whonix-update-wants.conf /etc/systemd/system/multi-user.target.d/wren-update.conf
done
put ws.qcow2 @WREN_DIR@/data/whonix-update-browser /usr/local/libexec/wren-update-browser 755
# (each must end without "Couldn't write"; stop here if one does)

# The VMs, as your user: the storage pool, the disks, the definitions
mkdir -p ~/.local/share/libvirt/images
virsh -c qemu:///session pool-define-as default dir --target ~/.local/share/libvirt/images
virsh -c qemu:///session pool-autostart default
virsh -c qemu:///session pool-start default
mv gw.qcow2 ~/.local/share/libvirt/images/Whonix-Gateway.qcow2
mv ws.qcow2 ~/.local/share/libvirt/images/Whonix-Workstation.qcow2
virsh -c qemu:///session pool-refresh default
# The Gateway's outside card: in Whonix-Gateway.xml, delete the block
# <interface type='user'> ... </interface>, add xmlns:qemu to its first line, so
# it reads <domain xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0' type='kvm'>,
# and put this just before </domain> (with your home folder in the path; slot
# 0x1e must be free on bus 0x00: virsh dumpxml shows the slots in use)
#   <qemu:commandline>
#     <qemu:arg value='-netdev'/>
#     <qemu:arg value='{"type": "stream", "id": "wren-gw-netdev", "server": false, "reconnect-ms": 1000, "addr": {"type": "unix", "path": "/home/YOU/.local/state/wren/whonix-gateway-net.sock"}}'/>
#     <qemu:arg value='-device'/>
#     <qemu:arg value='{"driver": "virtio-net-pci", "netdev": "wren-gw-netdev", "id": "wren-gw-net", "mac": "@GW_NET_MAC@", "bus": "pcie.0", "addr": "0x1e"}'/>
#   </qemu:commandline>
virsh -c qemu:///session define Whonix-Gateway.xml
virsh -c qemu:///session define Whonix-Workstation.xml
# Its network, before every start of the Gateway (the whonix command does this)
mkdir -p ~/.local/state/wren && chmod 700 ~/.local/state/wren
passt --quiet --one-off --socket ~/.local/state/wren/whonix-gateway-net.sock \
    --pid ~/.local/state/wren/whonix-gateway-net.pid --ipv4-only --address 10.0.2.15 \
    --netmask 24 --gateway 10.0.2.2 --no-map-gw --dns-forward 10.0.2.3 --no-dhcp

# The whonix command
install -D -m 0755 @WREN_DIR@/bin/whonix ~/.local/bin/whonix
# The VMs' windows don't keep sway's keys (Super+1 and the like still switch workspaces)
printf '%s\n' 'for_window [app_id="^virt-manager$"] shortcuts_inhibitor disable' >~/.config/sway/config.d/whonix-keys.conf
swaymsg reload
# When the last update worked: the install counts (whonix update needs this file)
touch ~/.local/state/wren/whonix-updated
cd ~ && rm -rf ~/whonix-download
whonix

To undo:

# Switch the VMs off and forget them
virsh -c qemu:///session destroy Whonix-Workstation
virsh -c qemu:///session destroy Whonix-Gateway
virsh -c qemu:///session undefine Whonix-Workstation
virsh -c qemu:///session undefine Whonix-Gateway
# Their disks, and everything saved inside the VMs
rm ~/.local/share/libvirt/images/Whonix-Gateway.qcow2 ~/.local/share/libvirt/images/Whonix-Workstation.qcow2
# The command
rm ~/.local/bin/whonix ~/.local/state/wren/whonix-updated ~/.config/sway/config.d/whonix-keys.conf
# KVM, libvirt and virt-manager, marked "automatic": autoremove takes them out
# only if nothing (mockvps-kvm, claude-vm, the onion site) needs them
sudo apt-mark auto qemu-system-x86 qemu-system-gui qemu-system-modules-spice qemu-utils libvirt-daemon \
    libvirt-daemon-driver-qemu libvirt-daemon-driver-storage libvirt-daemon-driver-nodedev libvirt-clients \
    virt-manager gir1.2-spiceclientgtk-3.0 passt ovmf swtpm swtpm-tools
sudo apt autoremove --purge