pax_global_header00006660000000000000000000000064136347330150014517gustar00rootroot0000000000000052 comment=6f90c4d07a35b494c2d2c1a496b5efa95ffa3fc4 ntp-0.3.0/000077500000000000000000000000001363473301500123205ustar00rootroot00000000000000ntp-0.3.0/.travis.yml000066400000000000000000000002021363473301500144230ustar00rootroot00000000000000language: go sudo: false go: - 1.9.x - 1.12.x - tip matrix: allow_failures: - go: tip script: - go test -v ./... ntp-0.3.0/CONTRIBUTORS000066400000000000000000000002511363473301500141760ustar00rootroot00000000000000Brett Vickers (beevik) Mikhail Salosin (AlphaB) Anton Tolchanov (knyar) Christopher Batey (chbatey) Meng Zhuo (mengzhuo) Leonid Evdokimov (darkk) Ask Bjørn Hansen (abh)ntp-0.3.0/LICENSE000066400000000000000000000024151363473301500133270ustar00rootroot00000000000000Copyright 2015-2017 Brett Vickers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ntp-0.3.0/README.md000066400000000000000000000072501363473301500136030ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/beevik/ntp.svg?branch=master)](https://travis-ci.org/beevik/ntp) [![GoDoc](https://godoc.org/github.com/beevik/ntp?status.svg)](https://godoc.org/github.com/beevik/ntp) ntp === The ntp package is an implementation of a Simple NTP (SNTP) client based on [RFC5905](https://tools.ietf.org/html/rfc5905). It allows you to connect to a remote NTP server and request information about the current time. ## Querying the current time If all you care about is the current time according to a remote NTP server, simply use the `Time` function: ```go time, err := ntp.Time("0.beevik-ntp.pool.ntp.org") ``` ## Querying time metadata To obtain the current time as well as some additional metadata about the time, use the [`Query`](https://godoc.org/github.com/beevik/ntp#Query) function: ```go response, err := ntp.Query("0.beevik-ntp.pool.ntp.org") time := time.Now().Add(response.ClockOffset) ``` Alternatively, use the [`QueryWithOptions`](https://godoc.org/github.com/beevik/ntp#QueryWithOptions) function if you want to change the default behavior used by the `Query` function: ```go options := ntp.QueryOptions{ Timeout: 30*time.Second, TTL: 5 } response, err := ntp.QueryWithOptions("0.beevik-ntp.pool.ntp.org", options) time := time.Now().Add(response.ClockOffset) ``` The [`Response`](https://godoc.org/github.com/beevik/ntp#Response) structure returned by `Query` includes the following information: * `Time`: The time the server transmitted its response, according to its own clock. * `ClockOffset`: The estimated offset of the local system clock relative to the server's clock. For a more accurate time reading, you may add this offset to any subsequent system clock reading. * `RTT`: An estimate of the round-trip-time delay between the client and the server. * `Precision`: The precision of the server's clock reading. * `Stratum`: The server's stratum, which indicates the number of hops from the server to the reference clock. A stratum 1 server is directly attached to the reference clock. If the stratum is zero, the server has responded with the "kiss of death". * `ReferenceID`: A unique identifier for the consulted reference clock. * `ReferenceTime`: The time at which the server last updated its local clock setting. * `RootDelay`: The server's aggregate round-trip-time delay to the stratum 1 server. * `RootDispersion`: The server's estimated maximum measurement error relative to the reference clock. * `RootDistance`: An estimate of the root synchronization distance between the client and the stratum 1 server. * `Leap`: The leap second indicator, indicating whether a second should be added to or removed from the current month's last minute. * `MinError`: A lower bound on the clock error between the client and the server. * `KissCode`: A 4-character string describing the reason for a "kiss of death" response (stratum=0). * `Poll`: The maximum polling interval between successive messages to the server. The `Response` structure's [`Validate`](https://godoc.org/github.com/beevik/ntp#Response.Validate) method performs additional sanity checks to determine whether the response is suitable for time synchronization purposes. ```go err := response.Validate() if err == nil { // response data is suitable for synchronization purposes } ``` ## Using the NTP pool The NTP pool is a shared resource used by people all over the world. To prevent it from becoming overloaded, please avoid querying the standard `pool.ntp.org` zone names in your applications. Instead, consider requesting your own [vendor zone](http://www.pool.ntp.org/en/vendors.html) or [joining the pool](http://www.pool.ntp.org/join.html). ntp-0.3.0/RELEASE_NOTES.md000066400000000000000000000051031363473301500146710ustar00rootroot00000000000000Release v0.3.0 ============== There have been no breaking changes or further deprecations since the previous release. **Changes** * Fixed a bug in the calculation of NTP timestamps. Release v0.2.0 ============== There are no breaking changes or further deprecations in this release. **Changes** * Added `KissCode` to the `Response` structure. Release v0.1.1 ============== **Breaking changes** * Removed the `MaxStratum` constant. **Deprecations** * Officially deprecated the `TimeV` function. **Internal changes** * Removed `minDispersion` from the `RootDistance` calculation, since the value was arbitrary. * Moved some validation into main code path so that invalid `TransmitTime` and `mode` responses trigger an error even when `Response.Validate` is not called. Release v0.1.0 ============== This is the initial release of the `ntp` package. Currently it supports the following features: * `Time()` to query the current time according to a remote NTP server. * `Query()` to query multiple pieces of time-related information from a remote NTP server. * `QueryWithOptions()`, which is like `Query()` but with the ability to override default query options. Time-related information returned by the `Query` functions includes: * `Time`: the time the server transmitted its response, according to the server's clock. * `ClockOffset`: the estimated offset of the client's clock relative to the server's clock. You may apply this offset to any local system clock reading once the query is complete. * `RTT`: an estimate of the round-trip-time delay between the client and the server. * `Precision`: the precision of the server's clock reading. * `Stratum`: the "stratum" level of the server, where 1 indicates a server directly connected to a reference clock, and values greater than 1 indicating the number of hops from the reference clock. * `ReferenceID`: A unique identifier for the NTP server that was contacted. * `ReferenceTime`: The time at which the server last updated its local clock setting. * `RootDelay`: The server's round-trip delay to the reference clock. * `RootDispersion`: The server's total dispersion to the referenced clock. * `RootDistance`: An estimate of the root synchronization distance. * `Leap`: The leap second indicator. * `MinError`: A lower bound on the clock error between the client and the server. * `Poll`: the maximum polling interval between successive messages on the server. The `Response` structure returned by the `Query` functions also contains a `Response.Validate()` function that returns an error if any of the fields returned by the server are invalid. ntp-0.3.0/ntp.go000066400000000000000000000415371363473301500134620ustar00rootroot00000000000000// Copyright 2015-2017 Brett Vickers. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package ntp provides an implementation of a Simple NTP (SNTP) client // capable of querying the current time from a remote NTP server. See // RFC5905 (https://tools.ietf.org/html/rfc5905) for more details. // // This approach grew out of a go-nuts post by Michael Hofmann: // https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/FlcdMU5fkLQ package ntp import ( "crypto/rand" "encoding/binary" "errors" "fmt" "net" "time" "golang.org/x/net/ipv4" ) // The LeapIndicator is used to warn if a leap second should be inserted // or deleted in the last minute of the current month. type LeapIndicator uint8 const ( // LeapNoWarning indicates no impending leap second. LeapNoWarning LeapIndicator = 0 // LeapAddSecond indicates the last minute of the day has 61 seconds. LeapAddSecond = 1 // LeapDelSecond indicates the last minute of the day has 59 seconds. LeapDelSecond = 2 // LeapNotInSync indicates an unsynchronized leap second. LeapNotInSync = 3 ) // Internal constants const ( defaultNtpVersion = 4 nanoPerSec = 1000000000 maxStratum = 16 defaultTimeout = 5 * time.Second maxPollInterval = (1 << 17) * time.Second maxDispersion = 16 * time.Second ) // Internal variables var ( ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC) ) type mode uint8 // NTP modes. This package uses only client mode. const ( reserved mode = 0 + iota symmetricActive symmetricPassive client server broadcast controlMessage reservedPrivate ) // An ntpTime is a 64-bit fixed-point (Q32.32) representation of the number of // seconds elapsed. type ntpTime uint64 // Duration interprets the fixed-point ntpTime as a number of elapsed seconds // and returns the corresponding time.Duration value. func (t ntpTime) Duration() time.Duration { sec := (t >> 32) * nanoPerSec frac := (t & 0xffffffff) * nanoPerSec nsec := frac >> 32 if uint32(frac) >= 0x80000000 { nsec++ } return time.Duration(sec + nsec) } // Time interprets the fixed-point ntpTime as an absolute time and returns // the corresponding time.Time value. func (t ntpTime) Time() time.Time { return ntpEpoch.Add(t.Duration()) } // toNtpTime converts the time.Time value t into its 64-bit fixed-point // ntpTime representation. func toNtpTime(t time.Time) ntpTime { nsec := uint64(t.Sub(ntpEpoch)) sec := nsec / nanoPerSec nsec = uint64(nsec-sec*nanoPerSec) << 32 frac := uint64(nsec / nanoPerSec) if nsec%nanoPerSec >= nanoPerSec/2 { frac++ } return ntpTime(sec<<32 | frac) } // An ntpTimeShort is a 32-bit fixed-point (Q16.16) representation of the // number of seconds elapsed. type ntpTimeShort uint32 // Duration interprets the fixed-point ntpTimeShort as a number of elapsed // seconds and returns the corresponding time.Duration value. func (t ntpTimeShort) Duration() time.Duration { sec := uint64(t>>16) * nanoPerSec frac := uint64(t&0xffff) * nanoPerSec nsec := frac >> 16 if uint16(frac) >= 0x8000 { nsec++ } return time.Duration(sec + nsec) } // msg is an internal representation of an NTP packet. type msg struct { LiVnMode uint8 // Leap Indicator (2) + Version (3) + Mode (3) Stratum uint8 Poll int8 Precision int8 RootDelay ntpTimeShort RootDispersion ntpTimeShort ReferenceID uint32 ReferenceTime ntpTime OriginTime ntpTime ReceiveTime ntpTime TransmitTime ntpTime } // setVersion sets the NTP protocol version on the message. func (m *msg) setVersion(v int) { m.LiVnMode = (m.LiVnMode & 0xc7) | uint8(v)<<3 } // setMode sets the NTP protocol mode on the message. func (m *msg) setMode(md mode) { m.LiVnMode = (m.LiVnMode & 0xf8) | uint8(md) } // setLeap modifies the leap indicator on the message. func (m *msg) setLeap(li LeapIndicator) { m.LiVnMode = (m.LiVnMode & 0x3f) | uint8(li)<<6 } // getVersion returns the version value in the message. func (m *msg) getVersion() int { return int((m.LiVnMode >> 3) & 0x07) } // getMode returns the mode value in the message. func (m *msg) getMode() mode { return mode(m.LiVnMode & 0x07) } // getLeap returns the leap indicator on the message. func (m *msg) getLeap() LeapIndicator { return LeapIndicator((m.LiVnMode >> 6) & 0x03) } // QueryOptions contains the list of configurable options that may be used // with the QueryWithOptions function. type QueryOptions struct { Timeout time.Duration // defaults to 5 seconds Version int // NTP protocol version, defaults to 4 LocalAddress string // IP address to use for the client address Port int // Server port, defaults to 123 TTL int // IP TTL to use, defaults to system default } // A Response contains time data, some of which is returned by the NTP server // and some of which is calculated by the client. type Response struct { // Time is the transmit time reported by the server just before it // responded to the client's NTP query. Time time.Time // ClockOffset is the estimated offset of the client clock relative to // the server. Add this to the client's system clock time to obtain a // more accurate time. ClockOffset time.Duration // RTT is the measured round-trip-time delay estimate between the client // and the server. RTT time.Duration // Precision is the reported precision of the server's clock. Precision time.Duration // Stratum is the "stratum level" of the server. The smaller the number, // the closer the server is to the reference clock. Stratum 1 servers are // attached directly to the reference clock. A stratum value of 0 // indicates the "kiss of death," which typically occurs when the client // issues too many requests to the server in a short period of time. Stratum uint8 // ReferenceID is a 32-bit identifier identifying the server or // reference clock. ReferenceID uint32 // ReferenceTime is the time when the server's system clock was last // set or corrected. ReferenceTime time.Time // RootDelay is the server's estimated aggregate round-trip-time delay to // the stratum 1 server. RootDelay time.Duration // RootDispersion is the server's estimated maximum measurement error // relative to the stratum 1 server. RootDispersion time.Duration // RootDistance is an estimate of the total synchronization distance // between the client and the stratum 1 server. RootDistance time.Duration // Leap indicates whether a leap second should be added or removed from // the current month's last minute. Leap LeapIndicator // MinError is a lower bound on the error between the client and server // clocks. When the client and server are not synchronized to the same // clock, the reported timestamps may appear to violate the principle of // causality. In other words, the NTP server's response may indicate // that a message was received before it was sent. In such cases, the // minimum error may be useful. MinError time.Duration // KissCode is a 4-character string describing the reason for a // "kiss of death" response (stratum = 0). For a list of standard kiss // codes, see https://tools.ietf.org/html/rfc5905#section-7.4. KissCode string // Poll is the maximum interval between successive NTP polling messages. // It is not relevant for simple NTP clients like this one. Poll time.Duration } // Validate checks if the response is valid for the purposes of time // synchronization. func (r *Response) Validate() error { // Handle invalid stratum values. if r.Stratum == 0 { return fmt.Errorf("kiss of death received: %s", r.KissCode) } if r.Stratum >= maxStratum { return errors.New("invalid stratum in response") } // Handle invalid leap second indicator. if r.Leap == LeapNotInSync { return errors.New("invalid leap second") } // Estimate the "freshness" of the time. If it exceeds the maximum // polling interval (~36 hours), then it cannot be considered "fresh". freshness := r.Time.Sub(r.ReferenceTime) if freshness > maxPollInterval { return errors.New("server clock not fresh") } // Calculate the peer synchronization distance, lambda: // lambda := RootDelay/2 + RootDispersion // If this value exceeds MAXDISP (16s), then the time is not suitable // for synchronization purposes. // https://tools.ietf.org/html/rfc5905#appendix-A.5.1.1. lambda := r.RootDelay/2 + r.RootDispersion if lambda > maxDispersion { return errors.New("invalid dispersion") } // If the server's transmit time is before its reference time, the // response is invalid. if r.Time.Before(r.ReferenceTime) { return errors.New("invalid time reported") } // nil means the response is valid. return nil } // Query returns a response from the remote NTP server host. It contains // the time at which the server transmitted the response as well as other // useful information about the time and the remote server. func Query(host string) (*Response, error) { return QueryWithOptions(host, QueryOptions{}) } // QueryWithOptions performs the same function as Query but allows for the // customization of several query options. func QueryWithOptions(host string, opt QueryOptions) (*Response, error) { m, now, err := getTime(host, opt) if err != nil { return nil, err } return parseTime(m, now), nil } // TimeV returns the current time using information from a remote NTP server. // On error, it returns the local system time. The version may be 2, 3, or 4. // // Deprecated: TimeV is deprecated. Use QueryWithOptions instead. func TimeV(host string, version int) (time.Time, error) { m, recvTime, err := getTime(host, QueryOptions{Version: version}) if err != nil { return time.Now(), err } r := parseTime(m, recvTime) err = r.Validate() if err != nil { return time.Now(), err } // Use the clock offset to calculate the time. return time.Now().Add(r.ClockOffset), nil } // Time returns the current time using information from a remote NTP server. // It uses version 4 of the NTP protocol. On error, it returns the local // system time. func Time(host string) (time.Time, error) { return TimeV(host, defaultNtpVersion) } // getTime performs the NTP server query and returns the response message // along with the local system time it was received. func getTime(host string, opt QueryOptions) (*msg, ntpTime, error) { if opt.Version == 0 { opt.Version = defaultNtpVersion } if opt.Version < 2 || opt.Version > 4 { return nil, 0, errors.New("invalid protocol version requested") } // Resolve the remote NTP server address. raddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "123")) if err != nil { return nil, 0, err } // Resolve the local address if specified as an option. var laddr *net.UDPAddr if opt.LocalAddress != "" { laddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(opt.LocalAddress, "0")) if err != nil { return nil, 0, err } } // Override the port if requested. if opt.Port != 0 { raddr.Port = opt.Port } // Prepare a "connection" to the remote server. con, err := net.DialUDP("udp", laddr, raddr) if err != nil { return nil, 0, err } defer con.Close() // Set a TTL for the packet if requested. if opt.TTL != 0 { ipcon := ipv4.NewConn(con) err = ipcon.SetTTL(opt.TTL) if err != nil { return nil, 0, err } } // Set a timeout on the connection. if opt.Timeout == 0 { opt.Timeout = defaultTimeout } con.SetDeadline(time.Now().Add(opt.Timeout)) // Allocate a message to hold the response. recvMsg := new(msg) // Allocate a message to hold the query. xmitMsg := new(msg) xmitMsg.setMode(client) xmitMsg.setVersion(opt.Version) xmitMsg.setLeap(LeapNotInSync) // To ensure privacy and prevent spoofing, try to use a random 64-bit // value for the TransmitTime. If crypto/rand couldn't generate a // random value, fall back to using the system clock. Keep track of // when the messsage was actually transmitted. bits := make([]byte, 8) _, err = rand.Read(bits) var xmitTime time.Time if err == nil { xmitMsg.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits)) xmitTime = time.Now() } else { xmitTime = time.Now() xmitMsg.TransmitTime = toNtpTime(xmitTime) } // Transmit the query. err = binary.Write(con, binary.BigEndian, xmitMsg) if err != nil { return nil, 0, err } // Receive the response. err = binary.Read(con, binary.BigEndian, recvMsg) if err != nil { return nil, 0, err } // Keep track of the time the response was received. delta := time.Since(xmitTime) if delta < 0 { // The local system may have had its clock adjusted since it // sent the query. In go 1.9 and later, time.Since ensures // that a monotonic clock is used, so delta can never be less // than zero. In versions before 1.9, a monotonic clock is // not used, so we have to check. return nil, 0, errors.New("client clock ticked backwards") } recvTime := toNtpTime(xmitTime.Add(delta)) // Check for invalid fields. if recvMsg.getMode() != server { return nil, 0, errors.New("invalid mode in response") } if recvMsg.TransmitTime == ntpTime(0) { return nil, 0, errors.New("invalid transmit time in response") } if recvMsg.OriginTime != xmitMsg.TransmitTime { return nil, 0, errors.New("server response mismatch") } if recvMsg.ReceiveTime > recvMsg.TransmitTime { return nil, 0, errors.New("server clock ticked backwards") } // Correct the received message's origin time using the actual // transmit time. recvMsg.OriginTime = toNtpTime(xmitTime) return recvMsg, recvTime, nil } // parseTime parses the NTP packet along with the packet receive time to // generate a Response record. func parseTime(m *msg, recvTime ntpTime) *Response { r := &Response{ Time: m.TransmitTime.Time(), ClockOffset: offset(m.OriginTime, m.ReceiveTime, m.TransmitTime, recvTime), RTT: rtt(m.OriginTime, m.ReceiveTime, m.TransmitTime, recvTime), Precision: toInterval(m.Precision), Stratum: m.Stratum, ReferenceID: m.ReferenceID, ReferenceTime: m.ReferenceTime.Time(), RootDelay: m.RootDelay.Duration(), RootDispersion: m.RootDispersion.Duration(), Leap: m.getLeap(), MinError: minError(m.OriginTime, m.ReceiveTime, m.TransmitTime, recvTime), Poll: toInterval(m.Poll), } // Calculate values depending on other calculated values r.RootDistance = rootDistance(r.RTT, r.RootDelay, r.RootDispersion) // If a kiss of death was received, interpret the reference ID as // a kiss code. if r.Stratum == 0 { r.KissCode = kissCode(r.ReferenceID) } return r } // The following helper functions calculate additional metadata about the // timestamps received from an NTP server. The timestamps returned by // the server are given the following variable names: // // org = Origin Timestamp (client send time) // rec = Receive Timestamp (server receive time) // xmt = Transmit Timestamp (server reply time) // dst = Destination Timestamp (client receive time) func rtt(org, rec, xmt, dst ntpTime) time.Duration { // round trip delay time // rtt = (dst-org) - (xmt-rec) a := dst.Time().Sub(org.Time()) b := xmt.Time().Sub(rec.Time()) rtt := a - b if rtt < 0 { rtt = 0 } return rtt } func offset(org, rec, xmt, dst ntpTime) time.Duration { // local clock offset // offset = ((rec-org) + (xmt-dst)) / 2 a := rec.Time().Sub(org.Time()) b := xmt.Time().Sub(dst.Time()) return (a + b) / time.Duration(2) } func minError(org, rec, xmt, dst ntpTime) time.Duration { // Each NTP response contains two pairs of send/receive timestamps. // When either pair indicates a "causality violation", we calculate the // error as the difference in time between them. The minimum error is // the greater of the two causality violations. var error0, error1 ntpTime if org >= rec { error0 = org - rec } if xmt >= dst { error1 = xmt - dst } if error0 > error1 { return error0.Duration() } return error1.Duration() } func rootDistance(rtt, rootDelay, rootDisp time.Duration) time.Duration { // The root distance is: // the maximum error due to all causes of the local clock // relative to the primary server. It is defined as half the // total delay plus total dispersion plus peer jitter. // (https://tools.ietf.org/html/rfc5905#appendix-A.5.5.2) // // In the reference implementation, it is calculated as follows: // rootDist = max(MINDISP, rootDelay + rtt)/2 + rootDisp // + peerDisp + PHI * (uptime - peerUptime) // + peerJitter // For an SNTP client which sends only a single packet, most of these // terms are irrelevant and become 0. totalDelay := rtt + rootDelay return totalDelay/2 + rootDisp } func toInterval(t int8) time.Duration { switch { case t > 0: return time.Duration(uint64(time.Second) << uint(t)) case t < 0: return time.Duration(uint64(time.Second) >> uint(-t)) default: return time.Second } } func kissCode(id uint32) string { isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 } b := []byte{ byte(id >> 24), byte(id >> 16), byte(id >> 8), byte(id), } for _, ch := range b { if !isPrintable(ch) { return "" } } return string(b) } ntp-0.3.0/ntp_test.go000066400000000000000000000206361363473301500145160ustar00rootroot00000000000000// Copyright 2015-2017 Brett Vickers. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package ntp import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" ) const ( host = "0.beevik-ntp.pool.ntp.org" refID = 0x58585858 // 'XXXX' ) func isNil(t *testing.T, err error) bool { switch { case err == nil: return true case strings.Contains(err.Error(), "timeout"): // log instead of error, so test isn't failed t.Logf("[%s] Query timeout: %s", host, err) return false case strings.Contains(err.Error(), "kiss of death"): // log instead of error, so test isn't failed t.Logf("[%s] Query kiss of death: %s", host, err) return false default: // error, so test fails t.Errorf("[%s] Query failed: %s", host, err) return false } } func assertValid(t *testing.T, r *Response) { err := r.Validate() _ = isNil(t, err) } func assertInvalid(t *testing.T, r *Response) { err := r.Validate() if err == nil { t.Errorf("[%s] Response unexpectedly valid\n", host) } } func TestTime(t *testing.T) { tm, err := Time(host) now := time.Now() if isNil(t, err) { t.Logf("Local Time %v\n", now) t.Logf("~True Time %v\n", tm) t.Logf("Offset %v\n", tm.Sub(now)) } } func TestTimeFailure(t *testing.T) { // Use a link-local IP address that won't have an NTP server listening // on it. This should return the local system's time. local, err := Time("169.254.122.229") assert.NotNil(t, err) now := time.Now() // When the NTP time query fails, it should return the system time. // Compare the "now" system time with the returned time. It should be // about the same. diffMinutes := now.Sub(local).Minutes() assert.True(t, diffMinutes > -1 && diffMinutes < 1) } func TestQuery(t *testing.T) { t.Logf("[%s] ----------------------", host) t.Logf("[%s] NTP protocol version %d", host, 4) r, err := QueryWithOptions(host, QueryOptions{Version: 4}) if !isNil(t, err) { return } t.Logf("[%s] LocalTime: %v", host, time.Now()) t.Logf("[%s] XmitTime: %v", host, r.Time) t.Logf("[%s] RefTime: %v", host, r.ReferenceTime) t.Logf("[%s] RTT: %v", host, r.RTT) t.Logf("[%s] Offset: %v", host, r.ClockOffset) t.Logf("[%s] Poll: %v", host, r.Poll) t.Logf("[%s] Precision: %v", host, r.Precision) t.Logf("[%s] Stratum: %v", host, r.Stratum) t.Logf("[%s] RefID: 0x%08x", host, r.ReferenceID) t.Logf("[%s] RootDelay: %v", host, r.RootDelay) t.Logf("[%s] RootDisp: %v", host, r.RootDispersion) t.Logf("[%s] RootDist: %v", host, r.RootDistance) t.Logf("[%s] MinError: %v", host, r.MinError) t.Logf("[%s] Leap: %v", host, r.Leap) t.Logf("[%s] KissCode: %v", host, stringOrEmpty(r.KissCode)) assertValid(t, r) } func stringOrEmpty(s string) string { if s == "" { return "" } return s } func TestValidate(t *testing.T) { var m msg var r *Response m.Stratum = 1 m.ReferenceID = refID m.ReferenceTime = 1 << 32 m.Precision = -1 // 500ms // Zero RTT m.OriginTime = 1 << 32 m.ReceiveTime = 1 << 32 m.TransmitTime = 1 << 32 r = parseTime(&m, 1<<32) assertValid(t, r) // Negative freshness m.ReferenceTime = 2 << 32 r = parseTime(&m, 1<<32) assertInvalid(t, r) // Unfresh clock (48h) m.OriginTime = 2 * 86400 << 32 m.ReceiveTime = 2 * 86400 << 32 m.TransmitTime = 2 * 86400 << 32 r = parseTime(&m, 2*86400<<32) assertInvalid(t, r) // Fresh clock (24h) m.ReferenceTime = 1 * 86400 << 32 r = parseTime(&m, 2*86400<<32) assertValid(t, r) // Values indicating a negative RTT m.RootDelay = 16 << 16 m.ReferenceTime = 1 << 32 m.OriginTime = 20 << 32 m.ReceiveTime = 10 << 32 m.TransmitTime = 15 << 32 r = parseTime(&m, 22<<32) assert.NotNil(t, r) assertValid(t, r) assert.Equal(t, r.RTT, 0*time.Second) assert.Equal(t, r.RootDistance, 8*time.Second) } func TestBadServerPort(t *testing.T) { // Not NTP port. tm, _, err := getTime(host, QueryOptions{Port: 9}) assert.Nil(t, tm) assert.NotNil(t, err) } func TestTTL(t *testing.T) { // TTL of 1 should cause a timeout. tm, _, err := getTime(host, QueryOptions{TTL: 1}) assert.Nil(t, tm) assert.NotNil(t, err) } func TestQueryTimeout(t *testing.T) { // Force an immediate timeout. tm, err := QueryWithOptions(host, QueryOptions{Version: 4, Timeout: time.Nanosecond}) assert.Nil(t, tm) assert.NotNil(t, err) } func TestShortConversion(t *testing.T) { var ts ntpTimeShort ts = 0x00000000 assert.Equal(t, 0*time.Nanosecond, ts.Duration()) ts = 0x00000001 assert.Equal(t, 15259*time.Nanosecond, ts.Duration()) // well, it's actually 15258.789, but it's good enough ts = 0x00008000 assert.Equal(t, 500*time.Millisecond, ts.Duration()) // precise ts = 0x0000c000 assert.Equal(t, 750*time.Millisecond, ts.Duration()) // precise ts = 0x0000ff80 assert.Equal(t, time.Second-(1000000000/512)*time.Nanosecond, ts.Duration()) // last precise sub-second value ts = 0x00010000 assert.Equal(t, 1000*time.Millisecond, ts.Duration()) // precise ts = 0x00018000 assert.Equal(t, 1500*time.Millisecond, ts.Duration()) // precise ts = 0xffff0000 assert.Equal(t, 65535*time.Second, ts.Duration()) // precise ts = 0xffffff80 assert.Equal(t, 65536*time.Second-(1000000000/512)*time.Nanosecond, ts.Duration()) // last precise value } func TestLongConversion(t *testing.T) { ts := []ntpTime{0x0, 0xff800000, 0x1ff800000, 0x80000000ff800000, 0xffffffffff800000} for _, v := range ts { assert.Equal(t, v, toNtpTime(v.Time())) } } func abs(d time.Duration) time.Duration { switch { case int64(d) < 0: return -d default: return d } } func TestOffsetCalculation(t *testing.T) { now := time.Now() t1 := toNtpTime(now) t2 := toNtpTime(now.Add(20 * time.Second)) t3 := toNtpTime(now.Add(21 * time.Second)) t4 := toNtpTime(now.Add(5 * time.Second)) // expectedOffset := ((T2 - T1) + (T3 - T4)) / 2 // ((119 - 99) + (121 - 104)) / 2 // (20 + 17) / 2 // 37 / 2 = 18 expectedOffset := 18 * time.Second offset := offset(t1, t2, t3, t4) assert.Equal(t, expectedOffset, offset) } func TestOffsetCalculationNegative(t *testing.T) { now := time.Now() t1 := toNtpTime(now.Add(101 * time.Second)) t2 := toNtpTime(now.Add(102 * time.Second)) t3 := toNtpTime(now.Add(103 * time.Second)) t4 := toNtpTime(now.Add(105 * time.Second)) // expectedOffset := ((T2 - T1) + (T3 - T4)) / 2 // ((102 - 101) + (103 - 105)) / 2 // (1 + -2) / 2 = -1 / 2 expectedOffset := -time.Second / 2 offset := offset(t1, t2, t3, t4) assert.Equal(t, expectedOffset, offset) } func TestMinError(t *testing.T) { start := time.Now() m := &msg{ Stratum: 1, ReferenceID: refID, ReferenceTime: toNtpTime(start), OriginTime: toNtpTime(start.Add(1 * time.Second)), ReceiveTime: toNtpTime(start.Add(2 * time.Second)), TransmitTime: toNtpTime(start.Add(3 * time.Second)), } r := parseTime(m, toNtpTime(start.Add(4*time.Second))) assertValid(t, r) assert.Equal(t, r.MinError, time.Duration(0)) for org := 1 * time.Second; org <= 10*time.Second; org += time.Second { for rec := 1 * time.Second; rec <= 10*time.Second; rec += time.Second { for xmt := rec; xmt <= 10*time.Second; xmt += time.Second { for dst := org; dst <= 10*time.Second; dst += time.Second { m.OriginTime = toNtpTime(start.Add(org)) m.ReceiveTime = toNtpTime(start.Add(rec)) m.TransmitTime = toNtpTime(start.Add(xmt)) r = parseTime(m, toNtpTime(start.Add(dst))) assertValid(t, r) var error0, error1 time.Duration if org >= rec { error0 = org - rec } if xmt >= dst { error1 = xmt - dst } var minError time.Duration if error0 > error1 { minError = error0 } else { minError = error1 } assert.Equal(t, r.MinError, minError) } } } } } func TestTimeConversions(t *testing.T) { nowNtp := toNtpTime(time.Now()) now := nowNtp.Time() startNow := now for i := 0; i < 100; i++ { nowNtp = toNtpTime(now) now = nowNtp.Time() } assert.Equal(t, now, startNow) } func TestKissCode(t *testing.T) { codes := []struct { id uint32 str string }{ {0x41435354, "ACST"}, {0x41555448, "AUTH"}, {0x4155544f, "AUTO"}, {0x42435354, "BCST"}, {0x43525950, "CRYP"}, {0x44454e59, "DENY"}, {0x44524f50, "DROP"}, {0x52535452, "RSTR"}, {0x494e4954, "INIT"}, {0x4d435354, "MCST"}, {0x4e4b4559, "NKEY"}, {0x52415445, "RATE"}, {0x524d4f54, "RMOT"}, {0x53544550, "STEP"}, {0x01010101, ""}, {0xfefefefe, ""}, {0x01544450, ""}, {0x41544401, ""}, } for _, c := range codes { assert.Equal(t, kissCode(c.id), c.str) } }