Applied live to the 5 running containers; install scripts and the post-install checklist now include the timedatectl step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.7 KiB
5.7 KiB
transmission-bt
BitTorrent client in an unprivileged Incus container on nuc, with an
always-on VPN: all peer traffic exits via ks4's public IP through a
WireGuard tunnel to the wireguard container on ks4. Kill switch by
construction — eth0 has no default route, so with the tunnel down
the container simply has no path to the internet.
- Image:
images:ubuntu/24.04, IP:192.168.0.7(macvlan, static via netplan) - Web UI:
http://192.168.0.7:9091— no auth (rpc-authentication-required: false); access control is the RPC whitelist (192.168.0.*only) - Egress: WG peer
10.8.0.21→193.70.35.17:51845,AllowedIPs 0.0.0.0/0(verified:curl ifconfig.mefrom the container returns ks4's IP) - Downloads:
/media/downloads(=usb4t/media, same dataset Jellyfin reads); in-progress files in/media/.incompleteso Jellyfin never scans partials transmission-daemonisBindsTo=wg-quick@wg0.serviceand binds peer traffic to10.8.0.21— three independent layers against leaks (no default route, unit binding, socket binding)
Anti-leak design
- netplan gives
eth0only: LAN/24(web UI + DNS via blocky) and a/32host route to the WG endpoint via the home gateway. wg-quickfull-tunnel mode adds its fwmark policy routing + iptables anti-leak rule (iptablespackage required — its absence makeswg-quickfail withiptables-restore: command not found).- LAN traffic keeps working thanks to wg-quick's
suppress_prefixlength 0rule (connected routes win over the tunnel's default).
Install script
Run as root on the Incus host. Requires the peer added on ks4 (below).
#!/usr/bin/env bash
set -euxo pipefail
CNAME="${CNAME:-transmission-bt}"
IMAGE="${IMAGE:-images:ubuntu/24.04}"
incus launch "$IMAGE" "$CNAME"
sleep 8
incus config set "$CNAME" environment.DEBIAN_FRONTEND=noninteractive
incus exec "$CNAME" -- timedatectl set-timezone Europe/Paris
# static LAN config, NO default route (kill switch), /32 to the WG endpoint
incus exec "$CNAME" -- bash -c 'cat > /etc/netplan/10-lxc.yaml <<EOF
network:
version: 2
ethernets:
eth0:
addresses: [192.168.0.7/24]
nameservers:
addresses: [192.168.0.254]
routes:
- to: 193.70.35.17/32
via: 192.168.0.1
EOF
chmod 600 /etc/netplan/10-lxc.yaml
netplan apply'
# packages need a temporary default route (removed right after)
incus exec "$CNAME" -- ip route add default via 192.168.0.1
incus exec "$CNAME" -- apt-get update
incus exec "$CNAME" -- apt-get install -y --no-install-recommends \
transmission-daemon wireguard-tools iptables curl
incus exec "$CNAME" -- ip route del default via 192.168.0.1
# WireGuard full tunnel (generate key, print pubkey for the ks4 side)
incus exec "$CNAME" -- bash -c 'umask 077
wg genkey > /etc/wireguard/wg0.key
wg pubkey < /etc/wireguard/wg0.key
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.8.0.21/24
PrivateKey = $(cat /etc/wireguard/wg0.key)
[Peer]
# wireguard container on ks4 — ALL traffic routes through it
PublicKey = TVs6d7bXTvJ0ZluTLb8wR+zIrsLvkH1944pzM+3dZXM=
Endpoint = 193.70.35.17:51845
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF'
incus exec "$CNAME" -- systemctl enable --now wg-quick@wg0
# media share (same dataset as jellyfin-server)
incus config device add "$CNAME" media disk source=/srv/media path=/media shift=true
incus exec "$CNAME" -- mkdir -p /media/downloads /media/.incomplete
incus exec "$CNAME" -- chown debian-transmission:debian-transmission \
/media/downloads /media/.incomplete
# transmission config (edit only while the daemon is stopped)
incus exec "$CNAME" -- systemctl stop transmission-daemon
incus exec "$CNAME" -- bash -c '
cd /var/lib/transmission-daemon/.config/transmission-daemon
sed -i \
-e "s|\"download-dir\":.*|\"download-dir\": \"/media/downloads\",|" \
-e "s|\"incomplete-dir\":.*|\"incomplete-dir\": \"/media/.incomplete\",|" \
-e "s|\"incomplete-dir-enabled\":.*|\"incomplete-dir-enabled\": true,|" \
-e "s|\"rpc-whitelist\":.*|\"rpc-whitelist\": \"127.0.0.1,::1,192.168.0.*\",|" \
-e "s|\"rpc-authentication-required\":.*|\"rpc-authentication-required\": false,|" \
-e "s|\"bind-address-ipv4\":.*|\"bind-address-ipv4\": \"10.8.0.21\",|" \
settings.json'
# transmission lives and dies with the tunnel
incus exec "$CNAME" -- mkdir -p /etc/systemd/system/transmission-daemon.service.d
incus exec "$CNAME" -- bash -c 'cat > /etc/systemd/system/transmission-daemon.service.d/vpn.conf <<EOF
[Unit]
BindsTo=wg-quick@wg0.service
After=wg-quick@wg0.service
EOF'
incus exec "$CNAME" -- systemctl daemon-reload
incus exec "$CNAME" -- systemctl start transmission-daemon
incus config set "$CNAME" boot.autostart=true
On ks4 (root), authorize the peer with the pubkey printed above:
incus exec wireguard -- wg set wg0 peer <PUBKEY> allowed-ips 10.8.0.21/32
incus exec wireguard -- wg-quick save wg0
Verification
incus exec transmission-bt -- wg show wg0 latest-handshakes # non-zero timestamp
incus exec transmission-bt -- curl -s https://ifconfig.me # must print 193.70.35.17
incus exec transmission-bt -- bash -c "ping -c1 -W2 8.8.8.8 || echo kill-switch OK" # with wg0 down
# web UI must be tested from a LAN machine — the macvlan quirk means the
# nuc host itself cannot reach 192.168.0.7
Notes
- No inbound peer port is forwarded (would need a proxy device on ks4
- DNAT through the tunnel); torrents work fine outbound-only, just connect to fewer peers.
- Jellyfin sees finished downloads under
/media/downloads— add it as a library folder or move files into the movie/show trees. - The image server check can make
incus launchhang on slow WAN — launching from the cached image fingerprint (incus image list) bypasses it.