Ghost Network Adapters

Ghost Network Adapters


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

Alexander Jebens

Alexander Jebens is a freelance consultant, software architect and engineer based in Vienna, Austria. He facilitates and drives cloud transition and adoption within organisations while ensuring performance, consistency, governance and cost control. He supports development projects in choosing their stack, securing their solution and adopting DevOps. When not conducting workshops or hacking away at his keyboard, he enjoys running and ski touring while he ponders upon the mysteries of life, the universe and everything.