Posted in : Active Directory, Azure, Microsoft, Powershell, Windows, Windows Server

4 months ago

Invoking commands remotely on machines is something we must do from time to time, in many scenarios speed is a critical factor as the remote command could be in response to anything from a incident to security breach. Speed combined with large environments in the cloud with many clients/servers deallocated often makes this a difficult task.
In smaller environments we would normally ping targets, and based on the response we execute a script through winrm asyncronously (invoke-command -asjob). The problem is that the ping part is often not executed in parallell.
While googling a solution i came across an Async ping sweep script (link at the end) that works brilliantly but only accepts ipranges and not hostnames, but the method to send async pings is so good that I used it in my implementation.
The result:

Function Test-ConnectionAsync {
<#
.Synopsis
    Ping targets asynchronously
.DESCRIPTION
    Ping targets using dns names or ip addresses asynchronously
.NOTES
    Name: Test-ConnectionAsync
    Author: Vikingur Saemundsson
    Date Created: 2020-07-20
    Version History:
        2020-07-20 - Vikingur Saemundsson
            Initial Creation
    Xenit AB
#>
Param(
    [Parameter(
        Mandatory=$true,
        ValueFromPipeline=$true
    )]
    [string[]]$Targets,
    $Timeout = 1000 # milliseconds
)
    Begin{
        #Cleanup
        Remove-Event -SourceIdentifier "Async-Ping*"
        Unregister-Event -SourceIdentifier "Async-Ping*"
    }
    Process{
        Foreach($target in $targets){
            #prepare a unique identifier for the ping object so we can later identify the results
            $thisName = "Ping_$target`_$([guid]::NewGuid().ToString())"
            #Create ping object in variable
            New-Variable -Name $thisName -Value (New-Object System.Net.NetworkInformation.Ping)
            #Register the variable as event
            Register-ObjectEvent -InputObject (Get-Variable $thisName -ValueOnly) -EventName PingCompleted -SourceIdentifier "Async-$thisName"
            #invoke async ping in variable
            (Get-Variable $thisName -ValueOnly).SendAsync($target,$timeout,$thisName)
            #Cleanup variable
            Remove-Variable $thisName
        }
        $pending = (Get-Event -SourceIdentifier "Async-Ping*").Count
        #await all responses
        While($pending -lt $targets.Count){
            Wait-Event -SourceIdentifier "Async-Ping*" | Out-Null
            Start-Sleep -Milliseconds 10
            $pending = (Get-Event -SourceIdentifier "Async-Ping*").Count
        }
        [array]$Return = Get-Event -SourceIdentifier "Async-Ping*" | ForEach-Object{
            [pscustomobject]@{
                Target = $_.SourceIdentifier.Substring(11).Substring(0,$_.SourceIdentifier.Length-48)
                Status = $_.SourceEventArgs.Reply.Status
                Error = $_.SourceEventArgs.Error
                Reply = $_.SourceEventArgs.Reply
            }
        }
    }
    End{
        #Cleanup
        Remove-Event -SourceIdentifier "Async-Ping*"
        Unregister-Event -SourceIdentifier "Async-Ping*"
        Return $Return
    }
}

The output is a object that contains original target name, the ping status (Success, Timeout etc), errors and the Ping reply object.

PS C:\Users\vikingur.saemundsson> $Targets = 'localhost','google.com'
PS C:\Users\vikingur.saemundsson> Test-ConnectionAsync -Targets $targets -Timeout $timeout
Target      Status Error Reply
------      ------ ----- -----
localhost  Success       System.Net.NetworkInformation.PingReply
google.com Success       System.Net.NetworkInformation.PingReply
PS C:\Users\vikingur.saemundsson> Test-ConnectionAsync -Targets $targets -Timeout $timeout | Select -ExpandProperty reply
Status        : Success
Address       : ::1
RoundtripTime : 8
Options       :
Buffer        : {97, 98, 99, 100...}
Status        : Success
Address       : 216.58.211.14
RoundtripTime : 12
Options       : System.Net.NetworkInformation.PingOptions
Buffer        : {97, 98, 99, 100...}

 
Using this we can use Invoke-Command with the -AsJob command to invoke.

Test-ConnectionAsync -Targets $Servers.Name -timeout 50 -verbose | ForEach-Object{
    Invoke-Command -ComputerName $_.Target -AsJob -JobName $_.Target -ThrottleLimit 100 -ScriptBlock {
        $env:COMPUTERNAME
        gpupdate /force | Out-Null
    } -ErrorAction Stop
}

To get a sense of the performance of the system I did some benchmark pings against a Google ip range. The average /24 network took 0.5 seconds, or 547 milliseconds to respond using a max timeout of 100ms per attempt. The function does not really scale linearly, but I believe that’s more on my side than the code.

pinging 256 targets (35.191.0.0 - 35.191.0.255) 50 times average: 547.62735 milliseconds
pinging 512 targets (35.191.0.0 - 35.191.1.255) 50 times average: 1618.175738 milliseconds
pinging 1024 targets (35.191.0.0 - 35.191.3.255) 50 times average: 2480.677944 milliseconds
pinging 2048 targets (35.191.0.0 - 35.191.7.255) 50 times average: 5737.749198 milliseconds
pinging 4096 targets (35.191.0.0 - 35.191.15.255) 50 times average: 10840.020774 milliseconds

Async network ping sweep
https://gallery.technet.microsoft.com/scriptcenter/Asynchronous-Network-Ping-abdf01aa

Tags : #Script, Async, asynchronous, Azure, code, How to, Microsoft, Ping, PowerShell, Security, Windows, Windows 10, Windows Server, Windows Server 2016

Add comment

Your comment will be revised by the site if needed.