942b398a53
Custom provider hardcodes 10.2.0.2/32 but ProtonVPN assigns a different IP per session via their API. WireGuard handshake succeeds but ProtonVPN doesn't route traffic for the wrong IP. Native protonvpn provider fetches the correct IP assignment automatically and enables port forwarding.
469 lines
17 KiB
YAML
469 lines
17 KiB
YAML
# 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: "protonvpn"
|
|
- name: VPN_TYPE
|
|
value: "wireguard"
|
|
- name: WIREGUARD_PRIVATE_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fornax-worker-tx253-secrets
|
|
key: PROTONVPN_PRIVATE_KEY
|
|
- name: SERVER_NAMES
|
|
value: "US-TX#253"
|
|
- name: VPN_PORT_FORWARDING
|
|
value: "on"
|
|
- name: DOT
|
|
value: "off"
|
|
- name: DNS_UPSTREAM_RESOLVER_TYPE
|
|
value: "plain"
|
|
- name: DNS_UPSTREAM_PLAIN_ADDRESSES
|
|
value: "10.43.0.10:53"
|
|
- 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 <pod-IP> 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: "protonvpn"
|
|
- name: VPN_TYPE
|
|
value: "wireguard"
|
|
- name: WIREGUARD_PRIVATE_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fornax-worker-tx34-secrets
|
|
key: PROTONVPN_PRIVATE_KEY
|
|
- name: SERVER_NAMES
|
|
value: "US-TX#34"
|
|
- name: VPN_PORT_FORWARDING
|
|
value: "on"
|
|
- name: DOT
|
|
value: "off"
|
|
- name: DNS_UPSTREAM_RESOLVER_TYPE
|
|
value: "plain"
|
|
- name: DNS_UPSTREAM_PLAIN_ADDRESSES
|
|
value: "10.43.0.10:53"
|
|
- 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
|