homelab/manifests/multus/30-browser-vpn-proxy.yaml
Nik Afiq 2e64687bb9
Some checks failed
validate / lint (push) Failing after 0s
feat: switch browser-vpn-proxy to LoadBalancer on port 1080
Replace the NodePort (30889) with type: LoadBalancer on the standard
SOCKS5 port 1080, 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.

Documented the actual behavior after checking, not assuming: k3s's
ServiceLB (Klipper) binds the port on every node via a DaemonSet by
default - loadBalancerIP only sets the reported external IP, it
doesn't scope which nodes bind the port. That needs a separate
enablelb/lbpool node-label mechanism this repo doesn't use anywhere
yet (including on the existing gitea-ssh Service, which likely has the
same characteristic, unnoticed until now). So this change doesn't
narrow exposure to nik-debian only - it's still reachable at any node
IP on port 1080, the same multi-node reachability the NodePort version
already had, just a different port number.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 00:06:54 +09:00

216 lines
10 KiB
YAML

# 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 <any node IP>: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