91d0f4a130
US-TX#253 and US-TX#34 not in gluetun embedded server list — gluetun crashes on startup. Reverted to valid names while investigating correct server mapping. Need to find server public keys for Vault endpoint IPs.
497 lines
19 KiB
YAML
497 lines
19 KiB
YAML
# 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: 1
|
|
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: "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#179"
|
|
- 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: 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 <pod-IP> 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: single-line Python to request TCP port mapping from ProtonVPN gateway
|
|
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,7776000),('10.2.0.1',5351));d=s.recvfrom(256)[0];r=struct.unpack('!BBHIHHH',d[:16]) 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 240
|
|
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: 1
|
|
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: "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#220"
|
|
- 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: 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: single-line Python to request TCP port mapping from ProtonVPN gateway
|
|
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,7776000),('10.2.0.1',5351));d=s.recvfrom(256)[0];r=struct.unpack('!BBHIHHH',d[:16]) 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 240
|
|
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
|