mirror of https://github.com/jetkvm/kvm.git
828 lines
25 KiB
Go
828 lines
25 KiB
Go
// Copyright © 2015-2023 Brett Vickers.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package ntp provides an implementation of a Simple NTP (SNTP) client
|
|
// capable of querying the current time from a remote NTP server. See
|
|
// RFC 5905 (https://tools.ietf.org/html/rfc5905) for more details.
|
|
//
|
|
// This approach grew out of a go-nuts post by Michael Hofmann:
|
|
// https://groups.google.com/forum/?fromgroups#!topic/golang-nuts/FlcdMU5fkLQ
|
|
package ntp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/net/ipv4"
|
|
)
|
|
|
|
var (
|
|
ErrAuthFailed = errors.New("authentication failed")
|
|
ErrInvalidAuthKey = errors.New("invalid authentication key")
|
|
ErrInvalidDispersion = errors.New("invalid dispersion in response")
|
|
ErrInvalidLeapSecond = errors.New("invalid leap second in response")
|
|
ErrInvalidMode = errors.New("invalid mode in response")
|
|
ErrInvalidProtocolVersion = errors.New("invalid protocol version requested")
|
|
ErrInvalidStratum = errors.New("invalid stratum in response")
|
|
ErrInvalidTime = errors.New("invalid time reported")
|
|
ErrInvalidTransmitTime = errors.New("invalid transmit time in response")
|
|
ErrKissOfDeath = errors.New("kiss of death received")
|
|
ErrServerClockFreshness = errors.New("server clock not fresh")
|
|
ErrServerResponseMismatch = errors.New("server response didn't match request")
|
|
ErrServerTickedBackwards = errors.New("server clock ticked backwards")
|
|
)
|
|
|
|
// The LeapIndicator is used to warn if a leap second should be inserted
|
|
// or deleted in the last minute of the current month.
|
|
type LeapIndicator uint8
|
|
|
|
const (
|
|
// LeapNoWarning indicates no impending leap second.
|
|
LeapNoWarning LeapIndicator = 0
|
|
|
|
// LeapAddSecond indicates the last minute of the day has 61 seconds.
|
|
LeapAddSecond = 1
|
|
|
|
// LeapDelSecond indicates the last minute of the day has 59 seconds.
|
|
LeapDelSecond = 2
|
|
|
|
// LeapNotInSync indicates an unsynchronized leap second.
|
|
LeapNotInSync = 3
|
|
)
|
|
|
|
// Internal constants
|
|
const (
|
|
defaultNtpVersion = 4
|
|
defaultNtpPort = 123
|
|
nanoPerSec = 1000000000
|
|
maxStratum = 16
|
|
defaultTimeout = 5 * time.Second
|
|
maxPollInterval = (1 << 17) * time.Second
|
|
maxDispersion = 16 * time.Second
|
|
)
|
|
|
|
// Internal variables
|
|
var (
|
|
ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
)
|
|
|
|
type mode uint8
|
|
|
|
// NTP modes. This package uses only client mode.
|
|
const (
|
|
reserved mode = 0 + iota
|
|
symmetricActive
|
|
symmetricPassive
|
|
client
|
|
server
|
|
broadcast
|
|
controlMessage
|
|
reservedPrivate
|
|
)
|
|
|
|
// An ntpTime is a 64-bit fixed-point (Q32.32) representation of the number of
|
|
// seconds elapsed.
|
|
type ntpTime uint64
|
|
|
|
// Duration interprets the fixed-point ntpTime as a number of elapsed seconds
|
|
// and returns the corresponding time.Duration value.
|
|
func (t ntpTime) Duration() time.Duration {
|
|
sec := (t >> 32) * nanoPerSec
|
|
frac := (t & 0xffffffff) * nanoPerSec
|
|
nsec := frac >> 32
|
|
if uint32(frac) >= 0x80000000 {
|
|
nsec++
|
|
}
|
|
return time.Duration(sec + nsec)
|
|
}
|
|
|
|
// Time interprets the fixed-point ntpTime as an absolute time and returns
|
|
// the corresponding time.Time value.
|
|
func (t ntpTime) Time() time.Time {
|
|
return ntpEpoch.Add(t.Duration())
|
|
}
|
|
|
|
// toNtpTime converts the time.Time value t into its 64-bit fixed-point
|
|
// ntpTime representation.
|
|
func toNtpTime(t time.Time) ntpTime {
|
|
nsec := uint64(t.Sub(ntpEpoch))
|
|
sec := nsec / nanoPerSec
|
|
nsec = uint64(nsec-sec*nanoPerSec) << 32
|
|
frac := uint64(nsec / nanoPerSec)
|
|
if nsec%nanoPerSec >= nanoPerSec/2 {
|
|
frac++
|
|
}
|
|
return ntpTime(sec<<32 | frac)
|
|
}
|
|
|
|
// An ntpTimeShort is a 32-bit fixed-point (Q16.16) representation of the
|
|
// number of seconds elapsed.
|
|
type ntpTimeShort uint32
|
|
|
|
// Duration interprets the fixed-point ntpTimeShort as a number of elapsed
|
|
// seconds and returns the corresponding time.Duration value.
|
|
func (t ntpTimeShort) Duration() time.Duration {
|
|
sec := uint64(t>>16) * nanoPerSec
|
|
frac := uint64(t&0xffff) * nanoPerSec
|
|
nsec := frac >> 16
|
|
if uint16(frac) >= 0x8000 {
|
|
nsec++
|
|
}
|
|
return time.Duration(sec + nsec)
|
|
}
|
|
|
|
// header is an internal representation of an NTP packet header.
|
|
type header struct {
|
|
LiVnMode uint8 // Leap Indicator (2) + Version (3) + Mode (3)
|
|
Stratum uint8
|
|
Poll int8
|
|
Precision int8
|
|
RootDelay ntpTimeShort
|
|
RootDispersion ntpTimeShort
|
|
ReferenceID uint32 // KoD code if Stratum == 0
|
|
ReferenceTime ntpTime
|
|
OriginTime ntpTime
|
|
ReceiveTime ntpTime
|
|
TransmitTime ntpTime
|
|
}
|
|
|
|
// setVersion sets the NTP protocol version on the header.
|
|
func (h *header) setVersion(v int) {
|
|
h.LiVnMode = (h.LiVnMode & 0xc7) | uint8(v)<<3
|
|
}
|
|
|
|
// setMode sets the NTP protocol mode on the header.
|
|
func (h *header) setMode(md mode) {
|
|
h.LiVnMode = (h.LiVnMode & 0xf8) | uint8(md)
|
|
}
|
|
|
|
// setLeap modifies the leap indicator on the header.
|
|
func (h *header) setLeap(li LeapIndicator) {
|
|
h.LiVnMode = (h.LiVnMode & 0x3f) | uint8(li)<<6
|
|
}
|
|
|
|
// getVersion returns the version value in the header.
|
|
func (h *header) getVersion() int {
|
|
return int((h.LiVnMode >> 3) & 0x7)
|
|
}
|
|
|
|
// getMode returns the mode value in the header.
|
|
func (h *header) getMode() mode {
|
|
return mode(h.LiVnMode & 0x07)
|
|
}
|
|
|
|
// getLeap returns the leap indicator on the header.
|
|
func (h *header) getLeap() LeapIndicator {
|
|
return LeapIndicator((h.LiVnMode >> 6) & 0x03)
|
|
}
|
|
|
|
// An Extension adds custom behaviors capable of modifying NTP packets before
|
|
// being sent to the server and processing packets after being received by the
|
|
// server.
|
|
type Extension interface {
|
|
// ProcessQuery is called when the client is about to send a query to the
|
|
// NTP server. The buffer contains the NTP header. It may also contain
|
|
// extension fields added by extensions processed prior to this one.
|
|
ProcessQuery(buf *bytes.Buffer) error
|
|
|
|
// ProcessResponse is called after the client has received the server's
|
|
// NTP response. The buffer contains the entire message returned by the
|
|
// server.
|
|
ProcessResponse(buf []byte) error
|
|
}
|
|
|
|
// QueryOptions contains configurable options used by the QueryWithOptions
|
|
// function.
|
|
type QueryOptions struct {
|
|
// Timeout determines how long the client waits for a response from the
|
|
// server before failing with a timeout error. Defaults to 5 seconds.
|
|
Timeout time.Duration
|
|
|
|
// Version of the NTP protocol to use. Defaults to 4.
|
|
Version int
|
|
|
|
// LocalAddress contains the local IP address to use when creating a
|
|
// connection to the remote NTP server. This may be useful when the local
|
|
// system has more than one IP address. This address should not contain
|
|
// a port number.
|
|
LocalAddress string
|
|
|
|
// TTL specifies the maximum number of IP hops before the query datagram
|
|
// is dropped by the network. Defaults to the local system's default value.
|
|
TTL int
|
|
|
|
// Auth contains the settings used to configure NTP symmetric key
|
|
// authentication. See RFC 5905 for further details.
|
|
Auth AuthOptions
|
|
|
|
// Extensions may be added to modify NTP queries before they are
|
|
// transmitted and to process NTP responses after they arrive.
|
|
Extensions []Extension
|
|
|
|
// Dialer is a callback used to override the default UDP network dialer.
|
|
// The localAddress is directly copied from the LocalAddress field
|
|
// specified in QueryOptions. It may be the empty string or a host address
|
|
// (without port number). The remoteAddress is the "host:port" string
|
|
// derived from the first parameter to QueryWithOptions. The
|
|
// remoteAddress is guaranteed to include a port number.
|
|
Dialer func(localAddress, remoteAddress string) (net.Conn, error)
|
|
|
|
// Dial is a callback used to override the default UDP network dialer.
|
|
//
|
|
// DEPRECATED. Use Dialer instead.
|
|
Dial func(laddr string, lport int, raddr string, rport int) (net.Conn, error)
|
|
|
|
// Port indicates the port used to reach the remote NTP server.
|
|
//
|
|
// DEPRECATED. Embed the port number in the query address string instead.
|
|
Port int
|
|
}
|
|
|
|
// A Response contains time data, some of which is returned by the NTP server
|
|
// and some of which is calculated by this client.
|
|
type Response struct {
|
|
// Time is the transmit time reported by the server just before it
|
|
// responded to the client's NTP query. You should not use this value
|
|
// for time synchronization purposes. Use the ClockOffset instead.
|
|
Time time.Time
|
|
|
|
// ClockOffset is the estimated offset of the local system clock relative
|
|
// to the server's clock. Add this value to subsequent local system time
|
|
// measurements in order to obtain a more accurate time.
|
|
ClockOffset time.Duration
|
|
|
|
// RTT is the measured round-trip-time delay estimate between the client
|
|
// and the server.
|
|
RTT time.Duration
|
|
|
|
// Precision is the reported precision of the server's clock.
|
|
Precision time.Duration
|
|
|
|
// Version is the NTP protocol version number reported by the server.
|
|
Version int
|
|
|
|
// Stratum is the "stratum level" of the server. The smaller the number,
|
|
// the closer the server is to the reference clock. Stratum 1 servers are
|
|
// attached directly to the reference clock. A stratum value of 0
|
|
// indicates the "kiss of death," which typically occurs when the client
|
|
// issues too many requests to the server in a short period of time.
|
|
Stratum uint8
|
|
|
|
// ReferenceID is a 32-bit integer identifying the server or reference
|
|
// clock. For stratum 1 servers, this is typically a meaningful
|
|
// zero-padded ASCII-encoded string assigned to the clock. For stratum 2+
|
|
// servers, this is a reference identifier for the server and is either
|
|
// the server's IPv4 address or a hash of its IPv6 address. For
|
|
// kiss-of-death responses (stratum 0), this is the ASCII-encoded "kiss
|
|
// code".
|
|
ReferenceID uint32
|
|
|
|
// ReferenceTime is the time when the server's system clock was last
|
|
// set or corrected.
|
|
ReferenceTime time.Time
|
|
|
|
// RootDelay is the server's estimated aggregate round-trip-time delay to
|
|
// the stratum 1 server.
|
|
RootDelay time.Duration
|
|
|
|
// RootDispersion is the server's estimated maximum measurement error
|
|
// relative to the stratum 1 server.
|
|
RootDispersion time.Duration
|
|
|
|
// RootDistance is an estimate of the total synchronization distance
|
|
// between the client and the stratum 1 server.
|
|
RootDistance time.Duration
|
|
|
|
// Leap indicates whether a leap second should be added or removed from
|
|
// the current month's last minute.
|
|
Leap LeapIndicator
|
|
|
|
// MinError is a lower bound on the error between the client and server
|
|
// clocks. When the client and server are not synchronized to the same
|
|
// clock, the reported timestamps may appear to violate the principle of
|
|
// causality. In other words, the NTP server's response may indicate
|
|
// that a message was received before it was sent. In such cases, the
|
|
// minimum error may be useful.
|
|
MinError time.Duration
|
|
|
|
// KissCode is a 4-character string describing the reason for a
|
|
// "kiss of death" response (stratum=0). For a list of standard kiss
|
|
// codes, see https://tools.ietf.org/html/rfc5905#section-7.4.
|
|
KissCode string
|
|
|
|
// Poll is the maximum interval between successive NTP query messages to
|
|
// the server.
|
|
Poll time.Duration
|
|
|
|
authErr error
|
|
}
|
|
|
|
// IsKissOfDeath returns true if the response is a "kiss of death" from the
|
|
// remote server. If this function returns true, you may examine the
|
|
// response's KissCode value to determine the reason for the kiss of death.
|
|
func (r *Response) IsKissOfDeath() bool {
|
|
return r.Stratum == 0
|
|
}
|
|
|
|
// ReferenceString returns the response's ReferenceID value formatted as a
|
|
// string. If the response's stratum is zero, then the "kiss o' death" string
|
|
// is returned. If stratum is one, then the server is a reference clock and
|
|
// the reference clock's name is returned. If stratum is two or greater, then
|
|
// the ID is either an IPv4 address or an MD5 hash of the IPv6 address; in
|
|
// either case the reference string is reported as 4 dot-separated
|
|
// decimal-based integers.
|
|
func (r *Response) ReferenceString() string {
|
|
if r.Stratum == 0 {
|
|
return kissCode(r.ReferenceID)
|
|
}
|
|
|
|
var b [4]byte
|
|
binary.BigEndian.PutUint32(b[:], r.ReferenceID)
|
|
|
|
if r.Stratum == 1 {
|
|
const dot = rune(0x22c5)
|
|
var r []rune
|
|
for i := range b {
|
|
if b[i] == 0 {
|
|
break
|
|
}
|
|
if b[i] >= 32 && b[i] <= 126 {
|
|
r = append(r, rune(b[i]))
|
|
} else {
|
|
r = append(r, dot)
|
|
}
|
|
}
|
|
return fmt.Sprintf(".%s.", string(r))
|
|
}
|
|
|
|
return fmt.Sprintf("%d.%d.%d.%d", b[0], b[1], b[2], b[3])
|
|
}
|
|
|
|
// Validate checks if the response is valid for the purposes of time
|
|
// synchronization.
|
|
func (r *Response) Validate() error {
|
|
// Forward authentication errors.
|
|
if r.authErr != nil {
|
|
return r.authErr
|
|
}
|
|
|
|
// Handle invalid stratum values.
|
|
if r.Stratum == 0 {
|
|
return ErrKissOfDeath
|
|
}
|
|
if r.Stratum >= maxStratum {
|
|
return ErrInvalidStratum
|
|
}
|
|
|
|
// Estimate the "freshness" of the time. If it exceeds the maximum
|
|
// polling interval (~36 hours), then it cannot be considered "fresh".
|
|
freshness := r.Time.Sub(r.ReferenceTime)
|
|
if freshness > maxPollInterval {
|
|
return ErrServerClockFreshness
|
|
}
|
|
|
|
// Calculate the peer synchronization distance, lambda:
|
|
// lambda := RootDelay/2 + RootDispersion
|
|
// If this value exceeds MAXDISP (16s), then the time is not suitable
|
|
// for synchronization purposes.
|
|
// https://tools.ietf.org/html/rfc5905#appendix-A.5.1.1.
|
|
lambda := r.RootDelay/2 + r.RootDispersion
|
|
if lambda > maxDispersion {
|
|
return ErrInvalidDispersion
|
|
}
|
|
|
|
// If the server's transmit time is before its reference time, the
|
|
// response is invalid.
|
|
if r.Time.Before(r.ReferenceTime) {
|
|
return ErrInvalidTime
|
|
}
|
|
|
|
// Handle invalid leap second indicator.
|
|
if r.Leap == LeapNotInSync {
|
|
return ErrInvalidLeapSecond
|
|
}
|
|
|
|
// nil means the response is valid.
|
|
return nil
|
|
}
|
|
|
|
// Query requests time data from a remote NTP server. The response contains
|
|
// information from which a more accurate local time can be inferred.
|
|
//
|
|
// The server address is of the form "host", "host:port", "host%zone:port",
|
|
// "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or
|
|
// domain name address. When specifying both a port and an IPv6 address, one
|
|
// of the bracket formats must be used. If no port is included, NTP default
|
|
// port 123 is used.
|
|
func Query(address string) (*Response, error) {
|
|
return QueryWithOptions(address, QueryOptions{})
|
|
}
|
|
|
|
// QueryWithOptions performs the same function as Query but allows for the
|
|
// customization of certain query behaviors. See the comments for Query and
|
|
// QueryOptions for further details.
|
|
func QueryWithOptions(address string, opt QueryOptions) (*Response, error) {
|
|
h, now, err := getTime(address, &opt)
|
|
if err != nil && err != ErrAuthFailed {
|
|
return nil, err
|
|
}
|
|
|
|
return generateResponse(h, now, err), nil
|
|
}
|
|
|
|
// Time returns the current, corrected local time using information returned
|
|
// from the remote NTP server. On error, Time returns the uncorrected local
|
|
// system time.
|
|
//
|
|
// The server address is of the form "host", "host:port", "host%zone:port",
|
|
// "[host]:port" or "[host%zone]:port". The host may contain an IPv4, IPv6 or
|
|
// domain name address. When specifying both a port and an IPv6 address, one
|
|
// of the bracket formats must be used. If no port is included, NTP default
|
|
// port 123 is used.
|
|
func Time(address string) (time.Time, error) {
|
|
r, err := Query(address)
|
|
if err != nil {
|
|
return time.Now(), err
|
|
}
|
|
|
|
err = r.Validate()
|
|
if err != nil {
|
|
return time.Now(), err
|
|
}
|
|
|
|
// Use the response's clock offset to calculate an accurate time.
|
|
return time.Now().Add(r.ClockOffset), nil
|
|
}
|
|
|
|
// getTime performs the NTP server query and returns the response header
|
|
// along with the local system time it was received.
|
|
func getTime(address string, opt *QueryOptions) (*header, ntpTime, error) {
|
|
if opt.Timeout == 0 {
|
|
opt.Timeout = defaultTimeout
|
|
}
|
|
if opt.Version == 0 {
|
|
opt.Version = defaultNtpVersion
|
|
}
|
|
if opt.Version < 2 || opt.Version > 4 {
|
|
return nil, 0, ErrInvalidProtocolVersion
|
|
}
|
|
if opt.Port == 0 {
|
|
opt.Port = defaultNtpPort
|
|
}
|
|
if opt.Dial != nil {
|
|
// wrapper for the deprecated Dial callback.
|
|
opt.Dialer = func(la, ra string) (net.Conn, error) {
|
|
return dialWrapper(la, ra, opt.Dial)
|
|
}
|
|
}
|
|
if opt.Dialer == nil {
|
|
opt.Dialer = defaultDialer
|
|
}
|
|
|
|
// Compose a conforming host:port remote address string if the address
|
|
// string doesn't already contain a port.
|
|
remoteAddress, err := fixHostPort(address, opt.Port)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Connect to the remote server.
|
|
con, err := opt.Dialer(opt.LocalAddress, remoteAddress)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer con.Close()
|
|
|
|
// Set a TTL for the packet if requested.
|
|
if opt.TTL != 0 {
|
|
ipcon := ipv4.NewConn(con)
|
|
err = ipcon.SetTTL(opt.TTL)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
// Set a timeout on the connection.
|
|
con.SetDeadline(time.Now().Add(opt.Timeout))
|
|
|
|
// Allocate a buffer big enough to hold an entire response datagram.
|
|
recvBuf := make([]byte, 8192)
|
|
recvHdr := new(header)
|
|
|
|
// Allocate the query message header.
|
|
xmitHdr := new(header)
|
|
xmitHdr.setMode(client)
|
|
xmitHdr.setVersion(opt.Version)
|
|
xmitHdr.setLeap(LeapNoWarning)
|
|
xmitHdr.Precision = 0x20
|
|
|
|
// To help prevent spoofing and client fingerprinting, use a
|
|
// cryptographically random 64-bit value for the TransmitTime. See:
|
|
// https://www.ietf.org/archive/id/draft-ietf-ntp-data-minimization-04.txt
|
|
bits := make([]byte, 8)
|
|
_, err = rand.Read(bits)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
xmitHdr.TransmitTime = ntpTime(binary.BigEndian.Uint64(bits))
|
|
|
|
// Write the query header to a transmit buffer.
|
|
var xmitBuf bytes.Buffer
|
|
binary.Write(&xmitBuf, binary.BigEndian, xmitHdr)
|
|
|
|
// Allow extensions to process the query and add to the transmit buffer.
|
|
for _, e := range opt.Extensions {
|
|
err = e.ProcessQuery(&xmitBuf)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
// If using symmetric key authentication, decode and validate the auth key
|
|
// string.
|
|
authKey, err := decodeAuthKey(opt.Auth)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Append a MAC if authentication is being used.
|
|
appendMAC(&xmitBuf, opt.Auth, authKey)
|
|
|
|
// Transmit the query and keep track of when it was transmitted.
|
|
xmitTime := time.Now()
|
|
_, err = con.Write(xmitBuf.Bytes())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Receive the response.
|
|
recvBytes, err := con.Read(recvBuf)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Keep track of the time the response was received. As of go 1.9, the
|
|
// time package uses a monotonic clock, so delta will never be less than
|
|
// zero for go version 1.9 or higher.
|
|
delta := time.Since(xmitTime)
|
|
if delta < 0 {
|
|
delta = 0
|
|
}
|
|
recvTime := xmitTime.Add(delta)
|
|
|
|
// Parse the response header.
|
|
recvBuf = recvBuf[:recvBytes]
|
|
recvReader := bytes.NewReader(recvBuf)
|
|
err = binary.Read(recvReader, binary.BigEndian, recvHdr)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Allow extensions to process the response.
|
|
for i := len(opt.Extensions) - 1; i >= 0; i-- {
|
|
err = opt.Extensions[i].ProcessResponse(recvBuf)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
// Check for invalid fields.
|
|
if recvHdr.getMode() != server {
|
|
return nil, 0, ErrInvalidMode
|
|
}
|
|
if recvHdr.TransmitTime == ntpTime(0) {
|
|
return nil, 0, ErrInvalidTransmitTime
|
|
}
|
|
if recvHdr.OriginTime != xmitHdr.TransmitTime {
|
|
return nil, 0, ErrServerResponseMismatch
|
|
}
|
|
if recvHdr.ReceiveTime > recvHdr.TransmitTime {
|
|
return nil, 0, ErrServerTickedBackwards
|
|
}
|
|
|
|
// Correct the received message's origin time using the actual
|
|
// transmit time.
|
|
recvHdr.OriginTime = toNtpTime(xmitTime)
|
|
|
|
// Perform authentication of the server response.
|
|
authErr := verifyMAC(recvBuf, opt.Auth, authKey)
|
|
|
|
return recvHdr, toNtpTime(recvTime), authErr
|
|
}
|
|
|
|
// defaultDialer provides a UDP dialer based on Go's built-in net stack.
|
|
func defaultDialer(localAddress, remoteAddress string) (net.Conn, error) {
|
|
var laddr *net.UDPAddr
|
|
if localAddress != "" {
|
|
var err error
|
|
laddr, err = net.ResolveUDPAddr("udp", net.JoinHostPort(localAddress, "0"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
raddr, err := net.ResolveUDPAddr("udp", remoteAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return net.DialUDP("udp", laddr, raddr)
|
|
}
|
|
|
|
// dialWrapper is used to wrap the deprecated Dial callback in QueryOptions.
|
|
func dialWrapper(la, ra string,
|
|
dial func(la string, lp int, ra string, rp int) (net.Conn, error)) (net.Conn, error) {
|
|
rhost, rport, err := net.SplitHostPort(ra)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rportValue, err := strconv.Atoi(rport)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dial(la, 0, rhost, rportValue)
|
|
}
|
|
|
|
// fixHostPort examines an address in one of the accepted forms and fixes it
|
|
// to include a port number if necessary.
|
|
func fixHostPort(address string, defaultPort int) (fixed string, err error) {
|
|
if len(address) == 0 {
|
|
return "", errors.New("address string is empty")
|
|
}
|
|
|
|
// If the address is wrapped in brackets, append a port if necessary.
|
|
if address[0] == '[' {
|
|
end := strings.IndexByte(address, ']')
|
|
switch {
|
|
case end < 0:
|
|
return "", errors.New("missing ']' in address")
|
|
case end+1 == len(address):
|
|
return fmt.Sprintf("%s:%d", address, defaultPort), nil
|
|
case address[end+1] == ':':
|
|
return address, nil
|
|
default:
|
|
return "", errors.New("unexpected character following ']' in address")
|
|
}
|
|
}
|
|
|
|
// No colons? Must be a port-less IPv4 or domain address.
|
|
last := strings.LastIndexByte(address, ':')
|
|
if last < 0 {
|
|
return fmt.Sprintf("%s:%d", address, defaultPort), nil
|
|
}
|
|
|
|
// Exactly one colon? A port have been included along with an IPv4 or
|
|
// domain address. (IPv6 addresses are guaranteed to have more than one
|
|
// colon.)
|
|
prev := strings.LastIndexByte(address[:last], ':')
|
|
if prev < 0 {
|
|
return address, nil
|
|
}
|
|
|
|
// Two or more colons means we must have an IPv6 address without a port.
|
|
return fmt.Sprintf("[%s]:%d", address, defaultPort), nil
|
|
}
|
|
|
|
// generateResponse processes NTP header fields along with the its receive
|
|
// time to generate a Response record.
|
|
func generateResponse(h *header, recvTime ntpTime, authErr error) *Response {
|
|
r := &Response{
|
|
Time: h.TransmitTime.Time(),
|
|
ClockOffset: offset(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime),
|
|
RTT: rtt(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime),
|
|
Precision: toInterval(h.Precision),
|
|
Version: h.getVersion(),
|
|
Stratum: h.Stratum,
|
|
ReferenceID: h.ReferenceID,
|
|
ReferenceTime: h.ReferenceTime.Time(),
|
|
RootDelay: h.RootDelay.Duration(),
|
|
RootDispersion: h.RootDispersion.Duration(),
|
|
Leap: h.getLeap(),
|
|
MinError: minError(h.OriginTime, h.ReceiveTime, h.TransmitTime, recvTime),
|
|
Poll: toInterval(h.Poll),
|
|
authErr: authErr,
|
|
}
|
|
|
|
// Calculate values depending on other calculated values
|
|
r.RootDistance = rootDistance(r.RTT, r.RootDelay, r.RootDispersion)
|
|
|
|
// If a kiss of death was received, interpret the reference ID as
|
|
// a kiss code.
|
|
if r.Stratum == 0 {
|
|
r.KissCode = kissCode(r.ReferenceID)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// The following helper functions calculate additional metadata about the
|
|
// timestamps received from an NTP server. The timestamps returned by
|
|
// the server are given the following variable names:
|
|
//
|
|
// org = Origin Timestamp (client send time)
|
|
// rec = Receive Timestamp (server receive time)
|
|
// xmt = Transmit Timestamp (server reply time)
|
|
// dst = Destination Timestamp (client receive time)
|
|
|
|
func rtt(org, rec, xmt, dst ntpTime) time.Duration {
|
|
a := int64(dst - org)
|
|
b := int64(xmt - rec)
|
|
rtt := a - b
|
|
if rtt < 0 {
|
|
rtt = 0
|
|
}
|
|
return ntpTime(rtt).Duration()
|
|
}
|
|
|
|
func offset(org, rec, xmt, dst ntpTime) time.Duration {
|
|
// The inputs are 64-bit unsigned integer timestamps. These timestamps can
|
|
// "roll over" at the end of an NTP era, which occurs approximately every
|
|
// 136 years starting from the year 1900. To ensure an accurate offset
|
|
// calculation when an era boundary is crossed, we need to take care that
|
|
// the difference between two 64-bit timestamp values is accurately
|
|
// calculated even when they are in neighboring eras.
|
|
//
|
|
// See: https://www.eecis.udel.edu/~mills/y2k.html
|
|
|
|
a := int64(rec - org)
|
|
b := int64(xmt - dst)
|
|
offset := a + (b-a)/2
|
|
if offset < 0 {
|
|
return -ntpTime(-offset).Duration()
|
|
}
|
|
return ntpTime(offset).Duration()
|
|
}
|
|
|
|
func minError(org, rec, xmt, dst ntpTime) time.Duration {
|
|
// Each NTP response contains two pairs of send/receive timestamps.
|
|
// When either pair indicates a "causality violation", we calculate the
|
|
// error as the difference in time between them. The minimum error is
|
|
// the greater of the two causality violations.
|
|
var error0, error1 ntpTime
|
|
if org >= rec {
|
|
error0 = org - rec
|
|
}
|
|
if xmt >= dst {
|
|
error1 = xmt - dst
|
|
}
|
|
if error0 > error1 {
|
|
return error0.Duration()
|
|
}
|
|
return error1.Duration()
|
|
}
|
|
|
|
func rootDistance(rtt, rootDelay, rootDisp time.Duration) time.Duration {
|
|
// The root distance is:
|
|
// the maximum error due to all causes of the local clock
|
|
// relative to the primary server. It is defined as half the
|
|
// total delay plus total dispersion plus peer jitter.
|
|
// (https://tools.ietf.org/html/rfc5905#appendix-A.5.5.2)
|
|
//
|
|
// In the reference implementation, it is calculated as follows:
|
|
// rootDist = max(MINDISP, rootDelay + rtt)/2 + rootDisp
|
|
// + peerDisp + PHI * (uptime - peerUptime)
|
|
// + peerJitter
|
|
// For an SNTP client which sends only a single packet, most of these
|
|
// terms are irrelevant and become 0.
|
|
totalDelay := rtt + rootDelay
|
|
return totalDelay/2 + rootDisp
|
|
}
|
|
|
|
func toInterval(t int8) time.Duration {
|
|
switch {
|
|
case t > 0:
|
|
return time.Duration(uint64(time.Second) << uint(t))
|
|
case t < 0:
|
|
return time.Duration(uint64(time.Second) >> uint(-t))
|
|
default:
|
|
return time.Second
|
|
}
|
|
}
|
|
|
|
func kissCode(id uint32) string {
|
|
isPrintable := func(ch byte) bool { return ch >= 32 && ch <= 126 }
|
|
|
|
b := [4]byte{
|
|
byte(id >> 24),
|
|
byte(id >> 16),
|
|
byte(id >> 8),
|
|
byte(id),
|
|
}
|
|
for _, ch := range b {
|
|
if !isPrintable(ch) {
|
|
return ""
|
|
}
|
|
}
|
|
return string(b[:])
|
|
}
|