mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
bba913751a
Implement full two-exchange NTP client state machine (RFC 5905 §8). First exchange stores offset/RTT, second averages both for improved accuracy. Client places T1 in TransmitTime per RFC 5905 §8; server echoes it as OriginTime in the response. Add NTP extension field codec (RFC 7822) with NextExtField iterator and AppendExtField builder. Add NTS extension type constants from RFC 8915. Add NTP Server (StackNode) that receives client requests via Demux and builds server responses via Encapsulate with configurable stratum, precision, reference ID, and pending request queue. Add Frame accessor methods: RawData(), ExtensionFields(), ValidateSize(), and Timestamp.Uint64(). Add ntp-client and ntp-server example programs with CalculateSystemPrecision, retry limits, and backoff. Use internal.LogAttrs pattern for non-allocating structured logging in the client, matching the tcp/debug.go convention. Generated with Claude assistance. Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
134 lines
2.9 KiB
Go
134 lines
2.9 KiB
Go
package ntp
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/soypat/lneto"
|
|
)
|
|
|
|
// ServerConfig configures an NTP [Server].
|
|
type ServerConfig struct {
|
|
Now func() time.Time
|
|
Stratum Stratum
|
|
Precision int8
|
|
RefID [4]byte
|
|
MaxPending int
|
|
}
|
|
|
|
// Server is a basic NTP server implementing [lneto.StackNode].
|
|
// It receives client requests via [Server.Demux] and builds server
|
|
// responses via [Server.Encapsulate].
|
|
//
|
|
// Server is not safe for concurrent use.
|
|
type Server struct {
|
|
connID uint64
|
|
_now func() time.Time
|
|
stratum Stratum
|
|
prec int8
|
|
refID [4]byte
|
|
pending []pendingRequest
|
|
}
|
|
|
|
type pendingRequest struct {
|
|
origin Timestamp
|
|
}
|
|
|
|
// Reset re-initialises the server with cfg. Increments connID.
|
|
func (h *Server) Reset(cfg ServerConfig) error {
|
|
if cfg.Now == nil {
|
|
return lneto.ErrInvalidConfig
|
|
}
|
|
if cfg.MaxPending <= 0 {
|
|
cfg.MaxPending = 4
|
|
}
|
|
pending := h.pending[:0]
|
|
if cap(pending) < cfg.MaxPending {
|
|
pending = make([]pendingRequest, 0, cfg.MaxPending)
|
|
}
|
|
*h = Server{
|
|
connID: h.connID + 1,
|
|
_now: cfg.Now,
|
|
stratum: cfg.Stratum,
|
|
prec: cfg.Precision,
|
|
refID: cfg.RefID,
|
|
pending: pending,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConnectionID implements [lneto.StackNode].
|
|
func (h *Server) ConnectionID() *uint64 { return &h.connID }
|
|
|
|
// Protocol implements [lneto.StackNode].
|
|
func (h *Server) Protocol() uint64 { return 0 }
|
|
|
|
// LocalPort implements [lneto.StackNode].
|
|
func (h *Server) LocalPort() uint16 { return ServerPort }
|
|
|
|
// Encapsulate implements [lneto.StackNode]. It writes one pending NTP server
|
|
// response into carrierData. Returns 0 when no pending requests exist.
|
|
func (h *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
|
if len(h.pending) == 0 {
|
|
return 0, nil
|
|
}
|
|
buf := carrierData[offsetToFrame:]
|
|
frm, err := NewFrame(buf)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
req := h.pending[len(h.pending)-1]
|
|
h.pending = h.pending[:len(h.pending)-1]
|
|
|
|
now := h.now()
|
|
xmt, err := TimestampFromTime(now)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
frm.ClearHeader()
|
|
frm.SetFlags(ModeServer, Version4, LeapNoWarning)
|
|
frm.SetStratum(h.stratum)
|
|
frm.SetPrecision(h.prec)
|
|
frm.SetPoll(6)
|
|
*frm.ReferenceID() = h.refID
|
|
frm.SetOriginTime(req.origin)
|
|
frm.SetReceiveTime(xmt)
|
|
frm.SetTransmitTime(xmt)
|
|
return SizeHeader, nil
|
|
}
|
|
|
|
// Demux implements [lneto.StackNode]. It validates an incoming NTP client
|
|
// request and queues it for response via [Server.Encapsulate].
|
|
func (h *Server) Demux(carrierData []byte, frameOffset int) error {
|
|
buf := carrierData[frameOffset:]
|
|
frm, err := NewFrame(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mode, version, _ := frm.Flags()
|
|
if mode != ModeClient {
|
|
return lneto.ErrPacketDrop
|
|
}
|
|
if version != Version4 {
|
|
return lneto.ErrPacketDrop
|
|
}
|
|
|
|
if len(h.pending) == cap(h.pending) {
|
|
return lneto.ErrExhausted
|
|
}
|
|
|
|
h.pending = append(h.pending, pendingRequest{
|
|
origin: frm.TransmitTime(),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (h *Server) now() time.Time {
|
|
if h._now == nil {
|
|
return time.Now()
|
|
}
|
|
return h._now()
|
|
}
|