NTP up and running

This commit is contained in:
soypat
2025-07-12 16:45:20 -03:00
parent 698610242e
commit 2bd056a04c
4 changed files with 255 additions and 81 deletions
+73 -12
View File
@@ -11,7 +11,6 @@ import (
"net/netip" "net/netip"
"os" "os"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
@@ -24,6 +23,7 @@ import (
"github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/internet" "github.com/soypat/lneto/internet"
"github.com/soypat/lneto/internet/pcap" "github.com/soypat/lneto/internet/pcap"
"github.com/soypat/lneto/ntp"
) )
var softRand = time.Now().Unix() var softRand = time.Now().Unix()
@@ -43,11 +43,13 @@ func run() (err error) {
flagUseHTTP = false flagUseHTTP = false
flagHostToResolve = "" flagHostToResolve = ""
flagRequestedIP = "" flagRequestedIP = ""
flagDoNTP = false
) )
flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.") flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.")
flag.BoolVar(&flagUseHTTP, "http", flagUseHTTP, "Use HTTP tap interface.") flag.BoolVar(&flagUseHTTP, "http", flagUseHTTP, "Use HTTP tap interface.")
flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.") flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.")
flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.") flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.")
flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time")
flag.Parse() flag.Parse()
fmt.Println("softrand", softRand) fmt.Println("softrand", softRand)
_, err = dns.NewName(flagHostToResolve) _, err = dns.NewName(flagHostToResolve)
@@ -80,34 +82,41 @@ func run() (err error) {
return err return err
} }
brHW := nicHW brHW := nicHW
brHW[5] += byte(softRand)%128 + 1 // We'll be using a similar HW address but with NIC specific identifier modified. brHW[4]++
mtu, err := iface.MTU() mtu, err := iface.MTU()
if err != nil { if err != nil {
return err return err
} }
nicAddr, err := iface.IPMask() nicAddr, err := iface.IPMask()
if err != nil { if err != nil {
return err return err
} }
fmt.Println("NIC hardware address:", net.HardwareAddr(nicHW[:]).String(), "bridgeHW:", net.HardwareAddr(brHW[:]).String(), "mtu:", mtu, "addr:", nicAddr.String()) fmt.Println("NIC hardware address:", net.HardwareAddr(nicHW[:]).String(), "bridgeHW:", net.HardwareAddr(brHW[:]).String(), "mtu:", mtu, "addr:", nicAddr.String())
var stack Stack var stack Stack
err = stack.Reset(brHW, netip.AddrFrom4([4]byte{}), uint16(mtu)) err = stack.Reset(brHW, netip.AddrFrom4([4]byte{}), uint16(mtu))
if err != nil { if err != nil {
return err return err
} }
err = stack.BeginDHCPRequest([4]byte{192, 168, 1, 199})
if err != nil {
return err
}
buf := make([]byte, mtu) buf := make([]byte, mtu)
lastAction := time.Now() lastAction := time.Now()
const ( const (
stateDHCP = iota stateDHCP = iota
stateInitARP stateInitARP
stateDNSNTP
stateNTP
stateDNS stateDNS
stateDone stateDone
) )
err = stack.BeginDHCPRequest([4]byte{192, 168, 1, 96})
if err != nil {
return err
}
state := stateDHCP state := stateDHCP
prevState := state
for { for {
switch state { switch state {
case stateDHCP: case stateDHCP:
@@ -128,14 +137,46 @@ func run() (err error) {
router := stack.dhcp.RouterAddr() router := stack.dhcp.RouterAddr()
hw, err := stack.ResultResolveHardwareAddress6(netip.AddrFrom4(router)) hw, err := stack.ResultResolveHardwareAddress6(netip.AddrFrom4(router))
if err == nil { if err == nil {
state = stateDNS
stack.link.SetGateway6(hw) stack.link.SetGateway6(hw)
stack.link.SetHardwareAddr6([6]byte{0xd8, 0x5e, 0xd3, 0x43, 0x03, 0xeb})
stack.ip.SetAddr(netip.AddrFrom4([4]byte{192, 168, 1, 53}))
if flagDoNTP {
state = stateDNSNTP
err = stack.StartLookupIP("pool.ntp.org")
} else {
state = stateDNS
err = stack.StartLookupIP(flagHostToResolve)
}
if err != nil {
return err
}
}
case stateDNSNTP:
addrs, done, err := stack.ResultLookupIP()
if err == nil {
state = stateNTP
fmt.Println("START NTP")
err = stack.StartNTP(addrs[0])
} else if !done {
err = nil
}
if err != nil {
return err
}
case stateNTP:
offset, done := stack.ResultNTP()
if done {
relative := "behind"
if offset < 0 {
relative = "ahead"
}
fmt.Println("NTP completed. You are", offset.Abs(), relative, "of the NTP server")
state = stateDNS
err = stack.StartLookupIP(flagHostToResolve) err = stack.StartLookupIP(flagHostToResolve)
if err != nil { if err != nil {
return err return err
} }
} }
case stateDNS: case stateDNS:
addrs, done, err := stack.ResultLookupIP() addrs, done, err := stack.ResultLookupIP()
if err == nil { if err == nil {
@@ -145,6 +186,10 @@ func run() (err error) {
return err return err
} }
} }
if prevState != state {
fmt.Println("STATE CHANGE", prevState, state)
}
prevState = state
clear(buf) clear(buf)
nwrite, err := stack.Encapsulate(buf[:], 0) nwrite, err := stack.Encapsulate(buf[:], 0)
@@ -189,6 +234,8 @@ type Stack struct {
dns dns.Client dns dns.Client
ednsopt dns.Resource ednsopt dns.Resource
lookup dns.Message lookup dns.Message
ntp ntp.Client
sysprec int8 // NTP system precision.
// Packet capture and top level filtering. // Packet capture and top level filtering.
shark pcap.PacketBreakdown shark pcap.PacketBreakdown
@@ -198,8 +245,9 @@ type Stack struct {
func (s *Stack) Demux(b []byte, _ int) (err error) { func (s *Stack) Demux(b []byte, _ int) (err error) {
s.aux, err = s.shark.CaptureEthernet(s.aux[:0], b, 0) s.aux, err = s.shark.CaptureEthernet(s.aux[:0], b, 0)
topFrame := s.aux[len(s.aux)-1] topFrame := s.aux[len(s.aux)-1]
isOK := topFrame.Protocol == "DHCPv4" || // Allow DHCP responses. isOK := topFrame.Protocol == "DHCPv4" || // Allow DHCP, DNS and NTP responses.
topFrame.Protocol == "DNS" || topFrame.Protocol == "DNS" ||
topFrame.Protocol == "NTP" ||
topFrame.Protocol == ethernet.TypeARP // Allow ARP responses. topFrame.Protocol == ethernet.TypeARP // Allow ARP responses.
if !isOK { if !isOK {
return nil return nil
@@ -269,6 +317,8 @@ func (s *Stack) Reset(mac [6]byte, addr netip.Addr, mtu uint16) error {
return err return err
} }
s.ip.SetLogger(slog.Default()) s.ip.SetLogger(slog.Default())
var timebuf [32]time.Time
s.sysprec = ntp.CalculateSystemPrecision(time.Now, timebuf[:])
return nil return nil
} }
@@ -281,8 +331,6 @@ func (s *Stack) StartLookupIP(host string) error {
if err != nil { if err != nil {
return err return err
} }
s.link.SetHardwareAddr6([6]byte{0xd8, 0x5e, 0xd3, 0x43, 0x03, 0xeb})
s.ip.SetAddr(netip.AddrFrom4([4]byte{192, 168, 1, 53}))
s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil) s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil)
err = s.dns.StartResolve(uint16(softRand>>1)+1024, uint16(softRand), dns.ResolveConfig{ err = s.dns.StartResolve(uint16(softRand>>1)+1024, uint16(softRand), dns.ResolveConfig{
Questions: []dns.Question{ Questions: []dns.Question{
@@ -330,6 +378,10 @@ func (s *Stack) ResultLookupIP() ([]netip.Addr, bool, error) {
return addrs, done, nil return addrs, done, nil
} }
func (s *Stack) ResultNTP() (time.Duration, bool) {
return s.ntp.Offset(), s.ntp.IsDone()
}
func (s *Stack) BeginDHCPRequest(request [4]byte) error { func (s *Stack) BeginDHCPRequest(request [4]byte) error {
var buf [4]byte var buf [4]byte
rand.Read(buf[:]) rand.Read(buf[:])
@@ -337,7 +389,7 @@ func (s *Stack) BeginDHCPRequest(request [4]byte) error {
err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{ err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{
RequestedAddr: request, RequestedAddr: request,
ClientHardwareAddr: s.link.HardwareAddr6(), ClientHardwareAddr: s.link.HardwareAddr6(),
Hostname: "lneto" + strconv.FormatInt(softRand%100, 16), Hostname: "lneto",
}) })
if err != nil { if err != nil {
return err return err
@@ -351,6 +403,15 @@ func (s *Stack) BeginDHCPRequest(request [4]byte) error {
return err return err
} }
func (s *Stack) StartNTP(addr netip.Addr) error {
s.ntp.Reset(time.Now, s.sysprec)
var u internet.StackUDPPort
addr4 := addr.As4()
u.SetStackNode(&s.ntp, addr4[:], ntp.ServerPort)
err := s.udps.Register(&u)
return err
}
func (s *Stack) StartResolveHardwareAddress6(ip netip.Addr) error { func (s *Stack) StartResolveHardwareAddress6(ip netip.Addr) error {
if !ip.Is4() { if !ip.Is4() {
return errors.New("unsupported or invalid IP address") return errors.New("unsupported or invalid IP address")
+104 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv6" "github.com/soypat/lneto/ipv6"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp" "github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp" "github.com/soypat/lneto/udp"
) )
@@ -324,10 +325,14 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
dst = append(dst, finfo) dst = append(dst, finfo)
end := bitOffset + 8*octet end := bitOffset + 8*octet
payload := ufrm.Payload() payload := ufrm.Payload()
dstport := ufrm.DestinationPort()
srcport := ufrm.SourcePort()
if dhcpv4.PayloadIsDHCPv4(payload) { if dhcpv4.PayloadIsDHCPv4(payload) {
dst, err = pc.CaptureDHCPv4(dst, pkt, end) dst, err = pc.CaptureDHCPv4(dst, pkt, end)
} else if ufrm.DestinationPort() == 53 || ufrm.SourcePort() == 53 { } else if dstport == dns.ServerPort || srcport == dns.ServerPort {
dst, err = pc.CaptureDNS(dst, pkt, end) dst, err = pc.CaptureDNS(dst, pkt, end)
} else if dstport == ntp.ServerPort || srcport == ntp.ServerPort {
dst, err = pc.CaptureNTP(dst, pkt, end)
} }
if err != nil { if err != nil {
dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)))
@@ -361,6 +366,24 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
return dst, nil return dst, nil
} }
func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return nil, errors.New("NTP must be parsed at byte boundary")
}
ntpData := pkt[bitOffset/8:]
_, err := ntp.NewFrame(ntpData)
if err != nil {
return dst, err
}
finfo := Frame{
Protocol: "NTP",
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseNTPFields[:]...)
dst = append(dst, finfo)
return dst, nil
}
func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 { if bitOffset%8 != 0 {
return nil, errors.New("DHCP must be parsed at byte boundary") return nil, errors.New("DHCP must be parsed at byte boundary")
@@ -954,6 +977,86 @@ var baseDHCPv4Fields = [...]FrameField{
}, },
} }
var baseNTPFields = [...]FrameField{
{
Name: "Mode",
Class: FieldClassType,
FrameBitOffset: 0,
BitLength: 3,
},
{
Class: FieldClassVersion,
FrameBitOffset: 3,
BitLength: 2,
},
{
Name: "Leap Indicator",
Class: fieldClassUndefined,
FrameBitOffset: 5,
BitLength: 3,
},
{
Name: "Stratum",
Class: fieldClassUndefined,
FrameBitOffset: 1 * octet,
BitLength: 1 * octet,
},
{
Name: "Poll",
Class: fieldClassUndefined,
FrameBitOffset: 2 * octet,
BitLength: 1 * octet,
},
{
Name: "System Precision",
Class: fieldClassUndefined,
FrameBitOffset: 3 * octet,
BitLength: 1 * octet,
},
{
Name: "Root Delay",
Class: fieldClassUndefined,
FrameBitOffset: 4 * octet,
BitLength: 4 * octet,
},
{
Name: "Root Dispersion",
Class: fieldClassUndefined,
FrameBitOffset: 8 * octet,
BitLength: 4 * octet,
},
{
Name: "Reference ID",
Class: FieldClassText,
FrameBitOffset: 12 * octet,
BitLength: 4 * octet,
},
{
Name: "Reference Time",
Class: FieldClassText,
FrameBitOffset: 16 * octet,
BitLength: 8 * octet,
},
{
Name: "Origin Time",
Class: FieldClassText,
FrameBitOffset: 24 * octet,
BitLength: 8 * octet,
},
{
Name: "Receive Time",
Class: FieldClassText,
FrameBitOffset: 32 * octet,
BitLength: 8 * octet,
},
{
Name: "Transit Time",
Class: FieldClassText,
FrameBitOffset: 40 * octet,
BitLength: 8 * octet,
},
}
func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame { func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame {
return Frame{ return Frame{
Protocol: proto, Protocol: proto,
+70 -48
View File
@@ -2,7 +2,6 @@ package ntp
import ( import (
"errors" "errors"
"io"
"time" "time"
) )
@@ -23,33 +22,37 @@ type Client struct {
connID uint64 connID uint64
start time.Time start time.Time
_now func() time.Time _now func() time.Time
t [4]Timestamp // t stores the time offsets needed to compute the time at client
// taking into consideration the round-trip delay.
// - t[0] (orig): Client timestamp of request packet transmission.
// - t[1] (rec): Server timestamp of request packet reception.
// - t[2] (xmt): Server timestamp of response packet transmission.
// - t[3]: Client timestamp of response packet reception.
t [4]Timestamp
// org Timestamp // org Timestamp
// rec Timestamp
xmt Timestamp
state state state state
serverStratum Stratum serverStratum Stratum
_sysprec int8 sysprec int8
} }
func (c *Client) Reset(now func() time.Time) { func (c *Client) Reset(now func() time.Time, sysprec int8) {
if c._sysprec == 0 {
c._sysprec = sysprecRecalcNeeded
}
*c = Client{ *c = Client{
connID: c.connID + 1, connID: c.connID + 1,
_now: now, _now: now,
_sysprec: c._sysprec, sysprec: sysprec,
state: stateSend1,
} }
} }
func (c *Client) Protocol() uint64 { return 0 }
func (c *Client) LocalPort() uint16 { return ClientPort }
func (c *Client) ConnectionID() *uint64 { func (c *Client) ConnectionID() *uint64 {
return &c.connID return &c.connID
} }
func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) { func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) {
if c.isDone() { if c.IsDone() {
return 0, io.EOF return 0, nil
} }
payload := carrierData[frameOffset:] payload := carrierData[frameOffset:]
frm, err := NewFrame(payload) frm, err := NewFrame(payload)
@@ -60,10 +63,10 @@ func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) {
switch c.state { switch c.state {
case stateSend1: case stateSend1:
c.start = c.now() c.start = c.now()
c.xmt = TimestampFromUint64(0) c.t[0] = TimestampFromUint64(0)
c.state = stateAwait1 c.state = stateAwait1
case stateSend2: case stateSend2:
c.xmt = c.unsyncTimestamp(c.now()) // c.xmt = c.unsyncTimestamp(c.now())
c.state = stateDone c.state = stateDone
default: default:
return 0, nil // Nothing to handle. return 0, nil // Nothing to handle.
@@ -72,46 +75,47 @@ func (c *Client) Encapsulate(carrierData []byte, frameOffset int) (int, error) {
for i := range payload[:SizeHeader] { for i := range payload[:SizeHeader] {
payload[i] = 0 payload[i] = 0
} }
sysprec := c.sysprec()
frm.ClearHeader() frm.ClearHeader()
frm.SetStratum(StratumUnsync) frm.SetStratum(StratumUnsync)
frm.SetPoll(6) frm.SetPoll(6)
frm.SetPrecision(sysprec) frm.SetPrecision(c.sysprec)
frm.SetOriginTime(c.xmt) frm.SetOriginTime(c.t[0])
frm.SetFlags(ModeClient, Version4, LeapNoWarning) frm.SetFlags(ModeClient, Version4, LeapNoWarning)
return SizeHeader, nil return SizeHeader, nil
} }
func (c *Client) Demux(carrierData []byte, frameOffset int) error { func (c *Client) Demux(carrierData []byte, frameOffset int) error {
if c.isDone() { if c.IsDone() {
return io.EOF return nil
} }
payload := carrierData[frameOffset:] payload := carrierData[frameOffset:]
frm, err := NewFrame(payload) frm, err := NewFrame(payload)
if err != nil { if err != nil {
return err return err
} }
t := &c.t
switch c.state { switch c.state {
case stateAwait1: case stateAwait1:
tstx := frm.TransmitTime() xmt := frm.TransmitTime()
tsorig := frm.OriginTime() orig := frm.OriginTime()
if tstx == tsorig || tsorig == c.xmt { if xmt == orig || orig != c.t[0] {
return errors.New("bogus NTP packet") return errors.New("bogus NTP packet")
} }
t[0] = tsorig
t[1] = frm.ReceiveTime() txelapsed := c.now().Sub(c.start)
t[2] = tstx c.t[1] = frm.ReceiveTime()
t[3] = c.unsyncTimestamp(c.now()) c.t[2] = xmt
c.t[3] = c.t[0].Add(txelapsed)
c.serverStratum = frm.Stratum() c.serverStratum = frm.Stratum()
c.state = stateDone c.state = stateDone // TODO: add second exchange part.
case stateAwait2: case stateAwait2:
c.state = stateAwait2 c.state = stateDone
} }
return nil return nil
} }
func (c *Client) isDone() bool { func (c *Client) IsDone() bool {
return c.state == stateDone return c.state == stateDone
} }
@@ -122,30 +126,48 @@ func (c *Client) now() time.Time {
return c._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. // Now returns the current time as corrected by NTP protocol.
func (c *Client) Now() time.Time { func (c *Client) Now() time.Time {
return c.now().Add(c.Offset()) now, off := c.offsetAndNow()
return now.Add(off)
} }
// ServerStratum returns the stratum of the server client synchronized with. // ServerStratum returns the stratum of the server client synchronized with.
func (c *Client) ServerStratum() Stratum { return c.serverStratum } func (c *Client) ServerStratum() Stratum { return c.serverStratum }
// Offset returns the // Offset is a helper method to determine the difference between the Client's clock and the server's clock.
// Use [Client.Now] to calculate the server's time.
func (c *Client) Offset() time.Duration { func (c *Client) Offset() time.Duration {
if c.isDone() { if c.IsDone() {
t := &c.t _, off := c.offsetAndNow()
return t[1].Sub(t[0])/2 + t[2].Sub(t[3])/2 return off
} }
return 0 return 0
} }
func (c *Client) offsetAndNow() (clientNow time.Time, offset time.Duration) {
now := c.now()
serverToBase := c.OffsetUnsynced()
clientToBase := now.Sub(BaseTime())
serverToClient := serverToBase - clientToBase
return now, serverToClient
}
// OffsetUnsynced returns the absolute time offset difference between client and server clock
// as calculated by the clock synchonization algorithm. It is unsynchonized- the result of OffsetUnsynced will not change with time.
func (c *Client) OffsetUnsynced() time.Duration {
if c.IsDone() {
t := &c.t
return (t[1].Sub(t[0]) + t[2].Sub(t[3])) / 2
}
return 0
}
func (c *Client) RoundTripDelay() time.Duration {
if c.IsDone() {
d0 := c.t[3].Sub(c.t[0])
d1 := c.t[2].Sub(c.t[1])
return d0 - d1
}
return -1
}
+8 -20
View File
@@ -43,13 +43,13 @@ type Frame struct {
func (frm Frame) Flags() (mode Mode, version uint8, lp LeapIndicator) { func (frm Frame) Flags() (mode Mode, version uint8, lp LeapIndicator) {
b := frm.buf[0] b := frm.buf[0]
mode = Mode(b & 0b111) mode = Mode(b & 0b111)
version = (b << 3) & 0b11 version = (b >> 3) & 0b111
lp = LeapIndicator(b >> 5) lp = LeapIndicator(b >> 6)
return mode, version, lp return mode, version, lp
} }
func (frm Frame) SetFlags(mode Mode, version uint8, lp LeapIndicator) { func (frm Frame) SetFlags(mode Mode, version uint8, lp LeapIndicator) {
b := uint8(mode)&0b111 | (Version4&0b11)<<3 | uint8(lp&0b111)<<5 b := uint8(mode)&0b111 | (version&0b111)<<3 | uint8(lp&0b11)<<6
frm.buf[0] = b frm.buf[0] = b
} }
@@ -269,31 +269,19 @@ var (
sysPrec int8 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. // 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. // If the time source is nil the default static call to [time.Now] is used.
func CalculateSystemPrecision(now func() time.Time) int8 { func CalculateSystemPrecision(now func() time.Time, iters []time.Time) int8 {
const maxIter = 16 maxIter := len(iters)
var times [maxIter]time.Time
if now == nil { if now == nil {
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
times[i] = time.Now() iters[i] = time.Now()
} }
} else { } else {
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
times[i] = now() iters[i] = now()
} }
} }
avg := times[maxIter-1].Sub(times[0]) / maxIter avg := iters[maxIter-1].Sub(iters[0]) / time.Duration(maxIter)
return int8(math.Log2(avg.Seconds())) return int8(math.Log2(avg.Seconds()))
} }