From c76ebc7063df65faf2f4500f9eaed63727dfb3d0 Mon Sep 17 00:00:00 2001 From: "nekomimi.pet" Date: Sun, 11 Oct 2026 16:11:59 -0400 Subject: [PATCH] valefar: qbittorrent exits via PIA Ontario and gets a forwarded port No US PIA region offers port forwarding, so the qbittorrent client moves to ca_ontario (pia-exit stays on Washington DC). A pia-portforward service in the guest asks PIA for a port (getSignature), keeps it bound (bindPort every 10 min), opens it in the guest firewall and writes it into qBittorrent's config, restarting qBittorrent only when the port changes. Toronto servers refuse the forwarding API despite the region flag, so the server was chosen by probing. --- hosts/valefar/pia-qbittorrent.nix | 86 +++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) 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 = [ -- 2.54.0