From 756fa120b312d492e252fedd6c69625561aa9950 Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Mon, 24 Aug 2026 22:45:08 +0900 Subject: [PATCH] feat: update JDownloader deployment for VLAN 50 migration with enhanced network configuration and egress guard --- manifests/media/jdownloader.yaml | 138 ++++++++++++++++++------------- 1 file changed, 82 insertions(+), 56 deletions(-) diff --git a/manifests/media/jdownloader.yaml b/manifests/media/jdownloader.yaml index 6528208..8bb1b62 100644 --- a/manifests/media/jdownloader.yaml +++ b/manifests/media/jdownloader.yaml @@ -1,6 +1,25 @@ # Apply: kubectl apply -f manifests/media/jdownloader.yaml # Delete: kubectl delete -f manifests/media/jdownloader.yaml # Description: JDownloader deployment with Ingress at jdownloader.home.arpa. +# +# *** VLAN 50 MIGRATION — DO NOT PUSH TO main WITHOUT SEPARATE, EXPLICIT +# APPROVAL — AND ONLY AFTER qBittorrent (manifests/media/ +# qbittorrent.yaml) HAS BEEN LIVE, VALIDATED, AND SOAKED. plan.md is +# explicit: migrate qBittorrent first, soak, then JDownloader, +# independently soaked — do not batch them. *** +# Same VLAN 50/Multus design as qbittorrent.yaml (see that file's header +# for the full precondition list — pia-gateway, vlan50-parent, and +# manifests/multus/ all applied and verified first). One difference: +# JDownloader has no reliably-persistent, file-editable interface-bind +# setting the way qBittorrent's qBittorrent.conf does (its own +# preferences aren't a simple INI this repo can safely patch), so this +# migration relies on the network-namespace egress guard alone for +# enforcement, exactly as plan.md anticipates for JDownloader +# specifically ("application-level interface binding may be weaker or +# unavailable... the namespace egress guard is mandatory"). +# `media` has selfHeal+automated sync already configured — pushing this +# file deploys it immediately. Rollback copy, not synced by any +# Application: manifests/media/rollback/jdownloader-gluetun.yaml. apiVersion: apps/v1 kind: Deployment metadata: @@ -17,66 +36,65 @@ spec: metadata: labels: app: jdownloader + annotations: + k8s.v1.cni.cncf.io/networks: | + [{"name": "vlan50", "namespace": "downloads", "interface": "net1", "ips": ["10.10.50.11/24"]}] spec: + # Hard-pinned to nik-debian, not just node-role: storage — see + # qbittorrent.yaml for why. nodeSelector: node-role: storage - containers: - - name: gluetun - image: qmcgaw/gluetun:v3.41 + kubernetes.io/hostname: nik-debian + # No cluster DNS/CoreDNS resolution needed or provided here — see + # qbittorrent.yaml for the full reasoning (identical here: this pod + # never looks up an in-cluster service by name). + dnsPolicy: None + dnsConfig: + nameservers: + - "10.10.40.53" + initContainers: + # NET_ADMIN lives here ONLY. No qBittorrent-style config-bind + # sibling init container — see this file's header for why + # JDownloader relies on the egress guard alone. + - name: vlan50-egress-guard + image: nicolaka/netshoot:v0.11 + command: ["/bin/sh", "/scripts/guard.sh"] + env: + - name: VLAN50_GATEWAY + value: "10.10.50.1" + - name: TECHNITIUM_IP + value: "10.10.40.53" + - name: POD_CIDR + value: "10.42.0.0/16" + - name: SERVICE_CIDR + value: "10.43.0.0/16" + - name: NODE_IP + value: "10.10.40.20" + - name: EXPECTED_VLAN50_IP + value: "10.10.50.11" securityContext: capabilities: - add: - - NET_ADMIN - env: - - name: VPN_SERVICE_PROVIDER - value: private internet access - - name: VPN_TYPE - value: openvpn - - name: SERVER_REGIONS - value: JP Tokyo,Taiwan,Hong Kong - # value: US Seattle,US Oregon,US Silicon Valley - - name: OPENVPN_USER - valueFrom: - secretKeyRef: - name: pia-credentials - key: OPENVPN_USER - - name: OPENVPN_PASSWORD - valueFrom: - secretKeyRef: - name: pia-credentials - key: OPENVPN_PASSWORD - - name: FIREWALL_OUTBOUND_SUBNETS - value: "10.42.0.0/16,10.43.0.0/16,10.10.40.0/24" - - name: BLOCK_IPV6 - value: "on" - startupProbe: - exec: - command: - - /gluetun-entrypoint - - healthcheck - periodSeconds: 10 - failureThreshold: 60 - livenessProbe: - exec: - command: - - /gluetun-entrypoint - - healthcheck - periodSeconds: 30 - failureThreshold: 3 - readinessProbe: - exec: - command: - - /gluetun-entrypoint - - healthcheck - periodSeconds: 10 - failureThreshold: 3 + drop: ["ALL"] + # NET_RAW alongside NET_ADMIN: the guard script's + # arping-based duplicate-address and gateway-reachability + # checks need it — see + # vlan50-egress-guard-script.yaml's header comment. + add: ["NET_ADMIN", "NET_RAW"] volumeMounts: - - name: tun - mountPath: /dev/net/tun + - name: guard-script + mountPath: /scripts + containers: - name: jdownloader image: jlesage/jdownloader-2:latest ports: - containerPort: 5800 + # No securityContext capability restriction here, deliberately + # — jlesage/jdownloader-2 uses the same docker-baseimage-gui + # PUID/GID pattern as linuxserver's images (USER_ID/GROUP_ID + # below): starts as root, its own init chowns /config to the + # requested uid/gid and drops privileges from there. See + # qbittorrent.yaml's app container for the fuller version of + # this reasoning — same conclusion, same kind of image. env: - name: USER_ID value: "1000" @@ -112,6 +130,12 @@ spec: image: python:3.12-alpine ports: - containerPort: 9667 + # Unlike the jdownloader container above, this is a plain + # Python base image with no PUID/GID privilege-drop machinery + # to preserve — dropping all capabilities here is safe. + securityContext: + capabilities: + drop: ["ALL"] resources: requests: cpu: 5m @@ -149,8 +173,10 @@ spec: # .torrent files aren't handled by JDownloader's crawler (no BT # plugin), so fetch the raw file ourselves instead of writing a - # crawljob. This container shares gluetun's netns, so the fetch - # is still VPN-routed same as JDownloader's own downloads. + # crawljob. This container shares the pod's network namespace + # (and its vlan50-egress-guard init container's routes/rules), + # so the fetch is still VPN-routed same as JDownloader's own + # downloads. if basename.lower().endswith('.torrent'): safe_name = basename.replace('/', '_').replace('\\', '_') try: @@ -183,10 +209,6 @@ spec: http.server.HTTPServer(('0.0.0.0', 9667), Handler).serve_forever() volumes: - - name: tun - hostPath: - path: /dev/net/tun - type: CharDevice - name: config hostPath: path: /data/jdownloader @@ -195,6 +217,10 @@ spec: hostPath: path: /mnt/storage/dl type: Directory + - name: guard-script + configMap: + name: vlan50-egress-guard-script + defaultMode: 365 # octal 0555, r-xr-xr-x --- apiVersion: v1 kind: Service