Skip to content

Commit b1f88cd

Browse files
committed
Add Timestamps field to Response
1 parent c40b21c commit b1f88cd

5 files changed

Lines changed: 108 additions & 84 deletions

File tree

ntp.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,14 @@ type Response struct {
242242
// clock.
243243
ClockOffset time.Duration
244244

245-
// Time is the time the server transmitted this response, measured using
246-
// its own clock. You should not use this value for time synchronization
247-
// purposes. Add ClockOffset to your system clock instead.
248-
Time time.Time
249-
250245
// RTT is the measured round-trip-time delay estimate between the client
251246
// and the server.
252247
RTT time.Duration
253248

249+
// Timestamps contains the four NTP protocol timestamps used to calculate
250+
// ClockOffset and RTT.
251+
Timestamps ProtocolTimestamps
252+
254253
// Precision is the reported precision of the server's clock.
255254
Precision time.Duration
256255

@@ -368,9 +367,36 @@ type Response struct {
368367
// Used only in NTPv5.
369368
ServerCookie uint64
370369

370+
// Time is the time the server transmitted this response, measured using
371+
// its own clock. You should not use this value for time synchronization
372+
// purposes. Add ClockOffset to your system clock instead.
373+
//
374+
// DEPRECATED. Use Timestamps.ServerXmit instead.
375+
Time time.Time
376+
371377
authErr error
372378
}
373379

380+
// ProtocolTimestamps contains the four timestamps used by the NTP protocol to
381+
// calculate clock offset and round-trip delay.
382+
type ProtocolTimestamps struct {
383+
// ClientXmit is the timestamp at which the client transmitted the
384+
// request. Recorded using the client's clock.
385+
ClientXmit time.Time
386+
387+
// ServerRecv is the timestamp at which the server received the request.
388+
// Recorded using the server's clock.
389+
ServerRecv time.Time
390+
391+
// ServerXmit is the timestamp at which the server transmitted the
392+
// response. Recorded using the server's clock.
393+
ServerXmit time.Time
394+
395+
// ClientRecv is the timestamp at which the client received the response.
396+
// Recorded using the client's clock.
397+
ClientRecv time.Time
398+
}
399+
374400
// The TimescaleOffset struct contains a timescale identifier and its
375401
// corresponding offset relative to the primary timescale specified in the
376402
// QueryOptions Timescale field. Used only in NTPv5.
@@ -558,7 +584,7 @@ func QueryWithOptions(remoteAddress string, opt QueryOptions) (*Response, error)
558584
opt.Port = defaultPort
559585
}
560586
if opt.GetSystemTime == nil {
561-
opt.GetSystemTime = time.Now
587+
opt.GetSystemTime = func() time.Time { return time.Now().UTC() }
562588
}
563589
if opt.Dial != nil {
564590
// wrapper for the deprecated Dial callback.

ntp4.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ func queryV4(conn net.Conn, opt *QueryOptions) (*Response, error) {
173173
// Compose the response struct.
174174
r := &Response{
175175
ClockOffset: offset(t1, t2, t3, t4),
176-
Time: m.TransmitTime.TimeV4(),
177176
RTT: rtt(t1, t2, t3, t4),
177+
Timestamps: ProtocolTimestamps{t1, t2, t3, t4},
178178
Precision: toInterval(m.Precision),
179179
Version: m.getVersion(),
180180
Stratum: m.Stratum,
@@ -188,6 +188,7 @@ func queryV4(conn net.Conn, opt *QueryOptions) (*Response, error) {
188188
MinError: minError(t1, t2, t3, t4),
189189
Poll: toInterval(m.Poll),
190190
Flags: 0,
191+
Time: m.TransmitTime.TimeV4(),
191192
}
192193

193194
// Calculate root distance.

ntp4_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ import (
1515
)
1616

1717
func logResponseV4(t *testing.T, r *Response) {
18-
now := time.Now()
18+
now := time.Now().Local()
1919
t.Logf("[%s] Version: %d", host, r.Version)
2020
t.Logf("[%s] ClockOffset: %s", host, r.ClockOffset)
2121
t.Logf("[%s] RTT: %s", host, r.RTT)
2222
t.Logf("[%s] SystemTime: %s", host, fmtTime(now))
2323
t.Logf("[%s] ~TrueTime: %s", host, fmtTime(now.Add(r.ClockOffset)))
24-
t.Logf("[%s] XmitTime: %s", host, fmtTime(r.Time))
24+
t.Logf("[%s] ClientXmit: %s", host, fmtTime(r.Timestamps.ClientXmit))
25+
t.Logf("[%s] ServerRecv: %s", host, fmtTime(r.Timestamps.ServerRecv))
26+
t.Logf("[%s] ServerXmit: %s", host, fmtTime(r.Timestamps.ServerXmit))
27+
t.Logf("[%s] ClientRecv: %s", host, fmtTime(r.Timestamps.ClientRecv))
2528
t.Logf("[%s] Stratum: %d", host, r.Stratum)
2629
t.Logf("[%s] Leap: %s", host, fmtLeapIndicator(r.Leap))
2730
t.Logf("[%s] Flags: %s", host, fmtResponseFlags(r.Flags))

ntp5.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ func queryV5(conn net.Conn, opt *QueryOptions) (*Response, error) {
276276
// Prepare the response struct.
277277
r := &Response{
278278
ClockOffset: offset(t1, t2, t3, t4),
279-
Time: serverXmitTime,
280279
RTT: rtt(t1, t2, t3, t4),
280+
Timestamps: ProtocolTimestamps{t1, t2, t3, t4},
281281
Precision: toInterval(m.Precision),
282282
Version: 5,
283283
Stratum: m.Stratum,
@@ -290,6 +290,7 @@ func queryV5(conn net.Conn, opt *QueryOptions) (*Response, error) {
290290
Poll: toInterval(m.Poll),
291291
Flags: 0,
292292
ServerCookie: m.ServerCookie,
293+
Time: serverXmitTime,
293294
}
294295

295296
// Calculate root distance.

ntp5_test.go

Lines changed: 67 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ import (
1919
)
2020

2121
func logResponseV5(t *testing.T, r *Response) {
22-
now := time.Now()
22+
now := time.Now().Local()
2323
t.Logf("[%s] Version: %d", host, r.Version)
2424
t.Logf("[%s] ClockOffset: %s", host, r.ClockOffset)
2525
t.Logf("[%s] RTT: %s", host, r.RTT)
2626
t.Logf("[%s] Correction: %s", host, fmtCorrection(r.Correction))
2727
t.Logf("[%s] SystemTime: %s", host, fmtTime(now))
2828
t.Logf("[%s] ~TrueTime: %s", host, fmtTime(now.Add(r.ClockOffset)))
29-
t.Logf("[%s] XmitTime: %s", host, fmtTime(r.Time))
29+
t.Logf("[%s] ClientXmit: %s", host, fmtTime(r.Timestamps.ClientXmit))
30+
t.Logf("[%s] ServerRecv: %s", host, fmtTime(r.Timestamps.ServerRecv))
31+
t.Logf("[%s] ServerXmit: %s", host, fmtTime(r.Timestamps.ServerXmit))
32+
t.Logf("[%s] ClientRecv: %s", host, fmtTime(r.Timestamps.ClientRecv))
3033
t.Logf("[%s] Stratum: %d", host, r.Stratum)
3134
t.Logf("[%s] Leap: %s", host, fmtLeapIndicator(r.Leap))
3235
t.Logf("[%s] Flags: %s", host, fmtResponseFlags(r.Flags))
@@ -352,29 +355,6 @@ func TestOfflineV5DraftIDExtension(t *testing.T) {
352355
assert.True(t, bytes.Contains(data[4:], []byte(draftID)), "Extension should contain draft ID string")
353356
}
354357

355-
func TestOfflineV5BuildRequest(t *testing.T) {
356-
opt := &QueryOptions{
357-
Version: 5,
358-
Timescale: TimescaleUTC,
359-
}
360-
361-
clientCookie := uint64(0x1234567890abcdef)
362-
buf, err := buildV5Request(opt, clientCookie)
363-
require.NoError(t, err)
364-
require.NotNil(t, buf)
365-
require.NotZero(t, clientCookie)
366-
367-
data := buf.Bytes()
368-
require.GreaterOrEqual(t, len(data), msgSize)
369-
370-
m, err := parseV5Response(data)
371-
require.NoError(t, err)
372-
assert.Equal(t, 5, m.getVersion())
373-
assert.Equal(t, requestMode, m.getMode())
374-
assert.Equal(t, clientCookie, m.ClientCookie)
375-
assert.Equal(t, uint8(TimescaleUTC), m.Timescale)
376-
}
377-
378358
func TestOfflineV5ParseMsg(t *testing.T) {
379359
m := &messageV5{
380360
Stratum: 2,
@@ -385,13 +365,13 @@ func TestOfflineV5ParseMsg(t *testing.T) {
385365
Timescale: 0,
386366
Era: 0,
387367
Flags: flagSynchronized,
388-
ServerCookie: 0x1234567890ABCDEF,
368+
ServerCookie: 0x1234567890abcdef,
389369
ClientCookie: 0xFEDCBA0987654321,
390370
ReceiveTime: 1 << 32, // 1 second
391371
TransmitTime: 2 << 32, // 2 seconds
392372
}
393373
m.setVersion(5)
394-
m.setMode(requestMode)
374+
m.setMode(responseMode)
395375
m.setLeap(LeapNoWarning)
396376

397377
buf := new(bytes.Buffer)
@@ -412,7 +392,7 @@ func TestOfflineV5ParseMsg(t *testing.T) {
412392
parsed, err := parseV5Response(buf.Bytes())
413393
require.NoError(t, err)
414394
assert.Equal(t, 5, parsed.getVersion())
415-
assert.Equal(t, requestMode, parsed.getMode())
395+
assert.Equal(t, responseMode, parsed.getMode())
416396
assert.Equal(t, LeapNoWarning, parsed.getLeap())
417397
assert.Equal(t, uint8(2), parsed.Stratum)
418398
assert.Equal(t, int8(6), parsed.Poll)
@@ -424,35 +404,70 @@ func TestOfflineV5ParseMsg(t *testing.T) {
424404
assert.True(t, parsed.Flags&flagSynchronized != 0)
425405
}
426406

427-
// mockV5Server creates a mock connection that echoes the client cookie.
428-
type mockV5Server struct {
407+
func TestOfflineV5QueryMock(t *testing.T) {
408+
conn := &mockV5Conn{
409+
stratum: 2,
410+
serverCookie: 0x1234567890abcdef,
411+
}
412+
413+
opt := QueryOptions{
414+
Version: 5,
415+
Timescale: TimescaleTAI,
416+
GetSystemTime: time.Now,
417+
Dialer: func(network, address string) (net.Conn, error) {
418+
return conn, nil
419+
},
420+
}
421+
422+
r, err := QueryWithOptions("mock.example.com", opt)
423+
require.NoError(t, err)
424+
require.NotNil(t, r)
425+
assert.Equal(t, 5, r.Version)
426+
assert.Equal(t, conn.stratum, r.Stratum)
427+
assert.Equal(t, LeapNoWarning, r.Leap)
428+
assert.True(t, r.Flags&flagSynchronized != 0)
429+
assert.False(t, r.Flags&flagInterleaved != 0)
430+
assert.False(t, r.Flags&flagAuthNAK != 0)
431+
assert.Equal(t, TimescaleTAI, r.Timescale)
432+
assert.Equal(t, uint8(0), r.Era)
433+
assert.Equal(t, conn.serverCookie, r.ServerCookie)
434+
assert.True(t, conn.closed)
435+
}
436+
437+
// mockV5Conn is a mock connection used to simulate a simple NTP exchange.
438+
type mockV5Conn struct {
429439
stratum uint8
430440
serverCookie uint64
431441
request []byte
432442
closed bool
433443
}
434444

435-
func (s *mockV5Server) Read(b []byte) (n int, err error) {
436-
if len(s.request) < msgSize {
445+
func (c *mockV5Conn) Read(b []byte) (n int, err error) {
446+
if c.closed {
447+
return 0, fmt.Errorf("read from closed connection")
448+
}
449+
if len(c.request) < msgSize {
437450
return 0, ErrInvalidTime
438451
}
439-
requestMsg, _ := parseV5Response(s.request)
440452

441453
now := time.Now()
442454
serverRecv := toTimestamp(now)
443-
serverXmit := toTimestamp(now.Add(1 * time.Millisecond))
455+
serverXmit := toTimestamp(now.Add(10 * time.Millisecond))
456+
457+
timescale := c.request[12]
458+
clientCookie := binary.BigEndian.Uint64(c.request[24:32])
444459

445460
responseMsg := &messageV5{
446-
Stratum: s.stratum,
461+
Stratum: c.stratum,
447462
Poll: 6,
448463
Precision: -20,
449464
RootDelay: toTimeShortV5(50 * time.Millisecond),
450465
RootDisp: toTimeShortV5(10 * time.Millisecond),
451-
Timescale: 0,
466+
Timescale: timescale,
452467
Era: 0,
453468
Flags: flagSynchronized,
454-
ServerCookie: s.serverCookie,
455-
ClientCookie: requestMsg.ClientCookie, // Echo the client cookie
469+
ServerCookie: c.serverCookie,
470+
ClientCookie: clientCookie,
456471
ReceiveTime: serverRecv,
457472
TransmitTime: serverXmit,
458473
}
@@ -479,45 +494,23 @@ func (s *mockV5Server) Read(b []byte) (n int, err error) {
479494
return buf.Len(), nil
480495
}
481496

482-
func (s *mockV5Server) Write(b []byte) (n int, err error) {
483-
s.request = make([]byte, len(b))
484-
copy(s.request, b)
497+
func (c *mockV5Conn) Write(b []byte) (n int, err error) {
498+
if c.closed {
499+
return 0, fmt.Errorf("write to closed connection")
500+
}
501+
502+
c.request = make([]byte, len(b))
503+
copy(c.request, b)
485504
return len(b), nil
486505
}
487506

488-
func (s *mockV5Server) Close() error {
489-
s.closed = true
507+
func (c *mockV5Conn) Close() error {
508+
c.closed = true
490509
return nil
491510
}
492511

493-
func (s *mockV5Server) LocalAddr() net.Addr { return nil }
494-
func (s *mockV5Server) RemoteAddr() net.Addr { return nil }
495-
func (s *mockV5Server) SetDeadline(t time.Time) error { return nil }
496-
func (s *mockV5Server) SetReadDeadline(t time.Time) error { return nil }
497-
func (s *mockV5Server) SetWriteDeadline(t time.Time) error { return nil }
498-
499-
func TestOfflineV5QueryMock(t *testing.T) {
500-
mockServer := &mockV5Server{
501-
stratum: 2,
502-
serverCookie: 0x1234567890ABCDEF,
503-
}
504-
505-
opt := &QueryOptions{
506-
Version: 5,
507-
Timeout: 5 * time.Second,
508-
GetSystemTime: time.Now,
509-
}
510-
511-
resp, err := queryV5(mockServer, opt)
512-
require.NoError(t, err)
513-
require.NotNil(t, resp)
514-
assert.Equal(t, 5, resp.Version)
515-
assert.Equal(t, uint8(2), resp.Stratum)
516-
assert.Equal(t, LeapNoWarning, resp.Leap)
517-
assert.True(t, resp.Flags&flagSynchronized != 0)
518-
assert.False(t, resp.Flags&flagInterleaved != 0)
519-
assert.Equal(t, TimescaleUTC, resp.Timescale)
520-
assert.Equal(t, uint8(0), resp.Era)
521-
assert.Equal(t, uint64(0x1234567890ABCDEF), resp.ServerCookie)
522-
assert.False(t, mockServer.closed)
523-
}
512+
func (c *mockV5Conn) LocalAddr() net.Addr { return nil }
513+
func (c *mockV5Conn) RemoteAddr() net.Addr { return nil }
514+
func (c *mockV5Conn) SetDeadline(t time.Time) error { return nil }
515+
func (c *mockV5Conn) SetReadDeadline(t time.Time) error { return nil }
516+
func (c *mockV5Conn) SetWriteDeadline(t time.Time) error { return nil }

0 commit comments

Comments
 (0)