docs(vpn-ingress): withdraw the L2-lease diagnosis — it was falsified by measurement #69
Closed
gitea-admin
wants to merge 1 commits from
fix/vpn-ingress-l2-lease-collision into main
pull from: fix/vpn-ingress-l2-lease-collision
merge into: Home-Network:main
Home-Network:main
Home-Network:feat/pihole-explicit-lan-names
Home-Network:feat/dns-via-cloudflare-tunnel
Home-Network:feature/cloudflare-tunnel-config
Home-Network:chore/rotate-cloudflared-tunnel
Home-Network:fix/cloudflared-creds-permission
Home-Network:feat/cloudflare-tunnel
Home-Network:feat/parallel-l0-checks
Home-Network:feat/gate-toolbox-container
Home-Network:fix/keycloak-servicemonitor-owner
Home-Network:feat/runner-capacity-2
Home-Network:chore/renovate-config-cleanup
Home-Network:fix/keycloak-operator-crds
Home-Network:feat/devcontainer-velero-cli
Home-Network:feat/gitea-28
Home-Network:feat/l0-externaldns-annotation-prefix
Home-Network:fix/public-dns-wan-a-record
Home-Network:feat/pm-agent-pr-mentions
Home-Network:docs/source-comment-audit
Home-Network:chore/pm-agent-digest-99
Home-Network:fix/pm-agent-triage-exit
Home-Network:chore/pm-agent-go-live
Home-Network:chore/pm-agent-runner-digest
Home-Network:feature/pm-agent-credentials
Home-Network:feat/pm-agent-registry-pull-secret
Home-Network:feat/pin-pihole-jackett-plex
Home-Network:fix/renovate-hold-auth-and-self
Home-Network:feat/renovate-track-keycloak-operator
Home-Network:fix/renovate-run-hygiene
Home-Network:fix/renovate-auth
Home-Network:fix/renovate-first-run
Home-Network:feat/keycloak-passkey-reconcile
Home-Network:fix/gitea-admit-buildkit
Home-Network:feat/image-pipeline
Home-Network:fix/renovate-config-filename
Home-Network:fix/renovate-cron-window
Home-Network:fix/renovate-invalid-config
Home-Network:fix/pihole-local-ttl
Home-Network:fix/unattended-upgrades-debian-sentinel
Home-Network:fix/ha-check-catastrophic-regex
Home-Network:feat/automated-upgrades
Home-Network:fix/coredns-ha
Home-Network:feat/keycloak-passkeys
Home-Network:fix/oauth2-proxy-oidc-startup-race
Home-Network:feat/graceful-cluster-shutdown
Home-Network:plex/volume-expamsion
Home-Network:fix/bulk-nfs-truenas
Home-Network:fix/peers-configmap-content-hash
Home-Network:vpn/add-keith-phone
Home-Network:fix/new-peer-ignores-template-comments
Home-Network:fix/vpn-ingress-ip-forward
Home-Network:feature/add-private-key-for-wireguard
Home-Network:fix/vpn-server-key-in-sops
Home-Network:docs/pm-agent-registry-pull-prereq
Home-Network:fix/vpn-runbook-ansible-command
Home-Network:feat/pm-agent-issue-to-pr
Home-Network:docs/agents-work-in-their-own-workspace
Home-Network:feat/vpn-remote-access-wireguard
Home-Network:feat/cilium-roll-out-on-config-change
Home-Network:docs/oauth2-sidecar-scrape-verified
Home-Network:fix/plex-resolver-independent-advertise
Home-Network:fix/plex-unblock-app-tier
Home-Network:feat/hubble-flow-export-plex
Home-Network:fix/oauth2-sidecar-metrics-scrape-netpol
Home-Network:feat/plex-vip-hostname-and-lan-networks
Home-Network:fix/plex-advertise-plexdirect
Home-Network:docs/pihole-ha-verified
Home-Network:feat/pihole-multi-replica
Home-Network:feat/pihole-declare-state
Home-Network:feat/plex-remote-access-l4
Home-Network:fix/cilium-cgroup-hostroot
Home-Network:fix/node-keycloak-hosts-pin
Home-Network:feat/public-dns-wan-target
Home-Network:fix/plex-quicksync-verification
Home-Network:sync-39-onto-main
Home-Network:fix/admin-approval-override
Home-Network:feat/quicksync-via-device-plugin
Home-Network:bugfix/arr-to-plex
Home-Network:feat/oauth2-proxy-api-route-per-app
Home-Network:feat/intel-gpu-device-plugin
Home-Network:feat/oauth2-proxy-auth-failure-visible
Home-Network:fix/render-selftest-runs-in-gate
Home-Network:feat/plex-quicksync
Home-Network:feat/sd-hardware-capability
Home-Network:fix/jackett-oauth2-proxy-session
Home-Network:feat/node-health-render-device
Home-Network:fix/oauth2-proxy-cookie-collision-fleet
Home-Network:fix/flux-deploy-notification-inhibited
Home-Network:feat/flux-deploy-notifications
Home-Network:feat/conformance-job-template
Home-Network:feat/conformance-trigger-kubeconfig
Home-Network:feat/conformance-runner-image
Home-Network:fix/crafty-oauth2-proxy-signin
Home-Network:chore/decisions-log-conflict-free
Home-Network:fix/crafty-bedrock-19132
Home-Network:fix/gitea-settings-comment-names-impossible-secret
Home-Network:feat/ci-is-the-approver
Home-Network:fix/kyverno-policy-was-never-applied
Home-Network:docs/handoff-2026-09-29
Home-Network:fix/secret-name-and-require-gitea-settings
Home-Network:feat/conformance-trigger-admission-boundary
Home-Network:fix/cluster-available-would-fake-a-green
Home-Network:feat/gitea-settings-drift-check
Home-Network:fix/gitea-admins-could-override-the-gate
Home-Network:docs/close-out-ci-hardening
Home-Network:docs/ci-reality-vs-phase-0
Home-Network:docs/runbooks-pr-flow
Home-Network:fix/dind-native-sidecar
Home-Network:fix/gate-push-trigger-matches-feat-prefix
Home-Network:feat/plex-claim-token-013124
Home-Network:fix/conformance-context-is-fail-open
Home-Network:docs/ci-delivery-flow-for-agents
Home-Network:feat/plex-claim-sops-secret
Home-Network:feat/prod-hardening
Home-Network:prod/deliver-fixes
Home-Network:docs/memattr-6-12-backport
Home-Network:migration/dns-apex-inversion
Home-Network:feat/prod-memattr-and-nodes
Home-Network:docs/node2-memory-refine
Home-Network:feat/prod-node-2
Home-Network:feat/r-state-03-proven
Home-Network:feat/ledger-section-parsing
Home-Network:feat/conformance-run-ledger
Home-Network:chore/cilium-split-merge2
Home-Network:feat/cilium-own-kustomization
Home-Network:docs/poststart-incident
Home-Network:fix/poststart-exit-status
Home-Network:fix/strict-postbuild-substitutions
Home-Network:docs/phase-7-open-prod-node-1
Home-Network:rescued/fix/act-runner-dind-readiness
Home-Network:rescued/fix/node-dns-resilient-upstream
Home-Network:docs/reconcile-with-reality
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
This PR previously claimed a bug that does not exist. The fix has been withdrawn.
Original claim: a node cannot reach a LoadBalancer VIP whose Cilium L2 announcement lease it holds itself (the announcer does not answer its own ARP request), and because
vpn-ingressruns onhostNetworkand MASQUERADEs clients to the node's own address, VPN clients inherit that — so all 16 Gateway hostnames were dead over the VPN.That causal claim is falsified. Re-measured on prod with all 8
cilium-l2announce-*leases held byk3s-prod-1:k3s-prod-1(holds the gateway lease)ip neigh show 192.168.2.20INCOMPLETE— unresolved, as the original claim predictsnc -z 192.168.2.20 443curl --resolve home.keithjackson.dev:443:192.168.2.20 https://home.keithjackson.dev/The asserted cause is present and the asserted effect is absent. prod-2 and prod-3, holding no lease, return the same 200. That is sufficient to withdraw the claim.
Why it was never going to bite
cilium-dbg statuson every node:Cilium's LB datapath is attached to
wg0. A client packet arriving onwg0destined for a VIP is DNAT'd in BPF before any neighbour lookup happens, so the VIP's ARP state is irrelevant to forwarded client traffic.cilium-dbg service listalso programs192.168.2.20:443 => 127.0.0.1:13606— the Gateway backend is a node-local Envoy socket, so every node serves that VIP without putting a packet on the LAN at all.Why the original investigation went wrong — the methodological error
Every probe behind the original diagnosis was run from the
wireguardpod on the grounds that it "shares the host netns, so this is the path a MASQUERADEd client takes". That reasoning is wrong. A MASQUERADEd client is forwarded (netfilter FORWARD + routing); a probe started inside the pod is socket-originated (Cilium socket-LB). Different hooks. Sharing a network namespace is not sharing a datapath hook.Two specific bad probes:
ip neigh ... FAILEDis permanent, harmless, and partly apingartefact — VIPs answer no ICMP even when perfectly healthy.curl --interface wg0was used as the control to rule out a host-netns artefact. It fails (rc=28) on all three nodes, including ones whose ARP resolves perfectly. It discriminates nothing, so it never supported the conclusion it was cited for.curl -k https://192.168.2.20/returnsrc=35on all three nodes — that is missing SNI, not unreachability. Envoy needs SNI to select a listener.What remains in this PR
Documentation only — no behaviour change. The Cilium
nodeSelectoredit, thenode_baselinelabel tasks, thek3s_prodvar, both alerts, the promtool tests, the KSM allowlist entry and the new runbook have all been reverted or deleted.Net diff vs
mainis three files:platform/vpn-ingress/README.md— records that Cilium is attached towg0, why VIP ARP state is a red herring, and that forwarded ≠ socket-originatedrunbooks/vpn-remote-access.md— the same, plus the pre-existing192.168.2.21:8443(unifi) fault, which fails from all three nodes including ones with resolving ARPdecisions/2026-10-17-vpn-ingress-l2-lease-collision.md— rewritten in place to record the falsificationAlso corrected: the client profile is a FULL tunnel by default
infra/vpn-ingress/new-peer.sh:178-184emitsAllowedIPs = 0.0.0.0/0by default;--splitis the opt-in that narrows it to${WG_LAN_CIDR}, ${WG_CLIENT_CIDR}. An earlier reading of this file had the two branches backwards. There is no full-tunnel option to add — it is already the default, andrunbooks/vpn-remote-access.mdalready documented both modes.The reported internet problem is NOT diagnosed
No client has connected since the power-cycle —
wg show wg0shows the peer with no endpoint and no handshake on all three nodes, andFORWARD -i wg0plus theMASQUERADErule both read 0 packets. So no measurement of the forwarded path is possible yet and nothing here should be read as explaining it.Leading hypothesis, reasoned not measured: the profile is IPv4-only (
0.0.0.0/0, no::/0, and there is no::/0anywhere innew-peer.sh). A dual-stack client therefore tunnels all IPv4 but sends IPv6 outside the tunnel, while PiHole — reached over IPv4 through the tunnel — still answers with AAAA records. Address selection prefers IPv6, so public dual-stack sites fail while LAN and cluster targets (IPv4-only, no AAAA) keep working. That matches the reported symptom precisely. Untested.Local gate: rc 3, zero failures, committed-first and verified (
decisions-logcounted 48 entries = main's 47 + this one). Onlygitea-settings(CI-only token) and L2/L3conformance(no cluster) did not run. Not green — rc 3.🤖 Generated with Claude Code
A node cannot reach a LoadBalancer VIP whose Cilium L2 announcement lease it holds itself: the announcer does not answer its own ARP request, so on the lease holder the neighbour entry settles at FAILED and packets to that VIP die before leaving the node. Measured on prod 2026-10-08, symmetric across 3 nodes x 5 VIPs -- each node reached every VIP EXCEPT the ones it alone announced. platform/vpn-ingress is uniquely exposed: it runs WireGuard on hostNetwork and MASQUERADEs remote clients to the node's own address, so a VPN client takes the node's LAN path and inherits its ARP failures. The router's udp/51820 forward pointed at k3s-prod-1, which held the shared Gateway lease, so all 16 Gateway hostnames were dead over the VPN -- while PiHole DNS kept answering (its VIP has two Services and k3s-prod-2 held the other lease). The tunnel handshook, names resolved, every app hung, and nothing reported it. on k3s-prod-1: ip neigh show 192.168.2.20 -> FAILED (its own VIP) on prod-2/3: ip neigh show 192.168.2.20 -> lladdr ... (prod-1), 443 OK Mark the node and exclude it from announcing: * node_baseline labels the forwarded node (node_baseline_vpn_ingress_node, k3s-prod-1 on prod) homelab.keithjackson.dev/vpn-ingress=true, and removes the label from any node that no longer matches so moving it MOVES the label. * l2-announcement-policy.yaml excludes that label, so the node ARP-resolves every VIP. Fixes all VIPs for VPN clients, not just the one that collided. Cost: announcement candidacy drops from 3 nodes to 2. * VpnIngressNodeAnnouncesL2VIP joins kube_lease_owner against kube_node_labels (label added to the kube-state-metrics allowlist), with VpnIngressNodeLabelMissing as its watchdog -- NotIn on an absent key matches, so a missing label would silently re-arm the whole failure AND leave the critical alert inert. Four promtool cases, including that inertness. A node LABEL rather than a cluster-vars knob because the alert must read it too, and extract_rules.py rightly refuses a ${...} placeholder in an alert expr -- the rule unit-tested would not be the rule deployed. One source of truth, and it touches no sensitive path. Runbook/docs: step 3 of vpn-remote-access.md said "pick any node" and "failover is this one line". Both false; the first caused this. UNAPPLIED and unproven end to end; the Ansible tasks have never run. See the handoff entry's third section. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>0913ea9450toe82dab5df5fix(vpn-ingress): the VPN ingress node must announce no L2 VIPs, or clients route nothingto docs(vpn-ingress): withdraw the L2-lease diagnosis — it was falsified by measuremente82dab5df5tob258e47aa6This merge request is just historical info of an effort that is now OBE and has no affect on the codebase. History of the work will be kept in the git history and PR history and, therefore, does not need to be merged.
Pull request closed