NtpTime.psm1

<#
Chris Warwick, @cjwarwickps, August 2012. Updates September 2015.
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. This NTP function will be more accurate than
Daytime (since it takes network delays into account) but the result is only ever based on a
single sample. Depending on the source server and network conditions the actual returned time
may not be as accurate as required.

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

 
Script Operation, Detail:

Construct an NTP request packet
Record the current local time; This is time t1, the 'Originate Timestamp'
Send the NTP request packet to the selected server
Read the server response
Record the current local time after reception. This is time t4.

The received packet now contains:
  t1 - Originate Timestamp (the time the request packet was sent from the client)
  t2 - Receive Timestamp (the time the request packet arrived at the server)
  t3 - Transmit Timestamp (the time the response packet left the server)
(Note that we don't send the originate timestamp (t1) so this will be 0 in the response)

Calculate clock offset and delay:

Estimated Clock Offset
This is the difference between the server clock and the local clock taking into account
the network latency. If both server and client clocks have the same absolute time
then the clock difference minus the network latency will be 0.

Assuming symetric send/receive delays, the average of the out and return times will
equal the offset.

   Offset = (OutTime+ReturnTime)/2

   Offset = ((t2 - t1) + (t3 - t4))/2

Adding the offset to the local clock will give the correct time.


Round Trip Delay (= the time actually spent on the network)
This is the total transaction time (between t1..t4) minus the server 'thinking
time' (between t2..t3)

   Delay = (t4 - t1) - (t3 - t2)

This value is useful for NTP servers because the most accurate offsets will be obtained from
responses with lower network delays. When considering the single response obtained by this
script the Delay value is only useful as an indicator of the likely quality of the result

#>



#Requires -Version 3

Set-StrictMode -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.
.PARAMETER MaxOffset
   The maximum acceptable offset between the local clock and the NTP Server, in milliseconds.
   The script will throw an exception if the time difference exceeds this value (on the assumption
   that the returned time may be incorrect). Default = 10000 (10s).
.PARAMETER NoDns
   (Switch) If specified do not attempt to resolve Version 3 Secondary Server ReferenceIdentifiers.
.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()]
    [OutputType('NtpTime')]
    Param (
        [String]$Server = 'pool.ntp.org',
        [Int]$MaxOffset = 10000,     # (Milliseconds) Throw exception if network time offset is larger
        [Switch]$NoDns               # Do not attempt to lookup V3 secondary-server referenceIdentifier
    )


    # NTP Times are all UTC and are relative to midnight on 1/1/1900
    $StartOfEpoch = New-Object -TypeName DateTime -ArgumentList (1900,1,1,0,0,0,[DateTimeKind]::Utc)


    Function Convert-OffsetToLocal {
    Param ([Long]$Offset)
        # Convert milliseconds since midnight on 1/1/1900 to local time
        $StartOfEpoch.AddMilliseconds($Offset).ToLocalTime()
    }


    # Construct a 48-byte client NTP time packet to send to the specified server
    [Byte[]]$NtpData = ,0 * 48

    # (Construct Request Header: [00=No Leap Warning; 011=Version 3; 011=Client Mode]; 00011011 = 0x1B)
    $NtpData[0] = 0x1B    # NTP Request header in first byte


    ## Todo: See email about calling UDP connect with no internet connection...
    $Socket = New-Object -TypeName Net.Sockets.Socket -ArgumentList ([Net.Sockets.AddressFamily]::InterNetwork,
                                                                     [Net.Sockets.SocketType]::Dgram,
                                                                     [Net.Sockets.ProtocolType]::Udp)
    $Socket.SendTimeOut = 2000  # ms
    $Socket.ReceiveTimeOut = 2000   # ms

    Try {
        $Socket.Connect($Server,123)
    }
    Catch {
        Write-Error -Message "Failed to connect to server $Server"
        Throw 
    }


# NTP Transaction -------------------------------------------------------

        $t1 = Get-Date    # t1, = Start time of transaction...
    
        Try {
            [Void]$Socket.Send($NtpData)      # Send request header
            [Void]$Socket.Receive($NtpData)   # Receive 48-byte NTP response
        }
        Catch {
            Write-Error -Message "Failed to communicate with server $Server"
            Throw
        }

        $t4 = Get-Date    # t4, = End of NTP transaction time

# End of NTP Transaction ------------------------------------------------

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

# We now have an NTP response packet in $NtpData to decode. Start with the LI flag
# as this is used to indicate errors as well as leap-second information

    # Check the Leap Indicator (LI) flag for an alarm condition - extract the flag
    # from the first byte in the packet by masking and shifting

    $LI = ($NtpData[0] -band 0xC0) -shr 6    # Leap Second indicator
    If ($LI -eq 3) {
        Throw 'Alarm condition from server (clock not synchronized)'
    } 

    # Decode the 64-bit NTP times

    # 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 the (64-bit) t3 NTP time from the byte array
    $IntPart = [BitConverter]::ToUInt32($NtpData[43..40],0)
    $FracPart = [BitConverter]::ToUInt32($NtpData[47..44],0)

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

    # Perform the same calculations for t2 (in bytes [32..39])
    $IntPart = [BitConverter]::ToUInt32($NtpData[35..32],0)
    $FracPart = [BitConverter]::ToUInt32($NtpData[39..36],0)
    $t2ms = $IntPart * 1000 + ($FracPart * 1000 / 0x100000000)

    # Calculate values for t1 and t4 as milliseconds since 1/1/1900 (NTP format)
    $t1ms = ([TimeZoneInfo]::ConvertTimeToUtc($t1) - $StartOfEpoch).TotalMilliseconds
    $t4ms = ([TimeZoneInfo]::ConvertTimeToUtc($t4) - $StartOfEpoch).TotalMilliseconds
 
    # Calculate the NTP Offset and Delay values
    $Offset = (($t2ms - $t1ms) + ($t3ms-$t4ms))/2
    $Delay = ($t4ms - $t1ms) - ($t3ms - $t2ms)

    # Make sure the result looks sane...
    If ([Math]::Abs($Offset) -gt $MaxOffset) {
        # Network server time is too different from local time
        Throw "Network time offset exceeds maximum ($($MaxOffset)ms)"
    }

    # Decode other useful parts of the received NTP time packet

    # We already have the Leap Indicator (LI) flag. Now extract the remaining data
    # flags (NTP Version, Server Mode) from the first byte by masking and shifting (dividing)

    $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) -shr 3    # Server version 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'}
    }

    # Other NTP information (Stratum, PollInterval, Precision)

    $Stratum = $NtpData[1]   # [UInt8] (=[Byte])
    $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 as it indicates a precision of less than 1 second)
        [Int]$Precision = $PrecisionBits   # top bit clear - just use positive value
    }
    $PrecisionSeconds = [Math]::Pow(2, $Precision)
    

<# Reference Identifier, notes:

   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. 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
      DCF Mainflingen (Germany) Radio 77.5 kHz
      MSF Rugby (UK) Radio 60 kHz
      GPS Global Positioning Service
   
   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.

#>


    # Determine the format of the ReferenceIdentifier field and decode
    
    If ($Stratum -le 1) {
        # Response from Primary Server. RefId is ASCII string describing source
        $ReferenceIdentifier = [String]([Char[]]$NtpData[12..15] -join '')
    }
    Else {

        # Response from Secondary Server; determine server version and decode

        Switch ($VN) {
            3       {
                        # Version 3 Secondary Server, RefId = IPv4 address of reference source
                        $ReferenceIdentifier = $NtpData[12..15] -join '.'

                        If (-Not $NoDns) {
                            If ($DnsLookup =  Resolve-DnsName $ReferenceIdentifier -QuickTimeout -ErrorAction SilentlyContinue) {
                                $ReferenceIdentifier = "$ReferenceIdentifier <$($DnsLookup.NameHost)>"
                            }
                        }
                        Break
                    }

            4       {
                        # Version 4 Secondary Server, RefId = low-order 32-bits of latest transmit time of reference source
                        $ReferenceIdentifier = [BitConverter]::ToUInt32($NtpData[15..12],0) * 1000 / 0x100000000
                        Break
                    }

            Default {
                        # Unhandled NTP version...
                        $ReferenceIdentifier = $Null
                    }
        }
    }


    # Calculate Root Delay and Root Dispersion values
    
    $RootDelay = [BitConverter]::ToInt32($NtpData[7..4],0) / 0x10000
    $RootDispersion = [BitConverter]::ToUInt32($NtpData[11..8],0) / 0x10000


    # Finally, create the NtpTime custom output object and pass it to the output
    
    [PSCustomObject]@{
        
        PsTypeName = 'NtpTime'

        NtpServer           = $Server
        NtpTime             = Convert-OffsetToLocal($t4ms + $Offset)
        Offset              = $Offset
        OffsetSeconds       = [Math]::Round($Offset/1000, 3)
        Delay               = $Delay
        ReferenceIdentifier = $ReferenceIdentifier

        LI      = $LI
        LI_text = $LI_text

        NtpVersionNumber = $VN
        Mode             = $Mode
        Mode_text        = $Mode_text
        Stratum          = $Stratum
        Stratum_text     = $Stratum_text

        t1ms = $t1ms
        t2ms = $t2ms
        t3ms = $t3ms
        t4ms = $t4ms
        t1   = Convert-OffsetToLocal($t1ms)
        t2   = Convert-OffsetToLocal($t2ms)
        t3   = Convert-OffsetToLocal($t3ms)
        t4   = Convert-OffsetToLocal($t4ms)
        
        PollIntervalRaw     = $PollInterval
        PollInterval        = New-Object -TypeName TimeSpan -ArgumentList (0,0,$PollIntervalSeconds)
        Precision           = $Precision
        PrecisionSeconds    = $PrecisionSeconds
        RootDelay           = $RootDelay
        RootDispersion      = $RootDispersion

        Raw = $NtpData   # The undecoded bytes returned from the NTP server
    }
}



<#

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

48-byte NTP time packet format

                                 1 2 3
   BitOffset 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
Bytes +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    0-3 |LI | VN |Mode | Stratum | Poll | Precision |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    4-7 | Root Delay |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    8-11 | Root Dispersion |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    12-15 | Reference Identifier |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    16-23 | |
            | Reference Timestamp (64) |
            | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    24-31 | |
            | Originate Timestamp (64) |
            | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    32-39 | |
            | Receive Timestamp (64) |
            | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    40-47 | |
            | 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.

#>