sources: add option to limit selection of unreachable sources

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.
This commit is contained in:
Miroslav Lichvar
2025-08-05 16:08:40 +02:00
parent be7f5e8916
commit 9b183fe98f
16 changed files with 108 additions and 15 deletions

View File

@@ -306,7 +306,7 @@ typedef struct {
int32_t filter_length; int32_t filter_length;
uint32_t cert_set; uint32_t cert_set;
Float max_delay_quant; Float max_delay_quant;
uint32_t reserved[1]; int32_t max_unreach;
int32_t EOR; int32_t EOR;
} REQ_NTP_Source; } REQ_NTP_Source;

View File

@@ -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.cert_set = htonl(data.params.cert_set);
msg->data.ntp_source.max_delay_quant = msg->data.ntp_source.max_delay_quant =
UTI_FloatHostToNetwork(data.params.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; result = 1;

View File

@@ -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.max_sources = ntohl(rx_message->data.ntp_source.max_sources);
params.min_samples = ntohl(rx_message->data.ntp_source.min_samples); params.min_samples = ntohl(rx_message->data.ntp_source.min_samples);
params.max_samples = ntohl(rx_message->data.ntp_source.max_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.filter_length = ntohl(rx_message->data.ntp_source.filter_length);
params.authkey = ntohl(rx_message->data.ntp_source.authkey); params.authkey = ntohl(rx_message->data.ntp_source.authkey);
params.nts_port = ntohl(rx_message->data.ntp_source.nts_port); params.nts_port = ntohl(rx_message->data.ntp_source.nts_port);

View File

@@ -66,6 +66,7 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
src->params.max_sources = SRC_DEFAULT_MAXSOURCES; src->params.max_sources = SRC_DEFAULT_MAXSOURCES;
src->params.min_samples = SRC_DEFAULT_MINSAMPLES; src->params.min_samples = SRC_DEFAULT_MINSAMPLES;
src->params.max_samples = SRC_DEFAULT_MAXSAMPLES; src->params.max_samples = SRC_DEFAULT_MAXSAMPLES;
src->params.max_unreach = SRC_DEFAULT_MAXUNREACH;
src->params.filter_length = 0; src->params.filter_length = 0;
src->params.interleaved = 0; src->params.interleaved = 0;
src->params.sel_options = 0; src->params.sel_options = 0;
@@ -158,6 +159,9 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
} else if (!strcasecmp(cmd, "maxsources")) { } else if (!strcasecmp(cmd, "maxsources")) {
if (!SSCANF_IN_RANGE(line, "%d%n", &src->params.max_sources, &n, 1, INT_MAX)) if (!SSCANF_IN_RANGE(line, "%d%n", &src->params.max_sources, &n, 1, INT_MAX))
return CPS_InvalidValue; 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")) { } else if (!strcasecmp(cmd, "mindelay")) {
if (sscanf(line, "%lf%n", &src->params.min_delay, &n) != 1) if (sscanf(line, "%lf%n", &src->params.min_delay, &n) != 1)
return CPS_InvalidValue; return CPS_InvalidValue;

7
conf.c
View File

@@ -958,7 +958,7 @@ static void
parse_refclock(char *line) parse_refclock(char *line)
{ {
int n, poll, dpoll, filter_length, pps_rate, min_samples, max_samples, sel_options; 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; uint32_t ref_id, lock_ref_id;
double offset, delay, precision, max_dispersion, pulse_width; double offset, delay, precision, max_dispersion, pulse_width;
char *p, *cmd, *name, *param; char *p, *cmd, *name, *param;
@@ -972,6 +972,7 @@ parse_refclock(char *line)
pps_rate = 0; pps_rate = 0;
min_samples = SRC_DEFAULT_MINSAMPLES; min_samples = SRC_DEFAULT_MINSAMPLES;
max_samples = SRC_DEFAULT_MAXSAMPLES; max_samples = SRC_DEFAULT_MAXSAMPLES;
max_unreach = SRC_DEFAULT_MAXUNREACH;
sel_options = 0; sel_options = 0;
offset = 0.0; offset = 0.0;
delay = 1e-9; delay = 1e-9;
@@ -1036,6 +1037,9 @@ parse_refclock(char *line)
} else if (!strcasecmp(cmd, "maxsamples")) { } else if (!strcasecmp(cmd, "maxsamples")) {
if (!SSCANF_IN_RANGE(line, "%d%n", &max_samples, &n, 0, INT_MAX)) if (!SSCANF_IN_RANGE(line, "%d%n", &max_samples, &n, 0, INT_MAX))
break; 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")) { } else if (!strcasecmp(cmd, "offset")) {
if (sscanf(line, "%lf%n", &offset, &n) != 1) if (sscanf(line, "%lf%n", &offset, &n) != 1)
break; break;
@@ -1085,6 +1089,7 @@ parse_refclock(char *line)
refclock->pps_rate = pps_rate; refclock->pps_rate = pps_rate;
refclock->min_samples = min_samples; refclock->min_samples = min_samples;
refclock->max_samples = max_samples; refclock->max_samples = max_samples;
refclock->max_unreach = max_unreach;
refclock->sel_options = sel_options; refclock->sel_options = sel_options;
refclock->stratum = stratum; refclock->stratum = stratum;
refclock->tai = tai; refclock->tai = tai;

View File

@@ -218,6 +218,12 @@ Set the minimum number of samples kept for this source. This overrides the
*maxsamples* _samples_::: *maxsamples* _samples_:::
Set the maximum number of samples kept for this source. This overrides the Set the maximum number of samples kept for this source. This overrides the
<<maxsamples,*maxsamples*>> directive. <<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_::: *filter* _polls_:::
This option enables a median filter to reduce noise in NTP measurements. The This option enables a median filter to reduce noise in NTP measurements. The
filter will process samples collected in the specified number of polls 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_::: *maxsamples* _samples_:::
Set the maximum number of samples kept for this source. This overrides the Set the maximum number of samples kept for this source. This overrides the
<<maxsamples,*maxsamples*>> directive. <<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*:: [[manual]]*manual*::
The *manual* directive enables support at run-time for the The *manual* directive enables support at run-time for the

View File

@@ -479,7 +479,8 @@ synchronisation:
* _~_ - has a jitter larger than the maximum jitter (configured by the * _~_ - has a jitter larger than the maximum jitter (configured by the
<<chrony.conf.adoc#maxjitter,*maxjitter*>> directive). <<chrony.conf.adoc#maxjitter,*maxjitter*>> directive).
* _w_ - waits for other sources to get out of the _M_ state. * _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 * _O_ - has a stratum equal or larger than the orphan stratum (configured by
the <<chrony.conf.adoc#local,*local*>> directive). the <<chrony.conf.adoc#local,*local*>> directive).
* _T_ - does not fully agree with sources that have the *trust* option. * _T_ - does not fully agree with sources that have the *trust* option.

View File

@@ -686,7 +686,8 @@ NCR_CreateInstance(NTP_Remote_Address *remote_addr, NTP_Source_Type type,
SRC_NTP, NAU_IsAuthEnabled(result->auth), SRC_NTP, NAU_IsAuthEnabled(result->auth),
params->sel_options, &result->remote_addr.ip_addr, params->sel_options, &result->remote_addr.ip_addr,
params->min_samples, params->max_samples, 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) { if (params->max_delay_quant > 0.0) {
int k = round(CLAMP(0.05, params->max_delay_quant, 0.95) * DELAY_QUANT_Q); int k = round(CLAMP(0.05, params->max_delay_quant, 0.95) * DELAY_QUANT_Q);

View File

@@ -244,7 +244,7 @@ RCL_AddRefclock(RefclockParameters *params)
inst->source = SRC_CreateNewInstance(inst->ref_id, SRC_REFCLOCK, 0, params->sel_options, inst->source = SRC_CreateNewInstance(inst->ref_id, SRC_REFCLOCK, 0, params->sel_options,
NULL, params->min_samples, params->max_samples, 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", DEBUG_LOG("refclock %s refid=%s poll=%d dpoll=%d filter=%d",
params->driver_name, UTI_RefidToString(inst->ref_id), params->driver_name, UTI_RefidToString(inst->ref_id),

View File

@@ -42,6 +42,7 @@ typedef struct {
int pps_rate; int pps_rate;
int min_samples; int min_samples;
int max_samples; int max_samples;
int max_unreach;
int sel_options; int sel_options;
int max_lock_age; int max_lock_age;
int stratum; int stratum;

View File

@@ -106,13 +106,20 @@ struct SRC_Instance_Record {
/* Number of set bits in the reachability register */ /* Number of set bits in the reachability register */
int reachability_size; 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; int updates;
/* Updates left before allowing combining */ /* Number of selection updates left before allowing combining again */
int distant; int distant;
/* Updates with a status requiring source replacement */ /* Number of selection updates with a status requiring source replacement */
int bad; int bad;
/* Flag indicating the status of the source */ /* 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, SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authenticated,
int sel_options, IPAddr *addr, int min_samples, 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; SRC_Instance result;
@@ -295,6 +303,7 @@ SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authentic
result->authenticated = authenticated; result->authenticated = authenticated;
result->conf_sel_options = sel_options; result->conf_sel_options = sel_options;
result->sel_options = sel_options; result->sel_options = sel_options;
result->max_unreachable_run = max_unreach;
result->active = 0; result->active = 0;
SRC_SetRefid(result, ref_id, addr); SRC_SetRefid(result, ref_id, addr);
@@ -351,6 +360,7 @@ SRC_ResetInstance(SRC_Instance instance)
instance->updates = 0; instance->updates = 0;
instance->reachability = 0; instance->reachability = 0;
instance->reachability_size = 0; instance->reachability_size = 0;
instance->unreachable_run = 0;
instance->distant = 0; instance->distant = 0;
instance->bad = 0; instance->bad = 0;
instance->status = SRC_BAD_STATS; instance->status = SRC_BAD_STATS;
@@ -524,6 +534,13 @@ SRC_UpdateReachability(SRC_Instance inst, int reachable)
if (inst->reachability_size < SOURCE_REACH_BITS) if (inst->reachability_size < SOURCE_REACH_BITS)
inst->reachability_size++; 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 */ /* Check if special reference update mode failed */
if (REF_GetMode() != REF_ModeNormal && special_mode_end()) { if (REF_GetMode() != REF_ModeNormal && special_mode_end()) {
REF_SetUnsynchronised(); REF_SetUnsynchronised();
@@ -545,6 +562,7 @@ SRC_ResetReachability(SRC_Instance inst)
{ {
inst->reachability = 0; inst->reachability = 0;
inst->reachability_size = 0; inst->reachability_size = 0;
inst->unreachable_run = 0;
SRC_UpdateReachability(inst, 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 /* Reachability is not a requirement for selection. An unreachable source
can still be selected if its newest sample is not older than the oldest can still be selected if its newest sample is not older than the oldest
sample from reachable sources. */ sample from reachable sources and the number of consecutive unreachable
if (!sources[i]->reachability && max_reach_sample_ago < si->last_sample_ago) { 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); mark_source(sources[i], SRC_STALE);
continue; continue;
} }

View File

@@ -69,7 +69,8 @@ typedef enum {
extern SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authenticated, extern SRC_Instance SRC_CreateNewInstance(uint32_t ref_id, SRC_Type type, int authenticated,
int sel_options, IPAddr *addr, int min_samples, 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. /* Function to get rid of a source when it is being unconfigured.
This may cause the current reference source to be reselected, if this This may cause the current reference source to be reselected, if this

View File

@@ -49,6 +49,7 @@ typedef struct {
int max_sources; int max_sources;
int min_samples; int min_samples;
int max_samples; int max_samples;
int max_unreach;
int filter_length; int filter_length;
int interleaved; int interleaved;
int sel_options; int sel_options;
@@ -79,6 +80,7 @@ typedef struct {
#define SRC_DEFAULT_MAXSOURCES 4 #define SRC_DEFAULT_MAXSOURCES 4
#define SRC_DEFAULT_MINSAMPLES (-1) #define SRC_DEFAULT_MINSAMPLES (-1)
#define SRC_DEFAULT_MAXSAMPLES (-1) #define SRC_DEFAULT_MAXSAMPLES (-1)
#define SRC_DEFAULT_MAXUNREACH 100000
#define SRC_DEFAULT_ASYMMETRY 1.0 #define SRC_DEFAULT_ASYMMETRY 1.0
#define SRC_DEFAULT_NTSPORT 4460 #define SRC_DEFAULT_NTSPORT 4460
#define SRC_DEFAULT_CERTSET 0 #define SRC_DEFAULT_CERTSET 0

View File

@@ -114,7 +114,7 @@ limit=1
for chronyc_conf in \ for chronyc_conf in \
"accheck 1.2.3.4" \ "accheck 1.2.3.4" \
"add peer 10.0.0.0 minpoll 2 maxpoll 6" \ "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" \ "add server node1.net1.clk" \
"allow 1.2.3.4" \ "allow 1.2.3.4" \
"allow 1.2" \ "allow 1.2" \

43
test/simulation/150-maxunreach Executable file
View File

@@ -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

View 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, return SRC_CreateNewInstance(UTI_IPToRefid(addr), type, authenticated, sel_options,
type == SRC_NTP ? addr : NULL, 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 void