Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pkg/asset/manifests/azure/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
75 changes: 70 additions & 5 deletions pkg/infrastructure/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type Provider struct {
clientOptions *arm.ClientOptions
computeClientOptions *arm.ClientOptions
publicLBIP string
publicLBIPv6 string
}

var _ clusterapi.InfraReadyProvider = (*Provider)(nil)
Expand Down Expand Up @@ -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)
Expand All @@ -433,30 +435,47 @@ 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,
region: in.InstallConfig.Config.Azure.Region,
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)
}
Expand All @@ -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 ||
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Comment on lines +622 to +645
Copy link

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This block never creates the IPv6 SSH forward it describes.

It allocates a public IPv6 and patches the outbound node LB, but it never calls addInboundNatRuleToLoadBalancer or associateInboundNatRuleToInterface. Dual-stack still only gets the IPv4 *_ssh_in path.

💡 Suggested change
 		// For dual-stack, create IPv6 inbound NAT rule for SSH access to bootstrap.
 		if in.InstallConfig.Config.Azure.IPFamily.DualStackEnabled() {
+			frontendIPv6ConfigID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s",
+				subscriptionID,
+				p.ResourceGroupName,
+				loadBalancerName,
+				"public-lb-ip-v6",
+			)
+			sshRuleNameV6 := fmt.Sprintf("%s_ssh_in_v6", in.InfraID)
+			inboundNatRuleV6, err := addInboundNatRuleToLoadBalancer(ctx, &inboundNatRuleInput{
+				resourceGroupName:    p.ResourceGroupName,
+				loadBalancerName:     loadBalancerName,
+				frontendIPConfigID:   frontendIPv6ConfigID,
+				inboundNatRuleName:   sshRuleNameV6,
+				inboundNatRulePort:   22,
+				networkClientFactory: p.NetworkClientFactory,
+			})
+			if err != nil {
+				return fmt.Errorf("failed to create inbound ipv6 nat rule: %w", err)
+			}
+			_, err = associateInboundNatRuleToInterface(ctx, &inboundNatRuleInput{
+				resourceGroupName:    p.ResourceGroupName,
+				loadBalancerName:     loadBalancerName,
+				bootstrapNicName:     fmt.Sprintf("%s-bootstrap-nic", in.InfraID),
+				frontendIPConfigID:   frontendIPv6ConfigID,
+				inboundNatRuleID:     *inboundNatRuleV6.ID,
+				inboundNatRuleName:   sshRuleNameV6,
+				inboundNatRulePort:   22,
+				networkClientFactory: p.NetworkClientFactory,
+			})
+			if err != nil {
+				return fmt.Errorf("failed to associate inbound ipv6 nat rule to interface: %w", err)
+			}
+
 			publicIPv6outbound, err := createPublicIP(ctx, &pipInput{
 				name:          fmt.Sprintf("%s-pip-v6-outbound-lb", in.InfraID),
 				infraID:       in.InfraID,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/infrastructure/azure/azure.go` around lines 622 - 645, The code creates a
public IPv6 and updates the outbound IPv6 LB but never provisions the IPv6 SSH
inbound NAT rule; after updateOutboundIPv6LoadBalancer(...) use
addInboundNatRuleToLoadBalancer(...) to create the SSH inbound NAT rule on
outboundLBName using publicIPv6outbound (pass the load balancer client from
p.NetworkClientFactory.NewLoadBalancersClient(), the resource group and
infraID), then call associateInboundNatRuleToInterface(...) (using
p.NetworkClientFactory.NewInterfacesClient(), the created NAT rule name,
resource group and the relevant interface(s)/VMSS NICs for bootstrap) so the
IPv6 SSH port is actually forwarded (refer to addInboundNatRuleToLoadBalancer
and associateInboundNatRuleToInterface for exact parameter ordering and naming).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

called in a later PR

Copy link

Choose a reason for hiding this comment

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

@rna-afk, understood — thanks for the heads-up! Would you like me to open a follow-up GitHub issue to track the missing IPv6 SSH inbound NAT rule creation and association for the dual-stack bootstrap path, so it isn't lost before that later PR lands?

}

return nil
Expand Down
Loading