Files
infrastructure/servers/legion/k8s/media/fornax-workers.yaml
T
Will Anderson ef8dc293e5 Set HEALTH_ICMP_TARGET_IPS to WireGuard gateway for periodic health check
ProtonVPN does not NAT ICMP to external IPs (1.1.1.1, 8.8.8.8), causing
the periodic small health check to always fail and restart the VPN loop.
10.2.0.1 (WireGuard gateway) responds to ICMP at ~28ms and remains
reachable as long as the WireGuard tunnel itself is up.
2026-04-14 22:08:40 -05:00

444 lines
16 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: "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