So today I needed to see what hosts were up on a network. I can think of dozens of sensible ways you could achieve this task – my usual method is something like:
$ nmap -sP 192.168.0.*
But today I had PowerShell open form pinging and tracerouting things, and I had time on my hands to do a little exploring. I’ve heard that PowerShell is actually pretty powerful so I started playing.
The first step – how to ping a range of IPs:
1..254 | ForEach-Object { ping "192.168.0.$_" }
So that’s working. I could now add some arguments to the ping call to just ping once and to clean up the output, but this is dumb as a rock and I’m looking to learn about PowerShell here, not a glorified batch file.
So with the whole of .net to play with, let’s dig out the Ping object that I’ve played with before (that time while learning WPF) and see how we call that from PowerShell.
$ping = New-Object System.Net.NetworkInformation.Ping;
1..254 | ForEach-Object { $ping.Send("192.168.0.$_") }
Great, no command line options needed, we’re just doing one ping to each IP now and waiting for a response, which is nice, though that’s still going to take some time to complete through. Having a quick look at what the type ahead offers us on the $ping object reveals SendAsync and SendPingAsync, the documentation says that the first does the whole thing async and the latter only does the waiting for a reply async. I’ll go with the latter since the waiting around is clearly going to be the main bottleneck here given most of them will time out.
Just calling SendPingAsync returns a handle to the task object, but we don’t get any actual response about what the pings result is, so we need to listen for the PingCompleted event, how do we do that in PowerShell?
Register-ObjectEvent is the answer to that problem – not the cleanest syntax compared to something like the += operator in .net and I’m not sure how arguments are handled, so lets dump everything from the $event object that I saw in an example and explore from there…
$ping = New-Object System.Net.NetworkInformation.Ping;
(Register-ObjectEvent $ping PingCompleted -Action { Write-Host ($event | Format-List | Out-String) })
$ping.SendPingAsync("192.168.0.1")
Which results in something like this:
ComputerName :
RunspaceId : 01796479-76c4-4166-a0e1-89e3ed351f19
EventIdentifier : 1
Sender : System.Net.NetworkInformation.Ping
SourceEventArgs : System.Net.NetworkInformation.PingCompletedEventArgs
SourceArgs : {System.Net.NetworkInformation.Ping, System.Net.NetworkInformation.PingCompletedEventArgs}
SourceIdentifier : 8ff57984-22ce-491d-87bd-2d2922d16dcc
TimeGenerated : 22/11/2015 11:43:27
MessageData :
OK, so SourceEventArgs looks like a good place to explore, and lets work out how to suppress the other console output too as it’s getting in the way a little.
$ping = New-Object System.Net.NetworkInformation.Ping;
[Void](Register-ObjectEvent $ping PingCompleted -Action { Write-Host ($event.SourceEventArgs | Format-List | Out-String) })
[Void]$ping.SendPingAsync("192.168.0.1")
Reply : System.Net.NetworkInformation.PingReply
Cancelled : False
Error :
UserState : System.Threading.Tasks.TaskCompletionSource`1[System.Net.NetworkInformation.PingReply]
And one more step into $event.SourceEventArgs.Reply gets us what we wanted:
Status : Success
Address : 192.168.0.1
RoundtripTime : 0
Options : System.Net.NetworkInformation.PingOptions
Buffer : {97, 98, 99, 100...}
While poking around the internet I also found that there’s a nice helper called params which will take the content of the $event.SourceArgs and let us give them names, so using param($s, $e); gives us the sender, event style that I was expecting from .net land, so lets do that too and maybe output something a little cleaner too.
$ping = New-Object System.Net.NetworkInformation.Ping;
[Void](Register-ObjectEvent $ping PingCompleted -Action { param($s, $e); if($e.Reply.Status -eq"Success") { Write-Host$e.Reply.Address, ($e.Reply.RoundtripTime.toString() + "ms") } })
[Void]$ping.SendPingAsync("192.168.0.1")
This is where I had hoped I could just throw our loop around the last call and we’d be in business, but it seems there’s a couple of flaws in that plan, firstly:
Exception calling "SendPingAsync" with "1" argument(s): "An asynchronous call is already in progress. It must be completed or canceled before you can call this method."
At line:3 char:16
+ (1..254) | % { [Void]$ping.SendPingAsync("192.168.0.$_") }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : InvalidOperationException
Which is a shame, not being able to run more than one async ping from the same ping object means we’re going to have to instantiate the $ping and assign the event for every address we want to ping, which makes me feel a little sad, but hey needs must when you’re trying to avoid building your own objects, right?
(1..254) | % {
$ping = New-Object System.Net.NetworkInformation.Ping;
[Void](Register-ObjectEvent $ping PingCompleted -Action {
param($s, $e);
if($e.Reply.Status -eq"Success") {
Write-Host$e.Reply.Address, ($e.Reply.RoundtripTime.toString() + "ms")
}
})
[Void]$ping.SendPingAsync("192.168.0.$_")
}
Did you notice that ForEach-Object got switched out for a percentage symbol? Turns out that there’s an alias setup for that, try running Get-Alias on the command line to see what other aliases are setup already.
So we’re nearly there, lets run it and see what the last issue is:
PS C:\WINDOWS\system32> (1..254) | % {
$ping = New-Object System.Net.NetworkInformation.Ping;
[Void](Register-ObjectEvent $ping PingCompleted -Action {
param($s, $e);
if($e.Reply.Status -eq"Success") {
Write-Host$e.Reply.Address, ($e.Reply.RoundtripTime.toString() + "ms")
}
})
[Void]$ping.SendPingAsync("192.168.0.$_")
}
192.168.0.2 0ms
192.168.0.1 0ms
192.168.0.10 0ms
192.168.0.51 0ms
PS C:\WINDOWS\system32> 192.168.0.160 393ms
192.168.0.158 401ms
192.168.0.157 498ms
So it’s working, and it’s nice and fast too, but the problem is that we’re not waiting for the pings to all finish before we’re returning to the command prompt and we’re messing things up a little.
What we really want is to add all of the tasks to a list and then call wait on them before we finish our command. New arrays are @() so we could create an array and away we go, but there’s a better way – remember that output we suppressed before? Well it turns out if we don’t suppress the output from the Async call then the ForEach will, very helpfully, return them all in a nice collection for our use!
(
(1..254) | % {
$ping = New-Object System.Net.NetworkInformation.Ping;
[Void](Register-ObjectEvent $ping PingCompleted -Action {
param($s, $e);
if($e.Reply.Status -eq"Success") {
Write-Host$e.Reply.Address, ($e.Reply.RoundtripTime.toString() + "ms")
}
})
$ping.SendPingAsync("192.168.0.$_")
}
).Wait()
Ta da, we’ve got a nice little PowerShell command that will ping sweep a bunch of hosts and show which ones respond.
Where do we go from here?
Well there’s lots more we can do here. Firstly we’re not returning a list of online IPs we’re just writing text out, so maybe the next step would be to put them into an array to be returned after the wait has completed so we can pipe it into some other command action we wanted to take.
Also, it’s worth putting it in a ps1 file so we can run this when we want, but to enable scripts we need to enable running PowerShell scripts. I used the following since it’s a shared PC and I don’t want to have to worry about having introduced vulnerability to other users who might not know what they’re running:
PS C:\WINDOWS\system32> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted
For a full accounting of all the options take a look at the TechNet descriptions here, and make sure you understand the implications (and how to put things back when you’ve finished playing) before you go changing security settings.
About Execution Policies article on Microsoft’s Developer Network.
I must confess that the default script policy seems like security paranoia to me – if I can run arbitrary executables with, at best, some fuzzily defined source warnings if I download it from the net, being able to run shell scripts hardly seems like increased vulnerability to me, but I guess it’s easier to introduce security in a new feature like PowerShell scripting than it is to retcon it in for all executables.
Maybe this is a sign of the times where running our own code, or even running code from third parties not explicitly vetted by our device provider, is considered a risk too far, but anything that discourages people from poking around in the world of scripting – which is a great gateway to learn stuff – makes me sad as a code nerd.
On which depressing note, Enjoy PowerShell, it’s actually quite fun!