Compare commits

..
Author SHA1 Message Date
security-bot e8645d11ed chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-11-02-1004
Updates managed by image-automation-controller.
2026-11-02 10:21:19 +00:00
k.renner f3c9188807 Merge pull request 'fix(shelly-manager): disable TCP black-hole detection in the pod netns' (#1550) from shelly-manager-no-mtu-probing into main
Reviewed-on: #1550
2026-11-02 09:37:08 +00:00
claude-botandClaude Opus 5.5 c68b9ad7c2 fix(shelly-manager): disable TCP black-hole detection in the pod netns
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Cilium sets net.ipv4.tcp_mtu_probing=1 in every pod netns
(packetization-layer-pmtud-mode: blackhole). Shelly devices stop ACKing
for seconds while they write flash during an OTA; after tcp_retries1
timeouts the kernel treats that as a PMTU black hole and halves the MSS
on every stall, from 1440 down to the 48-byte floor. Upward probes need
an open congestion state with cwnd >= 11, which the stalling device
never allows, so the download stays at 48-byte segments and fills the
device's receive buffer.

The tuning plugin of the IoT attachment sets the sysctl to 0 for the pod
netns; the annotation bump rolls the pod so the attachment is re-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185Qb5YG28FfnVi1ujMEV5C
2026-11-02 10:36:01 +01:00
k.renner 59c9cc29fe Merge pull request 'fix(talos): route LoadBalancer replies through table 47' (#1549) from lb-replies-table-47 into main
Reviewed-on: #1549
2026-11-02 07:45:38 +00:00
claude-botandClaude Opus 5.5 3501586841 fix(talos): route LoadBalancer replies through table 47
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
A reply from a Cilium LoadBalancer VIP reaches the routing decision with
the VIP as its source, so the pod-source rules 1000/1001 missed it and
main sent it out mgmt0 while the request came in over ingress0, the BGP
next hop (k8s-3, 2026-11-02: replies from 10.2.99.90 to an IoT device
seen only on mgmt0). Rules 1002/1003 send the pool to table 47.

The pool becomes a site fact: each site patch carries lb-pool-site
(flux-system), and bgp-pool takes its blocks from it by Flux
substitution in infrastructure-crds. The CIDR blocks cover the same
addresses as the start/stop ranges did (allowFirstLastIPs defaults to
Yes). The qemu and hcloud sites carry the production pool, since
Services pin their addresses from it.

Refs #1547

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185Qb5YG28FfnVi1ujMEV5C
2026-11-02 08:42:49 +01:00
security-bot 9e1452cfe7 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-11-02-0659
Updates managed by image-automation-controller.
2026-11-02 07:21:19 +00:00
12 changed files with 76 additions and 115 deletions
+8 -10
View File
@@ -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.
+12
View File
@@ -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
+28 -2
View File
@@ -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
+12
View File
@@ -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