mirror of
https://github.com/soypat/lneto.git
synced 2026-09-01 12:29:05 +00:00
restructure package, rename module/repo
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
package ntp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type state uint8
|
||||
|
||||
const (
|
||||
stateClosed state = iota
|
||||
stateSend1
|
||||
stateAwait1
|
||||
stateSend2
|
||||
stateAwait2
|
||||
stateDone
|
||||
)
|
||||
|
||||
const sysprecRecalcNeeded int8 = 127
|
||||
|
||||
func NewClient(now func() time.Time) *Client {
|
||||
return &Client{
|
||||
_now: now,
|
||||
_sysprec: sysprecRecalcNeeded,
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
start time.Time
|
||||
_now func() time.Time
|
||||
t [4]Timestamp
|
||||
// org Timestamp
|
||||
// rec Timestamp
|
||||
xmt Timestamp
|
||||
state state
|
||||
serverStratum Stratum
|
||||
_sysprec int8
|
||||
}
|
||||
|
||||
func (c *Client) Send(payload []byte) (int, error) {
|
||||
if c.isDone() {
|
||||
return 0, io.EOF
|
||||
}
|
||||
frm, err := NewFrame(payload)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch c.state {
|
||||
case stateSend1:
|
||||
c.start = c.now()
|
||||
c.xmt = TimestampFromUint64(0)
|
||||
c.state = stateAwait1
|
||||
case stateSend2:
|
||||
c.xmt = c.unsyncTimestamp(c.now())
|
||||
c.state = stateDone
|
||||
default:
|
||||
return 0, nil // Nothing to handle.
|
||||
}
|
||||
|
||||
for i := range payload[:SizeHeader] {
|
||||
payload[i] = 0
|
||||
}
|
||||
sysprec := c.sysprec()
|
||||
frm.ClearHeader()
|
||||
frm.SetStratum(StratumUnsync)
|
||||
frm.SetPoll(6)
|
||||
frm.SetPrecision(sysprec)
|
||||
frm.SetOriginTime(c.xmt)
|
||||
frm.SetFlags(ModeClient, Version4, LeapNoWarning)
|
||||
return SizeHeader, nil
|
||||
}
|
||||
|
||||
func (c *Client) Read(payload []byte) error {
|
||||
if c.isDone() {
|
||||
return io.EOF
|
||||
}
|
||||
frm, err := NewFrame(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t := &c.t
|
||||
switch c.state {
|
||||
case stateAwait1:
|
||||
tstx := frm.TransmitTime()
|
||||
tsorig := frm.OriginTime()
|
||||
if tstx == tsorig || tsorig == c.xmt {
|
||||
return errors.New("bogus NTP packet")
|
||||
}
|
||||
t[0] = tsorig
|
||||
t[1] = frm.ReceiveTime()
|
||||
t[2] = tstx
|
||||
t[3] = c.unsyncTimestamp(c.now())
|
||||
c.serverStratum = frm.Stratum()
|
||||
c.state = stateDone
|
||||
case stateAwait2:
|
||||
c.state = stateAwait2
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) isDone() bool {
|
||||
return c.state == stateDone
|
||||
}
|
||||
|
||||
func (c *Client) now() time.Time {
|
||||
if c._now == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return c._now()
|
||||
}
|
||||
|
||||
func (c *Client) unsyncTimestamp(now time.Time) Timestamp {
|
||||
return TimestampFromUint64(0).Add(now.Sub(c.start))
|
||||
}
|
||||
|
||||
func (c *Client) sysprec() int8 {
|
||||
if c._sysprec == sysprecRecalcNeeded {
|
||||
c._sysprec = CalculateSystemPrecision(c._now)
|
||||
}
|
||||
return c._sysprec
|
||||
}
|
||||
|
||||
// Now returns the current time as corrected by NTP protocol.
|
||||
func (c *Client) Now() time.Time {
|
||||
return c.now().Add(c.Offset())
|
||||
}
|
||||
|
||||
// ServerStratum returns the stratum of the server client synchronized with.
|
||||
func (c *Client) ServerStratum() Stratum { return c.serverStratum }
|
||||
|
||||
// Offset returns the
|
||||
func (c *Client) Offset() time.Duration {
|
||||
if c.isDone() {
|
||||
t := &c.t
|
||||
return t[1].Sub(t[0])/2 + t[2].Sub(t[3])/2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package ntp
|
||||
|
||||
// LeapIndicator represents the leap second indicator.
|
||||
// It indicates whether there is no warning, an extra second (61 seconds in the last minute),
|
||||
// or a missing second (59 seconds in the last minute).
|
||||
type LeapIndicator uint8
|
||||
|
||||
const (
|
||||
LeapNoWarning LeapIndicator = iota // no warning
|
||||
LeapLastMinute61 // last minute 61
|
||||
LeapLastMinute59 // last minute 59
|
||||
)
|
||||
|
||||
// Stratum represents the stratum level of the NTP server.
|
||||
type Stratum uint8
|
||||
|
||||
const (
|
||||
// If the Stratum field is 0, which implies unspecified or invalid, the
|
||||
// Reference Identifier field can be used to convey messages useful for
|
||||
// status reporting and access control. These are called Kiss-o'-Death
|
||||
// (KoD) packets and the ASCII messages they convey are called kiss codes.
|
||||
StratumUnspecified Stratum = 0 // unspecified
|
||||
StratumPrimary Stratum = 1 // primary
|
||||
StratumUnsync Stratum = 16 // unsynchronized
|
||||
)
|
||||
|
||||
// String returns a human readable representation of the Stratum.
|
||||
func (s Stratum) String() string {
|
||||
switch s {
|
||||
case 0:
|
||||
return "unspecified"
|
||||
case 1:
|
||||
return "primary"
|
||||
case 16:
|
||||
return "unsynchronized"
|
||||
}
|
||||
if s < 16 {
|
||||
return "secondary"
|
||||
}
|
||||
return "invalid"
|
||||
}
|
||||
|
||||
func (s Stratum) IsSecondary() bool {
|
||||
return s > 1 && s < 16
|
||||
}
|
||||
|
||||
// Mode represents the mode of the NTP message.
|
||||
// It can be undefined, symmetric active, symmetric passive, client, server, broadcast,
|
||||
// NTP control message, or private use.
|
||||
type Mode uint8
|
||||
|
||||
const (
|
||||
modeUndef Mode = iota // undefined
|
||||
ModeSymmetricActive // symmetric active
|
||||
ModeSymmetricPassive // symmetric passive
|
||||
ModeClient // client
|
||||
ModeServer // server
|
||||
ModeBroadcast // broadcast
|
||||
ModeNTPControlMessage // control message
|
||||
ModePrivateUse // private use
|
||||
)
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
// 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) & 0b11
|
||||
lp = LeapIndicator(b >> 5)
|
||||
return mode, version, lp
|
||||
}
|
||||
|
||||
func (frm Frame) SetFlags(mode Mode, version uint8, lp LeapIndicator) {
|
||||
b := uint8(mode)&0b111 | (Version4&0b11)<<3 | uint8(lp&0b111)<<5
|
||||
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
|
||||
)
|
||||
|
||||
// SystemPrecision calculates the Precision field value for the NTP header once
|
||||
// and reuses it for all future calls.
|
||||
func SystemPrecision() int8 {
|
||||
ntpOnceSystemClock.Do(recalculateSystemPrecision)
|
||||
return sysPrec
|
||||
}
|
||||
|
||||
func recalculateSystemPrecision() {
|
||||
sysPrec = CalculateSystemPrecision(nil)
|
||||
}
|
||||
|
||||
// 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) int8 {
|
||||
const maxIter = 16
|
||||
var times [maxIter]time.Time
|
||||
if now == nil {
|
||||
for i := 0; i < maxIter; i++ {
|
||||
times[i] = time.Now()
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < maxIter; i++ {
|
||||
times[i] = now()
|
||||
}
|
||||
}
|
||||
avg := times[maxIter-1].Sub(times[0]) / maxIter
|
||||
return int8(math.Log2(avg.Seconds()))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ntp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimestamp(t *testing.T) {
|
||||
now := time.Now()
|
||||
nowp1 := now.Add(time.Second)
|
||||
t1, err := TimestampFromTime(now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t2, _ := TimestampFromTime(nowp1)
|
||||
gotnow := t1.Time()
|
||||
if gotnow.Sub(now) > time.Microsecond {
|
||||
t.Fatalf("got %v, want %v", gotnow, now)
|
||||
}
|
||||
|
||||
d := t2.Sub(t1)
|
||||
if d != time.Second {
|
||||
t.Fatalf("expected 1s, got %s", d)
|
||||
}
|
||||
d = t1.Sub(t2)
|
||||
if d != -time.Second {
|
||||
t.Fatalf("expected -1s, got %s", d)
|
||||
}
|
||||
|
||||
var b [8]byte
|
||||
t1.Put(b[:])
|
||||
readback := binary.BigEndian.Uint64(b[:])
|
||||
t1got := TimestampFromUint64(readback)
|
||||
if t1got != t1 {
|
||||
t.Fatalf("got %v, want %v", t1got, t1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampOverflow(t *testing.T) {
|
||||
const tol = time.Microsecond
|
||||
var now = time.Now() // time.Date(2035, 2, 7, 6, 28, 16, 0, time.UTC)
|
||||
told, err := TimestampFromTime(baseTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmodern, err := TimestampFromTime(now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
diff := tmodern.Sub(told)
|
||||
if diff < 0 {
|
||||
t.Fatalf("got %v, want positive", diff)
|
||||
}
|
||||
diffwant := now.Sub(baseTime)
|
||||
differr := (diff - diffwant).Abs()
|
||||
if differr > tol {
|
||||
t.Fatalf("got diff %v", differr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user