Skip to content
47 changes: 41 additions & 6 deletions pkg/providers/v1/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,16 @@ const ServiceAnnotationLoadBalancerEIPAllocations = "service.beta.kubernetes.io/
// static IP addresses for the NLB. Only supported on elbv2 (NLB)
const ServiceAnnotationLoadBalancerPrivateIPv4Addresses = "service.beta.kubernetes.io/aws-load-balancer-private-ipv4-addresses"

// ServiceAnnotationLoadBalancerIPAddressType is the annotation used on the service
// to specify the IP address type for the load balancer. Supported values are "ipv4" and "dualstack".
// Defaults to "ipv4". Only supported on NLB.
const ServiceAnnotationLoadBalancerIPAddressType = "service.beta.kubernetes.io/aws-load-balancer-ip-address-type"

// ServiceAnnotationLoadBalancerTargetGroupIPAddressType is the annotation used on the service
// to specify the IP address type for the target groups. Supported values are "ipv4" and "ipv6".
// Defaults to "ipv4". Only supported on NLB.
const ServiceAnnotationLoadBalancerTargetGroupIPAddressType = "service.beta.kubernetes.io/aws-load-balancer-target-group-ip-address-type"

Comment on lines +237 to +246
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also update the service controller documentation: https://github.com/kubernetes/cloud-provider-aws/blob/master/docs/service_controller.md

// ServiceAnnotationLoadBalancerTargetNodeLabels is the annotation used on the service
// to specify a comma-separated list of key-value pairs which will be used to select
// the target nodes for the load balancer
Expand Down Expand Up @@ -1296,6 +1306,20 @@ func ipPermissionExists(newPermission, existing *ec2types.IpPermission, compareG
}
}

// Check IPv6 ranges
for j := range newPermission.Ipv6Ranges {
found := false
for k := range existing.Ipv6Ranges {
if isEqualStringPointer(newPermission.Ipv6Ranges[j].CidrIpv6, existing.Ipv6Ranges[k].CidrIpv6) {
found = true
break
}
}
if !found {
return false
}
}

for _, leftPair := range newPermission.UserIdGroupPairs {
found := false
for _, rightPair := range existing.UserIdGroupPairs {
Expand Down Expand Up @@ -2116,7 +2140,16 @@ func (c *Cloud) getSubnetCidrs(ctx context.Context, subnetIDs []string) ([]strin

cidrs := make([]string, 0, len(subnets))
for _, subnet := range subnets {
// Add IPv4 CIDR
cidrs = append(cidrs, aws.ToString(subnet.CidrBlock))

// Add IPv6 CIDRs if present
for _, ipv6Association := range subnet.Ipv6CidrBlockAssociationSet {
if ipv6Association.Ipv6CidrBlockState != nil &&
ipv6Association.Ipv6CidrBlockState.State == ec2types.SubnetCidrBlockStateCodeAssociated {
cidrs = append(cidrs, aws.ToString(ipv6Association.Ipv6CidrBlock))
}
}
}
return cidrs, nil
}
Expand Down Expand Up @@ -2430,11 +2463,6 @@ func (c *Cloud) EnsureLoadBalancer(ctx context.Context, clusterName string, apiS
loadBalancerName := c.GetLoadBalancerName(ctx, clusterName, apiService)
serviceName := types.NamespacedName{Namespace: apiService.Namespace, Name: apiService.Name}

instanceIDs := []string{}
for id := range instances {
instanceIDs = append(instanceIDs, string(id))
}

securityGroups, err := c.ensureNLBSecurityGroup(ctx,
loadBalancerName,
clusterName,
Expand All @@ -2447,11 +2475,12 @@ func (c *Cloud) EnsureLoadBalancer(ctx context.Context, clusterName string, apiS
serviceName,
loadBalancerName,
v2Mappings,
instanceIDs,
instances,
discoveredSubnetIDs,
internalELB,
annotations,
securityGroups,
apiService,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -2483,6 +2512,12 @@ func (c *Cloud) EnsureLoadBalancer(ctx context.Context, clusterName string, apiS
}
if len(sourceRangeCidrs) == 0 {
sourceRangeCidrs = append(sourceRangeCidrs, "0.0.0.0/0")

// For dual-stack or IPv6 load balancers, also add IPv6 default route
Copy link
Contributor

@mtulio mtulio Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

route or rule?

Suggested change
// For dual-stack or IPv6 load balancers, also add IPv6 default route
// For dual-stack or IPv6 load balancers, also add IPv6 default rule

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Route. "0.0.0.0/0" and "::/0" act as a gateway of last resort, directing unmatched traffic to a specific next-hop or upstream router.

lbIPAddressType := c.getLBIPAddressType(apiService)
if lbIPAddressType == elbv2types.IpAddressTypeDualstack {
sourceRangeCidrs = append(sourceRangeCidrs, "::/0")
}
}

err = c.updateInstanceSecurityGroupsForNLB(ctx, loadBalancerName, instances, subnetCidrs, sourceRangeCidrs, v2Mappings)
Expand Down
99 changes: 85 additions & 14 deletions pkg/providers/v1/aws_fakes.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ type FakeAWSServices struct {
func NewFakeAWSServices(clusterID string) *FakeAWSServices {
s := &FakeAWSServices{}
s.region = "us-west-2"
s.ec2 = &FakeEC2Impl{aws: s}
s.ec2 = &FakeEC2Impl{
aws: s,
SecurityGroups: make(map[string]*ec2types.SecurityGroup),
}
s.elb = &FakeELB{aws: s}
s.elbv2 = &FakeELBV2{aws: s}
s.asg = &FakeASG{aws: s}
Expand Down Expand Up @@ -198,6 +201,7 @@ type FakeEC2Impl struct {
DescribeSubnetsInput *ec2.DescribeSubnetsInput
RouteTables []ec2types.RouteTable
DescribeRouteTablesInput *ec2.DescribeRouteTablesInput
SecurityGroups map[string]*ec2types.SecurityGroup
}

// DescribeInstances returns fake instance descriptions
Expand Down Expand Up @@ -272,18 +276,32 @@ func (ec2i *FakeEC2Impl) DeleteVolume(request *ec2.DeleteVolumeInput) (resp *ec2
panic("Not implemented")
}

// DescribeSecurityGroups is not implemented but is required for interface
// conformance
// DescribeSecurityGroups returns fake security group descriptions
func (ec2i *FakeEC2Impl) DescribeSecurityGroups(ctx context.Context, request *ec2.DescribeSecurityGroupsInput, optFns ...func(*ec2.Options)) ([]ec2types.SecurityGroup, error) {
panic("Not implemented")
var result []ec2types.SecurityGroup

for _, groupID := range request.GroupIds {
if sg, ok := ec2i.SecurityGroups[groupID]; ok {
result = append(result, *sg)
}
}

return result, nil
}

// CreateSecurityGroup is not implemented but is required for interface
// conformance
// CreateSecurityGroup creates a fake security group
func (ec2i *FakeEC2Impl) CreateSecurityGroup(ctx context.Context, request *ec2.CreateSecurityGroupInput, optFns ...func(*ec2.Options)) (*ec2.CreateSecurityGroupOutput, error) {
// Mock implementation for testing
groupID := fmt.Sprintf("sg-%d", len(ec2i.SecurityGroups)+1)
sg := &ec2types.SecurityGroup{
GroupId: aws.String(groupID),
GroupName: request.GroupName,
Description: request.Description,
VpcId: request.VpcId,
IpPermissions: []ec2types.IpPermission{},
}
ec2i.SecurityGroups[groupID] = sg
return &ec2.CreateSecurityGroupOutput{
GroupId: aws.String("sg-123456"),
GroupId: aws.String(groupID),
}, nil
}

Expand All @@ -293,20 +311,73 @@ func (ec2i *FakeEC2Impl) DeleteSecurityGroup(ctx context.Context, request *ec2.D
panic("Not implemented")
}

// AuthorizeSecurityGroupIngress is not implemented but is required for
// interface conformance
// AuthorizeSecurityGroupIngress adds ingress rules to a security group
func (ec2i *FakeEC2Impl) AuthorizeSecurityGroupIngress(ctx context.Context, request *ec2.AuthorizeSecurityGroupIngressInput, optFns ...func(*ec2.Options)) (*ec2.AuthorizeSecurityGroupIngressOutput, error) {
// Mock implementation for testing
if request.GroupId == nil || len(request.IpPermissions) == 0 {
return nil, errors.New("Invalid input: GroupId or IpPermissions missing")
}

sg, ok := ec2i.SecurityGroups[*request.GroupId]
if !ok {
return nil, fmt.Errorf("Security group not found: %s", *request.GroupId)
}

// Add the new permissions to the security group
sg.IpPermissions = append(sg.IpPermissions, request.IpPermissions...)

return &ec2.AuthorizeSecurityGroupIngressOutput{}, nil
}

// RevokeSecurityGroupIngress is not implemented but is required for interface
// conformance
// RevokeSecurityGroupIngress removes ingress rules from a security group
func (ec2i *FakeEC2Impl) RevokeSecurityGroupIngress(ctx context.Context, request *ec2.RevokeSecurityGroupIngressInput, optFns ...func(*ec2.Options)) (*ec2.RevokeSecurityGroupIngressOutput, error) {
panic("Not implemented")
if request.GroupId == nil || len(request.IpPermissions) == 0 {
return nil, errors.New("Invalid input: GroupId or IpPermissions missing")
}

sg, ok := ec2i.SecurityGroups[*request.GroupId]
if !ok {
return nil, fmt.Errorf("Security group not found: %s", *request.GroupId)
}

// Remove the specified permissions from the security group
// This is a simplified implementation for testing
var newPermissions []ec2types.IpPermission
for _, existingPerm := range sg.IpPermissions {
keep := true
for _, removePerm := range request.IpPermissions {
if ipPermissionsEqual(existingPerm, removePerm) {
keep = false
break
}
}
if keep {
newPermissions = append(newPermissions, existingPerm)
}
}
sg.IpPermissions = newPermissions

return &ec2.RevokeSecurityGroupIngressOutput{}, nil
}

// ipPermissionsEqual is a helper function to compare two IpPermission objects
func ipPermissionsEqual(a, b ec2types.IpPermission) bool {
if aws.ToString(a.IpProtocol) != aws.ToString(b.IpProtocol) {
return false
}
if aws.ToInt32(a.FromPort) != aws.ToInt32(b.FromPort) {
return false
}
if aws.ToInt32(a.ToPort) != aws.ToInt32(b.ToPort) {
return false
}
// For simplicity, just compare the first IP range/IPv6 range
if len(a.IpRanges) > 0 && len(b.IpRanges) > 0 {
return aws.ToString(a.IpRanges[0].CidrIp) == aws.ToString(b.IpRanges[0].CidrIp)
}
if len(a.Ipv6Ranges) > 0 && len(b.Ipv6Ranges) > 0 {
return aws.ToString(a.Ipv6Ranges[0].CidrIpv6) == aws.ToString(b.Ipv6Ranges[0].CidrIpv6)
}
return false
}

// DescribeVolumeModifications is not implemented but is required for interface
Expand Down
Loading
Loading