Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8645d11ed | ||
|
|
f3c9188807 | ||
|
|
c68b9ad7c2 | ||
|
|
59c9cc29fe | ||
|
|
3501586841 | ||
|
|
9e1452cfe7 |
@@ -8,7 +8,7 @@ Issue #576. Five classes, each on its own VLAN.
|
||||
|---|---|---|
|
||||
| Ceph replication | `ceph-san`, VLAN 49 | the whole link is handed to the OSD pod (Multus `host-device`) |
|
||||
| pod↔pod | `pod0`, VLAN 48 | pod-sourced traffic is routed from table 47; bird puts the peers' pod CIDRs there via `pod0` |
|
||||
| world↔pod | `ingress0`, VLAN 47 | BGP session sourced there (next hop of every LoadBalancer VIP); pod egress and VIP replies take table 47's default |
|
||||
| world↔pod | `ingress0`, VLAN 47 | BGP session sourced there (next hop of every LoadBalancer VIP); pod egress takes table 47's default |
|
||||
| node↔node | `node0`, VLAN 46 | the node InternalIP and etcd live there; so do apiserver↔kubelet, cilium-health and host→remote-pod traffic |
|
||||
| world↔node | `mgmt0`, VLAN 40 | API VIP for kubectl, talosctl from outside, node egress (DNS, NTP, image pulls) |
|
||||
|
||||
@@ -22,6 +22,13 @@ Two rules, one per family, send every packet with a pod source to table 47
|
||||
(`talos/patches/site-homelab.yaml`, priorities 1000/1001, below main). The
|
||||
table decides by longest prefix; no rule carves out exceptions.
|
||||
|
||||
A reply from a LoadBalancer VIP is pod traffic as well, but it reaches the
|
||||
routing decision with the VIP as its source. Two more rules (1002/1003) send
|
||||
the LoadBalancer pool to table 47, so the reply leaves over `ingress0`, where
|
||||
the request arrived as the BGP next hop. Without them main sent it out
|
||||
`mgmt0`. The pool is a site fact: the site patch's `lb-pool-site` ConfigMap,
|
||||
which `bgp-pool` takes by Flux substitution, and the rules beside it.
|
||||
|
||||
bird (`kubernetes/infrastructure/bird/`) owns table 47 and fills it with:
|
||||
|
||||
- the node's attached networks — `mgmt0`, `ingress0`, `pod0`, `node0` —
|
||||
@@ -41,15 +48,6 @@ address, or a hostNetwork pod — would otherwise take the default to the UDM.
|
||||
The routing decision runs before Cilium's masquerade hook, so the rule sees the
|
||||
pod address and the masquerade then rewrites it to the egress link's.
|
||||
|
||||
A reply from a LoadBalancer VIP is the exception: Cilium reverses the service
|
||||
NAT before the packet reaches the routing decision, so its source is the VIP
|
||||
and the pod rules miss it. bird's `vip-rules` container adds one rule per
|
||||
block of every `CiliumLoadBalancerIPPool` (priority 1002, `from <block>
|
||||
lookup 47`) and keeps them in sync with the pools every minute. The blocks
|
||||
must be CIDRs; a start/stop block stops the container. Without these rules
|
||||
the reply takes main's default over `mgmt0` while the request arrived on
|
||||
`ingress0` (#1547).
|
||||
|
||||
Node-originated traffic does not match the rules and uses main, where Cilium
|
||||
routes each peer's pod CIDR via the peer's InternalIP — on `node0`.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ spec:
|
||||
k8s.v1.cni.cncf.io/networks: shelly-manager-iot
|
||||
# Changing this rolls the pod, which is the only way a change to the
|
||||
# shelly-manager-iot attachment takes effect.
|
||||
net.renner.uno/iot-attachment-config: "tuning-no-ra"
|
||||
net.renner.uno/iot-attachment-config: "tuning-no-ra-no-mtu-probing"
|
||||
spec:
|
||||
# eth0 (Cilium-managed pod network) is still the primary — it
|
||||
# carries the ingress, mosquitto LB return path, etc. The macvlan
|
||||
@@ -64,7 +64,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: shelly-manager
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-10-30-1343 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-11-02-1004 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -50,6 +50,9 @@ spec:
|
||||
# to IPv6 clients on v30iot leave via net1, bypassing Cilium's LoadBalancer
|
||||
# reverse NAT. tuning turns RA processing off on net1; the link-local
|
||||
# address mDNS uses stays.
|
||||
#
|
||||
# tcp_mtu_probing (pod-wide; Cilium sets 1): Shelly flash-write stalls
|
||||
# trip black-hole detection, which cuts firmware downloads to 48-byte MSS.
|
||||
config: |
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
@@ -67,7 +70,8 @@ spec:
|
||||
"type": "tuning",
|
||||
"sysctl": {
|
||||
"net.ipv6.conf.IFNAME.accept_ra": "0",
|
||||
"net.ipv6.conf.IFNAME.autoconf": "0"
|
||||
"net.ipv6.conf.IFNAME.autoconf": "0",
|
||||
"net.ipv4.tcp_mtu_probing": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -89,6 +89,9 @@ spec:
|
||||
substituteFrom:
|
||||
- kind: ConfigMap
|
||||
name: cluster-settings
|
||||
# The LoadBalancer pool, from the site's Talos patch (site-*.yaml).
|
||||
- kind: ConfigMap
|
||||
name: lb-pool-site
|
||||
dependsOn:
|
||||
- name: infrastructure
|
||||
---
|
||||
|
||||
@@ -14,10 +14,6 @@ rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["get", "list"]
|
||||
# vip-rules reads the LoadBalancer pool blocks it routes into table 47.
|
||||
- apiGroups: ["cilium.io"]
|
||||
resources: ["ciliumloadbalancerippools"]
|
||||
verbs: ["get", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
@@ -115,24 +111,6 @@ spec:
|
||||
volumeMounts:
|
||||
- name: etc-bird
|
||||
mountPath: /etc/bird
|
||||
# Routes replies from LoadBalancer VIPs into table 47 (vip-rules.sh).
|
||||
- name: vip-rules
|
||||
image: gitea.renner.uno/gitea_admin/bird@sha256:65fc93efead1e9b065d2c78e748f6c3c1ad7216e28b757d9fa4e204fa2ffe9a9
|
||||
command: ["/bin/sh", "/config/vip-rules.sh"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: "5m"
|
||||
memory: "16Mi"
|
||||
limits:
|
||||
cpu: "100m"
|
||||
memory: "64Mi"
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
add: ["NET_ADMIN"]
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
|
||||
@@ -10,5 +10,3 @@ configMapGenerator:
|
||||
- bird.conf
|
||||
- defines.tmpl
|
||||
- peers.tmpl
|
||||
- vip-rules.sh
|
||||
- vip-rules.tmpl
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Keeps one routing rule per LoadBalancer pool block: from <block> lookup 47.
|
||||
#
|
||||
# A reply from a VIP reaches the routing decision with the VIP as its source:
|
||||
# Cilium reverses the service NAT before the packet enters the stack. The pod
|
||||
# rules (1000/1001, talos/patches/site-homelab.yaml) do not match it, so
|
||||
# without these rules it takes main's default over mgmt0 while its request
|
||||
# arrived on ingress0. The blocks are read from the pools, so the prefix is
|
||||
# written only there.
|
||||
#
|
||||
# Rules are left in place on exit: if bird is gone, table 47 is empty and the
|
||||
# lookup falls through to main, as it does for the pod rules.
|
||||
set -eu
|
||||
|
||||
PRIO=1002
|
||||
TABLE=47
|
||||
|
||||
family_of() {
|
||||
case $1 in *:*) echo 6 ;; *) echo 4 ;; esac
|
||||
}
|
||||
|
||||
reconcile() {
|
||||
# An API error skips this round; the rules in place stay.
|
||||
if ! blocks=$(kubectl get ciliumloadbalancerippools.cilium.io \
|
||||
-o go-template-file=/config/vip-rules.tmpl); then
|
||||
echo "reading the pools failed; retrying" >&2
|
||||
return 0
|
||||
fi
|
||||
for b in $blocks; do
|
||||
case $b in
|
||||
range:*)
|
||||
echo "pool block ${b#range:} is a range; only cidr blocks map to a rule" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for fam in 4 6; do
|
||||
want=$(for b in $blocks; do
|
||||
if [ "$(family_of "$b")" = "$fam" ]; then echo "$b"; fi
|
||||
done | sort -u)
|
||||
have=$(ip -"$fam" rule show |
|
||||
awk -v p="$PRIO:" -v t="$TABLE" '$1 == p && $2 == "from" && $4 == "lookup" && $5 == t { print $3 }' |
|
||||
sort -u)
|
||||
|
||||
for b in $have; do
|
||||
if ! echo "$want" | grep -qxF "$b"; then
|
||||
ip -"$fam" rule del priority "$PRIO" from "$b" table "$TABLE"
|
||||
echo "removed: priority $PRIO from $b table $TABLE"
|
||||
fi
|
||||
done
|
||||
for b in $want; do
|
||||
if ! echo "$have" | grep -qxF "$b"; then
|
||||
ip -"$fam" rule add priority "$PRIO" from "$b" table "$TABLE"
|
||||
echo "added: priority $PRIO from $b table $TABLE"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
trap 'exit 0' TERM INT
|
||||
while :; do
|
||||
reconcile
|
||||
sleep 60 &
|
||||
wait $!
|
||||
done
|
||||
@@ -1,5 +0,0 @@
|
||||
{{- /* One line per block of every enabled CiliumLoadBalancerIPPool. A start/stop
|
||||
block renders as range:<start>-<stop>, which vip-rules.sh refuses. */ -}}
|
||||
{{- range .items }}{{ if not .spec.disabled }}{{ range .spec.blocks }}
|
||||
{{- if .cidr }}{{ .cidr }}{{ else }}range:{{ .start }}-{{ .stop }}{{ end }}
|
||||
{{ end }}{{ end }}{{ end -}}
|
||||
@@ -8,11 +8,12 @@
|
||||
# Selector is opt-in via `lb-pool: bgp`. There is no default pool —
|
||||
# Services without this label do not get a LoadBalancer IP.
|
||||
#
|
||||
# The ranges are a site fact: lb-pool-site from the site's Talos patch
|
||||
# (talos/patches/site-*.yaml), by Flux substitution. A CIDR block uses all its
|
||||
# addresses (allowFirstLastIPs defaults to Yes).
|
||||
#
|
||||
# IPv4 + IPv6 ranges keep matching last bytes (10.2.99.x ↔ fd99::x)
|
||||
# so each LB has the same "tail" in both families.
|
||||
#
|
||||
# Blocks are CIDRs: bird's vip-rules turns each into a routing rule into
|
||||
# table 47, so VIP replies leave on ingress0 (infrastructure-inhouse/bird).
|
||||
apiVersion: cilium.io/v2alpha1
|
||||
kind: CiliumLoadBalancerIPPool
|
||||
metadata:
|
||||
@@ -22,8 +23,8 @@ spec:
|
||||
matchLabels:
|
||||
lb-pool: bgp
|
||||
blocks:
|
||||
- cidr: "10.2.99.0/24"
|
||||
- cidr: "fd99::/120"
|
||||
- cidr: "${LB_POOL_V4}"
|
||||
- cidr: "${LB_POOL_V6}"
|
||||
---
|
||||
# Cilium BGPv2 control-plane config peering each node with the UDM over the
|
||||
# ingress VLAN 47 — never over v30iot, which is the untrusted IoT segment.
|
||||
|
||||
@@ -67,6 +67,18 @@ cluster:
|
||||
nameservers:
|
||||
- 185.12.64.1
|
||||
- 185.12.64.2
|
||||
# The LoadBalancer pool (see site-homelab.yaml). Services pin their
|
||||
# addresses from the production pool, so a test run carries the same one.
|
||||
- name: lb-pool-site
|
||||
contents: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lb-pool-site
|
||||
namespace: flux-system
|
||||
data:
|
||||
LB_POOL_V4: 10.2.99.0/24
|
||||
LB_POOL_V6: fd99::/120
|
||||
---
|
||||
# Host DNS asks k8s-gateway first, so containerd resolves in-cluster names
|
||||
# (gitea.renner.uno) in the cluster; bootstrap.sh installs k8s-gateway right
|
||||
|
||||
@@ -85,6 +85,18 @@ cluster:
|
||||
nameservers:
|
||||
- fd02:47::1
|
||||
- 10.2.47.1
|
||||
# The LoadBalancer pool (bgp-pool, infrastructure/post-crds/cilium-bgp.yaml,
|
||||
# by Flux substitution). The routing rules 1002/1003 below name it too.
|
||||
- name: lb-pool-site
|
||||
contents: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lb-pool-site
|
||||
namespace: flux-system
|
||||
data:
|
||||
LB_POOL_V4: 10.2.99.0/24
|
||||
LB_POOL_V6: fd99::/120
|
||||
---
|
||||
# All pod traffic goes to table 47, and the table decides — longest prefix
|
||||
# match, no exceptions to carve out. bird fills it with the node's attached
|
||||
@@ -102,8 +114,7 @@ cluster:
|
||||
#
|
||||
# Priorities are below main (32766) and clear of the rule Cilium installs at 9.
|
||||
# The two families are separate rule tables, so the priorities need not match;
|
||||
# the document name is the priority and must be unique. 1002 is taken by bird's
|
||||
# vip-rules: replies from LoadBalancer VIPs, whose source is the VIP.
|
||||
# the document name is the priority and must be unique.
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1000"
|
||||
@@ -116,6 +127,21 @@ name: "1001"
|
||||
src: 10.244.0.0/16
|
||||
table: "47"
|
||||
---
|
||||
# A LoadBalancer reply is pod traffic too, but it reaches the routing decision
|
||||
# with the VIP as its source, so the rules above miss it and main sends it out
|
||||
# mgmt0 while the request came in over ingress0. The pool is lb-pool-site above.
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1002"
|
||||
src: fd99::/120
|
||||
table: "47"
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1003"
|
||||
src: 10.2.99.0/24
|
||||
table: "47"
|
||||
---
|
||||
# Host DNS asks k8s-gateway first, so containerd resolves in-cluster names
|
||||
# (gitea.renner.uno) in the cluster; bootstrap.sh installs k8s-gateway right
|
||||
# after the CNI, before anything else needs DNS. The UDM is the site's
|
||||
|
||||
@@ -47,3 +47,15 @@ cluster:
|
||||
values.yaml: |
|
||||
deployment:
|
||||
dnsPolicy: Default
|
||||
# The LoadBalancer pool (see site-homelab.yaml). Services pin their
|
||||
# addresses from the production pool, so a test run carries the same one.
|
||||
- name: lb-pool-site
|
||||
contents: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lb-pool-site
|
||||
namespace: flux-system
|
||||
data:
|
||||
LB_POOL_V4: 10.2.99.0/24
|
||||
LB_POOL_V6: fd99::/120
|
||||
|
||||
Reference in New Issue
Block a user