Skip to content

Commit d51af0e

Browse files
committed
subnets: configure default subnets to use NAT64/DNS64
This allows IPv6-only workloads to reach IPv4-only services. AWS supports this via NAT64/DNS64. More details: https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-nat64-dns64.html
1 parent 37827e9 commit d51af0e

File tree

5 files changed

+118
-34
lines changed

5 files changed

+118
-34
lines changed

pkg/cloud/services/interfaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const (
3535
AnyIPv4CidrBlock = "0.0.0.0/0"
3636
// AnyIPv6CidrBlock is the CIDR block to match all IPv6 addresses.
3737
AnyIPv6CidrBlock = "::/0"
38+
// NAT64CidrBlock is the well-known CIDR block defined in RFC6052 for NAT64.
39+
NAT64CidrBlock = "64:ff9b::/96"
3840
)
3941

4042
// ASGInterface encapsulates the methods exposed to the machinepool

pkg/cloud/services/network/routetables.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,13 @@ func (s *Service) getNatGatewayPrivateRoute(natGatewayID string) *ec2.CreateRout
362362
}
363363
}
364364

365+
func (s *Service) getNat64PrivateRoute(natGatewayID string) *ec2.CreateRouteInput {
366+
return &ec2.CreateRouteInput{
367+
NatGatewayId: aws.String(natGatewayID),
368+
DestinationIpv6CidrBlock: aws.String(services.NAT64CidrBlock),
369+
}
370+
}
371+
365372
func (s *Service) getEgressOnlyInternetGateway() *ec2.CreateRouteInput {
366373
return &ec2.CreateRouteInput{
367374
DestinationIpv6CidrBlock: aws.String(services.AnyIPv6CidrBlock),
@@ -456,6 +463,7 @@ func (s *Service) getRoutesToPrivateSubnet(sn *infrav1.SubnetSpec) (routes []*ec
456463

457464
routes = append(routes, s.getNatGatewayPrivateRoute(natGatewayID))
458465
if sn.IsIPv6 {
466+
routes = append(routes, s.getNat64PrivateRoute(natGatewayID))
459467
if !s.scope.VPC().IsIPv6Enabled() {
460468
// Safety net because EgressOnlyInternetGateway needs the ID from the ipv6 block.
461469
// if, for whatever reason by this point that is not available, we don't want to

pkg/cloud/services/network/routetables_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ func TestReconcileRouteTables(t *testing.T) {
161161
})).
162162
After(privateRouteTable)
163163

164+
m.CreateRoute(context.TODO(), gomock.Eq(&ec2.CreateRouteInput{
165+
DestinationIpv6CidrBlock: aws.String("64:ff9b::/96"),
166+
NatGatewayId: aws.String("nat-01"),
167+
RouteTableId: aws.String("rt-1"),
168+
})).
169+
After(privateRouteTable)
170+
164171
m.CreateRoute(context.TODO(), gomock.Eq(&ec2.CreateRouteInput{
165172
DestinationIpv6CidrBlock: aws.String("::/0"),
166173
EgressOnlyInternetGatewayId: aws.String("eigw-01"),
@@ -247,6 +254,13 @@ func TestReconcileRouteTables(t *testing.T) {
247254
})).
248255
After(privateRouteTable)
249256

257+
m.CreateRoute(context.TODO(), gomock.Eq(&ec2.CreateRouteInput{
258+
DestinationIpv6CidrBlock: aws.String("64:ff9b::/96"),
259+
NatGatewayId: aws.String("nat-01"),
260+
RouteTableId: aws.String("rt-1"),
261+
})).
262+
After(privateRouteTable)
263+
250264
m.CreateRoute(context.TODO(), gomock.Eq(&ec2.CreateRouteInput{
251265
DestinationIpv6CidrBlock: aws.String("::/0"),
252266
EgressOnlyInternetGatewayId: aws.String("eigw-01"),
@@ -1199,6 +1213,10 @@ func TestService_getRoutesForSubnet(t *testing.T) {
11991213
DestinationCidrBlock: aws.String("0.0.0.0/0"),
12001214
NatGatewayId: aws.String("nat-gw-fromZone-us-east-1a"),
12011215
},
1216+
{
1217+
DestinationIpv6CidrBlock: aws.String("64:ff9b::/96"),
1218+
NatGatewayId: aws.String("nat-gw-fromZone-us-east-1a"),
1219+
},
12021220
{
12031221
DestinationIpv6CidrBlock: aws.String("::/0"),
12041222
EgressOnlyInternetGatewayId: aws.String("vpc-eigw"),

pkg/cloud/services/network/subnets.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,28 @@ func (s *Service) createSubnet(sn *infrav1.SubnetSpec) (*infrav1.SubnetSpec, err
545545
return nil, errors.Wrapf(err, "failed to set subnet %q attribute assign ipv6 address on creation", *out.Subnet.SubnetId)
546546
}
547547
record.Eventf(s.scope.InfraCluster(), "SuccessfulModifySubnetAttributes", "Modified managed Subnet %q attributes", *out.Subnet.SubnetId)
548+
549+
// Enable DNS64 so that the Route 53 Resolver returns DNS records for IPv4-only services
550+
// containing a synthesized IPv6 address prefixed 64:ff9b::/96.
551+
// This is needed alongside NAT64 to allow IPv6-only workloads to reach IPv4-only services.
552+
// We only need to enable on private subnets.
553+
if !sn.IsPublic {
554+
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
555+
if _, err := s.EC2Client.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
556+
SubnetId: out.Subnet.SubnetId,
557+
EnableDns64: &types.AttributeBooleanValue{
558+
Value: aws.Bool(true),
559+
},
560+
}); err != nil {
561+
return false, err
562+
}
563+
return true, nil
564+
}, awserrors.SubnetNotFound); err != nil {
565+
record.Warnf(s.scope.InfraCluster(), "FailedModifySubnetAttributes", "Failed modifying managed Subnet %q attributes: %v", *out.Subnet.SubnetId, err)
566+
return nil, errors.Wrapf(err, "failed to set subnet %q attribute enable dns64", *out.Subnet.SubnetId)
567+
}
568+
record.Eventf(s.scope.InfraCluster(), "SuccessfulModifySubnetAttributes", "Modified managed Subnet %q attributes", *out.Subnet.SubnetId)
569+
}
548570
}
549571

550572
// AWS Wavelength Zone's public subnets does not support to map Carrier IP address on launch, and

pkg/cloud/services/network/subnets_test.go

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,14 +1781,6 @@ func TestReconcileSubnets(t *testing.T) {
17811781
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).
17821782
After(firstSubnet)
17831783

1784-
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
1785-
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
1786-
Value: aws.Bool(true),
1787-
},
1788-
SubnetId: aws.String("subnet-2"),
1789-
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).
1790-
After(firstSubnet)
1791-
17921784
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
17931785
MapPublicIpOnLaunch: &types.AttributeBooleanValue{
17941786
Value: aws.Bool(true),
@@ -1865,6 +1857,22 @@ func TestReconcileSubnets(t *testing.T) {
18651857
}, nil).
18661858
After(secondSubnet)
18671859

1860+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
1861+
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
1862+
Value: aws.Bool(true),
1863+
},
1864+
SubnetId: aws.String("subnet-2"),
1865+
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).
1866+
After(secondSubnet)
1867+
1868+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
1869+
EnableDns64: &types.AttributeBooleanValue{
1870+
Value: aws.Bool(true),
1871+
},
1872+
SubnetId: aws.String("subnet-2"),
1873+
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).
1874+
After(secondSubnet)
1875+
18681876
m.DescribeAvailabilityZones(context.TODO(), gomock.Any()).
18691877
Return(&ec2.DescribeAvailabilityZonesOutput{
18701878
AvailabilityZones: []types.AvailabilityZone{
@@ -3656,15 +3664,6 @@ func TestReconcileSubnets(t *testing.T) {
36563664
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
36573665
After(firstSubnet)
36583666

3659-
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
3660-
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
3661-
Value: aws.Bool(true),
3662-
},
3663-
SubnetId: aws.String("subnet-2"),
3664-
}).
3665-
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
3666-
After(firstSubnet)
3667-
36683667
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
36693668
MapPublicIpOnLaunch: &types.AttributeBooleanValue{
36703669
Value: aws.Bool(true),
@@ -3741,6 +3740,22 @@ func TestReconcileSubnets(t *testing.T) {
37413740
}, nil).
37423741
After(secondSubnet)
37433742

3743+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
3744+
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
3745+
Value: aws.Bool(true),
3746+
},
3747+
SubnetId: aws.String("subnet-2"),
3748+
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).
3749+
After(secondSubnet)
3750+
3751+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
3752+
EnableDns64: &types.AttributeBooleanValue{
3753+
Value: aws.Bool(true),
3754+
},
3755+
SubnetId: aws.String("subnet-2"),
3756+
}).Return(&ec2.ModifySubnetAttributeOutput{}, nil).
3757+
After(secondSubnet)
3758+
37443759
m.DescribeAvailabilityZones(context.TODO(), gomock.Any()).
37453760
Return(&ec2.DescribeAvailabilityZonesOutput{
37463761
AvailabilityZones: []types.AvailabilityZone{
@@ -3903,15 +3918,6 @@ func TestReconcileSubnets(t *testing.T) {
39033918
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
39043919
After(zone1PublicSubnet)
39053920

3906-
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
3907-
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
3908-
Value: aws.Bool(true),
3909-
},
3910-
SubnetId: aws.String("subnet-2"),
3911-
}).
3912-
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
3913-
After(zone1PublicSubnet)
3914-
39153921
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
39163922
MapPublicIpOnLaunch: &types.AttributeBooleanValue{
39173923
Value: aws.Bool(true),
@@ -3988,6 +3994,24 @@ func TestReconcileSubnets(t *testing.T) {
39883994
}, nil).
39893995
After(zone1PrivateSubnet)
39903996

3997+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
3998+
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
3999+
Value: aws.Bool(true),
4000+
},
4001+
SubnetId: aws.String("subnet-2"),
4002+
}).
4003+
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
4004+
After(zone1PrivateSubnet)
4005+
4006+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
4007+
EnableDns64: &types.AttributeBooleanValue{
4008+
Value: aws.Bool(true),
4009+
},
4010+
SubnetId: aws.String("subnet-2"),
4011+
}).
4012+
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
4013+
After(zone1PrivateSubnet)
4014+
39914015
// zone 2
39924016
m.DescribeAvailabilityZones(context.TODO(), &ec2.DescribeAvailabilityZonesInput{
39934017
ZoneNames: []string{"us-east-1c"},
@@ -4077,14 +4101,6 @@ func TestReconcileSubnets(t *testing.T) {
40774101
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
40784102
After(zone2PublicSubnet)
40794103

4080-
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
4081-
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
4082-
Value: aws.Bool(true),
4083-
},
4084-
SubnetId: aws.String("subnet-2"),
4085-
}).
4086-
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
4087-
After(zone2PublicSubnet)
40884104
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
40894105
MapPublicIpOnLaunch: &types.AttributeBooleanValue{
40904106
Value: aws.Bool(true),
@@ -4160,6 +4176,24 @@ func TestReconcileSubnets(t *testing.T) {
41604176
},
41614177
}, nil).
41624178
After(zone2PrivateSubnet)
4179+
4180+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
4181+
AssignIpv6AddressOnCreation: &types.AttributeBooleanValue{
4182+
Value: aws.Bool(true),
4183+
},
4184+
SubnetId: aws.String("subnet-2"),
4185+
}).
4186+
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
4187+
After(zone2PrivateSubnet)
4188+
4189+
m.ModifySubnetAttribute(context.TODO(), &ec2.ModifySubnetAttributeInput{
4190+
EnableDns64: &types.AttributeBooleanValue{
4191+
Value: aws.Bool(true),
4192+
},
4193+
SubnetId: aws.String("subnet-2"),
4194+
}).
4195+
Return(&ec2.ModifySubnetAttributeOutput{}, nil).
4196+
After(zone2PrivateSubnet)
41634197
},
41644198
},
41654199
}

0 commit comments

Comments
 (0)