]> git.ipfire.org Git - thirdparty/chrony.git/commitdiff
clientlog: separate NTP timestamps from IP addresses
authorMiroslav Lichvar <mlichvar@redhat.com>
Mon, 11 Oct 2021 11:27:35 +0000 (13:27 +0200)
committerMiroslav Lichvar <mlichvar@redhat.com>
Thu, 14 Oct 2021 14:42:20 +0000 (16:42 +0200)
Instead of keeping one pair of RX and TX timestamp for each address, add
a separate RX->TX map using an ordered circular buffer. Save the RX
timestamps as 64-bit integers and search them with a combined linear
interpolation and binary algorithm.

This enables the server to support multiple interleaved clients sharing
the same IP address (e.g. NAT) and it will allow other improvements to
be implemented later. A drawback is that a single broken client sending
interleaved requests at a high rate (without spoofing the source
address) can now prevent clients on other addresses from getting
interleaved responses.

The total number of saved timestamps does not change. It's still
determined by the clientloglimit directive. A new option may be added
later if needed. The whole buffer is allocated at once, but only on
first use to not waste memory on client-only configurations.

clientlog.c
clientlog.h
ntp_core.c
test/simulation/122-xleave
test/unit/clientlog.c

index cec2e8e4ec5ac0dfcd0fd8f6c420a02b450af2f0..09af04be98c2f2ed937eea7d558a260277a05e65 100644 (file)
@@ -55,8 +55,6 @@ typedef struct {
   int8_t rate[MAX_SERVICES];
   int8_t ntp_timeout_rate;
   uint8_t drop_flags;
-  NTP_int64 ntp_rx_ts;
-  NTP_int64 ntp_tx_ts;
 } Record;
 
 /* Hash table of records, there is a fixed number of records per slot */
@@ -124,6 +122,35 @@ static int limit_interval[MAX_SERVICES];
 /* Flag indicating whether facility is turned on or not */
 static int active;
 
+/* RX and TX timestamp saved for clients using interleaved mode */
+typedef struct {
+  uint64_t rx_ts;
+  uint32_t flags;
+  int32_t tx_ts_offset;
+} NtpTimestamps;
+
+/* Flags for NTP timestamps */
+#define NTPTS_DISABLED 1
+#define NTPTS_VALID_TX 2
+
+/* RX->TX map using a circular buffer with ordered timestamps */
+typedef struct {
+  ARR_Instance timestamps;
+  uint32_t first;
+  uint32_t size;
+  uint32_t max_size;
+  uint32_t cached_index;
+  uint64_t cached_rx_ts;
+} NtpTimestampMap;
+
+static NtpTimestampMap ntp_ts_map;
+
+/* Maximum interval of NTP timestamps in future after a backward step */
+#define NTPTS_FUTURE_LIMIT (1LL << 32) /* 1 second */
+
+/* Maximum number of timestamps moved in the array to insert a new timestamp */
+#define NTPTS_INSERT_LIMIT 64
+
 /* Global statistics */
 static uint32_t total_hits[MAX_SERVICES];
 static uint32_t total_drops[MAX_SERVICES];
@@ -229,8 +256,6 @@ get_record(IPAddr *ip)
     record->rate[i] = INVALID_RATE;
   record->ntp_timeout_rate = INVALID_RATE;
   record->drop_flags = 0;
-  UTI_ZeroNtp64(&record->ntp_rx_ts);
-  UTI_ZeroNtp64(&record->ntp_tx_ts);
 
   return record;
 }
@@ -359,7 +384,8 @@ CLG_Initialise(void)
   /* Calculate the maximum number of slots that can be allocated in the
      configured memory limit.  Take into account expanding of the hash
      table where two copies exist at the same time. */
-  max_slots = CNF_GetClientLogLimit() / (sizeof (Record) * SLOT_SIZE * 3 / 2);
+  max_slots = CNF_GetClientLogLimit() /
+              ((sizeof (Record) + sizeof (NtpTimestamps)) * SLOT_SIZE * 3 / 2);
   max_slots = CLAMP(MIN_SLOTS, max_slots, MAX_SLOTS);
   for (slots2 = 0; 1U << (slots2 + 1) <= max_slots; slots2++)
     ;
@@ -373,6 +399,13 @@ CLG_Initialise(void)
 
   UTI_GetRandomBytes(&ts_offset, sizeof (ts_offset));
   ts_offset %= NSEC_PER_SEC / (1U << TS_FRAC);
+
+  ntp_ts_map.timestamps = NULL;
+  ntp_ts_map.first = 0;
+  ntp_ts_map.size = 0;
+  ntp_ts_map.max_size = 1U << (slots2 + SLOT_BITS);
+  ntp_ts_map.cached_index = 0;
+  ntp_ts_map.cached_rx_ts = 0ULL;
 }
 
 /* ================================================== */
@@ -384,6 +417,8 @@ CLG_Finalise(void)
     return;
 
   ARR_DestroyInstance(records);
+  if (ntp_ts_map.timestamps)
+    ARR_DestroyInstance(ntp_ts_map.timestamps);
 }
 
 /* ================================================== */
@@ -598,22 +633,294 @@ CLG_LogAuthNtpRequest(void)
 
 /* ================================================== */
 
-void CLG_GetNtpTimestamps(int index, NTP_int64 **rx_ts, NTP_int64 **tx_ts)
+int
+CLG_GetNtpMinPoll(void)
 {
-  Record *record;
+  return limit_interval[CLG_NTP];
+}
 
-  record = ARR_GetElement(records, index);
+/* ================================================== */
+
+static NtpTimestamps *
+get_ntp_tss(uint32_t index)
+{
+  return ARR_GetElement(ntp_ts_map.timestamps,
+                        (ntp_ts_map.first + index) & (ntp_ts_map.max_size - 1));
+}
+
+/* ================================================== */
+
+static int
+find_ntp_rx_ts(uint64_t rx_ts, uint32_t *index)
+{
+  uint64_t rx_x, rx_lo, rx_hi, step;
+  uint32_t i, x, lo, hi;
+
+  if (ntp_ts_map.cached_rx_ts == rx_ts && rx_ts != 0ULL) {
+    *index = ntp_ts_map.cached_index;
+    return 1;
+  }
+
+  if (ntp_ts_map.size == 0) {
+    *index = 0;
+    return 0;
+  }
+
+  lo = 0;
+  hi = ntp_ts_map.size - 1;
+  rx_lo = get_ntp_tss(lo)->rx_ts;
+  rx_hi = get_ntp_tss(hi)->rx_ts;
+
+  /* Check for ts < lo before ts > hi to trim timestamps from "future" later
+     if both conditions are true to not break the order of the endpoints.
+     Compare timestamps by their difference to allow adjacent NTP eras. */
+  if ((int64_t)(rx_ts - rx_lo) < 0) {
+    *index = 0;
+    return 0;
+  } else if ((int64_t)(rx_ts - rx_hi) > 0) {
+    *index = ntp_ts_map.size;
+    return 0;
+  }
+
+  /* Perform a combined linear interpolation and binary search */
+
+  for (i = 0; ; i++) {
+    if (rx_ts == rx_hi) {
+      *index = ntp_ts_map.cached_index = hi;
+      ntp_ts_map.cached_rx_ts = rx_ts;
+      return 1;
+    } else if (rx_ts == rx_lo) {
+      *index = ntp_ts_map.cached_index = lo;
+      ntp_ts_map.cached_rx_ts = rx_ts;
+      return 1;
+    } else if (lo + 1 == hi) {
+      *index = hi;
+      return 0;
+    }
+
+    if (hi - lo > 3 && i % 2 == 0) {
+      step = (rx_hi - rx_lo) / (hi - lo);
+      if (step == 0)
+        step = 1;
+      x = lo + (rx_ts - rx_lo) / step;
+    } else {
+      x = lo + (hi - lo) / 2;
+    }
+
+    if (x <= lo)
+      x = lo + 1;
+    else if (x >= hi)
+      x = hi - 1;
+
+    rx_x = get_ntp_tss(x)->rx_ts;
+
+    if ((int64_t)(rx_x - rx_ts) <= 0) {
+      lo = x;
+      rx_lo = rx_x;
+    } else {
+      hi = x;
+      rx_hi = rx_x;
+    }
+  }
+}
+
+/* ================================================== */
+
+static uint64_t
+ntp64_to_int64(NTP_int64 *ts)
+{
+  return (uint64_t)ntohl(ts->hi) << 32 | ntohl(ts->lo);
+}
+
+/* ================================================== */
+
+static void
+int64_to_ntp64(uint64_t ts, NTP_int64 *ntp_ts)
+{
+  ntp_ts->hi = htonl(ts >> 32);
+  ntp_ts->lo = htonl(ts & ((1ULL << 32) - 1));
+}
+
+/* ================================================== */
+
+static uint32_t
+push_ntp_tss(uint32_t index)
+{
+  if (ntp_ts_map.size < ntp_ts_map.max_size) {
+    ntp_ts_map.size++;
+  } else {
+    ntp_ts_map.first = (ntp_ts_map.first + 1) % (ntp_ts_map.max_size);
+    if (index > 0)
+      index--;
+  }
 
-  *rx_ts = &record->ntp_rx_ts;
-  *tx_ts = &record->ntp_tx_ts;
+  return index;
+}
+
+/* ================================================== */
+
+static void
+set_ntp_tx_offset(NtpTimestamps *tss, NTP_int64 *rx_ts, struct timespec *tx_ts)
+{
+  struct timespec ts;
+
+  if (!tx_ts) {
+    tss->flags &= ~NTPTS_VALID_TX;
+    return;
+  }
+
+  UTI_Ntp64ToTimespec(rx_ts, &ts);
+  UTI_DiffTimespecs(&ts, tx_ts, &ts);
+
+  if (ts.tv_sec < -2 || ts.tv_sec > 1) {
+    tss->flags &= ~NTPTS_VALID_TX;
+    return;
+  }
+
+  tss->tx_ts_offset = (int32_t)ts.tv_nsec + (int32_t)ts.tv_sec * (int32_t)NSEC_PER_SEC;
+  tss->flags |= NTPTS_VALID_TX;
+}
+
+/* ================================================== */
+
+static void
+get_ntp_tx(NtpTimestamps *tss, struct timespec *tx_ts)
+{
+  int32_t offset = tss->tx_ts_offset;
+  NTP_int64 ntp_ts;
+
+  if (tss->flags & NTPTS_VALID_TX) {
+    int64_to_ntp64(tss->rx_ts, &ntp_ts);
+    UTI_Ntp64ToTimespec(&ntp_ts, tx_ts);
+    if (offset >= (int32_t)NSEC_PER_SEC) {
+      offset -= NSEC_PER_SEC;
+      tx_ts->tv_sec++;
+    }
+    tx_ts->tv_nsec += offset;
+    UTI_NormaliseTimespec(tx_ts);
+  } else {
+    UTI_ZeroTimespec(tx_ts);
+  }
+}
+
+/* ================================================== */
+
+void
+CLG_SaveNtpTimestamps(NTP_int64 *rx_ts, struct timespec *tx_ts)
+{
+  NtpTimestamps *tss;
+  uint32_t i, index;
+  uint64_t rx;
+
+  if (!active)
+    return;
+
+  /* Allocate the array on first use */
+  if (!ntp_ts_map.timestamps) {
+    ntp_ts_map.timestamps = ARR_CreateInstance(sizeof (NtpTimestamps));
+    ARR_SetSize(ntp_ts_map.timestamps, ntp_ts_map.max_size);
+  }
+
+  rx = ntp64_to_int64(rx_ts);
+
+  if (rx == 0ULL)
+    return;
+
+  /* Disable the RX timestamp if it already exists to avoid responding
+     with a wrong TX timestamp */
+  if (find_ntp_rx_ts(rx, &index)) {
+    get_ntp_tss(index)->flags |= NTPTS_DISABLED;
+    return;
+  }
+
+  assert(index <= ntp_ts_map.size);
+
+  if (index == ntp_ts_map.size) {
+    /* Increase the size or drop the oldest timestamp to make room for
+       the new timestamp */
+    index = push_ntp_tss(index);
+  } else {
+    /* Trim timestamps in distant future after backward step */
+    while (index < ntp_ts_map.size &&
+           get_ntp_tss(ntp_ts_map.size - 1)->rx_ts - rx > NTPTS_FUTURE_LIMIT)
+      ntp_ts_map.size--;
+
+    /* Insert the timestamp if it is close to the latest timestamp.
+       Otherwise, replace the closest older or the oldest timestamp. */
+    if (index + NTPTS_INSERT_LIMIT >= ntp_ts_map.size) {
+      index = push_ntp_tss(index);
+      for (i = ntp_ts_map.size - 1; i > index; i--)
+        *get_ntp_tss(i) = *get_ntp_tss(i - 1);
+    } else {
+      if (index > 0)
+        index--;
+    }
+  }
+
+  ntp_ts_map.cached_index = index;
+  ntp_ts_map.cached_rx_ts = rx;
+
+  tss = get_ntp_tss(index);
+  tss->rx_ts = rx;
+  tss->flags = 0;
+  set_ntp_tx_offset(tss, rx_ts, tx_ts);
+
+  DEBUG_LOG("Saved RX+TX index=%"PRIu32" first=%"PRIu32" size=%"PRIu32,
+            index, ntp_ts_map.first, ntp_ts_map.size);
+}
+
+/* ================================================== */
+
+void
+CLG_UpdateNtpTxTimestamp(NTP_int64 *rx_ts, struct timespec *tx_ts)
+{
+  uint32_t index;
+
+  if (!ntp_ts_map.timestamps)
+    return;
+
+  if (!find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
+    return;
+
+  set_ntp_tx_offset(get_ntp_tss(index), rx_ts, tx_ts);
 }
 
 /* ================================================== */
 
 int
-CLG_GetNtpMinPoll(void)
+CLG_GetNtpTxTimestamp(NTP_int64 *rx_ts, struct timespec *tx_ts)
 {
-  return limit_interval[CLG_NTP];
+  NtpTimestamps *tss;
+  uint32_t index;
+
+  if (!ntp_ts_map.timestamps)
+    return 0;
+
+  if (!find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
+    return 0;
+
+  tss = get_ntp_tss(index);
+
+  if (tss->flags & NTPTS_DISABLED)
+    return 0;
+
+  get_ntp_tx(tss, tx_ts);
+
+  return 1;
+}
+
+/* ================================================== */
+
+void
+CLG_DisableNtpTimestamps(NTP_int64 *rx_ts)
+{
+  uint32_t index;
+
+  if (!ntp_ts_map.timestamps)
+    return;
+
+  if (find_ntp_rx_ts(ntp64_to_int64(rx_ts), &index))
+    get_ntp_tss(index)->flags |= NTPTS_DISABLED;
 }
 
 /* ================================================== */
index 6349646bb8d5bb1681da38e82da0cab821a96abc..a4001025126bcb35bad9072f30895d564f5e08bb 100644 (file)
@@ -43,9 +43,14 @@ extern int CLG_GetClientIndex(IPAddr *client);
 extern int CLG_LogServiceAccess(CLG_Service service, IPAddr *client, struct timespec *now);
 extern int CLG_LimitServiceRate(CLG_Service service, int index);
 extern void CLG_LogAuthNtpRequest(void);
-extern void CLG_GetNtpTimestamps(int index, NTP_int64 **rx_ts, NTP_int64 **tx_ts);
 extern int CLG_GetNtpMinPoll(void);
 
+/* Functions to save and retrieve timestamps for server interleaved mode */
+extern void CLG_SaveNtpTimestamps(NTP_int64 *rx_ts, struct timespec *tx_ts);
+extern void CLG_UpdateNtpTxTimestamp(NTP_int64 *rx_ts, struct timespec *tx_ts);
+extern int CLG_GetNtpTxTimestamp(NTP_int64 *rx_ts, struct timespec *tx_ts);
+extern void CLG_DisableNtpTimestamps(NTP_int64 *rx_ts);
+
 /* And some reporting functions, for use by chronyc. */
 
 extern int CLG_GetNumberOfIndices(void);
index 2fb51837cc3296eb574ae26a625b2f36318db485..d113d226a050c6ca51f3143940d15eca595de5e0 100644 (file)
@@ -2042,8 +2042,8 @@ NCR_ProcessRxUnknown(NTP_Remote_Address *remote_addr, NTP_Local_Address *local_a
 {
   NTP_PacketInfo info;
   NTP_Mode my_mode;
-  NTP_int64 *local_ntp_rx, *local_ntp_tx;
   NTP_Local_Timestamp local_tx, *tx_ts;
+  NTP_int64 ntp_rx, *local_ntp_rx;
   int log_index, interleaved, poll, version;
   uint32_t kod;
 
@@ -2106,7 +2106,7 @@ NCR_ProcessRxUnknown(NTP_Remote_Address *remote_addr, NTP_Local_Address *local_a
     CLG_LogAuthNtpRequest();
   }
 
-  local_ntp_rx = local_ntp_tx = NULL;
+  local_ntp_rx = NULL;
   tx_ts = NULL;
   interleaved = 0;
 
@@ -2115,18 +2115,15 @@ NCR_ProcessRxUnknown(NTP_Remote_Address *remote_addr, NTP_Local_Address *local_a
      in the interleaved mode.  This means the third reply to a new client is
      the earliest one that can be interleaved.  We don't want to waste time
      on clients that are not using the interleaved mode. */
-  if (kod == 0 && log_index >= 0) {
-    CLG_GetNtpTimestamps(log_index, &local_ntp_rx, &local_ntp_tx);
-    interleaved = !UTI_IsZeroNtp64(local_ntp_rx) &&
-                  !UTI_CompareNtp64(&message->originate_ts, local_ntp_rx) &&
-                  UTI_CompareNtp64(&message->receive_ts, &message->transmit_ts);
+  if (kod == 0 &&
+      UTI_CompareNtp64(&message->receive_ts, &message->transmit_ts) != 0) {
+    ntp_rx = message->originate_ts;
+    local_ntp_rx = &ntp_rx;
+    interleaved = CLG_GetNtpTxTimestamp(&ntp_rx, &local_tx.ts);
 
     if (interleaved) {
-      UTI_Ntp64ToTimespec(local_ntp_tx, &local_tx.ts);
       tx_ts = &local_tx;
-    } else {
-      UTI_ZeroNtp64(local_ntp_tx);
-      local_ntp_tx = NULL;
+      CLG_DisableNtpTimestamps(&ntp_rx);
     }
   }
 
@@ -2144,9 +2141,8 @@ NCR_ProcessRxUnknown(NTP_Remote_Address *remote_addr, NTP_Local_Address *local_a
                   rx_ts, tx_ts, local_ntp_rx, NULL, remote_addr, local_addr,
                   message, &info);
 
-  /* Save the transmit timestamp */
-  if (tx_ts)
-    UTI_TimespecToNtp64(&tx_ts->ts, local_ntp_tx, NULL);
+  if (local_ntp_rx)
+    CLG_SaveNtpTimestamps(local_ntp_rx, tx_ts ? &tx_ts->ts : NULL);
 }
 
 /* ================================================== */
@@ -2208,10 +2204,9 @@ void
 NCR_ProcessTxUnknown(NTP_Remote_Address *remote_addr, NTP_Local_Address *local_addr,
                      NTP_Local_Timestamp *tx_ts, NTP_Packet *message, int length)
 {
-  NTP_int64 *local_ntp_rx, *local_ntp_tx;
   NTP_Local_Timestamp local_tx;
+  NTP_int64 *local_ntp_rx;
   NTP_PacketInfo info;
-  int log_index;
 
   if (!parse_packet(message, length, &info))
     return;
@@ -2219,18 +2214,17 @@ NCR_ProcessTxUnknown(NTP_Remote_Address *remote_addr, NTP_Local_Address *local_a
   if (info.mode == MODE_BROADCAST)
     return;
 
-  log_index = CLG_GetClientIndex(&remote_addr->ip_addr);
-  if (log_index < 0)
-    return;
-
   if (SMT_IsEnabled() && info.mode == MODE_SERVER)
     UTI_AddDoubleToTimespec(&tx_ts->ts, SMT_GetOffset(&tx_ts->ts), &tx_ts->ts);
 
-  CLG_GetNtpTimestamps(log_index, &local_ntp_rx, &local_ntp_tx);
+  local_ntp_rx = &message->receive_ts;
+
+  if (!CLG_GetNtpTxTimestamp(local_ntp_rx, &local_tx.ts))
+    return;
 
-  UTI_Ntp64ToTimespec(local_ntp_tx, &local_tx.ts);
   update_tx_timestamp(&local_tx, tx_ts, local_ntp_rx, NULL, message);
-  UTI_TimespecToNtp64(&local_tx.ts, local_ntp_tx, NULL);
+
+  CLG_UpdateNtpTxTimestamp(local_ntp_rx, &local_tx.ts);
 }
 
 /* ================================================== */
index f137f19701efe8e411ef4a06d3d86ee580470ee7..5cb603dd7ed223784de7fc323136e34239d77e41 100755 (executable)
@@ -8,6 +8,18 @@ client_conf="
 logdir tmp
 log rawmeasurements"
 
+server_conf="noclientlog"
+
+run_test || test_fail
+check_chronyd_exit || test_fail
+check_source_selection || test_fail
+check_sync || test_fail
+
+check_file_messages "111 111 1111.* 4I [DKH] [DKH]\$" 0 0 measurements.log || test_fail
+rm -f tmp/measurements.log
+
+server_conf=""
+
 run_test || test_fail
 check_chronyd_exit || test_fail
 check_source_selection || test_fail
index 850cedf0d1e8b2e6ccf8f27d664a31e9a84e4d62..9d1066c58a62dbf9c7fb9bd8fd6053ac95669da3 100644 (file)
 
 #include <clientlog.c>
 
+static uint64_t
+get_random64(void)
+{
+  return ((uint64_t)random() << 40) ^ ((uint64_t)random() << 20) ^ random();
+}
+
 void
 test_unit(void)
 {
-  int i, j, index;
+  uint64_t ts64, prev_first_ts64, prev_last_ts64, max_step;
+  uint32_t index2, prev_first, prev_size;
+  struct timespec ts, ts2;
+  int i, j, k, index, shift;
   CLG_Service s;
-  struct timespec ts;
+  NTP_int64 ntp_ts;
   IPAddr ip;
   char conf[][100] = {
-    "clientloglimit 10000",
+    "clientloglimit 20000",
     "ratelimit interval 3 burst 4 leak 3",
     "cmdratelimit interval 3 burst 4 leak 3",
     "ntsratelimit interval 6 burst 8 leak 3",
@@ -67,7 +76,7 @@ test_unit(void)
   }
 
   DEBUG_LOG("records %u", ARR_GetSize(records));
-  TEST_CHECK(ARR_GetSize(records) == 64);
+  TEST_CHECK(ARR_GetSize(records) == 128);
 
   s = CLG_NTP;
 
@@ -82,6 +91,158 @@ test_unit(void)
   DEBUG_LOG("requests %d responses %d", i, j);
   TEST_CHECK(j * 4 < i && j * 6 > i);
 
+  TEST_CHECK(!ntp_ts_map.timestamps);
+
+  UTI_ZeroNtp64(&ntp_ts);
+  CLG_SaveNtpTimestamps(&ntp_ts, NULL);
+  TEST_CHECK(ntp_ts_map.timestamps);
+  TEST_CHECK(ntp_ts_map.first == 0);
+  TEST_CHECK(ntp_ts_map.size == 0);
+  TEST_CHECK(ntp_ts_map.max_size == 128);
+  TEST_CHECK(ARR_GetSize(ntp_ts_map.timestamps) == ntp_ts_map.max_size);
+
+  TEST_CHECK(ntp_ts_map.max_size > NTPTS_INSERT_LIMIT);
+
+  for (i = 0; i < 200; i++) {
+    DEBUG_LOG("iteration %d", i);
+
+    max_step = (1ULL << (i % 50));
+    ts64 = 0ULL - 100 * max_step;
+
+    ntp_ts_map.first = i % ntp_ts_map.max_size;
+    ntp_ts_map.size = 0;
+    ntp_ts_map.cached_rx_ts = 0ULL;
+
+    for (j = 0; j < 500; j++) {
+      do {
+        ts64 += get_random64() % max_step + 1;
+      } while (ts64 == 0ULL);
+
+      int64_to_ntp64(ts64, &ntp_ts);
+
+      if (random() % 10) {
+        UTI_Ntp64ToTimespec(&ntp_ts, &ts);
+        UTI_AddDoubleToTimespec(&ts, TST_GetRandomDouble(-1.999, 1.999), &ts);
+      } else {
+        UTI_ZeroTimespec(&ts);
+      }
+
+      CLG_SaveNtpTimestamps(&ntp_ts,
+                            UTI_IsZeroTimespec(&ts) ? (random() % 2 ? &ts : NULL) : &ts);
+
+      if (j < ntp_ts_map.max_size) {
+        TEST_CHECK(ntp_ts_map.size == j + 1);
+        TEST_CHECK(ntp_ts_map.first == i % ntp_ts_map.max_size);
+      } else {
+        TEST_CHECK(ntp_ts_map.size == ntp_ts_map.max_size);
+        TEST_CHECK(ntp_ts_map.first == (i + j + ntp_ts_map.size + 1) % ntp_ts_map.max_size);
+      }
+      TEST_CHECK(CLG_GetNtpTxTimestamp(&ntp_ts, &ts2));
+      TEST_CHECK(UTI_CompareTimespecs(&ts, &ts2) == 0);
+
+      for (k = random() % 4; k > 0; k--) {
+        int64_to_ntp64(get_ntp_tss(random() % ntp_ts_map.size)->rx_ts, &ntp_ts);
+        if (random() % 2)
+          TEST_CHECK(CLG_GetNtpTxTimestamp(&ntp_ts, &ts));
+
+        UTI_Ntp64ToTimespec(&ntp_ts, &ts);
+        UTI_AddDoubleToTimespec(&ts, TST_GetRandomDouble(-1.999, 1.999), &ts);
+        CLG_UpdateNtpTxTimestamp(&ntp_ts, &ts);
+
+        TEST_CHECK(CLG_GetNtpTxTimestamp(&ntp_ts, &ts2));
+        TEST_CHECK(UTI_CompareTimespecs(&ts, &ts2) == 0);
+
+        if (ntp_ts_map.size > 1) {
+          index = random() % (ntp_ts_map.size - 1);
+          if (get_ntp_tss(index)->rx_ts + 1 != get_ntp_tss(index + 1)->rx_ts) {
+            int64_to_ntp64(get_ntp_tss(index)->rx_ts + 1, &ntp_ts);
+            TEST_CHECK(!CLG_GetNtpTxTimestamp(&ntp_ts, &ts));
+            int64_to_ntp64(get_ntp_tss(index + 1)->rx_ts - 1, &ntp_ts);
+            TEST_CHECK(!CLG_GetNtpTxTimestamp(&ntp_ts, &ts));
+            CLG_UpdateNtpTxTimestamp(&ntp_ts, &ts);
+          }
+        }
+
+        if (random() % 2) {
+          int64_to_ntp64(get_ntp_tss(0)->rx_ts - 1, &ntp_ts);
+          TEST_CHECK(!CLG_GetNtpTxTimestamp(&ntp_ts, &ts));
+          int64_to_ntp64(get_ntp_tss(ntp_ts_map.size - 1)->rx_ts + 1, &ntp_ts);
+          TEST_CHECK(!CLG_GetNtpTxTimestamp(&ntp_ts, &ts));
+          CLG_UpdateNtpTxTimestamp(&ntp_ts, &ts);
+        }
+      }
+    }
+
+    for (j = 0; j < 500; j++) {
+      shift = (i % 3) * 26;
+
+      if (i % 7 == 0) {
+        while (ntp_ts_map.size < ntp_ts_map.max_size) {
+          ts64 += get_random64() >> (shift + 8);
+          int64_to_ntp64(ts64, &ntp_ts);
+          CLG_SaveNtpTimestamps(&ntp_ts, NULL);
+          if (ntp_ts_map.cached_index + NTPTS_INSERT_LIMIT < ntp_ts_map.size)
+            ts64 = get_ntp_tss(ntp_ts_map.size - 1)->rx_ts;
+        }
+      }
+      do {
+        if (ntp_ts_map.size > 1 && random() % 2) {
+          k = random() % (ntp_ts_map.size - 1);
+          ts64 = get_ntp_tss(k)->rx_ts +
+                 (get_ntp_tss(k + 1)->rx_ts - get_ntp_tss(k)->rx_ts) / 2;
+        } else {
+          ts64 = get_random64() >> shift;
+        }
+      } while (ts64 == 0ULL);
+
+      int64_to_ntp64(ts64, &ntp_ts);
+
+      prev_first = ntp_ts_map.first;
+      prev_size = ntp_ts_map.size;
+      prev_first_ts64 = get_ntp_tss(0)->rx_ts;
+      prev_last_ts64 = get_ntp_tss(prev_size - 1)->rx_ts;
+      CLG_SaveNtpTimestamps(&ntp_ts, NULL);
+
+      TEST_CHECK(find_ntp_rx_ts(ts64, &index2));
+
+      if (ntp_ts_map.size > 1) {
+        TEST_CHECK(ntp_ts_map.size > 0 && ntp_ts_map.size <= ntp_ts_map.max_size);
+        if (get_ntp_tss(index2)->flags & NTPTS_DISABLED)
+          continue;
+
+        TEST_CHECK(get_ntp_tss(ntp_ts_map.size - 1)->rx_ts - ts64 <= NTPTS_FUTURE_LIMIT);
+
+        if ((int64_t)(prev_last_ts64 - ts64) <= NTPTS_FUTURE_LIMIT) {
+          TEST_CHECK(prev_size + 1 >= ntp_ts_map.size);
+          if (index2 + NTPTS_INSERT_LIMIT + 1 >= ntp_ts_map.size &&
+              !(index2 == 0 &&
+                ((NTPTS_INSERT_LIMIT == prev_size && (int64_t)(ts64 - prev_first_ts64) > 0) ||
+                 (NTPTS_INSERT_LIMIT + 1 == prev_size && (int64_t)(ts64 - prev_first_ts64) < 0))))
+            TEST_CHECK((prev_first + prev_size + 1) % ntp_ts_map.max_size ==
+                       (ntp_ts_map.first + ntp_ts_map.size) % ntp_ts_map.max_size);
+          else
+            TEST_CHECK(prev_first + prev_size == ntp_ts_map.first + ntp_ts_map.size);
+        }
+
+        TEST_CHECK((int64_t)(get_ntp_tss(ntp_ts_map.size - 1)->rx_ts -
+                             get_ntp_tss(0)->rx_ts) > 0);
+        for (k = 0; k + 1 < ntp_ts_map.size; k++)
+          TEST_CHECK((int64_t)(get_ntp_tss(k + 1)->rx_ts - get_ntp_tss(k)->rx_ts) > 0);
+      }
+
+      if (random() % 10 == 0) {
+        CLG_DisableNtpTimestamps(&ntp_ts);
+        TEST_CHECK(!CLG_GetNtpTxTimestamp(&ntp_ts, &ts));
+      }
+
+      for (k = random() % 10; k > 0; k--) {
+        ts64 = get_random64() >> shift;
+        int64_to_ntp64(ts64, &ntp_ts);
+        CLG_GetNtpTxTimestamp(&ntp_ts, &ts);
+      }
+    }
+  }
+
   CLG_Finalise();
   CNF_Finalise();
 }