From 7b6dffac684971195a3c1aeb1ad4c9ccbf5810a3 Mon Sep 17 00:00:00 2001 From: Aditya Narayanaswamy Date: Mon, 23 Feb 2026 09:17:33 -0500 Subject: [PATCH] azure: Add IPv6 frontend IP configurations for dual-stack Adding IPv6 public IP creation and frontend IP configurations on external load balancers and adding IPv6 frontend IP to CAPZ apiServerLB manifest. --- pkg/asset/manifests/azure/cluster.go | 8 + pkg/infrastructure/azure/azure.go | 75 ++++++- pkg/infrastructure/azure/network.go | 289 +++++++++++++++++++-------- 3 files changed, 286 insertions(+), 86 deletions(-) diff --git a/pkg/asset/manifests/azure/cluster.go b/pkg/asset/manifests/azure/cluster.go index fb9a8ab3ffd..793c9c8d056 100644 --- a/pkg/asset/manifests/azure/cluster.go +++ b/pkg/asset/manifests/azure/cluster.go @@ -153,6 +153,14 @@ func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID PrivateIPAddress: lbip, }, }} + + // For dual-stack, add IPv6 frontend IP for internal LB (dynamic allocation) + if installConfig.Config.Azure.IPFamily.DualStackEnabled() { + apiServerLB.FrontendIPs = append(apiServerLB.FrontendIPs, capz.FrontendIP{ + Name: fmt.Sprintf("%s-internal-frontEnd-v6", clusterID.InfraID), + // No PrivateIPAddress specified = dynamic allocation + }) + } vnetResourceGroup := installConfig.Config.Azure.ResourceGroupName if installConfig.Config.Azure.VirtualNetwork != "" { virtualNetworkAddressPrefixes = make([]string, 0) diff --git a/pkg/infrastructure/azure/azure.go b/pkg/infrastructure/azure/azure.go index 12afe71bf9e..2d71dbc744c 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) @@ -414,14 +415,15 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput region: platform.Region, resourceGroup: resourceGroupName, subscriptionID: session.Credentials.SubscriptionID, - frontendIPConfigName: "public-lb-ip-v4", + frontendIPConfigName: "public-lb-ip", backendAddressPoolName: fmt.Sprintf("%s-internal", in.InfraID), idPrefix: fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers", 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) @@ -433,6 +435,7 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput var lbBaps []*armnetwork.BackendAddressPool 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,23 +443,39 @@ 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 in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() { + 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) } @@ -466,6 +485,9 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput lbBaps = loadBalancer.Properties.BackendAddressPools extLBFQDN = *publicIP.Properties.DNSSettings.Fqdn p.publicLBIP = *publicIP.Properties.IPAddress + if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() { + p.publicLBIPv6 = *publicIPv6.Properties.IPAddress + } } if (in.InstallConfig.Config.Azure.OutboundType == aztypes.NATGatewayMultiZoneOutboundType || @@ -512,6 +534,24 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio } subscriptionID := ssn.Credentials.SubscriptionID + // Add IPv6 frontend IP to internal LB after CAPZ finishes machine reconciliation. + if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() { + lbClient := p.NetworkClientFactory.NewLoadBalancersClient() + lbInput := &lbInput{ + loadBalancerName: fmt.Sprintf("%s-internal", in.InfraID), + infraID: in.InfraID, + region: in.InstallConfig.Config.Azure.Region, + resourceGroup: p.ResourceGroupName, + subscriptionID: subscriptionID, + lbClient: lbClient, + tags: p.Tags, + } + if err := addIPv6InternalLBFrontend(ctx, lbInput); err != nil { + return fmt.Errorf("failed to add IPv6 frontend to internal load balancer: %w", err) + } + logrus.Debugf("added IPv6 frontend to internal load balancer") + } + if in.InstallConfig.Config.PublicAPI() { vmClient, err := armcompute.NewVirtualMachinesClient(subscriptionID, ssn.TokenCreds, p.computeClientOptions) if err != nil { @@ -578,6 +618,31 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio if err != nil { return fmt.Errorf("failed to associate inbound nat rule to interface: %w", err) } + + // For dual-stack, create IPv6 inbound NAT rule for SSH access to bootstrap. + if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() { + publicIPv6outbound, err := createPublicIP(ctx, &pipInput{ + name: fmt.Sprintf("%s-pip-v6-outbound-lb", in.InfraID), + infraID: in.InfraID, + region: in.InstallConfig.Config.Azure.Region, + resourceGroup: p.ResourceGroupName, + pipClient: p.NetworkClientFactory.NewPublicIPAddressesClient(), + tags: p.Tags, + ipversion: armnetwork.IPVersionIPv6, + }) + if err != nil { + return fmt.Errorf("failed to create public ipv6 for outbound ipv6 lb: %w", err) + } + logrus.Debugf("created public ipv6 for outbound ipv6 lb: %s", *publicIPv6outbound.ID) + + // Update the outbound node IPv6 load balancer. + outboundLBName := fmt.Sprintf("%s-ipv6-outbound-node-lb", in.InfraID) + err = updateOutboundIPv6LoadBalancer(ctx, publicIPv6outbound, p.NetworkClientFactory.NewLoadBalancersClient(), p.ResourceGroupName, outboundLBName, in.InfraID) + if err != nil { + return fmt.Errorf("failed to set public ipv6 to outbound ipv6 lb: %w", err) + } + logrus.Debugf("updated outbound ipv6 lb %s with public ipv6: %s", outboundLBName, *publicIPv6outbound.ID) + } } return nil diff --git a/pkg/infrastructure/azure/network.go b/pkg/infrastructure/azure/network.go index 75de4d50a5b..1b9eb554383 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,70 +104,83 @@ 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" - - pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, - in.resourceGroup, - in.loadBalancerName, - 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: &in.frontendIPConfigName, - Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ - PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), - PublicIPAddress: pip, - }, + 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, - }, + }, + 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"), - }, + }, + 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, 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)), - }, + }, + 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, - }, nil) + }, + 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, + }, + }) + } + pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, + in.resourceGroup, + in.loadBalancerName, + loadBalancer, nil) if err != nil { return nil, fmt.Errorf("cannot create load balancer: %w", err) @@ -177,7 +193,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. @@ -193,17 +209,50 @@ func updateOutboundLoadBalancerToAPILoadBalancer(ctx context.Context, pip *armne // API server. extLB.Properties.FrontendIPConfigurations = append(extLB.Properties.FrontendIPConfigurations, &armnetwork.FrontendIPConfiguration{ - Name: &in.frontendIPConfigName, + Name: to.Ptr(fmt.Sprintf("%s-v4", in.frontendIPConfigName)), Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), PublicIPAddress: pip, }, }) + if in.isDualstack { + extLB.Properties.FrontendIPConfigurations = append(extLB.Properties.FrontendIPConfigurations, + &armnetwork.FrontendIPConfiguration{ + Name: to.Ptr(fmt.Sprintf("%s-v6", in.frontendIPConfigName)), + Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PublicIPAddress: pipv6, + }, + }) + } extLB.Properties.BackendAddressPools = append(extLB.Properties.BackendAddressPools, &armnetwork.BackendAddressPool{ 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)), + }, + }, + }) + pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, in.resourceGroup, in.loadBalancerName, @@ -228,29 +277,8 @@ func updateOutboundLoadBalancerToAPILoadBalancer(ctx context.Context, pip *armne }, }, }, - 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, 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)), - }, - }, - }, - }, - OutboundRules: extLB.Properties.OutboundRules, + LoadBalancingRules: extLB.Properties.LoadBalancingRules, + OutboundRules: extLB.Properties.OutboundRules, }, Tags: in.tags, }, nil) @@ -316,13 +344,14 @@ 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) + 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) @@ -332,6 +361,57 @@ func updateInternalLoadBalancer(ctx context.Context, in *lbInput) (*armnetwork.L return &resp.LoadBalancer, nil } +// addIPv6InternalLBFrontend adds an IPv6 frontend IP configuration to the +// internal load balancer. +func addIPv6InternalLBFrontend(ctx context.Context, in *lbInput) error { + lbResp, err := in.lbClient.Get(ctx, in.resourceGroup, in.loadBalancerName, nil) + if err != nil { + return fmt.Errorf("could not get internal load balancer: %w", err) + } + intLB := lbResp.LoadBalancer + + existingFrontEndIPConfig := intLB.Properties.FrontendIPConfigurations + if len(existingFrontEndIPConfig) == 0 { + return fmt.Errorf("could not get frontEndIPConfig for internal LB %s", *intLB.Name) + } + + var subnetID string + if existingFrontEndIPConfig[0].Properties != nil && existingFrontEndIPConfig[0].Properties.Subnet != nil { + subnetID = *existingFrontEndIPConfig[0].Properties.Subnet.ID + } + + existingFrontEndIPConfigName := *(existingFrontEndIPConfig[0].Name) + frontendIPv6Name := fmt.Sprintf("%s-v6", existingFrontEndIPConfigName) + frontendIPv6 := &armnetwork.FrontendIPConfiguration{ + Name: to.Ptr(frontendIPv6Name), + Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PrivateIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv6), + }, + } + if subnetID != "" { + frontendIPv6.Properties.Subnet = &armnetwork.Subnet{ + ID: to.Ptr(subnetID), + } + } + intLB.Properties.FrontendIPConfigurations = append(intLB.Properties.FrontendIPConfigurations, frontendIPv6) + + pollerResp, err := in.lbClient.BeginCreateOrUpdate(ctx, + in.resourceGroup, + in.loadBalancerName, + intLB, + nil) + if err != nil { + return fmt.Errorf("cannot update internal load balancer with IPv6 frontend: %w", err) + } + + _, err = pollerResp.PollUntilDone(ctx, nil) + if err != nil { + return err + } + return nil +} + func associateVMToBackendPool(ctx context.Context, in vmInput) error { for _, id := range in.ids { vmName := path.Base(id) @@ -450,11 +530,30 @@ func associateInboundNatRuleToInterface(ctx context.Context, in *inboundNatRuleI } inboundNatRule := inboundNatRulesResp.InboundNatRule + // Determine if this is an IPv6 NAT rule by checking the frontend IP config name + isIPv6NatRule := strings.Contains(in.frontendIPConfigID, "-v6") + for i, ipConfig := range bootstrapInterface.Properties.IPConfigurations { + var ipVersion armnetwork.IPVersion + if ipConfig.Properties.PrivateIPAddressVersion != nil { + ipVersion = *ipConfig.Properties.PrivateIPAddressVersion + } else { + ipVersion = armnetwork.IPVersionIPv4 + } + + // Match NAT rule IP version to IP config version + isIPv6Config := ipVersion == armnetwork.IPVersionIPv6 + if isIPv6NatRule != isIPv6Config { + continue + } + + // Add NAT rule to the first matching IP config and break + // (A NAT rule can only be attached to one IP config) ipConfig.Properties.LoadBalancerInboundNatRules = append(ipConfig.Properties.LoadBalancerInboundNatRules, &inboundNatRule, ) bootstrapInterface.Properties.IPConfigurations[i] = ipConfig + break } interfacesPollerResp, err := interfacesClient.BeginCreateOrUpdate(ctx, @@ -547,3 +646,31 @@ func associateNatGatewayToSubnet(ctx context.Context, in natGatewayInput) error } return nil } + +func updateOutboundIPv6LoadBalancer(ctx context.Context, pipv6 *armnetwork.PublicIPAddress, lbClient *armnetwork.LoadBalancersClient, resourceGroup, loadBalancerName, infraID string) error { + outboundIPv6LB, err := lbClient.Get(ctx, resourceGroup, loadBalancerName, nil) + if err != nil { + return fmt.Errorf("failed to get external load balancer: %w", err) + } + + loadBalancer := outboundIPv6LB.LoadBalancer + loadBalancer.Properties.FrontendIPConfigurations = append(loadBalancer.Properties.FrontendIPConfigurations, &armnetwork.FrontendIPConfiguration{ + Name: to.Ptr(fmt.Sprintf("%s-frontend-ipv6", infraID)), + Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PublicIPAddress: pipv6, + }, + }) + + pollerResp, err := lbClient.BeginCreateOrUpdate(ctx, + resourceGroup, + loadBalancerName, + loadBalancer, nil) + + if err != nil { + return fmt.Errorf("cannot update outbound node ipv6 load balancer: %w", err) + } + + _, err = pollerResp.PollUntilDone(ctx, nil) + return err +}