From 18f2f94f8e6115bc5458af7c2d4b43046b6939ce Mon Sep 17 00:00:00 2001 From: Story Crater Bot <19826264+Riotpiaole@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:19:44 -0700 Subject: [PATCH] k8s/cilium: add lb-ipam pool configuration - Cilium LB-IPAM pool (192.168.1.160-192.168.1.170) - Fixed IP assignment for LoadBalancer services --- k8s/cilium/l2-announcement-policy.yaml | 39 ++++++++++++++++++++++++++ k8s/cilium/lb-ipam-pool.yaml | 36 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 k8s/cilium/l2-announcement-policy.yaml create mode 100644 k8s/cilium/lb-ipam-pool.yaml diff --git a/k8s/cilium/l2-announcement-policy.yaml b/k8s/cilium/l2-announcement-policy.yaml new file mode 100644 index 0000000..1be4cf6 --- /dev/null +++ b/k8s/cilium/l2-announcement-policy.yaml @@ -0,0 +1,39 @@ +# k8s/cilium/l2-announcement-policy.yaml +# CiliumL2AnnouncementPolicy — without this, LB-IPAM (lb-ipam-pool.yaml) +# assigns IPs to LoadBalancer Services but nothing ARPs for them on the LAN, +# so they're unreachable from outside the cluster even though `kubectl get +# svc` shows a real EXTERNAL-IP. Confirmed both forgejo's .165 and +# shadowsocks' .166 were 100% packet loss / incomplete ARP before this. +# +# loadBalancerIPs: true makes Cilium announce every Service's LB-IPAM IP via +# ARP from whichever node currently holds the lease for it (one node per IP, +# decided by leaderElection — not all nodes simultaneously, which would +# otherwise cause ARP flapping/duplicate-IP confusion on the LAN). +# +# externalIPs/loadBalancerIPs split exists because Cilium also supports +# announcing Service externalIPs (a different field, unused in this repo); +# we only need loadBalancerIPs since every exposed Service here is type +# LoadBalancer via lb-ipam-pool.yaml. +# +# requires kube-proxy replacement (already the case — see +# k8s/talos-iam or helmfile.yaml.gotmpl kubeProxyReplacement=true) and a +# Cilium build with L2 announcements enabled (default since v1.14). +# +# Apply once after cluster bootstrap, alongside lb-ipam-pool.yaml: +# kubectl apply -f k8s/cilium/l2-announcement-policy.yaml +# +# Verify: +# kubectl get ciliuml2announcementpolicy +# ping 192.168.1.165 && ping 192.168.1.166 # both should now respond +# arp -a | grep 192.168.1.16 # should resolve to a real MAC +apiVersion: cilium.io/v2alpha1 +kind: CiliumL2AnnouncementPolicy +metadata: + name: homelab-l2-announce +spec: + loadBalancerIPs: true + interfaces: + - eno1 + # No nodeSelector restriction — all 3 nodes already run workloads + # (allowSchedulingOnControlPlanes: true in controlplane.yaml), and with + # 3 zone-labeled nodes, redundancy for per-IP leader election is maintained. diff --git a/k8s/cilium/lb-ipam-pool.yaml b/k8s/cilium/lb-ipam-pool.yaml new file mode 100644 index 0000000..f5ba867 --- /dev/null +++ b/k8s/cilium/lb-ipam-pool.yaml @@ -0,0 +1,36 @@ +# k8s/cilium/lb-ipam-pool.yaml +# CiliumLoadBalancerIPPool — tells Cilium LB-IPAM which IPs it can assign +# to LoadBalancer services in this cluster. +# +# CIDR 192.168.1.160/28 covers .160–.175 on the LAN: +# .160 talos-cp-1 (node — not assignable to services) +# .161 reserved +# .162 talos-worker-1 (node — not assignable to services) +# .163–.175 free for LoadBalancer services +# +# Current service IP assignments (via io.cilium/lb-ipam-ips annotation): +# 192.168.1.165 forgejo-gitea-http (cicd) +# 192.168.1.165 forgejo-gitea-ssh (cicd) — same IP, different ports +# 192.168.1.166 shadowsocks (vpn) +# +# Apply once after cluster bootstrap: +# kubectl apply -f k8s/cilium/lb-ipam-pool.yaml +# +# Verify assignment: +# kubectl get svc -n cicd forgejo-gitea-http forgejo-gitea-ssh +# # EXTERNAL-IP should change from to 192.168.1.165 + +apiVersion: "cilium.io/v2alpha1" +kind: CiliumLoadBalancerIPPool +metadata: + name: homelab-pool +spec: + blocks: + - cidr: "192.168.1.160/28" + # DO NOT add any 10.6.0.0/24 block here. That is the WireGuard subnet + # (10.6.0.1 = talos-cp-1 tunnel IP, 10.6.0.2 = DNS — see + # cluster-config/controlplane.yaml). A 10.6.0.x block let Cilium LB-IPAM + # auto-assign the CP's own tunnel IP to a Service, which broke the + # WireGuard tunnel and locked out the default kubectl context. It also + # can't work over WireGuard anyway — L2 announcements only ARP on eno1 + # (the LAN interface), not wg0. Keep this pool LAN-only.