Contents [show]
As a frequent user of virtualisation, particularly with regards to Windows Server, I have come across a problem that frequently troubles many: Hidden or Ghost Network Adapters.
Sometimes, after a VM is deallocated from the host and then reallocated, the Guest OS recognizes the NIC as a new device and installs a new driver for it.
Problems
These installations remain after any number of reboots. Even though they are marked as inactive some of them can remain active (probably through some corrupt registry setting) and can cause a bevy of errors including:
- “The IP address x.x.x.x you have entered for this network adapter is already assigned to another adapter.”
- Inability to use Fileshares.
- Problems authenticating with a Domain Controller.
- Failure for 2 VMs to communicate with each other.
- …
This problem is particularly common for machines hosted in the cloud, for example on AWS or Azure.
Solutions
There is a host of articles out there explaining how you can use the device manager to show these hidden adapters and remove them. However, this is a manual approach that can take a long time. In 9 Azure Machines I had 100 Ghost Network Adapters each, imagine the amount of time and frustration this would require. This approach also doesn’t work for Windows Server 2008 (R2).
PowerShell and devcon.exe solution
This led me to articles explaining how devcon.exe, part of the Windows SDK, could be used to find and uninstall these adapters.
Here is the script I used to automatically remove ghost network adapters using devcon.exe:
param( | |
# Path to devcon.exe | |
[string]$devconPath = "C:\Program Files (x86)\Windows Kits\8.0\Tools\x64\devcon.exe" | |
) | |
<# | |
.Synopsis | |
Helper function to seperate devcon output between device id and name | |
#> | |
function Convert-ToDevice { | |
[CmdletBinding()] | |
[OutputType([PSObject])] | |
Param | |
( | |
[Parameter(Mandatory=$true, | |
ValueFromPipelineByPropertyName=$true, | |
Position=0)] | |
[string]$DeviceString | |
) | |
Begin { | |
} | |
Process { | |
#PCI\VEN_1414&DEV_5353&SUBSYS_00000000&REV_00\3&267A616A&1&40: Microsoft Hyper-V S3 Cap | |
#[id]: [name] | |
$split = $DeviceString.Split(":"); | |
$id=$null | |
$name = $null | |
if ($split.Count -gt 1) { | |
$name = $split[1].TrimStart(" ") | |
$id = $split[0] | |
#prefix @ to id, needed by remove | |
$id = "@" + $id | |
} | |
return $obj = New-Object PSObject -Property @{ | |
Name = $name | |
Id = $id | |
} | |
} | |
End { | |
} | |
} | |
# Get the standard Hyper-V Network Adapter name for the current Guest OS. | |
$version = [System.Environment]::OSVersion.Version | |
if($version.Major -eq 6) | |
{ | |
switch ($version.Minor) | |
{ | |
#2008R2 | |
1 {$adapterName = "Microsoft Virtual Machine Bus Network Adapter "} | |
#2012 / 2012R2 | |
{($_ -eq 2) -or ($_ -eq 3)} {$adapterName = "Microsoft Hyper-V Network Adapter "} | |
Default { | |
Write-Host "Unknown Windows Minor Version $($version.Minor). Major Version: $($version.Major). Known minor versions are: 1, 2, 3" -ForegroundColor Red | |
return 666 | |
} | |
} | |
} elseif($version.Major -eq 10) { | |
#Server 2016 | |
switch ($version.Minor) | |
{ | |
0 {$adapterName = "Microsoft Hyper-V Network Adapter "} | |
Default { | |
Write-Host "Unknown Windows Minor Version $($version.Minor). Major Version: $($version.Major). Known minor versions are: 0" -ForegroundColor Red | |
return 666 | |
} | |
} | |
} else { | |
Write-Host "Unknown Windows Major Version $($version.Major). Known major versions are: 6, 10" -ForegroundColor Red | |
return 666 | |
} | |
# You don't want to uninstall an active adapter. | |
[array]$activeAdapterNames = Get-NetAdapter | Select-Object -ExpandProperty "InterfaceDescription" | Where-Object { $_.StartsWith($adapterName) } | |
# Array of format: | |
# PCI\VEN_1414&DEV_5353&SUBSYS_00000000&REV_00\3&267A616A&1&40: Microsoft Hyper-V S3 Cap | |
$devices = & $devconPath findall * | |
foreach ($deviceString in $devices) { | |
$device = Convert-ToDevice $deviceString | |
if ($device.Name -ne $null -and $device.Name.StartsWith($adapterName) -and (! $activeAdapterNames.Contains($device.Name))) { | |
Write-Host "Removing adapter $($device.Name)..." | |
& $devconPath remove "$($device.Id)" | |
} | |
} |
The script uses the friendly names and the patterns associated with Hyper-V Network Adapter names, depending on the OS version, to identify Ghost Network Adapters.
This approach still has some caveats though:
- It requires seperately downloading the SDK at least once (It has over 2 GB).
- Using this script requires that you ensure that devcon.exe has been added to the computer. In certain environments where Internet connectivity is not granted this can be tough.
- It is unclear whether devcon.exe from one Windows OS SDK is truly compatible with other OS Versions.
- Due to the licensing terms I cannot share the exe (which only has ~250KB) with you directly ie. provide a link.
In terms of DevOps and provisioning machines quickly and reliably with scheduled tasks that run at boot this is not an ideal solution.
Pure PowerShell solution
I therefore wrote a script that works completely without external applications or scripts. It relies heavily on PInvoke and the Windows Setup API, which, I believe, devcon.exe also uses.
As a bonus, in the implementation of ForEach-Device
you can see how to implement your own function that accepts a lambda type expression like Where-Object or ForEach-Object.
Enjoy
$T = @" | |
using System; | |
using System.Diagnostics; | |
using System.Runtime.InteropServices; | |
namespace Win32 | |
{ | |
public static class SetupApi | |
{ | |
// 1st form using a ClassGUID only, with Enumerator = IntPtr.Zero | |
[DllImport("setupapi.dll", CharSet = CharSet.Auto)] | |
public static extern IntPtr SetupDiGetClassDevs( | |
ref Guid ClassGuid, | |
IntPtr Enumerator, | |
IntPtr hwndParent, | |
int Flags | |
); | |
// 2nd form uses an Enumerator only, with ClassGUID = IntPtr.Zero | |
[DllImport("setupapi.dll", CharSet = CharSet.Auto)] | |
public static extern IntPtr SetupDiGetClassDevs( | |
IntPtr ClassGuid, | |
string Enumerator, | |
IntPtr hwndParent, | |
int Flags | |
); | |
[DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
public static extern bool SetupDiEnumDeviceInfo( | |
IntPtr DeviceInfoSet, | |
uint MemberIndex, | |
ref SP_DEVINFO_DATA DeviceInfoData | |
); | |
[DllImport("setupapi.dll", SetLastError = true)] | |
public static extern bool SetupDiDestroyDeviceInfoList( | |
IntPtr DeviceInfoSet | |
); | |
[DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
public static extern bool SetupDiGetDeviceRegistryProperty( | |
IntPtr deviceInfoSet, | |
ref SP_DEVINFO_DATA deviceInfoData, | |
uint property, | |
out UInt32 propertyRegDataType, | |
byte[] propertyBuffer, | |
uint propertyBufferSize, | |
out UInt32 requiredSize | |
); | |
[DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
public static extern bool SetupDiRemoveDevice(IntPtr DeviceInfoSet,ref SP_DEVINFO_DATA DeviceInfoData); | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
public struct SP_DEVINFO_DATA | |
{ | |
public uint cbSize; | |
public Guid classGuid; | |
public uint devInst; | |
public IntPtr reserved; | |
} | |
[Flags] | |
public enum DiGetClassFlags : uint | |
{ | |
DIGCF_DEFAULT = 0x00000001, // only valid with DIGCF_DEVICEINTERFACE | |
DIGCF_PRESENT = 0x00000002, | |
DIGCF_ALLCLASSES = 0x00000004, | |
DIGCF_PROFILE = 0x00000008, | |
DIGCF_DEVICEINTERFACE = 0x00000010, | |
} | |
public enum SetupDiGetDeviceRegistryPropertyEnum : uint | |
{ | |
SPDRP_DEVICEDESC = 0x00000000, // DeviceDesc (R/W) | |
SPDRP_HARDWAREID = 0x00000001, // HardwareID (R/W) | |
SPDRP_COMPATIBLEIDS = 0x00000002, // CompatibleIDs (R/W) | |
SPDRP_UNUSED0 = 0x00000003, // unused | |
SPDRP_SERVICE = 0x00000004, // Service (R/W) | |
SPDRP_UNUSED1 = 0x00000005, // unused | |
SPDRP_UNUSED2 = 0x00000006, // unused | |
SPDRP_CLASS = 0x00000007, // Class (R--tied to ClassGUID) | |
SPDRP_CLASSGUID = 0x00000008, // ClassGUID (R/W) | |
SPDRP_DRIVER = 0x00000009, // Driver (R/W) | |
SPDRP_CONFIGFLAGS = 0x0000000A, // ConfigFlags (R/W) | |
SPDRP_MFG = 0x0000000B, // Mfg (R/W) | |
SPDRP_FRIENDLYNAME = 0x0000000C, // FriendlyName (R/W) | |
SPDRP_LOCATION_INFORMATION = 0x0000000D, // LocationInformation (R/W) | |
SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0x0000000E, // PhysicalDeviceObjectName (R) | |
SPDRP_CAPABILITIES = 0x0000000F, // Capabilities (R) | |
SPDRP_UI_NUMBER = 0x00000010, // UiNumber (R) | |
SPDRP_UPPERFILTERS = 0x00000011, // UpperFilters (R/W) | |
SPDRP_LOWERFILTERS = 0x00000012, // LowerFilters (R/W) | |
SPDRP_BUSTYPEGUID = 0x00000013, // BusTypeGUID (R) | |
SPDRP_LEGACYBUSTYPE = 0x00000014, // LegacyBusType (R) | |
SPDRP_BUSNUMBER = 0x00000015, // BusNumber (R) | |
SPDRP_ENUMERATOR_NAME = 0x00000016, // Enumerator Name (R) | |
SPDRP_SECURITY = 0x00000017, // Security (R/W, binary form) | |
SPDRP_SECURITY_SDS = 0x00000018, // Security (W, SDS form) | |
SPDRP_DEVTYPE = 0x00000019, // Device Type (R/W) | |
SPDRP_EXCLUSIVE = 0x0000001A, // Device is exclusive-access (R/W) | |
SPDRP_CHARACTERISTICS = 0x0000001B, // Device Characteristics (R/W) | |
SPDRP_ADDRESS = 0x0000001C, // Device Address (R) | |
SPDRP_UI_NUMBER_DESC_FORMAT = 0X0000001D, // UiNumberDescFormat (R/W) | |
SPDRP_DEVICE_POWER_DATA = 0x0000001E, // Device Power Data (R) | |
SPDRP_REMOVAL_POLICY = 0x0000001F, // Removal Policy (R) | |
SPDRP_REMOVAL_POLICY_HW_DEFAULT = 0x00000020, // Hardware Removal Policy (R) | |
SPDRP_REMOVAL_POLICY_OVERRIDE = 0x00000021, // Removal Policy Override (RW) | |
SPDRP_INSTALL_STATE = 0x00000022, // Device Install State (R) | |
SPDRP_LOCATION_PATHS = 0x00000023, // Device Location Paths (R) | |
SPDRP_BASE_CONTAINERID = 0x00000024 // Base ContainerID (R) | |
} | |
} | |
"@ | |
Add-Type -TypeDefinition $T | |
<# | |
.Synopsis | |
Executes a Process for each Device | |
.EXAMPLE | |
# Print all friendly names | |
ForEach-Device { param($friendlyName, $devices, $deviceInfo) Write-Host $friendlyName } | |
#> | |
function ForEach-Device | |
{ | |
[CmdletBinding()] | |
Param | |
( | |
#The Process to be invoked for each friendly Name. params([string] $friendlyName, $devs = devices, $devInfo = deviceInfo | |
[Parameter(Mandatory=$true)] | |
[ScriptBlock] | |
$Process | |
) | |
$setupClass = [Guid]::Empty | |
#Get all devices | |
$devs = [Win32.SetupApi]::SetupDiGetClassDevs([ref]$setupClass, [IntPtr]::Zero, [IntPtr]::Zero, [Win32.DiGetClassFlags]::DIGCF_ALLCLASSES) | |
#Initialise Struct to hold device info Data | |
$devInfo = new-object Win32.SP_DEVINFO_DATA | |
$devInfo.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($devInfo) | |
#Device Counter | |
$devCount = 0 | |
#Enumerate Devices | |
while([Win32.SetupApi]::SetupDiEnumDeviceInfo($devs, $devCount, [ref]$devInfo)){ | |
# Will contain an enum depending on the type of the registry Property, not used but required for call | |
$propType = 0 | |
# Buffer is initially null and buffer size 0 so that we can get the required Buffer size first | |
[byte[]]$propBuffer = $null | |
$propBufferSize = 0 | |
# Get Buffer size | |
[Win32.SetupApi]::SetupDiGetDeviceRegistryProperty($devs, [ref]$devInfo, [Win32.SetupDiGetDeviceRegistryPropertyEnum]::SPDRP_FRIENDLYNAME, [ref]$propType, $propBuffer, 0, [ref]$propBufferSize) | Out-null | |
# Initialize Buffer with right size | |
[byte[]]$propBuffer = [byte[]]::new($propBufferSize) | |
# Read FriendlyName property into Buffer | |
if(![Win32.SetupApi]::SetupDiGetDeviceRegistryProperty($devs, [ref]$devInfo,[Win32.SetupDiGetDeviceRegistryPropertyEnum]::SPDRP_FRIENDLYNAME, [ref]$propType, $propBuffer, $propBufferSize, [ref]$propBufferSize)){ | |
# Ignore if Error | |
} else { | |
# Get Unicode String from Buffer | |
$FriendlyName = [System.Text.Encoding]::Unicode.GetString($propBuffer) | |
# The friendly Name ends with a weird character | |
$FriendlyName = $FriendlyName.Substring(0,$FriendlyName.Length-1) | |
$Process.Invoke($FriendlyName, $devices, $devInfo) | |
} | |
$devCount++ | |
} | |
[Win32.SetupApi]::SetupDiDestroyDeviceInfoList($devs) | Out-Null | |
} | |
<# | |
.Synopsis | |
Get the friendly name of all installed devices | |
#> | |
function Get-DeviceFriendlyNames | |
{ | |
[CmdletBinding()] | |
[OutputType([string[]])] | |
param() | |
return ForEach-Device {param($friendlyName) $friendlyName} | |
} | |
<# | |
.Synopsis | |
Short description | |
.DESCRIPTION | |
Long description | |
.EXAMPLE | |
Example of how to use this cmdlet | |
.EXAMPLE | |
Another example of how to use this cmdlet | |
#> | |
function Remove-Devices | |
{ | |
[CmdletBinding()] | |
[Alias()] | |
[OutputType()] | |
Param | |
( | |
# Param1 help description | |
[Parameter(Mandatory=$true, | |
ValueFromPipelineByPropertyName=$true, | |
Position=0)] | |
[string[]]$FriendlyNames | |
) | |
if($FriendlyNames -eq $null -or $FriendlyNames.Count -eq 0) { | |
return | |
} | |
$count = 0; | |
ForEach-Device { | |
param([string]$FriendlyName, $devs, $devInfo) | |
if($FriendlyNames.Contains($FriendlyName)){ | |
if([Win32.SetupApi]::SetupDiRemoveDevice($devs, [ref]$devInfo)){ | |
$count++ | |
} else { | |
Write-Verbose "Failed to Remove device $FriendlyName" | |
} | |
} | |
} | |
Write-Verbose "Successfully Removed $count Devices" | |
} | |
function Remove-GhostNetworkAdapters | |
{ | |
$version = [System.Environment]::OSVersion.Version | |
if($version.Major -eq 6) | |
{ | |
switch ($version.Minor) | |
{ | |
#2008R2 | |
1 {$adapterName = "Microsoft Virtual Machine Bus Network Adapter "} | |
#2012 / 2012R2 | |
{($_ -eq 2) -or ($_ -eq 3)} {$adapterName = "Microsoft Hyper-V Network Adapter "} | |
Default { | |
Write-Host "Unknown Windows Minor Version $($version.Minor). Major Version: $($version.Major). Known minor versions are: 1, 2, 3" -ForegroundColor Red | |
return 666 | |
} | |
} | |
} elseif($version.Major -eq 10) { | |
#Server 2016 | |
switch ($version.Minor) | |
{ | |
0 {$adapterName = "Microsoft Hyper-V Network Adapter "} | |
Default { | |
Write-Host "Unknown Windows Minor Version $($version.Minor). Major Version: $($version.Major). Known minor versions are: 0" -ForegroundColor Red | |
return 666 | |
} | |
} | |
} else { | |
Write-Host "Unknown Windows Major Version $($version.Major). Known major versions are: 6, 10" -ForegroundColor Red | |
return 666 | |
} | |
$activeAdapters = Get-NetAdapter | ? {$_.InterfaceDescription.StartsWith($adapterName)} | select -ExpandProperty InterfaceDescription | |
Get-DeviceFriendlyNames | ? { $_.Name.StartsWith($adapterName) -and !$activeAdapters.Contains($_)}| Remove-Devices | |
} | |
Remove-GhostNetworkAdapters |
You must log in to post a comment.