Saturday, June 18, 2016

Getting the current time from an NTP service using PowerShell

Recently I needed to be able to query the current time from an NTP service using PowerShell. I was surprised to find out that it wasn’t going to be easy.

There isn’t a simple-to-call way to do it. With modern Windows computers, you tell the operating system which NTP server to sync with, or let it sync with a domain controller or a Hyper-V host, and Windows does all of the doing automatically and invisibly.

But I was automating minimal configuration of thousands of brand new physical machines with a generic OS installed. My script needed to prepare them to successfully and securely talk to the system that would handle all of the configuration, domain join, etc. The challenge was the occasional new machine which, for whatever reason, had an internal clock that was far enough off that secure communications were being rejected. I wanted to add a simple piece of code that would query a corporate NTP service and set the system time, without modifying the machine’s NTP configuration.

After some searching, I found a script in the PowerShell Gallery name Get-NTPTime, written by Chris J Warwick. https://gallery.technet.microsoft.com/scriptcenter/Get-Network-NTP-Time-with-07b216ca

It’s a great script, but it outputs a lot more than I need, does a lot of work to create that output, is more precise than I need, and at over 500 lines (mostly comments) a bit too large to comfortably drop into my script.

So I heavily modified his script, ripping out everything I didn’t need, ripping out the high level of accuracy, and simplifying some of the syntax to make it easier for future engineers to support my script. The result is the reasonably concise function below.

There is no error handling in the function. It is presumed that error handling, if needed, will be handled at a higher level.

Our only parameter is a string with the (resolvable) name or IP address of the NTP service to query. We will assume the service is listening on port 123.

Function Get-NtpTime ( [String]$NTPServer )
{

We create an array of 48 bytes which we will use to hold our NTP request packet, and then reuse to hold the response packet.

$NTPData    = New-Object byte[] 48

We populate the first byte with the request header.

$NTPData[0] = 27 # Request header: 00 = No Leap Warning; 011 = Version 3; 011 = Client Mode; 00011011 = 27

We open a connection to the NTP service, setting timeouts so it doesn’t hang.

$Socket = New-Object Net.Sockets.Socket ( 'InterNetwork', 'Dgram', 'Udp' )
$Socket.SendTimeOut    = 2000  # ms
$Socket.ReceiveTimeOut = 2000  # ms
$Socket.Connect( $NTPServer, 123 )

Then we send the request and receive the results. Both of the .Send() and .Receive() methods output the number of bytes sent or received. We don’t need that output, so we’ll send those to $Null. Setting $Null equal to the result is much faster than piping to Out-Null, as PowerShell doesn’t have to waste the time and resources creating and handling a pipeline to nowhere.

$Null = $Socket.Send(    $NTPData )
$Null = $Socket.Receive( $NTPData )

Close the connection.

$Socket.Shutdown( 'Both' )
$Socket.Close()

The date/times in the response packet are in a weird epoch-based format. 4 bytes hold the number of seconds which passed between midnight of January 1, 1900, and the UTC time* represented. An additional 4 bytes hold a number which, when divided by 2^32, is an additional fraction of a second to a ridiculous precision.

In his version of this script, Chris uses two date/times in the response packet in conjunction with local times captured immediately before and after the query to calculate an extremely precise offset between the local system time and the NTP service time which even accounts for network latency.

I don’t need that level of accuracy; I just need to get it reasonable close. So instead of working with four time stamps, I am going to just use the first one in the response packet. And I am going to ignore the fraction of a second and any network latency. Most of the time, this will give me a time within one second of the NTP service time.

We can use the static .Net class [System.BitConverter] to convert the relevant 4 bytes into a single integer. We convert to UInt32 (unsigned integer) instead of Int32, because in this case the highest bit is set to one, and Int32 would interpret that as indicating a negative number rather than as part of the number itself. This gives us the number of seconds since 1/1/1900. (Who designs this stuff? Engineers are weird.)**

$Seconds = [BitConverter]::ToUInt32( $NTPData[43..40], 0 )

Lastly, we convert the number of seconds elapsed to the correct date time by creating a datetime object for 1/1/1900 and adding the seconds to it. Then we convert it from UTC to whatever the local time zone is, and return the result.

[datetime]'1/1/1900' ).AddSeconds( $Seconds ).ToLocalTime()
}

Using our function to set the system time can be as simple as this.

Set-Date ( Get-NTPDateTime $NTPServiceIPAddress )



Here is how it looks all together.

Function Get-NtpTime ( [String]$NTPServer )
{
# Build NTP request packet. We'll reuse this variable for the response packet
$NTPData    = New-Object byte[] 48  # Array of 48 bytes set to zero
$NTPData[0] = 27                    # Request header: 00 = No Leap Warning; 011 = Version 3; 011 = Client Mode; 00011011 = 27

# Open a connection to the NTP service
$Socket = New-Object Net.Sockets.Socket ( 'InterNetwork', 'Dgram', 'Udp' )
$Socket.SendTimeOut    = 2000  # ms
$Socket.ReceiveTimeOut = 2000  # ms
$Socket.Connect( $NTPServer, 123 )

# Make the request
$Null = $Socket.Send(    $NTPData )
$Null = $Socket.Receive( $NTPData )

# Clean up the connection
$Socket.Shutdown( 'Both' )
$Socket.Close()

# Extract relevant portion of first date in result (Number of seconds since "Start of Epoch")
$Seconds = [BitConverter]::ToUInt32( $NTPData[43..40], 0 )

# Add them to the "Start of Epoch", convert to local time zone, and return
[datetime]'1/1/1900' ).AddSeconds( $Seconds ).ToLocalTime()
}



* Irrelevant bonus fact: We use the abbreviation UTC because it is wrong in two different languages. In English, it should be CUT for “coordinated universal time”. In French, it should be TUC for “temps universel coordonné”. We compromised by agreeing to be universally uncoordinated with our time abbreviations.

** Irrelevant bonus fact 2: Storing date/times by using an Epoch-based system has been a common solution. For example, the .Net datetime objects we use in PowerShell store the number of ten millionths of a second since midnight of January 1 of the year 1. We lost a space probe to a short-sighted epoch-based system. On August 11, 2013, shortly after midnight, NASA's Deep Impact space probe, travelling between comets, stopped talking to us, because it was 2^32 tenths of a second after 1/1/2000. The clock rolled over, the probe thought it was suddenly 13 years earlier and it wasn’t due to check in again for over a decade.

No comments:

Post a Comment