Save to My DOJO
All physical network cards ship with a hard-coded unique identifier known as a media access control (MAC) address. These 48-bit identifiers help to ensure that Ethernet traffic finds its intended destination. If you want more information on that, follow our networking series. Here, I want to talk about the virtual MAC addresses that Hyper-V’s virtual network adapters use. I’ve got a script that can help you to quickly identify problems with MAC addresses in Hyper-V.
An Overview of Hyper-V MACs
Virtualization allows us to “fake” just about anything computer-related. Since virtual network adapters participate on Ethernet networks, they get “faked” MAC addresses. Each Hyper-V host generates a default pool. They all use the same first three octets: 00-15-5D, as Microsoft owns that prefix. The host generates the next two octets using an algorithm on its own physical hardware IDs. When virtual adapters start for the first time https://github.com/ejsiron/Posher-V/blob/master/Docs/Get-VMMacConflict.md and have no MAC, the host assigns them the next available number in the final octet.
Unfortunately, only having two octets available for uniqueness can quickly lead to duplicates. Reloaded systems will start over at 0 and might generate an octet set already in use. Virtual machines can move, so without some sort of external arbitration system, you can easily wind up with MAC collisions. Working only with symptoms, you might not even realize you have MAC problems at first. Detecting duplicate MACs can be a difficult process, especially when you don’t have tools built for the job.
Native Tools to Detect MAC Addresses
Hyper-V does not include any tool just for MAC duplicate detection. It does allow you to easily view all of the MACs on a host.
PowerShell code to see the virtual machine MACs:
Get-VMNetworkAdapter -VMName *
It will display all virtual adapters, their virtual machines, their MAC addresses, and some other information:
That only shows virtual machine information, though. For any virtual adapters that belong to the management operating system, use this:
Get-VMNetworkAdapter -ManagementOS
That won’t quite do it, though. You need one more cmdlet to see information for the host’s unbound physical adapters:
Get-NetAdapter
Now you have enough information to figure out if you have locally-conflicting addresses. You’ll have to figure out a way to collate and compare them all, though. And then you’d have to do it across all of your hosts. So, I did that for you.
Script Notes
A couple of points to note:
- This is the final version of this script that I will post on this article. Any future updates will appear on its Github page.
- This is my first script that uses the CIM cmdlets exclusively. I still use Windows PowerShell because it works perfectly well for me, but I believe that this script will also work with PowerShell Core.
- Because I use CIM and not any add-in cmdlet packs (e.g. Hyper-V or VMM), this script will run on any PowerShell system. It can target remote systems. I tested against 2012 R2 and 2016 (simultaneously, even). It might work on 2012.
- I built the script to look for collisions, not duplicates. MACs in distinct VLANs do not collide. Note these things:
- You can use the -ExcludeVlans switch to ignore VLANs. That will uncover duplicate MACs no matter their VLAN.
- The script cannot reliably detect VLAN membership of physical adapters. It will treat them as members of the untagged VLAN.
- During testing, I discovered that a host’s virtual adapter has the same MAC as a virtual machine’s adapter on the same switch, it will cause collisions even if they do not share a VLAN. Due to the script’s architecture, that’s a difficult thing to capture using defaults. Use the -ExcludeVlans switch for now.
- The script does work against vNICs in private VLANs and trunking vNICs. Each combination will appear as PrimaryVLAN:SecondaryVLAN, ex: 2:5.
- The script has full help, including examples. Use the -Online switch to access a browser-friendly version of help.
- If the script outputs nothing, that means that it didn’t find any duplicates.
- You can pipe the output to Out-GridView or any of the CSV or HTML cmdlets.
Script File Listing
The text of the script’s initial release appears below:
<# .SYNOPSIS Locate conflicting Hyper-V virtual network adapter MAC addresses. .DESCRIPTION Locate conflicting Hyper-V virtual network adapter MAC addresses. With default settings, will scan the indicated hosts and generate a report of all adapters, virtual and physical, that use the same MAC in the same VLAN. Skips physical adapters bound by a virtual switch or team as these generate false positives. .PARAMETER ComputerName Name of one or more hosts running Hyper-V. If -HostFile is also set, uses both sources. If neither is set, uses the local system. .PARAMETER ExcludeHost If set, will not examine host MAC addresses for conflicts. .PARAMETER ExcludeVlan If set, will treat identical MAC addresses in distinct subnets as conflicts. .PARAMETER IncludeAllZero If set, will include virtual NICs with an all-zero MAC. .PARAMETER IncludeDisconnected If set, will include enabled but unplugged management operating system adapters. No effect if ExcludeHost is set. .PARAMETER IncludeDisabled If set, will include disabled management operating system adapters. No effect if ExcludeHost is set. .PARAMETER HostFile If provided, reads host names from the specified file. If -ComputerName is also set, uses both sources. If neither is set, uses the local system. .PARAMETER FileHasHeader If set, the first row in the file will be treated as a header row. If not set, the parser will assume the first column contains host names. Ignored if HostFile is not specified. .PARAMETER HeaderColumn If HostFile is a delimited type, use this to indicate which column contains the host names. If -HeaderColumn is set, but -FileHeader is NOT set, then this value will be treated as a column header AND a host name. If not set and the file is delimited, the first column will be used. Ignored if HostFile is not specified. .PARAMETER Delimiter The parser will treat this character as the delimiter in -HostFile. Defaults to the separator defined in the local machine's current culture. Ignored if HostFile is not specified. .NOTES Author: Eric Siron Version 1.0a, December 7, 2018 Released under MIT license .INPUTS String[] .EXAMPLE PS C:> Get-VMMacConflict Checks the local machine for duplicate Hyper-V virtual machine MAC addresses. Includes active host adapters. .EXAMPLE PS C:> Get-VMMacConflict -ComputerName svhv1 Checks the Hyper-V system named "svhv1" for duplicate Hyper-V virtual machine MAC addresses. Includes active host adapters. .EXAMPLE PS C:> Get-VMMacConflict -ComputerName svhv1, svhv2, svhv3, svhv4 Checks all of the named Hyper-V systems for duplicate Hyper-V virtual machine MAC addresses. Includes active host adapters. .EXAMPLE PS C:> Get-VMMacConflict -HostFile C:hostnames.txt Reads host names from C:hostnames.txt; it must be a single-column file of host names or all host names must be in the first column. VMs on these hosts are scanned for duplicate MAC addresses. .EXAMPLE PS C:> Get-VMMacConflict -HostFile C:hostnames.txt -FileHasHeader -HeaderColumn HostName Reads host names from C:hostnames.txt; host names must be in a column named "HostName". VMs on these hosts are scanned for duplicate MAC addresses. .EXAMPLE PS C:> Get-VMMacConflict -HostFile C:hostnames.txt -HeaderColumn svhv1 Reads host names from C:hostnames.txt; looks for host names in a header-less column starting with svhv1. VMs on these hosts are scanned for duplicate MAC addresses. .EXAMPLE PS C:> Get-VMMacConflict -ExcludeVlan Checks the local machine for duplicate Hyper-V virtual machine MAC addresses, even if they are in distinct VLANs. Includes active host adapters. .EXAMPLE PS C:> Get-VMMacConflict -IncludeDisconnected -IncludeDisabled Checks the local machine for duplicate Hyper-V virtual machine MAC addresses. Includes active host adapters, even if they are disconnected or disabled. .LINK https://github.com/ejsiron/Posher-V/blob/master/Docs/Get-VMMacConflict.md #> [CmdletBinding()] [OutputType([psobject[]])] param ( [Parameter(ValueFromPipeline = $true, Position = 1)][String[]]$ComputerName = [String]::Empty, [Parameter()][Switch]$ExcludeHost, [Parameter()][Switch]$ExcludeVlan, [Parameter()][Switch]$IncludeAllZero, [Parameter()][Switch]$IncludeDisconnected, [Parameter()][Switch]$IncludeDisabled, [Parameter()][String]$HostFile = [String]::Empty, [Parameter()][Switch]$FileHasHeader, [Parameter()][String]$HeaderColumn = [String]::Empty, [Parameter()][Char]$Delimiter = (Get-Culture).TextInfo.ListSeparator ) begin { Set-StrictMode -Off # script uses .Count to determine if an item is a collection and sometimes passes empty parameters intentionally $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Continue # ensure that, even if errors occur, PSDefaultParameters is reset $ExistingDefaultParams = $PSDefaultParameterValues.Clone() $PSDefaultParameterValues['Get-CimInstance:Namespace'] = 'root/virtualization/v2' $PathToHostSwitchPort = 'Msvm_LANEndpoint/Msvm_LANEndpoint/Msvm_EthernetSwitchPort' $PathToHostVlanSettings = 'Msvm_EthernetPortAllocationSettingData/Msvm_EthernetSwitchPortVlanSettingData' $MacList = New-Object -TypeName System.Collections.ArrayList $SuppliedHostNames = New-Object -TypeName System.Collections.ArrayList $VerifiedHostNames = New-Object -TypeName System.Collections.ArrayList $ProcessedHostNames = New-Object -TypeName System.Collections.ArrayList if (-not $ComputerName -and [String]::IsNullOrEmpty($HostFile)) { $OutNull = $SuppliedHostNames.Add($env:COMPUTERNAME) } if ($HostFile) { Write-Verbose -Message ('Importing host names from "{0}"' -f $HostFile) $HostListFile = (Resolve-Path -Path $HostFile).Path $FileData = Import-Csv -Path $HostFile -Delimiter $Delimiter if ($FileData) { if ([String]::IsNullOrEmpty($HeaderColumn)) { $HeaderColumn = ($FileData | Get-Member -MemberType NoteProperty)[0].Name } if ($FileHasHeader.ToBool() -eq $false) { $OutNull = $SuppliedHostNames.Add($HeaderColumn) # Import-CSV ALWAYS treats line 1 as a header } $SuppliedHostNames.AddRange($FileData.$HeaderColumn) } } function Get-CimPathedAssociation { param ( [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)][Microsoft.Management.Infrastructure.CimInstance]$CimInstance, [Parameter(Mandatory = $true, Position = 2)][String]$PathToInstance, [Parameter()][Switch]$KeyOnly ) $PathNodes = $PathToInstance.Split('/') $SearchInstances = New-Object System.Collections.ArrayList $OutNull = $SearchInstances.Add($CimInstance) for ($i = 0; $i -lt $PathNodes.Length; $i++) { $ChildCounter = 1 if ($SearchInstances.Count) { $OnlyKeys = [bool]($KeyOnly -or $i -ne ($PathNodes.Count - 1)) $TemporarySearchInstances = New-Object System.Collections.ArrayList foreach ($SearchInstance in $SearchInstances) { Write-Progress -Id 2 -Activity 'Querying CIM instances' -Status ('At distance {0} of {1}' -f ($i + 1), $PathNodes.Count) -CurrentOperation ('Loading {0} instances related to {1}' -f $SearchInstances.Count, $SearchInstance.CimClass.CimClassName) -PercentComplete (($ChildCounter++) / $SearchInstances.Count * 100) $AssociatedInstances = Get-CimAssociatedInstance -InputObject $SearchInstance -ResultClassName $PathNodes[$i] if ($AssociatedInstances) { if ($AssociatedInstances.Count) { $OutNull = $TemporarySearchInstances.AddRange($AssociatedInstances) } else { $OutNull = $TemporarySearchInstances.Add($AssociatedInstances) } } } Write-Progress -Id 2 -Activity 'Querying CIM instances' -Completed if ($TemporarySearchInstances.Count) { $SearchInstances = $TemporarySearchInstances } else { $SearchInstances.Clear() } } } $SearchInstances } function New-MacReportItem { param ( [Parameter(Mandatory = $true)][String]$MacAddress, [Parameter()][String]$VMName = [String]::Empty, [Parameter()][String]$VmID = [String]::Empty, [Parameter(Mandatory = $true)][String]$ComputerName, [Parameter(Mandatory = $true)][String]$AdapterName, [Parameter(Mandatory = $true)][String]$AdapterID, [Parameter()][bool]$IsStatic = $false, [Parameter()][String]$SwitchName = [String]::Empty, [Parameter()][String]$VlanInfo ) $MacReportItem = New-Object psobject $MacReportItemNoteProperties = [ordered]@{ VMName = $VMName; VmID = $VmID; ComputerName = $ComputerName; AdapterName = $AdapterName; AdapterID = $AdapterID; MacAddress = $MacAddress; IsStatic = $IsStatic; SwitchName = $SwitchName; Vlan = $VlanInfo } $AddMemberInvariants = @{ InputObject = $MacReportItem; NotePropertyMembers = $MacReportItemNoteProperties; } $OutNull = Add-Member @AddMemberInvariants $MacReportItem } function Get-VlanInfoArray { param ( [Parameter()][Microsoft.Management.Infrastructure.CimInstance]$VlanInfo ) $VlanInfoArray = New-Object -TypeName System.Collections.ArrayList $OutNull = $VlanInfoArray.Add('ph') # prevent PS from decaying the arraylist if ($VlanInfo) { switch ($VlanInfo.OperationMode) { 2 { # Trunk $OutNull = $VlanInfoArray.Add($VlanInfo.NativeVlanId) foreach ($VlanId in $VlanInfo.TrunkVlanIdArray) { if ($VlanId -ne $VlanInfo.NativeVlanId) { $OutNull = $VlanInfoArray.Add($VlanId) } } } 3 { # Private if ($VlanInfo.PvlanMode -eq 3) # promiscuous; allows multiple secondaries { foreach ($SecondaryVlan in $VlanInfo.SecondaryVlanIdArray) { $OutNull = $VlanInfoArray.Add("$($VlanInfo.PrimaryVlanId):$SecondaryVlan") } } else # community & isolated; one secondary { $OutNull = $VlanInfoArray.Add("${$VlanInfo.PrimaryVlanId}:${$VlanInfo.SecondaryVlanId}") } } default # 1 is access mode; 0 should never occur but if it does, treat it as access { $OutNull = $VlanInfoArray.Add($VlanInfo.AccessVlanId) } } } else { $OutNull = $VlanInfoArray.Add("0") } $VlanInfoArray } function IsDuplicate { param( [Parameter(Mandatory = $true)][psobject]$Left, [Parameter(Mandatory = $true)][psobject]$Right, [Parameter()][bool]$ExcludeVlan ) [bool]( $Left.MacAddress -eq $Right.MacAddress -and ( $Left.ComputerName -ne $Right.ComputerName -or $Left.VmID -ne $Right.VmID -or $Left.AdapterID -ne $Right.AdapterID ) -and ($ExcludeVlan -or $Left.Vlan -eq $Right.Vlan) ) } } process { foreach ($HostName in $ComputerName) { if (-not $SuppliedHostNames.Contains($HostName)) { $OutNull = $SuppliedHostNames.Add($HostName) } } if ($SuppliedHostNames.Count) { $Activity = 'Verifying hosts lists' foreach ($HostName in $SuppliedHostNames) { if (-not $HostName) { continue } Write-Progress -Activity $Activity -Status $HostName if (-not $VerifiedHostNames.Contains($HostName)) { $DiscoveredName = $HostName try { $DiscoveredName = (Get-CimInstance -ComputerName $HostName -Namespace 'root/cimv2' -ClassName 'Win32_ComputerSystem' -ErrorAction Stop).Name if (-not $VerifiedHostNames.Contains($HostName)) { $OutNull = $VerifiedHostNames.Add($DiscoveredName) } } catch { Write-Error -Exception $_.Exception -ErrorAction Continue continue } } } Write-Progress -Activity $Activity -Completed $Activity = 'Discovering MAC addresses' foreach ($HostName in $VerifiedHostNames) { if ($ProcessedHostNames.Contains($HostName)) { continue } else { $OutNull = $ProcessedHostNames.Add($HostName) } $Session = $null try { Write-Progress -Activity $Activity -Status ('Connecting to host "{0}"' -f $HostName) $Session = New-CimSession -ComputerName $HostName -ErrorAction Stop } catch { Write-Warning -Message ('Cannot connect to {0}' -f $HostName) Write-Error -Exception $_.Exception -ErrorAction Continue continue } foreach ($VM in Get-CimInstance -CimSession $Session -ClassName Msvm_ComputerSystem) { $CurrentOperation = 'Querying {0}' -f $VM.ElementName if ($HostName -eq $VM.Name) { if (-not $ExcludeHost) { Write-Progress -Activity $Activity -Status 'Loading host adapters' -CurrentOperation $CurrentOperation $AdapterList = New-Object System.Collections.ArrayList $ExternalPorts = Get-CimAssociatedInstance -InputObject $VM -ResultClassName Msvm_ExternalEthernetPort -ErrorAction SilentlyContinue $InternalPorts = Get-CimAssociatedInstance -InputObject $VM -ResultClassName Msvm_InternalEthernetPort -ErrorAction SilentlyContinue if ($ExternalPorts) { if ($ExternalPorts.Count) { $AdapterList.AddRange($ExternalPorts) } else { $OutNull = $AdapterList.Add($ExternalPorts) } } if ($InternalPorts) { if ($InternalPorts.Count) { $AdapterList.AddRange($InternalPorts) } else { $OutNull = $AdapterList.Add($InternalPorts) } } foreach ($Adapter in $AdapterList) { if ($Adapter.IsBound) { continue } $TargetDeviceId = $null if ($Adapter.DeviceId -match '{.*}') { $TargetDeviceId = $Matches[0] } $VLAN = 0 $SwitchName = [String]::Empty Write-Progress -Activity $Activity -Status 'Loading host adapter information' -CurrentOperation $CurrentOperation $MSAdapter = Get-CimInstance -CimSession $Session -Namespace root/StandardCimv2 -ClassName MSFT_NetAdapter -Filter ('DeviceId="{0}"' -f $TargetDeviceId) $AdapterID = $TargetDeviceId $Enabled = $MSAdapter.State -eq 2 -or $IncludeDisabled $Connected = $MSAdapter.MediaConnectState -eq 1 -or ($IncludeDisconnected -or ($MSAdapter.State -ne 2 -and $IncludeDisabled)) if ($Enabled -and $Connected) { if ($Adapter.CimClass.CimClassName -eq 'Msvm_InternalEthernetPort') { Write-Progress -Activity $Activity -Status 'Loading host adapter switch information' -CurrentOperation $CurrentOperation $SwitchPort = Get-CimPathedAssociation -CimInstance $Adapter -PathToInstance $PathToHostSwitchPort if ($SwitchPort) { $AdapterID = ('Microsoft:{0}{1}' -f $SwitchPort.SystemName, $SwitchPort.Name) $VMSwitch = Get-CimAssociatedInstance -InputObject $SwitchPort -ResultClassName Msvm_VirtualEthernetSwitch if ($VMSwitch) { $SwitchName = $VMSwitch.ElementName } $VlanSettings = Get-CimPathedAssociation -CimInstance $SwitchPort -PathToInstance $PathToHostVlanSettings if ($VlanSettings) { $VLAN = $VlanSettings.AccessVlanId } else { $VLAN = $MSAdapter.VlanID } } } $OutNull = $MacList.Add((New-MacReportItem -MacAddress $Adapter.PermanentAddress -ComputerName $HostName -AdapterName $MSAdapter.Name -AdapterID $AdapterID -IsStatic $true -SwitchName $SwitchName -Vlan $VLAN)) } } } } else { Write-Progress -Activity $Activity -Status 'Loading virtual machine settings' -CurrentOperation $CurrentOperation $VMSettings = Get-CimAssociatedInstance -InputObject $VM -ResultClassName Msvm_VirtualSystemSettingData | where -Property VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' if ($VMSettings) { Write-Progress -Activity $Activity -Status 'Loading virtual machine vNIC settings' -CurrentOperation $CurrentOperation foreach ($EthPortSettings in Get-CimAssociatedInstance -InputObject $VMSettings -ResultClassName Msvm_EthernetPortAllocationSettingData) { $VMSwitchName = [String]::Empty if ($EthPortSettings.EnabledState -eq 2) { $VMSwitchName = $EthPortSettings.LastKnownSwitchName } $VNICPortSettings = Get-CimAssociatedInstance -InputObject $EthPortSettings -ResultClassName Msvm_SyntheticEthernetPortSettingData if (-not $VNICPortSettings) { $VNICPortSettings = Get-CimAssociatedInstance -InputObject $EthPortSettings -ResultClassName Msvm_EmulatedEthernetPortSettingData } if ($VNICPortSettings -and ($IncludeAllZero -or $VNICPortSettings.Address -ne '0' * 12)) { $VlanSettings = Get-CimAssociatedInstance -InputObject $EthPortSettings -ResultClassName Msvm_EthernetSwitchPortVlanSettingData foreach ($VlanSet in (Get-VlanInfoArray $VlanSettings | where { $_ -ne 'ph'})) { $OutNull = $MacList.Add((New-MacReportItem -MacAddress $VNICPortSettings.Address -VMName $VM.ElementName -VmID $VM.Name -ComputerName $HostName -AdapterName $VNICPortSettings.ElementName -AdapterID $VNICPortSettings.InstanceID -IsStatic $VNICPortSettings.StaticMacAddress -VlanInfo $VlanSet -SwitchName $VMSwitchName)) } } } } } } $Session.Close() } Write-Progress -Activity $Activity -Completed } } end { $Duplicates = New-Object -TypeName System.Collections.ArrayList foreach ($OuterItem in $MacList) { foreach ($InnerItem in $MacList) { if (-not $Duplicates.Contains($InnerItem)) { if (IsDuplicate -Left $InnerItem -Right $OuterItem -ExcludeVlan $ExcludeVlan.ToBool()) { $OutNull = $Duplicates.Add($InnerItem) } } } } $Duplicates.ToArray() $PSDefaultParameterValues.Clear() foreach ($ParamKey in $ExistingDefaultParams.Keys) { $PSDefaultParameterValues.Add($ParamKey, $ExistingDefaultParams[$ParamKey]) } }
I Need Your Help!
I did test this, but the more eyes looking at it, the better. If you find any problems, share them here or use the GitHub’s issues feature to file a bug report.
Which PowerShell Script would you like?
Is there a particular PowerShell script of a problem you think would be solved using PowerShell you’d like to know about? Let me know in the comments below or head on over to the Altaro Dojo Forums and open a new thread. I’m active in the Dojo Forums community and will gladly answer your PowerShell questions there – who knows I might already have that PowerShell script you need!
Not a DOJO Member yet?
Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!
6 thoughts on "Free PowerShell Script for Hyper-V: Detect MAC Address Conflicts"
Awesome! Thank you for taking the time to publish this!
Thank you, Eric! We’ll put this to use.
I get this error message numerous times when I run the script.
The property ‘LastKnownSwitchName’ cannot be found on this object. Verify that the property exists.
At line:410 char:9
$VMSwitchName = $EthPortSettings.LastKnownSwitchName
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
FullyQualifiedErrorId : PropertyNotFoundStrict
What’s your Hyper-V/Management OS version? PowerShell version where you’re running the script?
I see that you’ve opened an issue on the GitHub repository. I’ll move the troubleshooting to there.