revert(vpn-ingress): withdraw the L2 lease-collision fix — a node CAN reach a VIP it announces
PR #69 claimed a node cannot reach a LoadBalancer VIP whose Cilium L2 announcement
lease it holds, and excluded the VPN ingress node from announcing on that basis.
The claim makes a sharp prediction; today's power-cycle put ALL EIGHT
cilium-l2announce-* leases on k3s-prod-1, and the prediction failed.
Measured 2026-10-17 from the wireguard pod's host netns on each node: the holder
reaches 192.168.2.20/.22/.23/.24 normally (curl 200 in 18ms on .20) while its
ip neigh for every one of them sits at FAILED. The asserted cause is present
without the asserted effect, which falsifies it. .21 (unifi) fails from all three
nodes and is a separate pre-existing fault.
It is inert because no path resolves a VIP by ARP: cilium-dbg reports
KubeProxyReplacement on [eno1, wg0] and programs 192.168.2.20:443 =>
127.0.0.1:13606, so VIPs are DNAT'd in BPF before any neighbour lookup, on the
forwarded path as well as the host one.
Both probes behind the original diagnosis were bad. `ip neigh` FAILED is
permanent and harmless, and a ping artefact. `curl --interface wg0` — the control
used to rule out a host-netns artefact — fails on all three nodes, including ones
whose ARP resolves, so it discriminates nothing. The deeper error: a MASQUERADEd
client is FORWARDED while every pod-runnable probe is socket-originated. Sharing
a netns is not sharing a hook.
Reverted the policy nodeSelector, the Ansible node label, both alerts, their
promtool tests and the kube-state-metrics allowlist entry. Kept documentation
only: the measurements, which probes lie, and the fact that the forwarded path
remains unmeasured without a connected client.
Also recorded, for the reported "LAN and cluster work, internet does not":
new-peer.sh has emitted a FULL tunnel (0.0.0.0/0) by default since f92707f, so
there is no full-tunnel option to add and no code change is made. The server side
is capable — ip_forward=1, the 10.90.0.0/24 MASQUERADE present with an empty
CILIUM_POST_nat ahead of it, FORWARD accepts, default route intact, nodes reach
1.1.1.1. A full tunnel egresses the HOUSE WAN, not PIA: the egress policy selects
pods by egress-class=vpn, this pod is labelled internet, and tun0 exists on
k3s-prod-3 only. Root cause NOT found; no client has connected since the
power-cycle (0 forwarded packets, no handshake), so it is not reproducible here.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
4656b07178
commit
e82dab5df5
13 files changed
+338
-871
No files matched your search
@@ -420,22 +420,3 @@ flux_bootstrap_env_path: "clusters/prod"
|
||||
# rather than leaving an inert drop-in behind, so this group-wide `true` is still correct
|
||||
# for it.
|
||||
etcd_io_priority_enabled: true
|
||||
|
||||
# --- The node terminating remote-access WireGuard — DECIDED (measured 2026-10-08) ---------------
|
||||
# k3s-prod-1 (192.168.2.10) is what the SOHO router forwards WAN udp/51820 to, confirmed from the
|
||||
# cluster rather than assumed: only prod-1's `wg show` had a peer with an `endpoint` and a recent
|
||||
# handshake, the other two listeners were idle.
|
||||
#
|
||||
# node_baseline labels it `homelab.keithjackson.dev/vpn-ingress=true`, and
|
||||
# platform-config/cilium/l2-announcement-policy.yaml excludes that label from announcing any
|
||||
# LoadBalancer VIP -- because a node CANNOT reach a VIP whose Cilium L2 announcement lease it holds
|
||||
# itself (the announcer does not answer its own ARP request; the neighbour entry goes to `FAILED`).
|
||||
# prod-1 held the `shared` Gateway lease, and since vpn-ingress MASQUERADEs clients to the node's
|
||||
# own address, all 16 Gateway hostnames were dead over the VPN while PiHole DNS still resolved.
|
||||
#
|
||||
# [parity] MUST equal the node named by the router's forward (runbooks/vpn-remote-access.md step 3).
|
||||
# Nothing in CI can read that forward, so re-pointing it means changing BOTH: this value plus a
|
||||
# node_baseline re-run, and the router line. `VpnIngressNodeAnnouncesL2VIP` catches the case where
|
||||
# the label lands on a node that then takes a lease; it CANNOT catch the forward being moved
|
||||
# without this value following, which is why the runbook spells the two-step out.
|
||||
node_baseline_vpn_ingress_node: k3s-prod-1
|
||||
@@ -259,30 +259,3 @@ node_baseline_dns_options:
|
||||
# Written as ONE marked block, so removing an entry here removes it from the node, and
|
||||
# an empty list removes the block entirely.
|
||||
node_baseline_hosts_pins: []
|
||||
|
||||
# --- The node that terminates remote-access WireGuard (tags: vpn-ingress) ----------------------
|
||||
# Hostname of the node the SOHO router forwards WAN udp/51820 to, i.e. the one platform/vpn-ingress
|
||||
# actually serves remote clients from. Set it in group_vars for the environment; EMPTY by default,
|
||||
# because dev has no such forward and because a wrong value is worse than none.
|
||||
#
|
||||
# WHAT IT IS FOR, AND WHY IT IS NOT COSMETIC. The matching node gets
|
||||
# `homelab.keithjackson.dev/vpn-ingress=true`, and
|
||||
# platform-config/cilium/l2-announcement-policy.yaml EXCLUDES that label from announcing any
|
||||
# LoadBalancer VIP. MEASURED on prod 2026-10-08: a node cannot reach a VIP whose Cilium L2
|
||||
# announcement lease it holds itself (the announcer does not answer its own ARP request -- the
|
||||
# neighbour entry goes to `FAILED`). Because vpn-ingress runs on `hostNetwork` and MASQUERADEs
|
||||
# clients to the node's own address, a VPN client inherits that: with the forward on the node
|
||||
# holding the `shared` Gateway lease, all 16 Gateway hostnames were dead over the VPN while PiHole
|
||||
# DNS still resolved. Names worked, every app hung, nothing alerted.
|
||||
#
|
||||
# [parity] MUST name the node the router's forward actually points at. The forward is off-repo and
|
||||
# no check can read it (runbooks/vpn-remote-access.md step 3). Changing the forward is therefore a
|
||||
# two-part change: this value (then re-run the role), and the router line.
|
||||
#
|
||||
# Only ONE node should ever carry the label: it removes that node from VIP announcement candidacy,
|
||||
# so labelling all three would leave nothing announcing any VIP at all. The task below removes the
|
||||
# label from a node that no longer matches, so moving the value moves the label.
|
||||
node_baseline_vpn_ingress_node: ""
|
||||
node_baseline_vpn_ingress_label_key: homelab.keithjackson.dev/vpn-ingress
|
||||
# k3s ships its kubeconfig here; every prod node is a k3s server, so `k3s kubectl` works locally.
|
||||
node_baseline_vpn_ingress_kubeconfig: /etc/rancher/k3s/k3s.yaml
|
||||
@@ -1109,45 +1109,3 @@
|
||||
state: "{{ 'present' if node_baseline_hosts_pins | length > 0 else 'absent' }}"
|
||||
backup: true
|
||||
tags: [dns, hosts]
|
||||
|
||||
# ── vpn-ingress node label — tag `vpn-ingress` ────────────────────────────────────────────────
|
||||
# Marks the node terminating remote-access WireGuard so Cilium's L2 announcement policy can EXCLUDE
|
||||
# it from announcing LoadBalancer VIPs (a node cannot reach a VIP it announces itself -- see
|
||||
# defaults/main.yml). Same shape as roles/vpn_egress_gateway's egress-gateway label, and for the
|
||||
# same reason: the label is the one place BOTH a nodeSelector and a PromQL alert can read it from.
|
||||
#
|
||||
# Skipped entirely when node_baseline_vpn_ingress_node is empty (dev, or before the forward exists),
|
||||
# so this adds nothing to an environment that has no WAN forward.
|
||||
- name: Label this node as the vpn-ingress node
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
k3s kubectl label node {{ inventory_hostname }}
|
||||
{{ node_baseline_vpn_ingress_label_key }}=true --overwrite
|
||||
environment:
|
||||
KUBECONFIG: "{{ node_baseline_vpn_ingress_kubeconfig }}"
|
||||
register: node_baseline_vpn_ingress_label_apply
|
||||
changed_when: "'not labeled' not in node_baseline_vpn_ingress_label_apply.stdout"
|
||||
when:
|
||||
- node_baseline_vpn_ingress_node | length > 0
|
||||
- inventory_hostname == node_baseline_vpn_ingress_node
|
||||
tags: [vpn-ingress]
|
||||
|
||||
# The other half: moving the knob must MOVE the label, not add a second one. Two labelled nodes
|
||||
# would remove two of three nodes from announcement candidacy; three would leave no announcer at
|
||||
# all and take every VIP off the LAN.
|
||||
- name: Remove the vpn-ingress label from a node that no longer holds the forward
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
k3s kubectl label node {{ inventory_hostname }}
|
||||
{{ node_baseline_vpn_ingress_label_key }}-
|
||||
environment:
|
||||
KUBECONFIG: "{{ node_baseline_vpn_ingress_kubeconfig }}"
|
||||
register: node_baseline_vpn_ingress_label_remove
|
||||
changed_when: "'not labeled' not in node_baseline_vpn_ingress_label_remove.stdout"
|
||||
failed_when:
|
||||
- node_baseline_vpn_ingress_label_remove.rc != 0
|
||||
- "'not found' not in node_baseline_vpn_ingress_label_remove.stderr"
|
||||
when:
|
||||
- node_baseline_vpn_ingress_node | length > 0
|
||||
- inventory_hostname != node_baseline_vpn_ingress_node
|
||||
tags: [vpn-ingress]
|
||||
Reference in new issue
Block a user