# Fornax distributed torrent workers — each is a gluetun+qBittorrent pod on a different VPN server # Worker tx253: US-TX#253 (endpoint 95.173.217.29), WireGuard key in vault secret/fornax/worker-tx253 # Worker tx34: US-TX#34 (endpoint 146.70.58.130), WireGuard key in vault secret/fornax/worker-tx34 # 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#253 ───────────────────────────────────────────────────────────── apiVersion: apps/v1 kind: Deployment metadata: name: fornax-worker-tx253 namespace: media labels: app: fornax-worker-tx253 fornax-role: worker fornax-server: us-tx-253 spec: replicas: 0 strategy: type: Recreate selector: matchLabels: app: fornax-worker-tx253 template: metadata: labels: app: fornax-worker-tx253 fornax-role: worker fornax-server: us-tx-253 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 valueFrom: secretKeyRef: name: fornax-worker-tx253-secrets key: PROTONVPN_PEER_PUBLIC_KEY - name: WIREGUARD_ENDPOINT_IP valueFrom: secretKeyRef: name: fornax-worker-tx253-secrets key: PROTONVPN_ENDPOINT_IP - name: WIREGUARD_ENDPOINT_PORT value: "51820" - name: WIREGUARD_ADDRESSES value: "10.2.0.2/32" - name: DOT value: "off" - name: DNS_UPSTREAM_RESOLVER_TYPE value: "plain" - name: DNS_UPSTREAM_PLAIN_ADDRESSES value: "10.43.0.10:53" - name: FIREWALL_OUTBOUND_SUBNETS value: "10.42.0.0/16,10.43.0.0/16" - name: MTU value: "1320" volumeMounts: - name: gluetun-data mountPath: /tmp/gluetun ports: - containerPort: 8888 resources: requests: memory: 128Mi cpu: 50m limits: memory: 512Mi cpu: 200m # Port forwarding helper — aggressively fixes gluetun routing rule, then watches for VPN port - name: portforward-helper image: alpine:latest securityContext: capabilities: add: ["NET_ADMIN"] command: - sh - -c - | # gluetun adds 'ip rule priority 100 from lookup 200' which routes ALL pod # traffic through eth0. iptables OUTPUT DROP then blocks non-VPN traffic, including # gluetun's own health check. Remove rule 100 aggressively from startup — BEFORE # gluetun can complete its first health check — so tun0 routing (rule 101) takes over. echo "Starting routing fix loop..." (while true; do ip rule del priority 100 2>/dev/null; sleep 1; done) & echo "Waiting for gluetun health endpoint..." until wget -q -T 3 -O- http://127.0.0.1:9999 > /dev/null 2>&1; do sleep 3; done echo "gluetun ready — VPN is up" # Install curl and python3 for NAT-PMP port forwarding until apk add -q curl python3 2>/dev/null; do sleep 5; done # NAT-PMP: request TCP port from ProtonVPN gateway (60s lease; renew every 45s) # Response is 16 bytes: BB H I H H I (last field is uint32 lifetime, not uint16) get_natpmp_port() { python3 -c "import socket,struct;s=socket.socket(2,2);s.settimeout(5);s.sendto(struct.pack('!BBHHHI',0,2,0,0,0,60),('10.2.0.1',5351));d=s.recvfrom(256)[0];r=struct.unpack('!BBHIHHI',d) if len(d)==16 else None;print(r[5] if r and r[2]==0 else 0)" 2>/dev/null || echo 0 } echo "Requesting port via NAT-PMP..." PORT=$(get_natpmp_port) if [ -n "$PORT" ] && [ "$PORT" != "0" ]; then echo "$(date): NAT-PMP port: $PORT" echo "$PORT" > /tmp/gluetun/forwarded_port else echo "$(date): NAT-PMP failed, will retry..." fi LAST_PORT="" while true; do PORT=$(get_natpmp_port) if [ -n "$PORT" ] && [ "$PORT" != "0" ] && [ "$PORT" != "$LAST_PORT" ]; then echo "$(date): Port: $PORT — syncing to qBittorrent" echo "$PORT" > /tmp/gluetun/forwarded_port 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\": 300}" >/dev/null 2>&1 || true fi LAST_PORT="$PORT" 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#34 ────────────────────────────────────────────────────────────── apiVersion: apps/v1 kind: Deployment metadata: name: fornax-worker-tx34 namespace: media labels: app: fornax-worker-tx34 fornax-role: worker fornax-server: us-tx-34 spec: replicas: 0 strategy: type: Recreate selector: matchLabels: app: fornax-worker-tx34 template: metadata: labels: app: fornax-worker-tx34 fornax-role: worker fornax-server: us-tx-34 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 valueFrom: secretKeyRef: name: fornax-worker-tx34-secrets key: PROTONVPN_PEER_PUBLIC_KEY - name: WIREGUARD_ENDPOINT_IP valueFrom: secretKeyRef: name: fornax-worker-tx34-secrets key: PROTONVPN_ENDPOINT_IP - name: WIREGUARD_ENDPOINT_PORT value: "51820" - name: WIREGUARD_ADDRESSES value: "10.2.0.2/32" - name: DOT value: "off" - name: DNS_UPSTREAM_RESOLVER_TYPE value: "plain" - name: DNS_UPSTREAM_PLAIN_ADDRESSES value: "10.43.0.10:53" - name: FIREWALL_OUTBOUND_SUBNETS value: "10.42.0.0/16,10.43.0.0/16" - name: MTU value: "1320" 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 "Starting routing fix loop..." (while true; do ip rule del priority 100 2>/dev/null; sleep 1; done) & echo "Waiting for gluetun health endpoint..." until wget -q -T 3 -O- http://127.0.0.1:9999 > /dev/null 2>&1; do sleep 3; done echo "gluetun ready — VPN is up" until apk add -q curl python3 2>/dev/null; do sleep 5; done # NAT-PMP: request TCP port from ProtonVPN gateway (60s lease; renew every 45s) # Response is 16 bytes: BB H I H H I (last field is uint32 lifetime, not uint16) get_natpmp_port() { python3 -c "import socket,struct;s=socket.socket(2,2);s.settimeout(5);s.sendto(struct.pack('!BBHHHI',0,2,0,0,0,60),('10.2.0.1',5351));d=s.recvfrom(256)[0];r=struct.unpack('!BBHIHHI',d) if len(d)==16 else None;print(r[5] if r and r[2]==0 else 0)" 2>/dev/null || echo 0 } echo "Requesting port via NAT-PMP..." PORT=$(get_natpmp_port) if [ -n "$PORT" ] && [ "$PORT" != "0" ]; then echo "$(date): NAT-PMP port: $PORT" echo "$PORT" > /tmp/gluetun/forwarded_port fi LAST_PORT="" while true; do PORT=$(get_natpmp_port) if [ -n "$PORT" ] && [ "$PORT" != "0" ] && [ "$PORT" != "$LAST_PORT" ]; then echo "$(date): Port: $PORT — syncing to qBittorrent" echo "$PORT" > /tmp/gluetun/forwarded_port 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\": 300}" >/dev/null 2>&1 || true fi LAST_PORT="$PORT" 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