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!