Repository: cf-natali/ntplib

Issue:
Very inaccurate client fails to request NTP data
When the client thinks the current time is after the year 2037, the library fails to make an NTP request.

This can be easily demonstrated using [the faketime library](https://packages.debian.org/stable/faketime):

```pytb
$ faketime '2037-01-01 00:00:00' python3 -c 'import ntplib; ntplib.NTPClient().request("0.pool.ntp.org")'
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/ntplib.py", line 170, in to_data
    packed = struct.pack(
struct.error: 'I' format requires 0 <= number <= 4294967295

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/local/lib/python3.10/site-packages/ntplib.py", line 313, in request
    s.sendto(query_packet.to_data(), sockaddr)
  File "/usr/local/lib/python3.10/site-packages/ntplib.py", line 189, in to_data
    raise NTPException("Invalid NTP packet fields.")
ntplib.NTPException: Invalid NTP packet fields.
```

Before the year 2036 the above succeeds as expected. Tested on the `python:3.10` docker image with ntplib `0.4.0`. I'm not sure if the problem is just due to the ~15 year inaccuracy, or this version of the library will stop working at some point in 2036.

Discussion:

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 9834bec..b9cdfd2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,9 @@
+version 0.X.X - 2022-XX-XX
+--------------------------
+- Add a new `NTPRolloverException` raised when system time is beyond NTPv3
+  rollover (https://en.wikipedia.org/wiki/Network_Time_Protocol#Timestamps)
+  Closes #18.
+
 version 0.4.0 - 2021-05-28
 --------------------------
 - flake8 conformance
diff --git a/ntplib.py b/ntplib.py
index b70d017..7cb839d 100644
--- a/ntplib.py
+++ b/ntplib.py
@@ -39,6 +39,12 @@ class NTPException(Exception):
     pass
 
 
+class NTPRolloverException(NTPException):
+    """Exception raised when the system time is beyond the NTPv3 rollover. """
+    # See https://en.wikipedia.org/wiki/Network_Time_Protocol#Timestamps
+    pass
+
+
 class NTP:
     """Helper class defining constants."""
 
@@ -392,6 +398,10 @@ def system_to_ntp_time(timestamp):
     Returns:
     corresponding NTP time
     """
+    if timestamp >= (NTP._NTP_EPOCH +
+                     datetime.timedelta(seconds=2 ** 32)).timestamp():
+        raise NTPRolloverException("Timestamp %s is beyond NTPv3 rollover" %
+                                   timestamp)
     return timestamp + NTP.NTP_DELTA
 
 


Test patch:
diff --git a/test_ntplib.py b/test_ntplib.py
index b85c98a..a362ec8 100755
--- a/test_ntplib.py
+++ b/test_ntplib.py
@@ -1,6 +1,7 @@
 """Python NTP library tests."""
 
 
+import datetime
 import time
 import unittest
 
@@ -95,6 +96,22 @@ class TestNTPLib(unittest.TestCase):
         self.assertTrue(isinstance(ntplib.ref_id_to_text(info.ref_id,
                                                          info.stratum), str))
 
+    def test_rollover(self):
+        """ Test for rollover - see
+            https://en.wikipedia.org/wiki/Network_Time_Protocol#Timestamps.
+        """
+        ts = datetime.datetime(2022, 8, 5, 19, 8, 42).timestamp()
+        self.assertEqual(
+                ntplib.ntp_to_system_time(ntplib.system_to_ntp_time(ts)), ts)
+
+        ts = datetime.datetime(2036, 2, 7).timestamp()
+        self.assertEqual(
+                ntplib.ntp_to_system_time(ntplib.system_to_ntp_time(ts)), ts)
+
+        ts = datetime.datetime(2036, 2, 7, 12).timestamp()
+        self.assertRaises(ntplib.NTPRolloverException,
+                          ntplib.system_to_ntp_time, ts)
+
 
 if __name__ == "__main__":
     unittest.main()