mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
feat(ntp): add two-exchange client, server handler, and extension fields (#101)
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>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
// Command ntp-client performs a two-exchange NTP clock synchronization against
|
||||
// a remote server and prints the corrected time, clock offset, and round-trip
|
||||
// delay.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./examples/ntp-client/ -server pool.ntp.org:123
|
||||
// go run ./examples/ntp-client/ -server 127.0.0.1:10123 -debug
|
||||
//
|
||||
// This tool uses the standard library net package for UDP transport instead of
|
||||
// lneto's own networking stack. These examples exercise one protocol layer at a
|
||||
// time in isolation, keeping the transport concern separate so failures are
|
||||
// clearly attributable to the NTP codec and state machine rather than the
|
||||
// full-stack IP/UDP path.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/ntp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
addr := flag.String("server", "pool.ntp.org:123", "NTP server address (host:port)")
|
||||
debug := flag.Bool("debug", false, "enable debug logging")
|
||||
flag.Parse()
|
||||
|
||||
conn, err := net.DialTimeout("udp", *addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var precBuf [64]int64
|
||||
sysprec := ntp.CalculateSystemPrecision(nil, precBuf[:])
|
||||
|
||||
var client ntp.Client
|
||||
client.Reset(sysprec, time.Now)
|
||||
if *debug {
|
||||
client.SetLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||
}
|
||||
|
||||
const maxRetries = 10
|
||||
var buf [1500]byte
|
||||
for attempt := 0; !client.IsDone() && attempt < maxRetries; attempt++ {
|
||||
n, err := client.Encapsulate(buf[:ntp.SizeHeader], 0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encapsulate: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("encapsulate returned 0 bytes unexpectedly")
|
||||
}
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err = conn.Write(buf[:n]); err != nil {
|
||||
return fmt.Errorf("write: %w", err)
|
||||
}
|
||||
|
||||
rn, err := conn.Read(buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read: %w", err)
|
||||
}
|
||||
|
||||
if err = client.Demux(buf[:rn], 0); err != nil {
|
||||
return fmt.Errorf("demux: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !client.IsDone() {
|
||||
return fmt.Errorf("NTP exchange did not complete within %d attempts", maxRetries)
|
||||
}
|
||||
|
||||
fmt.Printf("NTP time: %s\n", client.Now().Format(time.RFC3339Nano))
|
||||
fmt.Printf("Offset: %s\n", client.Offset())
|
||||
fmt.Printf("RTD: %s\n", client.RoundTripDelay())
|
||||
fmt.Printf("Stratum: %s\n", client.ServerStratum())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Command ntp-server is a minimal NTP server that listens for client requests
|
||||
// on a UDP socket and responds with the current system time. It serves as an
|
||||
// integration test target for the ntp-client example.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./examples/ntp-server/ -addr :10123
|
||||
//
|
||||
// The listen address defaults to :123 (requires root).
|
||||
//
|
||||
// This tool uses the standard library net package for UDP transport instead of
|
||||
// lneto's own networking stack. These examples exercise one protocol layer at a
|
||||
// time in isolation, keeping the transport concern separate so failures are
|
||||
// clearly attributable to the NTP codec and state machine rather than the
|
||||
// full-stack IP/UDP path.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/ntp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
listenAddr := flag.String("addr", ":123", "UDP listen address (host:port)")
|
||||
flag.Parse()
|
||||
|
||||
pc, err := net.ListenPacket("udp", *listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
defer pc.Close()
|
||||
fmt.Printf("NTP server listening on %s\n", pc.LocalAddr())
|
||||
|
||||
var precBuf [64]int64
|
||||
sysprec := ntp.CalculateSystemPrecision(nil, precBuf[:])
|
||||
|
||||
var handler ntp.Server
|
||||
err = handler.Reset(ntp.ServerConfig{
|
||||
Now: time.Now,
|
||||
Stratum: ntp.StratumPrimary,
|
||||
Precision: sysprec,
|
||||
RefID: [4]byte{'G', 'O', 'L', 'N'},
|
||||
MaxPending: 16,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("server reset: %w", err)
|
||||
}
|
||||
|
||||
var buf [1500]byte
|
||||
var backoff uint
|
||||
for {
|
||||
pc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
n, raddr, err := pc.ReadFrom(buf[:])
|
||||
if err != nil {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
backoff++
|
||||
if backoff > 10 {
|
||||
time.Sleep(time.Duration(backoff) * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
fmt.Printf("read error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
backoff = 0
|
||||
|
||||
if err = handler.Demux(buf[:n], 0); err != nil {
|
||||
fmt.Printf("demux error from %s: %v\n", raddr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var resp [ntp.SizeHeader]byte
|
||||
rn, err := handler.Encapsulate(resp[:], 0, 0)
|
||||
if err != nil {
|
||||
fmt.Printf("encapsulate error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if rn > 0 {
|
||||
if _, err = pc.WriteTo(resp[:rn], raddr); err != nil {
|
||||
fmt.Printf("write error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user