# 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 — fixes gluetun routing rule then watches for VPN port assignment - name: portforward-helper image: alpine:latest securityContext: capabilities: add: ["NET_ADMIN"] command: - sh - -c - | # Wait for gluetun to finish setting up its routing rules echo "Waiting for gluetun health endpoint..." while ! wget -q -T 3 -O- http://127.0.0.1:9999 > /dev/null 2>&1; do sleep 3; done echo "gluetun ready" # gluetun adds 'ip rule priority 100 from lookup 200' which routes # ALL pod traffic through eth0, bypassing tun0. The iptables OUTPUT DROP policy # then blocks non-VPN, non-cluster traffic. Removing rule 100 allows traffic to # fall through to 'priority 101 not fwmark 0xca6c lookup 51820' (tun0/VPN). ip rule del priority 100 2>/dev/null && echo "Fixed VPN routing (removed rule 100)" || echo "Rule 100 not present" # Install curl (succeeds now that VPN traffic routes correctly) until apk add -q curl 2>/dev/null; do sleep 5; done echo "Watching /tmp/gluetun/forwarded_port for assigned port..." TTL=300 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" if [ -n "$COORDINATOR_URL" ] && [ -n "$WORKER_ID" ]; then curl -s -X POST "$COORDINATOR_URL/api/v2/workers/$WORKER_ID/port-lease" \ -H "Content-Type: application/json" \ -d "{\"port\": $PORT, \"ttlSeconds\": $TTL}" >/dev/null 2>&1 || true fi fi fi sleep 45 done env: - name: COORDINATOR_URL value: "https://fornax.neuralplatform.ai" - name: WORKER_ID value: "tx253" 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 securityContext: capabilities: add: ["NET_ADMIN"] command: - sh - -c - | echo "Waiting for gluetun health endpoint..." while ! wget -q -T 3 -O- http://127.0.0.1:9999 > /dev/null 2>&1; do sleep 3; done echo "gluetun ready" ip rule del priority 100 2>/dev/null && echo "Fixed VPN routing (removed rule 100)" || echo "Rule 100 not present" until apk add -q curl 2>/dev/null; do sleep 5; done echo "Watching /tmp/gluetun/forwarded_port for assigned port..." TTL=300 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" if [ -n "$COORDINATOR_URL" ] && [ -n "$WORKER_ID" ]; then curl -s -X POST "$COORDINATOR_URL/api/v2/workers/$WORKER_ID/port-lease" \ -H "Content-Type: application/json" \ -d "{\"port\": $PORT, \"ttlSeconds\": $TTL}" >/dev/null 2>&1 || true fi fi fi sleep 45 done env: - name: COORDINATOR_URL value: "https://fornax.neuralplatform.ai" - name: WORKER_ID value: "tx34" 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