]> git.ipfire.org Git - thirdparty/chrony.git/commitdiff
sources: add option to limit selection of unreachable sources
authorMiroslav Lichvar <mlichvar@redhat.com>
Tue, 5 Aug 2025 14:08:40 +0000 (16:08 +0200)
committerMiroslav Lichvar <mlichvar@redhat.com>
Thu, 7 Aug 2025 08:18:31 +0000 (10:18 +0200)
Add maxunreach option to NTP sources and refclocks to specify the
maximum number of polls that the source can stay selected for
synchronization when it is unreachable (i.e. no valid sample was
received in the last 8 polls).

It is an additional requirement to having at least one sample more
recent than the oldest sample of reachable sources.

The default value is 100000. Setting the option to 0 disables selection
of unreachable sources, which matches RFC 5905.

16 files changed:
candm.h
client.c
cmdmon.c
cmdparse.c
conf.c
doc/chrony.conf.adoc
doc/chronyc.adoc
ntp_core.c
refclock.c
refclock.h
sources.c
sources.h
srcparams.h
test/simulation/110-chronyc
test/simulation/150-maxunreach [new file with mode: 0755]
test/unit/sources.c

diff --git a/candm.h b/candm.h
index c120b0b4a3add8c5905b3e0bd1dbc49e796b2fb0..34024efdab8a3da8ea0108ee07c1fea5e6f8372c 100644 (file)
--- a/candm.h
+++ b/candm.h
@@ -306,7 +306,7 @@ typedef struct {
   int32_t filter_length;
   uint32_t cert_set;
   Float max_delay_quant;
-  uint32_t reserved[1];
+  int32_t max_unreach;
   int32_t EOR;
 } REQ_NTP_Source;
 
index 1701dfd0301f41f4ce778f834a56a5de8b3caf9b..10d846d826339196fba3b2b0ca58801e3d38c6a0 100644 (file)
--- a/client.c
+++ b/client.c
@@ -1104,7 +1104,7 @@ process_cmd_add_source(CMD_Request *msg, char *line)
       msg->data.ntp_source.cert_set = htonl(data.params.cert_set);
       msg->data.ntp_source.max_delay_quant =
         UTI_FloatHostToNetwork(data.params.max_delay_quant);
-      memset(msg->data.ntp_source.reserved, 0, sizeof (msg->data.ntp_source.reserved));
+      msg->data.ntp_source.max_unreach = htonl(data.params.max_unreach);
 
       result = 1;
 
index e90a949f004aeea081116c77444d8ceea0d71f57..a21c3ada433591964d129ff9a65d19b2602b1f9a 100644 (file)
--- a/cmdmon.c
+++ b/cmdmon.c
@@ -686,6 +686,7 @@ handle_add_source(CMD_Request *rx_message, CMD_Reply *tx_message)
   params.max_sources = ntohl(rx_message->data.ntp_source.max_sources);
   params.min_samples = ntohl(rx_message->data.ntp_source.min_samples);
   params.max_samples = ntohl(rx_message->data.ntp_source.max_samples);
+  params.max_unreach = ntohl(rx_message->data.ntp_source.max_unreach);
   params.filter_length = ntohl(rx_message->data.ntp_source.filter_length);
   params.authkey = ntohl(rx_message->data.ntp_source.authkey);
   params.nts_port = ntohl(rx_message->data.ntp_source.nts_port);
index e84bca9d13d0816d7ca9384bf144b5586fe36a60..2e109fb281a14ac454115be5aee850552f2af532 100644 (file)
@@ -66,6 +66,7 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
   src->params.max_sources = SRC_DEFAULT_MAXSOURCES;
   src->params.min_samples = SRC_DEFAULT_MINSAMPLES;
   src->params.max_samples = SRC_DEFAULT_MAXSAMPLES;
+  src->params.max_unreach = SRC_DEFAULT_MAXUNREACH;
   src->params.filter_length = 0;
   src->params.interleaved = 0;
   src->params.sel_options = 0;
@@ -158,6 +159,9 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
     } else if (!strcasecmp(cmd, "maxsources")) {
       if (!SSCANF_IN_RANGE(line, "%d%n", &src->params.max_sources, &n, 1, INT_MAX))
         return CPS_InvalidValue;
+    } else if (!strcasecmp(cmd, "maxunreach")) {
+      if (!SSCANF_IN_RANGE(line, "%d%n", &src->params.max_unreach, &n, 0, INT_MAX))
+        return CPS_InvalidValue;
     } else if (!strcasecmp(cmd, "mindelay")) {
       if (sscanf(line, "%lf%n", &src->params.min_delay, &n) != 1)
         return CPS_InvalidValue;
diff --git a/conf.c b/conf.c
index 3fe1d1feafc071df35d1c0ca751abe1c8e80085a..1f362d7f37d898a1496d09e389afdcd1488982e1 100644 (file)
--- a/conf.c
+++ b/conf.c
@@ -958,7 +958,7 @@ static void
 parse_refclock(char *line)
 {
   int n, poll, dpoll, filter_length, pps_rate, min_samples, max_samples, sel_options;
-  int local, max_lock_age, pps_forced, sel_option, stratum, tai;
+  int local, max_lock_age, max_unreach, pps_forced, sel_option, stratum, tai;
   uint32_t ref_id, lock_ref_id;
   double offset, delay, precision, max_dispersion, pulse_width;
   char *p, *cmd, *name, *param;
@@ -972,6 +972,7 @@ parse_refclock(char *line)
   pps_rate = 0;
   min_samples = SRC_DEFAULT_MINSAMPLES;
   max_samples = SRC_DEFAULT_MAXSAMPLES;
+  max_unreach = SRC_DEFAULT_MAXUNREACH;
   sel_options = 0;
   offset = 0.0;
   delay = 1e-9;
@@ -1036,6 +1037,9 @@ parse_refclock(char *line)
     } else if (!strcasecmp(cmd, "maxsamples")) {
       if (!SSCANF_IN_RANGE(line, "%d%n", &max_samples, &n, 0, INT_MAX))
         break;
+    } else if (!strcasecmp(cmd, "maxunreach")) {
+      if (!SSCANF_IN_RANGE(line, "%d%n", &max_unreach, &n, 0, INT_MAX))
+        break;
     } else if (!strcasecmp(cmd, "offset")) {
       if (sscanf(line, "%lf%n", &offset, &n) != 1)
         break;
@@ -1085,6 +1089,7 @@ parse_refclock(char *line)
   refclock->pps_rate = pps_rate;
   refclock->min_samples = min_samples;
   refclock->max_samples = max_samples;
+  refclock->max_unreach = max_unreach;
   refclock->sel_options = sel_options;
   refclock->stratum = stratum;
   refclock->tai = tai;
index b90cf90b0c4bf95145ce25ad9e05e9b1d9327b8d..fee500e62d6140df6f674ad00691fe63320b3f98 100644 (file)
@@ -218,6 +218,12 @@ Set the minimum number of samples kept for this source. This overrides the
 *maxsamples* _samples_:::
 Set the maximum number of samples kept for this source. This overrides the
 <<maxsamples,*maxsamples*>> directive.
+*maxunreach* _polls_:::
+This option specifies the maximum number of polls that this source can stay
+selected for synchronisation when it is unreachable (i.e. no valid response was
+received to the last 8 requests). Only sources with at least one sample more
+recent than the oldest sample of all reachable sources can be selected. The
+default is 100000.
 *filter* _polls_:::
 This option enables a median filter to reduce noise in NTP measurements. The
 filter will process samples collected in the specified number of polls
@@ -731,6 +737,12 @@ Set the minimum number of samples kept for this source. This overrides the
 *maxsamples* _samples_:::
 Set the maximum number of samples kept for this source. This overrides the
 <<maxsamples,*maxsamples*>> directive.
+*maxunreach* _polls_:::
+This option specifies the maximum number of polls that this source can stay
+selected for synchronisation when it is unreachable (i.e. no valid sample was
+received in the last 8 polls). Only sources with at least one sample more
+recent than the oldest sample of all reachable sources can be selected.
+The default is 100000.
 
 [[manual]]*manual*::
 The *manual* directive enables support at run-time for the
index 36790dad11b9d51071200d742ef772c7b21232d7..4f9e8be7208471431da98001fe6e4c43d55f0fe4 100644 (file)
@@ -479,7 +479,8 @@ synchronisation:
 * _~_ - has a jitter larger than the maximum jitter (configured by the
         <<chrony.conf.adoc#maxjitter,*maxjitter*>> directive).
 * _w_ - waits for other sources to get out of the _M_ state.
-* _S_ - has older measurements than other sources.
+* _S_ - has only measurements older than reachable sources, or is unreachable
+        for too many polls (configured by the *maxunreach* option).
 * _O_ - has a stratum equal or larger than the orphan stratum (configured by
         the <<chrony.conf.adoc#local,*local*>> directive).
 * _T_ - does not fully agree with sources that have the *trust* option.
index 501ea6db79d976aa0501b955dc04dbbbd8940b17..7395a6387d2b6d5f95b63a329313f708507c8e1d 100644 (file)
@@ -686,7 +686,8 @@ NCR_CreateInstance(NTP_Remote_Address *remote_addr, NTP_Source_Type type,
                                          SRC_NTP, NAU_IsAuthEnabled(result->auth),
                                          params->sel_options, &result->remote_addr.ip_addr,
                                          params->min_samples, params->max_samples,
-                                         params->min_delay, params->asymmetry);
+                                         params->min_delay, params->asymmetry,
+                                         params->max_unreach);
 
   if (params->max_delay_quant > 0.0) {
     int k = round(CLAMP(0.05, params->max_delay_quant, 0.95) * DELAY_QUANT_Q);
index 2cc4f621fd3a9344c119533753b07a28e81ed1df..c4f16f25763787c15f212c18d00d59a45038edc0 100644 (file)
@@ -244,7 +244,7 @@ RCL_AddRefclock(RefclockParameters *params)
 
   inst->source = SRC_CreateNewInstance(inst->ref_id, SRC_REFCLOCK, 0, params->sel_options,
                                        NULL, params->min_samples, params->max_samples,
-                                       0.0, 0.0);
+                                       0.0, 0.0, params->max_unreach);
 
   DEBUG_LOG("refclock %s refid=%s poll=%d dpoll=%d filter=%d",
       params->driver_name, UTI_RefidToString(inst->ref_id),
index 5fdbf9c75cb61b00a70b09229bc86bd298aab50f..1f4d29c3054ec9b8a032acd1c64fc44a4ed5887f 100644 (file)
@@ -42,6 +42,7 @@ typedef struct {
   int pps_rate;
   int min_samples;
   int max_samples;
+  int max_unreach;
   int sel_options;
   int max_lock_age;
   int stratum;
index 58a7110ae426f296280d472d8021e203b3bad757..dc8de916546841936d7c77de6c5b4da557f5b62f 100644 (file)
--- a/sources.c
+++ b/sources.c
@@ -106,13 +106,20 @@ struct SRC_Instance_Record {
   /* Number of set bits in the reachability register */
   int reachability_size;
 
-  /* Updates since last reference update */
+  /* Number of reachability updates with cleared register */
+  int unreachable_run;
+
+  /* Maximum number of reachability updates with cleared register to still
+     allow selection */
+  int max_unreachable_run;
+
+  /* Number of selection updates since last reference update */
   int updates;
 
-  /* Updates left before allowing combining */
+  /* Number of selection updates left before allowing combining again */
   int distant;
 
-  /* Updates with a status requiring source replacement */
+  /* Number of selection updates with a status requiring source replacement */
   int bad;
 
   /* Flag indicating the status of the source */
@@ -259,7 +266,8 @@ void SRC_Finalise(void)
 
 SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authenticated,
                                    int sel_options, IPAddr *addr, int min_samples,
-                                   int max_samples, double min_delay, double asymmetry)
+                                   int max_samples, double min_delay, double asymmetry,
+                                   int max_unreach)
 {
   SRC_Instance result;
 
@@ -295,6 +303,7 @@ SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authentic
   result->authenticated = authenticated;
   result->conf_sel_options = sel_options;
   result->sel_options = sel_options;
+  result->max_unreachable_run = max_unreach;
   result->active = 0;
 
   SRC_SetRefid(result, ref_id, addr);
@@ -351,6 +360,7 @@ SRC_ResetInstance(SRC_Instance instance)
   instance->updates = 0;
   instance->reachability = 0;
   instance->reachability_size = 0;
+  instance->unreachable_run = 0;
   instance->distant = 0;
   instance->bad = 0;
   instance->status = SRC_BAD_STATS;
@@ -524,6 +534,13 @@ SRC_UpdateReachability(SRC_Instance inst, int reachable)
   if (inst->reachability_size < SOURCE_REACH_BITS)
       inst->reachability_size++;
 
+  if (inst->reachability == 0) {
+    if (inst->unreachable_run < INT_MAX)
+      inst->unreachable_run++;
+  } else {
+    inst->unreachable_run = 0;
+  }
+
   /* Check if special reference update mode failed */
   if (REF_GetMode() != REF_ModeNormal && special_mode_end()) {
     REF_SetUnsynchronised();
@@ -545,6 +562,7 @@ SRC_ResetReachability(SRC_Instance inst)
 {
   inst->reachability = 0;
   inst->reachability_size = 0;
+  inst->unreachable_run = 0;
   SRC_UpdateReachability(inst, 0);
 }
 
@@ -1059,8 +1077,11 @@ SRC_SelectSource(SRC_Instance updated_inst)
 
     /* Reachability is not a requirement for selection.  An unreachable source
        can still be selected if its newest sample is not older than the oldest
-       sample from reachable sources. */
-    if (!sources[i]->reachability && max_reach_sample_ago < si->last_sample_ago) {
+       sample from reachable sources and the number of consecutive unreachable
+       updates does not exceed the configured maximum. */
+    if (sources[i]->reachability == 0 &&
+        (si->last_sample_ago > max_reach_sample_ago ||
+         sources[i]->unreachable_run > sources[i]->max_unreachable_run)) {
       mark_source(sources[i], SRC_STALE);
       continue;
     }
index da3a5335570ebe9299bdcf52e3872cb94ea77149..89cf0add27e25f6b2d77cf0e5021d1eee05e726f 100644 (file)
--- a/sources.h
+++ b/sources.h
@@ -69,7 +69,8 @@ typedef enum {
 
 extern SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authenticated,
                                           int sel_options, IPAddr *addr, int min_samples,
-                                          int max_samples, double min_delay, double asymmetry);
+                                          int max_samples, double min_delay, double asymmetry,
+                                          int max_unreach);
 
 /* Function to get rid of a source when it is being unconfigured.
    This may cause the current reference source to be reselected, if this
index 31baed77d6cf2dc98ab3ebcb1c5ad845f1ccce16..8a5a999dfd4635180604f67c768a19ccb79245d8 100644 (file)
@@ -49,6 +49,7 @@ typedef struct {
   int max_sources;
   int min_samples;
   int max_samples;
+  int max_unreach;
   int filter_length;
   int interleaved;
   int sel_options;
@@ -79,6 +80,7 @@ typedef struct {
 #define SRC_DEFAULT_MAXSOURCES 4
 #define SRC_DEFAULT_MINSAMPLES (-1)
 #define SRC_DEFAULT_MAXSAMPLES (-1)
+#define SRC_DEFAULT_MAXUNREACH 100000
 #define SRC_DEFAULT_ASYMMETRY 1.0
 #define SRC_DEFAULT_NTSPORT 4460
 #define SRC_DEFAULT_CERTSET 0
index 1872d5cd9af6fa7dfd995fbfec13bb8548d8ee25..448c89bc43397731d5d80aec4eb8b40b9e96df7a 100755 (executable)
@@ -114,7 +114,7 @@ limit=1
 for chronyc_conf in \
        "accheck 1.2.3.4" \
        "add peer 10.0.0.0 minpoll 2 maxpoll 6" \
-       "add server 10.0.0.0 minpoll 6 maxpoll 10 iburst burst key 1 certset 2 maxdelay 1e-3 maxdelayratio 10.0 maxdelaydevratio 10.0 maxdelayquant 0.5 mindelay 1e-4 asymmetry 0.5 offset 1e-5 minsamples 6 maxsamples 6 filter 3 offline auto_offline prefer noselect trust require xleave polltarget 20 port 123 presend 7 minstratum 3 version 4 nts ntsport 4460 copy extfield F323 extfield F324 ipv6 ipv4" \
+       "add server 10.0.0.0 minpoll 6 maxpoll 10 iburst burst key 1 certset 2 maxdelay 1e-3 maxdelayratio 10.0 maxdelaydevratio 10.0 maxdelayquant 0.5 mindelay 1e-4 asymmetry 0.5 offset 1e-5 minsamples 6 maxsamples 6 maxunreach 8 filter 3 offline auto_offline prefer noselect trust require xleave polltarget 20 port 123 presend 7 minstratum 3 version 4 nts ntsport 4460 copy extfield F323 extfield F324 ipv6 ipv4" \
        "add server node1.net1.clk" \
        "allow 1.2.3.4" \
        "allow 1.2" \
diff --git a/test/simulation/150-maxunreach b/test/simulation/150-maxunreach
new file mode 100755 (executable)
index 0000000..faf730c
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+. ./test.common
+
+test_start "maxunreach option"
+
+limit=5000
+servers=2
+client_server_options="minpoll 6 maxpoll 6 minsamples 64"
+base_delay=$(cat <<-EOF | tr -d '\n'
+  (+ 1e-4
+     (* -1
+        (equal 0.1 from 3)
+        (equal 0.1 to 1)
+        (equal 0.1 (min time 2000) 2000))
+     (* 0.5
+        (+ (equal 0.1 from 2)
+           (equal 0.1 to 2))))
+EOF
+)
+
+run_test || test_fail
+check_chronyd_exit || test_fail
+check_source_selection || test_fail
+check_packet_interval || test_fail
+check_sync || test_fail
+
+check_log_messages "Selected source 192.168.123.1" 1 1 || test_fail
+check_log_messages "Selected source 192.168.123.2" 0 0 || test_fail
+
+client_server_options="minpoll 6 maxpoll 6 minsamples 64 maxunreach 10"
+
+run_test || test_fail
+check_chronyd_exit || test_fail
+check_source_selection || test_fail
+check_packet_interval || test_fail
+check_sync || test_fail
+
+check_log_messages "Selected source 192.168.123.1" 1 1 || test_fail
+check_log_messages "Selected source 192.168.123.2" 1 1 || test_fail
+check_log_messages "00:52:..Z Selected source 192.168.123.2" 1 1 || test_fail
+
+test_pass
index 155e819251c17b63c4b5c4e0e9804656735addc7..c7216bb2d77cb287b3d8dbb40056db38a1dd2b9a 100644 (file)
@@ -28,7 +28,8 @@ create_source(SRC_Type type, IPAddr *addr, int authenticated, int sel_options)
 
   return SRC_CreateNewInstance(UTI_IPToRefid(addr), type, authenticated, sel_options,
                                type == SRC_NTP ? addr : NULL,
-                               SRC_DEFAULT_MINSAMPLES, SRC_DEFAULT_MAXSAMPLES, 0.0, 1.0);
+                               SRC_DEFAULT_MINSAMPLES, SRC_DEFAULT_MAXSAMPLES, 0.0, 1.0,
+                               SRC_DEFAULT_MAXUNREACH);
 }
 
 void