Files
lneto/ntp/ntp.go
T
2025-07-12 16:45:20 -03:00

288 lines
9.5 KiB
Go

// package ntp implements the NTP protocol as described in RFC 5905.
package ntp
import (
"encoding/binary"
"errors"
"math"
"math/bits"
"sync"
"time"
)
// NTP Global Parameters.
const (
SizeHeader = 48
ClientPort = 1023 // Typical Client port number.
ServerPort = 123 // NTP server port number
Version4 = 4 // Current NTP Version Number
MinPoll = 4 // Minimum poll exponent (16s)
MaxPoll = 17 // Maximum poll exponent (~36h)
MaxDisp = 16 // Maximum dispersion (16s)
MaxDist = 1 // Distance threshold (1s)
MaxStratum = 16 // Maximum stratum
MinDispDiv = 200 // Minimum dispersion divisor 1/(200) == 0.005
)
func NewFrame(buf []byte) (Frame, error) {
if len(buf) < SizeHeader {
return Frame{buf: nil}, errors.New("NTP frame too short")
}
return Frame{buf: buf}, nil
}
// Frame encapsulates the raw data of an NTP packet
// and provides methods for manipulating, validating and
// retrieving fields and payload data. See [RFC5905].
//
// [RFC5905]: https://tools.ietf.org/html/rfc5905
type Frame struct {
buf []byte
}
func (frm Frame) Flags() (mode Mode, version uint8, lp LeapIndicator) {
b := frm.buf[0]
mode = Mode(b & 0b111)
version = (b >> 3) & 0b111
lp = LeapIndicator(b >> 6)
return mode, version, lp
}
func (frm Frame) SetFlags(mode Mode, version uint8, lp LeapIndicator) {
b := uint8(mode)&0b111 | (version&0b111)<<3 | uint8(lp&0b11)<<6
frm.buf[0] = b
}
func (frm Frame) Stratum() Stratum { return Stratum(frm.buf[1]) }
func (frm Frame) SetStratum(stratum Stratum) { frm.buf[1] = byte(stratum) }
// Poll is 8-bit signed integer representing the maximum interval between
// successive messages, in log2 seconds. Suggested default limits for
// minimum and maximum poll intervals are 6 and 10, respectively.
func (frm Frame) Poll() int8 { return int8(frm.buf[2]) }
func (frm Frame) SetPoll(Poll int8) { frm.buf[2] = uint8(Poll) }
// Precision is 8-bit signed integer representing the precision of the
// system clock, in log2 seconds. For instance, a value of -18
// corresponds to a precision of about one microsecond. The precision
// can be determined when the service first starts up as the minimum
// time of several iterations to read the system clock.
func (frm Frame) Precision() int8 { return int8(frm.buf[3]) }
func (frm Frame) SetPrecision(Precision int8) { frm.buf[3] = uint8(Precision) }
// Total round-trip delay to the reference clock, in NTP short format.
func (frm Frame) RootDelay() Short {
return Short(binary.BigEndian.Uint32(frm.buf[4:8]))
}
func (frm Frame) SetRootDelay(rd Short) {
binary.BigEndian.PutUint32(frm.buf[4:8], uint32(rd))
}
// Total dispersion to the reference clock, in NTP short format.
func (frm Frame) RootDispersion() Short {
return Short(binary.BigEndian.Uint32(frm.buf[8:12]))
}
func (frm Frame) SetRootDispersion(rd Short) {
binary.BigEndian.PutUint32(frm.buf[8:12], uint32(rd))
}
// 32-bit code identifying the particular server or reference clock.
// The interpretation depends on the value in the stratum field.
// For packet stratum 0 (unspecified or invalid), this is a four-character
// ASCII [RFC1345] string, called the "kiss code", used for debugging and monitoring purposes.
// For stratum 1 (reference clock), this is a four-octet, left-justified,
// zero-padded ASCII string assigned to the reference clock.
// The authoritative list of Reference Identifiers is maintained by IANA; however, any string
// beginning with the ASCII character "X" is reserved for unregistered
// experimentation and development.
func (frm Frame) ReferenceID() *[4]byte {
return (*[4]byte)(frm.buf[12:16])
}
// ReferenceTime is when the system clock was last set or corrected, in NTP timestamp format.
func (frm Frame) ReferenceTime() Timestamp {
return TimestampFromUint64(binary.BigEndian.Uint64(frm.buf[16:24]))
}
func (frm Frame) SetReferenceTime(rt Timestamp) {
rt.Put(frm.buf[16:24])
}
// OriginTime is time at the client when the request departed for the server, in NTP timestamp format.
func (frm Frame) OriginTime() Timestamp {
return TimestampFromUint64(binary.BigEndian.Uint64(frm.buf[24:32]))
}
func (frm Frame) SetOriginTime(ot Timestamp) {
ot.Put(frm.buf[24:32])
}
// ReceiveTime time at the server when the request arrived from the client, in NTP timestamp format.
func (frm Frame) ReceiveTime() Timestamp {
return TimestampFromUint64(binary.BigEndian.Uint64(frm.buf[32:40]))
}
func (frm Frame) SetReceiveTime(rt Timestamp) {
rt.Put(frm.buf[32:40])
}
// TransmitTime at the server when the response left for the client, in NTP timestamp format.
func (frm Frame) TransmitTime() Timestamp {
return TimestampFromUint64(binary.BigEndian.Uint64(frm.buf[40:48]))
}
func (frm Frame) SetTransmitTime(rt Timestamp) {
rt.Put(frm.buf[40:48])
}
// ClearHeader zeros out the header contents.
func (frm Frame) ClearHeader() {
for i := range frm.buf[:SizeHeader] {
frm.buf[i] = 0
}
}
type Short uint32
var baseTime = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
// BaseTime returnsS the time that corresponds to the NTP base time.
// The zero value for [Timestamp] and [Date] types corresponds to this time.
func BaseTime() time.Time {
return baseTime
}
// In the date and timestamp formats, the prime epoch, or base date of
// era 0, is 0 h 1 January 1900 UTC, when all bits are zero. It should
// be noted that strictly speaking, UTC did not exist prior to 1 January
// 1972, but it is convenient to assume it has existed for all eternity,
// even if all knowledge of historic leap seconds has been lost. Dates
// are relative to the prime epoch; values greater than zero represent
// times after that date; values less than zero represent times before
// it. Note that the Era Offset field of the date format and the
// Seconds field of the timestamp format have the same interpretation.
// Timestamp format is used in packet headers and other
// places with limited word size. It includes a 32-bit unsigned seconds
// field spanning 136 years and a 32-bit fraction field resolving 232
// picoseconds. The 32-bit short format is used in delay and dispersion
// header fields where the full resolution and range of the other
// formats are not justified. It includes a 16-bit unsigned seconds
// field and a 16-bit fraction field.
type Timestamp struct {
sec uint32
fra uint32
}
func (t Timestamp) Put(b []byte) {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
binary.BigEndian.PutUint32(b[:4], t.sec)
binary.BigEndian.PutUint32(b[4:], t.fra)
}
// IsZero reports whether t represents the zero time instant.
func (t Timestamp) IsZero() bool { return t.sec == 0 && t.fra == 0 }
func TimestampFromUint64(ts uint64) Timestamp {
return Timestamp{
sec: uint32(ts >> 32),
fra: uint32(ts),
}
}
func TimestampFromTime(t time.Time) (Timestamp, error) {
t = t.UTC()
if t.Before(baseTime) {
return Timestamp{}, errors.New("ntp.TimestampFromTime: time is before baseTime")
}
off := t.Sub(baseTime)
sec := uint64(off / time.Second)
if sec > math.MaxUint32 {
return Timestamp{}, errors.New("ntp.TimestampFromTime: time is too large")
}
fra := uint64(off%time.Second) * math.MaxUint32 / uint64(time.Second)
return Timestamp{
sec: uint32(sec),
fra: uint32(fra),
}, nil
}
// The 128-bit date format is used where sufficient storage and word
// size are available. It includes a 64-bit signed seconds field
// spanning 584 billion years and a 64-bit fraction field resolving .05
// attosecond (i.e., 0.5e-18).
type Date struct {
sec int64
frac uint64
}
func (t Timestamp) Seconds() uint32 { return t.sec }
func (t Timestamp) Fractions() uint32 { return t.fra }
func (t Short) Seconds() uint16 { return uint16(t >> 16) }
func (t Short) Fractions() uint16 { return uint16(t) }
func (t Timestamp) Time() time.Time {
off := time.Second*time.Duration(t.Seconds()) + time.Second*time.Duration(t.Fractions())/math.MaxUint32
return baseTime.Add(off)
}
func (t Timestamp) Sub(v Timestamp) time.Duration {
dsec := time.Duration(t.sec) - time.Duration(v.sec)
dfra := time.Duration(t.fra) - time.Duration(v.fra)
// Work in uint64 to avoid overflow since fra is possibly MaxUint32-1
// which means the result of dfra*MaxUint32 would be MaxUint64-MaxUint32, overflowing time.Duration's
// underlying int64 representation by *a lot*.
dfraneg := dfra < 0
dfra = time.Duration(uint64(dfra.Abs()) * uint64(time.Second) / math.MaxUint32)
if dfraneg {
dfra = -dfra
}
return dsec*time.Second + dfra
}
func (t Timestamp) Add(d time.Duration) Timestamp {
add := uint32(uint64(d%time.Second) * math.MaxUint32 / uint64(time.Second))
add, carry := bits.Add32(t.fra, add, 0)
t.sec += uint32(d/time.Second) + carry
t.fra = add
return t
}
func (d Date) Time() (time.Time, error) {
sec := d.sec
neg := sec < 0
if neg {
sec = -sec
}
hi, seclo := bits.Mul64(uint64(sec), uint64(time.Second))
if hi != 0 || seclo > math.MaxInt64-uint64(time.Second)-1 {
return time.Time{}, errors.New("ntp.Date.Time overflow")
}
off := time.Duration(seclo)
off += time.Second * time.Duration(d.frac>>32) / math.MaxUint32
if neg {
off = -off
}
return baseTime.Add(off), nil
}
var (
ntpOnceSystemClock sync.Once
sysPrec int8
)
// CalculateSystemPrecision calculates the NTP system precision for a time source.
// If the time source is nil the default static call to [time.Now] is used.
func CalculateSystemPrecision(now func() time.Time, iters []time.Time) int8 {
maxIter := len(iters)
if now == nil {
for i := 0; i < maxIter; i++ {
iters[i] = time.Now()
}
} else {
for i := 0; i < maxIter; i++ {
iters[i] = now()
}
}
avg := iters[maxIter-1].Sub(iters[0]) / time.Duration(maxIter)
return int8(math.Log2(avg.Seconds()))
}