Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fc92841

Browse files
committed
Response now includes client/server timestamps
Added a Timestamps field to the Response struct. The field includes the four timestamps used by NTP client calculations: client transmit time, server receive time, server transmit time, and client receive time. Deprecated the "Time" field in Response, since it is now included in the new Timestamps field (as ServerXmit).
1 parent c934fff commit fc92841

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)