Public/Get-specNetworkAdapterInfo.ps1

function Get-specNetworkAdapterInfo {
    <#
    .SYNOPSIS
    Gets detailed information about the active network adapter used for the default IPv4 gateway.
 
    .DESCRIPTION
    This function identifies the network adapter that is currently providing the default IPv4 gateway for the system's internet access. It iterates through network adapters, checks for a valid IPv4 address and default gateway, and considers Hyper-V vEthernet adapters bound to physical network interfaces to accurately determine the underlying connection type (Wired or Wi-Fi).
 
    It retrieves key details such as the interface description, connection type (determined via WMI or fallback), IP address, gateway, MAC address, and interface index.
 
    .PARAMETER IncludeDisconnected
    If specified, the function will include network adapters with a status other than 'Up' when searching for the active connection. By default, only adapters with a status of 'Up' are considered.
 
    .INPUTS
    None.
    This function does not accept pipeline input or any other input types.
 
    .OUTPUTS
    System.Management.Automation.PSCustomObject
    This function outputs a custom object containing detailed information about the active network adapter. Properties include:
    - NetInterfaceDescription (string): The description of the active network interface.
    - ConnectionType (string): The determined type of connection ('Wired', 'Wi-Fi', or 'Unknown').
    - NetProfileName (string): The name of the network profile (e.g., 'Network', 'Network 2', 'corp.domain.com').
    - NetIPv4Address (string): The IPv4 address assigned to the adapter.
    - NetInterfaceAlias (string): The alias (name) of the network interface.
    - NetIPv4DefaultGateway (string): The IPv4 address of the default gateway.
    - MacAddress (string): The MAC address of the adapter.
    - InterfaceIndex (UInt32): The interface index of the adapter.
    - PhysicalAdapterDescription (string): The description of the underlying physical adapter if the active connection is via a Hyper-V vEthernet switch, otherwise 'N/A'.
 
    If no active connection is found, the function writes a host message and does not return an object.
 
    .EXAMPLE
    Get-specNetworkAdapterInfo
 
    Gets information about the currently active network adapter providing the default IPv4 gateway.
 
    .EXAMPLE
    Get-specNetworkAdapterInfo -IncludeDisconnected
 
    Gets information about the active network adapter, searching through all adapters including disconnected ones.
 
    .EXAMPLE
    Get-specNetworkAdapterInfo -Verbose
 
    Gets information about the active network adapter and displays detailed progress and diagnostic messages during execution.
 
    .NOTES
    Author: owen.heaume
    Version: 1.0 - Initial release
                1.1 - Add InterfaceIndex to output
                1.2 - Can now take into account Hyper-V vEthernet adapters and their physical bindings
    #>


    [CmdletBinding()]
    param (
        [Switch]$IncludeDisconnected
    )

    # Determine which network adapters to retrieve based on parameter
    # Ensure we get adapters that are potentially "Up" even if they don't have an IP yet,
    # as the IP config comes from the vSwitch for Hyper-V scenarios.
    $CurrentNetAdapters = if ($IncludeDisconnected) {
        Get-NetAdapter
    } else {
        Get-NetAdapter | Where-Object { $_.Status -eq 'Up' }
    }

    # Find the active connection by checking the default gateway
    $activeAdapter = $null
    $activeIPConfiguration = $null # Keep the IP config alongside the adapter

    Write-Verbose "Searching for active adapter among $($CurrentNetAdapters.Count) candidates..."

    foreach ($CurrentNetAdapter in $CurrentNetAdapters) {
        Write-Verbose "Checking adapter: $($CurrentNetAdapter.InterfaceAlias) (Index: $($CurrentNetAdapter.ifIndex))"
        # Skip only Network Bridges and potentially other unwanted types,
        # but DO NOT skip vEthernet adapters as they can hold the gateway.
        if ($CurrentNetAdapter.InterfaceDescription -match 'Microsoft Network Adapter Multiplexor Driver') {
            # Skipping Network Bridge.
            Write-Verbose "Skipping adapter $($CurrentNetAdapter.InterfaceAlias) (Network Bridge)."
            continue
        }

        # Get IP configuration for the current adapter.
        # Use SilentlyContinue so it doesn't stop if no IP config is found or if Get-NetIPInterface fails for some reason.
        $IPConfiguration = Get-NetIPConfiguration -InterfaceIndex $CurrentNetAdapter.ifIndex -ErrorAction SilentlyContinue

        # Check if we got IP configuration AND if it has a valid IPv4 default gateway and an IPv4 address
        if ($IPConfiguration -and $IPConfiguration.IPv4DefaultGateway -and $IPConfiguration.IPv4Address) {
            Write-Verbose "Found active adapter: $($CurrentNetAdapter.InterfaceAlias) (Index $($CurrentNetAdapter.ifIndex)) with gateway $($IPConfiguration.IPv4DefaultGateway.NextHop)"
            $activeAdapter = $CurrentNetAdapter
            $activeIPConfiguration = $IPConfiguration # Store the IP config
            break # Found the adapter with default gateway, exit the loop
        }
        Write-Verbose "Adapter $($CurrentNetAdapter.InterfaceAlias) (Index $($CurrentNetAdapter.ifIndex)) does not have a valid IPv4 gateway and address."
    }

    # If an active adapter is found, return its details
    if ($activeAdapter) {
        Write-Verbose "Active adapter found: $($activeAdapter.InterfaceAlias). Determining connection type."

        # Determine connection type
        $connectionType = 'Unknown' # Default to Unknown
        $physicalAdapterDescription = $null # Variable to hold the description of the physical adapter if found

        # Check if the active adapter is a Hyper-V Virtual Ethernet Adapter (by its alias pattern)
        if ($activeAdapter.InterfaceAlias -like 'vEthernet*') {
            Write-Verbose 'Active adapter is a vEthernet adapter. Attempting to find bound physical adapter via Get-VMSwitch.'
            try {
                # Extract the Virtual Switch Name from the InterfaceAlias
                # e.g., "vEthernet (OH-External WiFi)" -> "OH-External WiFi"
                # Use regex to handle potential variations in spacing or naming
                $vSwitchNameMatch = $activeAdapter.InterfaceAlias -match '^vEthernet \((.+)\)$'
                if ($vSwitchNameMatch) {
                    $vSwitchName = $matches[1] # Get the captured group (the name inside the parentheses)
                    Write-Verbose "Extracted potential Virtual Switch name: '$vSwitchName'"

                    # --- CHECK FOR LEADING/TRAILING SPACES IN EXTRACTED NAME ---
                    if ($vSwitchName -ne $vSwitchName.Trim()) {
                        Write-Warning "Extracted Virtual Switch name '$vSwitchName' contains leading or trailing whitespace. Ensure the actual Virtual Switch name matches this exactly, including any spaces, for successful lookup with Get-VMSwitch."
                    }
                    # --- DO NOT TRIM --- Use $vSwitchName as is for Get-VMSwitch

                    # Check if Get-VMSwitch cmdlet is available (requires Hyper-V module)
                    if (Get-Command Get-VMSwitch -ErrorAction SilentlyContinue) {
                        $vmSwitch = Get-VMSwitch -Name $vSwitchName -ErrorAction SilentlyContinue
                        if ($vmSwitch -and $vmSwitch.NetAdapterInterfaceDescription) {
                            $physicalAdapterDescription = $vmSwitch.NetAdapterInterfaceDescription
                            Write-Verbose "Found bound physical adapter description: '$($physicalAdapterDescription)' via vSwitch '$vSwitchName'."
                        } else {
                            Write-Warning "Hyper-V Virtual Switch '$vSwitchName' found, but it does not appear to be bound to an external physical adapter (NetAdapterInterfaceDescription is empty)."
                            # Fallback to checking the vEthernet adapter description itself if vSwitch has no physical binding info
                            $physicalAdapterDescription = $activeAdapter.InterfaceDescription
                            Write-Verbose "Falling back to using vEthernet adapter description '$($physicalAdapterDescription)' for WMI query."
                        }
                    } else {
                        Write-Warning 'Get-VMSwitch cmdlet not found. Hyper-V PowerShell module may not be installed or loaded. Cannot reliably find bound physical adapter via vSwitch.'
                        # Fallback to checking the vEthernet adapter description itself if Get-VMSwitch is not available
                        $physicalAdapterDescription = $activeAdapter.InterfaceDescription
                        Write-Verbose "Falling back to using vEthernet adapter description '$($physicalAdapterDescription)' for WMI query."
                    }
                } else {
                    Write-Warning "Could not parse Virtual Switch name from InterfaceAlias pattern: '$($activeAdapter.InterfaceAlias)'."
                    # Fallback to checking the vEthernet adapter description itself if alias pattern doesn't match
                    $physicalAdapterDescription = $activeAdapter.InterfaceDescription
                    Write-Verbose "Falling back to using vEthernet adapter description '$($physicalAdapterDescription)' for WMI query."
                }
            } catch {
                Write-Warning "An error occurred while trying to get Hyper-V Virtual Switch information for '$($activeAdapter.InterfaceAlias)': $($_.Exception.Message)."
                # Fallback to checking the vEthernet adapter description itself if an unexpected error occurs
                $physicalAdapterDescription = $activeAdapter.InterfaceDescription
                Write-Verbose "Falling back to using vEthernet adapter description '$($physicalAdapterDescription)' for WMI query due to error."
            }
        } else {
            # If it's not a vEthernet adapter, use its own description for the WMI query
            $physicalAdapterDescription = $activeAdapter.InterfaceDescription
            Write-Verbose "Active adapter is not a vEthernet adapter. Using its own description '$($physicalAdapterDescription)' for WMI query."
        }

        # Now, use the determined physical adapter description (or the vEthernet description if physical not found/applicable)
        # to query WMI for the physical medium type.
        if ($physicalAdapterDescription) {
            Write-Verbose "Querying WMI for physical medium type using description: '$physicalAdapterDescription'"
            try {
                # NdisPhysicalMediumType = 9 corresponds to Native 802.11 (Wi-Fi)
                # Use ErrorAction SilentlyContinue here too, in case the WMI instance doesn't exist for the description
                $physicalMedium = Get-CimInstance -Namespace 'root\WMI' -Class MSNdis_PhysicalMediumType -Filter "InstanceName = '$physicalAdapterDescription'" -ErrorAction SilentlyContinue
                if ($physicalMedium) {
                    $connectionType = if ($physicalMedium.NdisPhysicalMediumType -eq 9) {
                        'Wi-Fi'
                    } else {
                        'Wired' # Assuming anything not 9 is wired or other non-wireless type
                    }
                    Write-Verbose "Determined ConnectionType as '$($connectionType)' based on WMI NdisPhysicalMediumType ($($physicalMedium.NdisPhysicalMediumType)) for '$physicalAdapterDescription'."
                } else {
                    Write-Warning "WMI class MSNdis_PhysicalMediumType did not return data for '$physicalAdapterDescription'. Falling back to description pattern matching."
                    # Fallback if WMI query returns nothing for the description
                    $connectionType = if ($physicalAdapterDescription -match 'Wi-Fi|Wireless|WiFi|802.11') { 'Wi-Fi' } else { 'Wired' }
                    Write-Verbose "Falling back to pattern matching for ConnectionType: '$($connectionType)'."
                }
            } catch {
                Write-Warning "An error occurred while querying WMI for physical medium type for '$physicalAdapterDescription': $($_.Exception.Message). Falling back to description pattern matching."
                # Fallback to pattern matching if WMI query itself fails
                $connectionType = if ($physicalAdapterDescription -match 'Wi-Fi|Wireless|WiFi|802.11') { 'Wi-Fi' } else { 'Wired' }
                Write-Verbose "Falling back to pattern matching for ConnectionType: '$($connectionType)'."
            }
        } else {
            Write-Warning 'Could not determine a network adapter description to query WMI with. Setting ConnectionType to Unknown.'
            # If somehow we don't have a description to query WMI, default to Unknown
            $connectionType = 'Unknown'
        }

        # Return the custom object
        return [PSCustomObject]@{
            NetInterfaceDescription    = $activeAdapter.InterfaceDescription # Description of the adapter with the gateway
            ConnectionType             = $connectionType # Based on WMI of the underlying physical adapter (if Hyper-V) or the active adapter itself
            NetProfileName             = if ($activeIPConfiguration -and $activeIPConfiguration.NetProfile) { $activeIPConfiguration.NetProfile.Name } else { 'N/A' }
            NetIPv4Address             = if ($activeIPConfiguration -and $activeIPConfiguration.IPv4Address) { $activeIPConfiguration.IPv4Address.IPAddress } else { 'N/A' }
            NetInterfaceAlias          = $activeAdapter.InterfaceAlias
            NetIPv4DefaultGateway      = if ($activeIPConfiguration -and $activeIPConfiguration.IPv4DefaultGateway) { $activeIPConfiguration.IPv4DefaultGateway.NextHop } else { 'N/A' }
            MacAddress                 = $activeAdapter.MacAddress
            InterfaceIndex             = $activeAdapter.ifIndex
            # Optionally add the physical adapter description if different from NetInterfaceDescription
            PhysicalAdapterDescription = if ($activeAdapter.InterfaceDescription -ne $physicalAdapterDescription -and $physicalAdapterDescription) { $physicalAdapterDescription } else { 'N/A' }
        }
    } else {
        Write-Host 'No active connection found.'
        # Optionally, return $null or an empty object if no active connection
        # return $null
    }
}