From 0a52d188efc401bdcf6de375319c0b8b31e2c8d4 Mon Sep 17 00:00:00 2001 From: Brett Vickers Date: Sun, 2 Jun 2024 19:42:50 -0700 Subject: [PATCH] Response time fields robust against NTP rollover 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. --- ntp.go | 33 +++++++++++++++++++++------------ ntp_test.go | 46 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/ntp.go b/ntp.go index 197e19f..f6b8f9b 100644 --- a/ntp.go +++ b/ntp.go @@ -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 @@ -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) @@ -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 @@ -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 diff --git a/ntp_test.go b/ntp_test.go index bff159c..a929688 100644 --- a/ntp_test.go +++ b/ntp_test.go @@ -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"}, @@ -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) @@ -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