# sync-wave 1 — same wave as the NAD (10-nad-vlan50.yaml), the only # thing this actually needs to exist first. NOT wave 3/after-the-canary # as originally drafted: that was a documentation convention, not a real # dependency, and it turned out to actively break syncing. Root-caused # live (2026-08-24): this cluster's argocd-cm sets # resource.customizations.ignoreResourceUpdates.all to ignore /status # changes cluster-wide (a deliberate, existing setting, not something to # change just for this file) — Argo's wave orchestrator waits for each # wave's resources to report Healthy before starting the next wave, but # a Pod that's deleted and recreated (exactly what the canary is, every # test round) needs Argo to observe a fresh Pending -> Running /status # transition *during that sync*, which the ignore-updates config # prevents it from ever seeing — so the operation hangs forever on # "waiting for healthy state of /Pod/vlan50-canary" even once the Pod is # actually Running. Already-stable resources (the DaemonSet, RBAC, NAD) # never hit this because their cached health from a prior sync is still # valid when nothing about them changed. Moved here from # manifests/multus/reserved/ (2026-08-24) now that qBittorrent and # JDownloader have both independently passed their canary, validation, # and soak gates — the reservation this file's earlier header described # is satisfied. Still governed only by the `multus` Argo CD Application's # own manual-sync gate (no automated sync there at all) — the same gate # already used for the DaemonSet, NAD, and canary, not a new mechanism. # # Before applying: # 1. Verify port 1080 below doesn't collide with anything live — # confirmed free 2026-08-24 (`kubectl get svc -A`, checked against # every live LoadBalancer Service's ports: gitea-ssh uses 2222, # traefik uses 80/443). # # Design: # - serjs/go-socks5-proxy (github.com/serjs/socks5-server) — a small, # single-purpose Go SOCKS5 server, not a VPN client itself. It # doesn't need to be, here: unlike the legacy manifests/media/ # gluetun-proxy.yaml it's replacing (which runs its own OpenVPN # client), PIA egress for this workload comes from the same # netns-level routing as qBittorrent/JDownloader — the init # container below sends this pod's default route out net1/PIA, the # SOCKS5 server itself just needs to be a plain, correctly-behaving # proxy. Pinned to v0.0.4's digest, confirmed live against Docker # Hub's registry API 2026-08-24: # sha256:6828ddb2a6a93dec85209b69cd3842bd80094f7dd52f35ee1dd0f9e9165e2188 # - No auth (REQUIRE_AUTH=false), no ALLOWED_IPS — a deliberate choice, # not an oversight. plan.md originally called for auth AND # LAN-restriction together as a floor; discussed and explicitly # relaxed (2026-08-24) after confirming the home network's own # firewall already restricts this Service's port to Trusted # (VLAN 10) and Lab (VLAN 40) only — Trusted/Lab → Lab is ALLOW, # IoT/Guest/Management → Lab is DENY except the Technitium DNS # exception (see ~/repo/home-network/Home Network Implementation # Plan.md section 10). Residual risk accepted knowingly: Lab has its # own Wi-Fi SSID with open DHCP, so "reachable" means "anyone with # that Wi-Fi password or a device already on Trusted/Lab," not just # deliberately provisioned devices — there is no proxy-level control # backing this up. # - ALLOWED_IPS is not a fallback for the above — it's removed because # it cannot do what it looks like it does. Read this image's actual # source (server.go, and the go-socks5 fork it replaces via go.mod's # replace directive) to confirm, not assumed: ALLOWED_IPS values are # parsed with net.ParseIP (not net.ParseCIDR) and matched with exact # net.IP.Equal(), never CIDR containment. The original draft's value # here, "10.10.40.0/24", would have failed to parse as a plain IP, # producing a whitelist containing one nil entry — since Equal() # against nil never matches any real client address, every single # connection would have been rejected, including legitimate ones. # This library has no way to express a subnet allowlist at all, only # a list of exact individual IPs, which doesn't fit a dynamic-DHCP # LAN. Access restriction is the OpenWrt firewall alone; there is no # compensating netns-level (nft) inbound filter here either — one # could be added later, but would first need confirming whether this # Service actually preserves real client source IPs end-to-end # (default `externalTrafficPolicy: Cluster` typically does not — # this Service doesn't set `externalTrafficPolicy: Local` either, # unlike values/traefik.yaml's LoadBalancer, which does specifically # for this reason) rather than assuming it does. # - Service is LoadBalancer (k3s's built-in ServiceLB/Klipper), port # 1080 directly — not NodePort, at the user's request for a more # natural port than a 30000+ NodePort value. `loadBalancerIP: # 10.10.40.20` (nik-debian) matches values/gitea.yaml's gitea-ssh # Service pattern exactly (`loadBalancerIP: 10.10.40.53`, minisforum) # — confirmed by reading how Klipper actually works before adding # this, not assumed: `loadBalancerIP` only sets the *reported* # external IP (`.status.loadBalancer.ingress[].ip`); the underlying # svclb DaemonSet still binds port 1080 on every node by default, # same as gitea-ssh's `loadBalancerIP` pin does today. Actually # scoping it to nik-debian only needs k3s's separate # `svccontroller.k3s.cattle.io/enablelb`/`lbpool` node-label # mechanism, which nothing in this repo uses yet — not added here, # so this is reachable at :1080, functionally identical # multi-node reachability to the NodePort version it replaced, just # a different (and more memorable) port number. Never exposed # publicly either way (no port-forward on Flint/the router for it, # same as the now-decommissioned gluetun-proxy was). # # Browser configuration — this is the part that actually matters for # "DNS must be resolved through the proxy": configuring a SOCKS5 proxy # address alone is NOT enough. By default most browsers resolve # hostnames locally (via the LAN's normal DNS) and only send the already- # resolved IP through the proxy — that leaks every site you visit to # your normal LAN resolver and bypasses Technitium/PIA for DNS entirely, # defeating the point. "socks5h" (vs plain "socks5") in a proxy URL is # the conventional way tools signal "resolve hostnames on the proxy side, # not locally" (e.g. curl --socks5-hostname, or a proxy URL scheme of # socks5h://). In Firefox specifically: Settings -> Network Settings -> # Manual proxy configuration -> SOCKS Host set to this Service -> SOCKS # v5 selected -> the checkbox "Proxy DNS when using SOCKS v5" MUST be # checked, or Firefox resolves locally despite the proxy being SOCKS5. # Verify this actually holds (a DNS leak test site, or packet capture # showing no DNS queries leaving the browser's own host) before trusting # it — do not assume the checkbox alone is sufficient without checking. apiVersion: apps/v1 kind: Deployment metadata: name: browser-vpn-proxy namespace: downloads annotations: argocd.argoproj.io/sync-wave: "1" spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: browser-vpn-proxy template: metadata: labels: app: browser-vpn-proxy annotations: k8s.v1.cni.cncf.io/networks: | [{"name": "vlan50", "namespace": "downloads", "interface": "net1", "ips": ["10.10.50.12/24"]}] spec: nodeSelector: node-role: storage kubernetes.io/hostname: nik-debian dnsPolicy: None dnsConfig: nameservers: - "10.10.40.53" initContainers: - 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.12" securityContext: capabilities: 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: guard-script mountPath: /scripts containers: - name: socks5 image: "serjs/go-socks5-proxy@sha256:6828ddb2a6a93dec85209b69cd3842bd80094f7dd52f35ee1dd0f9e9165e2188" # v0.0.4 ports: - containerPort: 1080 securityContext: capabilities: drop: ["ALL"] env: # REQUIRE_AUTH defaults to true in this image (confirmed # against its real source, params struct in server.go) — it # must be explicitly set to "false" to actually go # passwordless, omitting it entirely would NOT have that # effect. Deliberately no auth, no ALLOWED_IPS — see the # header comment for the reasoning and what actually # restricts access instead. - name: REQUIRE_AUTH value: "false" resources: requests: cpu: 20m memory: 32Mi limits: cpu: 200m memory: 128Mi volumes: - name: guard-script configMap: name: vlan50-egress-guard-script defaultMode: 365 # octal 0555, r-xr-xr-x --- apiVersion: v1 kind: Service metadata: name: browser-vpn-proxy namespace: downloads spec: selector: app: browser-vpn-proxy type: LoadBalancer # Pinned to nik-debian's own address, matching values/gitea.yaml's # gitea-ssh Service (loadBalancerIP: 10.10.40.53, minisforum) — see the # header comment for what this does and does not actually restrict. loadBalancerIP: "10.10.40.20" ports: - name: socks5 port: 1080 targetPort: 1080