Building a simple NTP client with Powershell
2024-10-12Have you ever thougth about using acurate time in a PowerShell script? I have written a simple NTP Client in PowerShell so that I can query any NTP Server.
The motivation was not to have a NTP Client for testing NTP Server connections or getting the current Date and Time for simply using it in a script. But this was the postive side effect of this.
Building a simple NTP Client with Powershell
Why I have decided to built a NTP Client in Powershell
Everything started with a series of failed operating system deployment (OSD) task sequences (TS) executed withe PXE from a MECM/SCCM.
The first part of the deployment was ok but after Windows was starting up the TS fails.
After checking a lot of log files and do some TS debbuging. I found the problems:
- The certificate for SCCM-Client communication was not valid because the issuing date was in the future or the expiration date was overdue.
- Domain join faild
After checking the Computers date and time it turns out, that it was either up to 10 months in the future or in the past.
So I have tried some methods to get the time from the PXE server during WinPE phase but without any luck.
Now I have decided to make the PXE server also a NTP server and use a NTP Client to get the correct time from the PXE server to set the system clock to the current date and time.
After a little research I decided to write a easy NTP client in Powershell tha can work in WinPE.
The NTP data packet
The NTP data packet is 48 Byte long and stores some information in bit fields. The first 32 bits stores following information
-
LI (Leap Indicator): 2 bits witch can represent following status:
-
00b -> 0d -> no warning
-
01b -> 1d -> last minute has 61 seconds
-
10b -> 2d -> last minute has 59 seconds
-
11b -> 3d -> alarm condition (clock not syncronized)
-
VN (Version Number) 3 bits representing the version of NTP used by the client in the request packet and the server version in the response packet.
-
Mode: 3 bits representing the NTP Mode
-
0: reserved -> 000b
-
1: symetric active -> 001b
-
2: symetric passive -> 010b
-
3: client mode -> 011b
-
4: server mode -> 100b
-
5: broadcast mode -> 101b
-
6: reserved for NTP control message -> 110b
-
7: reserved for private use -> 111b
-
Stratum: 8 bits for the stratum level of the clock. Stratum 1 clock has the higest precision stratum 15 has the lowest precision
-
0: Unspecified or invalid 00000000b
-
1: Primary server 00000001b
-
2–15: Secondary server 00000010b - 00001111b
-
16: Unsynchronized 00010000b
-
17–255: Reserved 00010001b - 11111111b
-
Pol: 8 bit signed integer: Maximum intervall between successive messages in seconds
-
Precision: 8 bit signed integer: Precision of the local clock
After this header information follows the following information:
- Root Dealy: The total round-trip delay from the server to the primary reference source. The value is a 32-bit signed fixed-point number in units of seconds, with the fraction point between bits 15 and 16. This field is significant only in server messages.
- Root Dispersion: The maximum error due to clock frequency tolerance. The value is a 32-bit signed fixed-point number in units of seconds, with the fraction point between bits 15 and 16. This field is significant only in server messages.
- Reference Identifier: For stratum 1 servers this value is a four-character ASCII code that describes the external reference source (refer to Figure 2). For secondary servers this value is the 32-bit IPv4 address of the synchronization source, or the first 32 bits of the Message Digest Algorithm 5 (MD5) hash of the IPv6 address of the synchronization source.
- Reference Timestamp: 64 bit: Local time at which the local clock was last set or corrected. If the clock never syncronized with a tim esource the value should 0
- Originate Timestamp: 64 bit : Local time at which the requesting NTP packet departed the client for the server
- Receive Timestamp: 64 bit local server time at which the NTP packet arived the server
- Transmit Timestamp: 64 bit: Local server time at which the NTP packet departed the server for the client
Now we have the total amount of 384 bits or 48 bytes 8 bit each.
For NTPv4 there are aditional but optional 96 bits = 12 bytes 8 bit each for Authentication information
How to build the request packet
Before we can request the time information from a NTP server we need a request packet in the format, explained above.
For a very simple NTP client only following informations are necessary:
- Leap indicator
- Version Number
- NTP Mode
But anyway you have to send a request packet of 48 bytes, therefore we define a variable as a byte array with a length of 48
$ntpData = New-Object byte[] 48
Now we have to set the required header information, this is only the first byte.
The Leap Indicator is 0 so we can ignore it.
As NTP version we use 3.
The NTP mode have to be 3 (Client Mode)
The structure of the first header byte is:
LI | Version | Mode | |||||
---|---|---|---|---|---|---|---|
Worth of bits within field | |||||||
2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
Worth of bits within byte | |||||||
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
To place the version number at the right position in the header byte we use 3 and shift it left by 3 bits.
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
shift left by 3 bits | |||||||
0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
Now we can combine it with the value for the NTP mode by doing a binary or with 3.
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
binary or | |||||||
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
= | |||||||
0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
The complete header has the following format:
Precision | Poll | Stratum | LI | Version | Mode | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Worth of bits within field | |||||||||||||||||||||||||||||||
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
Worth of bits within header | |||||||||||||||||||||||||||||||
231 | 230 | 229 | 228 | 227 | 226 | 225 | 224 | 223 | 222 | 221 | 220 | 219 | 218 | 217 | 216 | 215 | 214 | 213 | 212 | 211 | 210 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
if the Leap Indicator have a other value than 0 we must do a shift left by 6 bits before we add it with a binary or to the header byte. In powershell this looks like follows:
[byte]$LI=0
[byte]$NTPVersion=3
[byte]$NTPMode=3
$ntpData[0]=($LI -shl 6) -bor ($NTPVersion -shl 3) -bor $NTPMode
The value of the header byte is now 27d or 1Bh therefore the packet is ready for transmssion.
To do that we need to know the FQDN or the IP-Address of a NTP Server and the port where the NTP-Server is listening.
The NTP Default port is 123 UDP.
Connect to the NTP server, sending the request and getting the result
Now its time to setup the connection. We do this using the .NET classes
- System.Net.Dns
- System.Net.IPEndPoint
- System.Net.Sockets.Socket
and by the enums
- System.Net.Sockets.AddressFamily
- System.Net.Sockets.SocketType
- System.Net.Sockets.ProtocolType
The System.Net.Dns class is used to resolve the FQDN of a NTP server to a address object.
$ntpServer="pool.ntp.org"
$addresses = [System.Net.Dns]::GetHostEntry($ntpServer).AddressList;
Next step is to create a System.Net.IPEndPoint object, therfore we will use the first address object in the
$addresses
object list.
#The UDP port number assigned to NTP is 123
#the constructor for IPEndPoint needs the address object ant the port number
$ipEndPoint = new-object System.Net.IPEndpoint($addresses[0], 123)
Now we are ready to connect to the NTP server. For this we need a network socket connection.
To do this we use the System.Net.Sockets.Socket class and using as parameters for the constructor:
- the address family InterNetwork
- the socket type is dgram (datagram for "connection less" communications)
- the protocol type UDP
For specifying these values we will use the enumeration classes that contain this information.
#NTP uses UDP
$socket = New-Object System.Net.Sockets.Socket([System.Net.Sockets.AddressFamily]::InterNetwork,
[System.Net.Sockets.SocketType]::Dgram, [System.Net.Sockets.ProtocolType]::Udp)
$socket.Connect($ipEndPoint);
To prevent frozen connections we set a send and recive timeout of 3000 milliseconds then we send the request packet and try to recive the answer packet. Then we close the connection.
#Stops code hang if NTP is blocked
$socket.SendTimeout=3000
$socket.ReceiveTimeout = 3000
[int32]$BytesSentReceived = $socket.Send($ntpData)
$BytesSentReceived = $socket.Receive($ntpData)
$socket.Close()
Getting required information from the answer packet
Now it is time for the funny part. We have all data we need but for using them we must extract it from the recived packet and converting it to a usable format. For doing this we specifies index variables containing the starting index in the byte array for our values. This is only for more transparency.
[byte[]]$IdxReferenceIdentifier = 12, 13, 14, 15 #Powershell allowes it to use multiple indices to get informations from
a array
[byte] $IdxRootDelay = 4;
[byte] $IdxRootDispersion = 8;
[byte] $IdxReferenceTimestamp = 16;
[byte] $IdxOriginatinTimestamp = 24;
[byte] $IdxReceiveTimestamp = 32;
[byte] $IdxServerReplyTime = 40;
Getting header information
First we get the header informations out of our answer packet.
For this we uses the System.BitConverter
class.
Remember the header has 32 bit. Therefore we convert the first 4 bytes to a unsigned 32bit integer
[uint32]$Header = [System.BitConverter]::ToUInt32($ntpData, 0)
Next step is to get the reqired informations to do this we use binary operators for getting the Leap Indicator we first do a shift right to the value in the header and then do binary and with 3d this preserves the value of the first 2 bits but set all other bits to 0
[byte]$leap = $Header -shr 6 -band 3
Now we get the NTP server version by do a shift right by 3 bits and than do a binary and with 7d (111b) to only get the value of the first 3 bits an set all other bits to 0
[byte]$ServerVersion = $Header -shr 3 -band 7
For getting the NTP mode we just do a binary and to the header field with the value 7d (111b)
[byte]$NTPMode = $Header -band 7
Getting the stratum value of the NTP server is done by shifting right the header value by 8 bits and do a binary and with 255d (11111111b / ffh)
[byte]$stratum = $Header -shr 8 -band 255
Getting the right value vor the Poll is a littel bit more tricky because it is a 8 bit signed integer.
A 8 bit signed integer have a maximum value of 127d 0111111b and a minimum value of -128d 10000000b this is because tnegative values are stored inverted and the higest bit (128) has a dual function: if it is set, the value is negative and it represents the value of -128. so the bit 7 hase the value of 64d and if it is set it will be added to -128d this means in math -128 + 64 = -64
Therefore the (signed 8 bit integer) binary value of 11111111b is the decimal value -1 = -128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1
Ok, first we get the value as a unsigned 8 bit integer out of the header by do a shift rigt by 16 bit and a binary and with 255d
[byte]$upoll = $header -shr 16 -band 255
now we check if the higest bit is set or not. This is done with a binary and with 128d if so, we do a binary and with the value of 127 and stores the result in a signed byte.
After this we have two options:
- do a binary or of our signed byte with -128
- use a signed byte with the value 1 and shift this 7 bits to the left and then do a binary or with our first signed
byte
if(($upoll -band 128) -eq 128 ){
[sbyte]$poll = $upoll -band 127
$poll = $poll -bor ([sbyte]-128)
} else {
[sbyte]$poll = $upoll
}
second option
if(($upoll -band 128) -eq 128 ){
[sbyte]$poll = $upoll -band 127
$poll = $poll -bor ([sbyte]1 -shl 7)
} else {
[sbyte]$poll = $upoll
}
alternatively, you can write it like this:
[sbyte]$poll = $upoll -band 127
if(($upoll -band 128) -eq 128 ){
$poll = $poll -bor ([sbyte]-128)
}
this will work because the higest bit will never set to 1 if the value is positive We get the value for the precision in the same way
[byte]$uPrecision = $header -shr 24 -band 255
[sbyte]$Precision = $uPrecision -band 127
if(($uPrecision -band 128) -eq 128 ){
$Precision = $Precision -bor ([sbyte]-128)
}
Now we have all information from the header.
For the other informations in the packet we have to consider, that they are in "Network Byte Order" also knowen as "Big Endian".
Therefor we must switch the byte order to Little Endian (the Intel CPU normal byte order) first. To do this we will use two little functions SwapEndianes and SwapEndianness64. This two functions should only demonstrate the process.
Later we will only use one function SwapEndianness where we can pass either a uint32 or a uint64.
The function than swaps the first with the last byte, the second with the penultimate byte. If the value was a uint32 we are finished now if it was a uint64 this process goes on...
Swaping the third with third to last byte and so on until the byte order is changed
function SwapEndianness64{
param(
[uint64]$x
)
[uint64]$swapmask = 0xff
return ([uint64]((($x -band $swapmask) -shl 56) -bor (($x -band ($swapmask -shl 8)) -shl 40) -bor (($x -band ($swapmask
-shl 16)) -shl 24) -bor (($x -band ($swapmask -shl 24)) -shl 8) -bor (($x -shr 56) -band ($swapmask )) -bor (($x -shr
40) -band ($swapmask -shl 8)) -bor ($x -shr 24 -band ($swapmask -shl 16)) -bor (($x -shr 8 -band ($swapmask -shl 24))))
)
}
function SwapEndiannesss{
param(
[uint32]$x
)
[uint32]$swapmask = 0xff
return ([uint32]((($x -band ($swapmask -shl 16)) -shl 24) -bor (($x -band ($swapmask -shl 24)) -shl 8) -bor (($x -shr
56) -band ($swapmask )) -bor (($x -shr 40) -band ($swapmask -shl 8)) -bor ($x -shr 24 -band ($swapmask -shl 16)) -bor
(($x -shr 8 -band ($swapmask -shl 24))))
)
}
Now its time to get the value for the root delay the problem here is, that this value is a signed fixpoint number with the decimal point between bit 15 and 16 but it is stored as 32 bit unsigned integer. because of this we have to do following steps:
Get the root delay field from the NTP response packet, change the byte order to little endian and convert the unsigned int into a floating point value.
This sounds complicated but it isn't.
Instead of bit operations we use the System.BitConverter
class to convert the unsigned int32 into a signed int32.
To do this we convert the unsigned integer into a byte array and then back to a signed integer.
After this we convert the now signed integer into a floatin gpoint number with single precison. This is done by casting the integer to single and than dividing it by the value that we get if we shift 1 to the left by the number of bits are used by the decimal part.
In this case the decimal point is beween bit 15 and 16 so we shift 1 to the left by 16 bits
[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)
For the sake of completeness here we uses the solution using bit operations for the root disperion:
[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)
Next we will need the value vor the reference identifier.
The reference identifier is special, because it stores different information types based on the startum level of the server.
If the Server is stratum 1 the reference identifier provides a the 4 char identifier of the reference clock in ASCII code.
If the server is stratum 2 or higher, the reference identifier stores the IPv4 address of the next higher NTP-server in the hirarchie.
For stratum 1 / 0 following referenz identifiers are typical
Code | External Reference Source |
---|---|
LOCL | uncalibrated local clock used as a primary reference for a subnet without external means of synchronization |
PPS | atomic clock or other pulse-per-second source individually calibrated to national standards |
ACTS | NIST dialup modem service |
USNO | USNO modem service |
PTB | PTB (Germany) modem service. There can a number after the PTB |
TDF | Allouis (France) Radio 164 kHz |
DCF | Mainflingen (Germany) Radio 77.5 kHz |
MSF | Rugby (UK) Radio 60 kHz |
WWV | Ft. Collins (US) Radio 2.5, 5, 10, 15, 20 MHz |
WWVB | Boulder (US) Radio 60 kHz |
WWVH | Kaui Hawaii (US) Radio 2.5, 5, 10, 15 MHz |
CHU | Ottawa (Canada) Radio 3330, 7335, 14670 kHz |
LORC | LORAN-C radionavigation system |
OMEG | OMEGA radionavigation system |
GPS | Global Positioning Service |
GOES | Geostationary Orbit Environment Satellite |
Different to the other fields, we can get the values directly from the bytes. We owe this to the fact, that Powershell allowes to use multiple indexes at the same time on a array. Or in other words, we can use a array of indexes to access the content of a array in any order we want.
Therefore the $IdxReferenceIdentifier
variable contains all 4 indexes we need to access the bytes. Therefore we get a byte array as result with exactly 4 entries.
In addition to this fact, the ipaddress
class stores IP-addresses in Big-Endian.
Now the only thing we have to do is to create a encoder for ASCII and than check the stratum level of the NTP server.
$enc = [System.Text.Encoding]::ASCII
if($stratum -eq 1){
[string]$ReferenceIdentifier = $enc.GetString($ntpData[$IdxReferenceIdentifier])
}
else {
[ipaddress]$ReferenceIdentifier = [ipaddress]::new($ntpData[$IdxReferenceIdentifier])
}
In the next step we get the time stamps out of the recived packet. Each is 8 byte or 64 bit and contains the elapsed time in seconds from 1900-01-01 until the moment the timestamp was generated.
To do this, we use the System.BitConverter
class and read the timestamps as 64 bit unsigned integer.
[uint64]$ReferenceTimestamp = [System.BitConverter]::ToUInt64($ntpData, $IdxReferenceTimestamp)
[uint64]$OriginateTimestamp = [System.BitConverter]::ToUInt64($ntpData, $IdxOriginatinTimestamp)
[uint64]$ReceiveTimestamp = [System.BitConverter]::ToUInt64($ntpData, $IdxReceiveTimestamp)
[uint64]$ServerReplayTime = [System.BitConverter]::ToUInt64($ntpData, $IdxserverReplyTime)
To convert this value now to a date time value we need it in milliseconds and we must consider, that this timestamp value contains a 32 bit integer value and a 32 bit fractional part.
So first we define two binary masks, one for the integer part and one for the fractional part.
[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
next we swap the endianness for the timestamp field we want to convert to date time. For us the most significant field is the ServerReplyTime
$NTPTimeField = SwapEndiannuess $ServerReplayTime
now we "convert" both parts to a 32 bit unsigned integer
[uint32]$fractPart = ($NTPTimeField -band $fractPartMask)
[uint32]$intPart = ($NTPTimeField -band $IntPartMask) -shr 32
now we convert the values to milliseconds and to data type double
[double]$milliseconds = ($intPart * 1000) + ((([double]$fractPart) * 1000) / 0x100000000);
After this we crate a datetime object with the value of 1st January 1900 and add the milliseconds.
$NetworkDateTimeUtc=([DateTime]::new(1900, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)).AddMilliseconds($milliseconds);
Now the main part is finished and you can set your system clock based on teh recived time information
Set-Date -Date $($NetworkDateTime.ToLocalTime())
Informations for calculating / estimate the delay and the offset of the client local clock
A NTP client estimates the delay between client and server and to determine the ofset of the client clock using the following time stamps:
Timestamp Name | ID | When Generated |
---|---|---|
Originate Timestamp | T1 | time request sent by client |
Receive Timestamp | T2 | time request received by server |
Transmit Timestamp | T3 | time reply sent by server |
Destination Timestamp | T4 | time reply recived by client |
The calculation of the estimation is done with this formulas:
Offset = [(t2 + t3) - (t4 + t1)] / 2
Delay = (t4 - t1) - (t3 - t2)
this all is very basic, so we now bring this to a script with functions
Full NTP client script
I'm shure there can be done a lot of improvements in this script an some error handling wouldn't hurt but for now it should do it.
param(
[string[]]$NTPServer
)
BEGIN {
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-NetworkTime {
param(
[string[]] $ntpServer = "pool.ntp.org"
)
# $ntpServer = "time.windows.com"
<# $ntpServer="fits-svr-01.f-it-s.net" $ntpServer="ptbtime1.ptb.de" $ntpServer="time.uni-paderborn.de" #>
# NTP message size - 16 bytes of the digest (RFC 2030)
<# 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
$ntpData = New-Object byte[] 48
[byte[]]$IdxReferenceIdentifier = 12, 13, 14, 15
[byte] $IdxRootDelay = 4;
[byte] $IdxRootDispersion = 8;
[byte] $IdxReferenceTimestamp = 16;
[byte] $IdxOriginatingTimestamp = 24;
[byte] $IdxReceiveTimestamp = 32;
[byte] $IdxServerReplyTime = 40;
#Setting the Leap Indicator, Version Number and Mode values
$ntpData[0] = 0x1B; #LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
$ntpData[0] = 0 -shl 6
$ntpData[0] = $ntpData[0] -bor (3 -shl 3) #Version
$ntpData[0] = $ntpData[0] -bor 3 # NTP Mode
$addresses = [System.Net.Dns]::GetHostEntry($ntpServer).AddressList;
#The UDP port number assigned to NTP is 123
$ipEndPoint = new-object IPEndpoint($addresses[0], 123)
#NTP uses UDP
$socket = New-Object System.Net.Sockets.Socket([System.Net.Sockets.AddressFamily]::InterNetwork,
[System.Net.Sockets.SocketType]::Dgram, [System.Net.Sockets.ProtocolType]::Udp)
$socket.Connect($ipEndPoint);
#Stops code hang if NTP is blocked
$socket.ReceiveTimeout = 6000;
$socket.SendTimeout = 3000;
$localSendTime = [DateTime]::UtcNow
#[uint64]$LocalSendTimestamp = Convert-DateTimeToNTPTimestamp -dateTime $localSendTime
#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.
[System.BitConverter]::GetBytes((Convert-DateTimeToNTPTimestamp -dateTime
([DateTime]::UtcNow))).CopyTo($ntpData, $IdxServerReplyTime)
[int32]$BytesSentReceived = $socket.Send($ntpData);
$BytesSentReceived = $socket.Receive($ntpData);
$LocalReciveTime = [DateTime]::UtcNow
$LocalReciveTimestamp = Convert-DateTimeToNTPTimestamp -dateTime $LocalReciveTime
#$LocalReciveTimestampLE = SwapEndianness $LocalReciveTimestamp
$LocalReciveTimeFromTS = Convert-NTPTimeStampToDateTime -NTPTimeField $LocalReciveTimestamp
$socket.Close();
$socket.Dispose();
$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])
}
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]$TempRootDelay = SwapEndianness -x $RootDelay
[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 = New-Object psobject -Property $([ordered]@{
LeapIndicator = $leap
Version = $ServerVersion
Mode = $NTPMode
Stratum = $stratum
Poll = $poll
Precision = $Precision
RootDelay = $fRootDelay
RootDispersion = $fRootDispersion
ReferenceIdentifier = $ReferenceIdentifier
ReferenceTimestamp = $ReferenceTime
OriginateTimestamp = $OriginateTime
ReceiveTimestamp = $ReceiveTime
TransmitTimestamp = $networkDateTime
UsedNTPServer = $ntpServer
UsedNTPServerIP = $addresses[0]
LocalReciveTime = $LocalReciveTime
RoundtripDelay = ($LocalReciveTimeFromTS - $OriginateTime).TotalSeconds - ($networkDateTime -
$ReceiveTime).TotalSeconds
Offset = ((( $ReceiveTime.Ticks / $TicksPerSecond) + ($networkDateTime.ticks / $TicksPerSecond)) -
(($LocalReciveTimeFromTS.Ticks / $TicksPerSecond) + ($OriginateTime.Ticks / $TicksPerSecond))) / 2
})
#$networkDateTime - $ReceiveTime
return $ReturnObj
}
}
PROCESS {
foreach ($NTPSrv in $NTPServer) {
Get-NetworkTime $NTPSrv
}
}
```