FITS.NetUtils.psm1
|
#region private helper functions (not exported) function SwapEndianness { param( [ValidateScript({ if ($_ -is [uint64] -or $_ -is [uint32]) { $true } else { write-warning "Invalid datatype" } })] [Alias("x")] $SwapValue ) if ($SwapValue -is [uint64]) { [uint64]$swapmask = 0xff [uint64]$retValue = 0 $NumberOfBytes = 8 } elseif ($SwapValue -is [uint32]) { [uint32]$swapmask = 0xff [uint32]$retValue = 0 $NumberOfBytes = 4 } else { throw "Invalid data type" } $NumberOfBitsPerByte = 8 # Average execution time after 10,000 iterations: 1480.1276 ticks or 0.14801276 milliseconds <# calculation of the shift values is as follows: 64 bit unsigned integer is divided into 8 bytes each byte is 8 bits long the first byte is shifted 56 bits to the left to bring it to the position of the last byte the second byte is shifted 40 bits to the left to bring it to the position of the second to last byte the third byte is shifted 24 bits to the left to bring it to the position of the third to last byte the fourth byte is shifted 8 bits to the left to bring it to the position of the fourth to last byte the fifth byte is shifted 8 bits to the right to bring it to the position of the fifth to last byte the sixth byte is shifted 24 bits to the right to bring it to the position of the sixth to last byte the seventh byte is shifted 40 bits to the right to bring it to the position of the seventh to last byte the eighth byte is shifted 56 bits to the right to bring it to the position of the first byte the shift values are calculated as follows: b1 is the byte number starting from 8 b2 is the byte number starting from 1 b1 is multipliedby 8 and decremented by b2*8 to get the shift value for the first and the last byte to get for the other bytes b1 is decremented by 1 after each iteration and b2 is incremented by 1 after each iteration to get the shift value for the swap mask b2 is decremented by 1 first and than multiplied by 8 or in short: y=1 x=n-th byte number. In the case of 64 bit integer=8 [shift bits for value] x * 8 - y * 8 [shift bits for mask] (y-1)*8 #> $NumberOfShifts = $NumberOfBytes / 2 $b1 = $NumberOfBytes for ($b2 = 1; $b2 -le $NumberOfShifts; $b2++) { $shiftForValue = $($b1 * $NumberOfBitsPerByte - $b2 * $NumberOfBitsPerByte) $shiftForMask = ($b2 - 1) * $NumberOfBitsPerByte # for the bytes that must be shifted to the left you must first shift the mask and do a bitwise and with the value than shift the result to the left $retValue = $retValue -bor (($swapValue -band ($swapmask -shl $shiftForMask)) -shl $shiftForValue) #for the bytes that must be shifted to the right you must first shift the value to the right and then shift the mask and do a bitwise and results $retValue = $retValue -bor (($swapValue -shr $shiftForValue) -band ($swapmask -shl $shiftForMask)) #to get all together do a binary or with the result $b1-- } return $retValue } function Convert-DateTimeToNTPTimestamp { param( [DateTime] $dateTime ) $Epoch = [DateTime]::new(1900, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) $TicksPerSecond = [timespan]::TicksPerSecond [double]$ticks = [UInt64]($dateTime.Ticks - $Epoch.Ticks) [double]$Seconds = ($ticks / $TicksPerSecond) [uint64]$Timestamp = $Seconds * 0x100000000 # This is the initial used conversion method but I have not found any advantages to the now used # [UInt64]$seconds = $ticks / $TicksPerSecond; # [UInt64]$fractions = ((([double]$ticks) % $TicksPerSecond) * 0x100000000) / $TicksPerSecond # [uint64] $Timestamp = SwapEndianness ($fractions -bor ($seconds -shl 32)) [uint64] $Timestamp = SwapEndianness $Timestamp return $Timestamp } function Convert-NTPTimeStampToDateTime { param( [uint64]$NTPTimeField, [switch]$NoSwapEndianess ) $Epoch = [DateTime]::new(1900, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) # $TicksPerSecond = [timespan]::TicksPerSecond if ($NoSwapEndianess) { [double]$NTPTimeFieldDouble = $NTPTimeField } else { $NTPTimeField = SwapEndianness $NTPTimeField [double]$NTPTimeFieldDouble = $NTPTimeField } [double]$milliseconds = (($NTPTimeFieldDouble * 1000) / 0x100000000) # This is the initial used conversion method but it is less accurate than the above # [uint64]$IntPartMask = ([uint64]( -bnot [uint32]0 )) -shl 32 #Workaround for using constant 0xffffffff00000000 cause suffix u or ul only exists since Powershell 6.2 # [uint64]$FractPartMask = ([uint64]( -bnot [uint32]0 ))# sufix l (lower cas L) exists in PS 5 but for consistency I will use the same workaround for 0x00000000ffffffff # There are also other ways to generate the two masks # [uint32]$fractPart = ($NTPTimeField -band $fractPartMask) # [uint32]$intPart = ($NTPTimeField -band $IntPartMask) -shr 32 ##[double]$milliseconds2 = ($intPart * 1000) + ((([double]$fractPart) * 1000) / 0x100000000); #**UTC** time #write-host $milliseconds ## return ([DateTime]::new(1900, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)).AddMilliseconds($milliseconds); return $Epoch.AddMilliseconds($milliseconds) } function Get-NTPServerAddresses { [cmdletbinding(DefaultParameterSetName = "DefaultSettings")] param( [Parameter(Mandatory = $true, parameterSetName = "DefaultSettings")] [Parameter(Mandatory = $true, parameterSetName = "IPv6Only")] [Parameter(Mandatory = $true, parameterSetName = "IPv4Only")] [string]$ntpServer, [Parameter(Mandatory = $false, parameterSetName = "DefaultSettings")] [switch]$PreferIPv6, [Parameter(Mandatory = $false, parameterSetName = "IPv6Only")] [switch]$OnlyIPv6, [Parameter(Mandatory = $false, parameterSetName = "IPv4Only")] [switch]$OnlyIPv4 ) $ipObj = $null if ( [ipaddress]::TryParse($ntpServer, [ref]$ipObj)) { $addresses = [IPAddress[]]@($ipObj) } else { try { $addresses = [IPAddress[]]@([System.Net.Dns]::GetHostEntry($ntpServer).AddressList) } catch { Write-Warning "Could not resolve NTP server $ntpServer. Error: $_" return } } if ($OnlyIPv6) { $addresses = [ipaddress[]]@($addresses.Where({ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 })) } elseIf ($OnlyIPv4) { $addresses = [ipaddress[]]@($addresses.Where({ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork })) } elseif ($PreferIPv6) { $addresses = ([ipaddress[]]@($addresses.Where({ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 })) + ([ipaddress[]]@($addresses.Where({ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork })))) } else { $addresses = ([ipaddress[]]@($addresses.Where({ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork })) + ([ipaddress[]]@($addresses.Where({ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 })))) } if ($addresses.Count -eq 0) { Write-Warning "No IP addresses found for NTP server $ntpServer or IP address does not match the specified address family. Please check the NTP server name or IP address and try again." return } return $(new-object psobject -Property $([ordered]@{ NTPServer = $ntpServer Addresses = $addresses }) ) } function Get-NetworkTime { param( [string] $ntpServer, [ipaddress]$ntpServerAddress, [validateSet(3, 4)] [byte]$NTPVersion = 4 ) $connectError = $false $sendReciveError = $false $ReturnObj = New-Object psobject -Property $([ordered]@{ LeapIndicator = [byte]3 Version = [byte]0 Mode = [byte]0 Stratum = [byte]15 Poll = [byte]0 Precision = [byte]0 RootDelay = [single]0.0 RootDispersion = [single]0.0 ReferenceIdentifier = $null ReferenceTimestamp = [datetime]::MinValue OriginateTimestamp = [datetime]::MinValue ReceiveTimestamp = [datetime]::MinValue TransmitTimestamp = [datetime]::MinValue UsedNTPServer = "" UsedNTPServerIP = [ipaddress]::Parse("0.0.0.0") LocalReciveTime = [datetime]::MinValue RoundtripDelay = [double]0.0 Offset = [double]0.0 SyncDistance = [double]([double]::MaxValue) connectError = $false sendReciveError = $false isValid = $false }) <# NTP Paket format Leap Indicator (LI) 2 bits A code warning of an impending leap second to be inserted or deleted in the NTP timescale. The bit values are defined as follows: 00: no warning / 01: last minute has 61 seconds / 10: last minute has 59 seconds / 11: alarm condition (clock not synchronized) VN (Version Number) 3 bits NTP version number. The current version is 3. Mode 3 bits NTP mode. The values are defined as follows: 0: reserved / 1: symmetric active / 2: symmetric passive / 3: client mode / 4: server mode / 5: broadcast mode / 6: reserved for NTP control messages / 7: reserved for private use Stratum 8 bits Stratum level of the local clock. It defines the precision of the clock. The value of this field ranges from 1 to 15. A stratum 1 clock has the highest precision. Poll 8 bits Maximum interval between successive messages. Precision 8 bits Precision of the local clock. Root Delay 32 bits Total round-trip delay to the primary reference source. Root Dispersion 32 bits Maximum error relative to the primary reference source. Reference Identifier 32 bits ID of a reference clock. Reference Timestamp 64 bits Local time at which the local clock was last set or corrected. Value 0 indicates that the local clock is never synchronized. Originate Timestamp 64 bits Local time at which an NTP request packet departed the client for the server. Receive Timestamp 64 bits Local time at which an NTP request packet arrived at the server. Transmit Timestamp 64 bits Local time at which an NTP response packet departed the server for the client. Authenticator 96 bits (Optional) Authenticator information. #> $TicksPerSecond = [timespan]::TicksPerSecond $ntpDataSend = New-Object byte[] 48 $ntpData = New-Object byte[] 1024 [byte[]]$IdxReferenceIdentifier = 12, 13, 14, 15 [byte] $IdxRootDelay = 4; [byte] $IdxRootDispersion = 8; [byte] $IdxReferenceTimestamp = 16; [byte] $IdxOriginatingTimestamp = 24; [byte] $IdxReceiveTimestamp = 32; [byte] $IdxServerReplyTime = 40; # [byte] $IdxExtensionFieldsStart = 48; #Setting the Leap Indicator, Version Number and Mode values $ntpDataSend[0] = 0 -shl 6 $ntpDataSend[0] = $ntpDataSend[0] -bor (($NTPVersion -band 7) -shl 3) #Version $ntpDataSend[0] = $ntpDataSend[0] -bor 3 # NTP Mode #The UDP port number assigned to NTP is 123 $ipEndPoint = new-object IPEndpoint($ntpServerAddress, 123) #NTP uses UDP $isIPv6 = $ntpServerAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 if ($isIPv6) { $socket = New-Object System.Net.Sockets.Socket([System.Net.Sockets.AddressFamily]::InterNetworkV6, [System.Net.Sockets.SocketType]::Dgram, [System.Net.Sockets.ProtocolType]::Udp) } else { $socket = New-Object System.Net.Sockets.Socket([System.Net.Sockets.AddressFamily]::InterNetwork, [System.Net.Sockets.SocketType]::Dgram, [System.Net.Sockets.ProtocolType]::Udp) } try { $socket.Connect($ipEndPoint); #Stops code hang if NTP is blocked $socket.ReceiveTimeout = 6000; $socket.SendTimeout = 3000; } catch { Write-Warning "Error connecting to NTP server $ntpServer at IP address $($ntpServerAddress). Error: $_" $connectError = $true $sendReciveError = $true $socket.Close(); $socket.Dispose(); } #Creating timestamp from the current local time and store it in the bytes with index 40 - 47. The NTP Server #copies this timestamp to the Originating Timestamp field of the response. [int32]$BytesSentReceived = 0 if (-not $connectError) { [System.BitConverter]::GetBytes((Convert-DateTimeToNTPTimestamp -dateTime([DateTime]::UtcNow))).CopyTo($ntpDataSend, $IdxServerReplyTime) try { $BytesSentReceived = $socket.Send($ntpDataSend); $BytesSentReceived = $socket.Receive($ntpData); $sendReciveError = $false } catch { Write-Warning "Error communicating with NTP server $ntpServer at IP address $($ntpServerAddress). Error: $_" $sendReciveError = $true } finally { $socket.Close(); $socket.Dispose(); } } # Erst jetzt, mit dem aktuellen (ggf. im catch gesetzten) Wert von $sendReciveError, # wird entschieden, ob der Antwortpuffer geparst wird oder nicht. if ($connectError -eq $false -and $sendReciveError -eq $false) { $LocalReciveTime = [DateTime]::UtcNow $LocalReciveTimestamp = Convert-DateTimeToNTPTimestamp -dateTime $LocalReciveTime $LocalReciveTimeFromTS = Convert-NTPTimeStampToDateTime -NTPTimeField $LocalReciveTimestamp $enc = [System.Text.Encoding]::ASCII #Offset to get to the "Transmit Timestamp" field (time at which the reply #departed the server for the client, in 64-bit timestamp format." [uint32]$Header = [System.BitConverter]::ToUInt32($ntpData, 0) [byte]$leap = $Header -shr 6 -band 3 [byte]$ServerVersion = $Header -shr 3 -band 7 [byte]$NTPMode = $Header -band 7 [byte]$stratum = $Header -shr 8 -band 255 [byte]$poll = $header -shr 16 -band 255 [byte]$Precision = $header -shr 24 -band 255 [uint32]$RootDelay = [System.BitConverter]::ToUInt32($ntpData, $IdxRootDelay) [uint32]$RootDispersion = [System.BitConverter]::ToUInt32($ntpData, $IdxRootDispersion) if ($stratum -eq 1) { [string]$ReferenceIdentifier = $enc.GetString($ntpData[$IdxReferenceIdentifier]) } elseif ($isIPv6) { [string]$ReferenceIdentifier = [BitConverter]::ToString($ntpData[$IdxReferenceIdentifier]) } else { [ipaddress]$ReferenceIdentifier = [ipaddress]::new($ntpData[$IdxReferenceIdentifier]) } [uint64]$ReferenceTimestamp = [System.BitConverter]::ToUInt64($ntpData, $idxReferenceTimestamp) [uint64]$OriginateTimestamp = [System.BitConverter]::ToUInt64($ntpData, $IdxOriginatingTimestamp) [uint64]$ReceiveTimestamp = [System.BitConverter]::ToUInt64($ntpData, $IdxReceiveTimestamp) [uint64]$ServerReplayTime = [System.BitConverter]::ToUInt64($ntpData, $IdxserverReplyTime) [uint32]$RootDelay = [System.BitConverter]::ToUInt32($ntpData, $IdxRootDelay) [uint32]$TempRootDelay = SwapEndianness -SwapValue $RootDelay [int32]$TempRootDelaySigned = [System.BitConverter]::ToInt32(([System.BitConverter]::GetBytes($TempRootDelay)), 0) [single]$fRootDelay = ([Single]$TempRootDelaySigned) / (1 -shl 16) [uint32]$RootDispersion = SwapEndianness -SwapValue ([System.BitConverter]::ToUInt32($ntpData, $IdxRootDispersion)) [int32]$RootDispersionSigned = $RootDispersion -band (-bnot ([uint32]1 -shl 31 )) if ($RootDispersion -band ([uint32]1 -shl 31 )) { $RootDispersionSigned = $RootDispersionSigned -bor ([int32]1 -shl 31) } [single]$fRootDispersion = ([single]$RootDispersionSigned) / (1 -shl 16) $ReceiveTime = Convert-NTPTimeStampToDateTime -NTPTimeField $ReceiveTimestamp $ReferenceTime = Convert-NTPTimeStampToDateTime -NTPTimeField $ReferenceTimestamp $OriginateTime = Convert-NTPTimeStampToDateTime -NTPTimeField $OriginateTimestamp $networkDateTime = Convert-NTPTimeStampToDateTime -NTPTimeField $ServerReplayTime $ReturnObj.LeapIndicator = $leap $ReturnObj.Version = $ServerVersion $ReturnObj.Mode = $NTPMode $ReturnObj.Stratum = $stratum $ReturnObj.Poll = $poll $ReturnObj.Precision = $Precision $ReturnObj.RootDelay = $fRootDelay $ReturnObj.RootDispersion = $fRootDispersion $ReturnObj.ReferenceIdentifier = $ReferenceIdentifier $ReturnObj.ReferenceTimestamp = $ReferenceTime $ReturnObj.OriginateTimestamp = $OriginateTime $ReturnObj.ReceiveTimestamp = $ReceiveTime $ReturnObj.TransmitTimestamp = $networkDateTime $ReturnObj.UsedNTPServer = $ntpServer $ReturnObj.UsedNTPServerIP = $ntpServerAddress $ReturnObj.LocalReciveTime = $LocalReciveTime $ReturnObj.RoundtripDelay = ($LocalReciveTimeFromTS - $OriginateTime).TotalSeconds - ($networkDateTime - $ReceiveTime).TotalSeconds $ReturnObj.Offset = ((( $ReceiveTime.Ticks / $TicksPerSecond) + ($networkDateTime.ticks / $TicksPerSecond)) - (($LocalReciveTimeFromTS.Ticks / $TicksPerSecond) + ($OriginateTime.Ticks / $TicksPerSecond))) / 2 # Synchronization Distance (RFC 5905): je kleiner, desto vertrauenswuerdiger die Zeitquelle. # Kombiniert die Serverqualitaet (RootDispersion/RootDelay) mit der eigenen Messunsicherheit (Roundtrip). $ReturnObj.SyncDistance = [double]$fRootDispersion + ([double]$fRootDelay / 2) + ($ReturnObj.RoundtripDelay / 2) $ReturnObj.connectError = $connectError $ReturnObj.sendReciveError = $sendReciveError $ReturnObj.isValid = $true } else { $ReturnObj.connectError = $connectError $ReturnObj.sendReciveError = $sendReciveError $ReturnObj.UsedNTPServer = $ntpServer $ReturnObj.UsedNTPServerIP = $ntpServerAddress $ReturnObj.isValid = $false } return $ReturnObj } function Invoke-NTPQueryParallel { # Fragt mehrere Adressen desselben NTP-Servers parallel ab (RunspacePool statt # sequenzieller Schleife), damit die Gesamtlaufzeit vom langsamsten einzelnen # Server dominiert wird statt von der Summe aller Roundtrips/Timeouts. param( [string]$ntpServer, [ipaddress[]]$ntpServerAddresses, [byte]$NTPVersion ) if ($null -eq $ntpServerAddresses -or $ntpServerAddresses.Count -eq 0) { return @() } # Ein frischer Runspace kennt unsere Modulfunktionen nicht automatisch - sie # muessen explizit ins InitialSessionState uebernommen werden. $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() foreach ($fn in @('Get-NetworkTime', 'Convert-DateTimeToNTPTimestamp', 'Convert-NTPTimeStampToDateTime', 'SwapEndianness')) { $def = Get-Content -Path "function:$fn" $entry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($fn, $def) $iss.Commands.Add($entry) } $maxThreads = [Math]::Min(8, $ntpServerAddresses.Count) $pool = [runspacefactory]::CreateRunspacePool(1, $maxThreads, $iss, $Host) $pool.Open() $jobs = New-Object System.Collections.Generic.List[object] try { foreach ($addr in $ntpServerAddresses) { $ps = [powershell]::Create() $ps.RunspacePool = $pool [void]$ps.AddScript({ param($srv, $address, $ver) Get-NetworkTime -ntpServer $srv -ntpServerAddress $address -NTPVersion $ver }) [void]$ps.AddArgument($ntpServer) [void]$ps.AddArgument($addr) [void]$ps.AddArgument($NTPVersion) $job = New-Object psobject -Property ([ordered]@{ PowerShell = $ps Handle = $ps.BeginInvoke() Address = $addr }) $jobs.Add($job) } $results = New-Object System.Collections.Generic.List[object] foreach ($job in $jobs) { try { $results.AddRange(@($job.PowerShell.EndInvoke($job.Handle))) } catch { Write-Warning "Error running parallel NTP query for $ntpServer at IP address $($job.Address). Error: $_" } finally { $job.PowerShell.Dispose() } } return $results } finally { $pool.Close() $pool.Dispose() } } #endregion private helper functions function Get-NTPTime { [cmdletbinding(DefaultParameterSetName = "DefaultSettings")] param( [Parameter(Mandatory = $true, parameterSetName = "DefaultSettings")] [Parameter(Mandatory = $true, parameterSetName = "IPv6Only")] [Parameter(Mandatory = $true, parameterSetName = "IPv4Only")] [string[]]$NTPServer, [Parameter(Mandatory = $false, parameterSetName = "DefaultSettings")] [Parameter(Mandatory = $false, parameterSetName = "IPv6Only")] [Parameter(Mandatory = $false, parameterSetName = "IPv4Only")] [ValidateSet(3, 4)] [byte]$NTPVersion = 4, [Parameter(Mandatory = $false, parameterSetName = "DefaultSettings")] [switch]$PreferIPv6, [Parameter(Mandatory = $false, parameterSetName = "IPv6Only")] [switch]$OnlyIPv6, [Parameter(Mandatory = $false, parameterSetName = "IPv4Only")] [switch]$OnlyIPv4, [Parameter(Mandatory = $false, parameterSetName = "DefaultSettings")] [Parameter(Mandatory = $false, parameterSetName = "IPv6Only")] [Parameter(Mandatory = $false, parameterSetName = "IPv4Only")] [ValidateSet("First", "Failover", "All", "BestSyncDistance")] [string]$AddressSelectionMode = "Failover", [Parameter(Mandatory = $false, parameterSetName = "DefaultSettings")] [Parameter(Mandatory = $false, parameterSetName = "IPv6Only")] [Parameter(Mandatory = $false, parameterSetName = "IPv4Only")] [switch]$AsDateTime ) BEGIN { $getNTPAddressFunctionParams = new-object hashtable foreach ($param in $PSBoundParameters.Keys) { switch ($param) { "PreferIPv6" { $getNTPAddressFunctionParams["$param"] = $PreferIPv6 } "OnlyIPv6" { $getNTPAddressFunctionParams["$param"] = $OnlyIPv6 } "OnlyIPv4" { $getNTPAddressFunctionParams["$param"] = $OnlyIPv4 } } } } PROCESS { foreach ($NTPSrv in $NTPServer) { $NTPServerAddresses = Get-NTPServerAddresses -ntpServer $NTPSrv @getNTPAddressFunctionParams if ($null -eq $NTPServerAddresses -or $NTPServerAddresses.Addresses.Count -eq 0) { # Get-NTPServerAddresses hat bereits eine Write-Warning ausgegeben (DNS-Fehler # oder keine Adresse der gewuenschten Familie) - hier gibt es nichts mehr zu tun. continue } if ($AddressSelectionMode -eq "All" -or $AddressSelectionMode -eq "BestSyncDistance") { Write-Verbose "Querying all $($NTPServerAddresses.Addresses.Count) address(es) of $NTPSrv in parallel" $allResults = Invoke-NTPQueryParallel -ntpServer $NTPSrv -ntpServerAddresses $NTPServerAddresses.Addresses -NTPVersion $NTPVersion if ($AddressSelectionMode -eq "All") { foreach ($NTPObj in $allResults) { if ($AsDateTime) { $NTPObj.TransmitTimestamp } else { $NTPObj } } } else { # BestSyncDistance: kleinste Synchronization Distance unter den gueltigen Antworten gewinnt. $best = $allResults | Where-Object { $_.isValid } | Sort-Object -Property SyncDistance | Select-Object -First 1 if ($null -eq $best) { Write-Warning "No valid NTP response received from any address of $NTPSrv." } elseif ($AsDateTime) { $best.TransmitTimestamp } else { $best } } continue } # First / Failover: bewusst sequenziell, weil die Reihenfolge der Adressen # (z.B. IPv6 zuerst bei -PreferIPv6) hier Teil des gewuenschten Verhaltens ist. if ($AddressSelectionMode -eq "First") { $maxIdx = 0 } else { $maxIdx = $NTPServerAddresses.Addresses.Count - 1 } $breakAfterSuccess = $false for ($i = 0; $i -le $maxIdx; $i++) { $NTPServerAddress = $NTPServerAddresses.Addresses[$i] Write-Verbose "Using NTP server $NTPSrv with IP address $NTPServerAddress" $NTPObj = Get-NetworkTime -ntpServer $NTPSrv -ntpServerAddress $NTPServerAddress -NTPVersion $NTPVersion if ($AddressSelectionMode -eq "Failover" -and -not $NTPObj.isValid) { continue } elseif ($AddressSelectionMode -eq "Failover" -and $NTPObj.isValid) { $breakAfterSuccess = $true } If ($AsDateTime) { $NTPObj.TransmitTimestamp } else { $NTPObj } if ($breakAfterSuccess) { break } } } } } Export-ModuleMember -Function 'Get-NTPTime' |