mirror of
https://github.com/soypat/lneto.git
synced 2026-08-05 23:43:40 +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>
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
package ntp
|
|
|
|
import (
|
|
"encoding/binary"
|
|
|
|
"github.com/soypat/lneto"
|
|
)
|
|
|
|
// ExtType identifies the type of an NTP extension field.
|
|
// See RFC 7822 and RFC 8915.
|
|
type ExtType uint16
|
|
|
|
const (
|
|
// NTS Unique Identifier extension field (RFC 8915 §5.3, critical).
|
|
// Contains a random nonce used to prevent replay attacks.
|
|
ExtNTSUniqueID ExtType = 0x0104
|
|
// NTS Cookie extension field (RFC 8915 §5.4).
|
|
// Contains an encrypted cookie obtained during NTS-KE key exchange.
|
|
ExtNTSCookie ExtType = 0x0204
|
|
// NTS Cookie Placeholder extension field (RFC 8915 §5.5).
|
|
// Requests additional cookies from the server in its response.
|
|
ExtNTSCookiePlaceholder ExtType = 0x0304
|
|
// NTS Authenticator and Encrypted Extension Fields (RFC 8915 §5.6, critical).
|
|
// Contains the AEAD-authenticated and encrypted extension fields.
|
|
//
|
|
// Full NTS authentication using this field requires an AEAD cipher
|
|
// (AEAD_AES_SIV_CMAC_256 per RFC 8915 §5.7) and session keys obtained
|
|
// during NTS-KE (RFC 8915 §4). The crypto portion is not implemented
|
|
// here due to AES-SIV not being available in the Go standard library.
|
|
// Callers may supply their own cipher.AEAD to build/verify this field.
|
|
ExtNTSAuthAndEEF ExtType = 0x0404
|
|
)
|
|
|
|
// sizeExtHeader is the fixed 4-byte header size of every NTP extension field (RFC 7822).
|
|
const sizeExtHeader = 4
|
|
|
|
// ExtField provides zero-copy access to a single NTP extension field
|
|
// within an existing packet buffer.
|
|
type ExtField struct {
|
|
buf []byte
|
|
}
|
|
|
|
// Type returns the extension field type.
|
|
func (ef ExtField) Type() ExtType {
|
|
return ExtType(binary.BigEndian.Uint16(ef.buf[0:2]))
|
|
}
|
|
|
|
// TotalLen returns the total length of the extension field, including the
|
|
// 4-byte header. Always a multiple of 4.
|
|
func (ef ExtField) TotalLen() uint16 {
|
|
return binary.BigEndian.Uint16(ef.buf[2:4])
|
|
}
|
|
|
|
// Value returns the extension field value bytes (body only, without the 4-byte header).
|
|
// Returns nil if the length field is inconsistent with the buffer.
|
|
func (ef ExtField) Value() []byte {
|
|
n := int(ef.TotalLen())
|
|
if n < sizeExtHeader || n > len(ef.buf) {
|
|
return nil
|
|
}
|
|
return ef.buf[sizeExtHeader:n]
|
|
}
|
|
|
|
// RawData returns the complete extension field bytes including the 4-byte header.
|
|
func (ef ExtField) RawData() []byte { return ef.buf }
|
|
|
|
// NextExtField parses the first NTP extension field from buf and returns it
|
|
// along with the number of bytes consumed. An empty buf returns a zero n
|
|
// with nil error. Use this in a loop:
|
|
//
|
|
// for off := 0; off < len(payload); {
|
|
// field, n, err := ntp.NextExtField(payload[off:])
|
|
// if err != nil { break }
|
|
// // process field
|
|
// off += n
|
|
// }
|
|
func NextExtField(buf []byte) (field ExtField, n int, err error) {
|
|
if len(buf) == 0 {
|
|
return ExtField{}, 0, nil
|
|
}
|
|
if len(buf) < sizeExtHeader {
|
|
return ExtField{}, 0, lneto.ErrTruncatedFrame
|
|
}
|
|
totalLen := int(binary.BigEndian.Uint16(buf[2:4]))
|
|
if totalLen < sizeExtHeader {
|
|
return ExtField{}, 0, lneto.ErrInvalidLengthField
|
|
}
|
|
if totalLen%4 != 0 {
|
|
return ExtField{}, 0, lneto.ErrInvalidLengthField
|
|
}
|
|
if totalLen > len(buf) {
|
|
return ExtField{}, 0, lneto.ErrTruncatedFrame
|
|
}
|
|
return ExtField{buf: buf[:totalLen]}, totalLen, nil
|
|
}
|
|
|
|
// AppendExtField appends a single NTP extension field with the given type and value
|
|
// to dst. The value is zero-padded to the nearest 4-byte boundary. Returns the
|
|
// extended dst slice. Panics if the padded total length exceeds 65535 (the uint16 maximum),
|
|
// which cannot occur with any valid NTP packet payload.
|
|
func AppendExtField(dst []byte, typ ExtType, value []byte) []byte {
|
|
padded := (len(value) + 3) &^ 3
|
|
total := sizeExtHeader + padded
|
|
if total > 0xFFFF {
|
|
panic("ntp: AppendExtField: value too large to encode in uint16 length field")
|
|
}
|
|
var hdr [sizeExtHeader]byte
|
|
binary.BigEndian.PutUint16(hdr[0:2], uint16(typ))
|
|
binary.BigEndian.PutUint16(hdr[2:4], uint16(total))
|
|
dst = append(dst, hdr[:]...)
|
|
dst = append(dst, value...)
|
|
for i := len(value); i < padded; i++ {
|
|
dst = append(dst, 0)
|
|
}
|
|
return dst
|
|
}
|