Created on
Updated on

Workaround for network bug in Azure Local 23H2


Intro

I wanted to share my recent work with a customer that have multiple Azure Local 23H2 stacks that have been upgraded from 22H2. We see an issue once we start installing NetworkATC feature. Uninstall of NetworkATC does not remove the issue sadly.

The issue is that every time a node is rebooted (and also sometime randomly then running), ALL VMSwitches on the node, switches from external to internal. This is because the bindings to the physical NICs are lost. Event logs tells us that “Physical network adapter not found” and “Physical network adapter disconnected”.

We see this issue both with Intel based network adapter cards and Melanox. Firmware and drivers are updated. Operation system on the stack nodes are also updated to newest version.

We have been running a few cases with Microsoft on this subject and so far we are still waiting for a solution for this issue. In the mean time the customer needed to ensure they could remediate the issue every time they needed to reboot a node or when it happened while node was running. I then decided it was better to write a complete script rather than letting the customer call me up every time they needed it fixed.

I wanted to share this script because I see this issue as production critical and can potentially cause downtime for workloads running on an affected stack. They customer deployed this script as a startup script via Scheduled Task. We could also choose to deploy it as a scheduled task that runs as often as possible.

Lets hope Microsoft will soon find a permanent fix for the issue – fingers crossed 🙂

UPDATE January 2026: still no permanent fix from Microsoft

Scripts

Update version of script - converged intent

This script version is written in January 2026 and supports one converged compute and management intent. It also supports running all the time in a while loop. To implement this script as a scheduled task on nodes, refer to my other guide about creating scheduled task on Azure Local node

<#
.SYNOPSIS
    Automated network self-heal loop for converged networking on Azure Local/Hyper-V
.DESCRIPTION
    This script is an automated “network self-heal” loop for converged networking on Azure Local/Hyper‑V. 
    When the expected converged vSwitch is missing or becomes an Internal switch, it safely disconnects VM network adapters,
    recreates the converged vSwitch on the specified physical adapters with embedded teaming and explicit load-balancing/teaming settings,
    renames the management vNIC to a consistent name (handling common naming variants), reconnects VM adapters to the restored switch,
    and reapplies the management vNIC’s IP/DNS configuration. It repeats every few seconds to quickly restore connectivity
    after reboots or unexpected switch state changes.
.NOTES
    Written by: Christoffer Klarskov Jakobsen
    Created: 2025-07-04
    Updated: 2026-01-16
    Version: 1.1
#>

# Parameters
$AdapterNames = @("X710pNic1", "X710pNic2")  # Physical adapters to use for the converged switch
$adapterPossibleNames = @(
    "vEthernet (vManagement(compute_management))",
    "vEthernet (ConvergedSwitch(compute_management))",
    "vEthernet (ConvergedSwitch(compute_management))",
    "vEthernet (vManagement(compute_management)"
)
$adapterVMSwitchPossibleNames = @(
    "vManagement(compute_management)",
    "vManagement(compute_management",
    "ConvergedSwitch(compute_management)"
)
$IPAddress = "10.0.0.11"
$IPPrefixLength = 24
$DefaultGateway = "10.0.0.1"
$DNSServers = @("10.10.10.10", "10.11.11.11")
$VMSwitchName = "ConvergedSwitch(compute_management)"
$VMSwitchAdapterName = "vManagement(compute_management)"

While ($true) {
    Write-Host "#[Debug]: Get-VMSwitch"
    $VMSwitch = Get-VMSwitch -Name $VMSwitchName -ErrorAction SilentlyContinue

    if ($null -eq $VMSwitch -or $VMSwitch.SwitchType -eq "Internal") {
        Write-Host "#[Debug]: Get all connected VMs"
        # Get All VMs beside MOC, because MOC runs on management intent - remove comment on next line to make active - only needed for stacks with separated management and compute intents
        # $VMs = Get-VM | Where-Object { $_.Name -notlike "*control-plan*" }
        $VMs = Get-VM
        $VMs | % { Get-VMNetworkAdapter -VMName $_.name | Disconnect-VMNetworkAdapter }

        Write-Host "#[Debug]: Removing VM-Switch"
        # Remove existing switch only if internal or multiple switches exists with same name
        if ($VMSwitch.SwitchType -eq "Internal" -or $VMSwitch.Count -gt 1) {
            Remove-VMSwitch -Name $VMSwitchName -Force
        }

        Start-Sleep -Seconds 3

        Write-Host "#[Debug]: Creating new VMSwitch"
        New-VMSwitch -Name $VMSwitchName -NetAdapterName $AdapterNames -EnableEmbeddedTeaming $true
        Set-VMSwitchTeam -Name $VMSwitchName -LoadBalancingAlgorithm HyperVPort
        Set-VMSwitchTeam -Name $VMSwitchName -TeamingMode SwitchIndependent

        Start-Sleep -Seconds 3

        Write-Host "#[Debug]: Setting correct name for VMSwitch"

        # Node can call the adapter many things, and try/catch is not working well, therefore we currently try all known names without error handling
        foreach ($adapterName in $adapterPossibleName) {
            Rename-NetAdapter -Name $adapterName -NewName $VMSwitchAdapterName -ErrorAction SilentlyContinue
        }

        Write-Host "#[Debug]: Attach network adapter on all virtual machines"
        $VMSW = Get-VMSwitch -Name $VMSwitchName
        $VMs | % { Get-VMNetworkAdapter -VMName $_.name | Connect-VMNetworkAdapter -SwitchName $VMSW.name }

        $NetAdapter = Get-NetAdapter -Name $VMSwitchAdapterName

        write-host "#[Debug]: Set IP address"
        # Remove existing IP addresses
        Get-NetIPAddress -InterfaceIndex $NetAdapter.ifIndex | Remove-NetIpAddress -Confirm:$false

        # Try/Catch not working to well, therefore we try to set the IP with and without gateway because this is a known issue
        New-NetIPAddress -InterfaceIndex $NetAdapter.ifIndex -IPAddress $IPAddress -PrefixLength $IPPrefixLength
        New-NetIPAddress -InterfaceIndex $NetAdapter.ifIndex -IPAddress $IPAddress -PrefixLength $IPPrefixLength -DefaultGateway $DefaultGateway
        Set-DnsClientServerAddress -InterfaceIndex $NetAdapter.ifIndex -ServerAddresses $DNSServers

        Write-Host "#[Debug]: Rename VMNetworkAdapter"
        # Node can call the adapter many things, and try/catch is not working well, therefore we currently try all known names without error handling
        Foreach ($adapterName in $adapterVMSwitchPossibleNames) {
        Rename-VMNetworkAdapter -Name $adapterName -NewName $VMSwitchAdapterName -ManagementOS
        }
    }

    # Wait before next check
    Start-Sleep 5
} 

Old version of script - 2 separate intents

This script version is written in July 2025 and supports separate compute and management intent, but was not written to run all the time.

# This script is used to recreate Hyper-V virtual switches for a converged network setup.
# Script is build to address a current bug in Azure Local 23H2 that has been upgraded from 22H2.
# The bug causes the Hyper-V virtual switches to loose its binding to the physical adapters after a reboot.
# It is designed to use VMSwitches for all network types: storage, compute, and management.
# Storage VMSwitch will be using multiple adapters running without VLANs, this would change once we migrate to Network ATC.
# It checks for existing switches and removes them if they are of type "Internal" or not found.
# It then creates new virtual switches with specific configurations for storage, compute, and management networks.

# Storage Switch - change parameter values as needed
$VMSwitch = $null
$VMSwitchName = "ConvergedSwitch(Storage)"
$AdapterNames = '"storage01","storage02"'
$IPAddress = "10.71.1.11"
$IPPrefixLength = 24

$VMSwitch = Get-VMSwitch -Name $VMSwitchName -ErrorAction SilentlyContinue

if($null -eq $VMSwitch -or $VMSwitch.SwitchType -eq "Internal") 
{
    # Remove existing switch if it exists
    if ($VMSwitch.SwitchType -eq "Internal")
    {
        Remove-VMSwitch -Name $VMSwitchName -Force
        Start-Sleep -Seconds 5
    }

    $VMSwitch = New-VMSwitch -Name $VMSwitchName -NetAdapterName $AdapterNames -EnableEmbeddedTeaming $true
    Set-VMSwitchTeam -Name $VMSwitch.Name -LoadBalancingAlgorithm HyperVPort
    Set-VMSwitchTeam -Name $VMSwitch.Name -TeamingMode SwitchIndependent
    # Set Jumbo Packet size for the adapters - asumees the adapters support it
    Set-NetAdapterAdvancedProperty -Name $AdapterNames -DisplayName "Jumbo Packet" -DisplayValue "9014"

    $NetAdapter = Get-NetAdapter -Name "vEthernet ($VMSwitchName)"

    New-NetIPAddress -InterfaceIndex $NetAdapter.ifIndex -IPAddress $IPAddress -PrefixLength $IPPrefixLength
}

# Compute Switch - change parameter values as needed
$VMSwitch = $null
$VMSwitchName = "ConvergedSwitch(Compute)"
$AdapterNames = '"compute01","compute02"'

$VMSwitch = Get-VMSwitch -Name $VMSwitchName -ErrorAction SilentlyContinue

if($null -eq $VMSwitch -or $VMSwitch.SwitchType -eq "Internal") {
    
    # Need to disconnect all VMs from the old switch before creating a new one
    # Replace name in the -ne with the name of the control plane VM running on your stack
    $VMs = Get-VM | Where-Object { $_.Name -ne "a61f1c05bc707c1df503e894463b49c8abbe1ce57a296-control-plane-0-827ef337" }
    $VMs | ForEach-Object{Get-VMNetworkAdapter -VMName $_.name | Disconnect-VMNetworkAdapter}

    # Remove existing switch if it exists
    if ($VMSwitch.SwitchType -eq "Internal")
    {
        Remove-VMSwitch -Name $VMSwitchName -Force
        Start-Sleep -Seconds 5
    }

    $VMSwitch = New-VMSwitch -Name $VMSwitchName -NetAdapterName $AdapterNames -EnableEmbeddedTeaming $true
    Set-VMSwitchTeam -Name $VMSwitch.Name -LoadBalancingAlgorithm HyperVPort
    Set-VMSwitchTeam -Name $VMSwitch.Name -TeamingMode SwitchIndependent

    # Connect all VMs to the new switch
    $VMSW = Get-VMSwitch -Name $VMSwitchName
    $VMs | ForEach-Object{Get-VMNetworkAdapter -VMName $_.name | Connect-VMNetworkAdapter -SwitchName $VMSW.name}
}

# Management Switch - change parameter values as needed
$VMSwitch = $null
$VMSwitchName = "ConvergedSwitch(Management)"
$AdapterNames = '"mgmt01","mgmt02"'
$IPAddress = "10.10.0.11"
$IPDefaultGateway = "10.10.0.1"
$IPPrefixLength = 24
$IPDNSServers = '"10.20.0.11","10.20.0.12"'

$VMSwitch = Get-VMSwitch -Name $VMSwitchName -ErrorAction SilentlyContinue

if($null -eq $VMSwitch -or $VMSwitch.SwitchType -eq "Internal") {

    # Need to disconnect control plane VM from the old switch before creating a new one
    # Replace name in the -eq with the name of the control plane VM running on your stack
    $VMs = Get-VM | Where-Object { $_.Name -eq "a61f1c05bc707c1df503e894463b49c8abbe1ce57a296-control-plane-0-827ef337" }
    $VMs | ForEach-Object{Get-VMNetworkAdapter -VMName $_.name | Disconnect-VMNetworkAdapter}
    
    # Remove existing switch if it exists
    if ($VMSwitch.SwitchType -eq "Internal")
    {
        Remove-VMSwitch -Name $VMSwitchName -Force
        Start-Sleep -Seconds 5
    }

    $VMSwitch = New-VMSwitch -Name $VMSwitchName -NetAdapterName $AdapterNames -EnableEmbeddedTeaming $true
    Set-VMSwitchTeam -Name $VMSwitch.Name -LoadBalancingAlgorithm HyperVPort
    Set-VMSwitchTeam -Name $VMSwitch.Name -TeamingMode SwitchIndependent

    $NetAdapter = Get-NetAdapter -Name "vEthernet ($VMSwitchName)"

    New-NetIPAddress -InterfaceIndex $NetAdapter.ifIndex -IPAddress $IPAddress -PrefixLength $IPPrefixLength -DefaultGateway $IPDefaultGateway
    Set-DnsClientServerAddress -InterfaceIndex $NetAdapter.ifIndex -ServerAddresses ($IPDNSServers)

    # Connect all control plane VMs to the new switch
    $VMSW = Get-VMSwitch -Name $VMSwitchName
    $VMs | ForEach-Object{Get-VMNetworkAdapter -VMName $_.name | Connect-VMNetworkAdapter -SwitchName $VMSW.name}
}