Skip to content

Commit 8a4c9c1

Browse files
authored
Gracefully handle v4 (unmanaged) ENIs on IPv6 node (#3489)
* Gracefully handle v4 (unmanaged) ENIs on IPv6 node * fix panic * nit * fix unit tests * remove dead code * bumping go version to 1.24.9 to fix CVEs * update go check-latest flag * clean up * correcting error handling
1 parent 85eff56 commit 8a4c9c1

File tree

6 files changed

+109
-14
lines changed

6 files changed

+109
-14
lines changed

.github/workflows/pr-automated-tests.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # refs/tags/v6.0.0
1919
with:
2020
go-version: "1.24"
21+
check-latest: true
2122
- name: Set up tools
2223
run: |
2324
go install golang.org/x/lint/golint@latest
@@ -54,6 +55,7 @@ jobs:
5455
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # refs/tags/v6.0.0
5556
with:
5657
go-version: "1.24"
58+
check-latest: true
5759
- name: Build CNI images
5860
run: make multi-arch-cni-build
5961
docker-build-init:
@@ -70,5 +72,6 @@ jobs:
7072
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # refs/tags/v6.0.0
7173
with:
7274
go-version: "1.24"
75+
check-latest: true
7376
- name: Build CNI Init images
7477
run: make multi-arch-cni-init-build

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/aws/amazon-vpc-cni-k8s
22

3-
go 1.24.6
3+
go 1.24.9
44

55
require (
66
github.com/apparentlymart/go-cidr v1.1.0

pkg/awsutils/awsutils.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,12 @@ func (cache *EC2InstanceMetadataCache) getENIMetadata(eniMAC string) (ENIMetadat
736736
awsAPIErrInc("GetSubnetIPv6CIDRBlocks", err)
737737
return ENIMetadata{}, err
738738
} else {
739-
subnetV6Cidr = v6cidr.String()
739+
// Handle the case where GetSubnetIPv6CIDRBlocks returns empty IPNet for IPv4-only subnets
740+
// IMPORTANT: This scenario includes cross-VPC IPv4 ENIs attached to IPv6 nodes
741+
// where the ENI subnet is IPv4-only but the node is configured for IPv6
742+
if v6cidr != nil && v6cidr.IP != nil && v6cidr.Mask != nil {
743+
subnetV6Cidr = v6cidr.String()
744+
}
740745
}
741746

742747
imdsIPv6s, err := cache.imds.GetIPv6s(ctx, eniMAC)

pkg/awsutils/awsutils_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ const (
100100
imdsMACFieldsEfaOnly = "security-group-ids subnet-id vpc-id vpc-ipv4-cidr-blocks device-number interface-id subnet-ipv4-cidr-block ipv4-prefix ipv6-prefix"
101101
imdsMACFieldsV6Only = "security-group-ids subnet-id vpc-id vpc-ipv4-cidr-blocks vpc-ipv6-cidr-blocks device-number interface-id subnet-ipv6-cidr-blocks ipv6s ipv6-prefix"
102102
imdsMACFieldsV4AndV6 = "security-group-ids subnet-id vpc-id vpc-ipv4-cidr-blocks device-number interface-id subnet-ipv4-cidr-block subnet-ipv6-cidr-blocks ipv6s local-ipv4s ipv6-prefix"
103+
104+
// Cross-VPC ENI test constants
105+
crossVPCMAC = "12:ef:2a:98:e5:5c"
106+
crossVPCENIID = "eni-crossvpc123"
107+
crossVPCDevice = "2"
108+
crossVPCPrivateIP = "172.16.0.10"
109+
crossVPCSubnetID = "subnet-crossvpc456"
110+
crossVPCSubnetCIDR = "172.16.0.0/24"
111+
crossVPCVpcID = "vpc-crossvpc789"
112+
noManageTagKey = "node.k8s.amazonaws.com/no_manage"
113+
noManageTagValue = "true"
103114
)
104115

105116
func testMetadata(overrides map[string]interface{}) FakeIMDS {
@@ -2268,3 +2279,68 @@ func Test_IsTrunkingCompatible(t *testing.T) {
22682279
})
22692280
}
22702281
}
2282+
2283+
// TestCrossVPCENIWithFix demonstrates that getCIDR now handles 404 errors gracefully,
2284+
// fixing GetSubnetIPv6CIDRBlocks and other CIDR functions for cross-VPC IPv4-only ENIs
2285+
func TestCrossVPCENIWithFix(t *testing.T) {
2286+
// Set up the same cross-VPC scenario as the bug reproduction test
2287+
mockMetadata := testMetadata(map[string]interface{}{
2288+
// Primary VPC ENI with IPv6 support
2289+
metadataMACPath: primaryMAC + " " + crossVPCMAC,
2290+
2291+
// Cross-VPC ENI basic metadata (this succeeds)
2292+
metadataMACPath + crossVPCMAC: imdsMACFields,
2293+
metadataMACPath + crossVPCMAC + metadataDeviceNum: crossVPCDevice,
2294+
metadataMACPath + crossVPCMAC + metadataInterface: crossVPCENIID,
2295+
metadataMACPath + crossVPCMAC + metadataSubnetID: crossVPCSubnetID,
2296+
metadataMACPath + crossVPCMAC + metadataVpcID: crossVPCVpcID,
2297+
metadataMACPath + crossVPCMAC + metadataSubnetCIDR: crossVPCSubnetCIDR,
2298+
metadataMACPath + crossVPCMAC + metadataIPv4s: crossVPCPrivateIP,
2299+
metadataMACPath + crossVPCMAC + metadataSGs: sgs,
2300+
2301+
// IPv6 subnet CIDR request fails with 404 for cross-VPC IPv4-only subnet
2302+
// With the fix, this 404 should be handled gracefully
2303+
metadataMACPath + crossVPCMAC + metadataSubnetV6CIDR: newIMDSRequestError("test", &CustomRequestFailure{
2304+
code: "NotFound",
2305+
message: "IPv6 CIDR not found for cross-VPC IPv4-only subnet",
2306+
fault: smithy.FaultUnknown,
2307+
statusCode: 404,
2308+
requestID: "test-req-id",
2309+
}),
2310+
})
2311+
2312+
// Create cache with IPv6 enabled (same as bug test)
2313+
cache := &EC2InstanceMetadataCache{
2314+
imds: TypedIMDS{mockMetadata},
2315+
v6Enabled: true, // IPv6 enabled cluster
2316+
v4Enabled: true,
2317+
}
2318+
2319+
// With the fix, GetAttachedENIs should now succeed
2320+
enis, err := cache.GetAttachedENIs()
2321+
2322+
// Verify the fix: initialization should now succeed
2323+
assert.NoError(t, err, "Should succeed with fix - getCIDR now handles 404 gracefully for all CIDR functions")
2324+
assert.NotNil(t, enis, "Should return ENI list")
2325+
assert.Equal(t, 2, len(enis), "Should return both primary and cross-VPC ENIs")
2326+
2327+
// Verify that both ENIs are present
2328+
var primaryFound, crossVPCFound bool
2329+
for _, eni := range enis {
2330+
if eni.ENIID == primaryeniID {
2331+
primaryFound = true
2332+
}
2333+
if eni.ENIID == crossVPCENIID {
2334+
crossVPCFound = true
2335+
// Cross-VPC ENI should have IPv4 data but no IPv6 (due to 404 handled gracefully)
2336+
assert.NotEmpty(t, eni.IPv4Addresses, "Cross-VPC ENI should have IPv4 addresses")
2337+
assert.Empty(t, eni.SubnetIPv6CIDR, "Cross-VPC ENI should have empty IPv6 CIDR (404 handled gracefully)")
2338+
}
2339+
}
2340+
2341+
assert.True(t, primaryFound, "Primary ENI should be found")
2342+
assert.True(t, crossVPCFound, "Cross-VPC ENI should be found")
2343+
2344+
t.Logf("Result: IPv6-enabled cluster successfully initializes with cross-VPC IPv4-only ENIs")
2345+
t.Logf("Found %d ENIs: Primary (%s) and Cross-VPC (%s)", len(enis), primaryeniID, crossVPCENIID)
2346+
}

pkg/awsutils/imds.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,29 +411,39 @@ func (typedimds TypedIMDS) getIPs(ctx context.Context, key string) ([]net.IP, er
411411
return ips, err
412412
}
413413

414-
func (typedimds TypedIMDS) getCIDR(ctx context.Context, key string) (net.IPNet, error) {
414+
func (typedimds TypedIMDS) getCIDR(ctx context.Context, key string) (*net.IPNet, error) {
415415
output, err := typedimds.GetMetadata(ctx, &imds.GetMetadataInput{
416416
Path: key})
417417
if err != nil {
418-
return net.IPNet{}, err
418+
imdsErr := new(imdsRequestError)
419+
oe := new(smithy.OperationError)
420+
if errors.As(err, &imdsErr) || errors.As(err, &oe) {
421+
if IsNotFound(err) {
422+
// No CIDR found. Not an error for IPv4-only subnets when requesting IPv6 CIDRs
423+
return nil, nil
424+
}
425+
log.Warnf("%v", err)
426+
return nil, newIMDSRequestError(err.Error(), err)
427+
}
428+
return nil, err
419429
}
420430
if output == nil || output.Content == nil {
421-
return net.IPNet{}, newIMDSRequestError(key, fmt.Errorf("empty response"))
431+
return nil, newIMDSRequestError(key, fmt.Errorf("empty response"))
422432
}
423433

424434
defer output.Content.Close()
425435
bytes, err := io.ReadAll(output.Content)
426436
if err != nil {
427-
return net.IPNet{}, newIMDSRequestError(key, fmt.Errorf("failed to read content: %w", err))
437+
return nil, newIMDSRequestError(key, fmt.Errorf("failed to read content: %w", err))
428438
}
429439

430440
data := strings.TrimSpace(string(bytes))
431441
ip, network, err := net.ParseCIDR(data)
432442
if err != nil {
433-
return net.IPNet{}, err
443+
return nil, err
434444
}
435445
// Why doesn't net.ParseCIDR just return values in this form?
436-
cidr := net.IPNet{IP: ip, Mask: network.Mask}
446+
cidr := &net.IPNet{IP: ip, Mask: network.Mask}
437447
return cidr, err
438448
}
439449

@@ -574,7 +584,7 @@ func (typedimds TypedIMDS) GetIPv6s(ctx context.Context, mac string) ([]net.IP,
574584
}
575585

576586
// GetSubnetIPv4CIDRBlock returns the IPv4 CIDR block for the subnet in which the interface resides.
577-
func (typedimds TypedIMDS) GetSubnetIPv4CIDRBlock(ctx context.Context, mac string) (net.IPNet, error) {
587+
func (typedimds TypedIMDS) GetSubnetIPv4CIDRBlock(ctx context.Context, mac string) (*net.IPNet, error) {
578588
key := fmt.Sprintf("network/interfaces/macs/%s/subnet-ipv4-cidr-block", mac)
579589
return typedimds.getCIDR(ctx, key)
580590
}
@@ -616,7 +626,7 @@ func (typedimds TypedIMDS) GetVPCIPv6CIDRBlocks(ctx context.Context, mac string)
616626
}
617627

618628
// GetSubnetIPv6CIDRBlocks returns the IPv6 CIDR block for the subnet in which the interface resides.
619-
func (typedimds TypedIMDS) GetSubnetIPv6CIDRBlocks(ctx context.Context, mac string) (net.IPNet, error) {
629+
func (typedimds TypedIMDS) GetSubnetIPv6CIDRBlocks(ctx context.Context, mac string) (*net.IPNet, error) {
620630
key := fmt.Sprintf("network/interfaces/macs/%s/subnet-ipv6-cidr-blocks", mac)
621631
return typedimds.getCIDR(ctx, key)
622632
}

pkg/awsutils/imds_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ func TestGetSubnetIPv4CIDRBlock(t *testing.T) {
202202

203203
ip, err := f.GetSubnetIPv4CIDRBlock(context.TODO(), "02:c5:f8:3e:6b:27")
204204
if assert.NoError(t, err) {
205-
assert.Equal(t, ip, net.IPNet{IP: net.IPv4(10, 0, 64, 0), Mask: net.CIDRMask(18, 32)})
205+
expected := net.IPNet{IP: net.IPv4(10, 0, 64, 0), Mask: net.CIDRMask(18, 32)}
206+
assert.Equal(t, *ip, expected)
206207
}
207208
}
208209

@@ -225,9 +226,9 @@ func TestGetSubnetIPv6CIDRBlocks(t *testing.T) {
225226

226227
ips, err := f.GetSubnetIPv6CIDRBlocks(context.TODO(), "02:c5:f8:3e:6b:27")
227228
if assert.NoError(t, err) {
228-
assert.Equal(t, ips,
229-
net.IPNet{IP: net.IP{0x20, 0x1, 0xd, 0xb8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
230-
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}})
229+
expected := net.IPNet{IP: net.IP{0x20, 0x1, 0xd, 0xb8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
230+
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}
231+
assert.Equal(t, *ips, expected)
231232
}
232233
}
233234

0 commit comments

Comments
 (0)