Ein einfacher NTP-Client mit PowerShell
2024-10-12Ist es möglich einen NTP-Client in PowerShell zu schreiben?. Das war eine Frage, die mir schon das eine oder andere mal gestellt wurde.
Der Grund warum ich es letztendlich aber getan habe war ein ganz anderer...
Esrstellung eines einfachen NTP-Clients in PowerShell
Warum habe ich beschlossen, einen NT-Client in PowerhShell zu schreiben?
Es begann eigentlich alles danit, dass eine ganze Reihe von Betriebssystem Installationen (OSD / Operating System Deplyment) Task Sequenzen (TS) mittels PXE über SCCM/MECM fehlgeschlagen sind.
Der Erste Teil der Installationen lief einwndfrei aber sobald Windows das erste mal startete und der Domänenbeitritt stattfinden sollte schlug die Installation fehl. Danach hatt dann verständlicherweis die restliche Installation nicht mehr funktioniert
Nachdem ich einen Haufen Protokolldateien durchgesehen hatte und auch sichergestellt hatte, dass es mit der TS selber kein Problem gibt. Hatte ich das Problem gefunden:
- Das Zertifikat, dass der SCCM/MECM-Client für die kommunikation verwenden sollte war ungültig und zwar entweder, da das Ausstellugnsdatum aus sicht des Clients noch in der Zukunft lag. Oder weil das Ablaufdatum berits überschritten war.
- Der Domänenbetritt schlug fehl
Als ich mir das dann genauer angesehen habe, stellte ich fest, dass die Uhr des Computers bis zu 10 Monaten in der Zukunft oder auch in der Vergangenheit stand.
Ich habe dann verschieden methoden versucht, wärend der PXE Phase die korrekte Uhrzeit und Datum vom PXE Server abzurufen und die Client-Uhr entsprechend einzustellen. Leider hat keine davon wirklich zuverlässig funktioniert.
Das war der Moment, in dem ich entschieden habe, dass ich einen NTP Client in PowerShel schreibe, der auch inder PXE-Phase (WinPE) funktioniert und die Client-Uhr auf diese Art mit der richtigen Zeit zu versorgen.
Um das zu tun, musst du dich aber als erstes ein wenig mit dem NTP-Protokoll und dem Verwendeten Datenpaket beschäftigen.
Das NTP-Datenpaket
Das NTP-Datenpaket ist 48 Byte groß und speichert die INformationen in Bit-Feldern. Die ersten 32-Bit speichern folgende Informationen:
-
LI (Leap Indicator): 2 Bits welche folgende Status darstellen:
- 00b -> 0d -> keine warnung
- 01b -> 1d -> die letzte Minute hat 61 Sekunden
- 10b -> 2d -> die letzte Minute hat 59 Sekunden
- 11b -> 3d -> Alarmzustand (Uhr ist nicht synchronisiert)
-
VN (Versions Nummer) 3 Bits welche die Versionsnummer angeben, die von dem NTP-Server und dem NTP-Client in dem Datanpaket verswendet werden.
-
Mode: 3 Bits welche den verwendeten NTP-Modus angeben
- 0: Reserviert -> 000b
- 1: Symetrisch aktiv -> 001b
- 2: Symetrisch passiv -> 010b
- 3: Client Modus -> 011b
- 4: Server Modus -> 100b
- 5: Rundrufmodus (broadcast mode) -> 101b
- 6: Reserviert für NTP Kontrollnachrichten -> 110b
- 7: Reserviert für private Nutzung -> 111b
-
Stratum: 8 Sits für die Stratum ebene der Uhr. Eine Stratum 1 Uhr hat die höchste präzision und eine Stratum 15 Uhr hat die niedrigste przision.
- 0: Unspezifiziert oder ungültig 00000000b
- 1: Primärer Server 00000001b
- 2–15: Sekundärer Server 00000010b - 00001111b
- 16: Nicht synchronisiert 00010000b
- 17–255: Reserviert 00010001b - 11111111b
-
Pol: 8 Bit vorzeichenbehaftete Ganzzahl (Integer (signed 8 bit integer) bzw. SBYTE (signed Byte)): Maximales Intervall zwischen erfolgreichen Nachrichten in sekunden.
-
Precision: 8 Bit signed integer: Präzision der Uhr
Nach diesen grundlegenden Informationen folgen:
- Root Dealy: die Zeitverzögerung in Sekunden, die dadurch den hin und rückweg des Datenpakets entsteht. Der Wert ist ein vorzeichenbehaftete Dezimalzahl mit einer festen anzahl von Vor- und Nachkommastellen (fixed point). Die Trennung zwischen dem Vor- und Nachkommaanteil erfolgt zwischen Bit 15 und Bit 16. Dieses Feld ist nur in Servernachrichten relevant.
- Root Dispersion: Maximale Fehlerzeit in Sekunden durch die Frequenztolleranz der Uhr. Der Wert ist ein vorzeichenbehaftete Dezimalzahl mit einer festen anzahl von Vor- und Nachkommastellen (fixed point). Die Trennung zwischen dem Vor- und Nachkommaanteil erfolgt zwischen Bit 15 und Bit 16. Dieses Feld ist nur in Servernachrichten relevant.
- Reference Identifier: Für Stratum 1 Server ist dies ein 4 Zeichen Code der die extrerne Zeitquelle beschreibt (refer to Figure 2). Für sekundäre Server ist hier die IPv4 Adresse der Synchronisationsquelle gespeichert oder die ersten 32 Bit des MD5 Hashs der IPv6 Adresse der Zeitquelle.
- Reference Timestamp: 64 Bit: Referenz-Zeitstempel, der Zeipunkt an dem die lokale Uhr zulezt gesetzt oder aktualisiert wurde. Wenn die lokale Uhr noch nie synchronisiert wurde, sollte der Wert 0 sein.
- Originate Timestamp: 64 Bit : Ursprungs-Zeitstempel mit der lokalen Zeit, wann das NTP Anfragepacket an den Server gesendet wurde.
- Receive Timestamp: 64 Bit Empfangs-Zeitstempel mit der lokalen Serverzeit, wann das NTP Anfragepacket empdfangen wurde.
- Transmit Timestamp: 64 bit: Sende-Zeitstempel mit der lokalen Serverzeit zu der das Antwortpaket an den Client gesendet wurde.
Nun haben wir insgesamt 384 Bit oder 48 Bzte von je 8 Bit.
Für NTP v4 gibt es optionale 96 Bit oder 12 Bytes zu je 8 Bit für Authentifizierungsinformationen
Bauen des Anfragepakets
Bevor du die Zeitinformationen per NTP abfragen kannst, brauchst du ein Anfragepacket in dem oben beschriebenen Format. Für eien ganz einfachen NTP-Client sind folgende informationen erforderlich:
- Leap indicator
- Version Number
- NTP Mode
Aber trotzdem muss das gesendete Anfragepaket 48 Byte groß sein.
Daher definierst du als erstes eine Variable als ein Byte Array mit einer lege von 48.
$ntpData = New-Object byte[] 48
Nun muss du die erforderlichen Informationen in dem Array speichern. Dies befinden sich alle in dem ersten Byte.
Der Leap Indicator ist 0, daher kannst du ihn "ignorieren".
Die NTP-Version setzt du auf 3
Den NTP-Modus setzt du auf Client Modus was ebenfalls einer 3 entspricht.
Die Struktur unseres ersten Byte ist:
LI | Version | Modus | |||||
---|---|---|---|---|---|---|---|
Wert der Bits im Feld | |||||||
2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
Wert der Bits im Byte | |||||||
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
Um die Versionsnummer an die richtige Position im ersten Byte zu bringen, verwendest du den bitweisen oder auch binären Operator "schieben nach links" (shift left) -shl
.
Damit schiebst du die Versionsnummer 3 um 3 Bit nach links.
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
Schieben nach links um 3 Bits (-shl 3 |
|||||||
0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
Nun musst du noch den Wert 3 für den NTP-Modus hinzufügen. Dazu verwendest du ebenfalls einen bitweisen oder binären Operator, und zwar das binäre ODER (OR) -bor
.
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
bitweises oder (-bor ) |
|||||||
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
= | |||||||
0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
Nun hat der Kopf des Anfragepaket (Header) folgendes Format
Precision | Poll | Stratum | LI | Version | Mode | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Wert der Bits innerhalb des Feldes | |||||||||||||||||||||||||||||||
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 |
Wert der Bits innerhalb des Anfragekopfes (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 |
Wenn der Leap Indicator einen anderen Wert wie 0 hat musst du ihn um 6 Bits nach links schieben bevor du ihn mittels binärem ODER zu dem ersten Byte des Headers hinzufügst.
In PowerAhell siht das dann so aus:
[byte]$LI=0
[byte]$NTPVersion=3
[byte]$NTPMode=3
$ntpData[0]=($LI -shl 6) -bor ($NTPVersion -shl 3) -bor $NTPMode
Der Wert des ersten Byte ist jetzt 27d oder 1Bh das Anfragepaket ist nun bereit zum versenden.
Um das Anfragepaket zu versenden, benötigst du entweder den FQDN (Full Qualyfied Domain Name) oder die IP-Adresse eines NTP-Servers.
Zusätzlich benötigst du noch den Port auf dem der NTP-Server auf Anfragen wartet. Standardmäßig ist das der Port 123 UDP (User Datagram Protocol)
Verbinden mit dem NTP-Server, senden der Anfrage und empfangen der Antwort
Nun ist es an der Zeit die Verbindung einzurichten. Dazu verwendest du folgende .NET Klassen:
- System.Net.Dns
- System.Net.IPEndPoint
- System.Net.Sockets.Socket
Und die folgenden Aufzählungen (enums / enumerations)
- System.Net.Sockets.AddressFamily
- System.Net.Sockets.SocketType
- System.Net.Sockets.ProtocolType
Die Klasse System.Net.Dns
wird verwendet, um den FQDN in eine IP-Adresse aufzulösen.
$ntpServer="pool.ntp.org"
$addresses = [System.Net.Dns]::GetHostEntry($ntpServer).AddressList;
Als nächstes erstellst du eine System.Net.IPEndPoint
Objekt. Dafür verwendest du das erste Element in der durch System.Net.Dns
erzeugten Adressliste $addresses
.
#The UDP port number assigned to NTP is 123
#the constructor for IPEndPoint needs the address object and the port number
$ipEndPoint = new-object System.Net.IPEndpoint($addresses[0], 123)
Jetzt bist du bereit für die verbindung zu dem NTP-Server. Dazu benötigst du eine so genannte "Network Socket Connection"
Um diese zu erstellen verwendest du die Klasse System.Net.Sockets.Socket
und verwendest als Parameter:
- Die Adress-Familie: InterNetwork
- Den Socket Typ: dgram (datagram for "connection less" communications)
- Den Protokolltyp: UDP
Um diese Parameter zu anzugeben, verwendest du die genannten Aufzählungsklassen.
#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);
Um "hängende" Verbindungen zu vermeiden, setzt du ein Zeitlimmit das angibt wie lange es dauern darf, bis einen Antwort erfolgt. Ich verwende hier 3 Sekunden bzw. 3000 Millisekunden. Anschließend wird die Verbindung geschlossen.
#Stops code hang if NTP is blocked
$socket.SendTimeout=3000
$socket.ReceiveTimeout = 3000
[int32]$BytesSentReceived = $socket.Send($ntpData)
$BytesSentReceived = $socket.Receive($ntpData)
$socket.Close()
Die Informationen aus dem Antwortpaket
Jetzt hast du alle Daten die du brauchst, aber um sie verwenden zu können, musst du sie aus dem Antwortpaket extrahieren, und in ein verwendbares Format bringen.
Um es etwas leichter zu haben, definierst du ein paar Indexvariablen, die den Startindex des jeweiligen Werts in dem Byte-Array enthalten.
[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;
Headerinformationen auslesen
Als erstes holst du die Header aus dem Antwortpaket. Dazu verwendest du die .NET Klasse System.BitConverter
.
Wenn du dich, daran erinnerst, dass der Header 32 Bit hat, dann macht es sinn, diesen in eine vorzeichenlose 32-Bit Ganzzahl (unsigned integer / uint32) zu konvertieren.
[uint32]$Header = [System.BitConverter]::ToUInt32($ntpData, 0)
Als nächstes holst du dir die benötigten Informationen aus der 32-Bit Zahl. Dafür verwendest du bitweise Operatoren. Um den Leap Indicator zu bekommen, nachst du zuerst ein schieben nach rechts (shift right) um 6 Bit (du erinnerst dich bestimmt, der Leap Indictor belegt die Bits 7 & 8) anschließend machst du ein bitweises UND (binary AND) mit 3d das sorgt dafür, dass der Wert er ersten 2 Bits erhalten bleibt und alle anderen Bits auf 0 gesetzt werden.
[byte]$leap = $Header -shr 6 -band 3
Dann mahcst du wieder ein schieben nach rechts (shift right) dismal aber um 3 Bit und anschließend ein bitweises UND (binary AND) mit 7d (111b). Dadurch bekommst du den Wert der in den ersten 3 Bit gespeichert ist.
[byte]$ServerVersion = $Header -shr 3 -band 7
Um den NTP-Modus zu bekommen, machst du einfach ein binäres UND (binary AND) mit 7d (111b)
[byte]$NTPMode = $Header -band 7
Als kleine Anmerkung: Du kannst die obigen Schritte in einer belibigen Reihenfolge ausführen. Bitweise Operatoren verändern nicht den Ursprungswert, sondern geben den veränderten Wert als ergebnis zurück.
$Zahl -shr 8
verändert also nicht den Wert von $Zahl
Um nun den Wert für Stratum zu bekommen, du ahnst es sicher schon, machst du ein schieben nach rechts (shift right) um 8 Bit und anschließend ein bitweise UND (binary AND) mit 255d (11111111b / ffh)
[byte]$stratum = $Header -shr 8 -band 255
Den richtigen Wert für Poll zu bekommen, ist ein bischen schwieriger, da es sich hier umd eine vorzeichenbehaftete Ganzzahl mit 8 Bit handelt.
Eine vorzeichenbehaftete 8 Bit Ganzzahl hat einen maximalen Wert von 127d 0111111b und einen minimalen Wert von -128d 10000000b das ist deshalb so, da negative Werte in der Komplementärdarstellung (2er-komplement) (Invertiert) dargestellt werden.
Das höchste Bit (128) hat eine doppelte funktion:
- ist es gesetzt ist die Zahl negativ
- Zum zweiten hat es nach wi vor den Wert 128
Ist also nur diese eine Bit gesetzt ergibt sich ein Wert von -128. Alle anderen bits behalten ihren positiven Wert.
Das heißt, das bit 7 hat also den Wert 64d und wenn es gesetzt istwird der Wert zu den -128d addiert.
Mathematisch heißt das dann -128 + 64 = -64
Daher ist bei einer vorzeichenbehafteten Ganzzahl mit 8 Bit der binäre Wert 11111111b der dezimale Wert -1.
Mathematisch betrachtet entspricht das: -128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1
Wenn du das nun weißt, ist es eigentlich auch nicht mehr schwer.
Als erstes holst du dir den Wert als vorzeichenlose 8 Bit Zahl aus dem Header indem du ein shieben nach rechts um 16 Bit machst und anschließend ein binäres UND mit 255d
[byte]$upoll = $header -shr 16 -band 255
Als nächstes prüfst du, ob das höchste Bit gesetzt ist oder nicht. Das machst du wieder mit einem binären UND, und zwar mit dem Wert 128d
Wenn das bit gesetzt ist machst du ein binäres UND mit 127 gegen den Wert und speicherst das Resultat in einer Variablem vom Typ vorzeichenbehaftetes Byte (SBYTE).
Danach hast du zwei Möglichkeiten:
- du machst ein ninäres ODER mit -128
- du nimmst ein vorzeichenbehaftetes Byte mit dem Wert 1 machst ein schieben nach links (shift left) um 7 Bit und noch ein binäres ODER ersten vorzeichenbehafteten Byute.
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
}
Alternativ kannst du das auch so schreiben:
[sbyte]$poll = $upoll -band 127
if(($upoll -band 128) -eq 128 ){
$poll = $poll -bor ([sbyte]-128)
}
Das funktioniert, da das höchste Bit niemals gesetzt sein wird, wenn die Zahl positiv ist, und für positive zahlen nun einmal nur die ersten 7 Bit zur verfügung stehen.
Den Wert für "Precision" bekommen wir auf die gleiche Art.
[byte]$uPrecision = $header -shr 24 -band 255
[sbyte]$Precision = $uPrecision -band 127
if(($uPrecision -band 128) -eq 128 ){
$Precision = $Precision -bor ([sbyte]-128)
}
Jetzt hast du alle Informationen aus dem Header
Für die anderen Daten in dem Antwortpaket musst du beachten, dass sie dort in der NetworkByte Order auch bekannt al Big Endian gespeichert sind.
Daher musst du zuerst die Anordnung der Bytes in das Little Endian Format bringen. (Dass ist auch die für Intel CPUs normale Reihenfolge)
Um das zu tun, habe ich hier zwei Funktionen die das machen: SwapEndianes und SwapEndianess64
Diese beiden Funktionen sollen den Vorgang verdeutlichen. Später verwendest du nur eine Funktion dafür der du entweder eine vorzeichenlose 32-Bit oder 64-Bit Ganzzahl übergeben kannst.
Die Funktion Vertauscht bei einer 32-Bit Zahl das erste mit dem letzten Byte und das zweite mit dem vorletzten Byte, damit ist der vorgang abgeschlossen. Bei einer 64-Bit Zahl geht es weiter... das dritte Byte wird mit dem drittletzten Byte vertauscht und so weiter...
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))))
)
}
Jetzt ist es an der Zeit, den Wert für "root delay" auszulesen. Das Problem hierbei ist, dass es eine "Festkommazahl" handelt und das Komma befindet sich zwischen dem 15ten und 16ten Bit. Sie ist aber als vorzeichenlose 32-Bit Ganzzahl gespeichert.
Deswegen musst du folgende Schritte machen.
Hole das "root delay" Feld aus dem NTP-Antwortpaket, ändere die Byte Anordnung auf Little Endian und konvertiere die Vorzeichenlose Ganzzahl in eine Fließkommazahl.
Das klingt komplizirter als es ist.
Anstatt bit operationen verwendest du hier die Methoden der Klasse System.BitConverter
. Damit konvertierst du die vorzeichenlose 32-Bit Ganzzahl (uint32) in eine vorzeichenbehaftete 32-Bit Ganzzahl (int32)
Um das zu tun, wird die vorzeichenlose Ganzzahl zuerst in ein Byte-Array konvertiert und anschließend wird das Byte-Array in dien vozeichenbehaftete Ganzzahl konvertiert.
Anschließend konvertierst du die vorzeichenbehaftete Ganzzahl in eine Fließkommazahl (floating point) mit einfacher genauigkeit (single). Das geht ganz einfach, indem du einen so genannten Cast durchführst. Das bedeutet nichts anderes, als dass du vor die Variable mit der vorzeichenbehafteten Ganzzahl den gewünschten Datentyp schreibst, in diesem fall [single]
. Damit jetzt auch noch der richtige Wert vorhanden ist, musst du die Zahl einmal durch 65536d bzw. 0x10000h dies entspricht dem Wert, den du erhältst, wenn du 1 um 16 Bit nach links schiebst (shift left) 1 -shl 16
= 65536d bzw. 0x10000h dadurch wird der Nachkommateil der Zahl hinter das Komma verschoben.
[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)
Der Vollständigkeit halber hier auch noch die Lösung für die root dispersion:
[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)
Als nächstes brauchst du den Wert für den Reference Identifier.
Der Reference Identifier is insofern besonders, da er, basierend auf dem Stratum Level des Servers, Informationen unterschiedlichen Typs speichert.
Wenn es sich um einen Stratum 1 Server handelt, enthält der Reference Identifier 4 ASCII Zeichen welceh die Referenz-Uhr bezeichnen.
Wenn es sich um einen Startum 2 Server oder höher handelt, speichert der Reference Identifier die IPv4-Adresse der letzten Synchronisationsquelle.
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 |
Im gegensatz zu andren Feldern, kannst du die Werte aus diesem direkt aus den einzelnen Bytes holen. Dies ist dem Fakt geschuldet, dass es Powershell ermöglicht mehrere Indices eines Array zusammen zu verwenden. Oder anderst ausgedrückt, wir können ein Array von Indexnummern verwenden, um auf die gewünschten Werte zuzugreifen.
Daher enthät die Varible $IdxReferenceIdentifier
alle 4 Indices die du brauchst um auf die Bytes zuzugreifen.
Als Resultat erhältst du ein Byte-Array mit genau 4 Einträgen.
Die Klasse ipaddress
speichert IP-Adressen in Big-Endian.
Das einzige was du jetzt machen musst, ist ein "Encoder" objekt für ASCII zu erstellen und den Statum Level des NTP-Servers prüfen.
$enc = [System.Text.Encoding]::ASCII
if($stratum -eq 1){
[string]$ReferenceIdentifier = $enc.GetString($ntpData[$IdxReferenceIdentifier])
}
else {
[ipaddress]$ReferenceIdentifier = [ipaddress]::new($ntpData[$IdxReferenceIdentifier])
}
Im nächsten Schritt holst du dir die Zeitstempel aus dem empfangengen Paket.
Jeder Zeitstempel hat 8 Byte oder 64 Bit und enthält die seit dem 01.01.1900 bis zur erstellung des Zeitstempels vergangenen Sekunden.
Dafür verwendest du die Klasse System.BitConverter
und wandelst die 8 Byte in eine vorzeichenlose 64-Bit Ganzzahl um.
[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)
Um jetzt diesen Wert in einen DateTime Wert umzuwanden musst du ihn zuerst in Millisekunden umrechnen. Dabei musst du berücksichtigenm, dass der Zeitstempel einen 32-Bit Ganzzahlteil und einen 32-Bit Nachkommaanteil enthält. Daher definierts du zuerst eine Bitmake für den Ganzzahlteil und eine für den Nachkommaanteil.
[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
Als nächstes wandelst du den Zeitstempel, der in Big-Endian gespeichert ist in Little-Endian um. Das für dich Wichtigste Feld, ist ServerReplyTime.
$NTPTimeField = SwapEndianness $ServerReplayTime
jetzt "konvertierst" du beide Teile jeweils in eine vorzeichenlose 32-Bit Ganzzahl.
[uint32]$fractPart = ($NTPTimeField -band $fractPartMask)
[uint32]$intPart = ($NTPTimeField -band $IntPartMask) -shr 32
Anshließend rechnest du biede Werte in Millisekunden um und Konvertierst den Nachkommateil in eine Fließkommazahl doppelter genauigkeit. Jetzt fügst du beide Wete durch eine Adition zusammen und sorgst dabei dafür, dass das Ergebnis ebenfalls eine Fließkommazahl doppelter genauigkeit ist.
[double]$milliseconds = ($intPart * 1000) + ((([double]$fractPart) * 1000) / 0x100000000);
Wenn du das erledigt hast, erstellst du ein Datumsobjekt mit dem Wert für den 01.01.1900 und addierst dann die erhaltenen Millisekunden hinzu.
$NetworkDateTimeUtc=([DateTime]::new(1900, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)).AddMilliseconds($milliseconds);
Das wichtigste ist nun erledigt und du kannst mit dem Erhaltenen Wert deine lokale System-Uhr einstellen.
Set-Date -Date $($NetworkDateTime.ToLocalTime())
Informations for calculating / estimate the delay and the offset of the client local clock
Ein NTP-Client, schätzt die zeitverzögerung zwischen Client und Server um den die Abweichung der Client Uhr zu ermitteln. Dazu werden folgende Zeitstempel verwendet:
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 |
Die Berechnung der "Schätzung" wird mit folgenden Formel durchgeführt:
Offset = [(t2 + t3) - (t4 + t1)] / 2
Delay = (t4 - t1) - (t3 - t2)
Das alles ist sehr rudimentär, daher kanns du das ganze wie folgt zu einem Script mit Funktionen zusammenfügen.
Vollständingges NTP-Client Script
Ich bin sicher, dass du hier eine menge Verbesserungen einbauen kannst, und die ein oder andere Fehlerbehandlung könnte auch nicht schaden...
Aber für den Moment sollte das ausreichen.
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="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
}
}
```