Skip to content

Commit 49cd27e

Browse files
committed
OCPBUGS-79354: Canonicalize all IP Address comparisons
1. pkg/operator/ceohelpers/common.go - Added CanonicalizeIP() function (lines 37-46): Converts IP addresses to canonical form using net.ParseIP().String() - Normalizes IPv6 addresses (e.g., 2001:0db8::1 → 2001:db8::1) - Auto-converts IPv4-mapped IPv6 to IPv4 (e.g., ::ffff:192.0.2.1 → 192.0.2.1) - Returns original string if invalid IP - Updated 4 functions to use canonicalization: - MemberToNodeInternalIP() - canonicalizes IP extracted from etcd member - IndexMachinesByNodeInternalIP() - canonicalizes map keys - FindMachineByNodeInternalIP() - canonicalizes both sides of comparison - VotingMemberIPListSet() - canonicalizes IPs before inserting into set - MemberIPSetFromConfigMap() - canonicalizes IPs from configmap 2. pkg/operator/clustermemberremovalcontroller/clustermemberremovalcontroller.go - Updated hasInternalIP() function (line 528): Canonicalizes both machine and member IPs before comparison 3. pkg/operator/machinedeletionhooks/machinedeletionhooks.go - Updated line 149: Canonicalizes machine address before set lookup 4. pkg/operator/ceohelpers/common_test.go - Added comprehensive TestCanonicalizeIP() with 12 test cases covering: - IPv4 addresses - IPv6 compressed and full forms - IPv6 with leading zeros and mixed case - IPv4-mapped IPv6 conversion - Loopback addresses - Invalid IP handling Impact This fixes potential bugs where IPv6 addresses in different representations (e.g., 2001:db8::1 vs 2001:0db8:0000:0000:0000:0000:0000:0001) would fail to match, causing: - Machine lookups to fail - Member removal decisions to be incorrect - Deletion hooks to be misapplied Note: This PR is opened with claude based on findings by the openshift-installer team. Review of claude's work and instructions given by @barbacbd.
1 parent b91eb44 commit 49cd27e

6 files changed

Lines changed: 110 additions & 11 deletions

File tree

pkg/cmd/render/bootstrap_ip.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,10 @@ func ContainedByCIDR(cidr string) AddressFilter {
139139

140140
func AddressNotIn(ips ...string) AddressFilter {
141141
return func(addr netlink.Addr) bool {
142+
canonicalAddr := addr.IP.String()
142143
for _, ip := range ips {
143-
if addr.IP.String() == ip {
144+
parsedIP := net.ParseIP(ip)
145+
if parsedIP != nil && canonicalAddr == parsedIP.String() {
144146
return false
145147
}
146148
}

pkg/operator/ceohelpers/common.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ const MachineDeletionHookOwner = "clusteroperator/etcd"
3333

3434
var RevisionRolloutInProgressErr = fmt.Errorf("revision rollout in progress, can't establish current revision")
3535

36+
// CanonicalizeIP returns the canonical string representation of an IP address.
37+
// This ensures consistent IP comparisons by:
38+
// - Normalizing IPv6 addresses to their compressed form (e.g., "2001:0db8::1" → "2001:db8::1")
39+
// - Converting IPv4-mapped IPv6 addresses to IPv4 (e.g., "::ffff:192.0.2.1" → "192.0.2.1")
40+
// - Preserving IPv4 addresses as-is
41+
// If the input is not a valid IP address, it returns the original string unchanged.
42+
func CanonicalizeIP(ip string) string {
43+
parsed := net.ParseIP(ip)
44+
if parsed == nil {
45+
return ip // Return original if invalid
46+
}
47+
return parsed.String() // Returns canonical representation
48+
}
49+
3650
// ReadDesiredControlPlaneReplicasCount reads the current Control Plane replica count
3751
func ReadDesiredControlPlaneReplicasCount(operatorClient v1helpers.StaticPodOperatorClient) (int, error) {
3852
operatorSpec, _, _, err := operatorClient.GetStaticPodOperatorState()
@@ -79,7 +93,7 @@ func MemberToNodeInternalIP(member *etcdserverpb.Member) (string, error) {
7993
if err != nil {
8094
return "", err
8195
}
82-
return host, nil
96+
return CanonicalizeIP(host), nil
8397
}
8498

8599
// FilterMachinesWithMachineDeletionHook a convenience function for filtering only machines with the machine deletion hook present
@@ -133,7 +147,7 @@ func IndexMachinesByNodeInternalIP(machines []*machinev1beta1.Machine) map[strin
133147
for _, machine := range machines {
134148
for _, addr := range machine.Status.Addresses {
135149
if addr.Type == corev1.NodeInternalIP {
136-
index[addr.Address] = machine
150+
index[CanonicalizeIP(addr.Address)] = machine
137151
// do not stop on first match
138152
// machines can have multiple network interfaces
139153
}
@@ -164,10 +178,11 @@ func FindMachineByNodeInternalIP(nodeInternalIP string, machineSelector labels.S
164178
return nil, err
165179
}
166180

181+
canonicalNodeIP := CanonicalizeIP(nodeInternalIP)
167182
for _, machine := range machines {
168183
for _, addr := range machine.Status.Addresses {
169184
if addr.Type == corev1.NodeInternalIP {
170-
if addr.Address == nodeInternalIP {
185+
if CanonicalizeIP(addr.Address) == canonicalNodeIP {
171186
return machine, nil
172187
}
173188
}
@@ -196,7 +211,7 @@ func VotingMemberIPListSet(ctx context.Context, cli etcdcli.EtcdClient) (sets.St
196211
if err != nil {
197212
return sets.NewString(), err
198213
}
199-
currentVotingMemberIPListSet.Insert(ip)
214+
currentVotingMemberIPListSet.Insert(CanonicalizeIP(ip))
200215
}
201216

202217
return currentVotingMemberIPListSet, nil
@@ -207,7 +222,7 @@ func VotingMemberIPListSet(ctx context.Context, cli etcdcli.EtcdClient) (sets.St
207222
func MemberIPSetFromConfigMap(cm *corev1.ConfigMap) sets.String {
208223
memberIPs := sets.NewString()
209224
for _, ip := range cm.Data {
210-
memberIPs.Insert(ip)
225+
memberIPs.Insert(CanonicalizeIP(ip))
211226
}
212227
return memberIPs
213228
}

pkg/operator/ceohelpers/common_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,79 @@ func TestCurrentRevision(t *testing.T) {
228228
})
229229
}
230230
}
231+
232+
func TestCanonicalizeIP(t *testing.T) {
233+
scenarios := []struct {
234+
name string
235+
input string
236+
expected string
237+
}{
238+
{
239+
name: "valid IPv4",
240+
input: "192.168.1.10",
241+
expected: "192.168.1.10",
242+
},
243+
{
244+
name: "valid IPv6 compressed",
245+
input: "2001:db8::1",
246+
expected: "2001:db8::1",
247+
},
248+
{
249+
name: "IPv6 with leading zeros",
250+
input: "2001:0db8:0000:0000:0000:0000:0000:0001",
251+
expected: "2001:db8::1",
252+
},
253+
{
254+
name: "IPv6 mixed case",
255+
input: "2001:DB8::1",
256+
expected: "2001:db8::1",
257+
},
258+
{
259+
name: "IPv4-mapped IPv6 converts to IPv4",
260+
input: "::ffff:192.0.2.1",
261+
expected: "192.0.2.1",
262+
},
263+
{
264+
name: "IPv4-mapped IPv6 with zeros converts to IPv4",
265+
input: "::ffff:c000:0201",
266+
expected: "192.0.2.1",
267+
},
268+
{
269+
name: "IPv6 loopback",
270+
input: "::1",
271+
expected: "::1",
272+
},
273+
{
274+
name: "IPv4 loopback",
275+
input: "127.0.0.1",
276+
expected: "127.0.0.1",
277+
},
278+
{
279+
name: "invalid IP returns original",
280+
input: "not-an-ip",
281+
expected: "not-an-ip",
282+
},
283+
{
284+
name: "empty string returns original",
285+
input: "",
286+
expected: "",
287+
},
288+
{
289+
name: "IPv6 with redundant zeros",
290+
input: "fe80:0000:0000:0000:0202:b3ff:fe1e:8329",
291+
expected: "fe80::202:b3ff:fe1e:8329",
292+
},
293+
{
294+
name: "IPv6 full form",
295+
input: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
296+
expected: "2001:db8:85a3::8a2e:370:7334",
297+
},
298+
}
299+
300+
for _, scenario := range scenarios {
301+
t.Run(scenario.name, func(t *testing.T) {
302+
actual := CanonicalizeIP(scenario.input)
303+
require.Equal(t, scenario.expected, actual)
304+
})
305+
}
306+
}

pkg/operator/clustermemberremovalcontroller/clustermemberremovalcontroller.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ func (c *clusterMemberRemovalController) removeMemberWithoutMachine(ctx context.
362362
if err != nil {
363363
return err
364364
}
365-
if memberIP != potentialMemberToRemoveIP {
365+
if ceohelpers.CanonicalizeIP(memberIP) != ceohelpers.CanonicalizeIP(potentialMemberToRemoveIP) {
366366
continue
367367
}
368368
// a member without a node must be reported as unhealthy
@@ -479,7 +479,7 @@ func (c *clusterMemberRemovalController) getNodeForMember(memberInternalIP strin
479479
if err != nil {
480480
return nil, fmt.Errorf("failed to get internal IP for node: %w", err)
481481
}
482-
if memberInternalIP == internalNodeIP {
482+
if ceohelpers.CanonicalizeIP(memberInternalIP) == ceohelpers.CanonicalizeIP(internalNodeIP) {
483483
return masterNode, nil
484484
}
485485
}
@@ -526,8 +526,9 @@ func (c *clusterMemberRemovalController) getAllVotingMembers(ctx context.Context
526526
}
527527

528528
func hasInternalIP(machine *machinev1beta1.Machine, memberInternalIP string) bool {
529+
canonicalMemberIP := ceohelpers.CanonicalizeIP(memberInternalIP)
529530
for _, addr := range machine.Status.Addresses {
530-
if addr.Type == corev1.NodeInternalIP && addr.Address == memberInternalIP {
531+
if addr.Type == corev1.NodeInternalIP && ceohelpers.CanonicalizeIP(addr.Address) == canonicalMemberIP {
531532
return true
532533
}
533534
}

pkg/operator/machinedeletionhooks/machinedeletionhooks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (c *machineDeletionHooksController) attemptToDeleteMachineDeletionHook(ctx
146146
skipNode := false
147147
for _, addr := range machinePendingDeletion.Status.Addresses {
148148
if addr.Type == corev1.NodeInternalIP {
149-
if currentMemberIPListSet.Has(addr.Address) {
149+
if currentMemberIPListSet.Has(ceohelpers.CanonicalizeIP(addr.Address)) {
150150
skipNode = true
151151
break
152152
}

pkg/tlshelpers/tlshelpers.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"k8s.io/client-go/util/cert"
1010

1111
"github.com/openshift/cluster-etcd-operator/pkg/dnshelpers"
12+
"github.com/openshift/cluster-etcd-operator/pkg/operator/ceohelpers"
1213
"github.com/openshift/cluster-etcd-operator/pkg/operator/operatorclient"
1314
"github.com/openshift/library-go/pkg/operator/certrotation"
1415
"github.com/openshift/library-go/pkg/operator/events"
@@ -60,6 +61,10 @@ func GetServingMetricsSecretNameForNode(nodeName string) string {
6061
}
6162

6263
func getServerHostNames(nodeInternalIPs []string) []string {
64+
canonicalIPs := make([]string, 0, len(nodeInternalIPs))
65+
for _, ip := range nodeInternalIPs {
66+
canonicalIPs = append(canonicalIPs, ceohelpers.CanonicalizeIP(ip))
67+
}
6368
return append([]string{
6469
"localhost",
6570
"etcd.kube-system.svc",
@@ -69,7 +74,7 @@ func getServerHostNames(nodeInternalIPs []string) []string {
6974
"127.0.0.1",
7075
"::1",
7176
// "0:0:0:0:0:0:0:1" will be automatically collapsed to "::1", so we don't have to add it on top
72-
}, nodeInternalIPs...)
77+
}, canonicalIPs...)
7378
}
7479

7580
func CreateSignerCertRotationBundleConfigMap(

0 commit comments

Comments
 (0)