├── .github └── workflows │ └── go.yml ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── auth.go ├── auth_test.go ├── go.mod ├── go.sum ├── ntp.go └── ntp_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: ["go"] 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 30 | with: 31 | languages: ${{ matrix.language }} 32 | 33 | - name: Autobuild 34 | uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 35 | 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 38 | with: 39 | category: "/language:${{matrix.language}}" 40 | 41 | build: 42 | runs-on: ubuntu-latest 43 | 44 | strategy: 45 | matrix: 46 | go-version: [ '1.17', '1.21', '1.22.x' ] 47 | 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 51 | 52 | - name: Setup Go ${{ matrix.go-version }} 53 | uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 54 | with: 55 | go-version: ${{ matrix.go-version }} 56 | 57 | - name: Build 58 | run: go build -v ./... 59 | 60 | - name: Test 61 | run: go test -run 'TestOffline.*' -v ./... 62 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Brett Vickers (beevik) 2 | Mikhail Salosin (AlphaB) 3 | Anton Tolchanov (knyar) 4 | Christopher Batey (chbatey) 5 | Meng Zhuo (mengzhuo) 6 | Leonid Evdokimov (darkk) 7 | Ask Bjørn Hansen (abh) 8 | Al Cutter (AlCutter) 9 | Silves-Xiang (silves-xiang) 10 | Andrey Smirnov (smira) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2015-2023 Brett Vickers. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY 15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 17 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR 18 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/beevik/ntp?status.svg)](https://godoc.org/github.com/beevik/ntp) 2 | [![Go](https://github.com/beevik/ntp/actions/workflows/go.yml/badge.svg)](https://github.com/beevik/ntp/actions/workflows/go.yml) 3 | 4 | ntp 5 | === 6 | 7 | The ntp package is an implementation of a Simple NTP (SNTP) client based on 8 | [RFC 5905](https://tools.ietf.org/html/rfc5905). It allows you to connect to 9 | a remote NTP server and request information about the current time. 10 | 11 | 12 | ## Querying the current time 13 | 14 | If all you care about is the current time according to a remote NTP server, 15 | simply use the `Time` function: 16 | ```go 17 | time, err := ntp.Time("0.beevik-ntp.pool.ntp.org") 18 | ``` 19 | 20 | 21 | ## Querying time synchronization data 22 | 23 | To obtain the current time as well as some additional synchronization data, 24 | use the [`Query`](https://godoc.org/github.com/beevik/ntp#Query) function: 25 | ```go 26 | response, err := ntp.Query("0.beevik-ntp.pool.ntp.org") 27 | time := time.Now().Add(response.ClockOffset) 28 | ``` 29 | 30 | The [`Response`](https://godoc.org/github.com/beevik/ntp#Response) structure 31 | returned by `Query` includes the following information: 32 | * `ClockOffset`: The estimated offset of the local system clock relative to 33 | the server's clock. For a more accurate time reading, you may add this 34 | offset to any subsequent system clock reading. 35 | * `Time`: The time the server transmitted its response, according to its own 36 | clock. 37 | * `RTT`: An estimate of the round-trip-time delay between the client and the 38 | server. 39 | * `Precision`: The precision of the server's clock reading. 40 | * `Stratum`: The server's stratum, which indicates the number of hops from the 41 | server to the reference clock. A stratum 1 server is directly attached to 42 | the reference clock. If the stratum is zero, the server has responded with 43 | the "kiss of death" and you should examine the `KissCode`. 44 | * `ReferenceID`: A unique identifier for the consulted reference clock. 45 | * `ReferenceTime`: The time at which the server last updated its local clock setting. 46 | * `RootDelay`: The server's aggregate round-trip-time delay to the stratum 1 server. 47 | * `RootDispersion`: The server's estimated maximum measurement error relative 48 | to the reference clock. 49 | * `RootDistance`: An estimate of the root synchronization distance between the 50 | client and the stratum 1 server. 51 | * `Leap`: The leap second indicator, indicating whether a second should be 52 | added to or removed from the current month's last minute. 53 | * `MinError`: A lower bound on the clock error between the client and the 54 | server. 55 | * `KissCode`: A 4-character string describing the reason for a "kiss of death" 56 | response (stratum=0). 57 | * `Poll`: The maximum polling interval between successive messages to the 58 | server. 59 | 60 | The `Response` structure's [`Validate`](https://godoc.org/github.com/beevik/ntp#Response.Validate) 61 | function performs additional sanity checks to determine whether the response 62 | is suitable for time synchronization purposes. 63 | ```go 64 | err := response.Validate() 65 | if err == nil { 66 | // response data is suitable for synchronization purposes 67 | } 68 | ``` 69 | 70 | If you wish to customize the behavior of the NTP query, use the 71 | [`QueryWithOptions`](https://godoc.org/github.com/beevik/ntp#QueryWithOptions) 72 | function: 73 | ```go 74 | options := ntp.QueryOptions{ Timeout: 30*time.Second, TTL: 5 } 75 | response, err := ntp.QueryWithOptions("0.beevik-ntp.pool.ntp.org", options) 76 | time := time.Now().Add(response.ClockOffset) 77 | ``` 78 | 79 | Configurable [`QueryOptions`](https://godoc.org/github.com/beevik/ntp#QueryOptions) 80 | include: 81 | * `Timeout`: How long to wait before giving up on a response from the NTP 82 | server. 83 | * `Version`: Which version of the NTP protocol to use (2, 3 or 4). 84 | * `TTL`: The maximum number of IP hops before the request packet is discarded. 85 | * `Auth`: The symmetric authentication key and algorithm used by the server to 86 | authenticate the query. The same information is used by the client to 87 | authenticate the server's response. 88 | * `Extensions`: Extensions may be added to modify NTP queries before they are 89 | transmitted and to process NTP responses after they arrive. 90 | * `Dialer`: A custom network connection "dialer" function used to override the 91 | default UDP dialer function. 92 | 93 | 94 | ## Using the NTP pool 95 | 96 | The NTP pool is a shared resource provided by the [NTP Pool 97 | Project](https://www.pool.ntp.org/en/) and used by people and services all 98 | over the world. To prevent it from becoming overloaded, please avoid querying 99 | the standard `pool.ntp.org` zone names in your applications. Instead, consider 100 | requesting your own [vendor zone](http://www.pool.ntp.org/en/vendors.html) or 101 | [joining the pool](http://www.pool.ntp.org/join.html). 102 | 103 | 104 | ## Network Time Security (NTS) 105 | 106 | Network Time Security (NTS) is a recent enhancement of NTP, designed to add 107 | better authentication and message integrity to the protocol. It is defined by 108 | [RFC 8915](https://tools.ietf.org/html/rfc8915). If you wish to use NTS, see 109 | the [nts package](https://github.com/beevik/nts). (The nts package is 110 | implemented as an extension to this package.) 111 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | Release v1.4.3 2 | ============== 3 | 4 | **Fixes** 5 | 6 | * Fixed an overflow bug in the clock offset calculation introduced by 7 | release v1.4.2. 8 | 9 | Release v1.4.2 10 | ============== 11 | 12 | **Fixes** 13 | 14 | * Fixed a bug in clock offset calculation. 15 | 16 | Release v1.4.1 17 | ============== 18 | 19 | **Updates** 20 | 21 | * Upgraded package dependencies to retrieve security fixes. 22 | 23 | Release v1.4.0 24 | ============== 25 | 26 | **Changes** 27 | 28 | * Added a protocol `Version` field to the `Response` struct. 29 | 30 | Release v1.3.1 31 | ============== 32 | 33 | **Changes** 34 | 35 | * Added AES-256-CMAC support for symmetric authentication. 36 | * Symmetric auth keys may now be specified as ASCII or HEX using the "ASCII:" 37 | or "HEX:" prefixes. 38 | * Updated dependencies to address security issues. 39 | 40 | **Fixes** 41 | 42 | * Added proper handling of the empty string when used as a server address. 43 | 44 | Release v1.3.0 45 | ============== 46 | 47 | **Changes** 48 | 49 | * Added the `ReferenceString` function to `Response`. This generates a 50 | stratum-specific string for the `ReferenceID` value. 51 | * Optimized the AES CMAC calculation for 64-bit architectures. 52 | 53 | **Fixes** 54 | 55 | * Fixed a bug introduced in release v1.2.0 that was causing IPv6 addresses 56 | to be interpreted incorrectly. 57 | 58 | Release v1.2.0 59 | ============== 60 | 61 | **Changes** 62 | 63 | * Added support for NTP extensions by exposing an extension interface. 64 | Extensions are able to (1) modify NTP messages before being sent to 65 | the server, and (2) process NTP messages after they arrive from the 66 | server. This feature has been added in preparation for NTS support. 67 | * Added support for RFC 5905 symmetric key authentication. 68 | * Allowed server address to be specified as a "host:port" pair. 69 | * Brought package into further compliance with IETF draft on client data 70 | minimization. 71 | * Declared error variables as part of the public API. 72 | * Added a `Dialer` field to `QueryOptions`. This replaces the deprecated 73 | `Dial` field. 74 | * Added an `IsKissOfDeath` function to the `Response` type. 75 | 76 | **Deprecated** 77 | 78 | * Deprecated the `Port` field in QueryOptions. 79 | * Deprecated the `Dial` field in QueryOptions. 80 | 81 | Release v1.1.1 82 | ============== 83 | 84 | **Fixes** 85 | 86 | * Fixed a missing indirect go module dependency. 87 | 88 | Release v1.1.0 89 | ============== 90 | 91 | **Changes** 92 | 93 | * Added the `Dial` property to the `QueryOptions` struct. This allows the user 94 | to override the default UDP dialer when setting up a connection to a remote 95 | NTP server. 96 | 97 | Release v1.0.0 98 | ============== 99 | 100 | This package has been stable for several years with no bug reports in that 101 | time. It is also pretty much feature complete. I am therefore updating the 102 | version to 1.0.0. 103 | 104 | Because this is a major release, all previously deprecated code has been 105 | removed from the package. 106 | 107 | **Breaking changes** 108 | 109 | * Removed the `TimeV` function. Use `Time` or `QueryWithOptions` instead. 110 | 111 | Release v0.3.2 112 | ============== 113 | 114 | **Changes** 115 | 116 | * Rename unit tests to enable easier test filtering. 117 | 118 | Release v0.3.0 119 | ============== 120 | 121 | There have been no breaking changes or further deprecations since the 122 | previous release. 123 | 124 | **Changes** 125 | 126 | * Fixed a bug in the calculation of NTP timestamps. 127 | 128 | Release v0.2.0 129 | ============== 130 | 131 | There are no breaking changes or further deprecations in this release. 132 | 133 | **Changes** 134 | 135 | * Added `KissCode` to the `Response` structure. 136 | 137 | 138 | Release v0.1.1 139 | ============== 140 | 141 | **Breaking changes** 142 | 143 | * Removed the `MaxStratum` constant. 144 | 145 | **Deprecations** 146 | 147 | * Officially deprecated the `TimeV` function. 148 | 149 | **Internal changes** 150 | 151 | * Removed `minDispersion` from the `RootDistance` calculation, since the value 152 | was arbitrary. 153 | * Moved some validation into main code path so that invalid `TransmitTime` and 154 | `mode` responses trigger an error even when `Response.Validate` is not 155 | called. 156 | 157 | 158 | Release v0.1.0 159 | ============== 160 | 161 | This is the initial release of the `ntp` package. Currently it supports the 162 | following features: 163 | * `Time()` to query the current time according to a remote NTP server. 164 | * `Query()` to query multiple pieces of time-related information from a remote 165 | NTP server. 166 | * `QueryWithOptions()`, which is like `Query()` but with the ability to 167 | override default query options. 168 | 169 | Time-related information returned by the `Query` functions includes: 170 | * `Time`: the time the server transmitted its response, according to the 171 | server's clock. 172 | * `ClockOffset`: the estimated offset of the client's clock relative to the 173 | server's clock. You may apply this offset to any local system clock reading 174 | once the query is complete. 175 | * `RTT`: an estimate of the round-trip-time delay between the client and the 176 | server. 177 | * `Precision`: the precision of the server's clock reading. 178 | * `Stratum`: the "stratum" level of the server, where 1 indicates a server 179 | directly connected to a reference clock, and values greater than 1 180 | indicating the number of hops from the reference clock. 181 | * `ReferenceID`: A unique identifier for the NTP server that was contacted. 182 | * `ReferenceTime`: The time at which the server last updated its local clock 183 | setting. 184 | * `RootDelay`: The server's round-trip delay to the reference clock. 185 | * `RootDispersion`: The server's total dispersion to the referenced clock. 186 | * `RootDistance`: An estimate of the root synchronization distance. 187 | * `Leap`: The leap second indicator. 188 | * `MinError`: A lower bound on the clock error between the client and the 189 | server. 190 | * `Poll`: the maximum polling interval between successive messages on the 191 | server. 192 | 193 | The `Response` structure returned by the `Query` functions also contains a 194 | `Response.Validate()` function that returns an error if any of the fields 195 | returned by the server are invalid. 196 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015-2023 Brett Vickers. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ntp 6 | 7 | import ( 8 | "bytes" 9 | "crypto/aes" 10 | "crypto/md5" 11 | "crypto/sha1" 12 | "crypto/sha256" 13 | "crypto/sha512" 14 | "crypto/subtle" 15 | "encoding/binary" 16 | "encoding/hex" 17 | ) 18 | 19 | // AuthType specifies the cryptographic hash algorithm used to generate a 20 | // symmetric key authentication digest (or CMAC) for an NTP message. Please 21 | // note that MD5 and SHA1 are no longer considered secure; they appear here 22 | // solely for compatibility with existing NTP server implementations. 23 | type AuthType int 24 | 25 | const ( 26 | AuthNone AuthType = iota // no authentication 27 | AuthMD5 // MD5 digest 28 | AuthSHA1 // SHA-1 digest 29 | AuthSHA256 // SHA-2 digest (256 bits) 30 | AuthSHA512 // SHA-2 digest (512 bits) 31 | AuthAES128 // AES-128-CMAC 32 | AuthAES256 // AES-256-CMAC 33 | ) 34 | 35 | // AuthOptions contains fields used to configure symmetric key authentication 36 | // for an NTP query. 37 | type AuthOptions struct { 38 | // Type determines the cryptographic hash algorithm used to compute the 39 | // authentication digest or CMAC. 40 | Type AuthType 41 | 42 | // The cryptographic key used by the client to perform authentication. The 43 | // key may be hex-encoded or ascii-encoded. To use a hex-encoded key, 44 | // prefix it by "HEX:". To use an ascii-encoded key, prefix it by 45 | // "ASCII:". For example, "HEX:6931564b4a5a5045766c55356b30656c7666316c" 46 | // or "ASCII:cvuZyN4C8HX8hNcAWDWp". 47 | Key string 48 | 49 | // The identifier used by the NTP server to identify which key to use 50 | // for authentication purposes. 51 | KeyID uint16 52 | } 53 | 54 | var algorithms = []struct { 55 | MinKeySize int 56 | MaxKeySize int 57 | DigestSize int 58 | CalcDigest func(payload, key []byte) []byte 59 | }{ 60 | {0, 0, 0, nil}, // AuthNone 61 | {4, 32, 16, calcDigest_MD5}, // AuthMD5 62 | {4, 32, 20, calcDigest_SHA1}, // AuthSHA1 63 | {4, 32, 20, calcDigest_SHA256}, // AuthSHA256 64 | {4, 32, 20, calcDigest_SHA512}, // AuthSHA512 65 | {16, 16, 16, calcCMAC_AES}, // AuthAES128 66 | {32, 32, 16, calcCMAC_AES}, // AuthAES256 67 | } 68 | 69 | func calcDigest_MD5(payload, key []byte) []byte { 70 | digest := md5.Sum(append(key, payload...)) 71 | return digest[:] 72 | } 73 | 74 | func calcDigest_SHA1(payload, key []byte) []byte { 75 | digest := sha1.Sum(append(key, payload...)) 76 | return digest[:] 77 | } 78 | 79 | func calcDigest_SHA256(payload, key []byte) []byte { 80 | digest := sha256.Sum256(append(key, payload...)) 81 | return digest[:20] 82 | } 83 | 84 | func calcDigest_SHA512(payload, key []byte) []byte { 85 | digest := sha512.Sum512(append(key, payload...)) 86 | return digest[:20] 87 | } 88 | 89 | func calcCMAC_AES(payload, key []byte) []byte { 90 | // calculate the CMAC according to the algorithm defined in RFC 4493. See 91 | // https://tools.ietf.org/html/rfc4493 for details. 92 | c, err := aes.NewCipher(key) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | // Generate subkeys. 98 | const rb = 0x87 99 | k1 := make([]byte, 16) 100 | k2 := make([]byte, 16) 101 | c.Encrypt(k1, k1) 102 | double(k1, k1, rb) 103 | double(k2, k1, rb) 104 | 105 | // Process all but the last block. 106 | cmac := make([]byte, 16) 107 | for ; len(payload) > 16; payload = payload[16:] { 108 | xor(cmac, payload[:16]) 109 | c.Encrypt(cmac, cmac) 110 | } 111 | 112 | // Process the last block, padding as necessary. 113 | if len(payload) == 16 { 114 | xor(cmac, payload) 115 | xor(cmac, k1) 116 | } else { 117 | xor(cmac, pad(payload)) 118 | xor(cmac, k2) 119 | } 120 | c.Encrypt(cmac, cmac) 121 | 122 | return cmac 123 | } 124 | 125 | func pad(block []byte) []byte { 126 | pad := make([]byte, 16-len(block)) 127 | pad[0] = 0x80 128 | return append(block, pad...) 129 | } 130 | 131 | func double(dst, src []byte, xor int) { 132 | _ = src[15] // compiler hint: bounds check 133 | s0 := binary.BigEndian.Uint64(src[0:8]) 134 | s1 := binary.BigEndian.Uint64(src[8:16]) 135 | 136 | carry := int(s0 >> 63) 137 | d0 := (s0 << 1) | (s1 >> 63) 138 | d1 := (s1 << 1) ^ uint64(subtle.ConstantTimeSelect(carry, xor, 0)) 139 | 140 | _ = dst[15] // compiler hint: bounds check 141 | binary.BigEndian.PutUint64(dst[0:8], d0) 142 | binary.BigEndian.PutUint64(dst[8:16], d1) 143 | } 144 | 145 | func xor(dst, src []byte) { 146 | _ = src[15] // compiler hint: bounds check 147 | s0 := binary.BigEndian.Uint64(src[0:8]) 148 | s1 := binary.BigEndian.Uint64(src[8:16]) 149 | 150 | _ = dst[15] // compiler hint: bounds check 151 | d0 := s0 ^ binary.BigEndian.Uint64(dst[0:8]) 152 | d1 := s1 ^ binary.BigEndian.Uint64(dst[8:16]) 153 | 154 | binary.BigEndian.PutUint64(dst[0:8], d0) 155 | binary.BigEndian.PutUint64(dst[8:16], d1) 156 | } 157 | 158 | func decodeAuthKey(opt AuthOptions) (key []byte, err error) { 159 | if opt.Type == AuthNone { 160 | return nil, nil 161 | } 162 | 163 | var keyIn string 164 | var isHex bool 165 | switch { 166 | case len(opt.Key) >= 4 && opt.Key[:4] == "HEX:": 167 | isHex, keyIn = true, opt.Key[4:] 168 | case len(opt.Key) >= 6 && opt.Key[:6] == "ASCII:": 169 | isHex, keyIn = false, opt.Key[6:] 170 | case len(opt.Key) > 20: 171 | isHex, keyIn = true, opt.Key 172 | default: 173 | isHex, keyIn = false, opt.Key 174 | } 175 | 176 | if isHex { 177 | key, err = hex.DecodeString(keyIn) 178 | if err != nil { 179 | return nil, ErrInvalidAuthKey 180 | } 181 | } else { 182 | key = []byte(keyIn) 183 | } 184 | 185 | a := algorithms[opt.Type] 186 | if len(key) < a.MinKeySize { 187 | return nil, ErrInvalidAuthKey 188 | } 189 | if len(key) > a.MaxKeySize { 190 | key = key[:a.MaxKeySize] 191 | } 192 | 193 | return key, nil 194 | } 195 | 196 | func appendMAC(buf *bytes.Buffer, opt AuthOptions, key []byte) { 197 | if opt.Type == AuthNone { 198 | return 199 | } 200 | 201 | a := algorithms[opt.Type] 202 | payload := buf.Bytes() 203 | digest := a.CalcDigest(payload, key) 204 | binary.Write(buf, binary.BigEndian, uint32(opt.KeyID)) 205 | binary.Write(buf, binary.BigEndian, digest) 206 | } 207 | 208 | func verifyMAC(buf []byte, opt AuthOptions, key []byte) error { 209 | if opt.Type == AuthNone { 210 | return nil 211 | } 212 | 213 | // Validate that there are enough bytes at the end of the message to 214 | // contain a MAC. 215 | const headerSize = 48 216 | a := algorithms[opt.Type] 217 | macLen := 4 + a.DigestSize 218 | remain := len(buf) - headerSize 219 | if remain < macLen || (remain%4) != 0 { 220 | return ErrAuthFailed 221 | } 222 | 223 | // The key ID returned by the server must be the same as the key ID sent 224 | // to the server. 225 | payloadLen := len(buf) - macLen 226 | mac := buf[payloadLen:] 227 | keyID := binary.BigEndian.Uint32(mac[:4]) 228 | if keyID != uint32(opt.KeyID) { 229 | return ErrAuthFailed 230 | } 231 | 232 | // Calculate and compare digests. 233 | payload := buf[:payloadLen] 234 | digest := a.CalcDigest(payload, key) 235 | if subtle.ConstantTimeCompare(digest, mac[4:]) != 1 { 236 | return ErrAuthFailed 237 | } 238 | 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015-2023 Brett Vickers. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ntp 6 | 7 | import ( 8 | "bytes" 9 | "encoding/hex" 10 | "errors" 11 | "os" 12 | "strings" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestOnlineAuthenticatedQuery(t *testing.T) { 18 | // By default, this unit test is skipped, because it requires a local NTP 19 | // server to be running and configured with known symmetric authentication 20 | // keys. 21 | // 22 | // To run this test, you must execute it with "-args test_auth". For 23 | // example: 24 | // 25 | // go test -v -run TestOnlineAuthenticatedQuery -args test_auth 26 | // 27 | // You must also run a local NTP server configured with the following 28 | // trusted symmetric keys (shown in chrony.keys format): 29 | // 30 | // 1 MD5 ASCII:cvuZyN4C8HX8hNcAWDWp 31 | // 2 SHA1 HEX:6931564b4a5a5045766c55356b30656c7666316c 32 | // 3 SHA256 HEX:7133736e777057764256777739706a5533326164 33 | // 4 SHA512 HEX:597675555446585868494d447543425971526e74 34 | // 5 AES128 HEX:68663033736f77706568707164304049 35 | // 6 AES256 HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a 36 | 37 | skip := true 38 | for _, arg := range os.Args { 39 | if arg == "test_auth" { 40 | skip = false 41 | } 42 | } 43 | if skip { 44 | t.Skip("Skipping authentication tests. Enable with -args test_auth") 45 | return 46 | } 47 | 48 | var errAuthFail = errors.New("timeout") 49 | 50 | cases := []struct { 51 | Type AuthType 52 | Key string 53 | KeyID uint16 54 | ExpectedErr error 55 | }{ 56 | // KeyID 1 (MD5) 57 | {AuthMD5, "cvuZyN4C8HX8hNcAWDWp", 1, nil}, 58 | {AuthMD5, "ASCII:cvuZyN4C8HX8hNcAWDWp", 1, nil}, 59 | {AuthMD5, "6376755a794e344338485838684e634157445770", 1, nil}, 60 | {AuthMD5, "HEX:6376755a794e344338485838684e634157445770", 1, nil}, 61 | {AuthMD5, "", 1, ErrInvalidAuthKey}, 62 | {AuthMD5, "HEX:6376755a794e344338485838684e63415744577", 1, ErrInvalidAuthKey}, 63 | {AuthMD5, "HEX:6376755a794e344338485838684e63415744577g", 1, ErrInvalidAuthKey}, 64 | {AuthMD5, "ASCII:XvuZyN4C8HX8hNcAWDWp", 1, errAuthFail}, 65 | {AuthMD5, "ASCII:cvuZyN4C8HX8hNcAWDWp", 2, errAuthFail}, 66 | {AuthSHA1, "ASCII:cvuZyN4C8HX8hNcAWDWp", 1, errAuthFail}, 67 | 68 | // KeyID 2 (SHA1) 69 | {AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, nil}, 70 | {AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, nil}, 71 | {AuthSHA1, "ASCII:i1VKJZPEvlU5k0elvf1l", 2, nil}, 72 | {AuthSHA1, "ASCII:i1VKJZPEvlU5k0elvf1l", 2, nil}, 73 | {AuthSHA1, "", 2, ErrInvalidAuthKey}, 74 | {AuthSHA1, "HEX:0031564b4a5a5045766c55356b30656c7666316c", 2, errAuthFail}, 75 | {AuthSHA1, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 1, errAuthFail}, 76 | {AuthMD5, "HEX:6931564b4a5a5045766c55356b30656c7666316c", 2, errAuthFail}, 77 | 78 | // KeyID 3 (SHA256) 79 | {AuthSHA256, "HEX:7133736e777057764256777739706a5533326164", 3, nil}, 80 | {AuthSHA256, "ASCII:q3snwpWvBVww9pjU32ad", 3, nil}, 81 | {AuthSHA256, "", 3, ErrInvalidAuthKey}, 82 | {AuthSHA256, "HEX:0033736e777057764256777739706a5533326164", 3, errAuthFail}, 83 | {AuthSHA256, "HEX:7133736e777057764256777739706a5533326164", 2, errAuthFail}, 84 | {AuthSHA1, "HEX:7133736e777057764256777739706a5533326164", 3, errAuthFail}, 85 | 86 | // // KeyID 4 (SHA512) 87 | {AuthSHA512, "HEX:597675555446585868494d447543425971526e74", 4, nil}, 88 | {AuthSHA512, "ASCII:YvuUTFXXhIMDuCBYqRnt", 4, nil}, 89 | {AuthSHA512, "", 4, ErrInvalidAuthKey}, 90 | {AuthSHA512, "HEX:007675555446585868494d447543425971526e74", 4, errAuthFail}, 91 | {AuthSHA512, "HEX:597675555446585868494d447543425971526e74", 3, errAuthFail}, 92 | {AuthSHA256, "HEX:597675555446585868494d447543425971526e74", 4, errAuthFail}, 93 | 94 | // KeyID 5 (AES128) 95 | {AuthAES128, "HEX:68663033736f77706568707164304049", 5, nil}, 96 | {AuthAES128, "HEX:68663033736f77706568707164304049fefefefe", 5, nil}, 97 | {AuthAES128, "ASCII:hf03sowpehpqd0@I", 5, nil}, 98 | {AuthAES128, "", 5, ErrInvalidAuthKey}, 99 | {AuthAES128, "HEX:00663033736f77706568707164304049", 5, errAuthFail}, 100 | {AuthAES128, "HEX:68663033736f77706568707164304049", 4, errAuthFail}, 101 | {AuthMD5, "HEX:68663033736f77706568707164304049", 5, errAuthFail}, 102 | 103 | // KeyID 6 (AES256) 104 | {AuthAES256, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, nil}, 105 | {AuthAES256, "", 6, ErrInvalidAuthKey}, 106 | {AuthAES256, "HEX:00cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, errAuthFail}, 107 | {AuthAES256, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 5, errAuthFail}, 108 | {AuthMD5, "HEX:47cb76a9a507cf26dc00eb0935f082f390f10308c3e0d58716273a63259a758a", 6, errAuthFail}, 109 | } 110 | 111 | for i, c := range cases { 112 | opt := QueryOptions{ 113 | Timeout: 250 * time.Millisecond, 114 | Auth: AuthOptions{c.Type, c.Key, c.KeyID}, 115 | } 116 | r, err := QueryWithOptions(host, opt) 117 | if c.ExpectedErr == errAuthFail { 118 | // With old NTP servers, failed authentication leads to Crypto-NAK 119 | // (ErrAuthFailed). With modern NTP servers, it leads to an I/O 120 | // timeout error. 121 | if err != ErrAuthFailed && !strings.Contains(err.Error(), "timeout") { 122 | t.Errorf("case %d: expected error [%v], got error [%v]\n", i, c.ExpectedErr, err) 123 | } 124 | continue 125 | } 126 | if c.ExpectedErr != nil && c.ExpectedErr == err { 127 | continue 128 | } 129 | if err == nil { 130 | err = r.Validate() 131 | if err != c.ExpectedErr { 132 | t.Errorf("case %d: expected error [%v], got error [%v]\n", i, c.ExpectedErr, err) 133 | } 134 | } 135 | } 136 | } 137 | 138 | func TestOfflineAesCmac(t *testing.T) { 139 | // Test cases taken from NIST document: 140 | // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf 141 | const ( 142 | Key128 = "2b7e1516 28aed2a6 abf71588 09cf4f3c" 143 | Key192 = "8e73b0f7 da0e6452 c810f32b 809079e5 62f8ead2 522c6b7b" 144 | Key256 = "603deb10 15ca71be 2b73aef0 857d7781 1f352c07 3b6108d7 2d9810a3 0914dff4" 145 | ) 146 | 147 | const ( 148 | Msg1 = "" 149 | Msg2 = "6bc1bee2 2e409f96 e93d7e11 7393172a" 150 | Msg3 = "6bc1bee2 2e409f96 e93d7e11 7393172a ae2d8a57" 151 | Msg4 = "6bc1bee2 2e409f96 e93d7e11 7393172a ae2d8a57 1e03ac9c 9eb76fac 45af8e51" + 152 | "30c81c46 a35ce411 e5fbc119 1a0a52ef f69f2445 df4f9b17 ad2b417b e66c3710" 153 | ) 154 | 155 | cases := []struct { 156 | key string 157 | plaintext string 158 | cmac string 159 | }{ 160 | // 128-bit key 161 | {Key128, Msg1, "bb1d6929 e9593728 7fa37d12 9b756746"}, 162 | {Key128, Msg2, "070a16b4 6b4d4144 f79bdd9d d04a287c"}, 163 | {Key128, Msg3, "7d85449e a6ea19c8 23a7bf78 837dfade"}, 164 | {Key128, Msg4, "51f0bebf 7e3b9d92 fc497417 79363cfe"}, 165 | 166 | // 192-bit key 167 | {Key192, Msg1, "d17ddf46 adaacde5 31cac483 de7a9367"}, 168 | {Key192, Msg2, "9e99a7bf 31e71090 0662f65e 617c5184"}, 169 | {Key192, Msg3, "3d75c194 ed960704 44a9fa7e c740ecf8"}, 170 | {Key192, Msg4, "a1d5df0e ed790f79 4d775896 59f39a11"}, 171 | 172 | // 256-bit key 173 | {Key256, Msg1, "028962f6 1b7bf89e fc6b551f 4667d983"}, 174 | {Key256, Msg2, "28a7023f 452e8f82 bd4bf28d 8c37c35c"}, 175 | {Key256, Msg3, "156727dc 0878944a 023c1fe0 3bad6d93"}, 176 | {Key256, Msg4, "e1992190 549f6ed5 696a2c05 6c315410"}, 177 | } 178 | 179 | for i, c := range cases { 180 | _ = i 181 | key, pt, cmac := hexDecode(c.key), hexDecode(c.plaintext), hexDecode(c.cmac) 182 | result := calcCMAC_AES(pt, key) 183 | if !bytes.Equal(cmac, result) { 184 | t.Errorf("case %d: CMACs do not match.\n", i) 185 | } 186 | } 187 | } 188 | 189 | func hexDecode(s string) []byte { 190 | s = strings.ReplaceAll(s, " ", "") 191 | b, err := hex.DecodeString(s) 192 | if err != nil { 193 | panic(err) 194 | } 195 | return b 196 | } 197 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/beevik/ntp 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | golang.org/x/net v0.25.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | golang.org/x/sys v0.20.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 13 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 14 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 16 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 17 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 18 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 19 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 20 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 21 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 22 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 23 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 24 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 25 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 26 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 27 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 28 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 29 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 30 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 42 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 43 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 44 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 45 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 46 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 47 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 48 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 51 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 52 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 53 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 54 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 55 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 58 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 59 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 60 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 65 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | -------------------------------------------------------------------------------- /ntp.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015-2023 Brett Vickers. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package ntp provides an implementation of a Simple NTP (SNTP) client 6 | // capable of querying the current time from a remote NTP server. See 7 | // RFC 5905 (https://tools.ietf.org/html/rfc5905) for more details. 8 | // 9 | // This approach grew out of a go-nuts post by Michael Hofmann: 10 | // https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/FlcdMU5fkLQ 11 | package ntp 12 | 13 | import ( 14 | "bytes" 15 | "crypto/rand" 16 | "encoding/binary" 17 | "errors" 18 | "fmt" 19 | "net" 20 | "strconv" 21 | "strings" 22 | "time" 23 | 24 | "golang.org/x/net/ipv4" 25 | ) 26 | 27 | var ( 28 | ErrAuthFailed = errors.New("authentication failed") 29 | ErrInvalidAuthKey = errors.New("invalid authentication key") 30 | ErrInvalidDispersion = errors.New("invalid dispersion in response") 31 | ErrInvalidLeapSecond = errors.New("invalid leap second in response") 32 | ErrInvalidMode = errors.New("invalid mode in response") 33 | ErrInvalidProtocolVersion = errors.New("invalid protocol version requested") 34 | ErrInvalidStratum = errors.New("invalid stratum in response") 35 | ErrInvalidTime = errors.New("invalid time reported") 36 | ErrInvalidTransmitTime = errors.New("invalid transmit time in response") 37 | ErrKissOfDeath = errors.New("kiss of death received") 38 | ErrServerClockFreshness = errors.New("server clock not fresh") 39 | ErrServerResponseMismatch = errors.New("server response didn't match request") 40 | ErrServerTickedBackwards = errors.New("server clock ticked backwards") 41 | ) 42 | 43 | // The LeapIndicator is used to warn if a leap second should be inserted 44 | // or deleted in the last minute of the current month. 45 | type LeapIndicator uint8 46 | 47 | const ( 48 | // LeapNoWarning indicates no impending leap second. 49 | LeapNoWarning LeapIndicator = 0 50 | 51 | // LeapAddSecond indicates the last minute of the day has 61 seconds. 52 | LeapAddSecond = 1 53 | 54 | // LeapDelSecond indicates the last minute of the day has 59 seconds. 55 | LeapDelSecond = 2 56 | 57 | // LeapNotInSync indicates an unsynchronized leap second. 58 | LeapNotInSync = 3 59 | ) 60 | 61 | // Internal constants 62 | const ( 63 | defaultNtpVersion = 4 64 | defaultNtpPort = 123 65 | nanoPerSec = 1000000000 66 | maxStratum = 16 67 | defaultTimeout = 5 * time.Second 68 | maxPollInterval = (1 << 17) * time.Second 69 | maxDispersion = 16 * time.Second 70 | ) 71 | 72 | // Internal variables 73 | var ( 74 | ntpEra0 = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC) 75 | ntpEra1 = time.Date(2036, 2, 7, 6, 28, 16, 0, time.UTC) 76 | ) 77 | 78 | type mode uint8 79 | 80 | // NTP modes. This package uses only client mode. 81 | const ( 82 | reserved mode = 0 + iota 83 | symmetricActive 84 | symmetricPassive 85 | client 86 | server 87 | broadcast 88 | controlMessage 89 | reservedPrivate 90 | ) 91 | 92 | // An ntpTime is a 64-bit fixed-point (Q32.32) representation of the number of 93 | // seconds elapsed. 94 | type ntpTime uint64 95 | 96 | // Duration interprets the fixed-point ntpTime as a number of elapsed seconds 97 | // and returns the corresponding time.Duration value. 98 | func (t ntpTime) Duration() time.Duration { 99 | sec := (t >> 32) * nanoPerSec 100 | frac := (t & 0xffffffff) * nanoPerSec 101 | nsec := frac >> 32 102 | if uint32(frac) >= 0x80000000 { 103 | nsec++ 104 | } 105 | return time.Duration(sec + nsec) 106 | } 107 | 108 | // Time interprets the fixed-point ntpTime as an absolute time and returns 109 | // the corresponding time.Time value. 110 | func (t ntpTime) Time() time.Time { 111 | // Assume NTP era 1 (year 2036+) if the raw timestamp suggests a year 112 | // before 1970. Otherwise assume NTP era 0. This allows the function to 113 | // report an accurate time value both before and after the 0-to-1 era 114 | // rollover. 115 | const t1970 = 0x83aa7e8000000000 116 | if uint64(t) < t1970 { 117 | return ntpEra1.Add(t.Duration()) 118 | } 119 | return ntpEra0.Add(t.Duration()) 120 | } 121 | 122 | // toNtpTime converts the time.Time value t into its 64-bit fixed-point 123 | // ntpTime representation. 124 | func toNtpTime(t time.Time) ntpTime { 125 | nsec := uint64(t.Sub(ntpEra0)) 126 | sec := nsec / nanoPerSec 127 | nsec = uint64(nsec-sec*nanoPerSec) << 32 128 | frac := uint64(nsec / nanoPerSec) 129 | if nsec%nanoPerSec >= nanoPerSec/2 { 130 | frac++ 131 | } 132 | return ntpTime(sec<<32 | frac) 133 | } 134 | 135 | // An ntpTimeShort is a 32-bit fixed-point (Q16.16) representation of the 136 | // number of seconds elapsed. 137 | type ntpTimeShort uint32 138 | 139 | // Duration interprets the fixed-point ntpTimeShort as a number of elapsed 140 | // seconds and returns the corresponding time.Duration value. 141 | func (t ntpTimeShort) Duration() time.Duration { 142 | sec := uint64(t>>16) * nanoPerSec 143 | frac := uint64(t&0xffff) * nanoPerSec 144 | nsec := frac >> 16 145 | if uint16(frac) >= 0x8000 { 146 | nsec++ 147 | } 148 | return time.Duration(sec + nsec) 149 | } 150 | 151 | // header is an internal representation of an NTP packet header. 152 | type header struct { 153 | LiVnMode uint8 // Leap Indicator (2) + Version (3) + Mode (3) 154 | Stratum uint8 155 | Poll int8 156 | Precision int8 157 | RootDelay ntpTimeShort 158 | RootDispersion ntpTimeShort 159 | ReferenceID uint32 // KoD code if Stratum == 0 160 | ReferenceTime ntpTime 161 | OriginTime ntpTime 162 | ReceiveTime ntpTime 163 | TransmitTime ntpTime 164 | } 165 | 166 | // setVersion sets the NTP protocol version on the header. 167 | func (h *header) setVersion(v int) { 168 | h.LiVnMode = (h.LiVnMode & 0xc7) | uint8(v)<<3 169 | } 170 | 171 | // setMode sets the NTP protocol mode on the header. 172 | func (h *header) setMode(md mode) { 173 | h.LiVnMode = (h.LiVnMode & 0xf8) | uint8(md) 174 | } 175 | 176 | // setLeap modifies the leap indicator on the header. 177 | func (h *header) setLeap(li LeapIndicator) { 178 | h.LiVnMode = (h.LiVnMode & 0x3f) | uint8(li)<<6 179 | } 180 | 181 | // getVersion returns the version value in the header. 182 | func (h *header) getVersion() int { 183 | return int((h.LiVnMode >> 3) & 0x7) 184 | } 185 | 186 | // getMode returns the mode value in the header. 187 | func (h *header) getMode() mode { 188 | return mode(h.LiVnMode & 0x07) 189 | } 190 | 191 | // getLeap returns the leap indicator on the header. 192 | func (h *header) getLeap() LeapIndicator { 193 | return LeapIndicator((h.LiVnMode >> 6) & 0x03) 194 | } 195 | 196 | // An Extension adds custom behaviors capable of modifying NTP packets before 197 | // being sent to the server and processing packets after being received by the 198 | // server. 199 | type Extension interface { 200 | // ProcessQuery is called when the client is about to send a query to the 201 | // NTP server. The buffer contains the NTP header. It may also contain 202 | // extension fields added by extensions processed prior to this one. 203 | ProcessQuery(buf *bytes.Buffer) error 204 | 205 | // ProcessResponse is called after the client has received the server's 206 | // NTP response. The buffer contains the entire message returned by the 207 | // server. 208 | ProcessResponse(buf []byte) error 209 | } 210 | 211 | // QueryOptions contains configurable options used by the QueryWithOptions 212 | // function. 213 | type QueryOptions struct { 214 | // Timeout determines how long the client waits for a response from the 215 | // server before failing with a timeout error. Defaults to 5 seconds. 216 | Timeout time.Duration 217 | 218 | // Version of the NTP protocol to use. Defaults to 4. 219 | Version int 220 | 221 | // LocalAddress contains the local IP address to use when creating a 222 | // connection to the remote NTP server. This may be useful when the local 223 | // system has more than one IP address. This address should not contain 224 | // a port number. 225 | LocalAddress string 226 | 227 | // TTL specifies the maximum number of IP hops before the query datagram 228 | // is dropped by the network. Defaults to the local system's default value. 229 | TTL int 230 | 231 | // Auth contains the settings used to configure NTP symmetric key 232 | // authentication. See RFC 5905 for further details. 233 | Auth AuthOptions 234 | 235 | // Extensions may be added to modify NTP queries before they are 236 | // transmitted and to process NTP responses after they arrive. 237 | Extensions []Extension 238 | 239 | // Dialer is a callback used to override the default UDP network dialer. 240 | // The localAddress is directly copied from the LocalAddress field 241 | // specified in QueryOptions. It may be the empty string or a host address 242 | // (without port number). The remoteAddress is the "host:port" string 243 | // derived from the first parameter to QueryWithOptions. The 244 | // remoteAddress is guaranteed to include a port number. 245 | Dialer func(localAddress, remoteAddress string) (net.Conn, error) 246 | 247 | // Dial is a callback used to override the default UDP network dialer. 248 | // 249 | // DEPRECATED. Use Dialer instead. 250 | Dial func(laddr string, lport int, raddr string, rport int) (net.Conn, error) 251 | 252 | // Port indicates the port used to reach the remote NTP server. 253 | // 254 | // DEPRECATED. Embed the port number in the query address string instead. 255 | Port int 256 | } 257 | 258 | // A Response contains time data, some of which is returned by the NTP server 259 | // and some of which is calculated by this client. 260 | type Response struct { 261 | // ClockOffset is the estimated offset of the local system clock relative 262 | // to the server's clock. Add this value to subsequent local system clock 263 | // times in order to obtain a time that is synchronized to the server's 264 | // clock. 265 | ClockOffset time.Duration 266 | 267 | // Time is the time the server transmitted this response, measured using 268 | // its own clock. You should not use this value for time synchronization 269 | // purposes. Add ClockOffset to your system clock instead. 270 | Time time.Time 271 | 272 | // RTT is the measured round-trip-time delay estimate between the client 273 | // and the server. 274 | RTT time.Duration 275 | 276 | // Precision is the reported precision of the server's clock. 277 | Precision time.Duration 278 | 279 | // Version is the NTP protocol version number reported by the server. 280 | Version int 281 | 282 | // Stratum is the "stratum level" of the server. The smaller the number, 283 | // the closer the server is to the reference clock. Stratum 1 servers are 284 | // attached directly to the reference clock. A stratum value of 0 285 | // indicates the "kiss of death," which typically occurs when the client 286 | // issues too many requests to the server in a short period of time. 287 | Stratum uint8 288 | 289 | // ReferenceID is a 32-bit integer identifying the server or reference 290 | // clock. For stratum 1 servers, this is typically a meaningful 291 | // zero-padded ASCII-encoded string assigned to the clock. For stratum 2+ 292 | // servers, this is a reference identifier for the server and is either 293 | // the server's IPv4 address or a hash of its IPv6 address. For 294 | // kiss-of-death responses (stratum 0), this is the ASCII-encoded "kiss 295 | // code". 296 | ReferenceID uint32 297 | 298 | // ReferenceTime is the time the server last updated its local clock. 299 | ReferenceTime time.Time 300 | 301 | // RootDelay is the server's estimated aggregate round-trip-time delay to 302 | // the stratum 1 server. 303 | RootDelay time.Duration 304 | 305 | // RootDispersion is the server's estimated maximum measurement error 306 | // relative to the stratum 1 server. 307 | RootDispersion time.Duration 308 | 309 | // RootDistance is an estimate of the total synchronization distance 310 | // between the client and the stratum 1 server. 311 | RootDistance time.Duration 312 | 313 | // Leap indicates whether a leap second should be added or removed from 314 | // the current month's last minute. 315 | Leap LeapIndicator 316 | 317 | // MinError is a lower bound on the error between the client and server 318 | // clocks. When the client and server are not synchronized to the same 319 | // clock, the reported timestamps may appear to violate the principle of 320 | // causality. In other words, the NTP server's response may indicate 321 | // that a message was received before it was sent. In such cases, the 322 | // minimum error may be useful. 323 | MinError time.Duration 324 | 325 | // KissCode is a 4-character string describing the reason for a 326 | // "kiss of death" response (stratum=0). For a list of standard kiss 327 | // codes, see https://tools.ietf.org/html/rfc5905#section-7.4. 328 | KissCode string 329 | 330 | // Poll is the maximum interval between successive NTP query messages to 331 | // the server. 332 | Poll time.Duration 333 | 334 | authErr error 335 | } 336 | 337 | // IsKissOfDeath returns true if the response is a "kiss of death" from the 338 | // remote server. If this function returns true, you may examine the 339 | // response's KissCode value to determine the reason for the kiss of death. 340 | func (r *Response) IsKissOfDeath() bool { 341 | return r.Stratum == 0 342 | } 343 | 344 | // ReferenceString returns the response's ReferenceID value formatted as a 345 | // string. If the response's stratum is zero, then the "kiss o' death" string 346 | // is returned. If stratum is one, then the server is a reference clock and 347 | // the reference clock's name is returned. If stratum is two or greater, then 348 | // the ID is either an IPv4 address or an MD5 hash of the IPv6 address; in 349 | // either case the reference string is reported as 4 dot-separated 350 | // decimal-based integers. 351 | func (r *Response) ReferenceString() string { 352 | if r.Stratum == 0 { 353 | return kissCode(r.ReferenceID) 354 | } 355 | 356 | var b [4]byte 357 | binary.BigEndian.PutUint32(b[:], r.ReferenceID) 358 | 359 | if r.Stratum == 1 { 360 | const dot = rune(0x22c5) 361 | var r []rune 362 | for i := range b { 363 | if b[i] == 0 { 364 | break 365 | } 366 | if b[i] >= 32 && b[i] <= 126 { 367 | r = append(r, rune(b[i])) 368 | } else { 369 | r = append(r, dot) 370 | } 371 | } 372 | return fmt.Sprintf(".%s.", string(r)) 373 | } 374 | 375 | return fmt.Sprintf("%d.%d.%d.%d", b[0], b[1], b[2], b[3]) 376 | } 377 | 378 | // Validate checks if the response is valid for the purposes of time 379 | // synchronization. 380 | func (r *Response) Validate() error { 381 | // Forward authentication errors. 382 | if r.authErr != nil { 383 | return r.authErr 384 | } 385 | 386 | // Handle invalid stratum values. 387 | if r.Stratum == 0 { 388 | return ErrKissOfDeath 389 | } 390 | if r.Stratum >= maxStratum { 391 | return ErrInvalidStratum 392 | } 393 | 394 | // Estimate the "freshness" of the time. If it exceeds the maximum 395 | // polling interval (~36 hours), then it cannot be considered "fresh". 396 | freshness := r.Time.Sub(r.ReferenceTime) 397 | if freshness > maxPollInterval { 398 | return ErrServerClockFreshness 399 | } 400 | 401 | // Calculate the peer synchronization distance, lambda: 402 | // lambda := RootDelay/2 + RootDispersion 403 | // If this value exceeds MAXDISP (16s), then the time is not suitable 404 | // for synchronization purposes. 405 | // https://tools.ietf.org/html/rfc5905#appendix-A.5.1.1. 406 | lambda := r.RootDelay/2 + r.RootDispersion 407 | if lambda > maxDispersion { 408 | return ErrInvalidDispersion 409 | } 410 | 411 | // If the server's transmit time is before its reference time, the 412 | // response is invalid. 413 | if r.Time.Before(r.ReferenceTime) { 414 | return ErrInvalidTime 415 | } 416 | 417 | // Handle invalid leap second indicator. 418 | if r.Leap == LeapNotInSync { 419 | return ErrInvalidLeapSecond 420 | } 421 | 422 | // nil means the response is valid. 423 | return nil 424 | } 425 | 426 | // Query requests time data from a remote NTP server. The response contains 427 | // information from which a more accurate local time can be inferred. 428 | // 429 | // The server address is of the form "host", "host:port", "host%zone:port", 430 | // "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or 431 | // domain name address. When specifying both a port and an IPv6 address, one 432 | // of the bracket formats must be used. If no port is included, NTP default 433 | // port 123 is used. 434 | func Query(address string) (*Response, error) { 435 | return QueryWithOptions(address, QueryOptions{}) 436 | } 437 | 438 | // QueryWithOptions performs the same function as Query but allows for the 439 | // customization of certain query behaviors. See the comments for Query and 440 | // QueryOptions for further details. 441 | func QueryWithOptions(address string, opt QueryOptions) (*Response, error) { 442 | h, now, err := getTime(address, &opt) 443 | if err != nil && err != ErrAuthFailed { 444 | return nil, err 445 | } 446 | 447 | return generateResponse(h, now, err), nil 448 | } 449 | 450 | // Time returns the current, corrected local time using information returned 451 | // from the remote NTP server. On error, Time returns the uncorrected local 452 | // system time. 453 | // 454 | // The server address is of the form "host", "host:port", "host%zone:port", 455 | // "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or 456 | // domain name address. When specifying both a port and an IPv6 address, one 457 | // of the bracket formats must be used. If no port is included, NTP default 458 | // port 123 is used. 459 | func Time(address string) (time.Time, error) { 460 | r, err := Query(address) 461 | if err != nil { 462 | return time.Now(), err 463 | } 464 | 465 | err = r.Validate() 466 | if err != nil { 467 | return time.Now(), err 468 | } 469 | 470 | // Use the response's clock offset to calculate an accurate time. 471 | return time.Now().Add(r.ClockOffset), nil 472 | } 473 | 474 | // getTime performs the NTP server query and returns the response header 475 | // along with the local system time it was received. 476 | func getTime(address string, opt *QueryOptions) (*header, ntpTime, error) { 477 | if opt.Timeout == 0 { 478 | opt.Timeout = defaultTimeout 479 | } 480 | if opt.Version == 0 { 481 | opt.Version = defaultNtpVersion 482 | } 483 | if opt.Version < 2 || opt.Version > 4 { 484 | return nil, 0, ErrInvalidProtocolVersion 485 | } 486 | if opt.Port == 0 { 487 | opt.Port = defaultNtpPort 488 | } 489 | if opt.Dial != nil { 490 | // wrapper for the deprecated Dial callback. 491 | opt.Dialer = func(la, ra string) (net.Conn, error) { 492 | return dialWrapper(la, ra, opt.Dial) 493 | } 494 | } 495 | if opt.Dialer == nil { 496 | opt.Dialer = defaultDialer 497 | } 498 | 499 | // Compose a conforming host:port remote address string if the address 500 | // string doesn't already contain a port. 501 | remoteAddress, err := fixHostPort(address, opt.Port) 502 | if err != nil { 503 | return nil, 0, err 504 | } 505 | 506 | // Connect to the remote server. 507 | con, err := opt.Dialer(opt.LocalAddress, remoteAddress) 508 | if err != nil { 509 | return nil, 0, err 510 | } 511 | defer con.Close() 512 | 513 | // Set a TTL for the packet if requested. 514 | if opt.TTL != 0 { 515 | ipcon := ipv4.NewConn(con) 516 | err = ipcon.SetTTL(opt.TTL) 517 | if err != nil { 518 | return nil, 0, err 519 | } 520 | } 521 | 522 | // Set a timeout on the connection. 523 | con.SetDeadline(time.Now().Add(opt.Timeout)) 524 | 525 | // Allocate a buffer big enough to hold an entire response datagram. 526 | recvBuf := make([]byte, 8192) 527 | recvHdr := new(header) 528 | 529 | // Allocate the query message header. 530 | xmitHdr := new(header) 531 | xmitHdr.setMode(client) 532 | xmitHdr.setVersion(opt.Version) 533 | xmitHdr.setLeap(LeapNoWarning) 534 | xmitHdr.Precision = 0x20 535 | 536 | // To help prevent spoofing and client fingerprinting, use a 537 | // cryptographically random 64-bit value for the TransmitTime. See: 538 | // https://www.ietf.org/archive/id/draft-ietf-ntp-data-minimization-04.txt 539 | bits := make([]byte, 8) 540 | _, err = rand.Read(bits) 541 | if err != nil { 542 | return nil, 0, err 543 | } 544 | xmitHdr.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits)) 545 | 546 | // Write the query header to a transmit buffer. 547 | var xmitBuf bytes.Buffer 548 | binary.Write(&xmitBuf, binary.BigEndian, xmitHdr) 549 | 550 | // Allow extensions to process the query and add to the transmit buffer. 551 | for _, e := range opt.Extensions { 552 | err = e.ProcessQuery(&xmitBuf) 553 | if err != nil { 554 | return nil, 0, err 555 | } 556 | } 557 | 558 | // If using symmetric key authentication, decode and validate the auth key 559 | // string. 560 | authKey, err := decodeAuthKey(opt.Auth) 561 | if err != nil { 562 | return nil, 0, err 563 | } 564 | 565 | // Append a MAC if authentication is being used. 566 | appendMAC(&xmitBuf, opt.Auth, authKey) 567 | 568 | // Transmit the query and keep track of when it was transmitted. 569 | xmitTime := time.Now() 570 | _, err = con.Write(xmitBuf.Bytes()) 571 | if err != nil { 572 | return nil, 0, err 573 | } 574 | 575 | // Receive the response. 576 | recvBytes, err := con.Read(recvBuf) 577 | if err != nil { 578 | return nil, 0, err 579 | } 580 | 581 | // Keep track of the time the response was received. As of go 1.9, the 582 | // time package uses a monotonic clock, so delta will never be less than 583 | // zero for go version 1.9 or higher. 584 | delta := time.Since(xmitTime) 585 | if delta < 0 { 586 | delta = 0 587 | } 588 | recvTime := xmitTime.Add(delta) 589 | 590 | // Parse the response header. 591 | recvBuf = recvBuf[:recvBytes] 592 | recvReader := bytes.NewReader(recvBuf) 593 | err = binary.Read(recvReader, binary.BigEndian, recvHdr) 594 | if err != nil { 595 | return nil, 0, err 596 | } 597 | 598 | // Allow extensions to process the response. 599 | for i := len(opt.Extensions) - 1; i >= 0; i-- { 600 | err = opt.Extensions[i].ProcessResponse(recvBuf) 601 | if err != nil { 602 | return nil, 0, err 603 | } 604 | } 605 | 606 | // Check for invalid fields. 607 | if recvHdr.getMode() != server { 608 | return nil, 0, ErrInvalidMode 609 | } 610 | if recvHdr.TransmitTime == ntpTime(0) { 611 | return nil, 0, ErrInvalidTransmitTime 612 | } 613 | if recvHdr.OriginTime != xmitHdr.TransmitTime { 614 | return nil, 0, ErrServerResponseMismatch 615 | } 616 | if recvHdr.ReceiveTime > recvHdr.TransmitTime { 617 | return nil, 0, ErrServerTickedBackwards 618 | } 619 | 620 | // Correct the received message's origin time using the actual 621 | // transmit time. 622 | recvHdr.OriginTime = toNtpTime(xmitTime) 623 | 624 | // Perform authentication of the server response. 625 | authErr := verifyMAC(recvBuf, opt.Auth, authKey) 626 | 627 | return recvHdr, toNtpTime(recvTime), authErr 628 | } 629 | 630 | // defaultDialer provides a UDP dialer based on Go's built-in net stack. 631 | func defaultDialer(localAddress, remoteAddress string) (net.Conn, error) { 632 | var laddr *net.UDPAddr 633 | if localAddress != "" { 634 | var err error 635 | laddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(localAddress, "0")) 636 | if err != nil { 637 | return nil, err 638 | } 639 | } 640 | 641 | raddr, err := net.ResolveUDPAddr("udp", remoteAddress) 642 | if err != nil { 643 | return nil, err 644 | } 645 | 646 | return net.DialUDP("udp", laddr, raddr) 647 | } 648 | 649 | // dialWrapper is used to wrap the deprecated Dial callback in QueryOptions. 650 | func dialWrapper(la, ra string, 651 | dial func(la string, lp int, ra string, rp int) (net.Conn, error)) (net.Conn, error) { 652 | rhost, rport, err := net.SplitHostPort(ra) 653 | if err != nil { 654 | return nil, err 655 | } 656 | 657 | rportValue, err := strconv.Atoi(rport) 658 | if err != nil { 659 | return nil, err 660 | } 661 | 662 | return dial(la, 0, rhost, rportValue) 663 | } 664 | 665 | // fixHostPort examines an address in one of the accepted forms and fixes it 666 | // to include a port number if necessary. 667 | func fixHostPort(address string, defaultPort int) (fixed string, err error) { 668 | if len(address) == 0 { 669 | return "", errors.New("address string is empty") 670 | } 671 | 672 | // If the address is wrapped in brackets, append a port if necessary. 673 | if address[0] == '[' { 674 | end := strings.IndexByte(address, ']') 675 | switch { 676 | case end < 0: 677 | return "", errors.New("missing ']' in address") 678 | case end+1 == len(address): 679 | return fmt.Sprintf("%s:%d", address, defaultPort), nil 680 | case address[end+1] == ':': 681 | return address, nil 682 | default: 683 | return "", errors.New("unexpected character following ']' in address") 684 | } 685 | } 686 | 687 | // No colons? Must be a port-less IPv4 or domain address. 688 | last := strings.LastIndexByte(address, ':') 689 | if last < 0 { 690 | return fmt.Sprintf("%s:%d", address, defaultPort), nil 691 | } 692 | 693 | // Exactly one colon? A port have been included along with an IPv4 or 694 | // domain address. (IPv6 addresses are guaranteed to have more than one 695 | // colon.) 696 | prev := strings.LastIndexByte(address[:last], ':') 697 | if prev < 0 { 698 | return address, nil 699 | } 700 | 701 | // Two or more colons means we must have an IPv6 address without a port. 702 | return fmt.Sprintf("[%s]:%d", address, defaultPort), nil 703 | } 704 | 705 | // generateResponse processes NTP header fields along with the its receive 706 | // time to generate a Response record. 707 | func generateResponse(h *header, recvTime ntpTime, authErr error) *Response { 708 | r := &Response{ 709 | Time: h.TransmitTime.Time(), 710 | ClockOffset: offset(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), 711 | RTT: rtt(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), 712 | Precision: toInterval(h.Precision), 713 | Version: h.getVersion(), 714 | Stratum: h.Stratum, 715 | ReferenceID: h.ReferenceID, 716 | ReferenceTime: h.ReferenceTime.Time(), 717 | RootDelay: h.RootDelay.Duration(), 718 | RootDispersion: h.RootDispersion.Duration(), 719 | Leap: h.getLeap(), 720 | MinError: minError(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), 721 | Poll: toInterval(h.Poll), 722 | authErr: authErr, 723 | } 724 | 725 | // Calculate values depending on other calculated values 726 | r.RootDistance = rootDistance(r.RTT, r.RootDelay, r.RootDispersion) 727 | 728 | // If a kiss of death was received, interpret the reference ID as 729 | // a kiss code. 730 | if r.Stratum == 0 { 731 | r.KissCode = kissCode(r.ReferenceID) 732 | } 733 | 734 | return r 735 | } 736 | 737 | // The following helper functions calculate additional metadata about the 738 | // timestamps received from an NTP server. The timestamps returned by 739 | // the server are given the following variable names: 740 | // 741 | // org = Origin Timestamp (client send time) 742 | // rec = Receive Timestamp (server receive time) 743 | // xmt = Transmit Timestamp (server reply time) 744 | // dst = Destination Timestamp (client receive time) 745 | 746 | func rtt(org, rec, xmt, dst ntpTime) time.Duration { 747 | a := int64(dst - org) 748 | b := int64(xmt - rec) 749 | rtt := a - b 750 | if rtt < 0 { 751 | rtt = 0 752 | } 753 | return ntpTime(rtt).Duration() 754 | } 755 | 756 | func offset(org, rec, xmt, dst ntpTime) time.Duration { 757 | // The inputs are 64-bit unsigned integer timestamps. These timestamps can 758 | // "roll over" at the end of an NTP era, which occurs approximately every 759 | // 136 years starting from the year 1900. To ensure an accurate offset 760 | // calculation when an era boundary is crossed, we need to take care that 761 | // the difference between two 64-bit timestamp values is accurately 762 | // calculated even when they are in neighboring eras. 763 | // 764 | // See: https://www.eecis.udel.edu/~mills/y2k.html 765 | 766 | a := int64(rec - org) 767 | b := int64(xmt - dst) 768 | offset := a + (b-a)/2 769 | if offset < 0 { 770 | return -ntpTime(-offset).Duration() 771 | } 772 | return ntpTime(offset).Duration() 773 | } 774 | 775 | func minError(org, rec, xmt, dst ntpTime) time.Duration { 776 | // Each NTP response contains two pairs of send/receive timestamps. 777 | // When either pair indicates a "causality violation", we calculate the 778 | // error as the difference in time between them. The minimum error is 779 | // the greater of the two causality violations. 780 | var error0, error1 ntpTime 781 | if org >= rec { 782 | error0 = org - rec 783 | } 784 | if xmt >= dst { 785 | error1 = xmt - dst 786 | } 787 | if error0 > error1 { 788 | return error0.Duration() 789 | } 790 | return error1.Duration() 791 | } 792 | 793 | func rootDistance(rtt, rootDelay, rootDisp time.Duration) time.Duration { 794 | // The root distance is: 795 | // the maximum error due to all causes of the local clock 796 | // relative to the primary server. It is defined as half the 797 | // total delay plus total dispersion plus peer jitter. 798 | // (https://tools.ietf.org/html/rfc5905#appendix-A.5.5.2) 799 | // 800 | // In the reference implementation, it is calculated as follows: 801 | // rootDist = max(MINDISP, rootDelay + rtt)/2 + rootDisp 802 | // + peerDisp + PHI * (uptime - peerUptime) 803 | // + peerJitter 804 | // For an SNTP client which sends only a single packet, most of these 805 | // terms are irrelevant and become 0. 806 | totalDelay := rtt + rootDelay 807 | return totalDelay/2 + rootDisp 808 | } 809 | 810 | func toInterval(t int8) time.Duration { 811 | switch { 812 | case t > 0: 813 | return time.Duration(uint64(time.Second) << uint(t)) 814 | case t < 0: 815 | return time.Duration(uint64(time.Second) >> uint(-t)) 816 | default: 817 | return time.Second 818 | } 819 | } 820 | 821 | func kissCode(id uint32) string { 822 | isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 } 823 | 824 | b := [4]byte{ 825 | byte(id >> 24), 826 | byte(id >> 16), 827 | byte(id >> 8), 828 | byte(id), 829 | } 830 | for _, ch := range b { 831 | if !isPrintable(ch) { 832 | return "" 833 | } 834 | } 835 | return string(b[:]) 836 | } 837 | -------------------------------------------------------------------------------- /ntp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015-2023 Brett Vickers. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ntp 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | "os" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | // The NTP server to use for online unit tests. May be overridden by the 19 | // NTP_HOST environment variable. 20 | var host string = "0.beevik-ntp.pool.ntp.org" 21 | 22 | const ( 23 | refID = 0xc0a80001 24 | timeFormat = "Mon Jan _2 2006 15:04:05.00000000 (MST)" 25 | ) 26 | 27 | func init() { 28 | h := os.Getenv("NTP_HOST") 29 | if h != "" { 30 | host = h 31 | } 32 | } 33 | 34 | func isNil(t *testing.T, host string, err error) bool { 35 | switch { 36 | case err == nil: 37 | return true 38 | case err == ErrKissOfDeath: 39 | // log instead of error, so test isn't failed 40 | t.Logf("[%s] Query kiss of death (ignored)", host) 41 | return false 42 | case strings.Contains(err.Error(), "timeout"): 43 | // log instead of error, so test isn't failed 44 | t.Logf("[%s] Query timeout (ignored): %s", host, err) 45 | return false 46 | default: 47 | // error, so test fails 48 | t.Errorf("[%s] Query failed: %s", host, err) 49 | return false 50 | } 51 | } 52 | 53 | func assertValid(t *testing.T, r *Response) { 54 | err := r.Validate() 55 | _ = isNil(t, host, err) 56 | } 57 | 58 | func assertInvalid(t *testing.T, r *Response) { 59 | err := r.Validate() 60 | if err == nil { 61 | t.Errorf("[%s] Response unexpectedly valid\n", host) 62 | } 63 | } 64 | 65 | func logResponse(t *testing.T, r *Response) { 66 | now := time.Now() 67 | t.Logf("[%s] ClockOffset: %s", host, r.ClockOffset) 68 | t.Logf("[%s] SystemTime: %s", host, now.Format(timeFormat)) 69 | t.Logf("[%s] ~TrueTime: %s", host, now.Add(r.ClockOffset).Format(timeFormat)) 70 | t.Logf("[%s] XmitTime: %s", host, r.Time.Format(timeFormat)) 71 | t.Logf("[%s] Version: %d", host, r.Version) 72 | t.Logf("[%s] Stratum: %d", host, r.Stratum) 73 | t.Logf("[%s] RefID: %s (0x%08x)", host, r.ReferenceString(), r.ReferenceID) 74 | t.Logf("[%s] RefTime: %s", host, r.ReferenceTime.Format(timeFormat)) 75 | t.Logf("[%s] RTT: %s", host, r.RTT) 76 | t.Logf("[%s] Poll: %s", host, r.Poll) 77 | t.Logf("[%s] Precision: %s", host, r.Precision) 78 | t.Logf("[%s] RootDelay: %s", host, r.RootDelay) 79 | t.Logf("[%s] RootDisp: %s", host, r.RootDispersion) 80 | t.Logf("[%s] RootDist: %s", host, r.RootDistance) 81 | t.Logf("[%s] MinError: %s", host, r.MinError) 82 | t.Logf("[%s] Leap: %d", host, r.Leap) 83 | t.Logf("[%s] KissCode: %s", host, stringOrEmpty(r.KissCode)) 84 | } 85 | 86 | func stringOrEmpty(s string) string { 87 | if s == "" { 88 | return "" 89 | } 90 | return s 91 | } 92 | 93 | func TestOnlineBadServerPort(t *testing.T) { 94 | // Not NTP port. 95 | tm, _, err := getTime(host+":9", &QueryOptions{Timeout: 1 * time.Second}) 96 | assert.Nil(t, tm) 97 | assert.NotNil(t, err) 98 | } 99 | 100 | func TestOnlineQuery(t *testing.T) { 101 | r, err := QueryWithOptions(host, QueryOptions{}) 102 | if !isNil(t, host, err) { 103 | return 104 | } 105 | assertValid(t, r) 106 | logResponse(t, r) 107 | } 108 | 109 | func TestOnlineQueryTimeout(t *testing.T) { 110 | if host == "localhost" { 111 | t.Skip("Timeout test not available with localhost NTP server.") 112 | return 113 | } 114 | 115 | // Force an immediate timeout. 116 | r, err := QueryWithOptions(host, QueryOptions{Timeout: time.Nanosecond}) 117 | assert.Nil(t, r) 118 | assert.NotNil(t, err) 119 | } 120 | 121 | func TestOnlineTime(t *testing.T) { 122 | tm, err := Time(host) 123 | now := time.Now() 124 | if isNil(t, host, err) { 125 | t.Logf(" System Time: %s\n", now.Format(timeFormat)) 126 | t.Logf(" ~True Time: %s\n", tm.Format(timeFormat)) 127 | t.Logf("~ClockOffset: %v\n", tm.Sub(now)) 128 | } 129 | } 130 | 131 | func TestOnlineTimeFailure(t *testing.T) { 132 | // Use a link-local IP address that won't have an NTP server listening 133 | // on it. This should return the local system's time. 134 | local, err := Time("169.254.122.229") 135 | assert.NotNil(t, err) 136 | 137 | // When the NTP time query fails, it should return the system time. 138 | // Compare the "now" system time with the returned time. It should be 139 | // about the same. 140 | now := time.Now() 141 | diffMinutes := now.Sub(local).Minutes() 142 | assert.True(t, diffMinutes > -1 && diffMinutes < 1) 143 | } 144 | 145 | func TestOnlineTTL(t *testing.T) { 146 | if host == "localhost" { 147 | t.Skip("TTL test not available with localhost NTP server.") 148 | return 149 | } 150 | 151 | // TTL of 1 should cause a timeout. 152 | hdr, _, err := getTime(host, &QueryOptions{TTL: 1, Timeout: 1 * time.Second}) 153 | assert.Nil(t, hdr) 154 | assert.NotNil(t, err) 155 | } 156 | 157 | func TestOfflineConvertLong(t *testing.T) { 158 | ts := []ntpTime{0x0, 0xff800000, 0x1ff800000, 0x80000000ff800000, 0xffffffffff800000} 159 | for _, v := range ts { 160 | assert.Equal(t, v, toNtpTime(v.Time())) 161 | } 162 | } 163 | 164 | func TestOfflineConvertShort(t *testing.T) { 165 | cases := []struct { 166 | NtpTime ntpTimeShort 167 | Duration time.Duration 168 | }{ 169 | {0x00000000, 0 * time.Nanosecond}, 170 | {0x00000001, 15259 * time.Nanosecond}, 171 | {0x00008000, 500 * time.Millisecond}, 172 | {0x0000c000, 750 * time.Millisecond}, 173 | {0x0000ff80, time.Second - (1000000000/512)*time.Nanosecond}, 174 | {0x00010000, 1000 * time.Millisecond}, 175 | {0x00018000, 1500 * time.Millisecond}, 176 | {0xffff0000, 65535 * time.Second}, 177 | {0xffffff80, 65536*time.Second - (1000000000/512)*time.Nanosecond}, 178 | } 179 | 180 | for _, c := range cases { 181 | ts := c.NtpTime 182 | assert.Equal(t, c.Duration, ts.Duration()) 183 | } 184 | } 185 | 186 | func TestOfflineCustomDialer(t *testing.T) { 187 | raddr := "remote:123" 188 | laddr := "local" 189 | dialerCalled := false 190 | notDialingErr := errors.New("not dialing") 191 | 192 | customDialer := func(la, ra string) (net.Conn, error) { 193 | assert.Equal(t, laddr, la) 194 | assert.Equal(t, raddr, ra) 195 | // Only expect to be called once: 196 | assert.False(t, dialerCalled) 197 | 198 | dialerCalled = true 199 | return nil, notDialingErr 200 | } 201 | 202 | opt := QueryOptions{ 203 | LocalAddress: laddr, 204 | Dialer: customDialer, 205 | } 206 | r, err := QueryWithOptions(raddr, opt) 207 | assert.Nil(t, r) 208 | assert.Equal(t, notDialingErr, err) 209 | assert.True(t, dialerCalled) 210 | } 211 | 212 | func TestOfflineCustomDialerDeprecated(t *testing.T) { 213 | raddr := "remote" 214 | laddr := "local" 215 | dialerCalled := false 216 | notDialingErr := errors.New("not dialing") 217 | 218 | customDial := func(la string, lp int, ra string, rp int) (net.Conn, error) { 219 | assert.Equal(t, laddr, la) 220 | assert.Equal(t, 0, lp) 221 | assert.Equal(t, raddr, ra) 222 | assert.Equal(t, 123, rp) 223 | // Only expect to be called once: 224 | assert.False(t, dialerCalled) 225 | 226 | dialerCalled = true 227 | return nil, notDialingErr 228 | } 229 | 230 | opt := QueryOptions{ 231 | LocalAddress: laddr, 232 | Dial: customDial, 233 | } 234 | r, err := QueryWithOptions(raddr, opt) 235 | assert.Nil(t, r) 236 | assert.Equal(t, notDialingErr, err) 237 | assert.True(t, dialerCalled) 238 | } 239 | 240 | func TestOfflineFixHostPort(t *testing.T) { 241 | const defaultPort = 123 242 | 243 | cases := []struct { 244 | address string 245 | fixed string 246 | errMsg string 247 | }{ 248 | {"192.168.1.1", "192.168.1.1:123", ""}, 249 | {"192.168.1.1:123", "192.168.1.1:123", ""}, 250 | {"192.168.1.1:1000", "192.168.1.1:1000", ""}, 251 | {"[192.168.1.1]:1000", "[192.168.1.1]:1000", ""}, 252 | {"www.example.com", "www.example.com:123", ""}, 253 | {"www.example.com:123", "www.example.com:123", ""}, 254 | {"www.example.com:1000", "www.example.com:1000", ""}, 255 | {"[www.example.com]:1000", "[www.example.com]:1000", ""}, 256 | {"::1", "[::1]:123", ""}, 257 | {"[::1]", "[::1]:123", ""}, 258 | {"[::1]:123", "[::1]:123", ""}, 259 | {"[::1]:1000", "[::1]:1000", ""}, 260 | {"fe80::1", "[fe80::1]:123", ""}, 261 | {"[fe80::1]", "[fe80::1]:123", ""}, 262 | {"[fe80::1]:123", "[fe80::1]:123", ""}, 263 | {"[fe80::1]:1000", "[fe80::1]:1000", ""}, 264 | {"[fe80::", "", "missing ']' in address"}, 265 | {"[fe80::]@", "", "unexpected character following ']' in address"}, 266 | {"ff06:0:0:0:0:0:0:c3", "[ff06:0:0:0:0:0:0:c3]:123", ""}, 267 | {"[ff06:0:0:0:0:0:0:c3]", "[ff06:0:0:0:0:0:0:c3]:123", ""}, 268 | {"[ff06:0:0:0:0:0:0:c3]:123", "[ff06:0:0:0:0:0:0:c3]:123", ""}, 269 | {"[ff06:0:0:0:0:0:0:c3]:1000", "[ff06:0:0:0:0:0:0:c3]:1000", ""}, 270 | {"::ffff:192.168.1.1", "[::ffff:192.168.1.1]:123", ""}, 271 | {"[::ffff:192.168.1.1]", "[::ffff:192.168.1.1]:123", ""}, 272 | {"[::ffff:192.168.1.1]:123", "[::ffff:192.168.1.1]:123", ""}, 273 | {"[::ffff:192.168.1.1]:1000", "[::ffff:192.168.1.1]:1000", ""}, 274 | {"", "", "address string is empty"}, 275 | } 276 | for _, c := range cases { 277 | fixed, err := fixHostPort(c.address, defaultPort) 278 | errMsg := "" 279 | if err != nil { 280 | errMsg = err.Error() 281 | } 282 | assert.Equal(t, c.fixed, fixed) 283 | assert.Equal(t, c.errMsg, errMsg) 284 | } 285 | } 286 | 287 | func TestOfflineKissCode(t *testing.T) { 288 | codes := []struct { 289 | id uint32 290 | str string 291 | }{ 292 | {0x41435354, "ACST"}, 293 | {0x41555448, "AUTH"}, 294 | {0x4155544f, "AUTO"}, 295 | {0x42435354, "BCST"}, 296 | {0x43525950, "CRYP"}, 297 | {0x44454e59, "DENY"}, 298 | {0x44524f50, "DROP"}, 299 | {0x52535452, "RSTR"}, 300 | {0x494e4954, "INIT"}, 301 | {0x4d435354, "MCST"}, 302 | {0x4e4b4559, "NKEY"}, 303 | {0x4e54534e, "NTSN"}, 304 | {0x52415445, "RATE"}, 305 | {0x524d4f54, "RMOT"}, 306 | {0x53544550, "STEP"}, 307 | {0x01010101, ""}, 308 | {0xfefefefe, ""}, 309 | {0x01544450, ""}, 310 | {0x41544401, ""}, 311 | } 312 | for _, c := range codes { 313 | assert.Equal(t, kissCode(c.id), c.str) 314 | } 315 | } 316 | 317 | func TestOfflineMinError(t *testing.T) { 318 | start := time.Now() 319 | h := &header{ 320 | Stratum: 1, 321 | ReferenceID: refID, 322 | ReferenceTime: toNtpTime(start), 323 | OriginTime: toNtpTime(start.Add(1 * time.Second)), 324 | ReceiveTime: toNtpTime(start.Add(2 * time.Second)), 325 | TransmitTime: toNtpTime(start.Add(3 * time.Second)), 326 | } 327 | r := generateResponse(h, toNtpTime(start.Add(4*time.Second)), nil) 328 | assertValid(t, r) 329 | assert.Equal(t, r.MinError, time.Duration(0)) 330 | 331 | for org := 1 * time.Second; org <= 10*time.Second; org += time.Second { 332 | for rec := 1 * time.Second; rec <= 10*time.Second; rec += time.Second { 333 | for xmt := rec; xmt <= 10*time.Second; xmt += time.Second { 334 | for dst := org; dst <= 10*time.Second; dst += time.Second { 335 | h.OriginTime = toNtpTime(start.Add(org)) 336 | h.ReceiveTime = toNtpTime(start.Add(rec)) 337 | h.TransmitTime = toNtpTime(start.Add(xmt)) 338 | r = generateResponse(h, toNtpTime(start.Add(dst)), nil) 339 | assertValid(t, r) 340 | var error0, error1 time.Duration 341 | if org >= rec { 342 | error0 = org - rec 343 | } 344 | if xmt >= dst { 345 | error1 = xmt - dst 346 | } 347 | var minError time.Duration 348 | if error0 > error1 { 349 | minError = error0 350 | } else { 351 | minError = error1 352 | } 353 | assert.Equal(t, r.MinError, minError) 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | func TestOfflineOffsetCalculation(t *testing.T) { 361 | now := time.Now() 362 | t1 := toNtpTime(now) 363 | t2 := toNtpTime(now.Add(20 * time.Second)) 364 | t3 := toNtpTime(now.Add(21 * time.Second)) 365 | t4 := toNtpTime(now.Add(5 * time.Second)) 366 | 367 | // expectedOffset := ((T2 - T1) + (T3 - T4)) / 2 368 | // ((119 - 99) + (121 - 104)) / 2 369 | // (20 + 17) / 2 370 | // 37 / 2 = 18 371 | expectedOffset := 18 * time.Second 372 | offset := offset(t1, t2, t3, t4) 373 | assert.Equal(t, expectedOffset, offset) 374 | } 375 | 376 | func TestOfflineOffsetCalculationNegative(t *testing.T) { 377 | now := time.Now() 378 | t1 := toNtpTime(now.Add(101 * time.Second)) 379 | t2 := toNtpTime(now.Add(102 * time.Second)) 380 | t3 := toNtpTime(now.Add(103 * time.Second)) 381 | t4 := toNtpTime(now.Add(105 * time.Second)) 382 | 383 | // expectedOffset := ((T2 - T1) + (T3 - T4)) / 2 384 | // ((102 - 101) + (103 - 105)) / 2 385 | // (1 + -2) / 2 = -1 / 2 386 | expectedOffset := -time.Second / 2 387 | offset := offset(t1, t2, t3, t4) 388 | assert.Equal(t, expectedOffset, offset) 389 | } 390 | 391 | func TestOfflineOffsetRollover(t *testing.T) { 392 | cases := []struct { 393 | clientTime string 394 | serverTime string 395 | }{ 396 | // both timestamps in NTP era 0 (with large difference) 397 | {"1970-01-01 00:00:00", "2024-05-30 00:00:00"}, 398 | {"2024-05-30 00:00:00", "1970-01-01 00:00:00"}, 399 | 400 | // one timestamp in NTP era 0 and another in era 1 401 | {"2047-01-01 00:00:00", "2024-01-01 00:00:00"}, 402 | {"2024-01-01 00:00:00", "2047-01-01 00:00:00"}, 403 | 404 | // both timestamps in NTP era 1 405 | {"2047-01-01 00:00:00", "2047-02-01 00:00:00"}, 406 | {"2047-02-01 00:00:00", "2047-01-01 00:00:00"}, 407 | } 408 | 409 | timeFormat := "2006-01-02 15:04:05" 410 | 411 | for _, c := range cases { 412 | clientTime, _ := time.Parse(timeFormat, c.clientTime) 413 | serverTime, _ := time.Parse(timeFormat, c.serverTime) 414 | 415 | org := toNtpTime(clientTime) 416 | rec := toNtpTime(serverTime) 417 | xmt := toNtpTime(serverTime.Add(1 * time.Second)) 418 | dst := toNtpTime(clientTime.Add(1 * time.Second)) 419 | 420 | expectedValue := serverTime.Sub(clientTime) 421 | value := offset(org, rec, xmt, dst) 422 | assert.Equal(t, expectedValue, value) 423 | } 424 | } 425 | 426 | func TestOfflineTimeRollover(t *testing.T) { 427 | cases := []struct { 428 | timestamp ntpTime 429 | time string 430 | }{ 431 | {0x0000000000000000, "2036-02-07 06:28:16"}, 432 | {0x0000000100000000, "2036-02-07 06:28:17"}, 433 | {0x1000000000000000, "2044-08-10 03:52:32"}, 434 | {0x2000000000000000, "2053-02-11 01:16:48"}, 435 | {0x3000000000000000, "2061-08-14 22:41:04"}, 436 | {0x4000000000000000, "2070-02-15 20:05:20"}, 437 | {0x5000000000000000, "2078-08-19 17:29:36"}, 438 | {0x6000000000000000, "2087-02-20 14:53:52"}, 439 | {0x7000000000000000, "2095-08-24 12:18:08"}, 440 | {0x8000000000000000, "2104-02-26 09:42:24"}, 441 | {0x83aa7e7000000000, "2106-02-07 06:28:00"}, 442 | {0x83aa7e8000000000, "1970-01-01 00:00:00"}, // <- ntpTime.Time() wrap 443 | {0x9000000000000000, "1976-07-23 00:38:24"}, 444 | {0xa000000000000000, "1985-01-23 22:02:40"}, 445 | {0xb000000000000000, "1993-07-27 19:26:56"}, 446 | {0xc000000000000000, "2002-01-28 16:51:12"}, 447 | {0xd000000000000000, "2010-08-01 14:15:28"}, 448 | {0xe000000000000000, "2019-02-02 11:39:44"}, 449 | {0xf000000000000000, "2027-08-06 09:04:00"}, 450 | {0xffffffff00000000, "2036-02-07 06:28:15"}, 451 | } 452 | 453 | timeFormat := "2006-01-02 15:04:05" 454 | 455 | for _, c := range cases { 456 | tm, _ := time.Parse(timeFormat, c.time) 457 | assert.Equal(t, tm, c.timestamp.Time()) 458 | assert.Equal(t, c.timestamp, toNtpTime(tm)) 459 | } 460 | } 461 | 462 | func TestOfflineReferenceString(t *testing.T) { 463 | cases := []struct { 464 | Stratum byte 465 | RefID uint32 466 | Str string 467 | }{ 468 | {0, 0x41435354, "ACST"}, 469 | {0, 0x41555448, "AUTH"}, 470 | {0, 0x4155544f, "AUTO"}, 471 | {0, 0x42435354, "BCST"}, 472 | {0, 0x43525950, "CRYP"}, 473 | {0, 0x44454e59, "DENY"}, 474 | {0, 0x44524f50, "DROP"}, 475 | {0, 0x52535452, "RSTR"}, 476 | {0, 0x494e4954, "INIT"}, 477 | {0, 0x4d435354, "MCST"}, 478 | {0, 0x4e4b4559, "NKEY"}, 479 | {0, 0x4e54534e, "NTSN"}, 480 | {0, 0x52415445, "RATE"}, 481 | {0, 0x524d4f54, "RMOT"}, 482 | {0, 0x53544550, "STEP"}, 483 | {0, 0x01010101, ""}, 484 | {0, 0xfefefefe, ""}, 485 | {0, 0x01544450, ""}, 486 | {0, 0x41544401, ""}, 487 | {1, 0x47505300, ".GPS."}, 488 | {1, 0x474f4553, ".GOES."}, 489 | {2, 0x0a0a1401, "10.10.20.1"}, 490 | {3, 0xc0a80001, "192.168.0.1"}, 491 | {4, 0xc0a80001, "192.168.0.1"}, 492 | {5, 0xc0a80001, "192.168.0.1"}, 493 | {6, 0xc0a80001, "192.168.0.1"}, 494 | {7, 0xc0a80001, "192.168.0.1"}, 495 | {8, 0xc0a80001, "192.168.0.1"}, 496 | {9, 0xc0a80001, "192.168.0.1"}, 497 | {10, 0xc0a80001, "192.168.0.1"}, 498 | } 499 | for _, c := range cases { 500 | r := Response{Stratum: c.Stratum, ReferenceID: c.RefID} 501 | assert.Equal(t, c.Str, r.ReferenceString()) 502 | } 503 | } 504 | 505 | func TestOfflineTimeConversions(t *testing.T) { 506 | nowNtp := toNtpTime(time.Now()) 507 | now := nowNtp.Time() 508 | startNow := now 509 | for i := 0; i < 100; i++ { 510 | nowNtp = toNtpTime(now) 511 | now = nowNtp.Time() 512 | } 513 | assert.Equal(t, now, startNow) 514 | } 515 | 516 | func TestOfflineValidate(t *testing.T) { 517 | var h header 518 | var r *Response 519 | h.Stratum = 1 520 | h.ReferenceID = refID 521 | h.ReferenceTime = 1 << 32 522 | h.Precision = -1 // 500ms 523 | 524 | // Zero RTT 525 | h.OriginTime = 1 << 32 526 | h.ReceiveTime = 1 << 32 527 | h.TransmitTime = 1 << 32 528 | r = generateResponse(&h, 1<<32, nil) 529 | assertValid(t, r) 530 | 531 | // Negative freshness 532 | h.ReferenceTime = 2 << 32 533 | r = generateResponse(&h, 1<<32, nil) 534 | assertInvalid(t, r) 535 | 536 | // Unfresh clock (48h) 537 | h.OriginTime = 2 * 86400 << 32 538 | h.ReceiveTime = 2 * 86400 << 32 539 | h.TransmitTime = 2 * 86400 << 32 540 | r = generateResponse(&h, 2*86400<<32, nil) 541 | assertInvalid(t, r) 542 | 543 | // Fresh clock (24h) 544 | h.ReferenceTime = 1 * 86400 << 32 545 | r = generateResponse(&h, 2*86400<<32, nil) 546 | assertValid(t, r) 547 | 548 | // Values indicating a negative RTT 549 | h.RootDelay = 16 << 16 550 | h.ReferenceTime = 1 << 32 551 | h.OriginTime = 20 << 32 552 | h.ReceiveTime = 10 << 32 553 | h.TransmitTime = 15 << 32 554 | r = generateResponse(&h, 22<<32, nil) 555 | assert.NotNil(t, r) 556 | assertValid(t, r) 557 | assert.Equal(t, r.RTT, 0*time.Second) 558 | assert.Equal(t, r.RootDistance, 8*time.Second) 559 | } 560 | --------------------------------------------------------------------------------