├── CONTRIBUTORS ├── go.mod ├── go.sum ├── LICENSE ├── .github └── workflows │ └── go.yml ├── README.md ├── RELEASE_NOTES.md ├── auth.go ├── auth_test.go ├── ntp_test.go └── ntp.go /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 | Christian Cedercrantz (chrisceder) 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/beevik/ntp 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/stretchr/testify v1.11.1 7 | golang.org/x/net v0.44.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.36.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 8 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 9 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 10 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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.24', '1.25.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 | -------------------------------------------------------------------------------- /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 | * `LocalAddress`: The local IP address to use when sending the query. Useful 86 | when the host has multiple network interfaces. 87 | * `Auth`: The symmetric authentication key and algorithm used by the server to 88 | authenticate the query. The same information is used by the client to 89 | authenticate the server's response. 90 | * `Extensions`: Extensions may be added to modify NTP queries before they are 91 | transmitted and to process NTP responses after they arrive. 92 | * `GetSystemTime`: A custom function to obtain the current system time, used 93 | to override the default `time.Now` function. 94 | * `Dialer`: A custom network connection "dialer" function used to override the 95 | default UDP dialer function. 96 | 97 | ## Using the NTP pool 98 | 99 | The NTP pool is a shared resource provided by the [NTP Pool 100 | Project](https://www.pool.ntp.org/en/) and used by people and services all 101 | over the world. To prevent it from becoming overloaded, please avoid querying 102 | the standard `pool.ntp.org` zone names in your applications. Instead, consider 103 | requesting your own [vendor zone](http://www.pool.ntp.org/en/vendors.html) or 104 | [joining the pool](http://www.pool.ntp.org/join.html). 105 | 106 | 107 | ## Network Time Security (NTS) 108 | 109 | Network Time Security (NTS) is a recent enhancement of NTP, designed to add 110 | better authentication and message integrity to the protocol. It is defined by 111 | [RFC 8915](https://tools.ietf.org/html/rfc8915). If you wish to use NTS, see 112 | the [nts package](https://github.com/beevik/nts). (The nts package is 113 | implemented as an extension to this package.) 114 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | Release v1.5.0 2 | ============== 3 | 4 | **Changes** 5 | 6 | * Added the `GetSystemTime` field to `QueryOptions`. 7 | * Updated minimum required Go version to 1.24. 8 | 9 | **Fixes** 10 | 11 | * Upgraded package dependencies to retrieve security fixes. 12 | 13 | Release v1.4.3 14 | ============== 15 | 16 | **Fixes** 17 | 18 | * Fixed an overflow bug in the clock offset calculation introduced by 19 | release v1.4.2. 20 | 21 | Release v1.4.2 22 | ============== 23 | 24 | **Fixes** 25 | 26 | * Fixed a bug in clock offset calculation. 27 | 28 | Release v1.4.1 29 | ============== 30 | 31 | **Updates** 32 | 33 | * Upgraded package dependencies to retrieve security fixes. 34 | 35 | Release v1.4.0 36 | ============== 37 | 38 | **Changes** 39 | 40 | * Added a protocol `Version` field to the `Response` struct. 41 | 42 | Release v1.3.1 43 | ============== 44 | 45 | **Changes** 46 | 47 | * Added AES-256-CMAC support for symmetric authentication. 48 | * Symmetric auth keys may now be specified as ASCII or HEX using the "ASCII:" 49 | or "HEX:" prefixes. 50 | * Updated dependencies to address security issues. 51 | 52 | **Fixes** 53 | 54 | * Added proper handling of the empty string when used as a server address. 55 | 56 | Release v1.3.0 57 | ============== 58 | 59 | **Changes** 60 | 61 | * Added the `ReferenceString` function to `Response`. This generates a 62 | stratum-specific string for the `ReferenceID` value. 63 | * Optimized the AES CMAC calculation for 64-bit architectures. 64 | 65 | **Fixes** 66 | 67 | * Fixed a bug introduced in release v1.2.0 that was causing IPv6 addresses 68 | to be interpreted incorrectly. 69 | 70 | Release v1.2.0 71 | ============== 72 | 73 | **Changes** 74 | 75 | * Added support for NTP extensions by exposing an extension interface. 76 | Extensions are able to (1) modify NTP messages before being sent to 77 | the server, and (2) process NTP messages after they arrive from the 78 | server. This feature has been added in preparation for NTS support. 79 | * Added support for RFC 5905 symmetric key authentication. 80 | * Allowed server address to be specified as a "host:port" pair. 81 | * Brought package into further compliance with IETF draft on client data 82 | minimization. 83 | * Declared error variables as part of the public API. 84 | * Added a `Dialer` field to `QueryOptions`. This replaces the deprecated 85 | `Dial` field. 86 | * Added an `IsKissOfDeath` function to the `Response` type. 87 | 88 | **Deprecated** 89 | 90 | * Deprecated the `Port` field in QueryOptions. 91 | * Deprecated the `Dial` field in QueryOptions. 92 | 93 | Release v1.1.1 94 | ============== 95 | 96 | **Fixes** 97 | 98 | * Fixed a missing indirect go module dependency. 99 | 100 | Release v1.1.0 101 | ============== 102 | 103 | **Changes** 104 | 105 | * Added the `Dial` property to the `QueryOptions` struct. This allows the user 106 | to override the default UDP dialer when setting up a connection to a remote 107 | NTP server. 108 | 109 | Release v1.0.0 110 | ============== 111 | 112 | This package has been stable for several years with no bug reports in that 113 | time. It is also pretty much feature complete. I am therefore updating the 114 | version to 1.0.0. 115 | 116 | Because this is a major release, all previously deprecated code has been 117 | removed from the package. 118 | 119 | **Breaking changes** 120 | 121 | * Removed the `TimeV` function. Use `Time` or `QueryWithOptions` instead. 122 | 123 | Release v0.3.2 124 | ============== 125 | 126 | **Changes** 127 | 128 | * Rename unit tests to enable easier test filtering. 129 | 130 | Release v0.3.0 131 | ============== 132 | 133 | There have been no breaking changes or further deprecations since the 134 | previous release. 135 | 136 | **Changes** 137 | 138 | * Fixed a bug in the calculation of NTP timestamps. 139 | 140 | Release v0.2.0 141 | ============== 142 | 143 | There are no breaking changes or further deprecations in this release. 144 | 145 | **Changes** 146 | 147 | * Added `KissCode` to the `Response` structure. 148 | 149 | 150 | Release v0.1.1 151 | ============== 152 | 153 | **Breaking changes** 154 | 155 | * Removed the `MaxStratum` constant. 156 | 157 | **Deprecations** 158 | 159 | * Officially deprecated the `TimeV` function. 160 | 161 | **Internal changes** 162 | 163 | * Removed `minDispersion` from the `RootDistance` calculation, since the value 164 | was arbitrary. 165 | * Moved some validation into main code path so that invalid `TransmitTime` and 166 | `mode` responses trigger an error even when `Response.Validate` is not 167 | called. 168 | 169 | 170 | Release v0.1.0 171 | ============== 172 | 173 | This is the initial release of the `ntp` package. Currently it supports the 174 | following features: 175 | * `Time()` to query the current time according to a remote NTP server. 176 | * `Query()` to query multiple pieces of time-related information from a remote 177 | NTP server. 178 | * `QueryWithOptions()`, which is like `Query()` but with the ability to 179 | override default query options. 180 | 181 | Time-related information returned by the `Query` functions includes: 182 | * `Time`: the time the server transmitted its response, according to the 183 | server's clock. 184 | * `ClockOffset`: the estimated offset of the client's clock relative to the 185 | server's clock. You may apply this offset to any local system clock reading 186 | once the query is complete. 187 | * `RTT`: an estimate of the round-trip-time delay between the client and the 188 | server. 189 | * `Precision`: the precision of the server's clock reading. 190 | * `Stratum`: the "stratum" level of the server, where 1 indicates a server 191 | directly connected to a reference clock, and values greater than 1 192 | indicating the number of hops from the reference clock. 193 | * `ReferenceID`: A unique identifier for the NTP server that was contacted. 194 | * `ReferenceTime`: The time at which the server last updated its local clock 195 | setting. 196 | * `RootDelay`: The server's round-trip delay to the reference clock. 197 | * `RootDispersion`: The server's total dispersion to the referenced clock. 198 | * `RootDistance`: An estimate of the root synchronization distance. 199 | * `Leap`: The leap second indicator. 200 | * `MinError`: A lower bound on the clock error between the client and the 201 | server. 202 | * `Poll`: the maximum polling interval between successive messages on the 203 | server. 204 | 205 | The `Response` structure returned by the `Query` functions also contains a 206 | `Response.Validate()` function that returns an error if any of the fields 207 | returned by the server are invalid. 208 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "sync/atomic" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | // The NTP server to use for online unit tests. May be overridden by the 20 | // NTP_HOST environment variable. 21 | var host string = "0.beevik-ntp.pool.ntp.org" 22 | 23 | const ( 24 | refID = 0xc0a80001 25 | timeFormat = "Mon Jan _2 2006 15:04:05.00000000 (MST)" 26 | ) 27 | 28 | func init() { 29 | h := os.Getenv("NTP_HOST") 30 | if h != "" { 31 | host = h 32 | } 33 | } 34 | 35 | func isNil(t *testing.T, host string, err error) bool { 36 | switch { 37 | case err == nil: 38 | return true 39 | case err == ErrKissOfDeath: 40 | // log instead of error, so test isn't failed 41 | t.Logf("[%s] Query kiss of death (ignored)", host) 42 | return false 43 | case strings.Contains(err.Error(), "timeout"): 44 | // log instead of error, so test isn't failed 45 | t.Logf("[%s] Query timeout (ignored): %s", host, err) 46 | return false 47 | default: 48 | // error, so test fails 49 | t.Errorf("[%s] Query failed: %s", host, err) 50 | return false 51 | } 52 | } 53 | 54 | func assertValid(t *testing.T, r *Response) { 55 | err := r.Validate() 56 | _ = isNil(t, host, err) 57 | } 58 | 59 | func assertInvalid(t *testing.T, r *Response) { 60 | err := r.Validate() 61 | if err == nil { 62 | t.Errorf("[%s] Response unexpectedly valid\n", host) 63 | } 64 | } 65 | 66 | func logResponse(t *testing.T, r *Response) { 67 | now := time.Now() 68 | t.Logf("[%s] ClockOffset: %s", host, r.ClockOffset) 69 | t.Logf("[%s] SystemTime: %s", host, now.Format(timeFormat)) 70 | t.Logf("[%s] ~TrueTime: %s", host, now.Add(r.ClockOffset).Format(timeFormat)) 71 | t.Logf("[%s] XmitTime: %s", host, r.Time.Format(timeFormat)) 72 | t.Logf("[%s] Version: %d", host, r.Version) 73 | t.Logf("[%s] Stratum: %d", host, r.Stratum) 74 | t.Logf("[%s] RefID: %s (0x%08x)", host, r.ReferenceString(), r.ReferenceID) 75 | t.Logf("[%s] RefTime: %s", host, r.ReferenceTime.Format(timeFormat)) 76 | t.Logf("[%s] RTT: %s", host, r.RTT) 77 | t.Logf("[%s] Poll: %s", host, r.Poll) 78 | t.Logf("[%s] Precision: %s", host, r.Precision) 79 | t.Logf("[%s] RootDelay: %s", host, r.RootDelay) 80 | t.Logf("[%s] RootDisp: %s", host, r.RootDispersion) 81 | t.Logf("[%s] RootDist: %s", host, r.RootDistance) 82 | t.Logf("[%s] MinError: %s", host, r.MinError) 83 | t.Logf("[%s] Leap: %d", host, r.Leap) 84 | t.Logf("[%s] KissCode: %s", host, stringOrEmpty(r.KissCode)) 85 | } 86 | 87 | func stringOrEmpty(s string) string { 88 | if s == "" { 89 | return "" 90 | } 91 | return s 92 | } 93 | 94 | func TestOnlineBadServerPort(t *testing.T) { 95 | // Not NTP port. 96 | tm, _, err := getTime(host+":9", &QueryOptions{Timeout: 1 * time.Second}) 97 | assert.Nil(t, tm) 98 | assert.NotNil(t, err) 99 | } 100 | 101 | func TestOnlineQuery(t *testing.T) { 102 | r, err := QueryWithOptions(host, QueryOptions{}) 103 | if !isNil(t, host, err) { 104 | return 105 | } 106 | assertValid(t, r) 107 | logResponse(t, r) 108 | } 109 | 110 | func TestOnlineQueryTimeout(t *testing.T) { 111 | if host == "localhost" { 112 | t.Skip("Timeout test not available with localhost NTP server.") 113 | return 114 | } 115 | 116 | // Force an immediate timeout. 117 | r, err := QueryWithOptions(host, QueryOptions{Timeout: time.Nanosecond}) 118 | assert.Nil(t, r) 119 | assert.NotNil(t, err) 120 | } 121 | 122 | func TestOnlineTime(t *testing.T) { 123 | tm, err := Time(host) 124 | now := time.Now() 125 | if isNil(t, host, err) { 126 | t.Logf(" System Time: %s\n", now.Format(timeFormat)) 127 | t.Logf(" ~True Time: %s\n", tm.Format(timeFormat)) 128 | t.Logf("~ClockOffset: %v\n", tm.Sub(now)) 129 | } 130 | } 131 | 132 | func TestOnlineTimeFailure(t *testing.T) { 133 | // Use a link-local IP address that won't have an NTP server listening 134 | // on it. This should return the local system's time. 135 | local, err := Time("169.254.122.229") 136 | assert.NotNil(t, err) 137 | 138 | // When the NTP time query fails, it should return the system time. 139 | // Compare the "now" system time with the returned time. It should be 140 | // about the same. 141 | now := time.Now() 142 | diffMinutes := now.Sub(local).Minutes() 143 | assert.True(t, diffMinutes > -1 && diffMinutes < 1) 144 | } 145 | 146 | func TestOnlineTTL(t *testing.T) { 147 | if host == "localhost" { 148 | t.Skip("TTL test not available with localhost NTP server.") 149 | return 150 | } 151 | 152 | // TTL of 1 should cause a timeout. 153 | hdr, _, err := getTime(host, &QueryOptions{TTL: 1, Timeout: 1 * time.Second}) 154 | assert.Nil(t, hdr) 155 | assert.NotNil(t, err) 156 | } 157 | 158 | func TestOnlineCustomGetSystemTime(t *testing.T) { 159 | if host == "localhost" { 160 | t.Skip("Timeout test not available with localhost NTP server.") 161 | return 162 | } 163 | 164 | var simuTime atomic.Value 165 | simuTime.Store(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) 166 | 167 | const timerInterval = 1 * time.Millisecond 168 | ctx := t.Context() 169 | 170 | // Start a simulated clock independent of the system wall clock, 171 | // initialized at 2020-01-01T00:00:00, advancing in 1 ms increments. 172 | go func() { 173 | ticker := time.NewTicker(timerInterval) 174 | defer ticker.Stop() 175 | 176 | for { 177 | select { 178 | case <-ctx.Done(): 179 | return 180 | case <-ticker.C: 181 | current := simuTime.Load().(time.Time) 182 | simuTime.Store(current.Add(timerInterval)) 183 | } 184 | } 185 | }() 186 | 187 | r, err := QueryWithOptions(host, QueryOptions{ 188 | GetSystemTime: func() time.Time { return simuTime.Load().(time.Time) }, 189 | }) 190 | if isNil(t, host, err) { 191 | tm := simuTime.Load().(time.Time) 192 | trueTime := tm.Add(r.ClockOffset) 193 | t.Logf(" Custom Time: %s\n", tm.Format(timeFormat)) 194 | t.Logf(" ~True Time: %s\n", trueTime.Format(timeFormat)) 195 | t.Logf("~ClockOffset: %v\n", trueTime.Sub(tm)) 196 | } 197 | } 198 | 199 | func TestOfflineConvertLong(t *testing.T) { 200 | ts := []ntpTime{0x0, 0xff800000, 0x1ff800000, 0x80000000ff800000, 0xffffffffff800000} 201 | for _, v := range ts { 202 | assert.Equal(t, v, toNtpTime(v.Time())) 203 | } 204 | } 205 | 206 | func TestOfflineConvertShort(t *testing.T) { 207 | cases := []struct { 208 | NtpTime ntpTimeShort 209 | Duration time.Duration 210 | }{ 211 | {0x00000000, 0 * time.Nanosecond}, 212 | {0x00000001, 15259 * time.Nanosecond}, 213 | {0x00008000, 500 * time.Millisecond}, 214 | {0x0000c000, 750 * time.Millisecond}, 215 | {0x0000ff80, time.Second - (1000000000/512)*time.Nanosecond}, 216 | {0x00010000, 1000 * time.Millisecond}, 217 | {0x00018000, 1500 * time.Millisecond}, 218 | {0xffff0000, 65535 * time.Second}, 219 | {0xffffff80, 65536*time.Second - (1000000000/512)*time.Nanosecond}, 220 | } 221 | 222 | for _, c := range cases { 223 | ts := c.NtpTime 224 | assert.Equal(t, c.Duration, ts.Duration()) 225 | } 226 | } 227 | 228 | func TestOfflineCustomDialer(t *testing.T) { 229 | raddr := "remote:123" 230 | laddr := "local" 231 | dialerCalled := false 232 | notDialingErr := errors.New("not dialing") 233 | 234 | customDialer := func(la, ra string) (net.Conn, error) { 235 | assert.Equal(t, laddr, la) 236 | assert.Equal(t, raddr, ra) 237 | // Only expect to be called once: 238 | assert.False(t, dialerCalled) 239 | 240 | dialerCalled = true 241 | return nil, notDialingErr 242 | } 243 | 244 | opt := QueryOptions{ 245 | LocalAddress: laddr, 246 | Dialer: customDialer, 247 | } 248 | r, err := QueryWithOptions(raddr, opt) 249 | assert.Nil(t, r) 250 | assert.Equal(t, notDialingErr, err) 251 | assert.True(t, dialerCalled) 252 | } 253 | 254 | func TestOfflineCustomDialerDeprecated(t *testing.T) { 255 | raddr := "remote" 256 | laddr := "local" 257 | dialerCalled := false 258 | notDialingErr := errors.New("not dialing") 259 | 260 | customDial := func(la string, lp int, ra string, rp int) (net.Conn, error) { 261 | assert.Equal(t, laddr, la) 262 | assert.Equal(t, 0, lp) 263 | assert.Equal(t, raddr, ra) 264 | assert.Equal(t, 123, rp) 265 | // Only expect to be called once: 266 | assert.False(t, dialerCalled) 267 | 268 | dialerCalled = true 269 | return nil, notDialingErr 270 | } 271 | 272 | opt := QueryOptions{ 273 | LocalAddress: laddr, 274 | Dial: customDial, 275 | } 276 | r, err := QueryWithOptions(raddr, opt) 277 | assert.Nil(t, r) 278 | assert.Equal(t, notDialingErr, err) 279 | assert.True(t, dialerCalled) 280 | } 281 | 282 | func TestOfflineFixHostPort(t *testing.T) { 283 | const defaultPort = 123 284 | 285 | cases := []struct { 286 | address string 287 | fixed string 288 | errMsg string 289 | }{ 290 | {"192.168.1.1", "192.168.1.1:123", ""}, 291 | {"192.168.1.1:123", "192.168.1.1:123", ""}, 292 | {"192.168.1.1:1000", "192.168.1.1:1000", ""}, 293 | {"[192.168.1.1]:1000", "[192.168.1.1]:1000", ""}, 294 | {"www.example.com", "www.example.com:123", ""}, 295 | {"www.example.com:123", "www.example.com:123", ""}, 296 | {"www.example.com:1000", "www.example.com:1000", ""}, 297 | {"[www.example.com]:1000", "[www.example.com]:1000", ""}, 298 | {"::1", "[::1]:123", ""}, 299 | {"[::1]", "[::1]:123", ""}, 300 | {"[::1]:123", "[::1]:123", ""}, 301 | {"[::1]:1000", "[::1]:1000", ""}, 302 | {"fe80::1", "[fe80::1]:123", ""}, 303 | {"[fe80::1]", "[fe80::1]:123", ""}, 304 | {"[fe80::1]:123", "[fe80::1]:123", ""}, 305 | {"[fe80::1]:1000", "[fe80::1]:1000", ""}, 306 | {"[fe80::", "", "missing ']' in address"}, 307 | {"[fe80::]@", "", "unexpected character following ']' in address"}, 308 | {"ff06:0:0:0:0:0:0:c3", "[ff06:0:0:0:0:0:0:c3]:123", ""}, 309 | {"[ff06:0:0:0:0:0:0:c3]", "[ff06:0:0:0:0:0:0:c3]:123", ""}, 310 | {"[ff06:0:0:0:0:0:0:c3]:123", "[ff06:0:0:0:0:0:0:c3]:123", ""}, 311 | {"[ff06:0:0:0:0:0:0:c3]:1000", "[ff06:0:0:0:0:0:0:c3]:1000", ""}, 312 | {"::ffff:192.168.1.1", "[::ffff:192.168.1.1]:123", ""}, 313 | {"[::ffff:192.168.1.1]", "[::ffff:192.168.1.1]:123", ""}, 314 | {"[::ffff:192.168.1.1]:123", "[::ffff:192.168.1.1]:123", ""}, 315 | {"[::ffff:192.168.1.1]:1000", "[::ffff:192.168.1.1]:1000", ""}, 316 | {"", "", "address string is empty"}, 317 | } 318 | for _, c := range cases { 319 | fixed, err := fixHostPort(c.address, defaultPort) 320 | errMsg := "" 321 | if err != nil { 322 | errMsg = err.Error() 323 | } 324 | assert.Equal(t, c.fixed, fixed) 325 | assert.Equal(t, c.errMsg, errMsg) 326 | } 327 | } 328 | 329 | func TestOfflineKissCode(t *testing.T) { 330 | codes := []struct { 331 | id uint32 332 | str string 333 | }{ 334 | {0x41435354, "ACST"}, 335 | {0x41555448, "AUTH"}, 336 | {0x4155544f, "AUTO"}, 337 | {0x42435354, "BCST"}, 338 | {0x43525950, "CRYP"}, 339 | {0x44454e59, "DENY"}, 340 | {0x44524f50, "DROP"}, 341 | {0x52535452, "RSTR"}, 342 | {0x494e4954, "INIT"}, 343 | {0x4d435354, "MCST"}, 344 | {0x4e4b4559, "NKEY"}, 345 | {0x4e54534e, "NTSN"}, 346 | {0x52415445, "RATE"}, 347 | {0x524d4f54, "RMOT"}, 348 | {0x53544550, "STEP"}, 349 | {0x01010101, ""}, 350 | {0xfefefefe, ""}, 351 | {0x01544450, ""}, 352 | {0x41544401, ""}, 353 | } 354 | for _, c := range codes { 355 | assert.Equal(t, kissCode(c.id), c.str) 356 | } 357 | } 358 | 359 | func TestOfflineMinError(t *testing.T) { 360 | start := time.Now() 361 | h := &header{ 362 | Stratum: 1, 363 | ReferenceID: refID, 364 | ReferenceTime: toNtpTime(start), 365 | OriginTime: toNtpTime(start.Add(1 * time.Second)), 366 | ReceiveTime: toNtpTime(start.Add(2 * time.Second)), 367 | TransmitTime: toNtpTime(start.Add(3 * time.Second)), 368 | } 369 | r := generateResponse(h, toNtpTime(start.Add(4*time.Second)), nil) 370 | assertValid(t, r) 371 | assert.Equal(t, r.MinError, time.Duration(0)) 372 | 373 | for org := 1 * time.Second; org <= 10*time.Second; org += time.Second { 374 | for rec := 1 * time.Second; rec <= 10*time.Second; rec += time.Second { 375 | for xmt := rec; xmt <= 10*time.Second; xmt += time.Second { 376 | for dst := org; dst <= 10*time.Second; dst += time.Second { 377 | h.OriginTime = toNtpTime(start.Add(org)) 378 | h.ReceiveTime = toNtpTime(start.Add(rec)) 379 | h.TransmitTime = toNtpTime(start.Add(xmt)) 380 | r = generateResponse(h, toNtpTime(start.Add(dst)), nil) 381 | assertValid(t, r) 382 | var error0, error1 time.Duration 383 | if org >= rec { 384 | error0 = org - rec 385 | } 386 | if xmt >= dst { 387 | error1 = xmt - dst 388 | } 389 | var minError time.Duration 390 | if error0 > error1 { 391 | minError = error0 392 | } else { 393 | minError = error1 394 | } 395 | assert.Equal(t, r.MinError, minError) 396 | } 397 | } 398 | } 399 | } 400 | } 401 | 402 | func TestOfflineOffsetCalculation(t *testing.T) { 403 | now := time.Now() 404 | t1 := toNtpTime(now) 405 | t2 := toNtpTime(now.Add(20 * time.Second)) 406 | t3 := toNtpTime(now.Add(21 * time.Second)) 407 | t4 := toNtpTime(now.Add(5 * time.Second)) 408 | 409 | // expectedOffset := ((T2 - T1) + (T3 - T4)) / 2 410 | // ((119 - 99) + (121 - 104)) / 2 411 | // (20 + 17) / 2 412 | // 37 / 2 = 18 413 | expectedOffset := 18 * time.Second 414 | offset := offset(t1, t2, t3, t4) 415 | assert.Equal(t, expectedOffset, offset) 416 | } 417 | 418 | func TestOfflineOffsetCalculationNegative(t *testing.T) { 419 | now := time.Now() 420 | t1 := toNtpTime(now.Add(101 * time.Second)) 421 | t2 := toNtpTime(now.Add(102 * time.Second)) 422 | t3 := toNtpTime(now.Add(103 * time.Second)) 423 | t4 := toNtpTime(now.Add(105 * time.Second)) 424 | 425 | // expectedOffset := ((T2 - T1) + (T3 - T4)) / 2 426 | // ((102 - 101) + (103 - 105)) / 2 427 | // (1 + -2) / 2 = -1 / 2 428 | expectedOffset := -time.Second / 2 429 | offset := offset(t1, t2, t3, t4) 430 | assert.Equal(t, expectedOffset, offset) 431 | } 432 | 433 | func TestOfflineOffsetRollover(t *testing.T) { 434 | cases := []struct { 435 | clientTime string 436 | serverTime string 437 | }{ 438 | // both timestamps in NTP era 0 (with large difference) 439 | {"1970-01-01 00:00:00", "2024-05-30 00:00:00"}, 440 | {"2024-05-30 00:00:00", "1970-01-01 00:00:00"}, 441 | 442 | // one timestamp in NTP era 0 and another in era 1 443 | {"2047-01-01 00:00:00", "2024-01-01 00:00:00"}, 444 | {"2024-01-01 00:00:00", "2047-01-01 00:00:00"}, 445 | 446 | // both timestamps in NTP era 1 447 | {"2047-01-01 00:00:00", "2047-02-01 00:00:00"}, 448 | {"2047-02-01 00:00:00", "2047-01-01 00:00:00"}, 449 | } 450 | 451 | timeFormat := "2006-01-02 15:04:05" 452 | 453 | for _, c := range cases { 454 | clientTime, _ := time.Parse(timeFormat, c.clientTime) 455 | serverTime, _ := time.Parse(timeFormat, c.serverTime) 456 | 457 | org := toNtpTime(clientTime) 458 | rec := toNtpTime(serverTime) 459 | xmt := toNtpTime(serverTime.Add(1 * time.Second)) 460 | dst := toNtpTime(clientTime.Add(1 * time.Second)) 461 | 462 | expectedValue := serverTime.Sub(clientTime) 463 | value := offset(org, rec, xmt, dst) 464 | assert.Equal(t, expectedValue, value) 465 | } 466 | } 467 | 468 | func TestOfflineTimeRollover(t *testing.T) { 469 | cases := []struct { 470 | timestamp ntpTime 471 | time string 472 | }{ 473 | {0x0000000000000000, "2036-02-07 06:28:16"}, 474 | {0x0000000100000000, "2036-02-07 06:28:17"}, 475 | {0x1000000000000000, "2044-08-10 03:52:32"}, 476 | {0x2000000000000000, "2053-02-11 01:16:48"}, 477 | {0x3000000000000000, "2061-08-14 22:41:04"}, 478 | {0x4000000000000000, "2070-02-15 20:05:20"}, 479 | {0x5000000000000000, "2078-08-19 17:29:36"}, 480 | {0x6000000000000000, "2087-02-20 14:53:52"}, 481 | {0x7000000000000000, "2095-08-24 12:18:08"}, 482 | {0x8000000000000000, "2104-02-26 09:42:24"}, 483 | {0x83aa7e7000000000, "2106-02-07 06:28:00"}, 484 | {0x83aa7e8000000000, "1970-01-01 00:00:00"}, // <- ntpTime.Time() wrap 485 | {0x9000000000000000, "1976-07-23 00:38:24"}, 486 | {0xa000000000000000, "1985-01-23 22:02:40"}, 487 | {0xb000000000000000, "1993-07-27 19:26:56"}, 488 | {0xc000000000000000, "2002-01-28 16:51:12"}, 489 | {0xd000000000000000, "2010-08-01 14:15:28"}, 490 | {0xe000000000000000, "2019-02-02 11:39:44"}, 491 | {0xf000000000000000, "2027-08-06 09:04:00"}, 492 | {0xffffffff00000000, "2036-02-07 06:28:15"}, 493 | } 494 | 495 | timeFormat := "2006-01-02 15:04:05" 496 | 497 | for _, c := range cases { 498 | tm, _ := time.Parse(timeFormat, c.time) 499 | assert.Equal(t, tm, c.timestamp.Time()) 500 | assert.Equal(t, c.timestamp, toNtpTime(tm)) 501 | } 502 | } 503 | 504 | func TestOfflineReferenceString(t *testing.T) { 505 | cases := []struct { 506 | Stratum byte 507 | RefID uint32 508 | Str string 509 | }{ 510 | {0, 0x41435354, "ACST"}, 511 | {0, 0x41555448, "AUTH"}, 512 | {0, 0x4155544f, "AUTO"}, 513 | {0, 0x42435354, "BCST"}, 514 | {0, 0x43525950, "CRYP"}, 515 | {0, 0x44454e59, "DENY"}, 516 | {0, 0x44524f50, "DROP"}, 517 | {0, 0x52535452, "RSTR"}, 518 | {0, 0x494e4954, "INIT"}, 519 | {0, 0x4d435354, "MCST"}, 520 | {0, 0x4e4b4559, "NKEY"}, 521 | {0, 0x4e54534e, "NTSN"}, 522 | {0, 0x52415445, "RATE"}, 523 | {0, 0x524d4f54, "RMOT"}, 524 | {0, 0x53544550, "STEP"}, 525 | {0, 0x01010101, ""}, 526 | {0, 0xfefefefe, ""}, 527 | {0, 0x01544450, ""}, 528 | {0, 0x41544401, ""}, 529 | {1, 0x47505300, ".GPS."}, 530 | {1, 0x474f4553, ".GOES."}, 531 | {2, 0x0a0a1401, "10.10.20.1"}, 532 | {3, 0xc0a80001, "192.168.0.1"}, 533 | {4, 0xc0a80001, "192.168.0.1"}, 534 | {5, 0xc0a80001, "192.168.0.1"}, 535 | {6, 0xc0a80001, "192.168.0.1"}, 536 | {7, 0xc0a80001, "192.168.0.1"}, 537 | {8, 0xc0a80001, "192.168.0.1"}, 538 | {9, 0xc0a80001, "192.168.0.1"}, 539 | {10, 0xc0a80001, "192.168.0.1"}, 540 | } 541 | for _, c := range cases { 542 | r := Response{Stratum: c.Stratum, ReferenceID: c.RefID} 543 | assert.Equal(t, c.Str, r.ReferenceString()) 544 | } 545 | } 546 | 547 | func TestOfflineTimeConversions(t *testing.T) { 548 | nowNtp := toNtpTime(time.Now()) 549 | now := nowNtp.Time() 550 | startNow := now 551 | for i := 0; i < 100; i++ { 552 | nowNtp = toNtpTime(now) 553 | now = nowNtp.Time() 554 | } 555 | assert.Equal(t, now, startNow) 556 | } 557 | 558 | func TestOfflineValidate(t *testing.T) { 559 | var h header 560 | var r *Response 561 | h.Stratum = 1 562 | h.ReferenceID = refID 563 | h.ReferenceTime = 1 << 32 564 | h.Precision = -1 // 500ms 565 | 566 | // Zero RTT 567 | h.OriginTime = 1 << 32 568 | h.ReceiveTime = 1 << 32 569 | h.TransmitTime = 1 << 32 570 | r = generateResponse(&h, 1<<32, nil) 571 | assertValid(t, r) 572 | 573 | // Negative freshness 574 | h.ReferenceTime = 2 << 32 575 | r = generateResponse(&h, 1<<32, nil) 576 | assertInvalid(t, r) 577 | 578 | // Unfresh clock (48h) 579 | h.OriginTime = 2 * 86400 << 32 580 | h.ReceiveTime = 2 * 86400 << 32 581 | h.TransmitTime = 2 * 86400 << 32 582 | r = generateResponse(&h, 2*86400<<32, nil) 583 | assertInvalid(t, r) 584 | 585 | // Fresh clock (24h) 586 | h.ReferenceTime = 1 * 86400 << 32 587 | r = generateResponse(&h, 2*86400<<32, nil) 588 | assertValid(t, r) 589 | 590 | // Values indicating a negative RTT 591 | h.RootDelay = 16 << 16 592 | h.ReferenceTime = 1 << 32 593 | h.OriginTime = 20 << 32 594 | h.ReceiveTime = 10 << 32 595 | h.TransmitTime = 15 << 32 596 | r = generateResponse(&h, 22<<32, nil) 597 | assert.NotNil(t, r) 598 | assertValid(t, r) 599 | assert.Equal(t, r.RTT, 0*time.Second) 600 | assert.Equal(t, r.RootDistance, 8*time.Second) 601 | } 602 | -------------------------------------------------------------------------------- /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 | // GetSystemTime is a callback used to override the default method of 240 | // obtaining the local system time during time synchronization. If not 241 | // specified, time.Now is used. 242 | GetSystemTime func() time.Time 243 | 244 | // Dialer is a callback used to override the default UDP network dialer. 245 | // The localAddress is directly copied from the LocalAddress field 246 | // specified in QueryOptions. It may be the empty string or a host address 247 | // (without port number). The remoteAddress is the "host:port" string 248 | // derived from the first parameter to QueryWithOptions. The 249 | // remoteAddress is guaranteed to include a port number. 250 | Dialer func(localAddress, remoteAddress string) (net.Conn, error) 251 | 252 | // Dial is a callback used to override the default UDP network dialer. 253 | // 254 | // DEPRECATED. Use Dialer instead. 255 | Dial func(laddr string, lport int, raddr string, rport int) (net.Conn, error) 256 | 257 | // Port indicates the port used to reach the remote NTP server. 258 | // 259 | // DEPRECATED. Embed the port number in the query address string instead. 260 | Port int 261 | } 262 | 263 | // A Response contains time data, some of which is returned by the NTP server 264 | // and some of which is calculated by this client. 265 | type Response struct { 266 | // ClockOffset is the estimated offset of the local system clock relative 267 | // to the server's clock. Add this value to subsequent local system clock 268 | // times in order to obtain a time that is synchronized to the server's 269 | // clock. 270 | ClockOffset time.Duration 271 | 272 | // Time is the time the server transmitted this response, measured using 273 | // its own clock. You should not use this value for time synchronization 274 | // purposes. Add ClockOffset to your system clock instead. 275 | Time time.Time 276 | 277 | // RTT is the measured round-trip-time delay estimate between the client 278 | // and the server. 279 | RTT time.Duration 280 | 281 | // Precision is the reported precision of the server's clock. 282 | Precision time.Duration 283 | 284 | // Version is the NTP protocol version number reported by the server. 285 | Version int 286 | 287 | // Stratum is the "stratum level" of the server. The smaller the number, 288 | // the closer the server is to the reference clock. Stratum 1 servers are 289 | // attached directly to the reference clock. A stratum value of 0 290 | // indicates the "kiss of death," which typically occurs when the client 291 | // issues too many requests to the server in a short period of time. 292 | Stratum uint8 293 | 294 | // ReferenceID is a 32-bit integer identifying the server or reference 295 | // clock. For stratum 1 servers, this is typically a meaningful 296 | // zero-padded ASCII-encoded string assigned to the clock. For stratum 2+ 297 | // servers, this is a reference identifier for the server and is either 298 | // the server's IPv4 address or a hash of its IPv6 address. For 299 | // kiss-of-death responses (stratum 0), this is the ASCII-encoded "kiss 300 | // code". 301 | ReferenceID uint32 302 | 303 | // ReferenceTime is the time the server last updated its local clock. 304 | ReferenceTime time.Time 305 | 306 | // RootDelay is the server's estimated aggregate round-trip-time delay to 307 | // the stratum 1 server. 308 | RootDelay time.Duration 309 | 310 | // RootDispersion is the server's estimated maximum measurement error 311 | // relative to the stratum 1 server. 312 | RootDispersion time.Duration 313 | 314 | // RootDistance is an estimate of the total synchronization distance 315 | // between the client and the stratum 1 server. 316 | RootDistance time.Duration 317 | 318 | // Leap indicates whether a leap second should be added or removed from 319 | // the current month's last minute. 320 | Leap LeapIndicator 321 | 322 | // MinError is a lower bound on the error between the client and server 323 | // clocks. When the client and server are not synchronized to the same 324 | // clock, the reported timestamps may appear to violate the principle of 325 | // causality. In other words, the NTP server's response may indicate 326 | // that a message was received before it was sent. In such cases, the 327 | // minimum error may be useful. 328 | MinError time.Duration 329 | 330 | // KissCode is a 4-character string describing the reason for a 331 | // "kiss of death" response (stratum=0). For a list of standard kiss 332 | // codes, see https://tools.ietf.org/html/rfc5905#section-7.4. 333 | KissCode string 334 | 335 | // Poll is the maximum interval between successive NTP query messages to 336 | // the server. 337 | Poll time.Duration 338 | 339 | authErr error 340 | } 341 | 342 | // IsKissOfDeath returns true if the response is a "kiss of death" from the 343 | // remote server. If this function returns true, you may examine the 344 | // response's KissCode value to determine the reason for the kiss of death. 345 | func (r *Response) IsKissOfDeath() bool { 346 | return r.Stratum == 0 347 | } 348 | 349 | // ReferenceString returns the response's ReferenceID value formatted as a 350 | // string. If the response's stratum is zero, then the "kiss o' death" string 351 | // is returned. If stratum is one, then the server is a reference clock and 352 | // the reference clock's name is returned. If stratum is two or greater, then 353 | // the ID is either an IPv4 address or an MD5 hash of the IPv6 address; in 354 | // either case the reference string is reported as 4 dot-separated 355 | // decimal-based integers. 356 | func (r *Response) ReferenceString() string { 357 | if r.Stratum == 0 { 358 | return kissCode(r.ReferenceID) 359 | } 360 | 361 | var b [4]byte 362 | binary.BigEndian.PutUint32(b[:], r.ReferenceID) 363 | 364 | if r.Stratum == 1 { 365 | const dot = rune(0x22c5) 366 | var r []rune 367 | for i := range b { 368 | if b[i] == 0 { 369 | break 370 | } 371 | if b[i] >= 32 && b[i] <= 126 { 372 | r = append(r, rune(b[i])) 373 | } else { 374 | r = append(r, dot) 375 | } 376 | } 377 | return fmt.Sprintf(".%s.", string(r)) 378 | } 379 | 380 | return fmt.Sprintf("%d.%d.%d.%d", b[0], b[1], b[2], b[3]) 381 | } 382 | 383 | // Validate checks if the response is valid for the purposes of time 384 | // synchronization. 385 | func (r *Response) Validate() error { 386 | // Forward authentication errors. 387 | if r.authErr != nil { 388 | return r.authErr 389 | } 390 | 391 | // Handle invalid stratum values. 392 | if r.Stratum == 0 { 393 | return ErrKissOfDeath 394 | } 395 | if r.Stratum >= maxStratum { 396 | return ErrInvalidStratum 397 | } 398 | 399 | // Estimate the "freshness" of the time. If it exceeds the maximum 400 | // polling interval (~36 hours), then it cannot be considered "fresh". 401 | freshness := r.Time.Sub(r.ReferenceTime) 402 | if freshness > maxPollInterval { 403 | return ErrServerClockFreshness 404 | } 405 | 406 | // Calculate the peer synchronization distance, lambda: 407 | // lambda := RootDelay/2 + RootDispersion 408 | // If this value exceeds MAXDISP (16s), then the time is not suitable 409 | // for synchronization purposes. 410 | // https://tools.ietf.org/html/rfc5905#appendix-A.5.1.1. 411 | lambda := r.RootDelay/2 + r.RootDispersion 412 | if lambda > maxDispersion { 413 | return ErrInvalidDispersion 414 | } 415 | 416 | // If the server's transmit time is before its reference time, the 417 | // response is invalid. 418 | if r.Time.Before(r.ReferenceTime) { 419 | return ErrInvalidTime 420 | } 421 | 422 | // Handle invalid leap second indicator. 423 | if r.Leap == LeapNotInSync { 424 | return ErrInvalidLeapSecond 425 | } 426 | 427 | // nil means the response is valid. 428 | return nil 429 | } 430 | 431 | // Query requests time data from a remote NTP server. The response contains 432 | // information from which a more accurate local time can be inferred. 433 | // 434 | // The server address is of the form "host", "host:port", "host%zone:port", 435 | // "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or 436 | // domain name address. When specifying both a port and an IPv6 address, one 437 | // of the bracket formats must be used. If no port is included, NTP default 438 | // port 123 is used. 439 | func Query(address string) (*Response, error) { 440 | return QueryWithOptions(address, QueryOptions{}) 441 | } 442 | 443 | // QueryWithOptions performs the same function as Query but allows for the 444 | // customization of certain query behaviors. See the comments for Query and 445 | // QueryOptions for further details. 446 | func QueryWithOptions(address string, opt QueryOptions) (*Response, error) { 447 | h, now, err := getTime(address, &opt) 448 | if err != nil && err != ErrAuthFailed { 449 | return nil, err 450 | } 451 | 452 | return generateResponse(h, now, err), nil 453 | } 454 | 455 | // Time returns the current, corrected local time using information returned 456 | // from the remote NTP server. On error, Time returns the uncorrected local 457 | // system time. 458 | // 459 | // The server address is of the form "host", "host:port", "host%zone:port", 460 | // "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or 461 | // domain name address. When specifying both a port and an IPv6 address, one 462 | // of the bracket formats must be used. If no port is included, NTP default 463 | // port 123 is used. 464 | func Time(address string) (time.Time, error) { 465 | r, err := Query(address) 466 | if err != nil { 467 | return time.Now(), err 468 | } 469 | 470 | err = r.Validate() 471 | if err != nil { 472 | return time.Now(), err 473 | } 474 | 475 | // Use the response's clock offset to calculate an accurate time. 476 | return time.Now().Add(r.ClockOffset), nil 477 | } 478 | 479 | // getTime performs the NTP server query and returns the response header 480 | // along with the local system time it was received. 481 | func getTime(address string, opt *QueryOptions) (*header, ntpTime, error) { 482 | if opt.Timeout == 0 { 483 | opt.Timeout = defaultTimeout 484 | } 485 | if opt.Version == 0 { 486 | opt.Version = defaultNtpVersion 487 | } 488 | if opt.Version < 2 || opt.Version > 4 { 489 | return nil, 0, ErrInvalidProtocolVersion 490 | } 491 | if opt.Port == 0 { 492 | opt.Port = defaultNtpPort 493 | } 494 | if opt.Dial != nil { 495 | // wrapper for the deprecated Dial callback. 496 | opt.Dialer = func(la, ra string) (net.Conn, error) { 497 | return dialWrapper(la, ra, opt.Dial) 498 | } 499 | } 500 | if opt.Dialer == nil { 501 | opt.Dialer = defaultDialer 502 | } 503 | if opt.GetSystemTime == nil { 504 | opt.GetSystemTime = time.Now 505 | } 506 | 507 | // Compose a conforming host:port remote address string if the address 508 | // string doesn't already contain a port. 509 | remoteAddress, err := fixHostPort(address, opt.Port) 510 | if err != nil { 511 | return nil, 0, err 512 | } 513 | 514 | // Connect to the remote server. 515 | con, err := opt.Dialer(opt.LocalAddress, remoteAddress) 516 | if err != nil { 517 | return nil, 0, err 518 | } 519 | defer con.Close() 520 | 521 | // Set a TTL for the packet if requested. 522 | if opt.TTL != 0 { 523 | ipcon := ipv4.NewConn(con) 524 | err = ipcon.SetTTL(opt.TTL) 525 | if err != nil { 526 | return nil, 0, err 527 | } 528 | } 529 | 530 | // Set a timeout on the connection. 531 | con.SetDeadline(time.Now().Add(opt.Timeout)) 532 | 533 | // Allocate a buffer big enough to hold an entire response datagram. 534 | recvBuf := make([]byte, 8192) 535 | recvHdr := new(header) 536 | 537 | // Allocate the query message header. 538 | xmitHdr := new(header) 539 | xmitHdr.setMode(client) 540 | xmitHdr.setVersion(opt.Version) 541 | xmitHdr.setLeap(LeapNoWarning) 542 | xmitHdr.Precision = 0x20 543 | 544 | // To help prevent spoofing and client fingerprinting, use a 545 | // cryptographically random 64-bit value for the TransmitTime. See: 546 | // https://www.ietf.org/archive/id/draft-ietf-ntp-data-minimization-04.txt 547 | bits := make([]byte, 8) 548 | _, err = rand.Read(bits) 549 | if err != nil { 550 | return nil, 0, err 551 | } 552 | xmitHdr.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits)) 553 | 554 | // Write the query header to a transmit buffer. 555 | var xmitBuf bytes.Buffer 556 | binary.Write(&xmitBuf, binary.BigEndian, xmitHdr) 557 | 558 | // Allow extensions to process the query and add to the transmit buffer. 559 | for _, e := range opt.Extensions { 560 | err = e.ProcessQuery(&xmitBuf) 561 | if err != nil { 562 | return nil, 0, err 563 | } 564 | } 565 | 566 | // If using symmetric key authentication, decode and validate the auth key 567 | // string. 568 | authKey, err := decodeAuthKey(opt.Auth) 569 | if err != nil { 570 | return nil, 0, err 571 | } 572 | 573 | // Append a MAC if authentication is being used. 574 | appendMAC(&xmitBuf, opt.Auth, authKey) 575 | 576 | // Transmit the query and keep track of when it was transmitted. 577 | xmitTime := opt.GetSystemTime() 578 | _, err = con.Write(xmitBuf.Bytes()) 579 | if err != nil { 580 | return nil, 0, err 581 | } 582 | 583 | // Receive the response. 584 | recvBytes, err := con.Read(recvBuf) 585 | if err != nil { 586 | return nil, 0, err 587 | } 588 | 589 | // Keep track of the time the response was received. As of go 1.9, the 590 | // time package uses a monotonic clock, so delta will never be less than 591 | // zero for go version 1.9 or higher. 592 | recvTime := opt.GetSystemTime() 593 | if recvTime.Sub(xmitTime) < 0 { 594 | recvTime = xmitTime 595 | } 596 | 597 | // Parse the response header. 598 | recvBuf = recvBuf[:recvBytes] 599 | recvReader := bytes.NewReader(recvBuf) 600 | err = binary.Read(recvReader, binary.BigEndian, recvHdr) 601 | if err != nil { 602 | return nil, 0, err 603 | } 604 | 605 | // Allow extensions to process the response. 606 | for i := len(opt.Extensions) - 1; i >= 0; i-- { 607 | err = opt.Extensions[i].ProcessResponse(recvBuf) 608 | if err != nil { 609 | return nil, 0, err 610 | } 611 | } 612 | 613 | // Check for invalid fields. 614 | if recvHdr.getMode() != server { 615 | return nil, 0, ErrInvalidMode 616 | } 617 | if recvHdr.TransmitTime == ntpTime(0) { 618 | return nil, 0, ErrInvalidTransmitTime 619 | } 620 | if recvHdr.OriginTime != xmitHdr.TransmitTime { 621 | return nil, 0, ErrServerResponseMismatch 622 | } 623 | if recvHdr.ReceiveTime > recvHdr.TransmitTime { 624 | return nil, 0, ErrServerTickedBackwards 625 | } 626 | 627 | // Correct the received message's origin time using the actual 628 | // transmit time. 629 | recvHdr.OriginTime = toNtpTime(xmitTime) 630 | 631 | // Perform authentication of the server response. 632 | authErr := verifyMAC(recvBuf, opt.Auth, authKey) 633 | 634 | return recvHdr, toNtpTime(recvTime), authErr 635 | } 636 | 637 | // defaultDialer provides a UDP dialer based on Go's built-in net stack. 638 | func defaultDialer(localAddress, remoteAddress string) (net.Conn, error) { 639 | var laddr *net.UDPAddr 640 | if localAddress != "" { 641 | var err error 642 | laddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(localAddress, "0")) 643 | if err != nil { 644 | return nil, err 645 | } 646 | } 647 | 648 | raddr, err := net.ResolveUDPAddr("udp", remoteAddress) 649 | if err != nil { 650 | return nil, err 651 | } 652 | 653 | return net.DialUDP("udp", laddr, raddr) 654 | } 655 | 656 | // dialWrapper is used to wrap the deprecated Dial callback in QueryOptions. 657 | func dialWrapper(la, ra string, 658 | dial func(la string, lp int, ra string, rp int) (net.Conn, error)) (net.Conn, error) { 659 | rhost, rport, err := net.SplitHostPort(ra) 660 | if err != nil { 661 | return nil, err 662 | } 663 | 664 | rportValue, err := strconv.Atoi(rport) 665 | if err != nil { 666 | return nil, err 667 | } 668 | 669 | return dial(la, 0, rhost, rportValue) 670 | } 671 | 672 | // fixHostPort examines an address in one of the accepted forms and fixes it 673 | // to include a port number if necessary. 674 | func fixHostPort(address string, defaultPort int) (fixed string, err error) { 675 | if len(address) == 0 { 676 | return "", errors.New("address string is empty") 677 | } 678 | 679 | // If the address is wrapped in brackets, append a port if necessary. 680 | if address[0] == '[' { 681 | end := strings.IndexByte(address, ']') 682 | switch { 683 | case end < 0: 684 | return "", errors.New("missing ']' in address") 685 | case end+1 == len(address): 686 | return fmt.Sprintf("%s:%d", address, defaultPort), nil 687 | case address[end+1] == ':': 688 | return address, nil 689 | default: 690 | return "", errors.New("unexpected character following ']' in address") 691 | } 692 | } 693 | 694 | // No colons? Must be a port-less IPv4 or domain address. 695 | last := strings.LastIndexByte(address, ':') 696 | if last < 0 { 697 | return fmt.Sprintf("%s:%d", address, defaultPort), nil 698 | } 699 | 700 | // Exactly one colon? A port have been included along with an IPv4 or 701 | // domain address. (IPv6 addresses are guaranteed to have more than one 702 | // colon.) 703 | prev := strings.LastIndexByte(address[:last], ':') 704 | if prev < 0 { 705 | return address, nil 706 | } 707 | 708 | // Two or more colons means we must have an IPv6 address without a port. 709 | return fmt.Sprintf("[%s]:%d", address, defaultPort), nil 710 | } 711 | 712 | // generateResponse processes NTP header fields along with the its receive 713 | // time to generate a Response record. 714 | func generateResponse(h *header, recvTime ntpTime, authErr error) *Response { 715 | r := &Response{ 716 | Time: h.TransmitTime.Time(), 717 | ClockOffset: offset(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), 718 | RTT: rtt(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), 719 | Precision: toInterval(h.Precision), 720 | Version: h.getVersion(), 721 | Stratum: h.Stratum, 722 | ReferenceID: h.ReferenceID, 723 | ReferenceTime: h.ReferenceTime.Time(), 724 | RootDelay: h.RootDelay.Duration(), 725 | RootDispersion: h.RootDispersion.Duration(), 726 | Leap: h.getLeap(), 727 | MinError: minError(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime), 728 | Poll: toInterval(h.Poll), 729 | authErr: authErr, 730 | } 731 | 732 | // Calculate values depending on other calculated values 733 | r.RootDistance = rootDistance(r.RTT, r.RootDelay, r.RootDispersion) 734 | 735 | // If a kiss of death was received, interpret the reference ID as 736 | // a kiss code. 737 | if r.Stratum == 0 { 738 | r.KissCode = kissCode(r.ReferenceID) 739 | } 740 | 741 | return r 742 | } 743 | 744 | // The following helper functions calculate additional metadata about the 745 | // timestamps received from an NTP server. The timestamps returned by 746 | // the server are given the following variable names: 747 | // 748 | // org = Origin Timestamp (client send time) 749 | // rec = Receive Timestamp (server receive time) 750 | // xmt = Transmit Timestamp (server reply time) 751 | // dst = Destination Timestamp (client receive time) 752 | 753 | func rtt(org, rec, xmt, dst ntpTime) time.Duration { 754 | a := int64(dst - org) 755 | b := int64(xmt - rec) 756 | rtt := a - b 757 | if rtt < 0 { 758 | rtt = 0 759 | } 760 | return ntpTime(rtt).Duration() 761 | } 762 | 763 | func offset(org, rec, xmt, dst ntpTime) time.Duration { 764 | // The inputs are 64-bit unsigned integer timestamps. These timestamps can 765 | // "roll over" at the end of an NTP era, which occurs approximately every 766 | // 136 years starting from the year 1900. To ensure an accurate offset 767 | // calculation when an era boundary is crossed, we need to take care that 768 | // the difference between two 64-bit timestamp values is accurately 769 | // calculated even when they are in neighboring eras. 770 | // 771 | // See: https://www.eecis.udel.edu/~mills/y2k.html 772 | 773 | a := int64(rec - org) 774 | b := int64(xmt - dst) 775 | offset := a + (b-a)/2 776 | if offset < 0 { 777 | return -ntpTime(-offset).Duration() 778 | } 779 | return ntpTime(offset).Duration() 780 | } 781 | 782 | func minError(org, rec, xmt, dst ntpTime) time.Duration { 783 | // Each NTP response contains two pairs of send/receive timestamps. 784 | // When either pair indicates a "causality violation", we calculate the 785 | // error as the difference in time between them. The minimum error is 786 | // the greater of the two causality violations. 787 | var error0, error1 ntpTime 788 | if org >= rec { 789 | error0 = org - rec 790 | } 791 | if xmt >= dst { 792 | error1 = xmt - dst 793 | } 794 | if error0 > error1 { 795 | return error0.Duration() 796 | } 797 | return error1.Duration() 798 | } 799 | 800 | func rootDistance(rtt, rootDelay, rootDisp time.Duration) time.Duration { 801 | // The root distance is: 802 | // the maximum error due to all causes of the local clock 803 | // relative to the primary server. It is defined as half the 804 | // total delay plus total dispersion plus peer jitter. 805 | // (https://tools.ietf.org/html/rfc5905#appendix-A.5.5.2) 806 | // 807 | // In the reference implementation, it is calculated as follows: 808 | // rootDist = max(MINDISP, rootDelay + rtt)/2 + rootDisp 809 | // + peerDisp + PHI * (uptime - peerUptime) 810 | // + peerJitter 811 | // For an SNTP client which sends only a single packet, most of these 812 | // terms are irrelevant and become 0. 813 | totalDelay := rtt + rootDelay 814 | return totalDelay/2 + rootDisp 815 | } 816 | 817 | func toInterval(t int8) time.Duration { 818 | switch { 819 | case t > 0: 820 | return time.Duration(uint64(time.Second) << uint(t)) 821 | case t < 0: 822 | return time.Duration(uint64(time.Second) >> uint(-t)) 823 | default: 824 | return time.Second 825 | } 826 | } 827 | 828 | func kissCode(id uint32) string { 829 | isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 } 830 | 831 | b := [4]byte{ 832 | byte(id >> 24), 833 | byte(id >> 16), 834 | byte(id >> 8), 835 | byte(id), 836 | } 837 | for _, ch := range b { 838 | if !isPrintable(ch) { 839 | return "" 840 | } 841 | } 842 | return string(b[:]) 843 | } 844 | --------------------------------------------------------------------------------