Getting SNTP Network Time with PowerShell (Improved!)

This was originally posted here, with further notes.

This is an improved version that includes the network delay as per the NTP rfc and uses the resulting offset to calculate a more accurate time.

The full code is here: https://skydrive.live.com/redir?resid=7CB58BE453F7E567!3445

Pipe to “Format-List *” to see all returned information.  Usage:

PS:\> Get-NtpTime uk.pool.ntp.org

NtpTime          : 16/09/2012 14:26:46
OffsetSeconds    : -0.19
NtpVersionNumber : 3
Mode_text        : server
Stratum          : 3
PollInterval     : 00:00:01
PrecisionSeconds : 1.9073486328125E-06

 

PS:\> Get-NtpTime uk.pool.ntp.org | fl *

NtpTime          : 16/09/2012 14:26:51
Offset           : -190.25927734375
OffsetSeconds    : -0.19
Delay            : 18.0205078125
t1ms             : 3556790811477.35
t2ms             : 3556790811296.1
t3ms             : 3556790811296.1
t4ms             : 3556790811495.37
t1               : 16/09/2012 14:26:51
t2               : 16/09/2012 14:26:51
t3               : 16/09/2012 14:26:51
t4               : 16/09/2012 14:26:51
LI               : 0
LI_text          : no warning
NtpVersionNumber : 3
Mode             : 4
Mode_text        : server
Stratum          : 3
Stratum_text     : secondary reference (via NTP or SNTP)
PollIntervalRaw  : 0
PollInterval     : 00:00:01
Precision        : -19
PrecisionSeconds : 1.9073486328125E-06
Raw              : {28, 3, 0, 237, 0, 0, 2, 183, 0, 0, 0, 0, 80, 93, 163, 202, 212, 0, 80, 97…}

If you don’t like comments in your code (and just want the resulting timestamp), here’s the bare bones version:

<#
Chris Warwick, @cjwarwickps, August 2012
chrisjwarwick.wordpress.com 
#>

$Server = 'pool.ntp.org'
$StartOfEpoch=New-Object DateTime(1900,1,1,0,0,0,[DateTimeKind]::Utc)   

[Byte[]]$NtpData = ,0 * 48
$NtpData[0] = 0x1B    # NTP Request header in first byte

$Socket = New-Object Net.Sockets.Socket([Net.Sockets.AddressFamily]::InterNetwork,
                                        [Net.Sockets.SocketType]::Dgram,
                                        [Net.Sockets.ProtocolType]::Udp)
$Socket.Connect($Server,123)
$t1 = Get-Date    # Start of transaction... the clock is ticking...
[Void]$Socket.Send($NtpData)
[Void]$Socket.Receive($NtpData)  
$t4 = Get-Date    # End of transaction time
$Socket.Close()

$IntPart = [BitConverter]::ToUInt32($NtpData[43..40],0)   # t3
$FracPart = [BitConverter]::ToUInt32($NtpData[47..44],0)
$t3ms = $IntPart * 1000 + ($FracPart * 1000 / 0x100000000)

$IntPart = [BitConverter]::ToUInt32($NtpData[35..32],0)   # t2
$FracPart = [BitConverter]::ToUInt32($NtpData[39..36],0)
$t2ms = $IntPart * 1000 + ($FracPart * 1000 / 0x100000000)

$t1ms = ([TimeZoneInfo]::ConvertTimeToUtc($t1) - $StartOfEpoch).TotalMilliseconds
$t4ms = ([TimeZoneInfo]::ConvertTimeToUtc($t4) - $StartOfEpoch).TotalMilliseconds
 
$Offset = (($t2ms - $t1ms) + ($t3ms-$t4ms))/2

$StartOfEpoch.AddMilliseconds($t4ms + $Offset).ToLocalTime() 
Advertisements

Getting NTP/SNTP Network Time with PowerShell

(Update: There’s now an improved version of this script here that calculates network offsets as per the NTP rfc)

I wrote this function because I needed to check the NTP responses from a Windows Active Directory Domain Controller.  I found some NTP procedures on the web but actually most of them were using the Daytime service on Port 13 (not NTP).

Here’s a very quick version of the active part of the script:

$Server = ‘uk.pool.ntp.org’

# Construct client NTP time packet to send to specified server
# (Request Header: [00=No Leap Warning; 011=Version 3; 011=Client Mode]; 00011011 = 0x1B)

[Byte[]]$NtpData = ,0 * 48
$NtpData[0] = 0x1B  

$Socket = New-Object Net.Sockets.Socket([Net.Sockets.AddressFamily]::InterNetwork,
[Net.Sockets.SocketType]::Dgram,
[Net.Sockets.ProtocolType]::Udp)

$Socket.Connect($Server,123)
[Void]$Socket.Send($NtpData)
[Void]$Socket.Receive($NtpData)    # Returns length – should be 48…

$Socket.Close()

# Decode the received NTP time packet

# We now have the 64-bit NTP time in the last 8 bytes of the received data.
# The NTP time is the number of seconds since 1/1/1900 and is split into an
# integer part (top 32 bits) and a fractional part, multipled by 2^32, in the
# bottom 32 bits.

# Convert Integer and Fractional parts of 64-bit NTP time from byte array
$IntPart=0;  Foreach ($Byte in $NtpData[40..43]) {$IntPart  = $IntPart  * 256 + $Byte}
$FracPart=0; Foreach ($Byte in $NtpData[44..47]) {$FracPart = $FracPart * 256 + $Byte}

# Convert to Millseconds (convert fractional part by dividing value by 2^32)
[UInt64]$Milliseconds = $IntPart * 1000 + ($FracPart * 1000 / 0x100000000)

# Create UTC date of 1 Jan 1900, add the NTP offset and convert result to local time
(New-Object DateTime(1900,1,1,0,0,0,[DateTimeKind]::Utc)).AddMilliseconds($Milliseconds).ToLocalTime()

 

This just returns a [DateTime] object derived from the NTP Server.  There’s more information available though; the following full function decodes the NTP Time data and returns it in a custom PowerShell object.  Note that the returned object does not display all of its attributes by default – pipe to “Format-List *” to see all the values.

Usage: Get-NtpTime <Server>

Sample output:

PS:\>  Get-NtpTime
NtpTime          : 26/08/2012 19:54:52
NtpVersionNumber : 3
Mode_text        : server
Stratum          : 2
PollInterval     : 00:00:08
PrecisionSeconds : 4.76837158203125E-07
LocalDifference  : -00:00:00.2891348

Full function:

<#
Chris Warwick, @cjwarwickps, August 2012
chrisjwarwick.wordpress.com

Get Datetime from NTP server.

This sends an NTP time packet to the specified NTP server and reads back the response.
The NTP time packet from the server is decoded and returned.

Note: this uses NTP (rfc-1305: http://www.faqs.org/rfcs/rfc1305.html) on UDP 123.  Because the
function makes a single call to a single server this is strictly a SNTP client (rfc-2030).
Although the SNTP protocol data is similar (and can be identical) and the clients and servers
are often unable to distinguish the difference.  Where SNTP differs is that is does not
accumulate historical data (to enable statistical averaging) and does not retain a session
between client and server.

An alternative to NTP or SNTP is to use Daytime (rfc-867) on TCP port 13 – although this is an
old protocol and is not supported by all NTP servers.

See comments at the end of the script for an extract of the SNTP rfc.

#>

#Requires -Version 3

Function Get-NtpTime {

<#
.Synopsis
Gets (Simple) Network Time Protocol time (SNTP/NTP, rfc-1305, rfc-2030) from a specified server
.DESCRIPTION
This function connects to an NTP server on UDP port 123 and retrieves the current NTP time.
Selected components of the returned time information are decoded and returned in a PSObject.
.PARAMETER Server
The NTP Server to contact.  Uses pool.ntp.org by default.
.EXAMPLE
Get-NtpTime uk.pool.ntp.org
Gets time from the specified server.
.EXAMPLE
Get-NtpTime | fl *
Get time from default server (pool.ntp.org) and displays all output object attributes.
.OUTPUTS
A PSObject containing decoded values from the NTP server.  Pipe to fl * to see all attributes.
.FUNCTIONALITY
Gets NTP time from a specified server.
#>

    [CmdletBinding()]
Param (
[String]$Server = ‘pool.ntp.org’
)

    # Construct a 48-byte client NTP time packet to send to the specified server
# (Request Header: [00=No Leap Warning; 011=Version 3; 011=Client Mode]; 00011011 = 0x1B)

    [Byte[]]$NtpData = ,0 * 48
$NtpData[0] = 0x1B  

    $Socket = New-Object Net.Sockets.Socket([Net.Sockets.AddressFamily]::InterNetwork,
[Net.Sockets.SocketType]::Dgram,
[Net.Sockets.ProtocolType]::Udp)

    $LocalTime = Get-Date

    Try {
$Socket.Connect($Server,123)
[Void]$Socket.Send($NtpData)
[Void]$Socket.Receive($NtpData)
}
Catch {
Write-Error “Failed to communicate with server $Server”
Throw $_
}

    $Socket.Close()

    # Decode the received NTP time packet

    # Extract the flags from the first byte by masking and shifting (dividing)

    $LI = ($NtpData[0] -band 0xC0)/64    # Leap Second indicator
$LI_text = Switch ($LI) {
0    {‘no warning’}
1    {‘last minute has 61 seconds’}
2    {‘last minute has 59 seconds’}
3    {‘alarm condition (clock not synchronized)’}
}
$VN = ($NtpData[0] -band 0x38)/8     # Server versions number
$Mode = ($NtpData[0] -band 0x07)     # Server mode (probably ‘server’!)
$Mode_text = Switch ($Mode) {
0    {‘reserved’}
1    {‘symmetric active’}
2    {‘symmetric passive’}
3    {‘client’}
4    {‘server’}
5    {‘broadcast’}
6    {‘reserved for NTP control message’}
7    {‘reserved for private use’}
}

    $Stratum = [UInt16]$NtpData[1]   # Actually [UInt8] but we don’t have one of those…
$Stratum_text = Switch ($Stratum) {
0                            {‘unspecified or unavailable’}
1                            {‘primary reference (e.g., radio clock)’}
{$_ -ge 2 -and $_ -le 15}    {‘secondary reference (via NTP or SNTP)’}
{$_ -ge 16}                  {‘reserved’}
}

    $PollInterval = $NtpData[2]              # Poll interval – to neareast power of 2
$PollIntervalSeconds = [Math]::Pow(2, $PollInterval)

    $PrecisionBits = $NtpData[3]      # Precision in seconds to nearest power of 2
# …this is a signed 8-bit int
If ($PrecisionBits -band 0x80) {    # ? negative (top bit set)
[Int]$Precision = $PrecisionBits -bor 0xFFFFFFE0    # Sign extend
} else {
# ..this is unlikely – indicates a precision of less than 1 second
[Int]$Precision = $PrecisionBits   # top bit clear – just use positive value
}
$PrecisionSeconds = [Math]::Pow(2, $Precision)

    # We now have the 64-bit NTP time in the last 8 bytes of the received data.
# The NTP time is the number of seconds since 1/1/1900 and is split into an
# integer part (top 32 bits) and a fractional part, multipled by 2^32, in the
# bottom 32 bits.

    # Convert Integer and Fractional parts of 64-bit NTP time from byte array
$IntPart=0;  Foreach ($Byte in $NtpData[40..43]) {$IntPart  = $IntPart  * 256 + $Byte}
$FracPart=0; Foreach ($Byte in $NtpData[44..47]) {$FracPart = $FracPart * 256 + $Byte}

    # Convert to Millseconds (convert fractional part by dividing value by 2^32)
[UInt64]$Milliseconds = $IntPart * 1000 + ($FracPart * 1000 / 0x100000000)

    # Create UTC date of 1 Jan 1900, add the NTP offset and convert the result from utc to local time
$NtpTime=(New-Object DateTime(1900,1,1,0,0,0,[DateTimeKind]::Utc)).AddMilliseconds($Milliseconds).ToLocalTime()

    # Create Output object and return

    $NtpTimeObj = [PSCustomObject]@{
NtpTime = $NtpTime
LI = $LI
LI_text = $LI_text
NtpVersionNumber = $VN
Mode = $Mode
Mode_text = $Mode_text
Stratum = $Stratum
Stratum_text = $Stratum_text
PollIntervalRaw = $PollInterval
PollInterval = New-Object TimeSpan(0,0,$PollIntervalSeconds)
Precision = $Precision
PrecisionSeconds = $PrecisionSeconds
LocalDifference = [TimeSpan]($NtpTime – $LocalTime)
Raw = $NtpData   # The undecoded bytes returned from the NTP server
}

    # Set default display properties for object

    [String[]]$DefaultProperties =  ‘NtpTime’, ‘NtpVersionNumber’,’Mode_text’,’Stratum’, ‘PollInterval’,
‘PrecisionSeconds’, ‘LocalDifference’

    # Add the PSStandardMembers.DefaultDisplayPropertySet member
$ddps = New-Object Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’, $DefaultProperties)
$PSStandardMembers = [Management.Automation.PSMemberInfo[]]$ddps

    # Attach default display property set and output object
$NtpTimeObj | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers -PassThru
}

 

<#

From rfc-2030
~~~~~~~~~~~~~

NTP time packet format

                           1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN  |Mode |    Stratum    |     Poll      |   Precision   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Root Delay                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Root Dispersion                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Reference Identifier                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                   Reference Timestamp (64)                    |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                   Originate Timestamp (64)                    |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                    Receive Timestamp (64)                     |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                    Transmit Timestamp (64)                    |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

   Leap Indicator (LI): This is a two-bit code warning of an impending
leap second to be inserted/deleted in the last minute of the current
day, with bit 0 and bit 1, respectively, coded as follows:

      LI       Value     Meaning
——————————————————-
00       0         no warning
01       1         last minute has 61 seconds
10       2         last minute has 59 seconds)
11       3         alarm condition (clock not synchronized)

   Version Number (VN): This is a three-bit integer indicating the
NTP/SNTP version number. The version number is 3 for Version 3 (IPv4
only) and 4 for Version 4 (IPv4, IPv6 and OSI). If necessary to
distinguish between IPv4, IPv6 and OSI, the encapsulating context
must be inspected.

   Mode: This is a three-bit integer indicating the mode, with values
defined as follows:

      Mode     Meaning
————————————
0        reserved
1        symmetric active
2        symmetric passive
3        client
4        server
5        broadcast
6        reserved for NTP control message
7        reserved for private use

   In unicast and anycast modes, the client sets this field to 3
(client) in the request and the server sets it to 4 (server) in the
reply. In multicast mode, the server sets this field to 5
(broadcast).

   Stratum: This is a eight-bit unsigned integer indicating the stratum
level of the local clock, with values defined as follows:

      Stratum  Meaning
———————————————-
0        unspecified or unavailable
1        primary reference (e.g., radio clock)
2-15     secondary reference (via NTP or SNTP)
16-255   reserved

   Poll Interval: This is an eight-bit signed integer indicating the
maximum interval between successive messages, in seconds to the
nearest power of two. The values that can appear in this field
presently range from 4 (16 s) to 14 (16284 s); however, most
applications use only the sub-range 6 (64 s) to 10 (1024 s).

   Precision: This is an eight-bit signed integer indicating the
precision of the local clock, in seconds to the nearest power of two.
The values that normally appear in this field range from -6 for
mains-frequency clocks to -20 for microsecond clocks found in some
workstations.

   Root Delay: This is a 32-bit signed fixed-point number indicating the
total roundtrip delay to the primary reference source, in seconds
with fraction point between bits 15 and 16. Note that this variable
can take on both positive and negative values, depending on the
relative time and frequency offsets. The values that normally appear
in this field range from negative values of a few milliseconds to
positive values of several hundred milliseconds.

   Root Dispersion: This is a 32-bit unsigned fixed-point number
indicating the nominal error relative to the primary reference
source, in seconds with fraction point between bits 15 and 16. The
values that normally appear in this field range from 0 to several
hundred milliseconds.

   Reference Identifier: This is a 32-bit bitstring identifying the
particular reference source. In the case of NTP Version 3 or Version
4 stratum-0 (unspecified) or stratum-1 (primary) servers, this is a
four-character ASCII string, left justified and zero padded to 32
bits. In NTP Version 3 secondary servers, this is the 32-bit IPv4
address of the reference source. In NTP Version 4 secondary servers,
this is the low order 32 bits of the latest transmit timestamp of the
reference source. NTP primary (stratum 1) servers should set this
field to a code identifying the external reference source according
to the following list. If the external reference is one of those
listed, the associated code should be used. Codes for sources not
listed can be contrived as appropriate.

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

   Reference Timestamp: This is the time at which the local clock was
last set or corrected, in 64-bit timestamp format.

   Originate Timestamp: This is the time at which the request departed
the client for the server, in 64-bit timestamp format.

   Receive Timestamp: This is the time at which the request arrived at
the server, in 64-bit timestamp format.

   Transmit Timestamp: This is the time at which the reply departed the
server for the client, in 64-bit timestamp format.

#>