ntp: add client support for network correction

If the network correction is known for both the request and response,
and their sum is not larger that the measured peer delay, allowing the
transparent clocks to be running up to 100 ppm faster than the client's
clock, apply the corrections to the NTP offset and peer delay. Don't
correct the root delay to not change the estimated maximum error.
This commit is contained in:
Miroslav Lichvar 2023-09-26 12:52:39 +02:00
parent 8eef631009
commit 70cdd8b1ef
10 changed files with 216 additions and 51 deletions

View file

@ -278,6 +278,7 @@ typedef struct {
#define REQ_ADDSRC_NTS 0x200 #define REQ_ADDSRC_NTS 0x200
#define REQ_ADDSRC_COPY 0x400 #define REQ_ADDSRC_COPY 0x400
#define REQ_ADDSRC_EF_EXP_MONO_ROOT 0x800 #define REQ_ADDSRC_EF_EXP_MONO_ROOT 0x800
#define REQ_ADDSRC_EF_EXP_NET_CORRECTION 0x1000
typedef struct { typedef struct {
uint32_t type; uint32_t type;

View file

@ -958,7 +958,10 @@ process_cmd_add_source(CMD_Request *msg, char *line)
(data.params.burst ? REQ_ADDSRC_BURST : 0) | (data.params.burst ? REQ_ADDSRC_BURST : 0) |
(data.params.nts ? REQ_ADDSRC_NTS : 0) | (data.params.nts ? REQ_ADDSRC_NTS : 0) |
(data.params.copy ? REQ_ADDSRC_COPY : 0) | (data.params.copy ? REQ_ADDSRC_COPY : 0) |
(data.params.ext_fields & NTP_EF_FLAG_EXP_MONO_ROOT ? REQ_ADDSRC_EF_EXP_MONO_ROOT : 0) | (data.params.ext_fields & NTP_EF_FLAG_EXP_MONO_ROOT ?
REQ_ADDSRC_EF_EXP_MONO_ROOT : 0) |
(data.params.ext_fields & NTP_EF_FLAG_EXP_NET_CORRECTION ?
REQ_ADDSRC_EF_EXP_NET_CORRECTION : 0) |
convert_addsrc_sel_options(data.params.sel_options)); convert_addsrc_sel_options(data.params.sel_options));
msg->data.ntp_source.filter_length = htonl(data.params.filter_length); msg->data.ntp_source.filter_length = htonl(data.params.filter_length);
msg->data.ntp_source.cert_set = htonl(data.params.cert_set); msg->data.ntp_source.cert_set = htonl(data.params.cert_set);

View file

@ -783,8 +783,10 @@ handle_add_source(CMD_Request *rx_message, CMD_Reply *tx_message)
params.burst = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_BURST ? 1 : 0; params.burst = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_BURST ? 1 : 0;
params.nts = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_NTS ? 1 : 0; params.nts = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_NTS ? 1 : 0;
params.copy = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_COPY ? 1 : 0; params.copy = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_COPY ? 1 : 0;
params.ext_fields = ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_EF_EXP_MONO_ROOT ? params.ext_fields = (ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_EF_EXP_MONO_ROOT ?
NTP_EF_FLAG_EXP_MONO_ROOT : 0; NTP_EF_FLAG_EXP_MONO_ROOT : 0) |
(ntohl(rx_message->data.ntp_source.flags) & REQ_ADDSRC_EF_EXP_NET_CORRECTION ?
NTP_EF_FLAG_EXP_NET_CORRECTION : 0);
params.sel_options = convert_addsrc_select_options(ntohl(rx_message->data.ntp_source.flags)); params.sel_options = convert_addsrc_select_options(ntohl(rx_message->data.ntp_source.flags));
status = NSR_AddSourceByName(name, port, pool, type, &params, NULL); status = NSR_AddSourceByName(name, port, pool, type, &params, NULL);

View file

@ -118,6 +118,9 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
case NTP_EF_EXP_MONO_ROOT: case NTP_EF_EXP_MONO_ROOT:
src->params.ext_fields |= NTP_EF_FLAG_EXP_MONO_ROOT; src->params.ext_fields |= NTP_EF_FLAG_EXP_MONO_ROOT;
break; break;
case NTP_EF_EXP_NET_CORRECTION:
src->params.ext_fields |= NTP_EF_FLAG_EXP_NET_CORRECTION;
break;
default: default:
return 0; return 0;
} }

View file

@ -322,7 +322,9 @@ server implementations do not respond to requests containing an unknown
extension field (*chronyd* as a server responded to such requests since extension field (*chronyd* as a server responded to such requests since
version 2.0). version 2.0).
+ +
The following extension field can be enabled by this option: This option can be used multiple times to enable multiple extension fields.
+
The following extension fields are supported:
+ +
_F323_:::: _F323_::::
An experimental extension field to enable several improvements that were An experimental extension field to enable several improvements that were
@ -331,6 +333,14 @@ root delay and dispersion in higher resolution and a monotonic receive
timestamp, which enables a frequency transfer between the server and client to timestamp, which enables a frequency transfer between the server and client to
significantly improve stability of the synchronisation. This field should be significantly improve stability of the synchronisation. This field should be
enabled only for servers known to be running *chronyd* version 4.2 or later. enabled only for servers known to be running *chronyd* version 4.2 or later.
_F324_::::
An experimental extension field to enable the use of the Precision Time
Protocol (PTP) correction field in NTP-over-PTP messages updated by one-step
end-to-end transparent clocks in network switches and routers to significantly
improve accuracy and stability of the synchronisation. NTP-over-PTP can be
enabled by the <<ptpport,*ptpport*>> directive and setting the *port* option to
the PTP port. This field should be enabled only for servers known to be running
*chronyd* version 4.5 or later.
{blank}::: {blank}:::
[[pool]]*pool* _name_ [_option_]...:: [[pool]]*pool* _name_ [_option_]...::
@ -2727,8 +2737,10 @@ pidfile /run/chronyd.pid
The *ptpport* directive enables *chronyd* to send and receive NTP messages The *ptpport* directive enables *chronyd* to send and receive NTP messages
contained in PTP event messages (NTP-over-PTP) to enable hardware timestamping contained in PTP event messages (NTP-over-PTP) to enable hardware timestamping
on NICs which cannot timestamp NTP packets, but can timestamp unicast PTP on NICs which cannot timestamp NTP packets, but can timestamp unicast PTP
packets. The port recognized by the NICs is 319 (PTP event port). The default packets, and also use corrections provided by PTP one-step end-to-end
value is 0 (disabled). transparent clocks in network switches and routers. The port recognized by the
NICs and PTP transparent clocks is 319 (PTP event port). The default value is 0
(disabled).
+ +
The NTP-over-PTP support is experimental. The protocol and configuration can The NTP-over-PTP support is experimental. The protocol and configuration can
change in future. It should be used only in local networks. change in future. It should be used only in local networks.
@ -2738,12 +2750,14 @@ server or client. The directive does not change the default protocol of
specified NTP sources. Each NTP source that should use NTP-over-PTP needs to specified NTP sources. Each NTP source that should use NTP-over-PTP needs to
be specified with the *port* option set to the PTP port. To actually enable be specified with the *port* option set to the PTP port. To actually enable
hardware timestamping on NICs which can timestamp PTP packets only, the hardware timestamping on NICs which can timestamp PTP packets only, the
*rxfilter* option of the *hwtimestamp* directive needs to be set to _ptp_. *rxfilter* option of the *hwtimestamp* directive needs to be set to _ptp_. The
extension field _F324_ needs to be enabled to use the corrections provided by
the PTP transparent clocks.
+ +
An example of client configuration is: An example of client configuration is:
+ +
---- ----
server foo.example.net minpoll 0 maxpoll 0 xleave port 319 server foo.example.net minpoll 0 maxpoll 0 xleave port 319 extfield F324
hwtimestamp * rxfilter ptp hwtimestamp * rxfilter ptp
ptpport 319 ptpport 319
---- ----

View file

@ -314,6 +314,9 @@ static ARR_Instance broadcasts;
/* Maximum acceptable change in server mono<->real offset */ /* Maximum acceptable change in server mono<->real offset */
#define MAX_MONO_DOFFSET 16.0 #define MAX_MONO_DOFFSET 16.0
/* Maximum assumed frequency error in network corrections */
#define MAX_NET_CORRECTION_FREQ 100.0e-6
/* Invalid socket, different from the one in ntp_io.c */ /* Invalid socket, different from the one in ntp_io.c */
#define INVALID_SOCK_FD -2 #define INVALID_SOCK_FD -2
@ -1661,6 +1664,53 @@ parse_packet(NTP_Packet *packet, int length, NTP_PacketInfo *info)
/* ================================================== */ /* ================================================== */
static void
apply_net_correction(NTP_Sample *sample, NTP_Local_Timestamp *rx, NTP_Local_Timestamp *tx,
double precision)
{
double rx_correction, tx_correction, low_delay_correction;
/* Require some correction from transparent clocks to be present
in both directions (not just the local RX timestamp correction) */
if (rx->net_correction <= rx->rx_duration || tx->net_correction <= 0.0)
return;
/* With perfect corrections from PTP transparent clocks and short cables
the peer delay would be close to zero, or even negative if the server or
transparent clocks were running faster than client, which would invert the
sample weighting. Adjust the correction to get a delay corresponding to
a direct connection to the server. For simplicity, assume the TX and RX
link speeds are equal. If not, the reported delay will be wrong, but it
will not cause an error in the offset. */
rx_correction = rx->net_correction - rx->rx_duration;
tx_correction = tx->net_correction - rx->rx_duration;
/* Use a slightly smaller value in the correction of delay to not overcorrect
if the transparent clocks run up to 100 ppm fast and keep a part of the
uncorrected delay for the sample weighting */
low_delay_correction = (rx_correction + tx_correction) *
(1.0 - MAX_NET_CORRECTION_FREQ);
/* Make sure the correction is sane. The values are not authenticated! */
if (low_delay_correction < 0.0 || low_delay_correction > sample->peer_delay) {
DEBUG_LOG("Invalid correction %.9f peer_delay=%.9f",
low_delay_correction, sample->peer_delay);
return;
}
/* Correct the offset and peer delay, but not the root delay to not
change the estimated maximum error */
sample->offset += (rx_correction - tx_correction) / 2.0;
sample->peer_delay -= low_delay_correction;
if (sample->peer_delay < precision)
sample->peer_delay = precision;
DEBUG_LOG("Applied correction rx=%.9f tx=%.9f dur=%.9f",
rx->net_correction, tx->net_correction, rx->rx_duration);
}
/* ================================================== */
static int static int
check_delay_ratio(NCR_Instance inst, SST_Stats stats, check_delay_ratio(NCR_Instance inst, SST_Stats stats,
struct timespec *sample_time, double delay) struct timespec *sample_time, double delay)
@ -1923,10 +1973,11 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
int parsed, ef_length, ef_type, ef_body_length; int parsed, ef_length, ef_type, ef_body_length;
void *ef_body; void *ef_body;
NTP_EFExpMonoRoot *ef_mono_root; NTP_EFExpMonoRoot *ef_mono_root;
NTP_EFExpNetCorrection *ef_net_correction;
NTP_Local_Timestamp local_receive, local_transmit; NTP_Local_Timestamp local_receive, local_transmit;
double remote_interval, local_interval, response_time; double remote_interval, local_interval, response_time;
double delay_time, precision, mono_doffset; double delay_time, precision, mono_doffset, net_correction;
int updated_timestamps; int updated_timestamps;
/* ==================== */ /* ==================== */
@ -1934,6 +1985,7 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
stats = SRC_GetSourcestats(inst->source); stats = SRC_GetSourcestats(inst->source);
ef_mono_root = NULL; ef_mono_root = NULL;
ef_net_correction = NULL;
/* Find requested non-authentication extension fields */ /* Find requested non-authentication extension fields */
if (inst->ext_field_flags & info->ext_field_flags) { if (inst->ext_field_flags & info->ext_field_flags) {
@ -1949,6 +2001,12 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
NTP_EF_EXP_MONO_ROOT_MAGIC)) NTP_EF_EXP_MONO_ROOT_MAGIC))
ef_mono_root = ef_body; ef_mono_root = ef_body;
break; break;
case NTP_EF_EXP_NET_CORRECTION:
if (inst->ext_field_flags & NTP_EF_FLAG_EXP_NET_CORRECTION &&
is_exp_ef(ef_body, ef_body_length, sizeof (*ef_net_correction),
NTP_EF_EXP_NET_CORRECTION_MAGIC))
ef_net_correction = ef_body;
break;
} }
} }
} }
@ -2055,6 +2113,12 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
mono_doffset = 0.0; mono_doffset = 0.0;
} }
if (ef_net_correction) {
net_correction = UTI_Ntp64ToDouble(&ef_net_correction->correction);
} else {
net_correction = 0.0;
}
/* Select remote and local timestamps for the new sample */ /* Select remote and local timestamps for the new sample */
if (interleaved_packet) { if (interleaved_packet) {
/* Prefer previous local TX and remote RX timestamps if it will make /* Prefer previous local TX and remote RX timestamps if it will make
@ -2074,6 +2138,7 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
UTI_Ntp64ToTimespec(&message->receive_ts, &remote_receive); UTI_Ntp64ToTimespec(&message->receive_ts, &remote_receive);
UTI_Ntp64ToTimespec(&inst->remote_ntp_rx, &remote_request_receive); UTI_Ntp64ToTimespec(&inst->remote_ntp_rx, &remote_request_receive);
local_transmit = inst->local_tx; local_transmit = inst->local_tx;
local_transmit.net_correction = net_correction;
root_delay = MAX(pkt_root_delay, inst->remote_root_delay); root_delay = MAX(pkt_root_delay, inst->remote_root_delay);
root_dispersion = MAX(pkt_root_dispersion, inst->remote_root_dispersion); root_dispersion = MAX(pkt_root_dispersion, inst->remote_root_dispersion);
} }
@ -2088,6 +2153,7 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
remote_request_receive = remote_receive; remote_request_receive = remote_receive;
local_receive = *rx_ts; local_receive = *rx_ts;
local_transmit = inst->local_tx; local_transmit = inst->local_tx;
local_transmit.net_correction = net_correction;
root_delay = pkt_root_delay; root_delay = pkt_root_delay;
root_dispersion = pkt_root_dispersion; root_dispersion = pkt_root_dispersion;
} }
@ -2132,6 +2198,9 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
sample.root_delay = root_delay + sample.peer_delay; sample.root_delay = root_delay + sample.peer_delay;
sample.root_dispersion = root_dispersion + sample.peer_dispersion; sample.root_dispersion = root_dispersion + sample.peer_dispersion;
/* Apply corrections from PTP transparent clocks if available and sane */
apply_net_correction(&sample, &local_receive, &local_transmit, precision);
/* If the source is an active peer, this is the minimum assumed interval /* If the source is an active peer, this is the minimum assumed interval
between previous two transmissions (if not constrained by minpoll) */ between previous two transmissions (if not constrained by minpoll) */
prev_remote_poll_interval = UTI_Log2ToDouble(MIN(inst->remote_poll, prev_remote_poll_interval = UTI_Log2ToDouble(MIN(inst->remote_poll,
@ -2186,6 +2255,7 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
sample.root_delay = sample.root_dispersion = 0.0; sample.root_delay = sample.root_dispersion = 0.0;
sample.time = rx_ts->ts; sample.time = rx_ts->ts;
mono_doffset = 0.0; mono_doffset = 0.0;
net_correction = 0.0;
local_receive = *rx_ts; local_receive = *rx_ts;
local_transmit = inst->local_tx; local_transmit = inst->local_tx;
testA = testB = testC = testD = 0; testA = testB = testC = testD = 0;
@ -2229,6 +2299,8 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
inst->mono_doffset = 0.0; inst->mono_doffset = 0.0;
} }
inst->local_tx.net_correction = net_correction;
/* Don't use the same set of timestamps for the next sample */ /* Don't use the same set of timestamps for the next sample */
if (interleaved_packet) if (interleaved_packet)
inst->prev_local_tx = inst->local_tx; inst->prev_local_tx = inst->local_tx;

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" \ "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" \
"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" \

106
test/simulation/142-ntpoverptp Executable file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env bash
. ./test.common
test_start "NTP over PTP"
# Block communication between 3 and 1
base_delay="(+ 1e-4 (* -1 (equal 0.1 from 3) (equal 0.1 to 1)))"
cat > tmp/peer.keys <<-EOF
1 MD5 1234567890
EOF
clients=2
peers=2
max_sync_time=420
server_conf="
ptpport 319"
client_conf="
ptpport 319
authselectmode ignore
keyfile tmp/peer.keys"
client_server_options="minpoll 6 maxpoll 6 port 319"
client_peer_options="minpoll 6 maxpoll 6 port 319 key 1"
run_test || test_fail
check_chronyd_exit || test_fail
check_source_selection || test_fail
check_sync || test_fail
check_file_messages " 2 1 .* 319 319 1 96 " 150 160 \
log.packets || test_fail
check_file_messages " 1 2 .* 319 319 1 96 " 150 160 \
log.packets || test_fail
check_file_messages " 2 3 .* 319 319 1 116 " 150 160 \
log.packets || test_fail
check_file_messages " 3 2 .* 319 319 1 116 " 150 160 \
log.packets || test_fail
check_config_h 'HAVE_LINUX_TIMESTAMPING 1' || test_skip
export CLKNETSIM_TIMESTAMPING=2
export CLKNETSIM_LINK_SPEED=100
client_server_options+=" extfield F324 minpoll 0 maxpoll 0"
client_peer_options+=" extfield F324 minpoll 0 maxpoll 0 maxdelaydevratio 1e6"
server_conf+="
clockprecision 1e-9
hwtimestamp eth0"
client_conf+="
clockprecision 1e-9
hwtimestamp eth0"
delay_correction="(+ delay (* -8e-8 (+ length 46)))"
wander=1e-9
limit=1000
freq_offset=-1e-4
min_sync_time=5
max_sync_time=20
time_max_limit=1e-7
time_rms_limit=2e-8
freq_max_limit=1e-7
freq_rms_limit=5e-8
client_chronyd_options="-d"
run_test || test_fail
check_chronyd_exit || test_fail
check_source_selection || test_fail
check_sync || test_fail
if check_config_h 'FEAT_DEBUG 1'; then
check_log_messages "apply_net_correction.*Applied" 900 2100 || test_fail
check_log_messages "apply_net_correction.*Invalid" 0 4 || test_fail
fi
client_server_options+=" xleave"
client_peer_options+=" xleave"
run_test || test_fail
check_chronyd_exit || test_fail
check_source_selection || test_fail
check_sync || test_fail
if check_config_h 'FEAT_DEBUG 1'; then
check_log_messages "apply_net_correction.*Applied" 900 2100 || test_fail
check_log_messages "apply_net_correction.*Invalid" 0 4 || test_fail
freq_offset=0.0
delay_correction="(+ -1.0e-9 (* 1.0001 delay))"
run_test || test_fail
check_chronyd_exit || test_fail
check_log_messages "apply_net_correction.*Applied" 350 1400 || test_fail
check_log_messages "apply_net_correction.*Invalid" 350 1400 || test_fail
server_conf="ptpport 319"
client_conf="ptpport 319"
run_test || test_fail
check_chronyd_exit || test_fail
check_log_messages "apply_net_correction.*Applied" 0 0 || test_fail
fi
test_pass

View file

@ -1,41 +0,0 @@
#!/usr/bin/env bash
. ./test.common
test_start "PTP port"
# Block communication between 3 and 1
base_delay="(+ 1e-4 (* -1 (equal 0.1 from 3) (equal 0.1 to 1)))"
cat > tmp/peer.keys <<-EOF
1 MD5 1234567890
EOF
clients=2
peers=2
max_sync_time=420
server_conf="
ptpport 319"
client_conf="
ptpport 319
authselectmode ignore
keyfile tmp/peer.keys"
client_server_options="minpoll 6 maxpoll 6 port 319"
client_peer_options="minpoll 6 maxpoll 6 port 319 key 1"
run_test || test_fail
check_chronyd_exit || test_fail
check_source_selection || test_fail
check_sync || test_fail
check_file_messages " 2 1 .* 319 319 1 96 " 150 160 \
log.packets || test_fail
check_file_messages " 1 2 .* 319 319 1 96 " 150 160 \
log.packets || test_fail
check_file_messages " 2 3 .* 319 319 1 116 " 150 160 \
log.packets || test_fail
check_file_messages " 3 2 .* 319 319 1 116 " 150 160 \
log.packets || test_fail
test_pass

View file

@ -31,6 +31,7 @@ default_primary_time_offset=0.0
default_time_offset=1e-1 default_time_offset=1e-1
default_freq_offset=1e-4 default_freq_offset=1e-4
default_base_delay=1e-4 default_base_delay=1e-4
default_delay_correction=""
default_jitter=1e-4 default_jitter=1e-4
default_jitter_asymmetry=0.0 default_jitter_asymmetry=0.0
default_wander=1e-9 default_wander=1e-9
@ -460,6 +461,10 @@ run_test() {
for j in $(seq 1 $nodes); do for j in $(seq 1 $nodes); do
echo "node${i}_delay${j} = $(get_delay_expr up)" echo "node${i}_delay${j} = $(get_delay_expr up)"
echo "node${j}_delay${i} = $(get_delay_expr down)" echo "node${j}_delay${i} = $(get_delay_expr down)"
if [ -n "$delay_correction" ]; then
echo "node${i}_delay_correction${j} = $delay_correction"
echo "node${j}_delay_correction${i} = $delay_correction"
fi
done done
done > tmp/conf done > tmp/conf