LoopbackAdapter.psm1

#Region './prefix.ps1' 0
<#
.EXTERNALHELP LoopbackAdapter-help.xml
#>


$moduleRoot = Split-Path `
    -Path $MyInvocation.MyCommand.Path `
    -Parent

#region LocalizedData
$culture = 'en-US'

if (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath $PSUICulture))
{
    $culture = $PSUICulture
}

Import-LocalizedData `
    -BindingVariable LocalizedData `
    -Filename 'LoopbackAdapter.strings.psd1' `
    -BaseDirectory $moduleRoot `
    -UICulture $culture
#endregion
#EndRegion './prefix.ps1' 23
#Region './Private/Install-Chocolatey.ps1' 0
<#
    .SYNOPSIS
        Install Chocolatey.

    .DESCRIPTION
        Installs Chocolatey from the internet if it is not installed.

    .PARAMETER Force
        Force the install of Chocolatey, without confirming with the user.

    .EXAMPLE
        Install-Chocolatey

    .OUTPUTS
        None
#>

function Install-Chocolatey
{
    [CmdLetBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High')]
    param
    (
        [Parameter()]
        [Switch]
        $Force
    )

    # Check chocolatey is installed - if not, install it
    $chocolateyInstalled = Test-Path -Path (Join-Path -Path $ENV:ProgramData -ChildPath 'Chocolatey\Choco.exe')

    if (-not $chocolateyInstalled)
    {
        if ($Force -or $PSCmdlet.ShouldProcess($LocalizedData.DownloadAndInstallChocolateyShould))
        {
            Write-Verbose -Message $LocalizedData.InstallingChocolateyMessage
            try
            {
                $chocolateyInstallScript = (Invoke-WebRequest -UseBasicParsing -Uri 'https://chocolatey.org/install.ps1').Content
                $chocolateyInstallScript = [scriptblock]::Create($chocolateyInstallScript)
                $null = $chocolateyInstallScript.Invoke()
            }
            catch
            {
                throw ($LocalizedData.ChocolateyInstallationError -f $_)
            }
        }
        else
        {
            throw $LocalizedData.NetworkAdapterExistsWrongTypeError
        }
    }
    else
    {
        Write-Verbose -Message $LocalizedData.ChocolateyInstalledMessage
    }
}
#EndRegion './Private/Install-Chocolatey.ps1' 58
#Region './Private/Install-Devcon.ps1' 0
<#
    .SYNOPSIS
        Install the DevCon.Portable (Windows Device Console) package using Chocolatey.

    .DESCRIPTION
        Installs Chocolatey from the internet if it is not installed, then uses
        it to download the DevCon.Portable (Windows Device Console) package.
        The devcon.portable Chocolatey package can be found here and installed manually
        if no internet connection is available:
        https://chocolatey.org/packages/devcon.portable/

        Chocolatey will remain installed after this function is called.

    .PARAMETER Force
        Force the install of Chocolatey and the Devcon.portable package if not already
        installed, without confirming with the user.

    .EXAMPLE
        Install-Devcon

    .OUTPUTS
        The fileinfo object containing the appropriate DevCon*.exe application that was
        installed for this architecture.
#>

function Install-Devcon
{
    [OutputType([System.IO.FileInfo])]
    [CmdLetBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High')]
    param
    (
        [Parameter()]
        [Switch]
        $Force
    )

    Install-Chocolatey @PSBoundParameters

    # Check DevCon installed - if not, install it.
    $devConInstalled = ((Test-Path -Path "$ENV:ProgramData\Chocolatey\Lib\devcon.portable\Devcon32.exe") `
        -and (Test-Path -Path "$ENV:ProgramData\Chocolatey\Lib\devcon.portable\Devcon64.exe"))

    if (-not $devConInstalled)
    {
        try
        {
            <#
                This will download and install DevCon.exe
                It will also be automatically placed into the path
            #>

            if ($Force -or $PSCmdlet.ShouldProcess($LocalizedData.DownloadAndInstallDevConShould))
            {
                Write-Verbose -Message $LocalizedData.InstallDevconMessage
                $null = & choco @('install','-r','-y','devcon.portable')
            }
            else
            {
                throw $LocalizedData.DevConNotInstalledError
            }
        }
        catch
        {
            throw ($LocalizedData.DevConInstallationError -f $_)
        }
    }

    if ([Environment]::Is64BitOperatingSystem -eq $True)
    {
        Get-ChildItem -Path "$ENV:ProgramData\Chocolatey\Lib\devcon.portable\Devcon64.exe"
    }
    else
    {
        Get-ChildItem -Path "$ENV:ProgramData\Chocolatey\Lib\devcon.portable\Devcon32.exe"
    }
}
#EndRegion './Private/Install-Devcon.ps1' 77
#Region './Private/Uninstall-Devcon.ps1' 0
<#
    .SYNOPSIS
        Install the DevCon.Portable (Windows Device Console) package using Chocolatey.

    .DESCRIPTION
        Installs Chocolatey from the internet if it is not installed, then uses
        it to uninstall the DevCon.Portable (Windows Device Console) package.

        Chocolatey will remain installed after this function is called.

    .PARAMETER Force
        Force the uninstall of the devcon.portable package if it is installed, without confirming with the user.

    .EXAMPLE
        Uninstall-Devcon

    .OUTPUTS
        None.
#>

function Uninstall-Devcon
{
    [CmdLetBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High')]
    param
    (
        [Parameter()]
        [Switch]
        $Force
    )

    Install-Chocolatey @PSBoundParameters

    try
    {
        <#
            This will download and install DevCon.exe
            It will also be automatically placed into the path
        #>

        if ($Force -or $PSCmdlet.ShouldProcess($LocalizedData.UninstallDevConShould))
        {
            $null = & choco @('uninstall','-r','-y','devcon.portable')
        }
        else
        {
            throw $LocalizedData.DevConNotUninstalledError
        }
    }
    catch
    {
        throw ($LocalizedData.DevConNotUninstallationError -f $_)
    }
}
#EndRegion './Private/Uninstall-Devcon.ps1' 54
#Region './Private/Wait-ForDevconUpdate.ps1' 0
<#
    .SYNOPSIS
        Waits for changes made by DevCon.exe to complete before continuing.

    .DESCRIPTION
        Waits for the DevCon.exe process to exit and then checks whether Get-NetAdapter completes successfully.
        Get-NetAdapter will throw "Illegal operation attempted on a registry key that has been marked for
        deletion." if the changes are still processing.

    .PARAMETER DevconExeTimeout
        Time in seconds to wait for the DevCon process to complete.

    .PARAMETER RegistryUpdateTimeout
        Time in seconds to wait for the registry to finish processing changes.

    .EXAMPLE
        Wait-ForDevconUpdate

    .NOTES
        N/A
#>

function Wait-ForDevconUpdate
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Int32]
        $DevconExeTimeout = 5,

        [Parameter()]
        [System.Int32]
        $RegistryUpdateTimeout = 5
    )

    Get-Process -Name 'DevCon' -ErrorAction SilentlyContinue | Wait-Process -Timeout $DevconExeTimeout
    $registryUpdated = $false
    $registryTimer = 0
    $waitIncrement = 0.5

    while ($registryUpdated -eq $false)
    {
        try
        {
            $null = Get-NetAdapter -ErrorAction Stop *>&1
            $registryUpdated = $true
        }
        catch [Microsoft.Management.Infrastructure.CimException]
        {
            if ($registryTimer -ge $RegistryUpdateTimeout)
            {
                throw $_
            }

            Start-Sleep -Seconds $waitIncrement
            $registryTimer += $waitIncrement
        }
    }
}
#EndRegion './Private/Wait-ForDevconUpdate.ps1' 60
#Region './Public/Get-LoopbackAdapter.ps1' 0
function Get-LoopbackAdapter
{
    [OutputType([Microsoft.Management.Infrastructure.CimInstance[]])]
    [CmdLetBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $Name
    )

    # Check for the existing Loopback Adapter
    if ($Name)
    {
        $adapter = Get-NetAdapter `
            -Name $Name `
            -ErrorAction Stop

        if ($adapter.DriverDescription -ne 'Microsoft KM-TEST Loopback Adapter')
        {
            throw ($LocalizedData.NetworkAdapterExistsWrongTypeError -f $Name)
        } # if

        return $adapter
    }
    else
    {
        Get-NetAdapter | Where-Object -Property DriverDescription -eq 'Microsoft KM-TEST Loopback Adapter'
    } # if
}
#EndRegion './Public/Get-LoopbackAdapter.ps1' 31
#Region './Public/New-LoopbackAdapter.ps1' 0

function New-LoopbackAdapter
{
    [OutputType([Microsoft.Management.Infrastructure.CimInstance])]
    [CmdLetBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [switch]
        $Force
    )

    $null = $PSBoundParameters.Remove('Name')

    # Check for the existing Loopback Adapter
    $adapter = Get-NetAdapter `
        -Name $Name `
        -ErrorAction SilentlyContinue

    # Is the loopback adapter installed?
    if ($adapter)
    {
        throw ($localizedData.NetworkAdapterExistsError -f $Name)
    } # if

    # Make sure DevCon is installed.
    $devConExe = (Install-Devcon @PSBoundParameters).FullName

    <#
        Get a list of existing Loopback adapters
        This will be used to figure out which adapter was just added
    #>

    $existingAdapters = (Get-LoopbackAdapter).PnPDeviceID

    <#
        Use Devcon.exe to install the Microsoft Loopback adapter
        Requires local Admin privs.
    #>

    Write-Verbose -Message ($LocalizedData.CreatingLoopbackAdapterMessage -f $Name)
    $null = & $DevConExe @('install', "$($ENV:SystemRoot)\inf\netloop.inf", '*MSLOOP')
    Wait-ForDevconUpdate

    # Find the newly added Loopback Adapter
    $adapters = Get-NetAdapter
    $adapter = $adapters |
        Where-Object -FilterScript {
            ($_.PnPDeviceID -notin $ExistingAdapters ) -and `
            ($_.DriverDescription -eq 'Microsoft KM-TEST Loopback Adapter')
        }

    if (-not $adapter)
    {
        throw ($LocalizedData.NewNetworkAdapterNotFoundError -f $Name)
    } # if

    # Rename the new Loopback adapter
    Write-Verbose -Message ($LocalizedData.SettingNameOfNewLoopbackAdapterMessage -f $Name)
    $null = Rename-NetAdapter `
        -Name $adapter.Name `
        -NewName $Name `
        -ErrorAction Stop

    # Set the metric to 254
    Write-Verbose -Message ($LocalizedData.SettingMetricOfNewLoopbackAdapterMessage -f $Name)
    $null = Set-NetIPInterface `
        -InterfaceAlias $Name `
        -InterfaceMetric 254 `
        -ErrorAction Stop

    <#
        Wait till IP address binding has registered in the CIM subsystem.
        if after 30 seconds it has not been registered then throw an exception.
    #>

    [System.Boolean] $adapterBindingReady = $false
    [System.DateTime] $startTime = Get-Date

    while (-not $adapterBindingReady `
            -and (((Get-Date) - $startTime).TotalSeconds) -lt 30)
    {
        try
        {
            $ipAddress = Get-CimInstance `
                -ClassName MSFT_NetIPAddress `
                -Namespace ROOT/StandardCimv2 `
                -Filter "((InterfaceAlias = '$Name') AND (AddressFamily = 2))" `
                -ErrorAction Stop

            if ($ipAddress)
            {
                $adapterBindingReady = $true
            } # if

            Write-Verbose -Message ($LocalizedData.WaitingForIPAddressMessage -f $Name)
            Start-Sleep -Seconds 1
        }
        catch
        {
            Write-Warning -Message ($LocalizedData.GetIPAddressWarning -f $_)
        }
    } # while

    if (-not $ipAddress)
    {
        throw $LocalizedData.NewNetworkAdapterNotFoundInCIMError
    }

    # Pull the newly named adapter (to be safe)
    $adapter = Get-NetAdapter `
        -Name $Name `
        -ErrorAction Stop

    return $adapter
}
#EndRegion './Public/New-LoopbackAdapter.ps1' 118
#Region './Public/Remove-LoopbackAdapter.ps1' 0
function Remove-LoopbackAdapter
{
    [CmdLetBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [switch]
        $Force
    )

    process {
        $null = $PSBoundParameters.Remove('Name')

        # Check for the existing Loopback Adapter
        $adapter = Get-NetAdapter `
            -Name $Name `
            -ErrorAction SilentlyContinue

        # Is the loopback adapter installed?
        if (-not $adapter)
        {
            # Adapter doesn't exist
            throw ($LocalizedData.LoopbackAdapterNotFound -f $Name)
        }

        # Is the adapter Loopback adapter?
        if ($adapter.DriverDescription -ne 'Microsoft KM-TEST Loopback Adapter')
        {
            # Not a loopback adapter - don't uninstall this!
            throw ($LocalizedData.NetworkAdapterWrongTypeError -f $Name)
        } # if

        # Make sure DevCon is installed and return path to executable
        $devConExe = (Install-Devcon @PSBoundParameters).FullName

        <#
            Use Devcon.exe to remove the Microsoft Loopback adapter using the PnPDeviceID.
            Requires local Admin privs.
        #>

        Write-Verbose -Message ($LocalizedData.RemovingLoopbackAdapterMessage -f $Name)
        $null = & $devConExe @('remove',"@$($adapter.PnPDeviceID)")
        Wait-ForDevconUpdate
    }
} # function Remove-LoopbackAdapter
#EndRegion './Public/Remove-LoopbackAdapter.ps1' 49
#Region './suffix.ps1' 0
#EndRegion './suffix.ps1' 1