diff --git a/pkg/infrastructure/azure/azure.go b/pkg/infrastructure/azure/azure.go index 12afe71bf9e..add324ba6a2 100644 --- a/pkg/infrastructure/azure/azure.go +++ b/pkg/infrastructure/azure/azure.go @@ -65,6 +65,7 @@ type Provider struct { clientOptions *arm.ClientOptions computeClientOptions *arm.ClientOptions publicLBIP string + publicLBIPv6 string } var _ clusterapi.InfraReadyProvider = (*Provider)(nil) @@ -420,8 +421,9 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput session.Credentials.SubscriptionID, resourceGroupName, ), - lbClient: lbClient, - tags: p.Tags, + lbClient: lbClient, + tags: p.Tags, + isDualstack: in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled(), } intLoadBalancer, err := updateInternalLoadBalancer(ctx, lbInput) @@ -430,9 +432,12 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput } logrus.Debugf("updated internal load balancer: %s", *intLoadBalancer.ID) + // Collect backend pools from internal load balancer - control plane VMs need to be in these pools var lbBaps []*armnetwork.BackendAddressPool + lbBaps = append(lbBaps, intLoadBalancer.Properties.BackendAddressPools...) var extLBFQDN string if in.InstallConfig.Config.PublicAPI() { + var publicIPv6 *armnetwork.PublicIPAddress publicIP, err := createPublicIP(ctx, &pipInput{ name: fmt.Sprintf("%s-pip-v4", in.InfraID), infraID: in.InfraID, @@ -440,32 +445,52 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput resourceGroup: resourceGroupName, pipClient: networkClientFactory.NewPublicIPAddressesClient(), tags: p.Tags, + ipversion: armnetwork.IPVersionIPv4, }) if err != nil { return fmt.Errorf("failed to create public ip: %w", err) } logrus.Debugf("created public ip: %s", *publicIP.ID) + if lbInput.isDualstack { + publicIPv6, err = createPublicIP(ctx, &pipInput{ + name: fmt.Sprintf("%s-pip-v6", in.InfraID), + infraID: in.InfraID, + region: in.InstallConfig.Config.Azure.Region, + resourceGroup: resourceGroupName, + pipClient: networkClientFactory.NewPublicIPAddressesClient(), + tags: p.Tags, + ipversion: armnetwork.IPVersionIPv6, + }) + if err != nil { + return fmt.Errorf("failed to create public ipv6: %w", err) + } + logrus.Debugf("created public ip v6: %s", *publicIPv6.ID) + } lbInput.loadBalancerName = in.InfraID lbInput.backendAddressPoolName = in.InfraID var loadBalancer *armnetwork.LoadBalancer if platform.OutboundType == aztypes.UserDefinedRoutingOutboundType { - loadBalancer, err = createAPILoadBalancer(ctx, publicIP, lbInput) + loadBalancer, err = createAPILoadBalancer(ctx, publicIP, publicIPv6, lbInput) if err != nil { return fmt.Errorf("failed to create API load balancer: %w", err) } } else { - loadBalancer, err = updateOutboundLoadBalancerToAPILoadBalancer(ctx, publicIP, lbInput) + loadBalancer, err = updateOutboundLoadBalancerToAPILoadBalancer(ctx, publicIP, publicIPv6, lbInput) if err != nil { return fmt.Errorf("failed to update external load balancer: %w", err) } } logrus.Debugf("updated external load balancer: %s", *loadBalancer.ID) - lbBaps = loadBalancer.Properties.BackendAddressPools + // Append external load balancer backend pools to the list (internal pools already added) + lbBaps = append(lbBaps, loadBalancer.Properties.BackendAddressPools...) extLBFQDN = *publicIP.Properties.DNSSettings.Fqdn p.publicLBIP = *publicIP.Properties.IPAddress + if lbInput.isDualstack { + p.publicLBIPv6 = *publicIPv6.Properties.IPAddress + } } if (in.InstallConfig.Config.Azure.OutboundType == aztypes.NATGatewayMultiZoneOutboundType || @@ -494,6 +519,13 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput p.NetworkClientFactory = networkClientFactory p.lbBackendAddressPools = lbBaps + // Add IPv6 load balancing rule for internal LB API server port in dual-stack mode + if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() { + if err := addIPv6InternalLBRule(ctx, in, networkClientFactory, resourceGroupName); err != nil { + return fmt.Errorf("failed to add IPv6 internal LB rule: %w", err) + } + } + if in.InstallConfig.Config.Azure.UserProvisionedDNS != dns.UserProvisionedDNSEnabled { if err := createDNSEntries(ctx, in, extLBFQDN, p.publicLBIP, resourceGroupName, p.clientOptions); err != nil { return fmt.Errorf("error creating DNS records: %w", err) @@ -847,3 +879,103 @@ func getMachinePoolSecurityType(installConfig *types.InstallConfig) string { } return "" } + +// addIPv6InternalLBRule adds an IPv6 load balancing rule for port 6443 on the internal load balancer. +// CAPZ only creates a single LBRuleHTTPS that uses the first (IPv4) frontend IP. +// For dual-stack, we need an additional rule for the IPv6 frontend IP. +func addIPv6InternalLBRule(ctx context.Context, in clusterapi.InfraReadyInput, networkClientFactory *armnetwork.ClientFactory, resourceGroup string) error { + lbClient := networkClientFactory.NewLoadBalancersClient() + internalLBName := fmt.Sprintf("%s-internal", in.InfraID) + + // Get the internal load balancer + lb, err := lbClient.Get(ctx, resourceGroup, internalLBName, nil) + if err != nil { + return fmt.Errorf("failed to get internal load balancer: %w", err) + } + + if lb.Properties == nil { + return fmt.Errorf("internal load balancer has no properties") + } + + // Find the IPv6 frontend IP configuration + var ipv6FrontendID string + for _, frontend := range lb.Properties.FrontendIPConfigurations { + if frontend.Properties != nil && + frontend.Properties.PrivateIPAddressVersion != nil && + *frontend.Properties.PrivateIPAddressVersion == armnetwork.IPVersionIPv6 { + ipv6FrontendID = *frontend.ID + break + } + } + if ipv6FrontendID == "" { + return fmt.Errorf("no IPv6 frontend IP configuration found on internal load balancer") + } + + // Find the IPv6 backend pool + var ipv6BackendPoolID string + for _, pool := range lb.Properties.BackendAddressPools { + if pool.Name != nil && strings.HasSuffix(*pool.Name, "-v6") { + ipv6BackendPoolID = *pool.ID + break + } + } + if ipv6BackendPoolID == "" { + return fmt.Errorf("no IPv6 backend pool found on internal load balancer") + } + + // Find the HTTPS probe + var httpsProbeID string + for _, probe := range lb.Properties.Probes { + if probe.Name != nil && *probe.Name == "HTTPSProbe" { + httpsProbeID = *probe.ID + break + } + } + if httpsProbeID == "" { + return fmt.Errorf("HTTPS probe not found on internal load balancer") + } + + // Check if the IPv6 rule already exists + for _, rule := range lb.Properties.LoadBalancingRules { + if rule.Name != nil && *rule.Name == "LBRuleHTTPS-v6" { + logrus.Infof("IPv6 load balancing rule for port 6443 already exists") + return nil + } + } + + // Add the IPv6 load balancing rule + lb.Properties.LoadBalancingRules = append(lb.Properties.LoadBalancingRules, &armnetwork.LoadBalancingRule{ + Name: to.Ptr("LBRuleHTTPS-v6"), + Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ + Protocol: to.Ptr(armnetwork.TransportProtocolTCP), + FrontendPort: to.Ptr(int32(6443)), + BackendPort: to.Ptr(int32(6443)), + EnableFloatingIP: to.Ptr(false), + IdleTimeoutInMinutes: to.Ptr(int32(4)), + LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault), + FrontendIPConfiguration: &armnetwork.SubResource{ + ID: to.Ptr(ipv6FrontendID), + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(ipv6BackendPoolID), + }, + Probe: &armnetwork.SubResource{ + ID: to.Ptr(httpsProbeID), + }, + }, + }) + + // Update the load balancer + poller, err := lbClient.BeginCreateOrUpdate(ctx, resourceGroup, internalLBName, lb.LoadBalancer, nil) + if err != nil { + return fmt.Errorf("failed to update internal load balancer with IPv6 rule: %w", err) + } + + _, err = poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to update internal load balancer with IPv6 rule: %w", err) + } + + logrus.Infof("Successfully added IPv6 load balancing rule for port 6443 on internal load balancer") + return nil +} diff --git a/pkg/infrastructure/azure/network.go b/pkg/infrastructure/azure/network.go index 75de4d50a5b..086c5e8ea63 100644 --- a/pkg/infrastructure/azure/network.go +++ b/pkg/infrastructure/azure/network.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path" + "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" @@ -28,6 +29,7 @@ type lbInput struct { idPrefix string lbClient *armnetwork.LoadBalancersClient tags map[string]*string + isDualstack bool } type pipInput struct { @@ -37,6 +39,7 @@ type pipInput struct { resourceGroup string pipClient *armnetwork.PublicIPAddressesClient tags map[string]*string + ipversion armnetwork.IPVersion } type vmInput struct { @@ -80,7 +83,7 @@ func createPublicIP(ctx context.Context, in *pipInput) (*armnetwork.PublicIPAddr Tier: to.Ptr(armnetwork.PublicIPAddressSKUTierRegional), }, Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv4), + PublicIPAddressVersion: to.Ptr(in.ipversion), PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic), DNSSettings: &armnetwork.PublicIPAddressDNSSettings{ DomainNameLabel: to.Ptr(in.infraID), @@ -101,9 +104,123 @@ func createPublicIP(ctx context.Context, in *pipInput) (*armnetwork.PublicIPAddr return &resp.PublicIPAddress, nil } -func createAPILoadBalancer(ctx context.Context, pip *armnetwork.PublicIPAddress, in *lbInput) (*armnetwork.LoadBalancer, error) { +func createAPILoadBalancer(ctx context.Context, pip, pipv6 *armnetwork.PublicIPAddress, in *lbInput) (*armnetwork.LoadBalancer, error) { probeName := "api-probe" - + frontendIPv4Name := to.Ptr(fmt.Sprintf("%s-v4", in.frontendIPConfigName)) + frontendIPv6Name := to.Ptr(fmt.Sprintf("%s-v6", in.frontendIPConfigName)) + loadBalancer := armnetwork.LoadBalancer{ + Location: to.Ptr(in.region), + SKU: &armnetwork.LoadBalancerSKU{ + Name: to.Ptr(armnetwork.LoadBalancerSKUNameStandard), + Tier: to.Ptr(armnetwork.LoadBalancerSKUTierRegional), + }, + Properties: &armnetwork.LoadBalancerPropertiesFormat{ + FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{ + { + Name: frontendIPv4Name, + Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PublicIPAddress: pip, + }, + }, + }, + BackendAddressPools: []*armnetwork.BackendAddressPool{ + { + Name: &in.backendAddressPoolName, + }, + }, + Probes: []*armnetwork.Probe{ + { + Name: &probeName, + Properties: &armnetwork.ProbePropertiesFormat{ + Protocol: to.Ptr(armnetwork.ProbeProtocolHTTPS), + Port: to.Ptr[int32](6443), + IntervalInSeconds: to.Ptr[int32](5), + ProbeThreshold: to.Ptr[int32](2), + RequestPath: to.Ptr("/readyz"), + }, + }, + }, + LoadBalancingRules: []*armnetwork.LoadBalancingRule{ + { + Name: to.Ptr("api-v4"), + Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ + Protocol: to.Ptr(armnetwork.TransportProtocolTCP), + FrontendPort: to.Ptr[int32](6443), + BackendPort: to.Ptr[int32](6443), + IdleTimeoutInMinutes: to.Ptr[int32](30), + EnableFloatingIP: to.Ptr(false), + LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault), + FrontendIPConfiguration: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, *frontendIPv4Name)), + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, in.backendAddressPoolName)), + }, + Probe: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, probeName)), + }, + }, + }, + }, + }, + Tags: in.tags, + } + if in.isDualstack { + loadBalancer.Properties.FrontendIPConfigurations = append(loadBalancer.Properties.FrontendIPConfigurations, + &armnetwork.FrontendIPConfiguration{ + Name: frontendIPv6Name, + Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PrivateIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv6), + PublicIPAddress: pipv6, + }, + }) + // TODO: backend pool + backendAddressPoolsv6 := []*armnetwork.BackendAddressPool{ + {Name: to.Ptr(fmt.Sprintf("%s-v6", in.backendAddressPoolName))}, + {Name: to.Ptr(fmt.Sprintf("%s-outbound-lb-outboundBackendPool-v6", in.backendAddressPoolName))}, + } + loadBalancer.Properties.BackendAddressPools = append(loadBalancer.Properties.BackendAddressPools, backendAddressPoolsv6...) + // TODO: load balancer rule + loadBalancerv6Rule := armnetwork.LoadBalancingRule{ + Name: to.Ptr("api-v6"), + Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ + DisableOutboundSnat: to.Ptr(true), + Protocol: to.Ptr(armnetwork.TransportProtocolTCP), + FrontendPort: to.Ptr[int32](6443), + BackendPort: to.Ptr[int32](6443), + IdleTimeoutInMinutes: to.Ptr[int32](30), + EnableFloatingIP: to.Ptr(false), + LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault), + FrontendIPConfiguration: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, *frontendIPv6Name)), + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, fmt.Sprintf("%s-v6", in.backendAddressPoolName))), + }, + Probe: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, probeName)), + }, + }, + } + loadBalancer.Properties.LoadBalancingRules = append(loadBalancer.Properties.LoadBalancingRules, &loadBalancerv6Rule) + // TODO: create an outbound rule for v6. + loadBalancer.Properties.OutboundRules = append(loadBalancer.Properties.OutboundRules, &armnetwork.OutboundRule{ + Name: to.Ptr("OutboundNATRulev6"), + Properties: &armnetwork.OutboundRulePropertiesFormat{ + Protocol: to.Ptr(armnetwork.LoadBalancerOutboundRuleProtocolAll), + FrontendIPConfigurations: []*armnetwork.SubResource{ + { + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, *frontendIPv6Name)), + }, + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, fmt.Sprintf("%s-outbound-lb-outboundBackendPool-v6", in.backendAddressPoolName))), + }, + }, + }) + } pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, in.resourceGroup, in.loadBalancerName, @@ -177,7 +294,7 @@ func createAPILoadBalancer(ctx context.Context, pip *armnetwork.PublicIPAddress, return &resp.LoadBalancer, nil } -func updateOutboundLoadBalancerToAPILoadBalancer(ctx context.Context, pip *armnetwork.PublicIPAddress, in *lbInput) (*armnetwork.LoadBalancer, error) { +func updateOutboundLoadBalancerToAPILoadBalancer(ctx context.Context, pip, pipv6 *armnetwork.PublicIPAddress, in *lbInput) (*armnetwork.LoadBalancer, error) { probeName := "api-probe" // Get the CAPI-created outbound load balancer so we can modify it. @@ -204,6 +321,84 @@ func updateOutboundLoadBalancerToAPILoadBalancer(ctx context.Context, pip *armne Name: &in.backendAddressPoolName, }) + // Add IPv4 load balancing rule + extLB.Properties.LoadBalancingRules = append(extLB.Properties.LoadBalancingRules, + &armnetwork.LoadBalancingRule{ + Name: to.Ptr("api-v4"), + Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ + Protocol: to.Ptr(armnetwork.TransportProtocolTCP), + FrontendPort: to.Ptr[int32](6443), + BackendPort: to.Ptr[int32](6443), + IdleTimeoutInMinutes: to.Ptr[int32](30), + EnableFloatingIP: to.Ptr(false), + LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault), + FrontendIPConfiguration: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s-v4", in.idPrefix, in.loadBalancerName, in.frontendIPConfigName)), + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, in.backendAddressPoolName)), + }, + Probe: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, probeName)), + }, + }, + }) + + if in.isDualstack { + frontendIPv6Name := to.Ptr(fmt.Sprintf("%s-v6", in.frontendIPConfigName)) + extLB.Properties.FrontendIPConfigurations = append(extLB.Properties.FrontendIPConfigurations, + &armnetwork.FrontendIPConfiguration{ + Name: to.Ptr(*frontendIPv6Name), + Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PublicIPAddress: pipv6, + }, + }) + // TODO: backend pool + backendAddressPoolsv6 := []*armnetwork.BackendAddressPool{ + {Name: to.Ptr(fmt.Sprintf("%s-v6", in.backendAddressPoolName))}, + {Name: to.Ptr(fmt.Sprintf("%s-outbound-lb-outboundBackendPool-v6", in.backendAddressPoolName))}, + } + extLB.Properties.BackendAddressPools = append(extLB.Properties.BackendAddressPools, + backendAddressPoolsv6...) + loadBalancerv6Rule := armnetwork.LoadBalancingRule{ + Name: to.Ptr("api-v6"), + Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ + DisableOutboundSnat: to.Ptr(true), + Protocol: to.Ptr(armnetwork.TransportProtocolTCP), + FrontendPort: to.Ptr[int32](6443), + BackendPort: to.Ptr[int32](6443), + IdleTimeoutInMinutes: to.Ptr[int32](30), + EnableFloatingIP: to.Ptr(false), + LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault), + FrontendIPConfiguration: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, *frontendIPv6Name)), + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, fmt.Sprintf("%s-v6", in.backendAddressPoolName))), + }, + Probe: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, probeName)), + }, + }, + } + extLB.Properties.LoadBalancingRules = append(extLB.Properties.LoadBalancingRules, &loadBalancerv6Rule) + // TODO: create an outbound rule for v6. + extLB.Properties.OutboundRules = append(extLB.Properties.OutboundRules, &armnetwork.OutboundRule{ + Name: to.Ptr("OutboundNATRulev6"), + Properties: &armnetwork.OutboundRulePropertiesFormat{ + Protocol: to.Ptr(armnetwork.LoadBalancerOutboundRuleProtocolAll), + FrontendIPConfigurations: []*armnetwork.SubResource{ + { + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, *frontendIPv6Name)), + }, + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, fmt.Sprintf("%s-outbound-lb-outboundBackendPool-v6", in.backendAddressPoolName))), + }, + }, + }) + } pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, in.resourceGroup, in.loadBalancerName, @@ -293,6 +488,11 @@ func updateInternalLoadBalancer(ctx context.Context, in *lbInput) (*armnetwork.L } existingFrontEndIPConfigName := *(existingFrontEndIPConfig[0].Name) + // For dual-stack, we need separate backend pools for IPv4 and IPv6 + // to avoid Azure error: RulesUseSameBackendPortProtocolAndPool + backendPoolV4 := in.backendAddressPoolName + backendPoolV6 := fmt.Sprintf("%s-v6", in.backendAddressPoolName) + mcsRule := &armnetwork.LoadBalancingRule{ Name: to.Ptr("sint-v4"), Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ @@ -306,7 +506,7 @@ func updateInternalLoadBalancer(ctx context.Context, in *lbInput) (*armnetwork.L ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, existingFrontEndIPConfigName)), }, BackendAddressPool: &armnetwork.SubResource{ - ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, in.backendAddressPoolName)), + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, backendPoolV4)), }, Probe: &armnetwork.SubResource{ ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, mcsProbeName)), @@ -316,13 +516,46 @@ func updateInternalLoadBalancer(ctx context.Context, in *lbInput) (*armnetwork.L intLB.Properties.Probes = append(intLB.Properties.Probes, mcsProbe) intLB.Properties.LoadBalancingRules = append(intLB.Properties.LoadBalancingRules, mcsRule) + + // For dual-stack, create IPv6 resources + if in.isDualstack { + // Create IPv6 backend pool + backendPoolIPv6 := &armnetwork.BackendAddressPool{ + Name: to.Ptr(backendPoolV6), + } + intLB.Properties.BackendAddressPools = append(intLB.Properties.BackendAddressPools, backendPoolIPv6) + + // Create IPv6 load balancing rule + frontendIPv6Name := fmt.Sprintf("%s-v6", existingFrontEndIPConfigName) + mcsRulev6 := &armnetwork.LoadBalancingRule{ + Name: to.Ptr("sint-v6"), + Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ + Protocol: to.Ptr(armnetwork.TransportProtocolTCP), + FrontendPort: to.Ptr[int32](22623), + BackendPort: to.Ptr[int32](22623), + IdleTimeoutInMinutes: to.Ptr[int32](30), + EnableFloatingIP: to.Ptr(false), + LoadDistribution: to.Ptr(armnetwork.LoadDistributionDefault), + FrontendIPConfiguration: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/frontendIPConfigurations/%s", in.idPrefix, in.loadBalancerName, frontendIPv6Name)), + }, + BackendAddressPool: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/backendAddressPools/%s", in.idPrefix, in.loadBalancerName, backendPoolV6)), + }, + Probe: &armnetwork.SubResource{ + ID: to.Ptr(fmt.Sprintf("/%s/%s/probes/%s", in.idPrefix, in.loadBalancerName, mcsProbeName)), + }, + }, + } + intLB.Properties.LoadBalancingRules = append(intLB.Properties.LoadBalancingRules, mcsRulev6) + } pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, in.resourceGroup, in.loadBalancerName, intLB, nil) if err != nil { - return nil, fmt.Errorf("cannot update load balancer: %w", err) + return nil, fmt.Errorf("cannot update internal load balancer: %w", err) } resp, err := pollerResp.PollUntilDone(ctx, nil) @@ -349,7 +582,24 @@ func associateVMToBackendPool(ctx context.Context, in vmInput) error { return fmt.Errorf("failed to get nic for vm %s: %w", vmName, err) } for _, ipconfig := range nic.Properties.IPConfigurations { - ipconfig.Properties.LoadBalancerBackendAddressPools = append(ipconfig.Properties.LoadBalancerBackendAddressPools, in.backendAddressPools...) + ipversion := armnetwork.IPVersionIPv4 + if ipconfig.Properties.PrivateIPAddressVersion != nil { + ipversion = *ipconfig.Properties.PrivateIPAddressVersion + } + for _, pool := range in.backendAddressPools { + poolAddressVersion := armnetwork.IPVersionIPv4 + if pool.Name != nil && strings.HasSuffix(*pool.Name, "v6") { + poolAddressVersion = armnetwork.IPVersionIPv6 + } + // Add pool if IP versions match + if ipversion == poolAddressVersion { + ipconfig.Properties.LoadBalancerBackendAddressPools = append( + ipconfig.Properties.LoadBalancerBackendAddressPools, + pool, + ) + } + + } } pollerResp, err := in.nicClient.BeginCreateOrUpdate(ctx, in.resourceGroup, nicName, nic.Interface, nil) if err != nil {