From be3158c4e5b2a88b199d5dce172e0bbb327a7dab Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Wed, 18 Aug 2021 12:42:07 +0200 Subject: [PATCH] ntp: add support for NTP over PTP Allow NTP messages to be exchanged as a payload of PTP messages to enable full hardware timestamping on NICs that can timestamp PTP packets only. Implemented is the protocol described in this draft (version 00): https://datatracker.ietf.org/doc/draft-mlichvar-ntp-over-ptp/ This is an experimental feature. It can be changed or removed in future. The used PTP domain is 123 and the NTP TLV type is 0x2023 from the "do not propagate" experimental range. The ptpport directive enables NTP-over-PTP as a server and as a client for all sources that have the port option set to the PTP port. The port should be the PTP event port (319) to trigger timestamping in the hardware. The implementation is contained to ntp_io. It is transparent to ntp_core. --- conf.c | 13 ++++ conf.h | 2 + doc/chrony.conf.adoc | 42 ++++++++++-- ntp_io.c | 154 +++++++++++++++++++++++++++++++++++++++---- ntp_io.h | 4 ++ ntp_io_linux.c | 5 +- ptp.h | 64 ++++++++++++++++++ socket.c | 2 + 8 files changed, 264 insertions(+), 22 deletions(-) create mode 100644 ptp.h diff --git a/conf.c b/conf.c index a525278..93e93fc 100644 --- a/conf.c +++ b/conf.c @@ -273,6 +273,9 @@ static int no_system_cert = 0; /* Array of CNF_HwTsInterface */ static ARR_Instance hwts_interfaces; +/* PTP event port (disabled by default) */ +static int ptp_port = 0; + typedef struct { NTP_Source_Type type; int pool; @@ -686,6 +689,8 @@ CNF_ParseLine(const char *filename, int number, char *line) parse_source(p, command, 1); } else if (!strcasecmp(command, "port")) { parse_int(p, &ntp_port); + } else if (!strcasecmp(command, "ptpport")) { + parse_int(p, &ptp_port); } else if (!strcasecmp(command, "ratelimit")) { parse_ratelimit(p, &ntp_ratelimit_enabled, &ntp_ratelimit_interval, &ntp_ratelimit_burst, &ntp_ratelimit_leak); @@ -2561,6 +2566,14 @@ CNF_GetHwTsInterface(unsigned int index, CNF_HwTsInterface **iface) /* ================================================== */ +int +CNF_GetPtpPort(void) +{ + return ptp_port; +} + +/* ================================================== */ + char * CNF_GetNtsDumpDir(void) { diff --git a/conf.h b/conf.h index 437fa96..f81f9aa 100644 --- a/conf.h +++ b/conf.h @@ -152,6 +152,8 @@ typedef struct { extern int CNF_GetHwTsInterface(unsigned int index, CNF_HwTsInterface **iface); +extern int CNF_GetPtpPort(void); + extern char *CNF_GetNtsDumpDir(void); extern char *CNF_GetNtsNtpServer(void); extern int CNF_GetNtsServerCertAndKeyFiles(const char ***certs, const char ***keys); diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc index 647270b..f6d26c6 100644 --- a/doc/chrony.conf.adoc +++ b/doc/chrony.conf.adoc @@ -2365,7 +2365,7 @@ timestamping, which can be verified with the *ethtool -T* command. The list of capabilities should include _SOF_TIMESTAMPING_RAW_HARDWARE_, _SOF_TIMESTAMPING_TX_HARDWARE_, and _SOF_TIMESTAMPING_RX_HARDWARE_. Receive filter _HWTSTAMP_FILTER_ALL_, or _HWTSTAMP_FILTER_NTP_ALL_, is necessary for -timestamping of received packets. Timestamping of packets received from bridged +timestamping of received NTP packets. Timestamping of packets received on bridged and bonded interfaces is supported on Linux 4.13 and newer. When *chronyd* is running, no other process (e.g. a PTP daemon) should be working with the NIC clock. @@ -2421,12 +2421,14 @@ Enables timestamping of received PTP packets. _none_:::: Disables timestamping of received packets. {blank}::: -The most specific filter for timestamping NTP packets which is supported by the -NIC is selected by default. Some NICs can timestamp only PTP packets, which -limits the selection to the _none_ filter. Forcing timestamping of all packets -with the _all_ filter when the NIC supports both _all_ and _ntp_ filters can be -useful when packets are received from or on a non-standard UDP port (e.g. -specified by the *port* directive). +The most specific filter for timestamping of NTP packets supported by the NIC +is selected by default. Some NICs can timestamp PTP packets only. By default, +they will be configured with the _none_ filter and expected to provide hardware +timestamps for transmitted packets only. Timestamping of PTP packets is useful +with NTP-over-PTP enabled by the <> +directive. Forcing timestamping of all packets with the _all_ filter could be +useful if the NIC supported both the _all_ and _ntp_ filters, and it should +timestamp both NTP and PTP packets, or NTP packets on a different UDP port. {blank}:: + Examples of the directive are: @@ -2519,6 +2521,32 @@ e.g.: pidfile /run/chronyd.pid ---- +[[ptpport]]*ptpport* _port_:: +The *ptpport* directive enables *chronyd* to send and receive NTP messages +contained in PTP event messages (NTP-over-PTP) to enable hardware timestamping +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 +value is 0 (disabled). ++ +The NTP-over-PTP support is experimental. The protocol and configuration can +change in future. It should be used only in local networks and expected to work +only between servers and clients running the same version of *chronyd*. ++ +The PTP port will be open even if *chronyd* is not configured to operate as a +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 +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 +*rxfilter* option of the *hwtimestamp* directive needs to be set to _ptp_. ++ +An example of client configuration is: ++ +---- +server foo.example.net port 319 +hwtimestamp * rxfilter ptp +ptpport 319 +---- + [[sched_priority]]*sched_priority* _priority_:: On Linux, FreeBSD, NetBSD, and Solaris, the *sched_priority* directive will select the SCHED_FIFO real-time scheduler at the specified priority (which must diff --git a/ntp_io.c b/ntp_io.c index 1bf8ba4..077a7cf 100644 --- a/ntp_io.c +++ b/ntp_io.c @@ -30,9 +30,11 @@ #include "sysincl.h" +#include "memory.h" #include "ntp_io.h" #include "ntp_core.h" #include "ntp_sources.h" +#include "ptp.h" #include "sched.h" #include "socket.h" #include "local.h" @@ -70,6 +72,16 @@ static int permanent_server_sockets; /* Flag indicating the server IPv4 socket is bound to an address */ static int bound_server_sock_fd4; +/* PTP event port, or 0 if disabled */ +static int ptp_port; + +/* Shared server/client sockets for NTP-over-PTP */ +static int ptp_sock_fd4; +static int ptp_sock_fd6; + +/* Buffer for transmitted NTP-over-PTP messages */ +static PTP_NtpMessage *ptp_message; + /* Flag indicating that we have been initialised */ static int initialised=0; @@ -221,6 +233,17 @@ NIO_Initialise(void) client_sock_fd4 == INVALID_SOCK_FD && client_sock_fd6 == INVALID_SOCK_FD)) { LOG_FATAL("Could not open NTP sockets"); } + + ptp_port = CNF_GetPtpPort(); + ptp_sock_fd4 = INVALID_SOCK_FD; + ptp_sock_fd6 = INVALID_SOCK_FD; + ptp_message = NULL; + + if (ptp_port > 0) { + ptp_sock_fd4 = open_socket(IPADDR_INET4, ptp_port, 0, NULL); + ptp_sock_fd6 = open_socket(IPADDR_INET6, ptp_port, 0, NULL); + ptp_message = MallocNew(PTP_NtpMessage); + } } /* ================================================== */ @@ -238,6 +261,11 @@ NIO_Finalise(void) close_socket(server_sock_fd6); server_sock_fd6 = client_sock_fd6 = INVALID_SOCK_FD; + close_socket(ptp_sock_fd4); + close_socket(ptp_sock_fd6); + ptp_sock_fd4 = ptp_sock_fd6 = INVALID_SOCK_FD; + Free(ptp_message); + #ifdef HAVE_LINUX_TIMESTAMPING NIO_Linux_Finalise(); #endif @@ -250,17 +278,21 @@ NIO_Finalise(void) int NIO_OpenClientSocket(NTP_Remote_Address *remote_addr) { - if (separate_client_sockets) { - return open_separate_client_socket(remote_addr); - } else { - switch (remote_addr->ip_addr.family) { - case IPADDR_INET4: - return client_sock_fd4; - case IPADDR_INET6: - return client_sock_fd6; - default: - return INVALID_SOCK_FD; - } + switch (remote_addr->ip_addr.family) { + case IPADDR_INET4: + if (ptp_port > 0 && remote_addr->port == ptp_port) + return ptp_sock_fd4; + if (separate_client_sockets) + return open_separate_client_socket(remote_addr); + return client_sock_fd4; + case IPADDR_INET6: + if (ptp_port > 0 && remote_addr->port == ptp_port) + return ptp_sock_fd6; + if (separate_client_sockets) + return open_separate_client_socket(remote_addr); + return client_sock_fd6; + default: + return INVALID_SOCK_FD; } } @@ -271,6 +303,8 @@ NIO_OpenServerSocket(NTP_Remote_Address *remote_addr) { switch (remote_addr->ip_addr.family) { case IPADDR_INET4: + if (ptp_port > 0 && remote_addr->port == ptp_port) + return ptp_sock_fd4; if (permanent_server_sockets) return server_sock_fd4; if (server_sock_fd4 == INVALID_SOCK_FD) @@ -279,6 +313,8 @@ NIO_OpenServerSocket(NTP_Remote_Address *remote_addr) server_sock_ref4++; return server_sock_fd4; case IPADDR_INET6: + if (ptp_port > 0 && remote_addr->port == ptp_port) + return ptp_sock_fd6; if (permanent_server_sockets) return server_sock_fd6; if (server_sock_fd6 == INVALID_SOCK_FD) @@ -293,9 +329,20 @@ NIO_OpenServerSocket(NTP_Remote_Address *remote_addr) /* ================================================== */ +static int +is_ptp_socket(int sock_fd) +{ + return ptp_port > 0 && (sock_fd == ptp_sock_fd4 || sock_fd == ptp_sock_fd6); +} + +/* ================================================== */ + void NIO_CloseClientSocket(int sock_fd) { + if (is_ptp_socket(sock_fd)) + return; + if (separate_client_sockets) close_socket(sock_fd); } @@ -305,7 +352,7 @@ NIO_CloseClientSocket(int sock_fd) void NIO_CloseServerSocket(int sock_fd) { - if (permanent_server_sockets || sock_fd == INVALID_SOCK_FD) + if (permanent_server_sockets || sock_fd == INVALID_SOCK_FD || is_ptp_socket(sock_fd)) return; if (sock_fd == server_sock_fd4) { @@ -329,7 +376,7 @@ int NIO_IsServerSocket(int sock_fd) { return sock_fd != INVALID_SOCK_FD && - (sock_fd == server_sock_fd4 || sock_fd == server_sock_fd6); + (sock_fd == server_sock_fd4 || sock_fd == server_sock_fd6 || is_ptp_socket(sock_fd)); } /* ================================================== */ @@ -337,7 +384,8 @@ NIO_IsServerSocket(int sock_fd) int NIO_IsServerSocketOpen(void) { - return server_sock_fd4 != INVALID_SOCK_FD || server_sock_fd6 != INVALID_SOCK_FD; + return server_sock_fd4 != INVALID_SOCK_FD || server_sock_fd6 != INVALID_SOCK_FD || + ptp_sock_fd4 != INVALID_SOCK_FD || ptp_sock_fd6 != INVALID_SOCK_FD; } /* ================================================== */ @@ -392,6 +440,9 @@ process_message(SCK_Message *message, int sock_fd, int event) DEBUG_LOG("Updated RX timestamp delay=%.9f tss=%u", UTI_DiffTimespecsToDouble(&sched_ts, &local_ts.ts), local_ts.source); + if (!NIO_UnwrapMessage(message, sock_fd)) + return; + /* Just ignore the packet if it's not of a recognized length */ if (message->length < NTP_HEADER_LENGTH || message->length > sizeof (NTP_Packet)) { DEBUG_LOG("Unexpected length"); @@ -430,6 +481,78 @@ read_from_socket(int sock_fd, int event, void *anything) process_message(&messages[i], sock_fd, event); } +/* ================================================== */ + +int +NIO_UnwrapMessage(SCK_Message *message, int sock_fd) +{ + PTP_NtpMessage *msg; + + if (!is_ptp_socket(sock_fd)) + return 1; + + if (message->length <= PTP_NTP_PREFIX_LENGTH) { + DEBUG_LOG("Unexpected length"); + return 0; + } + + msg = message->data; + + if (msg->header.type != PTP_TYPE_DELAY_REQ || msg->header.version != PTP_VERSION || + ntohs(msg->header.length) != message->length || + msg->header.domain != PTP_DOMAIN_NTP || + ntohs(msg->header.flags) != PTP_FLAG_UNICAST || + ntohs(msg->tlv_header.type) != PTP_TLV_NTP || + ntohs(msg->tlv_header.length) != message->length - PTP_NTP_PREFIX_LENGTH) { + DEBUG_LOG("Unexpected PTP message"); + return 0; + } + + message->data = (char *)message->data + PTP_NTP_PREFIX_LENGTH; + message->length -= PTP_NTP_PREFIX_LENGTH; + + DEBUG_LOG("Unwrapped PTP->NTP len=%d", message->length); + + return 1; +} + +/* ================================================== */ + +static int +wrap_message(SCK_Message *message, int sock_fd) +{ + assert(PTP_NTP_PREFIX_LENGTH == 48); + + if (!is_ptp_socket(sock_fd)) + return 1; + + if (!ptp_message) + return 0; + + if (message->length < NTP_HEADER_LENGTH || + message->length + PTP_NTP_PREFIX_LENGTH > sizeof (*ptp_message)) { + DEBUG_LOG("Unexpected length"); + return 0; + } + + memset(ptp_message, 0, PTP_NTP_PREFIX_LENGTH); + ptp_message->header.type = PTP_TYPE_DELAY_REQ; + ptp_message->header.version = PTP_VERSION; + ptp_message->header.length = htons(PTP_NTP_PREFIX_LENGTH + message->length); + ptp_message->header.domain = PTP_DOMAIN_NTP; + ptp_message->header.flags = htons(PTP_FLAG_UNICAST); + ptp_message->tlv_header.type = htons(PTP_TLV_NTP); + ptp_message->tlv_header.length = htons(message->length); + memcpy((char *)ptp_message + PTP_NTP_PREFIX_LENGTH, message->data, message->length); + + message->data = ptp_message; + message->length += PTP_NTP_PREFIX_LENGTH; + + DEBUG_LOG("Wrapped NTP->PTP len=%d", message->length - PTP_NTP_PREFIX_LENGTH); + + return 1; +} + /* ================================================== */ /* Send a packet to remote address from local address */ @@ -451,6 +574,9 @@ NIO_SendPacket(NTP_Packet *packet, NTP_Remote_Address *remote_addr, message.data = packet; message.length = length; + if (!wrap_message(&message, local_addr->sock_fd)) + return 0; + /* Specify remote address if the socket is not connected */ if (NIO_IsServerSocket(local_addr->sock_fd) || !separate_client_sockets) { message.remote_addr.ip.ip_addr = remote_addr->ip_addr; diff --git a/ntp_io.h b/ntp_io.h index 9787ca1..19ffeed 100644 --- a/ntp_io.h +++ b/ntp_io.h @@ -31,6 +31,7 @@ #include "ntp.h" #include "addressing.h" +#include "socket.h" /* Function to initialise the module. */ extern void NIO_Initialise(void); @@ -59,6 +60,9 @@ extern int NIO_IsServerSocketOpen(void); /* Function to check if client packets can be sent to a server */ extern int NIO_IsServerConnectable(NTP_Remote_Address *remote_addr); +/* Function to unwrap an NTP message from non-native transport (e.g. PTP) */ +extern int NIO_UnwrapMessage(SCK_Message *message, int sock_fd); + /* Function to transmit a packet */ extern int NIO_SendPacket(NTP_Packet *packet, NTP_Remote_Address *remote_addr, NTP_Local_Address *local_addr, int length, int process_tx); diff --git a/ntp_io_linux.c b/ntp_io_linux.c index a458ded..ce19b86 100644 --- a/ntp_io_linux.c +++ b/ntp_io_linux.c @@ -784,7 +784,10 @@ NIO_Linux_ProcessMessage(SCK_Message *message, NTP_Local_Address *local_addr, return 1; } - if (message->length < NTP_HEADER_LENGTH) + if (!NIO_UnwrapMessage(message, local_addr->sock_fd)) + return 1; + + if (message->length < NTP_HEADER_LENGTH || message->length > sizeof (NTP_Packet)) return 1; NSR_ProcessTx(&message->remote_addr.ip, local_addr, local_ts, message->data, message->length); diff --git a/ptp.h b/ptp.h new file mode 100644 index 0000000..7a93590 --- /dev/null +++ b/ptp.h @@ -0,0 +1,64 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Miroslav Lichvar 2021 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + ********************************************************************** + + ======================================================================= + + This is the header file for the Precision Time Protocol (PTP). + + */ +#ifndef GOT_PTP_H +#define GOT_PTP_H + +#include "sysincl.h" + +#include "ntp.h" + +#define PTP_VERSION 2 +#define PTP_TYPE_DELAY_REQ 1 +#define PTP_DOMAIN_NTP 123 +#define PTP_FLAG_UNICAST (1 << (2 + 8)) +#define PTP_TLV_NTP 0x2023 + +typedef struct { + uint8_t type; + uint8_t version; + uint16_t length; + uint8_t domain; + uint8_t min_sdoid; + uint16_t flags; + uint8_t rest[26]; +} PTP_Header; + +typedef struct { + uint16_t type; + uint16_t length; +} PTP_TlvHeader; + +typedef struct { + PTP_Header header; + uint8_t origin_ts[10]; + PTP_TlvHeader tlv_header; + NTP_Packet ntp_msg; +} PTP_NtpMessage; + +#define PTP_NTP_PREFIX_LENGTH (int)offsetof(PTP_NtpMessage, ntp_msg) + +#endif diff --git a/socket.c b/socket.c index 7a9c85e..a6010d8 100644 --- a/socket.c +++ b/socket.c @@ -40,6 +40,7 @@ #include "array.h" #include "logging.h" #include "privops.h" +#include "ptp.h" #include "util.h" #define INVALID_SOCK_FD (-4) @@ -60,6 +61,7 @@ struct Message { /* Buffer of sufficient length for all expected messages */ union { NTP_Packet ntp_msg; + PTP_NtpMessage ptp_msg; CMD_Request cmd_request; CMD_Reply cmd_reply; } msg_buf;