Ich habe immer wieder die Anforderung, mit ICMP-Echorequests (ping) die Erreichbarkeit von IP-Adressen zu prüfen. Vielleicht kennt ihr das ja aus eurem eigenen Alltag. Sehr beliebt ist diese Aufgabenstellung bei der Suche nach einer freien IP-Adresse.
Mit der PowerShell gibt es natürlich entsprechende Cmdlets, z.B test-connection. Aber diese haben Nachteile:
- IP-Adressen werden nacheinander angepingt. Sehr viele Adressen aznzupingen benötigt also viel Zeit.
- Jeder EchoRequest wartet bis zu 1 Sekunde auf einen EchoReply. Sind also sehr viele IP-Adressen nicht erreichbar, dann geht die Laufzeit ins Ungenießbare.
Es muss also eine Alternative her. Den Einstieg zur Optimierung habe ich in dieser kleinen Anweisung gefunden. Ich spreche die Methode Send der Funktion Ping direkt an. Hier kann ich den Timeout-Wert einstellen:

Und das ist gerade für lokale Netzwerke mit kurzen Paketlaufzeiten interessant: Statt bei jeder nicht erreichbaren IP-Adresse 1000 Millisekunden zu warten kann ich so den Timeout auf z.B.. 10 Millisekunden heruntersetzen!
Das habe ich in eine PowerShell-Funktion integriert. Diese erwartet beim Aufruf eine Menge von IP-Adressen. Das stellte mich vor eine weitere Herausforderung: Ich möchte z.B. ein ganzes Netzwerksegment alleine durch die Angabe der NetzID anpingen. So wird aus der Eingabe 192.168.1.0/24 die Menge der IP-Adressen 192.168.1.1 bis 192.168.1.254 – und diese Menge soll meine Funktion natürlich selber berechnen… Es war spät und mir kam nur die Idee mit dem Umweg über die binäre Schreibweise von IP-Adressen:

Ich konvertierte also eine IP-Adresse von Decimal nach Binary, erhöhe den Binary-Wert um eins und konvertiere das Ergebnis zurück nach Decimal. Das ist dann die FolgeIP. Den Prozess kann ich zwischen einer StartIP und einer EndIP oder von der ersten bist zur letzten IP eines Subnetzes wiederholen. Danach kenne ich alle IP-Adressen.
Das sind meine drei kleinen Helfer-Funktionen:



Diese habe ich in eine finale Funktion eingebaut, die ich bequem aufrufen kann.
Als kleines Extra wollte ich mich aber nicht mit der linearen Abarbeitung der IP-Adressen zufrieden geben. Daher habe ich meine Funktion pinge-Netzwerk mit einer Parallelisierung aufgewertet. Die zu pingenden IP-Adressen werden bei Bedarf durch PowerShell-Jobs in eigenen Unterprozessen angepingt. Die Menge der IPs wird dabei schön aufgeteilt. Das kann man sich so vorstellen:

Im blauen PowerShell-Prozess wird die Funktion gestartet. Dabei sollen 254 IP-Adressen angepingt werden. Die Powershell startet 4 Arbeitsprozesse – dieses sind ChildProcess-Objekte unter der eigentlichen PowerShell. Im ProcMon von Sysinternals kann man das gut sehen:

Jeder der Arbeitsprozesse erhält eine Teilmenge der zu pingenden IP-Adressen. Alle Arbeitsprozesse arbeiten parallel. Die PowerShell selber arbeitet als Controller und überwacht den Fortschritt. Sind alle Arbeitsprozesse fertig, dann holt sich die PowerShell deren Ergebnisse und stellt sie als Ausgabe zusammen:

Das fertige Script habe ich zur besseren Strukturierung in einzelne Regionen unterteilt. Damit kann man relativ einfach durch die Anweisungen navigieren:

Im oberen Block sind die drei Hilfsfunktionen integriert. Die Parametervalidierung soll Fehleingaben wie z.B. die IP-Adresse 192.168.1.500 verhindern. In der Region „Bestimmung der IP-Adressen“ wird die Menge der IPs ermittelt. Den eigentlichen Ping habe ich sowohl linear als auch in der Parallel-Variante integriert. Dabei kann die Funktion die Anzahl der Arbeitsprozesse auch selber bestimmen. Abschließend wird die Ausgabe zusammengestellt. Eigentlich recht einfach, oder?
Das hier ist der finale Scriptcode inklusive der Hilfe:
function pinge-Netzwerk { <# .Notes Scriptreihe: pinge-Netzwerk Datum: 2020-05-13 Version: V1.04 Programmierer: Stephan Walther .Synopsis Die Funktion sendet optimiert ICMP-Echorequsts (ping) an eine Menge von IP-Adressen. .DESCRIPTION Mit dieser Funktion kann ein Bereich oder ein Subnetz schnell angepingt werden. Optimierungen wurden durch die Herabsetzung des Timeouts bei Nichterreichbarkeit und durch Parallelisierung erreicht. .EXAMPLE pinge-Netzwerk -Subnetz 192.168.100.0/24 Die Funktion ermittelt aus der angegebenen SubnetzID die IP-Adressen 192.168.100.1 bis 192.168.100.254 und pingt sie nacheinander. .EXAMPLE pinge-Netzwerk -Subnetz 192.168.100.0/27 -parallel -AnzahlProzesse 3 Die Funktion ermittelt die im Subnetz enthaltenen IP-Adressen und teilt sie auf 3 Arbeitsprozesse auf. Die 3 Prozesse pingen parallel. Nach Abschluss werden alle Adressen sortiert ausgegeben. .EXAMPLE pinge-Netzwerk -StartIP 192.168.100.1 -EndIP 192.168.100.25 Die Funktion ermittelt die IP-Adressen zwischen der Start und der End-IP. Anschließend werden alle IPs (inklusive Start und Ende) hintereinander angepingt. .EXAMPLE pinge-Netzwerk -Subnetz 192.168.100.0/27 -timeout 3 Die Funktion pingt die angegebenen IP-Adressen. Es wird maximal 3 Millisekunden auf eine Antwort gewartet. Beim Überschreiten des timeout wird die IP-Adresse als nicht erreichbar gewertet. .EXAMPLE pinge-Netzwerk -StartIP 192.168.100.1 -EndIP 192.168.100.10 -Filter TimedOut Die Funktion pingt die angegebenen IPs. Ausgegeben werden aber nur IP-Adressen mit fehlgeschlagenem ping. .EXAMPLE pinge-Netzwerk -Subnetz 192.168.100.0/24 -parallel Die angegebenen IP-Adressen werden angepingt. Die Funktion bestimmt dabei selber die Anzahl der Arbeitsprozesse, die parallel im Hintergrund ausgeführt werden. .PARAMETER Subnetz Der Parameter erwartet eine SubnetzID und eine SubnetzMaske in der CIDR-Notation. Z.B. 172.16.0.0/16 Mit der Verwendung dieses Parameters kann keine StartIP und keine EndIP angegeben werden. .PARAMETER StartIP Der Parameter erwartet eine IP-Adresse vom Typ v4 ohne Angabe einer SubnetzMaske. Zusätzlich muss der Parameter -EndIP angegeben werden. Die Verwendung von -StartIP schließt die Verwendung des Parameters -Subnetz aus. .PARAMETER EndIP Der Parameter erwartet eine IP-Adresse vom Typ v4 ohne Angabe einer SubnetzMaske. Zusätzlich muss der Parameter -StartIP angegeben werden. Die Verwendung von -EndIP schließt die Verwendung des Parameters -Subnetz aus. .PARAMETER TimeOut Es wird ein Integer aus dem Wertebereich 3, 10, 50, 100, 500, 1000 erwartet. Der Standardwert ist 10. Mit dem Parameter wird die Wartezeit auf ein EchoReply in Millisekunden angegeben. Nach Ablauf des TimeOuts wird eine IP-Adresse als nicht erreichbar gewertet. .PARAMETER parallel Mit diesem Schalter kann zwischen der linearen und der parallelen Arbeitsweise umgeschaltet werden. Im Standard werden die IP-Adressen hintereinander ohne zusätzliche Arbeitsprozesse angepingt. Das kann bei besonders kleinen IP-Mengen schneller sein. Bei größeren Mengen bietet sich die parallele Arbeitsweise an. Mit dem Zusatzparameter -AnzahlProzesse kann die Anzahl der Arbeitsprozesse definiert werden. Ohne diesen Zusatzschalter ermittelt -parallel selber eine optimale Anzahl von Arbeitsprozessen. .PARAMETER AnzahlProzesse Hier kann die Anzahl der Arbeitsprozesse bei der parallelen Abarbeitung angegeben werden. Erlaubt sind die Werte zwischen 2 und 10. Mit der Angabe dieses Parameters wird -parallel automatisch verwendet. .PARAMETER Filter Das Ergebnis der ping-Aufrufe wird als Tabelle ausgegeben. Dabei kann ein Einzelergebnis "success" oder "timedout" sein. Mit dem Parameter -Filter kann man gezielt nach erfolgreichen oder fehlgeschlagenen ping-Ergebnissen suchen. Ohne Angabe des Parameters werden alle Ergebnisse ausgegeben. #> param( [parameter(Mandatory=$true,ParameterSetName="Subnet")] [string] $Subnetz, [parameter(Mandatory=$true,ParameterSetName="Range")] [ipaddress] $StartIP, [parameter(Mandatory=$true,ParameterSetName="Range")] [ipaddress] $EndIP, [parameter(Mandatory=$false)] [validateSet(3,10,50,100,500,1000)] [int] $TimeOut = 10, [parameter(Mandatory=$false)] [switch] $parallel = $false, [parameter(Mandatory=$false)] [validaterange(2,10)] [int] $AnzahlProzesse = 0, [parameter(Mandatory=$false)] [validateSet('*','Success','TimedOut')] [string] $Filter = '*' ) #region Hilfsfunktionen function tmp_IPDecimal2Binary { param($IP) $Binary = "" ($IP -split "\.") | ForEach-Object { $BinaryValue = [convert]::ToString($_,2) $OctetInBinary = ("0" * (8 - ($BinaryValue).Length) + $BinaryValue) $Binary += $OctetInBinary } Return $Binary } function tmp_IPBinary2Decimal { param($Binary) $IPInDecimal = @() 0..3 | ForEach-Object { $IPInDecimal += [convert]::ToInt32($Binary.Substring(8*$_,8),2) } $IPInDecimal = $IPInDecimal -join '.' Return $IPInDecimal } function tmp_ping { param($IP,$TimeOut) try { $ping = New-Object -TypeName System.Net.NetworkInformation.Ping $reply = $ping.Send($IP,$timeout) $result = $reply.Status } catch { $result = "error" } $out = New-Object -TypeName PSObject $out | Add-Member -MemberType NoteProperty -Name IP -Value ([ipaddress] $IP) $out | Add-Member -MemberType NoteProperty -Name Result -Value $result Return $out } #endregion #region Validierung der Parameter if ($PSBoundParameters.Keys -contains 'Subnetz') { if ($Subnetz -notmatch "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}") { Write-Host "$Subnetz ist kein Subnetz in der CIDR-Notation! Verwende z.B. 192.168.1.0/24" -ForegroundColor Yellow break } else { $SubnetIP,$Mask = $Subnetz -split '/' IF ($Mask -gt 32 -or $Mask -le 0) { Write-Host "Die Subnetzmaske muss zwischen 1 und 32 liegen!" -ForegroundColor Yellow break } try { [ipaddress]::Parse($SubnetIP) | Out-Null } catch { Write-Host "$SubnetIP ist keine SubnetzIP!" -ForegroundColor Yellow break } } Write-verbose "Subnet = $SubnetIP" Write-verbose "Mask = $Mask" } else { try { $curIP = $StartIP ; [ipaddress]::Parse($curIP) | Out-Null $curIP = $EndIP ; [ipaddress]::Parse($curIP) | Out-Null } catch { Write-Host "$curIP ist keine IP-Adresse!" -ForegroundColor Yellow break } } #endregion #region Bestimmung der IP-Adressen if ($PSBoundParameters.Keys -contains 'Subnetz') { # Konvertierung der Subnetz-IP von Decimal in Binary $SubnetIPInBinary = tmp_IPDecimal2Binary -IP $SubnetIP # Anzahl der IPs bestimmen $AnzIPs = [convert]::ToInt32('1' * (32 - $Mask),2) - 1 # alle IPs finden $IPAdressen = @() $CurBin = $SubnetIPInBinary.Substring(0,$Mask) + ("0" * (31-$Mask)) + "1" 1..$AnzIPs | ForEach-Object { # aktuelle IP in Decimal ermitteln $IPAdressen += tmp_IPBinary2Decimal -Binary $CurBin # nächste IP in Binary ermitteln $CurInt = [convert]::ToInt64($CurBin,2) $NextInt = $CurInt + 1 $CurBin = [convert]::ToString($NextInt,2) } } else { # Sortierung der IPs $StartIP,$EndIP = $StartIP,$EndIP | Sort-Object # Konvertierung der IPs von Decimal in Binary $StartIPInBinary = tmp_IPDecimal2Binary -IP $StartIP $EndIPInBinary = tmp_IPDecimal2Binary -IP $EndIP # alle IPs finden $IPAdressen = @() $CurBin = $StartIPInBinary do { # aktuelle IP in Decimal ermitteln $IPAdressen += tmp_IPBinary2Decimal -Binary $CurBin # nächste IP in Binary ermitteln $CurInt = [convert]::ToInt64($CurBin,2) $NextInt = $CurInt + 1 $CurBin = [convert]::ToString($NextInt,2) } while ( $CurBin -le $EndIPInBinary ) } #endregion #region Vorbereitung von Variablen $Ergebnis = @() $ZeitStart = Get-Date #endregion #region automatische Parallelisierung? if ($parallel -eq $true -or $AnzahlProzesse -ge 2) { # ggf. Festlegung der Prozessanzahl if ($AnzahlProzesse -eq 0) { $Divider = [math]::Max($IPAdressen.count,25) # Der Teiler darf nicht größer als die Anzahl der IPs sein $AnzahlProzesseInt = [int] ($IPAdressen.count / $Divider) # optimal: 25 Adressen je Prozess if ($AnzahlProzesseInt -gt 10) { $AnzahlProzesseInt = 10 } # optimal: max. 10 Prozesse if ($AnzahlProzesseInt -eq 1) { $AnzahlProzesseInt = 0 # keine Parallelisierung! $parallel = $false Write-Verbose "automatische Parallelisierung lohnt sich nicht ==> Umstellung auf linearen Ping" } else { Write-Verbose "automatische Parallelisierung = $AnzahlProzesseInt Prozesse" } } else { $AnzahlProzesseInt = $AnzahlProzesse } } else { $AnzahlProzesseInt = 0 } #endregion #region Ausführung der Pings (parallel) if ($parallel -eq $true -and $AnzahlProzesseInt -ge 2) { # Teile die IPs für die Subprozesse auf write-verbose "starte $AnzahlProzesse Jobs" $Submenge = (($IPAdressen.count - 1) / $AnzahlProzesseInt) if ([int] $SubMenge -le $SubMenge) { $SubMenge = [int] $SubMenge + 1 } else { $SubMenge = [int] $SubMenge } # Variablen $jobs = @() $erledigt = @() # PingCode für die Jobs auslesen $pingcode = @() $pingcode += 'function tmp_ping {' $pingcode += (Get-Command -Name tmp_ping).definition $pingcode += '}' $pingcode = $pingcode -join "`r`n" # Jobs starten 1..$AnzahlProzesseInt | ForEach-Object { $IPAdressenTeil = $IPAdressen | Where-Object { $erledigt -notcontains $_ } | Get-Random -Count $SubMenge $erledigt += $IPAdressenTeil $jobs += Start-Job -Name "#$_" -ScriptBlock { param( $IPAdressenTeil,$TimeOut,$pingcode ) Invoke-Expression -Command $pingcode $IPAdressenTeil | ForEach-Object { tmp_ping -IP $_ -TimeOut $timeout } } -ArgumentList $IPAdressenTeil,$TimeOut,$pingcode Write-Verbose -Message " Job #$_ gestartet" } # warte auf Ergebnisse write-verbose "warte auf Ergebnisse" $fertig = $false $Ergebnis = @() $cnt = 0 $ttl = (Get-Job).count Write-Progress -Activity "pinge parallel mit $AnzahlProzesseInt Prozessen" -PercentComplete 0 do { if ((Get-Job | Where-Object {$_.State -eq 'running'}) -eq $null) {$fertig = $true} Get-Job | Where-Object {$_.State -ne 'running'} | ForEach-Object { $Ergebnis += Receive-Job -Id $_.id Write-Verbose " Job $($_.name) ist fertig" Remove-Job -Id $_.id $cnt += 1 } $percent = [math]::min(100,[int]($cnt/$ttl*100)) Write-Progress -Activity "pinge parallel mit $AnzahlProzesseInt Prozessen" -PercentComplete $percent Start-Sleep -Seconds 1 } while (-not $fertig) } #endregion #region Ausführung der Pings (linear) if ($parallel -eq $false -and $AnzahlProzesseInt -eq 0) { write-verbose "pinge linear" $cnt = 0 $ttl = $IPAdressen.count Write-Progress -Activity 'pinge' -PercentComplete 0 $IPAdressen | ForEach-Object { $Ergebnis += tmp_ping -IP $_ -TimeOut $timeout $cnt += 1 $percent = [math]::min(100,[int]($cnt/$ttl*100)) Write-Progress -Activity 'pinge' -PercentComplete $percent } } Write-Progress -Activity 'pinge' -Completed #endregion #region Ausgabe # Filter anwenden if ($Filter -ne '*') { $Ergebnis = $Ergebnis | Where-Object { $_.Result -eq $Filter } } # Optimierung und Bereinigung $Ergebnis = $Ergebnis | Select-Object -Property @{ n="sort" ; e={$_.ip.address}},IP,Result | Sort-Object -Property sort | Select-Object -Property IP,Result # Informationen $ZeitEnde = Get-Date Write-Verbose "Laufzeit = $( [math]::round(($ZeitEnde - $ZeitStart).totalseconds,2) )" # Ausgabe Return $Ergebnis #endregion }
Das sind nun mögliche Aufrufe:
Aufruf | Funktion |
---|---|
pinge-Netzwerk -Subnetz 192.168.100.0/27 -parallel -AnzahlProzesse 3 | Die IP-Adressen von 192.168.100.1 bis 192.168.100.30 werden in 3 Arbeitsprozessen gepingt |
pinge-Netzwerk -StartIP 192.168.100.1 -EndIP 192.168.100.25 -AnzahlProzesse 6 | Die IP-Adressen von 192.168.100.1 bis 192.168.100.25 werden in 6 Arbeitsprozessen gepingt. |
pinge-Netzwerk -StartIP 192.168.100.1 -EndIP 192.168.100.10 | Die IP-Adressen von 192.168.100.1 bis 192.168.100.10 werden hintereinander ohne Arbeitsprozesse gepingt. |
pinge-Netzwerk -StartIP 192.168.100.1 -EndIP 192.168.100.10 -Filter TimedOut | IP-Adressen von 192.168.100.1 bis 192.168.100.10 werden hintereinander ohne Arbeitsprozesse gepingt. Ausgegeben werden nur fehlgeschlagene pings. |
pinge-Netzwerk -Subnetz 192.168.100.0/27 -TimeOut 3 -parallel -Verbose | Die IP-Adressen von 192.168.100.1 bis 192.168.100.30 werden mit einem maximalen Timeout von 3 Millisekunden gepingt. Die PowerShell ermittelt selbstständig die Anzahl der Arbeitsprozesse. Es werden Zusatzinformationen ausgegeben. |
Die Funktion ist fertig. Ich habe sie in mein Standard-Funktionsmodul integriert. So steht sie mir bei jedem PowerShell-Start zur Verfügung. Und hier gibt es noch das zip-Archiv mit dem Script.
Stay tuned!