diff --git a/hosts/valefar/pia-qbittorrent.nix b/hosts/valefar/pia-qbittorrent.nix index 4e24c4c..ff87c64 100644 --- a/hosts/valefar/pia-qbittorrent.nix +++ b/hosts/valefar/pia-qbittorrent.nix @@ -7,9 +7,17 @@ let wgAuth = config.age.secrets."pia-wireguard-auth.env".path; piaSource = ./pia-manual; - # Same story as pia-exit.nix: PIA rotates endpoint fleets without notice; - # rotate `endpoint` in lockstep with pia-exit.nix. - endpoint = "206.206.95.51"; # us-washingtondc / ovpntcp + # PIA rotates endpoint fleets without notice (see pia-exit.nix). This client + # no longer shares pia-exit's region: port forwarding needs a server that + # answers the forwarding API (https://:19999), and no US region has + # one. Probed 2026-10-11 by opening a throwaway tunnel per server and calling + # getSignature: every ca_ontario and ca (Montreal) ovpntcp server answered, + # every ca_toronto one refused the connection even though the region list + # flags it port_forward. ca_ontario: ~33 ms from valefar, 0% loss (Montreal + # 8-28%). Refresh from https://serverlist.piaservers.net/vpninfo/servers/v6 + # (ovpntcp ip + cn) and re-probe before switching. + endpoint = "178.93.200.239"; # ca_ontario / ovpntcp + endpointCn = "Server-12697-0a"; # certificate name of that server, for the forwarding API gateway = "192.168.172.1"; # Host-side bootstrap: token + OpenVPN config + credentials under # /run/pia-qbittorrent, bind-mounted read-only into the guest. @@ -53,6 +61,63 @@ let echo "" } > /run/pia-qbittorrent/pia.ovpn ''; + # Inbound peers need a forwarded port. PIA hands one out per token, and only + # keeps it bound while it is refreshed (docs: every 15 min). The port is + # opened in the guest firewall and written into qBittorrent's config, which is + # restarted only when the port actually changes. + portForward = pkgs.writeShellScript "pia-portforward" '' + set -euo pipefail + export PATH=${ + lib.makeBinPath [ + pkgs.coreutils + pkgs.curl + pkgs.gawk + pkgs.gnused + pkgs.iproute2 + pkgs.iptables + pkgs.jq + pkgs.systemd + ] + } + conf=/var/lib/qbittorrent/qBittorrent/config/qBittorrent.conf + gateway=$(ip -4 route show 0.0.0.0/1 dev pia | awk '{print $3; exit}') + [ -n "$gateway" ] || { echo "no route to the PIA gateway yet"; exit 1; } + api() { + curl --silent --show-error --max-time 10 --get \ + --connect-to "${endpointCn}::$gateway:" \ + --cacert ${piaSource}/ca.rsa.4096.crt "$@" + } + ok() { [ "$(jq -r .status <<<"$1")" = OK ]; } + + sig=$(api --data-urlencode "token=$(tr -d '\n' < /etc/openvpn/pia-creds)" \ + "https://${endpointCn}:19999/getSignature") + ok "$sig" || { echo "getSignature: $(jq -r '.message // .status' <<<"$sig")"; exit 1; } + payload=$(jq -r .payload <<<"$sig") + signature=$(jq -r .signature <<<"$sig") + port=$(base64 -d <<<"$payload" | jq -r .port) + echo "forwarded port $port, expires $(base64 -d <<<"$payload" | jq -r .expires_at)" + echo "$port" > /run/pia-portforward/port + + bind() { + local r + r=$(api --data-urlencode "payload=$payload" --data-urlencode "signature=$signature" \ + "https://${endpointCn}:19999/bindPort") + ok "$r" || { echo "bindPort: $r"; return 1; } + } + bind + # nixos-fw drops everything on the tunnel except what it accepts. + for proto in tcp udp; do + iptables -w -C nixos-fw -i pia -p "$proto" --dport "$port" -j nixos-fw-accept 2>/dev/null \ + || iptables -w -I nixos-fw 1 -i pia -p "$proto" --dport "$port" -j nixos-fw-accept + done + if [ "$(sed -n 's/^Session\\Port=//p' "$conf")" != "$port" ]; then + systemctl stop qbittorrent + sed -i -E 's/^(Session\\Port)=.*/\1='"$port"'/' "$conf" + systemctl start qbittorrent + fi + [ "$(sed -n 's/^Session\\Port=//p' "$conf")" = "$port" ] || { echo "qBittorrent is not on port $port"; exit 1; } + while sleep 600; do bind; done + ''; in { # Bootstrap on the host: the container never needs clear-net DNS or HTTPS. @@ -177,6 +242,21 @@ in RestartSec = "5s"; }; }; + systemd.services.pia-portforward = { + wantedBy = [ "pia-openvpn.service" ]; + requires = [ "pia-openvpn.service" ]; + after = [ + "pia-openvpn.service" + "firewall.service" + ]; + partOf = [ "pia-openvpn.service" ]; + serviceConfig = { + ExecStart = portForward; + RuntimeDirectory = "pia-portforward"; + Restart = "always"; + RestartSec = "1min"; + }; + }; systemd.services.pia-openvpn = { requires = [ "firewall.service" ]; after = [