Hi folks,

Please consider this patch to add support for using NTP and NTS-KE server
sockets passed to chrony from the systemd service manager.

For context, we're currently testing out this patch as part of moving
chrony to our tubular system for routing client traffic to services (
https://blog.cloudflare.com/tubular-fixing-the-socket-api-with-ebpf/).

Aside from our specific use case, socket activation is quite common in
services (see the list at
https://www.redhat.com/sysadmin/socket-activation-podman) and we hope that
others may benefit from the patch as well. More info on socket activation
is available from the links at the bottom of
https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html.

The patch only adds support for socket activation for the IPv4 and IPv6 NTP
and NTS-KE server sockets since those are relevant for our use case.
However, adding socket activation for the command socket is possible as
well.

The patch is also available for review convenience in this PR:
https://github.com/lukevalenta/chrony/pull/1. Any feedback on the patch is
welcome and appreciated!

Thanks,
Luke

---
>From 499ebf2e438393e4c95912c8eb277d615219e222 Mon Sep 17 00:00:00 2001
From: Luke Valenta <lvale...@cloudflare.com>
Date: Wed, 11 Oct 2023 13:51:39 -0400
Subject: [PATCH] Add support for systemd sockets

Check for file descriptors passed by the service manager as part of
systemd socket activation. If any of the following file descriptor names
are provided then chronyd will attempt to use the corresponding socket
instead of creating a new one.

ntp4: IPv4 NTP server (DGRAM socket bound to IPv4 *bindaddress* and *port*)
ntp6: IPv6 NTP server (DGRAM socket bound to IPv6 *bindaddress* and *port*)
nts4: IPv4 NTS-KE server (STREAM socket bound to IPv4 *bindaddress* and
*ntsport*)
nts6: IPv6 NTS-KE server (STREAM socket bound to IPv6 *bindaddress* and
*ntsport*)

Aside from IPV6_V6ONLY (which cannot be set on already-bound sockets),
all socket options are set in the daemon, overwriting any options that
may be configured in the systemd socket unit.

Unit tests test the correct parsing of the LISTEN_FDS and LISTEN_FDNAMES
environment variables.

System tests test socket activation functionality using
the test tool systemd-socket-activate (which has some limitations
requiring workarounds, discussed in test/system/011-systemd).
---
 doc/chrony.conf.adoc    |  43 ++++++++++
 doc/faq.adoc            |  11 +++
 main.c                  |   5 +-
 ntp_io.c                |  37 +++++----
 nts_ke_server.c         |  35 ++++----
 socket.c                | 180 ++++++++++++++++++++++++++++++++++++++--
 socket.h                |  11 +++
 test/system/011-systemd | 129 ++++++++++++++++++++++++++++
 test/system/test.common |  16 ++++
 test/unit/socket.c      |  60 ++++++++++++++
 10 files changed, 487 insertions(+), 40 deletions(-)
 create mode 100755 test/system/011-systemd
 create mode 100644 test/unit/socket.c

diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc
index 4af870d..e47332a 100644
--- a/doc/chrony.conf.adoc
+++ b/doc/chrony.conf.adoc
@@ -1534,6 +1534,49 @@ bindaddress 192.168.1.1
 Currently, for each of the IPv4 and IPv6 protocols, only one *bindaddress*
 directive can be specified. Therefore, it is not useful on computers which
 should serve NTP on multiple network interfaces.
++
+Before creating new server sockets, the chronyd daemon will check for file
+descriptors passed by the service manager as part of
+
https://www.freedesktop.org/software/systemd/man/latest/sd_listen_fds.html[systemd
socket activation].
+File descriptors to sockets that do not match corresponding directives in
+_chrony.conf_ will be ignored.  Currently, the following four named file
+descriptors are supported:
++
+----
+ntp4: IPv4 NTP server (DGRAM socket bound to IPv4 *bindaddress* and *port*)
+ntp6: IPv6 NTP server (DGRAM socket bound to IPv6 *bindaddress* and *port*)
+nts4: IPv4 NTS-KE server (STREAM socket bound to IPv4 *bindaddress* and
*ntsport*)
+nts6: IPv6 NTS-KE server (STREAM socket bound to IPv6 *bindaddress* and
*ntsport*)
+----
++
+An example systemd socket unit for the IPv4 NTP server is below, where
+*bindaddress* and *port* correspond to directives in _chrony.conf_.
++
+----
+[Unit]
+chronyd socket for IPv4 NTP server
+[Socket]
+FileDescriptorName=ntp4
+Service=chronyd.service
+ListenDatagram=*bindaddress*:*port*
+[Install]
+WantedBy=sockets.target
+----
++
+An example systemd socket unit for the IPv6 NTS-KE server is below, where
+*bindaddress* and *ntsport* correspond to directives in _chrony.conf_.
++
+----
+[Unit]
+chronyd socket for IPv6 NTS-KE server
+[Socket]
+FileDescriptorName=nts6
+Service=chronyd.service
+ListenStream=*bindaddress*:*port*
+BindIPv6Only=ipv6-only
+[Install]
+WantedBy=sockets.target
+----

 [[binddevice]]*binddevice* _interface_::
 The *binddevice* directive binds the NTP and NTS-KE server sockets to a
network
diff --git a/doc/faq.adoc b/doc/faq.adoc
index 57347c3..4873901 100644
--- a/doc/faq.adoc
+++ b/doc/faq.adoc
@@ -497,6 +497,17 @@ pidfile /var/run/chronyd-server1.pid
 driftfile /var/lib/chrony/drift-server1
 ----

+=== Does `chronyd` support systemd socket activation?
+
+Yes, for the NTP and NTS-KE server sockets.
+
+With socket activation, the service manager (systemd) creates sockets and
+passes file descriptors to them to the process instead of requiring the
process
+to create new sockets on startup. This allows for zero-downtime service
+upgrades, more parallelization, and simplified dependency logic at boot.
+
+See the *bindaddress* documention in _chrony.conf_ for more details.
+
 === Should be a leap smear enabled on NTP server?

 With the `smoothtime` and `leapsecmode` directives it is possible to
enable a
diff --git a/main.c b/main.c
index 3233707..c71c9dc 100644
--- a/main.c
+++ b/main.c
@@ -361,6 +361,7 @@ go_daemon(void)
       exit(0);
     } else {
       /* In the child we want to leave running as the daemon */
+      int n = SCK_GetSystemdListenFdCount();

       /* Change current directory to / */
       if (chdir("/") < 0) {
@@ -368,9 +369,9 @@ go_daemon(void)
       }

       /* Don't keep stdin/out/err from before. But don't close
-         the parent pipe yet. */
+         the parent pipe yet, or file descriptors from systemd. */
       for (fd=0; fd<1024; fd++) {
-        if (fd != pipefd[1])
+        if (fd != pipefd[1] && !(n > 0 && fd >= SD_LISTEN_FDS_START && fd
< SD_LISTEN_FDS_START + n))
           close(fd);
       }

diff --git a/ntp_io.c b/ntp_io.c
index fce7b17..ab4a4a8 100644
--- a/ntp_io.c
+++ b/ntp_io.c
@@ -93,7 +93,7 @@ static void read_from_socket(int sock_fd, int event, void
*anything);
 /* ================================================== */

 static int
-open_socket(int family, int local_port, int client_only, IPSockAddr
*remote_addr)
+open_socket(int family, int local_port, int client_only, IPSockAddr
*remote_addr, char *name)
 {
   int sock_fd, sock_flags, dscp, events = SCH_FILE_INPUT;
   IPSockAddr local_addr;
@@ -116,11 +116,18 @@ open_socket(int family, int local_port, int
client_only, IPSockAddr *remote_addr
   if (!client_only)
     sock_flags |= SCK_FLAG_BROADCAST;

-  sock_fd = SCK_OpenUdpSocket(remote_addr, &local_addr, iface, sock_flags);
-  if (sock_fd < 0) {
-    if (!client_only)
-      LOG(LOGS_ERR, "Could not open NTP socket on %s",
UTI_IPSockAddrToString(&local_addr));
-    return INVALID_SOCK_FD;
+  /* Attempt to retrieve socket file descriptor from service manager */
+  sock_fd = SCK_GetSystemdSocket(name, SOCK_DGRAM, &local_addr, iface,
sock_flags);
+
+  if (sock_fd > 0) {
+    LOG(LOGS_INFO, "Using NTP socket from systemd : fd=%d, name=%s",
sock_fd, name);
+  } else {
+    sock_fd = SCK_OpenUdpSocket(remote_addr, &local_addr, iface,
sock_flags);
+    if (sock_fd < 0) {
+      if (!client_only)
+        LOG(LOGS_ERR, "Could not open NTP socket on %s",
UTI_IPSockAddrToString(&local_addr));
+      return INVALID_SOCK_FD;
+    }
   }

   dscp = CNF_GetNtpDscp();
@@ -158,7 +165,7 @@ open_socket(int family, int local_port, int
client_only, IPSockAddr *remote_addr
 static int
 open_separate_client_socket(IPSockAddr *remote_addr)
 {
-  return open_socket(remote_addr->ip_addr.family, 0, 1, remote_addr);
+  return open_socket(remote_addr->ip_addr.family, 0, 1, remote_addr, NULL);
 }

 /* ================================================== */
@@ -216,14 +223,14 @@ NIO_Initialise(void)
   server_sock_ref6 = 0;

   if (permanent_server_sockets && server_port) {
-    server_sock_fd4 = open_socket(IPADDR_INET4, server_port, 0, NULL);
-    server_sock_fd6 = open_socket(IPADDR_INET6, server_port, 0, NULL);
+    server_sock_fd4 = open_socket(IPADDR_INET4, server_port, 0, NULL,
"ntp4");
+    server_sock_fd6 = open_socket(IPADDR_INET6, server_port, 0, NULL,
"ntp6");
   }

   if (!separate_client_sockets) {
     if (client_port != server_port || !server_port) {
-      client_sock_fd4 = open_socket(IPADDR_INET4, client_port, 1, NULL);
-      client_sock_fd6 = open_socket(IPADDR_INET6, client_port, 1, NULL);
+      client_sock_fd4 = open_socket(IPADDR_INET4, client_port, 1, NULL,
NULL);
+      client_sock_fd6 = open_socket(IPADDR_INET6, client_port, 1, NULL,
NULL);
     } else {
       client_sock_fd4 = server_sock_fd4;
       client_sock_fd6 = server_sock_fd6;
@@ -243,8 +250,8 @@ NIO_Initialise(void)
   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_sock_fd4 = open_socket(IPADDR_INET4, ptp_port, 0, NULL, NULL);
+    ptp_sock_fd6 = open_socket(IPADDR_INET6, ptp_port, 0, NULL, NULL);
     ptp_message = MallocNew(PTP_NtpMessage);
   }
 }
@@ -323,7 +330,7 @@ NIO_OpenServerSocket(NTP_Remote_Address *remote_addr)
       if (permanent_server_sockets)
         return server_sock_fd4;
       if (server_sock_fd4 == INVALID_SOCK_FD)
-        server_sock_fd4 = open_socket(IPADDR_INET4, CNF_GetNTPPort(), 0,
NULL);
+        server_sock_fd4 = open_socket(IPADDR_INET4, CNF_GetNTPPort(), 0,
NULL, "ntp4");
       if (server_sock_fd4 != INVALID_SOCK_FD)
         server_sock_ref4++;
       return server_sock_fd4;
@@ -333,7 +340,7 @@ NIO_OpenServerSocket(NTP_Remote_Address *remote_addr)
       if (permanent_server_sockets)
         return server_sock_fd6;
       if (server_sock_fd6 == INVALID_SOCK_FD)
-        server_sock_fd6 = open_socket(IPADDR_INET6, CNF_GetNTPPort(), 0,
NULL);
+        server_sock_fd6 = open_socket(IPADDR_INET6, CNF_GetNTPPort(), 0,
NULL, "ntp6");
       if (server_sock_fd6 != INVALID_SOCK_FD)
         server_sock_ref6++;
       return server_sock_fd6;
diff --git a/nts_ke_server.c b/nts_ke_server.c
index 5e25c50..fcfceb5 100644
--- a/nts_ke_server.c
+++ b/nts_ke_server.c
@@ -293,7 +293,7 @@ accept_connection(int listening_fd, int event, void
*arg)
 /* ================================================== */

 static int
-open_socket(int family)
+open_socket(int family, char *name)
 {
   IPSockAddr local_addr;
   int backlog, sock_fd;
@@ -306,19 +306,26 @@ open_socket(int family)
   local_addr.port = CNF_GetNtsServerPort();
   iface = CNF_GetBindNtpInterface();

-  sock_fd = SCK_OpenTcpSocket(NULL, &local_addr, iface, 0);
-  if (sock_fd < 0) {
-    LOG(LOGS_ERR, "Could not open NTS-KE socket on %s",
UTI_IPSockAddrToString(&local_addr));
-    return INVALID_SOCK_FD;
-  }
+  /* Attempt to retrieve socket file descriptor from service manager */
+  sock_fd = SCK_GetSystemdSocket(name, SOCK_STREAM, &local_addr, iface, 0);
+
+  if (sock_fd > 0) {
+    LOG(LOGS_INFO, "Using NTS-KE socket from systemd : fd=%d, name=%s",
sock_fd, name);
+  } else {
+    sock_fd = SCK_OpenTcpSocket(NULL, &local_addr, iface, 0);
+    if (sock_fd < 0) {
+      LOG(LOGS_ERR, "Could not open NTS-KE socket on %s",
UTI_IPSockAddrToString(&local_addr));
+      return INVALID_SOCK_FD;
+    }

-  /* Set the maximum number of waiting connections on the socket to the
maximum
-     number of concurrent sessions */
-  backlog = MAX(CNF_GetNtsServerProcesses(), 1) *
CNF_GetNtsServerConnections();
+    /* Set the maximum number of waiting connections on the socket to the
maximum
+       number of concurrent sessions */
+    backlog = MAX(CNF_GetNtsServerProcesses(), 1) *
CNF_GetNtsServerConnections();

-  if (!SCK_ListenOnSocket(sock_fd, backlog)) {
-    SCK_CloseSocket(sock_fd);
-    return INVALID_SOCK_FD;
+    if (!SCK_ListenOnSocket(sock_fd, backlog)) {
+      SCK_CloseSocket(sock_fd);
+      return INVALID_SOCK_FD;
+    }
   }

   SCH_AddFileHandler(sock_fd, SCH_FILE_INPUT, accept_connection, NULL);
@@ -811,8 +818,8 @@ NKS_Initialise(void)
   current_server_key = MAX_SERVER_KEYS - 1;

   if (!is_helper) {
-    server_sock_fd4 = open_socket(IPADDR_INET4);
-    server_sock_fd6 = open_socket(IPADDR_INET6);
+    server_sock_fd4 = open_socket(IPADDR_INET4, "nts4");
+    server_sock_fd6 = open_socket(IPADDR_INET6, "nts6");

     key_rotation_interval = MAX(CNF_GetNtsRotate(), 0);

diff --git a/socket.c b/socket.c
index aa060a8..ebfe596 100644
--- a/socket.c
+++ b/socket.c
@@ -214,7 +214,7 @@ set_socket_flags(int sock_fd, int flags)
   /* Close the socket automatically on exec */
   if (
 #ifdef SOCK_CLOEXEC
-      (supported_socket_flags & SOCK_CLOEXEC) == 0 &&
+      (flags & SCK_FLAG_SYSTEMD || (supported_socket_flags & SOCK_CLOEXEC)
== 0) &&
 #endif
       !UTI_FdSetCloexec(sock_fd))
     return 0;
@@ -222,7 +222,7 @@ set_socket_flags(int sock_fd, int flags)
   /* Enable non-blocking mode */
   if ((flags & SCK_FLAG_BLOCK) == 0 &&
 #ifdef SOCK_NONBLOCK
-      (supported_socket_flags & SOCK_NONBLOCK) == 0 &&
+      (flags & SCK_FLAG_SYSTEMD || (supported_socket_flags &
SOCK_NONBLOCK) == 0) &&
 #endif
       !set_socket_nonblock(sock_fd))
     return 0;
@@ -296,8 +296,15 @@ set_ip_options(int sock_fd, int family, int flags)
 {
 #if defined(FEAT_IPV6) && defined(IPV6_V6ONLY)
   /* Receive only IPv6 packets on an IPv6 socket */
-  if (family == IPADDR_INET6 && !SCK_SetIntOption(sock_fd, IPPROTO_IPV6,
IPV6_V6ONLY, 1))
-    return 0;
+  if (family == IPADDR_INET6) {
+    /* Only check if the option is set for systemd sockets as we cannot set
+     * IPV6_V6ONLY when the socket is already bound. */
+    int opt;
+    if (flags & SCK_FLAG_SYSTEMD && (!SCK_GetIntOption(sock_fd,
IPPROTO_IPV6, IPV6_V6ONLY, &opt) || opt != 1))
+      return 0;
+    else if (!SCK_SetIntOption(sock_fd, IPPROTO_IPV6, IPV6_V6ONLY, 1))
+      return 0;
+  }
 #endif

   /* Provide destination address of received packets if requested */
@@ -361,12 +368,8 @@ bind_device(int sock_fd, const char *iface)
 /* ================================================== */

 static int
-bind_ip_address(int sock_fd, IPSockAddr *addr, int flags)
+set_bind_options(int sock_fd, IPSockAddr *addr)
 {
-  union sockaddr_all saddr;
-  socklen_t saddr_len;
-  int s;
-
   /* Make the socket capable of re-using an old address if binding to a
specific port */
   if (addr->port > 0 && !SCK_SetIntOption(sock_fd, SOL_SOCKET,
SO_REUSEADDR, 1))
     ;
@@ -385,6 +388,71 @@ bind_ip_address(int sock_fd, IPSockAddr *addr, int
flags)
     ;
 #endif

+  return 1;
+}
+
+/* ================================================== */
+
+static int
+check_ip_bind_address(int fd, int type, IPSockAddr *addr)
+{
+  int opt;
+  socklen_t l;
+  union sockaddr_all saddr;
+  IPSockAddr ip_sa;
+
+  /* Check that chrony configured address is valid */
+  if (!addr || addr->port == 0 ||
+          (addr->ip_addr.family != IPADDR_INET4 && addr->ip_addr.family !=
IPADDR_INET6) ||
+          (type != SOCK_STREAM && type != SOCK_DGRAM)) {
+      DEBUG_LOG("invalid bind address");
+      return 0;
+  }
+
+  /* Check type */
+  l = sizeof(opt);
+  if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &opt, &l) < 0 || l !=
sizeof(opt) || opt != type) {
+    DEBUG_LOG("type mismatch : fd=%d %d != %d", fd, type, opt);
+    return 0;
+  }
+
+  if (type == SOCK_STREAM) {
+    /* Check if STREAM socket is listening */
+    l = sizeof(opt);
+    if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &opt, &l) < 0 || l !=
sizeof(opt) || opt == 0) {
+      DEBUG_LOG("STREAM socket not listening");
+      return 0;
+    }
+  }
+
+  l = sizeof(saddr);
+  if (getsockname(fd, &saddr.sa, &l) < 0 || l < sizeof(sa_family_t)) {
+    DEBUG_LOG("failed to get socket address from fd");
+    return 0;
+  }
+  SCK_SockaddrToIPSockAddr(&saddr.sa, l, &ip_sa);
+
+  if (UTI_CompareIPs(&ip_sa.ip_addr, &addr->ip_addr, NULL) != 0 ||
ip_sa.port != addr->port) {
+    DEBUG_LOG("address mismatch : %s != %s",
UTI_IPSockAddrToString(&ip_sa), UTI_IPSockAddrToString(addr));
+    return 0;
+  }
+
+  return 1;
+}
+
+
+/* ================================================== */
+
+static int
+bind_ip_address(int sock_fd, IPSockAddr *addr, int flags)
+{
+  union sockaddr_all saddr;
+  socklen_t saddr_len;
+  int s;
+
+  if (!set_bind_options(sock_fd, addr))
+    return 0;
+
   saddr_len = SCK_IPSockAddrToSockaddr(addr, (struct sockaddr *)&saddr,
sizeof (saddr));
   if (saddr_len == 0)
     return 0;
@@ -426,6 +494,41 @@ connect_ip_address(int sock_fd, IPSockAddr *addr)

 /* ================================================== */

+static int
+get_systemd_socket(const char *name)
+{
+  char *s;
+  int i, l, n;
+
+  if (!name)
+    return INVALID_SOCK_FD;
+
+  n = SCK_GetSystemdListenFdCount();
+  s = getenv("LISTEN_FDNAMES");
+
+  if (n > 0 && s) {
+    for (i = 0; i < n; i++) {
+      /* Find length of token */
+      l = 0;
+      while (s[l] != ':' && s[l] != '\0')
+        l++;
+
+      /* Compare (not always null-terminated) token to name */
+      if (strlen(name) == l && !strncmp(name, s, l))
+        return SD_LISTEN_FDS_START + i;
+
+      if (s[l] == '\0')
+        break;
+
+      /* Move to next token */
+      s+=l+1;
+    }
+  }
+  return INVALID_SOCK_FD;
+}
+
+/* ================================================== */
+
 static int
 open_ip_socket(IPSockAddr *remote_addr, IPSockAddr *local_addr, const char
*iface,
                int type, int flags)
@@ -1353,6 +1456,65 @@ SCK_OpenUnixSocketPair(int flags, int *other_fd)

 /* ================================================== */

+int SCK_GetSystemdListenFdCount(void)
+{
+  int n;
+  char *s, *ptr;
+
+  s = getenv("LISTEN_FDS");
+  if (!s)
+      return 0;
+  errno = 0;
+  n = strtol(s, &ptr, 10);
+  if (errno != 0 || *ptr != '\0')
+      return 0;
+
+  return n;
+}
+
+/* ================================================== */
+
+int
+SCK_GetSystemdSocket(const char *name, int type, IPSockAddr *addr, const
char *iface, int flags)
+{
+  int sock_fd;
+  flags |= SCK_FLAG_SYSTEMD;
+
+  sock_fd = get_systemd_socket(name);
+  if (sock_fd < 0)
+    return INVALID_SOCK_FD;
+  DEBUG_LOG("Found systemd socket : fd=%d, name=%s", sock_fd, name);
+
+  if (!set_socket_flags(sock_fd, flags))
+    goto error;
+
+  if (!set_socket_options(sock_fd, flags))
+    goto error;
+
+  if (!set_ip_options(sock_fd, addr->ip_addr.family, flags))
+    goto error;
+
+  /* Bind device if interface provided */
+  if (iface && !bind_device(sock_fd, iface))
+    goto error;
+
+  if (!set_bind_options(sock_fd, addr))
+    goto error;
+
+  /* Check that the socket is bound to the correct address */
+  if (!check_ip_bind_address(sock_fd, type, addr))
+    goto error;
+
+  return sock_fd;
+
+error:
+  SCK_CloseSocket(sock_fd);
+  LOG(LOGS_ERR, "Failed to use systemd socket : fd=%d, name=%s", sock_fd,
name);
+  return INVALID_SOCK_FD;
+}
+
+/* ================================================== */
+
 int
 SCK_SetIntOption(int sock_fd, int level, int name, int value)
 {
diff --git a/socket.h b/socket.h
index cdbae2d..5e350bd 100644
--- a/socket.h
+++ b/socket.h
@@ -36,11 +36,15 @@
 #define SCK_FLAG_RX_DEST_ADDR 4
 #define SCK_FLAG_ALL_PERMISSIONS 8
 #define SCK_FLAG_PRIV_BIND 16
+#define SCK_FLAG_SYSTEMD 32

 /* Flags for receiving and sending messages */
 #define SCK_FLAG_MSG_ERRQUEUE 1
 #define SCK_FLAG_MSG_DESCRIPTOR 2

+/* From sd-daemon.h */
+#define SD_LISTEN_FDS_START 3
+
 typedef enum {
   SCK_ADDR_UNSPEC = 0,
   SCK_ADDR_IP,
@@ -106,6 +110,13 @@ extern int SCK_OpenUnixStreamSocket(const char
*remote_addr, const char *local_a
                                     int flags);
 extern int SCK_OpenUnixSocketPair(int flags, int *other_fd);

+/* Get number of socket file descriptors from service manager */
+extern int SCK_GetSystemdListenFdCount(void);
+
+/* Get and configure socket file descriptor from service manager by name */
+extern int SCK_GetSystemdSocket(const char *name, int type, IPSockAddr
*local_addr,
+                             const char *iface, int flags);
+
 /* Set and get a socket option of int size */
 extern int SCK_SetIntOption(int sock_fd, int level, int name, int value);
 extern int SCK_GetIntOption(int sock_fd, int level, int name, int *value);
diff --git a/test/system/011-systemd b/test/system/011-systemd
new file mode 100755
index 0000000..9a79163
--- /dev/null
+++ b/test/system/011-systemd
@@ -0,0 +1,129 @@
+#!/usr/bin/env bash
+
+. ./test.common
+
+check_chronyd_features NTS || test_skip "NTS support disabled"
+has_ipv6=$(check_chronyd_features IPV6 && ping6 -c 1 ::1 > /dev/null 2>&1
&& echo 1 || echo 0)
+certtool --help &> /dev/null || test_skip "certtool missing"
+systemd-socket-activate -h &> /dev/null || test_skip
"systemd-socket-activate missing"
+
+test_start "systemd socket activation (ipv6=$has_ipv6)"
+
+cat > $TEST_DIR/cert.cfg <<EOF
+cn = "chrony-nts-test"
+dns_name = "chrony-nts-test"
+ip_address = "$server"
+serial = 001
+activation_date = "$[$(date '+%Y') - 1]-01-01 00:00:00 UTC"
+expiration_date = "$[$(date '+%Y') + 2]-01-01 00:00:00 UTC"
+signing_key
+encryption_key
+EOF
+
+certtool --generate-privkey --key-type=ed25519 --outfile
$TEST_DIR/server.key \
+ &> $TEST_DIR/certtool.log
+certtool --generate-self-signed --load-privkey $TEST_DIR/server.key \
+ --template $TEST_DIR/cert.cfg --outfile $TEST_DIR/server.crt &>>
$TEST_DIR/certtool.log
+chown $user $TEST_DIR/server.*
+
+ntpport=$(get_free_port)
+ntsport=$(get_free_port)
+
+server_options="port $ntpport nts ntsport $ntsport"
+extra_chronyd_directives="
+port $ntpport
+ntsport $ntsport
+ntsserverkey $TEST_DIR/server.key
+ntsservercert $TEST_DIR/server.crt
+ntstrustedcerts $TEST_DIR/server.crt
+ntsdumpdir $TEST_LIBDIR
+ntsprocesses 3"
+
+# Test UDP sockets for NTP (unfortunately systemd-socket-activate doesn't
+# support both datagram and stream sockets in the same invocation).
+if [ "$has_ipv6" = "1" ]; then
+CHRONYD_WRAPPER="systemd-socket-activate \
+ --datagram \
+ --listen 127.0.0.1:$ntpport --fdname=ntp4 \
+ --listen [::1]:$ntpport --fdname=ntp6"
+else
+CHRONYD_WRAPPER="systemd-socket-activate \
+ --datagram \
+ --listen 127.0.0.1:$ntpport --fdname=ntp4"
+fi
+
+# Hack to trigger systemd-socket-activate to activate the service.
Normally,
+# chronyd.service would be configured with the WantedBy= directive so it
starts
+# without waiting for socket activation.
+# (https://0pointer.de/blog/projects/socket-activation.html).
+(sleep 1 && echo "wake up" > /dev/udp/127.0.0.1/$ntpport) &
+start_chronyd || test_fail
+wait_for_sync || test_fail
+
+run_chronyd_client "server 127.0.0.1 port $ntpport iburst maxsamples 1" ||
test_fail
+check_chronyd_client_output || test_fail
+
+if [ "$has_ipv6" = "1" ]; then
+run_chronyd_client "server ::1 port $ntpport iburst maxsamples 1" ||
test_fail
+check_chronyd_client_output || test_fail
+fi
+
+stop_chronyd || test_fail
+check_chronyd_message_count "Using NTP socket from systemd : fd=3,
name=ntp4" 1 1 || test_fail
+if [ "$has_ipv6" = "1" ]; then
+check_chronyd_message_count "Using NTP socket from systemd : fd=4,
name=ntp6" 1 1 || test_fail
+fi
+check_chronyd_messages || test_fail
+check_chronyd_files || test_fail
+
+# Test TCP sockets for NTS-KE
+if [ "$has_ipv6" = "1" ]; then
+CHRONYD_WRAPPER="systemd-socket-activate \
+ --listen 127.0.0.1:$ntsport --fdname=nts4 \
+ --listen [::1]:$ntsport --fdname=nts6"
+else
+CHRONYD_WRAPPER="systemd-socket-activate \
+ --listen 127.0.0.1:$ntsport --fdname=nts4"
+fi
+# Same hack as above to trigger socket activation.
+(sleep 1 && echo "wake up" > /dev/tcp/127.0.0.1/$ntsport) &
+start_chronyd || test_fail
+wait_for_sync || test_fail
+
+run_chronyd_client "server 127.0.0.1 port $ntpport nts ntsport $ntsport
iburst maxsamples 1" "ntstrustedcerts $TEST_DIR/server.crt" || test_fail
+check_chronyd_client_output || test_fail
+
+if [ "$has_ipv6" = "1" ]; then
+run_chronyd_client "server ::1 port $ntpport iburst maxsamples 1" ||
test_fail
+check_chronyd_client_output || test_fail
+fi
+
+stop_chronyd || test_fail
+check_chronyd_message_count 'Using NTS-KE socket from systemd : fd=3,
name=nts4' 1 1 || test_fail
+if [ "$has_ipv6" = "1" ]; then
+check_chronyd_message_count 'Using NTS-KE socket from systemd : fd=4,
name=nts6' 1 1 || test_fail
+fi
+check_chronyd_messages || test_fail
+check_chronyd_files || test_fail
+
+# Test invalid socket configs
+# - nts4 as DGRAM socket should be ignored
+# - foo should be ignored
+CHRONYD_WRAPPER="systemd-socket-activate \
+ --datagram \
+ --listen 127.0.0.1:$ntpport --fdname=foo \
+ --listen 127.0.0.1:$ntsport --fdname=nts4"
+# Same hack as above to trigger socket activation.
+(sleep 1 && echo "wake up" > /dev/udp/127.0.0.1/$ntsport) &
+start_chronyd || test_fail
+wait_for_sync || test_fail
+
+run_chronyd_client "server 127.0.0.1 port $ntpport nts ntsport $ntsport
iburst maxsamples 1" "ntstrustedcerts $TEST_DIR/server.crt" || test_fail
+check_chronyd_client_output || test_fail
+
+stop_chronyd || test_fail
+check_chronyd_message_count 'Failed to use systemd socket : fd=4,
name=nts4' 1 1 || test_fail
+check_chronyd_messages || test_fail
+check_chronyd_files || test_fail
+
+test_pass
diff --git a/test/system/test.common b/test/system/test.common
index aa48ac6..381eb3c 100644
--- a/test/system/test.common
+++ b/test/system/test.common
@@ -349,6 +349,22 @@ check_chronyd_files() {
  test_ok || test_bad
 }

+# Run chronyd in client mode to retrieve and print out time
+run_chronyd_client() {
+ local args=( "$@" )
+ test_message 1 0 "running chronyd client"
+
+ "$chronyd" -Q "${args[@]}" > "$TEST_DIR/chronyd-client.out" 2>&1 &&
test_ok || test_error
+}
+
+check_chronyd_client_output() {
+ local pattern=$1
+
+ test_message 1 0 "checking chronyd client output"
+ grep -q "System clock wrong by .* seconds (ignored)"
"$TEST_DIR/chronyd-client.out" && \
+        test_ok || test_error
+}
+
 # Run a chronyc command
 run_chronyc() {
  local host=$chronyc_host options="-n -m"
diff --git a/test/unit/socket.c b/test/unit/socket.c
new file mode 100644
index 0000000..007984b
--- /dev/null
+++ b/test/unit/socket.c
@@ -0,0 +1,60 @@
+/*
+ **********************************************************************
+ * Copyright (C) Miroslav Lichvar  2023
+ *
+ * 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.
+ *
+ **********************************************************************
+ */
+
+#include <socket.c>
+#include "test.h"
+
+void
+test_unit(void)
+{
+  /* Test systemd environment variable parsing */
+
+  /* Normal case */
+  putenv("LISTEN_FDS=2");
+  putenv("LISTEN_FDNAMES=foo:bar");
+  TEST_CHECK(get_systemd_socket("foo") == SD_LISTEN_FDS_START);
+  TEST_CHECK(get_systemd_socket("bar") == SD_LISTEN_FDS_START + 1);
+  TEST_CHECK(get_systemd_socket("baz") < 0);
+
+  /* Only attempt to parse up to LISTEN_FDS entries */
+  putenv("LISTEN_FDS=1");
+  putenv("LISTEN_FDNAMES=foo:bar");
+  TEST_CHECK(get_systemd_socket("bar") < 0);
+
+  /* OK if a file descriptor name is empty */
+  putenv("LISTEN_FDS=2");
+  putenv("LISTEN_FDNAMES=:bar");
+  TEST_CHECK(get_systemd_socket("") == SD_LISTEN_FDS_START);
+
+  /* OK if LISTEN_FDS doesn't match number of entries in LISTEN_FDNAMES */
+  putenv("LISTEN_FDS=8");
+  putenv("LISTEN_FDNAMES=foo");
+  TEST_CHECK(get_systemd_socket("foo") == SD_LISTEN_FDS_START);
+
+  /* Make sure there are no trailing characters */
+  putenv("LISTEN_FDS=1a");
+  putenv("LISTEN_FDNAMES=foo");
+  TEST_CHECK(get_systemd_socket("foo") < 0);
+
+  /* Handle invalid LISTEN_FDS */
+  putenv("LISTEN_FDS=a1");
+  putenv("LISTEN_FDNAMES=foo");
+  TEST_CHECK(get_systemd_socket("foo") < 0);
+}
-- 
2.39.3 (Apple Git-145)


-- 
Luke Valenta
Systems Engineer - Research

Reply via email to