Skip to content

Commit 741f895

Browse files
authored
fix(charts): filter default-route discovery helpers to IPv4 family (#145)
* test(charts): pin IPv4-only contract on default-route discovery helpers Add two failing tests covering the dual-stack regression: - TestDefaultGatewayIsIPv4OnDualStack: helper-level assertion that talm.discovered.default_gateway returns the IPv4 default-route gateway on a node with both IPv4 and IPv6 default routes (IPv6 ordered first in discovery, mirroring real Hetzner-style nodes). - TestCozystackChartRendersIPv4GatewayOnDualStack: end-to-end assertion on the cozystack v1.12 multi-doc chart output. Both LinkConfig.routes gateway and LinkConfig.addresses must use the IPv4 default-route data even when an IPv6 default route also exists. Both fail today: default_gateway and default_addresses_by_gateway both iterate routes and break at the first default route without filtering by family, so an IPv6-first discovery yields: - gateway: fe80::1 paired with an implicit IPv4 destination, which Talos rejects as a malformed route (the headline VIP-not-addressable symptom users hit). - addresses: empty, because the IPv6 family selected by the first default route filters out the node's IPv4 addresses, leaving the primary NIC unconfigured. Add a dualStackNicLookup() fixture next to simpleNicLookup() so the chart-rendering test path can exercise dual-stack discovery without disturbing the existing IPv4-only fixtures the rest of the suite depends on. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * fix(charts): filter default-route discovery helpers to IPv4 family talm.discovered.default_gateway and talm.discovered.default_addresses_by_gateway now skip routes whose family is not inet4. The chart consumers (cozystack and generic) pair the returned gateway with a hardcoded IPv4 destination (network: 0.0.0.0/0 on the legacy schema, or no network field on the typed RouteConfig schema where Talos defaults to IPv4), and the addresses helper filters per-link addresses by the route's family. Without the filter, a node with both IPv4 and IPv6 default routes (the typical Hetzner / dual-stack setup) ends up with: - gateway: <ipv6 link-local> in a route block whose destination is IPv4 — Talos rejects the malformed entry and dependent features (Layer2 VIP, default routing through the rendered chart config) silently break. - addresses: empty in the rendered LinkConfig because the helper inherits the IPv6 family from the first iterated default route and the address filter then drops every IPv4 entry on the link. The IPv4-only filter is symmetric with gateway_by_link, which already documents "IPv4-only by convention to avoid family/address mismatch on dual-stack nodes". Doc comments updated to spell out the chain so a future maintainer who adds an IPv6-aware variant for new typed routes does not silently widen the existing helpers and re-introduce the regression. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> --------- Signed-off-by: Aleksei Sviridkin <f@lex.la>
1 parent 892c543 commit 741f895

4 files changed

Lines changed: 429 additions & 22 deletions

File tree

charts/cozystack/values.yaml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ clusterDomain: cozy.local
3131
floatingIP: ""
3232

3333
# Optional override for the link Layer2VIPConfig is pinned to. When
34-
# left empty the chart picks the default-gateway-bearing link the
35-
# node already has (the VLAN sub-interface if one carries the
34+
# left empty the chart picks the IPv4-default-gateway-bearing link
35+
# the node already has (the VLAN sub-interface if one carries the
3636
# default route, otherwise the physical NIC). Set this when the
3737
# target link does not yet exist on the live system at first apply
3838
# -- typically a VLAN sub-interface that the same template is about
@@ -57,12 +57,14 @@ serviceSubnets:
5757

5858
# Optional override for machine.kubelet.nodeIP.validSubnets and
5959
# cluster.etcd.advertisedSubnets. When left empty the chart derives
60-
# the value from the node's default-gateway-bearing link at render
61-
# time (via talm.discovered.default_addresses_by_gateway), so the
62-
# generated machine config matches the node's actual network without
63-
# any values.yaml edit. Set this only when you want to pin a specific
64-
# subnet — typically for multi-homed nodes where the default-gateway
65-
# link is not the subnet you want kubelet/etcd to use.
60+
# the value from the node's IPv4-default-gateway-bearing link at
61+
# render time (via talm.discovered.default_addresses_by_gateway), so
62+
# the generated machine config matches the node's actual network
63+
# without any values.yaml edit. Set this only when you want to pin a
64+
# specific subnet — typically for multi-homed nodes where the
65+
# IPv4-default-gateway link is not the subnet you want kubelet/etcd
66+
# to use, or for IPv6-only setups where the chart's IPv4 default
67+
# would otherwise leave validSubnets empty.
6668
# Example:
6769
# advertisedSubnets:
6870
# - "10.0.0.0/8"

charts/generic/values.yaml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ endpoint: ""
1919
floatingIP: ""
2020

2121
# Optional override for the link Layer2VIPConfig is pinned to. When
22-
# left empty the chart picks the default-gateway-bearing link the
23-
# node already has (the VLAN sub-interface if one carries the
22+
# left empty the chart picks the IPv4-default-gateway-bearing link
23+
# the node already has (the VLAN sub-interface if one carries the
2424
# default route, otherwise the physical NIC). Set this when the
2525
# target link does not yet exist on the live system at first apply
2626
# — typically a VLAN sub-interface that the same template is about
@@ -45,12 +45,14 @@ serviceSubnets:
4545

4646
# Optional override for machine.kubelet.nodeIP.validSubnets and
4747
# cluster.etcd.advertisedSubnets. When left empty the chart derives
48-
# the value from the node's default-gateway-bearing link at render
49-
# time (via talm.discovered.default_addresses_by_gateway), so the
50-
# generated machine config matches the node's actual network without
51-
# any values.yaml edit. Set this only when you want to pin a specific
52-
# subnet — typically for multi-homed nodes where the default-gateway
53-
# link is not the subnet you want kubelet/etcd to use.
48+
# the value from the node's IPv4-default-gateway-bearing link at
49+
# render time (via talm.discovered.default_addresses_by_gateway), so
50+
# the generated machine config matches the node's actual network
51+
# without any values.yaml edit. Set this only when you want to pin a
52+
# specific subnet — typically for multi-homed nodes where the
53+
# IPv4-default-gateway link is not the subnet you want kubelet/etcd
54+
# to use, or for IPv6-only setups where the chart's IPv4 default
55+
# would otherwise leave validSubnets empty.
5456
# Example:
5557
# advertisedSubnets:
5658
# - "10.0.0.0/8"

charts/talm/templates/_helpers.tpl

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,19 @@
9191
{{- end }}
9292
{{- end }}
9393

94+
{{- /* JSON list of IPv4 addresses on the link carrying the IPv4
95+
default route. Filters routes to family=inet4 because consumers
96+
(cozystack and generic chart helpers) pair the returned addresses
97+
with a hardcoded IPv4 destination route — selecting the first
98+
default route regardless of family on a dual-stack node would
99+
cascade into the IPv6 family being applied to the address filter,
100+
and the node's IPv4 addresses would silently disappear from the
101+
rendered config. Mirrors gateway_by_link's IPv4-only convention. */ -}}
94102
{{- define "talm.discovered.default_addresses_by_gateway" }}
95103
{{- $linkName := "" }}
96104
{{- $family := "" }}
97105
{{- range (lookup "routes" "" "").items }}
98-
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") }}
106+
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") (eq (.spec.family | toString) "inet4") }}
99107
{{- $linkName = .spec.outLinkName }}
100108
{{- $family = .spec.family }}
101109
{{- break }}
@@ -134,9 +142,20 @@
134142
{{- end }}
135143
{{- end }}
136144

145+
{{- /* All four default_link_*_by_gateway helpers below are IPv4-only.
146+
They identify the link that carries the chart's primary uplink, and
147+
that uplink is the same one default_gateway / default_addresses_by_gateway
148+
configure — selecting a different link here would produce a config
149+
where LinkConfig.name attaches to one NIC while LinkConfig.routes /
150+
addresses describe a different NIC, leaving both unconfigured.
151+
Picking the IPv4 default route's outLinkName keeps the whole
152+
chain pointed at the same NIC on multi-NIC dual-stack nodes
153+
(typical Hetzner shape: management NIC on IPv4, public NIC on
154+
IPv6, default routes on different links). Symmetric with
155+
gateway_by_link / default_gateway / default_addresses_by_gateway. */ -}}
137156
{{- define "talm.discovered.default_link_name_by_gateway" }}
138157
{{- range (lookup "routes" "" "").items }}
139-
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") }}
158+
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") (eq (.spec.family | toString) "inet4") }}
140159
{{- .spec.outLinkName }}
141160
{{- break }}
142161
{{- end }}
@@ -145,7 +164,7 @@
145164

146165
{{- define "talm.discovered.default_link_address_by_gateway" }}
147166
{{- range (lookup "routes" "" "").items }}
148-
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") }}
167+
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") (eq (.spec.family | toString) "inet4") }}
149168
{{- (lookup "links" "" .spec.outLinkName).spec.hardwareAddr }}
150169
{{- break }}
151170
{{- end }}
@@ -154,7 +173,7 @@
154173

155174
{{- define "talm.discovered.default_link_bus_by_gateway" }}
156175
{{- range (lookup "routes" "" "").items }}
157-
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") }}
176+
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") (eq (.spec.family | toString) "inet4") }}
158177
{{- (lookup "links" "" .spec.outLinkName).spec.busPath }}
159178
{{- break }}
160179
{{- end }}
@@ -163,7 +182,7 @@
163182

164183
{{- define "talm.discovered.default_link_selector_by_gateway" }}
165184
{{- range (lookup "routes" "" "").items }}
166-
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") }}
185+
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") (eq (.spec.family | toString) "inet4") }}
167186
{{- with (lookup "links" "" .spec.outLinkName) }}
168187
busPath: {{ .spec.busPath }}
169188
{{- break }}
@@ -176,9 +195,20 @@ busPath: {{ .spec.busPath }}
176195
{{ printf "enx%s" (lookup "links" "" . | dig "spec" "hardwareAddr" . | replace ":" "") }}
177196
{{- end }}
178197

198+
{{- /* Scalar IPv4 default-route gateway (dst="", main table, family=inet4).
199+
Empty if the node has no IPv4 default route. IPv4-only by convention
200+
so the helper stays symmetric with gateway_by_link and so consumers
201+
that pair the returned gateway with an IPv4 destination
202+
(`network: 0.0.0.0/0` on the legacy schema, or no `network:` field
203+
on the typed RouteConfig schema) never end up with a malformed
204+
IPv4-dst + IPv6-gateway route on a dual-stack node. Talos derives
205+
the route family from the gateway literal at validation time, so a
206+
RouteConfig with gateway `fe80::1` and no `network:` field is
207+
rejected outright — silently breaking Layer2 VIP and any other
208+
feature that depends on the chart-emitted route entry. */ -}}
179209
{{- define "talm.discovered.default_gateway" }}
180210
{{- range (lookup "routes" "" "").items }}
181-
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") }}
211+
{{- if and (eq .spec.dst "") (not (eq .spec.gateway "")) (eq .spec.table "main") (eq (.spec.family | toString) "inet4") }}
182212
{{- .spec.gateway }}
183213
{{- break }}
184214
{{- end }}

0 commit comments

Comments
 (0)