pax_global_header00006660000000000000000000000064150665702220014517gustar00rootroot0000000000000052 comment=bf2a50c2f9035259d3e3b2b27eb5d780d35805c6 beevik-ntp-bf2a50c/000077500000000000000000000000001506657022200142475ustar00rootroot00000000000000beevik-ntp-bf2a50c/.github/000077500000000000000000000000001506657022200156075ustar00rootroot00000000000000beevik-ntp-bf2a50c/.github/workflows/000077500000000000000000000000001506657022200176445ustar00rootroot00000000000000beevik-ntp-bf2a50c/.github/workflows/go.yml000066400000000000000000000027041506657022200207770ustar00rootroot00000000000000name: Go on: [push, pull_request] permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["go"] steps: - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Initialize CodeQL uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: category: "/language:${{matrix.language}}" build: runs-on: ubuntu-latest strategy: matrix: go-version: [ '1.24', '1.25.x' ] steps: - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} - name: Build run: go build -v ./... - name: Test run: go test -run 'TestOffline.*' -v ./... beevik-ntp-bf2a50c/CONTRIBUTORS000066400000000000000000000004251506657022200161300ustar00rootroot00000000000000Brett Vickers (beevik) Mikhail Salosin (AlphaB) Anton Tolchanov (knyar) Christopher Batey (chbatey) Meng Zhuo (mengzhuo) Leonid Evdokimov (darkk) Ask Bjørn Hansen (abh) Al Cutter (AlCutter) Silves-Xiang (silves-xiang) Andrey Smirnov (smira) Christian Cedercrantz (chrisceder) beevik-ntp-bf2a50c/LICENSE000066400000000000000000000024201506657022200152520ustar00rootroot00000000000000Copyright © 2015-2023 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. beevik-ntp-bf2a50c/README.md000066400000000000000000000123321506657022200155270ustar00rootroot00000000000000[![GoDoc](https://godoc.org/github.com/beevik/ntp?status.svg)](https://godoc.org/github.com/beevik/ntp) [![Go](https://github.com/beevik/ntp/actions/workflows/go.yml/badge.svg)](https://github.com/beevik/ntp/actions/workflows/go.yml) ntp === The ntp package is an implementation of a Simple NTP (SNTP) client based on [RFC 5905](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 synchronization data To obtain the current time as well as some additional synchronization data, 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) ``` The [`Response`](https://godoc.org/github.com/beevik/ntp#Response) structure returned by `Query` includes the following information: * `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. * `Time`: The time the server transmitted its response, according to its own clock. * `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" and you should examine the `KissCode`. * `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) function 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 } ``` If you wish to customize the behavior of the NTP query, use the [`QueryWithOptions`](https://godoc.org/github.com/beevik/ntp#QueryWithOptions) 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) ``` Configurable [`QueryOptions`](https://godoc.org/github.com/beevik/ntp#QueryOptions) include: * `Timeout`: How long to wait before giving up on a response from the NTP server. * `Version`: Which version of the NTP protocol to use (2, 3 or 4). * `TTL`: The maximum number of IP hops before the request packet is discarded. * `LocalAddress`: The local IP address to use when sending the query. Useful when the host has multiple network interfaces. * `Auth`: The symmetric authentication key and algorithm used by the server to authenticate the query. The same information is used by the client to authenticate the server's response. * `Extensions`: Extensions may be added to modify NTP queries before they are transmitted and to process NTP responses after they arrive. * `GetSystemTime`: A custom function to obtain the current system time, used to override the default `time.Now` function. * `Dialer`: A custom network connection "dialer" function used to override the default UDP dialer function. ## Using the NTP pool The NTP pool is a shared resource provided by the [NTP Pool Project](https://www.pool.ntp.org/en/) and used by people and services 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). ## Network Time Security (NTS) Network Time Security (NTS) is a recent enhancement of NTP, designed to add better authentication and message integrity to the protocol. It is defined by [RFC 8915](https://tools.ietf.org/html/rfc8915). If you wish to use NTS, see the [nts package](https://github.com/beevik/nts). (The nts package is implemented as an extension to this package.) beevik-ntp-bf2a50c/RELEASE_NOTES.md000066400000000000000000000130161506657022200166220ustar00rootroot00000000000000Release v1.5.0 ============== **Changes** * Added the `GetSystemTime` field to `QueryOptions`. * Updated minimum required Go version to 1.24. **Fixes** * Upgraded package dependencies to retrieve security fixes. Release v1.4.3 ============== **Fixes** * Fixed an overflow bug in the clock offset calculation introduced by release v1.4.2. Release v1.4.2 ============== **Fixes** * Fixed a bug in clock offset calculation. Release v1.4.1 ============== **Updates** * Upgraded package dependencies to retrieve security fixes. Release v1.4.0 ============== **Changes** * Added a protocol `Version` field to the `Response` struct. Release v1.3.1 ============== **Changes** * Added AES-256-CMAC support for symmetric authentication. * Symmetric auth keys may now be specified as ASCII or HEX using the "ASCII:" or "HEX:" prefixes. * Updated dependencies to address security issues. **Fixes** * Added proper handling of the empty string when used as a server address. Release v1.3.0 ============== **Changes** * Added the `ReferenceString` function to `Response`. This generates a stratum-specific string for the `ReferenceID` value. * Optimized the AES CMAC calculation for 64-bit architectures. **Fixes** * Fixed a bug introduced in release v1.2.0 that was causing IPv6 addresses to be interpreted incorrectly. Release v1.2.0 ============== **Changes** * Added support for NTP extensions by exposing an extension interface. Extensions are able to (1) modify NTP messages before being sent to the server, and (2) process NTP messages after they arrive from the server. This feature has been added in preparation for NTS support. * Added support for RFC 5905 symmetric key authentication. * Allowed server address to be specified as a "host:port" pair. * Brought package into further compliance with IETF draft on client data minimization. * Declared error variables as part of the public API. * Added a `Dialer` field to `QueryOptions`. This replaces the deprecated `Dial` field. * Added an `IsKissOfDeath` function to the `Response` type. **Deprecated** * Deprecated the `Port` field in QueryOptions. * Deprecated the `Dial` field in QueryOptions. Release v1.1.1 ============== **Fixes** * Fixed a missing indirect go module dependency. Release v1.1.0 ============== **Changes** * Added the `Dial` property to the `QueryOptions` struct. This allows the user to override the default UDP dialer when setting up a connection to a remote NTP server. Release v1.0.0 ============== This package has been stable for several years with no bug reports in that time. It is also pretty much feature complete. I am therefore updating the version to 1.0.0. Because this is a major release, all previously deprecated code has been removed from the package. **Breaking changes** * Removed the `TimeV` function. Use `Time` or `QueryWithOptions` instead. Release v0.3.2 ============== **Changes** * Rename unit tests to enable easier test filtering. Release 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. beevik-ntp-bf2a50c/auth.go000066400000000000000000000140201506657022200155340ustar00rootroot00000000000000// Copyright © 2015-2023 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 ( "bytes" "crypto/aes" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/binary" "encoding/hex" ) // AuthType specifies the cryptographic hash algorithm used to generate a // symmetric key authentication digest (or CMAC) for an NTP message. Please // note that MD5 and SHA1 are no longer considered secure; they appear here // solely for compatibility with existing NTP server implementations. type AuthType int const ( AuthNone AuthType = iota // no authentication AuthMD5 // MD5 digest AuthSHA1 // SHA-1 digest AuthSHA256 // SHA-2 digest (256 bits) AuthSHA512 // SHA-2 digest (512 bits) AuthAES128 // AES-128-CMAC AuthAES256 // AES-256-CMAC ) // AuthOptions contains fields used to configure symmetric key authentication // for an NTP query. type AuthOptions struct { // Type determines the cryptographic hash algorithm used to compute the // authentication digest or CMAC. Type AuthType // The cryptographic key used by the client to perform authentication. The // key may be hex-encoded or ascii-encoded. To use a hex-encoded key, // prefix it by "HEX:". To use an ascii-encoded key, prefix it by // "ASCII:". For example, "HEX:6931564b4a5a5045766c55356b30656c7666316c" // or "ASCII:cvuZyN4C8HX8hNcAWDWp". Key string // The identifier used by the NTP server to identify which key to use // for authentication purposes. KeyID uint16 } var algorithms = []struct { MinKeySize int MaxKeySize int DigestSize int CalcDigest func(payload, key []byte) []byte }{ {0, 0, 0, nil}, // AuthNone {4, 32, 16, calcDigest_MD5}, // AuthMD5 {4, 32, 20, calcDigest_SHA1}, // AuthSHA1 {4, 32, 20, calcDigest_SHA256}, // AuthSHA256 {4, 32, 20, calcDigest_SHA512}, // AuthSHA512 {16, 16, 16, calcCMAC_AES}, // AuthAES128 {32, 32, 16, calcCMAC_AES}, // AuthAES256 } func calcDigest_MD5(payload, key []byte) []byte { digest := md5.Sum(append(key, payload...)) return digest[:] } func calcDigest_SHA1(payload, key []byte) []byte { digest := sha1.Sum(append(key, payload...)) return digest[:] } func calcDigest_SHA256(payload, key []byte) []byte { digest := sha256.Sum256(append(key, payload...)) return digest[:20] } func calcDigest_SHA512(payload, key []byte) []byte { digest := sha512.Sum512(append(key, payload...)) return digest[:20] } func calcCMAC_AES(payload, key []byte) []byte { // calculate the CMAC according to the algorithm defined in RFC 4493. See // https://tools.ietf.org/html/rfc4493 for details. c, err := aes.NewCipher(key) if err != nil { panic(err) } // Generate subkeys. const rb = 0x87 k1 := make([]byte, 16) k2 := make([]byte, 16) c.Encrypt(k1, k1) double(k1, k1, rb) double(k2, k1, rb) // Process all but the last block. cmac := make([]byte, 16) for ; len(payload) > 16; payload = payload[16:] { xor(cmac, payload[:16]) c.Encrypt(cmac, cmac) } // Process the last block, padding as necessary. if len(payload) == 16 { xor(cmac, payload) xor(cmac, k1) } else { xor(cmac, pad(payload)) xor(cmac, k2) } c.Encrypt(cmac, cmac) return cmac } func pad(block []byte) []byte { pad := make([]byte, 16-len(block)) pad[0] = 0x80 return append(block, pad...) } func double(dst, src []byte, xor int) { _ = src[15] // compiler hint: bounds check s0 := binary.BigEndian.Uint64(src[0:8]) s1 := binary.BigEndian.Uint64(src[8:16]) carry := int(s0 >> 63) d0 := (s0 << 1) | (s1 >> 63) d1 := (s1 << 1) ^ uint64(subtle.ConstantTimeSelect(carry, xor, 0)) _ = dst[15] // compiler hint: bounds check binary.BigEndian.PutUint64(dst[0:8], d0) binary.BigEndian.PutUint64(dst[8:16], d1) } func xor(dst, src []byte) { _ = src[15] // compiler hint: bounds check s0 := binary.BigEndian.Uint64(src[0:8]) s1 := binary.BigEndian.Uint64(src[8:16]) _ = dst[15] // compiler hint: bounds check d0 := s0 ^ binary.BigEndian.Uint64(dst[0:8]) d1 := s1 ^ binary.BigEndian.Uint64(dst[8:16]) binary.BigEndian.PutUint64(dst[0:8], d0) binary.BigEndian.PutUint64(dst[8:16], d1) } func decodeAuthKey(opt AuthOptions) (key []byte, err error) { if opt.Type == AuthNone { return nil, nil } var keyIn string var isHex bool switch { case len(opt.Key) >= 4 && opt.Key[:4] == "HEX:": isHex, keyIn = true, opt.Key[4:] case len(opt.Key) >= 6 && opt.Key[:6] == "ASCII:": isHex, keyIn = false, opt.Key[6:] case len(opt.Key) > 20: isHex, keyIn = true, opt.Key default: isHex, keyIn = false, opt.Key } if isHex { key, err = hex.DecodeString(keyIn) if err != nil { return nil, ErrInvalidAuthKey } } else { key = []byte(keyIn) } a := algorithms[opt.Type] if len(key) < a.MinKeySize { return nil, ErrInvalidAuthKey } if len(key) > a.MaxKeySize { key = key[:a.MaxKeySize] } return key, nil } func appendMAC(buf *bytes.Buffer, opt AuthOptions, key []byte) { if opt.Type == AuthNone { return } a := algorithms[opt.Type] payload := buf.Bytes() digest := a.CalcDigest(payload, key) binary.Write(buf, binary.BigEndian, uint32(opt.KeyID)) binary.Write(buf, binary.BigEndian, digest) } func verifyMAC(buf []byte, opt AuthOptions, key []byte) error { if opt.Type == AuthNone { return nil } // Validate that there are enough bytes at the end of the message to // contain a MAC. const headerSize = 48 a := algorithms[opt.Type] macLen := 4 + a.DigestSize remain := len(buf) - headerSize if remain < macLen || (remain%4) != 0 { return ErrAuthFailed } // The key ID returned by the server must be the same as the key ID sent // to the server. payloadLen := len(buf) - macLen mac := buf[payloadLen:] keyID := binary.BigEndian.Uint32(mac[:4]) if keyID != uint32(opt.KeyID) { return ErrAuthFailed } // Calculate and compare digests. payload := buf[:payloadLen] digest := a.CalcDigest(payload, key) if subtle.ConstantTimeCompare(digest, mac[4:]) != 1 { return ErrAuthFailed } return nil } beevik-ntp-bf2a50c/auth_test.go000066400000000000000000000157451506657022200166120ustar00rootroot00000000000000// Copyright © 2015-2023 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 ( "bytes" "encoding/hex" "errors" "os" "strings" "testing" "time" ) func TestOnlineAuthenticatedQuery(t *testing.T) { // By default, this unit test is skipped, because it requires a local NTP // server to be running and configured with known symmetric authentication // keys. // // To run this test, you must execute it with "-args test_auth". For // example: // // go test -v -run TestOnlineAuthenticatedQuery -args test_auth // // You must also run a local NTP server configured with the following // trusted symmetric keys (shown in chrony.keys format): // // 1 MD5 ASCII:cvuZyN4C8HX8hNcAWDWp // 2 SHA1 HEX:6931564b4a5a5045766c55356b30656c7666316c // 3 SHA256 HEX:7133736e777057764256777739706a5533326164 // 4 SHA512 HEX:597675555446585868494d447543425971526e74 // 5 AES128 HEX:68663033736f77706568707164304049 // 6 AES256 HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a skip := true for _, arg := range os.Args { if arg == "test_auth" { skip = false } } if skip { t.Skip("Skipping authentication tests. Enable with -args test_auth") return } var errAuthFail = errors.New("timeout") cases := []struct { Type AuthType Key string KeyID uint16 ExpectedErr error }{ // KeyID 1 (MD5) {AuthMD5, "cvuZyN4C8HX8hNcAWDWp", 1, nil}, {AuthMD5, "ASCII:cvuZyN4C8HX8hNcAWDWp", 1, nil}, {AuthMD5, "6376755a794e344338485838684e634157445770", 1, nil}, {AuthMD5, "HEX:6376755a794e344338485838684e634157445770", 1, nil}, {AuthMD5, "", 1, ErrInvalidAuthKey}, {AuthMD5, "HEX:6376755a794e344338485838684e63415744577", 1, ErrInvalidAuthKey}, {AuthMD5, "HEX:6376755a794e344338485838684e63415744577g", 1, ErrInvalidAuthKey}, {AuthMD5, "ASCII:XvuZyN4C8HX8hNcAWDWp", 1, errAuthFail}, {AuthMD5, "ASCII:cvuZyN4C8HX8hNcAWDWp", 2, errAuthFail}, {AuthSHA1, "ASCII:cvuZyN4C8HX8hNcAWDWp", 1, errAuthFail}, // KeyID 2 (SHA1) {AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, nil}, {AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, nil}, {AuthSHA1, "ASCII:i1VKJZPEvlU5k0elvf1l", 2, nil}, {AuthSHA1, "ASCII:i1VKJZPEvlU5k0elvf1l", 2, nil}, {AuthSHA1, "", 2, ErrInvalidAuthKey}, {AuthSHA1, "HEX:0031564b4a5a5045766c55356b30656c7666316c", 2, errAuthFail}, {AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 1, errAuthFail}, {AuthMD5, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, errAuthFail}, // KeyID 3 (SHA256) {AuthSHA256, "HEX:7133736e777057764256777739706a5533326164", 3, nil}, {AuthSHA256, "ASCII:q3snwpWvBVww9pjU32ad", 3, nil}, {AuthSHA256, "", 3, ErrInvalidAuthKey}, {AuthSHA256, "HEX:0033736e777057764256777739706a5533326164", 3, errAuthFail}, {AuthSHA256, "HEX:7133736e777057764256777739706a5533326164", 2, errAuthFail}, {AuthSHA1, "HEX:7133736e777057764256777739706a5533326164", 3, errAuthFail}, // // KeyID 4 (SHA512) {AuthSHA512, "HEX:597675555446585868494d447543425971526e74", 4, nil}, {AuthSHA512, "ASCII:YvuUTFXXhIMDuCBYqRnt", 4, nil}, {AuthSHA512, "", 4, ErrInvalidAuthKey}, {AuthSHA512, "HEX:007675555446585868494d447543425971526e74", 4, errAuthFail}, {AuthSHA512, "HEX:597675555446585868494d447543425971526e74", 3, errAuthFail}, {AuthSHA256, "HEX:597675555446585868494d447543425971526e74", 4, errAuthFail}, // KeyID 5 (AES128) {AuthAES128, "HEX:68663033736f77706568707164304049", 5, nil}, {AuthAES128, "HEX:68663033736f77706568707164304049fefefefe", 5, nil}, {AuthAES128, "ASCII:hf03sowpehpqd0@I", 5, nil}, {AuthAES128, "", 5, ErrInvalidAuthKey}, {AuthAES128, "HEX:00663033736f77706568707164304049", 5, errAuthFail}, {AuthAES128, "HEX:68663033736f77706568707164304049", 4, errAuthFail}, {AuthMD5, "HEX:68663033736f77706568707164304049", 5, errAuthFail}, // KeyID 6 (AES256) {AuthAES256, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, nil}, {AuthAES256, "", 6, ErrInvalidAuthKey}, {AuthAES256, "HEX:00cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, errAuthFail}, {AuthAES256, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 5, errAuthFail}, {AuthMD5, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, errAuthFail}, } for i, c := range cases { opt := QueryOptions{ Timeout: 250 * time.Millisecond, Auth: AuthOptions{c.Type, c.Key, c.KeyID}, } r, err := QueryWithOptions(host, opt) if c.ExpectedErr == errAuthFail { // With old NTP servers, failed authentication leads to Crypto-NAK // (ErrAuthFailed). With modern NTP servers, it leads to an I/O // timeout error. if err != ErrAuthFailed && !strings.Contains(err.Error(), "timeout") { t.Errorf("case %d: expected error [%v], got error [%v]\n", i, c.ExpectedErr, err) } continue } if c.ExpectedErr != nil && c.ExpectedErr == err { continue } if err == nil { err = r.Validate() if err != c.ExpectedErr { t.Errorf("case %d: expected error [%v], got error [%v]\n", i, c.ExpectedErr, err) } } } } func TestOfflineAesCmac(t *testing.T) { // Test cases taken from NIST document: // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf const ( Key128 = "2b7e1516 28aed2a6 abf71588 09cf4f3c" Key192 = "8e73b0f7 da0e6452 c810f32b 809079e5 62f8ead2 522c6b7b" Key256 = "603deb10 15ca71be 2b73aef0 857d7781 1f352c07 3b6108d7 2d9810a3 0914dff4" ) const ( Msg1 = "" Msg2 = "6bc1bee2 2e409f96 e93d7e11 7393172a" Msg3 = "6bc1bee2 2e409f96 e93d7e11 7393172a ae2d8a57" Msg4 = "6bc1bee2 2e409f96 e93d7e11 7393172a ae2d8a57 1e03ac9c 9eb76fac 45af8e51" + "30c81c46 a35ce411 e5fbc119 1a0a52ef f69f2445 df4f9b17 ad2b417b e66c3710" ) cases := []struct { key string plaintext string cmac string }{ // 128-bit key {Key128, Msg1, "bb1d6929 e9593728 7fa37d12 9b756746"}, {Key128, Msg2, "070a16b4 6b4d4144 f79bdd9d d04a287c"}, {Key128, Msg3, "7d85449e a6ea19c8 23a7bf78 837dfade"}, {Key128, Msg4, "51f0bebf 7e3b9d92 fc497417 79363cfe"}, // 192-bit key {Key192, Msg1, "d17ddf46 adaacde5 31cac483 de7a9367"}, {Key192, Msg2, "9e99a7bf 31e71090 0662f65e 617c5184"}, {Key192, Msg3, "3d75c194 ed960704 44a9fa7e c740ecf8"}, {Key192, Msg4, "a1d5df0e ed790f79 4d775896 59f39a11"}, // 256-bit key {Key256, Msg1, "028962f6 1b7bf89e fc6b551f 4667d983"}, {Key256, Msg2, "28a7023f 452e8f82 bd4bf28d 8c37c35c"}, {Key256, Msg3, "156727dc 0878944a 023c1fe0 3bad6d93"}, {Key256, Msg4, "e1992190 549f6ed5 696a2c05 6c315410"}, } for i, c := range cases { _ = i key, pt, cmac := hexDecode(c.key), hexDecode(c.plaintext), hexDecode(c.cmac) result := calcCMAC_AES(pt, key) if !bytes.Equal(cmac, result) { t.Errorf("case %d: CMACs do not match.\n", i) } } } func hexDecode(s string) []byte { s = strings.ReplaceAll(s, " ", "") b, err := hex.DecodeString(s) if err != nil { panic(err) } return b } beevik-ntp-bf2a50c/go.mod000066400000000000000000000004551506657022200153610ustar00rootroot00000000000000module github.com/beevik/ntp go 1.24.0 require ( github.com/stretchr/testify v1.11.1 golang.org/x/net v0.44.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) beevik-ntp-bf2a50c/go.sum000066400000000000000000000022451506657022200154050ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= beevik-ntp-bf2a50c/ntp.go000066400000000000000000000637251506657022200154140ustar00rootroot00000000000000// Copyright © 2015-2023 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 // RFC 5905 (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 ( "bytes" "crypto/rand" "encoding/binary" "errors" "fmt" "net" "strconv" "strings" "time" "golang.org/x/net/ipv4" ) var ( ErrAuthFailed = errors.New("authentication failed") ErrInvalidAuthKey = errors.New("invalid authentication key") ErrInvalidDispersion = errors.New("invalid dispersion in response") ErrInvalidLeapSecond = errors.New("invalid leap second in response") ErrInvalidMode = errors.New("invalid mode in response") ErrInvalidProtocolVersion = errors.New("invalid protocol version requested") ErrInvalidStratum = errors.New("invalid stratum in response") ErrInvalidTime = errors.New("invalid time reported") ErrInvalidTransmitTime = errors.New("invalid transmit time in response") ErrKissOfDeath = errors.New("kiss of death received") ErrServerClockFreshness = errors.New("server clock not fresh") ErrServerResponseMismatch = errors.New("server response didn't match request") ErrServerTickedBackwards = errors.New("server clock ticked backwards") ) // 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 defaultNtpPort = 123 nanoPerSec = 1000000000 maxStratum = 16 defaultTimeout = 5 * time.Second maxPollInterval = (1 << 17) * time.Second maxDispersion = 16 * time.Second ) // Internal variables var ( ntpEra0 = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC) ntpEra1 = time.Date(2036, 2, 7, 6, 28, 16, 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 { // Assume NTP era 1 (year 2036+) if the raw timestamp suggests a year // before 1970. Otherwise assume NTP era 0. This allows the function to // report an accurate time value both before and after the 0-to-1 era // rollover. const t1970 = 0x83aa7e8000000000 if uint64(t) < t1970 { return ntpEra1.Add(t.Duration()) } return ntpEra0.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(ntpEra0)) 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) } // header is an internal representation of an NTP packet header. type header struct { LiVnMode uint8 // Leap Indicator (2) + Version (3) + Mode (3) Stratum uint8 Poll int8 Precision int8 RootDelay ntpTimeShort RootDispersion ntpTimeShort ReferenceID uint32 // KoD code if Stratum == 0 ReferenceTime ntpTime OriginTime ntpTime ReceiveTime ntpTime TransmitTime ntpTime } // setVersion sets the NTP protocol version on the header. func (h *header) setVersion(v int) { h.LiVnMode = (h.LiVnMode & 0xc7) | uint8(v)<<3 } // setMode sets the NTP protocol mode on the header. func (h *header) setMode(md mode) { h.LiVnMode = (h.LiVnMode & 0xf8) | uint8(md) } // setLeap modifies the leap indicator on the header. func (h *header) setLeap(li LeapIndicator) { h.LiVnMode = (h.LiVnMode & 0x3f) | uint8(li)<<6 } // getVersion returns the version value in the header. func (h *header) getVersion() int { return int((h.LiVnMode >> 3) & 0x7) } // getMode returns the mode value in the header. func (h *header) getMode() mode { return mode(h.LiVnMode & 0x07) } // getLeap returns the leap indicator on the header. func (h *header) getLeap() LeapIndicator { return LeapIndicator((h.LiVnMode >> 6) & 0x03) } // An Extension adds custom behaviors capable of modifying NTP packets before // being sent to the server and processing packets after being received by the // server. type Extension interface { // ProcessQuery is called when the client is about to send a query to the // NTP server. The buffer contains the NTP header. It may also contain // extension fields added by extensions processed prior to this one. ProcessQuery(buf *bytes.Buffer) error // ProcessResponse is called after the client has received the server's // NTP response. The buffer contains the entire message returned by the // server. ProcessResponse(buf []byte) error } // QueryOptions contains configurable options used by the QueryWithOptions // function. type QueryOptions struct { // Timeout determines how long the client waits for a response from the // server before failing with a timeout error. Defaults to 5 seconds. Timeout time.Duration // Version of the NTP protocol to use. Defaults to 4. Version int // LocalAddress contains the local IP address to use when creating a // connection to the remote NTP server. This may be useful when the local // system has more than one IP address. This address should not contain // a port number. LocalAddress string // TTL specifies the maximum number of IP hops before the query datagram // is dropped by the network. Defaults to the local system's default value. TTL int // Auth contains the settings used to configure NTP symmetric key // authentication. See RFC 5905 for further details. Auth AuthOptions // Extensions may be added to modify NTP queries before they are // transmitted and to process NTP responses after they arrive. Extensions []Extension // GetSystemTime is a callback used to override the default method of // obtaining the local system time during time synchronization. If not // specified, time.Now is used. GetSystemTime func() time.Time // Dialer is a callback used to override the default UDP network dialer. // The localAddress is directly copied from the LocalAddress field // specified in QueryOptions. It may be the empty string or a host address // (without port number). The remoteAddress is the "host:port" string // derived from the first parameter to QueryWithOptions. The // remoteAddress is guaranteed to include a port number. Dialer func(localAddress, remoteAddress string) (net.Conn, error) // Dial is a callback used to override the default UDP network dialer. // // DEPRECATED. Use Dialer instead. Dial func(laddr string, lport int, raddr string, rport int) (net.Conn, error) // Port indicates the port used to reach the remote NTP server. // // DEPRECATED. Embed the port number in the query address string instead. Port int } // A Response contains time data, some of which is returned by the NTP server // and some of which is calculated by this client. type Response struct { // ClockOffset is the estimated offset of the local system clock relative // to the server's clock. Add this value to subsequent local system clock // times in order to obtain a time that is synchronized to the server's // clock. ClockOffset time.Duration // Time is the time the server transmitted this response, measured using // its own clock. You should not use this value for time synchronization // purposes. Add ClockOffset to your system clock instead. Time time.Time // 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 // Version is the NTP protocol version number reported by the server. Version int // 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 integer identifying the server or reference // clock. For stratum 1 servers, this is typically a meaningful // zero-padded ASCII-encoded string assigned to the clock. For stratum 2+ // servers, this is a reference identifier for the server and is either // the server's IPv4 address or a hash of its IPv6 address. For // kiss-of-death responses (stratum 0), this is the ASCII-encoded "kiss // code". ReferenceID uint32 // ReferenceTime is the time the server last updated its local clock. 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 query messages to // the server. Poll time.Duration authErr error } // IsKissOfDeath returns true if the response is a "kiss of death" from the // remote server. If this function returns true, you may examine the // response's KissCode value to determine the reason for the kiss of death. func (r *Response) IsKissOfDeath() bool { return r.Stratum == 0 } // ReferenceString returns the response's ReferenceID value formatted as a // string. If the response's stratum is zero, then the "kiss o' death" string // is returned. If stratum is one, then the server is a reference clock and // the reference clock's name is returned. If stratum is two or greater, then // the ID is either an IPv4 address or an MD5 hash of the IPv6 address; in // either case the reference string is reported as 4 dot-separated // decimal-based integers. func (r *Response) ReferenceString() string { if r.Stratum == 0 { return kissCode(r.ReferenceID) } var b [4]byte binary.BigEndian.PutUint32(b[:], r.ReferenceID) if r.Stratum == 1 { const dot = rune(0x22c5) var r []rune for i := range b { if b[i] == 0 { break } if b[i] >= 32 && b[i] <= 126 { r = append(r, rune(b[i])) } else { r = append(r, dot) } } return fmt.Sprintf(".%s.", string(r)) } return fmt.Sprintf("%d.%d.%d.%d", b[0], b[1], b[2], b[3]) } // Validate checks if the response is valid for the purposes of time // synchronization. func (r *Response) Validate() error { // Forward authentication errors. if r.authErr != nil { return r.authErr } // Handle invalid stratum values. if r.Stratum == 0 { return ErrKissOfDeath } if r.Stratum >= maxStratum { return ErrInvalidStratum } // 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 ErrServerClockFreshness } // 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 ErrInvalidDispersion } // If the server's transmit time is before its reference time, the // response is invalid. if r.Time.Before(r.ReferenceTime) { return ErrInvalidTime } // Handle invalid leap second indicator. if r.Leap == LeapNotInSync { return ErrInvalidLeapSecond } // nil means the response is valid. return nil } // Query requests time data from a remote NTP server. The response contains // information from which a more accurate local time can be inferred. // // The server address is of the form "host", "host:port", "host%zone:port", // "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or // domain name address. When specifying both a port and an IPv6 address, one // of the bracket formats must be used. If no port is included, NTP default // port 123 is used. func Query(address string) (*Response, error) { return QueryWithOptions(address, QueryOptions{}) } // QueryWithOptions performs the same function as Query but allows for the // customization of certain query behaviors. See the comments for Query and // QueryOptions for further details. func QueryWithOptions(address string, opt QueryOptions) (*Response, error) { h, now, err := getTime(address, &opt) if err != nil && err != ErrAuthFailed { return nil, err } return generateResponse(h, now, err), nil } // Time returns the current, corrected local time using information returned // from the remote NTP server. On error, Time returns the uncorrected local // system time. // // The server address is of the form "host", "host:port", "host%zone:port", // "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or // domain name address. When specifying both a port and an IPv6 address, one // of the bracket formats must be used. If no port is included, NTP default // port 123 is used. func Time(address string) (time.Time, error) { r, err := Query(address) if err != nil { return time.Now(), err } err = r.Validate() if err != nil { return time.Now(), err } // Use the response's clock offset to calculate an accurate time. return time.Now().Add(r.ClockOffset), nil } // getTime performs the NTP server query and returns the response header // along with the local system time it was received. func getTime(address string, opt *QueryOptions) (*header, ntpTime, error) { if opt.Timeout == 0 { opt.Timeout = defaultTimeout } if opt.Version == 0 { opt.Version = defaultNtpVersion } if opt.Version < 2 || opt.Version > 4 { return nil, 0, ErrInvalidProtocolVersion } if opt.Port == 0 { opt.Port = defaultNtpPort } if opt.Dial != nil { // wrapper for the deprecated Dial callback. opt.Dialer = func(la, ra string) (net.Conn, error) { return dialWrapper(la, ra, opt.Dial) } } if opt.Dialer == nil { opt.Dialer = defaultDialer } if opt.GetSystemTime == nil { opt.GetSystemTime = time.Now } // Compose a conforming host:port remote address string if the address // string doesn't already contain a port. remoteAddress, err := fixHostPort(address, opt.Port) if err != nil { return nil, 0, err } // Connect to the remote server. con, err := opt.Dialer(opt.LocalAddress, remoteAddress) 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. con.SetDeadline(time.Now().Add(opt.Timeout)) // Allocate a buffer big enough to hold an entire response datagram. recvBuf := make([]byte, 8192) recvHdr := new(header) // Allocate the query message header. xmitHdr := new(header) xmitHdr.setMode(client) xmitHdr.setVersion(opt.Version) xmitHdr.setLeap(LeapNoWarning) xmitHdr.Precision = 0x20 // To help prevent spoofing and client fingerprinting, use a // cryptographically random 64-bit value for the TransmitTime. See: // https://www.ietf.org/archive/id/draft-ietf-ntp-data-minimization-04.txt bits := make([]byte, 8) _, err = rand.Read(bits) if err != nil { return nil, 0, err } xmitHdr.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits)) // Write the query header to a transmit buffer. var xmitBuf bytes.Buffer binary.Write(&xmitBuf, binary.BigEndian, xmitHdr) // Allow extensions to process the query and add to the transmit buffer. for _, e := range opt.Extensions { err = e.ProcessQuery(&xmitBuf) if err != nil { return nil, 0, err } } // If using symmetric key authentication, decode and validate the auth key // string. authKey, err := decodeAuthKey(opt.Auth) if err != nil { return nil, 0, err } // Append a MAC if authentication is being used. appendMAC(&xmitBuf, opt.Auth, authKey) // Transmit the query and keep track of when it was transmitted. xmitTime := opt.GetSystemTime() _, err = con.Write(xmitBuf.Bytes()) if err != nil { return nil, 0, err } // Receive the response. recvBytes, err := con.Read(recvBuf) if err != nil { return nil, 0, err } // Keep track of the time the response was received. As of go 1.9, the // time package uses a monotonic clock, so delta will never be less than // zero for go version 1.9 or higher. recvTime := opt.GetSystemTime() if recvTime.Sub(xmitTime) < 0 { recvTime = xmitTime } // Parse the response header. recvBuf = recvBuf[:recvBytes] recvReader := bytes.NewReader(recvBuf) err = binary.Read(recvReader, binary.BigEndian, recvHdr) if err != nil { return nil, 0, err } // Allow extensions to process the response. for i := len(opt.Extensions) - 1; i >= 0; i-- { err = opt.Extensions[i].ProcessResponse(recvBuf) if err != nil { return nil, 0, err } } // Check for invalid fields. if recvHdr.getMode() != server { return nil, 0, ErrInvalidMode } if recvHdr.TransmitTime == ntpTime(0) { return nil, 0, ErrInvalidTransmitTime } if recvHdr.OriginTime != xmitHdr.TransmitTime { return nil, 0, ErrServerResponseMismatch } if recvHdr.ReceiveTime > recvHdr.TransmitTime { return nil, 0, ErrServerTickedBackwards } // Correct the received message's origin time using the actual // transmit time. recvHdr.OriginTime = toNtpTime(xmitTime) // Perform authentication of the server response. authErr := verifyMAC(recvBuf, opt.Auth, authKey) return recvHdr, toNtpTime(recvTime), authErr } // defaultDialer provides a UDP dialer based on Go's built-in net stack. func defaultDialer(localAddress, remoteAddress string) (net.Conn, error) { var laddr *net.UDPAddr if localAddress != "" { var err error laddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(localAddress, "0")) if err != nil { return nil, err } } raddr, err := net.ResolveUDPAddr("udp", remoteAddress) if err != nil { return nil, err } return net.DialUDP("udp", laddr, raddr) } // dialWrapper is used to wrap the deprecated Dial callback in QueryOptions. func dialWrapper(la, ra string, dial func(la string, lp int, ra string, rp int) (net.Conn, error)) (net.Conn, error) { rhost, rport, err := net.SplitHostPort(ra) if err != nil { return nil, err } rportValue, err := strconv.Atoi(rport) if err != nil { return nil, err } return dial(la, 0, rhost, rportValue) } // fixHostPort examines an address in one of the accepted forms and fixes it // to include a port number if necessary. func fixHostPort(address string, defaultPort int) (fixed string, err error) { if len(address) == 0 { return "", errors.New("address string is empty") } // If the address is wrapped in brackets, append a port if necessary. if address[0] == '[' { end := strings.IndexByte(address, ']') switch { case end < 0: return "", errors.New("missing ']' in address") case end+1 == len(address): return fmt.Sprintf("%s:%d", address, defaultPort), nil case address[end+1] == ':': return address, nil default: return "", errors.New("unexpected character following ']' in address") } } // No colons? Must be a port-less IPv4 or domain address. last := strings.LastIndexByte(address, ':') if last < 0 { return fmt.Sprintf("%s:%d", address, defaultPort), nil } // Exactly one colon? A port have been included along with an IPv4 or // domain address. (IPv6 addresses are guaranteed to have more than one // colon.) prev := strings.LastIndexByte(address[:last], ':') if prev < 0 { return address, nil } // Two or more colons means we must have an IPv6 address without a port. return fmt.Sprintf("[%s]:%d", address, defaultPort), nil } // generateResponse processes NTP header fields along with the its receive // time to generate a Response record. func generateResponse(h *header, recvTime ntpTime, authErr error) *Response { r := &Response{ Time: h.TransmitTime.Time(), ClockOffset: offset(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), RTT: rtt(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), Precision: toInterval(h.Precision), Version: h.getVersion(), Stratum: h.Stratum, ReferenceID: h.ReferenceID, ReferenceTime: h.ReferenceTime.Time(), RootDelay: h.RootDelay.Duration(), RootDispersion: h.RootDispersion.Duration(), Leap: h.getLeap(), MinError: minError(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), Poll: toInterval(h.Poll), authErr: authErr, } // 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 { a := int64(dst - org) b := int64(xmt - rec) rtt := a - b if rtt < 0 { rtt = 0 } return ntpTime(rtt).Duration() } func offset(org, rec, xmt, dst ntpTime) time.Duration { // The inputs are 64-bit unsigned integer timestamps. These timestamps can // "roll over" at the end of an NTP era, which occurs approximately every // 136 years starting from the year 1900. To ensure an accurate offset // calculation when an era boundary is crossed, we need to take care that // the difference between two 64-bit timestamp values is accurately // calculated even when they are in neighboring eras. // // See: https://www.eecis.udel.edu/~mills/y2k.html a := int64(rec - org) b := int64(xmt - dst) offset := a + (b-a)/2 if offset < 0 { return -ntpTime(-offset).Duration() } return ntpTime(offset).Duration() } 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 := [4]byte{ byte(id >> 24), byte(id >> 16), byte(id >> 8), byte(id), } for _, ch := range b { if !isPrintable(ch) { return "" } } return string(b[:]) } beevik-ntp-bf2a50c/ntp_test.go000066400000000000000000000405131506657022200164410ustar00rootroot00000000000000// Copyright © 2015-2023 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 ( "errors" "net" "os" "strings" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" ) // The NTP server to use for online unit tests. May be overridden by the // NTP_HOST environment variable. var host string = "0.beevik-ntp.pool.ntp.org" const ( refID = 0xc0a80001 timeFormat = "Mon Jan _2 2006 15:04:05.00000000 (MST)" ) func init() { h := os.Getenv("NTP_HOST") if h != "" { host = h } } func isNil(t *testing.T, host string, err error) bool { switch { case err == nil: return true case err == ErrKissOfDeath: // log instead of error, so test isn't failed t.Logf("[%s] Query kiss of death (ignored)", host) return false case strings.Contains(err.Error(), "timeout"): // log instead of error, so test isn't failed t.Logf("[%s] Query timeout (ignored): %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, host, err) } func assertInvalid(t *testing.T, r *Response) { err := r.Validate() if err == nil { t.Errorf("[%s] Response unexpectedly valid\n", host) } } func logResponse(t *testing.T, r *Response) { now := time.Now() t.Logf("[%s] ClockOffset: %s", host, r.ClockOffset) t.Logf("[%s] SystemTime: %s", host, now.Format(timeFormat)) t.Logf("[%s] ~TrueTime: %s", host, now.Add(r.ClockOffset).Format(timeFormat)) t.Logf("[%s] XmitTime: %s", host, r.Time.Format(timeFormat)) t.Logf("[%s] Version: %d", host, r.Version) t.Logf("[%s] Stratum: %d", host, r.Stratum) t.Logf("[%s] RefID: %s (0x%08x)", host, r.ReferenceString(), r.ReferenceID) t.Logf("[%s] RefTime: %s", host, r.ReferenceTime.Format(timeFormat)) t.Logf("[%s] RTT: %s", host, r.RTT) t.Logf("[%s] Poll: %s", host, r.Poll) t.Logf("[%s] Precision: %s", host, r.Precision) t.Logf("[%s] RootDelay: %s", host, r.RootDelay) t.Logf("[%s] RootDisp: %s", host, r.RootDispersion) t.Logf("[%s] RootDist: %s", host, r.RootDistance) t.Logf("[%s] MinError: %s", host, r.MinError) t.Logf("[%s] Leap: %d", host, r.Leap) t.Logf("[%s] KissCode: %s", host, stringOrEmpty(r.KissCode)) } func stringOrEmpty(s string) string { if s == "" { return "" } return s } func TestOnlineBadServerPort(t *testing.T) { // Not NTP port. tm, _, err := getTime(host+":9", &QueryOptions{Timeout: 1 * time.Second}) assert.Nil(t, tm) assert.NotNil(t, err) } func TestOnlineQuery(t *testing.T) { r, err := QueryWithOptions(host, QueryOptions{}) if !isNil(t, host, err) { return } assertValid(t, r) logResponse(t, r) } func TestOnlineQueryTimeout(t *testing.T) { if host == "localhost" { t.Skip("Timeout test not available with localhost NTP server.") return } // Force an immediate timeout. r, err := QueryWithOptions(host, QueryOptions{Timeout: time.Nanosecond}) assert.Nil(t, r) assert.NotNil(t, err) } func TestOnlineTime(t *testing.T) { tm, err := Time(host) now := time.Now() if isNil(t, host, err) { t.Logf(" System Time: %s\n", now.Format(timeFormat)) t.Logf(" ~True Time: %s\n", tm.Format(timeFormat)) t.Logf("~ClockOffset: %v\n", tm.Sub(now)) } } func TestOnlineTimeFailure(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) // 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. now := time.Now() diffMinutes := now.Sub(local).Minutes() assert.True(t, diffMinutes > -1 && diffMinutes < 1) } func TestOnlineTTL(t *testing.T) { if host == "localhost" { t.Skip("TTL test not available with localhost NTP server.") return } // TTL of 1 should cause a timeout. hdr, _, err := getTime(host, &QueryOptions{TTL: 1, Timeout: 1 * time.Second}) assert.Nil(t, hdr) assert.NotNil(t, err) } func TestOnlineCustomGetSystemTime(t *testing.T) { if host == "localhost" { t.Skip("Timeout test not available with localhost NTP server.") return } var simuTime atomic.Value simuTime.Store(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) const timerInterval = 1 * time.Millisecond ctx := t.Context() // Start a simulated clock independent of the system wall clock, // initialized at 2020-01-01T00:00:00, advancing in 1 ms increments. go func() { ticker := time.NewTicker(timerInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: current := simuTime.Load().(time.Time) simuTime.Store(current.Add(timerInterval)) } } }() r, err := QueryWithOptions(host, QueryOptions{ GetSystemTime: func() time.Time { return simuTime.Load().(time.Time) }, }) if isNil(t, host, err) { tm := simuTime.Load().(time.Time) trueTime := tm.Add(r.ClockOffset) t.Logf(" Custom Time: %s\n", tm.Format(timeFormat)) t.Logf(" ~True Time: %s\n", trueTime.Format(timeFormat)) t.Logf("~ClockOffset: %v\n", trueTime.Sub(tm)) } } func TestOfflineConvertLong(t *testing.T) { ts := []ntpTime{0x0, 0xff800000, 0x1ff800000, 0x80000000ff800000, 0xffffffffff800000} for _, v := range ts { assert.Equal(t, v, toNtpTime(v.Time())) } } func TestOfflineConvertShort(t *testing.T) { cases := []struct { NtpTime ntpTimeShort Duration time.Duration }{ {0x00000000, 0 * time.Nanosecond}, {0x00000001, 15259 * time.Nanosecond}, {0x00008000, 500 * time.Millisecond}, {0x0000c000, 750 * time.Millisecond}, {0x0000ff80, time.Second - (1000000000/512)*time.Nanosecond}, {0x00010000, 1000 * time.Millisecond}, {0x00018000, 1500 * time.Millisecond}, {0xffff0000, 65535 * time.Second}, {0xffffff80, 65536*time.Second - (1000000000/512)*time.Nanosecond}, } for _, c := range cases { ts := c.NtpTime assert.Equal(t, c.Duration, ts.Duration()) } } func TestOfflineCustomDialer(t *testing.T) { raddr := "remote:123" laddr := "local" dialerCalled := false notDialingErr := errors.New("not dialing") customDialer := func(la, ra string) (net.Conn, error) { assert.Equal(t, laddr, la) assert.Equal(t, raddr, ra) // Only expect to be called once: assert.False(t, dialerCalled) dialerCalled = true return nil, notDialingErr } opt := QueryOptions{ LocalAddress: laddr, Dialer: customDialer, } r, err := QueryWithOptions(raddr, opt) assert.Nil(t, r) assert.Equal(t, notDialingErr, err) assert.True(t, dialerCalled) } func TestOfflineCustomDialerDeprecated(t *testing.T) { raddr := "remote" laddr := "local" dialerCalled := false notDialingErr := errors.New("not dialing") customDial := func(la string, lp int, ra string, rp int) (net.Conn, error) { assert.Equal(t, laddr, la) assert.Equal(t, 0, lp) assert.Equal(t, raddr, ra) assert.Equal(t, 123, rp) // Only expect to be called once: assert.False(t, dialerCalled) dialerCalled = true return nil, notDialingErr } opt := QueryOptions{ LocalAddress: laddr, Dial: customDial, } r, err := QueryWithOptions(raddr, opt) assert.Nil(t, r) assert.Equal(t, notDialingErr, err) assert.True(t, dialerCalled) } func TestOfflineFixHostPort(t *testing.T) { const defaultPort = 123 cases := []struct { address string fixed string errMsg string }{ {"192.168.1.1", "192.168.1.1:123", ""}, {"192.168.1.1:123", "192.168.1.1:123", ""}, {"192.168.1.1:1000", "192.168.1.1:1000", ""}, {"[192.168.1.1]:1000", "[192.168.1.1]:1000", ""}, {"www.example.com", "www.example.com:123", ""}, {"www.example.com:123", "www.example.com:123", ""}, {"www.example.com:1000", "www.example.com:1000", ""}, {"[www.example.com]:1000", "[www.example.com]:1000", ""}, {"::1", "[::1]:123", ""}, {"[::1]", "[::1]:123", ""}, {"[::1]:123", "[::1]:123", ""}, {"[::1]:1000", "[::1]:1000", ""}, {"fe80::1", "[fe80::1]:123", ""}, {"[fe80::1]", "[fe80::1]:123", ""}, {"[fe80::1]:123", "[fe80::1]:123", ""}, {"[fe80::1]:1000", "[fe80::1]:1000", ""}, {"[fe80::", "", "missing ']' in address"}, {"[fe80::]@", "", "unexpected character following ']' in address"}, {"ff06:0:0:0:0:0:0:c3", "[ff06:0:0:0:0:0:0:c3]:123", ""}, {"[ff06:0:0:0:0:0:0:c3]", "[ff06:0:0:0:0:0:0:c3]:123", ""}, {"[ff06:0:0:0:0:0:0:c3]:123", "[ff06:0:0:0:0:0:0:c3]:123", ""}, {"[ff06:0:0:0:0:0:0:c3]:1000", "[ff06:0:0:0:0:0:0:c3]:1000", ""}, {"::ffff:192.168.1.1", "[::ffff:192.168.1.1]:123", ""}, {"[::ffff:192.168.1.1]", "[::ffff:192.168.1.1]:123", ""}, {"[::ffff:192.168.1.1]:123", "[::ffff:192.168.1.1]:123", ""}, {"[::ffff:192.168.1.1]:1000", "[::ffff:192.168.1.1]:1000", ""}, {"", "", "address string is empty"}, } for _, c := range cases { fixed, err := fixHostPort(c.address, defaultPort) errMsg := "" if err != nil { errMsg = err.Error() } assert.Equal(t, c.fixed, fixed) assert.Equal(t, c.errMsg, errMsg) } } func TestOfflineKissCode(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"}, {0x4e54534e, "NTSN"}, {0x52415445, "RATE"}, {0x524d4f54, "RMOT"}, {0x53544550, "STEP"}, {0x01010101, ""}, {0xfefefefe, ""}, {0x01544450, ""}, {0x41544401, ""}, } for _, c := range codes { assert.Equal(t, kissCode(c.id), c.str) } } func TestOfflineMinError(t *testing.T) { start := time.Now() h := &header{ 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 := generateResponse(h, toNtpTime(start.Add(4*time.Second)), nil) 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 { h.OriginTime = toNtpTime(start.Add(org)) h.ReceiveTime = toNtpTime(start.Add(rec)) h.TransmitTime = toNtpTime(start.Add(xmt)) r = generateResponse(h, toNtpTime(start.Add(dst)), nil) 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 TestOfflineOffsetCalculation(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 TestOfflineOffsetCalculationNegative(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 TestOfflineOffsetRollover(t *testing.T) { cases := []struct { clientTime string serverTime string }{ // both timestamps in NTP era 0 (with large difference) {"1970-01-01 00:00:00", "2024-05-30 00:00:00"}, {"2024-05-30 00:00:00", "1970-01-01 00:00:00"}, // one timestamp in NTP era 0 and another in era 1 {"2047-01-01 00:00:00", "2024-01-01 00:00:00"}, {"2024-01-01 00:00:00", "2047-01-01 00:00:00"}, // both timestamps in NTP era 1 {"2047-01-01 00:00:00", "2047-02-01 00:00:00"}, {"2047-02-01 00:00:00", "2047-01-01 00:00:00"}, } timeFormat := "2006-01-02 15:04:05" for _, c := range cases { clientTime, _ := time.Parse(timeFormat, c.clientTime) serverTime, _ := time.Parse(timeFormat, c.serverTime) org := toNtpTime(clientTime) rec := toNtpTime(serverTime) xmt := toNtpTime(serverTime.Add(1 * time.Second)) dst := toNtpTime(clientTime.Add(1 * time.Second)) expectedValue := serverTime.Sub(clientTime) value := offset(org, rec, xmt, dst) assert.Equal(t, expectedValue, value) } } func TestOfflineTimeRollover(t *testing.T) { cases := []struct { timestamp ntpTime time string }{ {0x0000000000000000, "2036-02-07 06:28:16"}, {0x0000000100000000, "2036-02-07 06:28:17"}, {0x1000000000000000, "2044-08-10 03:52:32"}, {0x2000000000000000, "2053-02-11 01:16:48"}, {0x3000000000000000, "2061-08-14 22:41:04"}, {0x4000000000000000, "2070-02-15 20:05:20"}, {0x5000000000000000, "2078-08-19 17:29:36"}, {0x6000000000000000, "2087-02-20 14:53:52"}, {0x7000000000000000, "2095-08-24 12:18:08"}, {0x8000000000000000, "2104-02-26 09:42:24"}, {0x83aa7e7000000000, "2106-02-07 06:28:00"}, {0x83aa7e8000000000, "1970-01-01 00:00:00"}, // <- ntpTime.Time() wrap {0x9000000000000000, "1976-07-23 00:38:24"}, {0xa000000000000000, "1985-01-23 22:02:40"}, {0xb000000000000000, "1993-07-27 19:26:56"}, {0xc000000000000000, "2002-01-28 16:51:12"}, {0xd000000000000000, "2010-08-01 14:15:28"}, {0xe000000000000000, "2019-02-02 11:39:44"}, {0xf000000000000000, "2027-08-06 09:04:00"}, {0xffffffff00000000, "2036-02-07 06:28:15"}, } timeFormat := "2006-01-02 15:04:05" for _, c := range cases { tm, _ := time.Parse(timeFormat, c.time) assert.Equal(t, tm, c.timestamp.Time()) assert.Equal(t, c.timestamp, toNtpTime(tm)) } } func TestOfflineReferenceString(t *testing.T) { cases := []struct { Stratum byte RefID uint32 Str string }{ {0, 0x41435354, "ACST"}, {0, 0x41555448, "AUTH"}, {0, 0x4155544f, "AUTO"}, {0, 0x42435354, "BCST"}, {0, 0x43525950, "CRYP"}, {0, 0x44454e59, "DENY"}, {0, 0x44524f50, "DROP"}, {0, 0x52535452, "RSTR"}, {0, 0x494e4954, "INIT"}, {0, 0x4d435354, "MCST"}, {0, 0x4e4b4559, "NKEY"}, {0, 0x4e54534e, "NTSN"}, {0, 0x52415445, "RATE"}, {0, 0x524d4f54, "RMOT"}, {0, 0x53544550, "STEP"}, {0, 0x01010101, ""}, {0, 0xfefefefe, ""}, {0, 0x01544450, ""}, {0, 0x41544401, ""}, {1, 0x47505300, ".GPS."}, {1, 0x474f4553, ".GOES."}, {2, 0x0a0a1401, "10.10.20.1"}, {3, 0xc0a80001, "192.168.0.1"}, {4, 0xc0a80001, "192.168.0.1"}, {5, 0xc0a80001, "192.168.0.1"}, {6, 0xc0a80001, "192.168.0.1"}, {7, 0xc0a80001, "192.168.0.1"}, {8, 0xc0a80001, "192.168.0.1"}, {9, 0xc0a80001, "192.168.0.1"}, {10, 0xc0a80001, "192.168.0.1"}, } for _, c := range cases { r := Response{Stratum: c.Stratum, ReferenceID: c.RefID} assert.Equal(t, c.Str, r.ReferenceString()) } } func TestOfflineTimeConversions(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 TestOfflineValidate(t *testing.T) { var h header var r *Response h.Stratum = 1 h.ReferenceID = refID h.ReferenceTime = 1 << 32 h.Precision = -1 // 500ms // Zero RTT h.OriginTime = 1 << 32 h.ReceiveTime = 1 << 32 h.TransmitTime = 1 << 32 r = generateResponse(&h, 1<<32, nil) assertValid(t, r) // Negative freshness h.ReferenceTime = 2 << 32 r = generateResponse(&h, 1<<32, nil) assertInvalid(t, r) // Unfresh clock (48h) h.OriginTime = 2 * 86400 << 32 h.ReceiveTime = 2 * 86400 << 32 h.TransmitTime = 2 * 86400 << 32 r = generateResponse(&h, 2*86400<<32, nil) assertInvalid(t, r) // Fresh clock (24h) h.ReferenceTime = 1 * 86400 << 32 r = generateResponse(&h, 2*86400<<32, nil) assertValid(t, r) // Values indicating a negative RTT h.RootDelay = 16 << 16 h.ReferenceTime = 1 << 32 h.OriginTime = 20 << 32 h.ReceiveTime = 10 << 32 h.TransmitTime = 15 << 32 r = generateResponse(&h, 22<<32, nil) assert.NotNil(t, r) assertValid(t, r) assert.Equal(t, r.RTT, 0*time.Second) assert.Equal(t, r.RootDistance, 8*time.Second) }