Ein einfacher NTP-Client mit PowerShell

2024-10-12

Ist 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:

  1. 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.
  2. 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:

  1. du machst ein ninäres ODER mit -128
  2. 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
            }
            }


            ```