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;