From adc87c2cc1edf74e4b18e9264464257b980ed57d Mon Sep 17 00:00:00 2001 From: jklein Date: Sun, 23 Aug 2026 11:15:43 +0200 Subject: [PATCH] Bind the qBittorrent session to the VPN interface qBittorrent had no network interface configured, so libtorrent listened and announced on every interface of the shared pod network namespace - including eth0 (the pod IP) and loopback. gluetun's kill switch drops exactly those packets, so trackers answered with "skipping tracker announce (unreachable)" or timed out, no peers were found and torrents stalled at 0 bytes in stalledDL. Beyond the stall this is a leak: without the binding qBittorrent also listens on the pod IP, i.e. reachable past the tunnel. The setting only exists in the persisted config file, which qBittorrent creates itself on first start, so an init container seeds it before the app comes up. The edit is idempotent and preserves existing user settings: it replaces the keys if present, inserts them into the [BitTorrent] section if missing, and creates file or section if needed. The interface is configurable via qbittorrent.networkInterface (default tun0); emptying it drops the init container. --- freesailarr/templates/freesailarr.sfs.yaml | 57 ++++++++++++++++++++++ freesailarr/values.yaml | 35 +++++++++++++ 2 files changed, 92 insertions(+) diff --git a/freesailarr/templates/freesailarr.sfs.yaml b/freesailarr/templates/freesailarr.sfs.yaml index 8297e23..f99095d 100644 --- a/freesailarr/templates/freesailarr.sfs.yaml +++ b/freesailarr/templates/freesailarr.sfs.yaml @@ -19,6 +19,63 @@ spec: automountServiceAccountToken: false securityContext: {{- toYaml .Values.securityContext.pod | nindent 8 }} + {{- if and .Values.qbittorrent.enabled .Values.qbittorrent.networkInterface }} + initContainers: + # qBittorrent has no environment variable for the session's network + # interface - it only reads it from the persisted config file, which + # the app itself creates on first start. Without the binding + # libtorrent listens and announces on every interface of the shared + # pod network namespace (eth0/pod IP and loopback included); gluetun's + # kill switch drops those packets, trackers stay unreachable and + # torrents stall at 0 bytes. Seeding the two keys before qbittorrent + # starts closes that leak and makes a fresh install work out of the + # box. The edit is idempotent and preserves all other settings: + # existing keys are replaced, missing ones are inserted into the + # [BitTorrent] section (qBittorrent 5.x reads them there), and a + # missing file or section is created. + - name: qbittorrent-config-interface + image: "{{ .Values.qbittorrent.configInit.image.repository }}:{{ .Values.qbittorrent.configInit.image.tag }}" + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - | + set -eu + conf=/config/qBittorrent/qBittorrent.conf + mkdir -p /config/qBittorrent + [ -f "$conf" ] || printf '[BitTorrent]\n' > "$conf" + # A file without a trailing newline would glue an appended + # section header onto the last key. + if [ -s "$conf" ] && [ -n "$(tail -c 1 "$conf")" ]; then + printf '\n' >> "$conf" + fi + grep -q '^\[BitTorrent\]$' "$conf" || printf '[BitTorrent]\n' >> "$conf" + # Drop any existing binding keys, then re-insert them directly + # after the [BitTorrent] header. + awk -v ifc="$QBT_INTERFACE" ' + /^Session\\Interface=/ { next } + /^Session\\InterfaceName=/ { next } + { print } + /^\[BitTorrent\]$/ { + print "Session\\Interface=" ifc + print "Session\\InterfaceName=" ifc + } + ' "$conf" > "${conf}.tmp" + mv "${conf}.tmp" "$conf" + echo "qBittorrent session bound to interface $QBT_INTERFACE" + env: + - name: QBT_INTERFACE + value: {{ .Values.qbittorrent.networkInterface | quote }} + resources: + {{- include "freesailarr.resources" .Values.qbittorrent.configInit.resources | nindent 12 }} + securityContext: + runAsUser: {{ .Values.puid }} + runAsGroup: {{ .Values.pgid }} + {{- toYaml .Values.qbittorrent.configInit.securityContext | nindent 12 }} + volumeMounts: + - mountPath: /config + name: qbittorrent-config + {{- end }} containers: # gluetun is the VPN gateway: all app containers share the pod's # network namespace, talk to each other over localhost and reach the diff --git a/freesailarr/values.yaml b/freesailarr/values.yaml index b012180..64aa8ab 100644 --- a/freesailarr/values.yaml +++ b/freesailarr/values.yaml @@ -161,6 +161,41 @@ qbittorrent: enabled: true storage: size: 1Gi + # Network interface qBittorrent binds its BitTorrent session to - gluetun's + # Wireguard device. Without this binding libtorrent listens and announces on + # every interface in the shared pod network namespace, including eth0 (the + # pod IP) and loopback. Those announces bypass the tunnel, so gluetun's kill + # switch (OUTPUT DROP except tun0 and the allowed cluster subnets) drops + # them: trackers report "skipping tracker announce (unreachable)", no peers + # are found and torrents stall at 0 bytes in stalledDL. Binding to tun0 is + # therefore both the functional fix and a leak fix - it stops qBittorrent + # from listening on the pod IP past the tunnel. The value is written into + # /config/qBittorrent/qBittorrent.conf by the config-interface init + # container below; setting it to null or "" removes that init container and + # leaves the binding to the user (not recommended). + networkInterface: tun0 + # The init container that seeds qbittorrent.networkInterface into the + # persisted qBittorrent config before the app starts. Runs as puid/pgid so + # the config file keeps the ownership the app expects. + configInit: + # Auxiliary busybox image. Pinned independently of appVersion (which + # tracks gluetun); reviewed alongside every app-version update round. + image: + repository: busybox + tag: "1.38.0" + resources: + limitFactor: 3 + requests: + cpu: 10m + memory: 16Mi + ephemeralStorage: 1Mi + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL env: [] livenessProbe: httpGet: