Ping mit PowerShell

An verschiedenen Stellen nutze ich "Ping" oder besser das "Test-Connection"-Commandlet um die Erreichbarkeit von Systemen vorab zu prüfe oder auch einfache LAN/WAN-Checks zu machen. Dabei gilt es einige Besonderheiten zu beachten, die ich hier beschreibe.

Basis Ping

Wenn man sich in der PowerShell bewegt, sollte man auch die Vorteile der Pipeline und von Rückgabeobjekten nutzen. Wer also aus einer PowerShell einen "PING" machen will, sollte sicher nicht "PING.EXE" aufrufen und dann die Bildschirmausgabe auswerten. Aber auch der Umweg über WMI-Klassen ist heute eigentlich nicht mehr erforderlich. Ein einfaches "Test-Connection" sendet per Default 4 ICMP-Pakete a 32 Byte an die angegebene Gegenstelle und liefert sehr ausführliche Daten zurück. Hier mal ein Beispiel:

PS C:\> Test-Connection -Count 1 192.168.178.1 | fl

__GENUS                        : 2
__CLASS                        : Win32_PingStatus
__SUPERCLASS                   :
__DYNASTY                      : Win32_PingStatus
__RELPATH                      : Win32_PingStatus.Address="192.168.178.1",BufferSize=32,NoFragmentation=FALSE,RecordRoute=0,Resolve
                                 AddressNames=FALSE,SourceRoute="",SourceRouteType=0,Timeout=4000,TimestampRoute=0,TimeToLive=80,Ty
                                 peofService=0
__PROPERTY_COUNT               : 24
__DERIVATION                   : {}
__SERVER                       : NAWNBFC
__NAMESPACE                    : root\cimv2
__PATH                         : \\NAWNBFC\root\cimv2:Win32_PingStatus.Address="192.168.178.1",BufferSize=32,NoFragmentation=FALSE,
                                 RecordRoute=0,ResolveAddressNames=FALSE,SourceRoute="",SourceRouteType=0,Timeout=4000,TimestampRou
                                 te=0,TimeToLive=80,TypeofService=0
Address                        : 192.168.178.1
BufferSize                     : 32
NoFragmentation                : False
PrimaryAddressResolutionStatus : 0
ProtocolAddress                : 192.168.178.1
ProtocolAddressResolved        :
RecordRoute                    : 0
ReplyInconsistency             : False
ReplySize                      : 32
ResolveAddressNames            : False
ResponseTime                   : 3
ResponseTimeToLive             : 64
RouteRecord                    :
RouteRecordResolved            :
SourceRoute                    :
SourceRouteType                : 0
StatusCode                     : 0
Timeout                        : 4000
TimeStampRecord                :
TimeStampRecordAddress         :
TimeStampRecordAddressResolved :
TimestampRoute                 : 0
TimeToLive                     : 80
TypeofService                  : 0
PSComputerName                 : NAWNBFC
IPV4Address                    : 192.168.178.1
IPV6Address                    :

Viele Daten sind rein informativ aber einige Felder sind natürlich für eine weitere Verarbeitung interessant wie z.B.

  • ResponseTime
    Die Zeit in Millisekunden für die Antwort
  • ResponseTimeToLive
    Wenn man davon ausgeht, dass der Absender eine Pakete mit einem TTL=64 sendet, kann man aus der Differenz so die Anzahl der Hops auf dem Rückweg ermitteln. Das ist aber nicht sicher, da der Absender den Startwert für TTL vorgibt.

Übrigens können Sie beim Versenden einen TTL mitgeben und damit die Entfernung abschätzen. Einfach ein PING mit hohem TTL senden um die generelle Erreichbarkeit zu prüfen und dann mit aufsteigendem TTL die Entfernung ermitteln.

Wenn die Gegenstelle aber nicht erreichbar ist, dann kommt gar nichts zurück und es gibt eine Exception, die Sie natürlich abfangen müssen:

PS D:\> Test-Connection -Count 1 192.168.178.2 | fl
Test-Connection : Fehler beim Testen der Verbindung mit dem Computer "192.168.178.2": Fehler aufgrund von zu wenigen Ressourcen
In Zeile:1 Zeichen:1
+ Test-Connection -Count 1 192.168.178.2 | fl
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ResourceUnavailable: (192.168.178.2:String) [Test-Connection], PingException
    + FullyQualifiedErrorId : TestConnectionException,Microsoft.PowerShell.Commands.TestConnectionCommand

Ein Try/Catch ist angebracht, wenn Sie auf den zurückgegebenen Daten weitere Informationen erwarten. Alternativ können Sie

Test-Connection -Count 1 192.168.178.2 -ErrorAction silentlycontinue -ErrorVariable c

Dann sollten Sie aber die Variable "$c" danach abfragen und auch nicht vergessen, dass der Fehler zusätzlich auch in $Error hinterlegt wird. Nicht dass eine spätere Anfrage nach $Error sie irrtümlich einen Fehler annehmen lässt.

"Stiller" Ping

Eine Sonderform erhalten Sie mit dem Parameter "-quiet". Dann gibt es keine Details aber auch keinen Fehler sondern nur ein "False" oder "True" als Antwort.

Test-Connection -Count 1 192.168.178.2 -Quiet
False

Test-Connection -Count 1 192.168.178.1 -Quiet
True

Diese Funktion erfordert auch keine Fehlerbehandlung und es wird auch nichts in $error geloggt. Insofern ist dies der bessere Weg um die Erreichbarkeit eines Hosts abzufragen wenn die Dauer nicht relevant ist.

Paralleles Ping

Wenn ich mehrere Systeme prüfen will, kann ich natürlich mehrere "Test-Connection"-Aufrufe nacheinander ketten. Der Parameter "Computer" erlaubt aber auch die Angabe einer Liste von Gegenstellen. Das können Sie recht einfach testen mit:

PS D:\> Test-Connection -ComputerName www.msxfaq.de,www.netatwork.de,www.heise.de -Count 2

Source  Destination      IPV4Address    IPV6Address Bytes Time(ms)
------  -----------      -----------    ----------- ----- --------
NAWNBFC www.heise.de     193.99.144.85              32    25
NAWNBFC www.msxfaq.de    217.160.0.234              32    28
NAWNBFC www.netatwork.de 178.77.109.224             32    38
NAWNBFC www.heise.de     193.99.144.85              32    27
NAWNBFC www.msxfaq.de    217.160.0.234              32    30
NAWNBFC www.netatwork.de 178.77.109.224             32    38

Leider macht Test-Connection hier keine Parallelisierung und pro "Ping" wartet er am Ende auch wieder eine Sekunde, so dass die Abfrage der drei Hosts drei Sekunden dauert. Test-Connection erlaubt allerdings auch kein Übergabe der Hostnamen als Pipeline, so dass Sie mit einer Loop erst zum Ziel kommen.

"www.msxfaq.de","www.netatwork.de","www.heise.de" `
| %{Test-Connection $_ -Count 1}

So geht es schneller aber noch nicht "parallel". Wenn ein Host nicht erreichbar ist, dann wartet Test-Connection auf den Timeout.

Mit PowerShell Core können Sie aber auch bei einer For-Schleife mit "-parallel" mehrere Skript-Blöke gleichzeitig abarbeiten lassen, z.B.

$result= 1..254 | % -Parallel {test-connection "192.168.100.$($_)" -Count 2}
$result

$result | ft destination,status

Destination                         Status
-----------                         ------
192.168.100.5                      Success
192.168.100.5                      Success
192.168.100.4   DestinationHostUnreachable
192.168.100.3   DestinationHostUnreachable
192.168.100.2   DestinationHostUnreachable
...

PowerShell hat ein Standard-Limit von 5 Thread aber das kann natürlich auch mit dem Parameter "-ThrottleLimit" angepasst werden.

Test-Connection und "-AsJob"

Sehr viele Commandlets lassen sich einfach mit dem Parameter "-asJob" in den Hintergrund schicken. Da Test-Connection auch diesen Parameter kennt, habe ich das doch gleich mal ausprobiert. mit mehreren Hosts wäre der Aufruf dann ja sehr einfach. Ich habe mal schnell versucht, mit der Liste einfach entsprechende "Dauerping"-Jobs loszusetzen. Das geht ja recht schnell mit

# Skript funktioniert nicht mehr mit PSCore, da hier der "-asjob"-Parameter weggefallen ist.
foreach ($entry in import-csv .\hostlist.csv) {
   Test-Connection `
      -asjob `
      -buffersize 512 `
      -delay 1 `
      -count 300
}

Dann müsste ich mit einem Receive Job einfach nur die Daten einsammeln und visualisieren. Ich habe es dann doch erst mal manuell gemacht. Einmal manuell und einmal als "job

Test-Connection www.msxfaq.de -Count 4
Test-Connection www.msxfaq.de -Count 4 -asjob

Der erste Aufruf hat wie erwartet 4 "PING" mit einem Abstand von 1 Sekunde versendet und empfangen. Der zweite Befehl hat einen Job angelegt, der das Gleiche im Hintergrund tun sollte. Ich sage absichtlich "sollte", denn als ich mit Get-Job den Status überprüfen wollte, war er schon "Completed". Als ich die Rückgabe mit "Receive-Job" abgeholt habe, habe ich aber vier Ergebnisse gesehen. Irgendwas war falsch gelaufen. Ich habe die gleiche Prozedur noch mal mit Wireshark mitgeschnitten.

Der Aufruf mit "AsJob" feuert ganz schnell sogar parallel die PINGs. ab. Ich habe dann mit den Parametern experimentiert aber keine Besserung erreicht. In der Betriebsart mit "-asJob" scheint der Parameter "-Delay" fest mit "0" hinterlegt zu sein und das Commandlet flutet das Netzwerk. Wenn ich aber den Parameter "Delay" angebe, dann muss er in Sekunden angegeben werden und "0" ist nicht erlaubt. Sehr suspektes Verhalten.

Wenn ich aber z.B. 10 Pings in einer Sekunden absenden will, dann geht das nur wie folgt:

1..10  | % {
   test-connection www.msxfaq.de -count 1
   start-sleep -milliseconds 100
}

So kommt "TestConnection" sofort wieder zurück und kann wieder gestartet werden. Allerdings sollten Sie das nicht mit jeder Gegenstelle machen, da Firewalls natürlich sehr schnell so einen "Ping Flood" unterbrechen, was die PowerShell so meldet:

Passend dazu das Paket in Wireshark:

Im internen LAN/WAN kann man aber wohl damit arbeiten. So darf ich das Skript also nicht starten. Wenn ich viele Host "Parallel" abprüfen will, dann muss ich das mit einem Start-Job machen.

foreach ($entry in import-csv .\hostlist.csv) {
   Start-Job `
      -ArgumentList @($entry) `
      -ScriptBlock { `
            Test-Connection `
               -ComputerName $Args[0] 
               -buffersize 512 `
               -delay 1 `
               -count 300
}

Die einzelnen Jobs laufen in dem Beispiel per Default schon einmal 5 Minuten aber stoppen dann alleine. Allerdings ist Start-Job nicht immer besonders ressourcenschonend.

Messen

Ein Ping ist für eine Messung nicht repräsentativ aber 5 Minuten möchte ich auch nicht auf ein Ergebnis warten. Daher habe ich mir überlegt, dass ich alle 5 Sekunden ein Update mache und dazu 5 Pings mit 1KB Puffergröße sende. Die Auswertung kann darüber denn den Mittelweg, Max, Min erstellen und ausgeben. Ich erstelle also alle 5 Sekunden für jedes Ziel einen Job, der Pings versendet und die Ergebnisse dann liefert.

Achtung: Die Rückgabe von "Test-Connection" hat sich mit PowerShell 6 geändert. Statt "ResponseTime" muss "Latency" genutzt werden oder sie messen direkt den Aufruf von Test-Connection

1..50 `
| %{ `
   Test-Connection 8.8.8.8 `
      -Count 1  `
      -BufferSize 1kb ;  `
   start-sleep  `
      -Milliseconds 50}  `
| Measure-Object Responsetime `
   -Minimum `
   -Maximum `
   -Average

Dieser Code wird einmal pro Ziel aufgerufen und am Ende sammle ich von allen Jobs die Ergebnisse ein und zeige sie an.

Get-Content `
   -path C:\Utilities\servers.txt `
| ForEach-Object { `
      Test-Connection `
         -ComputerName $_ `
         -Count 1 `
         -AsJob `
   }`
| Get-Job `
| Receive-Job `
   -Wait `
| Select-Object @{Name='ComputerName';Expression={$_.Address}},`
                @{Name='Reachable';Expression={if ($_.StatusCode -eq 0) { $true } else { $false }}} `
| ft -AutoSize ResponseTimeToLive

SendPingAsync

Wenn ich nicht per "Test-Connection" sondern direkt über die .NET-Klasse "System.Net.NetworkInformation.Ping" gehen, können Sie neben der Methode "SendPing" auch ein "SendPingAsync" machen. Ihr Code wartet dann nicht auf die Antwort oder dem Timeout, sondern kann weiter arbeiten. Sie können damit auch mehrere Pings nacheinander abfeuern. Sie müssen natürlich über einen weiteren Code dann die Rückantworten wieder einsammeln und verarbeiten. Die in den Hintergrund gelegten Aufrufe sind aber keine Jobs sondern generieren einen Event, den Sie per PowerShell dann abholen müssen. Der Code ist dann schon etwas kniffliger zu lesen.

$ipaddress="192.168.178.1"
$timeout=5000

$ping1 = New-Object System.Net.NetworkInformation.Ping
Register-ObjectEvent `
      -InputObject $ping1 `
      -EventName PingCompleted `
      -SourceIdentifier "PingEvent1"

# Diese drei Aufrufe 
$ping1.SendAsync($ipaddress, $timeout, $ping1)
$ping1.SendAsync("192.168.178.50", $timeout, $ping1)
$ping1.SendAsync("192.168.178.51", 10, $ping1)
$ping1.SendAsync("192.168.178.200", $timeout, $ping1)

while (!(Get-Event "PingEvent1")) {
   Start-Sleep -Milliseconds 100
}
(Get-Event "PingEvent1").SourceEventArgs.reply

Ich habe hier absichtlich vier PING1.SendAsync-Aufrufe addiert, um auch zwei mögliche Fehler aufzuzeigen. In meinem Fall sieht die Ausgabe wie folgt aus:

PS C:> (Get-Event "PingEvent1").SourceEventArgs.reply

Status        : DestinationHostUnreachable
Address       : 192.168.178.50
RoundtripTime : 0
Options       :
Buffer        : {}

Status        : Timeout
Address       : 0.0.0.0
RoundtripTime : 0
Options       :
Buffer        : {}

Status        : Success
Address       : 192.168.178.1
RoundtripTime : 1
Options       : System.Net.NetworkInformation.PingOptions
Buffer        : {97, 98, 99, 100…}

Sie sehen hier nur drei statt vier Ergebnisse und zudem fällt auf, dass bei dem Ping mit einem sehr kurzen "Timeout" auch der Fehler "Timeout" kommt. Allerdings ist dann die "Address" mit "0.0.0.0" gefüllt und nicht mit der "192.168.178.51". Ich kann aus dem Event bei diesem Fehler also nicht mehr erkennen, welche IP-Adresse ich angesprochen habe. Daher sollte ich beim Register-Event über einen individuelle "SourceIdentifier" sicherstellen, dass ich die Antwort auch zuordnen kann. Wenn ich die Ergebnisse anders ausgebe, dann sehen Sie vier Events:

PS C:\> (Get-Event).SourceEventArgs | fl *

Reply     : System.Net.NetworkInformation.PingReply
Cancelled : False
Error     :
UserState : System.Net.NetworkInformation.Ping

Reply     : System.Net.NetworkInformation.PingReply
Cancelled : False
Error     :
UserState : System.Net.NetworkInformation.Ping

Reply     : System.Net.NetworkInformation.PingReply
Cancelled : False
Error     :
UserState : System.Net.NetworkInformation.Ping

Reply     :
Cancelled : False
Error     : System.AggregateException: One or more errors occurred. (An asynchronous call is
            already in progress. It must be completed or canceled before you can call this method.)
             ---> System.InvalidOperationException: An asynchronous call is already in progress.
            It must be completed or canceled before you can call this method.
               at System.Net.NetworkInformation.Ping.CheckStart()
               at System.Net.NetworkInformation.Ping.SendPingAsyncInternal(IPAddress address,
            Int32 timeout, Byte[] buffer, PingOptions options)
               --- End of inner exception stack trace ---
UserState : System.Net.NetworkInformation.Ping

Ein weitere Einschränkung besteht darin, dass ein "SendAync" pro Objekt nur einmal aufgerufen werden kann. Ich muss dann also für jedem Aufruf eine eigenes "System.Net.NetworkInformation.Ping"-Objekt instanzieren um einen SendAsync-Aufruf zu starten. Ansonsten wird gar nichts gesendet. Ich habe mal versucht das in ein PowerShell-Skript zu pressen.

Ping mit PowerShell

Es gibt im Internet aber auch noch sehr viele andere vergleichbare Skripte. Ganz besonders ist die Seite von https://learn-powershell.net, welche auch auf die Performance eingeht.

Well, it means that most of the time, you could get by with Test-Connection using AsJob assuming that your systems have remoting enabled. But if you wanted to quick way to look at hundreds of IPs or hostnames, then using the Async approach will not fail you.
https://learn-powershell.net/2016/04/22/speedy-ping-using-powershell/

So schlecht ist aber "Test-Connection -AsJob" wohl nicht, um die Daten dann vom Job abzurufen. Allerdings ist dieser Parameter mit PowerShell Core nicht mehr verfügbar. Dann wird es doch wieder ein "Start-Job" oder aber PowerShell Code eventuell eine "For-Schleife" mit dem Parameter "-parallel".

Offen

Leider habe ich noch keinen Weg gefunden, wie ich per ICMP zurück erhaltene Pakete parsen kann. Gerade beim Versand von zu großen Paketen per ICMP in Verbindung mit dem "Don't Fragment (DF)"-Flag kommt ja ein "ICMP Size Exceeded" zurück. Dort steht aber sowohl die MTU Size als auch die IP-Adresse des generierenden Systems drin. Auch andere ICMP-Rückmeldungen wären für weitere Auswertungen eine interessante Quelle. Eventuell muss man tatsächlich ICMP komplette selbst empfangen und auswerten.

Aufgrund von Missbrauchs durch Viren um das Jahr 2000 hat Microsoft die Verwendung von Raw-Sockets auf Nicht-Server-Editionen des Windows-Betriebssystems eingeschränkt.

On Windows 7, Windows Vista, Windows XP with Service Pack 2 (SP2), and Windows XP with Service Pack 3 (SP3), the ability to send traffic over raw sockets has been restricted in several ways:
https://docs.microsoft.com/en-us/windows/win32/winsock/tcp-ip-raw-sockets-2#limitations-on-raw-sockets

Das scheint aber gar nicht so einfach zu sein. Microsoft selbst hat das Commandlet "Test-Connection" mit der Option "MTUSizeDetect" ausgestattet und die macht auch ein "Annähern" an die MTU-Size durch mehrere Versuche. Eine genauere Beschreibung dazu finden Sie auf MTU.

Weitere Links