# Fornax distributed torrent workers — each is a gluetun+qBittorrent pod on a different VPN server # Worker TX#179: US-TX#179, NAT-PMP via gluetun native ProtonVPN port forwarding # Worker TX#220: US-TX#220, NAT-PMP via gluetun native ProtonVPN port forwarding # Both workers share the media-data PVC; each has its own config PVC and VPN credentials # Port file shared via emptyDir: gluetun writes /tmp/gluetun/forwarded_port, helper reads it # ── Worker TX#179 ───────────────────────────────────────────────────────────── apiVersion: apps/v1 kind: Deployment metadata: name: fornax-worker-tx253 namespace: media labels: app: fornax-worker-tx253 fornax-role: worker fornax-server: us-tx-179 spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: fornax-worker-tx253 template: metadata: labels: app: fornax-worker-tx253 fornax-role: worker fornax-server: us-tx-179 spec: initContainers: - name: tun-setup image: busybox:latest command: ["sh", "-c", "mkdir -p /dev/net && [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 && chmod 666 /dev/net/tun && sysctl -w net.ipv6.conf.all.disable_ipv6=1 || true && ip route flush table 51820 2>/dev/null || true && ip rule del priority 101 2>/dev/null || true && ip rule del table 51820 2>/dev/null || true"] securityContext: privileged: true - name: qbt-config-patch image: python:3.13-alpine command: - python3 - -c - | import os, re CONF = '/config/qBittorrent/qBittorrent.conf' os.makedirs('/config/qBittorrent', exist_ok=True) text = open(CONF).read() if os.path.exists(CONF) else '' def set_key(text, section, key, value): """Set key=value under [section], inserting if missing.""" key_pat = re.compile(r'^' + re.escape(key) + r'=.*', re.M) if key_pat.search(text): return key_pat.sub(lambda m: key + '=' + value, text) sec_pat = re.compile(r'^\[' + re.escape(section) + r'\]', re.M) if sec_pat.search(text): return sec_pat.sub(lambda m: '[' + section + ']\n' + key + '=' + value, text) return text + f'\n[{section}]\n{key}={value}\n' HASH = '@ByteArray(HqYj1eGsdXlQ4CSy597Y9A==:J9hsJIlU5FYfHb5rY5qQoIpVpTijryS/H+CE07oMtplL/ytneBVFd2tfVJtqGjhdht8tEi4wmqSSlqTgEu444w==)' # Preferences text = set_key(text, 'Preferences', r'WebUI\Password_PBKDF2', f'"{HASH}"') text = set_key(text, 'Preferences', r'WebUI\LocalhostAuthEnabled', 'false') # BitTorrent performance defaults (only applied when key absent — API updates persist) bt_defaults = { r'Session\MaxActiveDownloads': '50', r'Session\MaxActiveTorrents': '100', r'Session\MaxActiveUploads': '20', r'Session\MaxConnections': '3000', r'Session\MaxConnectionsPerTorrent': '300', r'Session\MaxUploads': '-1', r'Session\MaxUploadsPerTorrent': '10', r'Session\GlobalDLSpeedLimit': '0', r'Session\GlobalUPSpeedLimit': '0', r'Session\DHTEnabled': 'true', } for key, val in bt_defaults.items(): text = set_key(text, 'BitTorrent', key, val) open(CONF, 'w').write(text) print('qBittorrent config patched.') volumeMounts: - name: config mountPath: /config containers: - name: gluetun image: ghcr.io/qdm12/gluetun:latest securityContext: capabilities: add: ["NET_ADMIN"] env: - name: VPN_SERVICE_PROVIDER value: "custom" - name: VPN_TYPE value: "wireguard" - name: WIREGUARD_PRIVATE_KEY valueFrom: secretKeyRef: name: fornax-worker-tx253-secrets key: PROTONVPN_PRIVATE_KEY - name: WIREGUARD_PUBLIC_KEY value: "mngiSxBpH7GU24nnWdBEcnhDnCPn2jq5+ZP3zwPwISA=" - name: WIREGUARD_ADDRESSES value: "10.2.0.2/32" - name: WIREGUARD_ENDPOINT_IP value: "95.173.217.29" - name: WIREGUARD_ENDPOINT_PORT value: "51820" - name: DOT value: "off" - name: DNS_UPSTREAM_RESOLVER_TYPE value: "plain" - name: DNS_UPSTREAM_PLAIN_ADDRESSES value: "10.43.0.10:53" - name: HEALTH_TARGET_ADDRESS value: "api.protonvpn.ch:443,account.proton.me:443" - name: HEALTH_ICMP_TARGET_IPS value: "10.2.0.1" - name: FIREWALL_OUTBOUND_SUBNETS value: "10.42.0.0/16,10.43.0.0/16" volumeMounts: - name: gluetun-data mountPath: /tmp/gluetun ports: - containerPort: 8888 resources: requests: memory: 128Mi cpu: 50m limits: memory: 512Mi cpu: 200m # Port forwarding helper — reads port from file gluetun writes to /tmp/gluetun/forwarded_port - name: portforward-helper image: alpine:latest command: - sh - -c - | until apk add -q curl 2>/dev/null; do sleep 5; done echo "Watching /tmp/gluetun/forwarded_port for assigned port..." while true; do if [ -f /tmp/gluetun/forwarded_port ]; then PORT=$(cat /tmp/gluetun/forwarded_port) if [ -n "$PORT" ] && [ "$PORT" != "0" ]; then echo "$(date): Forwarded port: $PORT — updating qBittorrent" curl -s -c /tmp/qbt.txt -X POST http://localhost:8080/api/v2/auth/login \ -d "username=admin&password=adminadmin" >/dev/null 2>&1 curl -s -b /tmp/qbt.txt -X POST http://localhost:8080/api/v2/app/setPreferences \ -d "json={\"listen_port\":$PORT,\"random_port\":false}" >/dev/null 2>&1 echo "$(date): qBittorrent listen port set to $PORT" else echo "$(date): Port file empty, waiting..." fi else echo "$(date): Waiting for gluetun to write port file..." fi sleep 45 done volumeMounts: - name: gluetun-data mountPath: /tmp/gluetun resources: requests: memory: 32Mi cpu: 10m limits: memory: 96Mi cpu: 50m - name: qbittorrent image: lscr.io/linuxserver/qbittorrent:latest env: - name: PUID value: "1000" - name: PGID value: "1000" - name: TZ value: "America/Chicago" - name: WEBUI_PORT value: "8080" ports: - containerPort: 8080 volumeMounts: - name: config mountPath: /config - name: media mountPath: /media resources: requests: memory: 256Mi cpu: 100m limits: memory: 1Gi cpu: 500m volumes: - name: gluetun-data emptyDir: {} - name: config persistentVolumeClaim: claimName: fornax-worker-tx253-config - name: media persistentVolumeClaim: claimName: media-data --- apiVersion: v1 kind: Service metadata: name: fornax-worker-tx253 namespace: media labels: app: fornax-worker-tx253 fornax-role: worker spec: selector: app: fornax-worker-tx253 ports: - name: webui port: 8080 targetPort: 8080 type: ClusterIP --- # ── Worker TX#220 ───────────────────────────────────────────────────────────── apiVersion: apps/v1 kind: Deployment metadata: name: fornax-worker-tx34 namespace: media labels: app: fornax-worker-tx34 fornax-role: worker fornax-server: us-tx-220 spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: fornax-worker-tx34 template: metadata: labels: app: fornax-worker-tx34 fornax-role: worker fornax-server: us-tx-220 spec: initContainers: - name: tun-setup image: busybox:latest command: ["sh", "-c", "mkdir -p /dev/net && [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 && chmod 666 /dev/net/tun && sysctl -w net.ipv6.conf.all.disable_ipv6=1 || true && ip route flush table 51820 2>/dev/null || true && ip rule del priority 101 2>/dev/null || true && ip rule del table 51820 2>/dev/null || true"] securityContext: privileged: true - name: qbt-config-patch image: python:3.13-alpine command: - python3 - -c - | import os, re CONF = '/config/qBittorrent/qBittorrent.conf' os.makedirs('/config/qBittorrent', exist_ok=True) text = open(CONF).read() if os.path.exists(CONF) else '' def set_key(text, section, key, value): """Set key=value under [section], inserting if missing.""" key_pat = re.compile(r'^' + re.escape(key) + r'=.*', re.M) if key_pat.search(text): return key_pat.sub(lambda m: key + '=' + value, text) sec_pat = re.compile(r'^\[' + re.escape(section) + r'\]', re.M) if sec_pat.search(text): return sec_pat.sub(lambda m: '[' + section + ']\n' + key + '=' + value, text) return text + f'\n[{section}]\n{key}={value}\n' HASH = '@ByteArray(HqYj1eGsdXlQ4CSy597Y9A==:J9hsJIlU5FYfHb5rY5qQoIpVpTijryS/H+CE07oMtplL/ytneBVFd2tfVJtqGjhdht8tEi4wmqSSlqTgEu444w==)' # Preferences text = set_key(text, 'Preferences', r'WebUI\Password_PBKDF2', f'"{HASH}"') text = set_key(text, 'Preferences', r'WebUI\LocalhostAuthEnabled', 'false') # BitTorrent performance defaults (only applied when key absent — API updates persist) bt_defaults = { r'Session\MaxActiveDownloads': '50', r'Session\MaxActiveTorrents': '100', r'Session\MaxActiveUploads': '20', r'Session\MaxConnections': '3000', r'Session\MaxConnectionsPerTorrent': '300', r'Session\MaxUploads': '-1', r'Session\MaxUploadsPerTorrent': '10', r'Session\GlobalDLSpeedLimit': '0', r'Session\GlobalUPSpeedLimit': '0', r'Session\DHTEnabled': 'true', } for key, val in bt_defaults.items(): text = set_key(text, 'BitTorrent', key, val) open(CONF, 'w').write(text) print('qBittorrent config patched.') volumeMounts: - name: config mountPath: /config containers: - name: gluetun image: ghcr.io/qdm12/gluetun:latest securityContext: capabilities: add: ["NET_ADMIN"] env: - name: VPN_SERVICE_PROVIDER value: "custom" - name: VPN_TYPE value: "wireguard" - name: WIREGUARD_PRIVATE_KEY valueFrom: secretKeyRef: name: fornax-worker-tx34-secrets key: PROTONVPN_PRIVATE_KEY - name: WIREGUARD_PUBLIC_KEY value: "wqJcz4akzVFxx35aJ5B7G/IJ9qsRvpcGNub3rLHcqXo=" - name: WIREGUARD_ADDRESSES value: "10.2.0.2/32" - name: WIREGUARD_ENDPOINT_IP value: "146.70.58.130" - name: WIREGUARD_ENDPOINT_PORT value: "51820" - name: DOT value: "off" - name: DNS_UPSTREAM_RESOLVER_TYPE value: "plain" - name: DNS_UPSTREAM_PLAIN_ADDRESSES value: "10.43.0.10:53" - name: HEALTH_TARGET_ADDRESS value: "api.protonvpn.ch:443,account.proton.me:443" - name: HEALTH_ICMP_TARGET_IPS value: "10.2.0.1" - name: FIREWALL_OUTBOUND_SUBNETS value: "10.42.0.0/16,10.43.0.0/16" volumeMounts: - name: gluetun-data mountPath: /tmp/gluetun ports: - containerPort: 8888 resources: requests: memory: 128Mi cpu: 50m limits: memory: 512Mi cpu: 200m - name: portforward-helper image: alpine:latest command: - sh - -c - | until apk add -q curl 2>/dev/null; do sleep 5; done echo "Watching /tmp/gluetun/forwarded_port for assigned port..." while true; do if [ -f /tmp/gluetun/forwarded_port ]; then PORT=$(cat /tmp/gluetun/forwarded_port) if [ -n "$PORT" ] && [ "$PORT" != "0" ]; then echo "$(date): Forwarded port: $PORT — updating qBittorrent" curl -s -c /tmp/qbt.txt -X POST http://localhost:8080/api/v2/auth/login \ -d "username=admin&password=adminadmin" >/dev/null 2>&1 curl -s -b /tmp/qbt.txt -X POST http://localhost:8080/api/v2/app/setPreferences \ -d "json={\"listen_port\":$PORT,\"random_port\":false}" >/dev/null 2>&1 echo "$(date): qBittorrent listen port set to $PORT" else echo "$(date): Port file empty, waiting..." fi else echo "$(date): Waiting for gluetun to write port file..." fi sleep 45 done volumeMounts: - name: gluetun-data mountPath: /tmp/gluetun resources: requests: memory: 32Mi cpu: 10m limits: memory: 96Mi cpu: 50m - name: qbittorrent image: lscr.io/linuxserver/qbittorrent:latest env: - name: PUID value: "1000" - name: PGID value: "1000" - name: TZ value: "America/Chicago" - name: WEBUI_PORT value: "8080" ports: - containerPort: 8080 volumeMounts: - name: config mountPath: /config - name: media mountPath: /media resources: requests: memory: 256Mi cpu: 100m limits: memory: 1Gi cpu: 500m volumes: - name: gluetun-data emptyDir: {} - name: config persistentVolumeClaim: claimName: fornax-worker-tx34-config - name: media persistentVolumeClaim: claimName: media-data --- apiVersion: v1 kind: Service metadata: name: fornax-worker-tx34 namespace: media labels: app: fornax-worker-tx34 fornax-role: worker spec: selector: app: fornax-worker-tx34 ports: - name: webui port: 8080 targetPort: 8080 type: ClusterIP