Skip to content

Commit

Permalink
Response time fields robust against NTP rollover
Browse files Browse the repository at this point in the history
In 2036, the NTP timestamp clock will roll over. This change ensures
that the Time and ReferenceTime values reported in the Response
struct will report accurate time values before and after the rollover.
  • Loading branch information
beevik committed Jun 3, 2024
1 parent 59e2d71 commit 0a52d18
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 17 deletions.
33 changes: 21 additions & 12 deletions ntp.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ const (

// Internal variables
var (
ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
ntpEra0 = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
ntpEra1 = time.Date(2036, 2, 7, 6, 28, 16, 0, time.UTC)
)

type mode uint8
Expand Down Expand Up @@ -107,13 +108,21 @@ func (t ntpTime) Duration() time.Duration {
// Time interprets the fixed-point ntpTime as an absolute time and returns
// the corresponding time.Time value.
func (t ntpTime) Time() time.Time {
return ntpEpoch.Add(t.Duration())
// Assume NTP era 1 (year 2036+) if the raw timestamp suggests a year
// before 1970. Otherwise assume NTP era 0. This allows the function to
// report an accurate time value both before and after the 0-to-1 era
// rollover.
const t1970 = 0x83aa7e8000000000
if uint64(t) < t1970 {
return ntpEra1.Add(t.Duration())
}
return ntpEra0.Add(t.Duration())
}

// toNtpTime converts the time.Time value t into its 64-bit fixed-point
// ntpTime representation.
func toNtpTime(t time.Time) ntpTime {
nsec := uint64(t.Sub(ntpEpoch))
nsec := uint64(t.Sub(ntpEra0))
sec := nsec / nanoPerSec
nsec = uint64(nsec-sec*nanoPerSec) << 32
frac := uint64(nsec / nanoPerSec)
Expand Down Expand Up @@ -249,16 +258,17 @@ type QueryOptions struct {
// A Response contains time data, some of which is returned by the NTP server
// and some of which is calculated by this client.
type Response struct {
// Time is the transmit time reported by the server just before it
// responded to the client's NTP query. You should not use this value
// for time synchronization purposes. Use the ClockOffset instead.
Time time.Time

// ClockOffset is the estimated offset of the local system clock relative
// to the server's clock. Add this value to subsequent local system time
// measurements in order to obtain a more accurate time.
// to the server's clock. Add this value to subsequent local system clock
// times in order to obtain a time that is synchronized to the server's
// clock.
ClockOffset time.Duration

// Time is the time the server transmitted this response, measured using
// its own clock. You should not use this value for time synchronization
// purposes. Add ClockOffset to your system clock instead.
Time time.Time

// RTT is the measured round-trip-time delay estimate between the client
// and the server.
RTT time.Duration
Expand All @@ -285,8 +295,7 @@ type Response struct {
// code".
ReferenceID uint32

// ReferenceTime is the time when the server's system clock was last
// set or corrected.
// ReferenceTime is the time the server last updated its local clock.
ReferenceTime time.Time

// RootDelay is the server's estimated aggregate round-trip-time delay to
Expand Down
46 changes: 41 additions & 5 deletions ntp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,10 @@ func TestOfflineOffsetCalculationNegative(t *testing.T) {
assert.Equal(t, expectedOffset, offset)
}

func TestOfflineOffsetCalculationNegativeBig(t *testing.T) {
func TestOfflineOffsetRollover(t *testing.T) {
cases := []struct {
ClientTime string
ServerTime string
clientTime string
serverTime string
}{
// both timestamps in NTP era 0 (with large difference)
{"1970-01-01 00:00:00", "2024-05-30 00:00:00"},
Expand All @@ -409,8 +409,8 @@ func TestOfflineOffsetCalculationNegativeBig(t *testing.T) {
timeFormat := "2006-01-02 15:04:05"

for _, c := range cases {
clientTime, _ := time.Parse(timeFormat, c.ClientTime)
serverTime, _ := time.Parse(timeFormat, c.ServerTime)
clientTime, _ := time.Parse(timeFormat, c.clientTime)
serverTime, _ := time.Parse(timeFormat, c.serverTime)

org := toNtpTime(clientTime)
rec := toNtpTime(serverTime)
Expand All @@ -423,6 +423,42 @@ func TestOfflineOffsetCalculationNegativeBig(t *testing.T) {
}
}

func TestOfflineTimeRollover(t *testing.T) {
cases := []struct {
timestamp ntpTime
time string
}{
{0x0000000000000000, "2036-02-07 06:28:16"},
{0x0000000100000000, "2036-02-07 06:28:17"},
{0x1000000000000000, "2044-08-10 03:52:32"},
{0x2000000000000000, "2053-02-11 01:16:48"},
{0x3000000000000000, "2061-08-14 22:41:04"},
{0x4000000000000000, "2070-02-15 20:05:20"},
{0x5000000000000000, "2078-08-19 17:29:36"},
{0x6000000000000000, "2087-02-20 14:53:52"},
{0x7000000000000000, "2095-08-24 12:18:08"},
{0x8000000000000000, "2104-02-26 09:42:24"},
{0x83aa7e7000000000, "2106-02-07 06:28:00"},
{0x83aa7e8000000000, "1970-01-01 00:00:00"}, // <- ntpTime.Time() wrap
{0x9000000000000000, "1976-07-23 00:38:24"},
{0xa000000000000000, "1985-01-23 22:02:40"},
{0xb000000000000000, "1993-07-27 19:26:56"},
{0xc000000000000000, "2002-01-28 16:51:12"},
{0xd000000000000000, "2010-08-01 14:15:28"},
{0xe000000000000000, "2019-02-02 11:39:44"},
{0xf000000000000000, "2027-08-06 09:04:00"},
{0xffffffff00000000, "2036-02-07 06:28:15"},
}

timeFormat := "2006-01-02 15:04:05"

for _, c := range cases {
tm, _ := time.Parse(timeFormat, c.time)
assert.Equal(t, tm, c.timestamp.Time())
assert.Equal(t, c.timestamp, toNtpTime(tm))
}
}

func TestOfflineReferenceString(t *testing.T) {
cases := []struct {
Stratum byte
Expand Down

0 comments on commit 0a52d18

Please sign in to comment.