netutils: prefer DHCP-provided NTP servers for ntpc

Use the DHCP-learned NTP server list as the default ntpc server source when DHCP option 42 is available.

Fall back to CONFIG_NETUTILS_NTPCLIENT_SERVER only when DHCP does not provide any NTP servers, and restart ntpc when the DHCP-provided server list changes.

Signed-off-by: Jerry Ma <masc2008@gmail.com>
diff --git a/include/netutils/netlib.h b/include/netutils/netlib.h
index 2b3146e..eb3933f 100644
--- a/include/netutils/netlib.h
+++ b/include/netutils/netlib.h
@@ -203,6 +203,11 @@
 };
 #endif
 
+#ifdef CONFIG_NETUTILS_DHCPC
+typedef CODE void (*netlib_dhcp_ntp_callback_t)
+  (FAR const char *ntp_server_list, FAR void *arg);
+#endif
+
 /****************************************************************************
  * Public Data
  ****************************************************************************/
@@ -247,6 +252,44 @@
 bool netlib_ipv4addrconv(FAR const char *addrstr, FAR uint8_t *addr);
 bool netlib_ethaddrconv(FAR const char *hwstr, FAR uint8_t *hw);
 
+#ifdef CONFIG_NETUTILS_DHCPC
+/****************************************************************************
+ * Name: netlib_set_ntp_servers_from_dhcp
+ *
+ * Description:
+ *   Update the currently active DHCP option 42 NTP server list. The list
+ *   contains semicolon-separated hostnames or addresses. Passing NULL or
+ *   an empty string clears the DHCP-provided list.
+ *
+ ****************************************************************************/
+
+int netlib_set_ntp_servers_from_dhcp(FAR const char *ntp_server_list);
+
+/****************************************************************************
+ * Name: netlib_register_dhcp_ntp_callback
+ *
+ * Description:
+ *   Register a callback to receive DHCP option 42 NTP server list updates.
+ *   The current list, if any, is replayed immediately after registration.
+ *   Only one callback may be registered at a time.
+ *
+ ****************************************************************************/
+
+int netlib_register_dhcp_ntp_callback(netlib_dhcp_ntp_callback_t callback,
+                                      FAR void *arg);
+
+/****************************************************************************
+ * Name: netlib_unregister_dhcp_ntp_callback
+ *
+ * Description:
+ *   Unregister a previously registered DHCP option 42 NTP update callback.
+ *
+ ****************************************************************************/
+
+int netlib_unregister_dhcp_ntp_callback(netlib_dhcp_ntp_callback_t callback,
+                                        FAR void *arg);
+#endif
+
 #ifdef CONFIG_NET_ETHERNET
 /* Get and set IP/MAC addresses (Ethernet L2 only) */
 
diff --git a/netutils/netlib/CMakeLists.txt b/netutils/netlib/CMakeLists.txt
index 49e8aa8..2f0f11e 100644
--- a/netutils/netlib/CMakeLists.txt
+++ b/netutils/netlib/CMakeLists.txt
@@ -57,6 +57,7 @@
     endif()
     if(CONFIG_NETUTILS_DHCPC)
       list(APPEND SRCS netlib_obtainipv4addr.c)
+      list(APPEND SRCS netlib_dhcp_ntp.c)
     endif()
   endif()
 
diff --git a/netutils/netlib/Makefile b/netutils/netlib/Makefile
index 4bfc8d9..7bba856 100644
--- a/netutils/netlib/Makefile
+++ b/netutils/netlib/Makefile
@@ -57,6 +57,7 @@
 endif
 ifeq ($(CONFIG_NETUTILS_DHCPC),y)
 CSRCS += netlib_obtainipv4addr.c
+CSRCS += netlib_dhcp_ntp.c
 endif
 endif
 
diff --git a/netutils/netlib/netlib_dhcp_ntp.c b/netutils/netlib/netlib_dhcp_ntp.c
new file mode 100644
index 0000000..b1d608d
--- /dev/null
+++ b/netutils/netlib/netlib_dhcp_ntp.c
@@ -0,0 +1,182 @@
+/****************************************************************************
+ * apps/netutils/netlib/netlib_dhcp_ntp.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#ifdef CONFIG_NETUTILS_DHCPC
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "netutils/netlib.h"
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static pthread_mutex_t g_dhcp_ntp_lock = PTHREAD_MUTEX_INITIALIZER;
+static FAR char *g_dhcp_ntp_servers;
+static netlib_dhcp_ntp_callback_t g_dhcp_ntp_callback;
+static FAR void *g_dhcp_ntp_callback_arg;
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static FAR char *netlib_dhcp_ntp_dup(FAR const char *ntp_server_list)
+{
+  if (ntp_server_list == NULL || ntp_server_list[0] == '\0')
+    {
+      return NULL;
+    }
+
+  return strdup(ntp_server_list);
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int netlib_set_ntp_servers_from_dhcp(FAR const char *ntp_server_list)
+{
+  FAR char *new_servers;
+  FAR char *notify_servers = NULL;
+  FAR char *old_servers;
+  netlib_dhcp_ntp_callback_t callback;
+  FAR void *arg;
+  int ret = OK;
+
+  new_servers = netlib_dhcp_ntp_dup(ntp_server_list);
+  if (ntp_server_list != NULL && ntp_server_list[0] != '\0' &&
+      new_servers == NULL)
+    {
+      return -ENOMEM;
+    }
+
+  pthread_mutex_lock(&g_dhcp_ntp_lock);
+
+  if ((g_dhcp_ntp_servers == NULL && new_servers == NULL) ||
+      (g_dhcp_ntp_servers != NULL && new_servers != NULL &&
+       strcmp(g_dhcp_ntp_servers, new_servers) == 0))
+    {
+      pthread_mutex_unlock(&g_dhcp_ntp_lock);
+      free(new_servers);
+      return OK;
+    }
+
+  callback = g_dhcp_ntp_callback;
+  arg = g_dhcp_ntp_callback_arg;
+
+  if (callback != NULL && new_servers != NULL)
+    {
+      notify_servers = strdup(new_servers);
+      if (notify_servers == NULL)
+        {
+          ret = -ENOMEM;
+          goto errout_with_lock;
+        }
+    }
+
+  old_servers = g_dhcp_ntp_servers;
+  g_dhcp_ntp_servers = new_servers;
+  pthread_mutex_unlock(&g_dhcp_ntp_lock);
+
+  free(old_servers);
+
+  if (callback != NULL)
+    {
+      callback(notify_servers, arg);
+    }
+
+  free(notify_servers);
+  return OK;
+
+errout_with_lock:
+  pthread_mutex_unlock(&g_dhcp_ntp_lock);
+  free(new_servers);
+  return ret;
+}
+
+int netlib_register_dhcp_ntp_callback(netlib_dhcp_ntp_callback_t callback,
+                                      FAR void *arg)
+{
+  FAR char *notify_servers = NULL;
+
+  if (callback == NULL)
+    {
+      return -EINVAL;
+    }
+
+  pthread_mutex_lock(&g_dhcp_ntp_lock);
+
+  if (g_dhcp_ntp_callback != NULL &&
+      (g_dhcp_ntp_callback != callback || g_dhcp_ntp_callback_arg != arg))
+    {
+      pthread_mutex_unlock(&g_dhcp_ntp_lock);
+      return -EBUSY;
+    }
+
+  if (g_dhcp_ntp_servers != NULL)
+    {
+      notify_servers = strdup(g_dhcp_ntp_servers);
+      if (notify_servers == NULL)
+        {
+          pthread_mutex_unlock(&g_dhcp_ntp_lock);
+          return -ENOMEM;
+        }
+    }
+
+  g_dhcp_ntp_callback = callback;
+  g_dhcp_ntp_callback_arg = arg;
+
+  pthread_mutex_unlock(&g_dhcp_ntp_lock);
+
+  callback(notify_servers, arg);
+  free(notify_servers);
+  return OK;
+}
+
+int netlib_unregister_dhcp_ntp_callback(netlib_dhcp_ntp_callback_t callback,
+                                        FAR void *arg)
+{
+  int ret = -ENOENT;
+
+  pthread_mutex_lock(&g_dhcp_ntp_lock);
+
+  if (g_dhcp_ntp_callback == callback && g_dhcp_ntp_callback_arg == arg)
+    {
+      g_dhcp_ntp_callback = NULL;
+      g_dhcp_ntp_callback_arg = NULL;
+      ret = OK;
+    }
+
+  pthread_mutex_unlock(&g_dhcp_ntp_lock);
+  return ret;
+}
+
+#endif /* CONFIG_NETUTILS_DHCPC */
diff --git a/netutils/netlib/netlib_obtainipv4addr.c b/netutils/netlib/netlib_obtainipv4addr.c
index cab55b7..df07fef 100644
--- a/netutils/netlib/netlib_obtainipv4addr.c
+++ b/netutils/netlib/netlib_obtainipv4addr.c
@@ -25,6 +25,10 @@
  ****************************************************************************/
 
 #include <nuttx/debug.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
 #include <sys/types.h>
 
 #include "netutils/dhcpc.h"
@@ -105,6 +109,68 @@
   return OK;
 }
 
+#ifdef CONFIG_NETUTILS_NTPCLIENT
+static int dhcp_set_ntp_servers(FAR const struct dhcpc_state *ds)
+{
+  char ntp_server_list[CONFIG_NETUTILS_DHCPC_NTP_SERVERS *
+                       (INET_ADDRSTRLEN + 1)];
+  size_t offset = 0;
+  uint8_t i;
+
+  /* Clear the DHCP-provided NTP server list,
+   * consider case: that device has joined another dhcp domain,
+   * it need refresh related settings.
+   */
+
+  if (ds->num_ntpaddr == 0)
+    {
+      return netlib_set_ntp_servers_from_dhcp(NULL);
+    }
+
+  ntp_server_list[0] = '\0';
+
+  for (i = 0; i < ds->num_ntpaddr; i++)
+    {
+      char addrbuf[INET_ADDRSTRLEN];
+      int ret;
+
+      /* Skip empty entries */
+
+      if (ds->ntpaddr[i].s_addr == 0)
+        {
+          continue;
+        }
+
+      if (inet_ntop(AF_INET, &ds->ntpaddr[i], addrbuf, sizeof(addrbuf)) ==
+          NULL)
+        {
+          return -EINVAL;
+        }
+
+      /* Append the server to the list */
+
+      ret = snprintf(ntp_server_list + offset,
+                     sizeof(ntp_server_list) - offset,
+                     "%s%s", offset == 0 ? "" : ";", addrbuf);
+      if (ret < 0 || (size_t)ret >= sizeof(ntp_server_list) - offset)
+        {
+          return -E2BIG;
+        }
+
+      offset += (size_t)ret;
+    }
+
+  /* Clear the list if all entries were empty */
+
+  if (offset == 0)
+    {
+      return netlib_set_ntp_servers_from_dhcp(NULL);
+    }
+
+  return netlib_set_ntp_servers_from_dhcp(ntp_server_list);
+}
+#endif
+
 /****************************************************************************
  * Name: dhcp_obtain_statefuladdr
  *
@@ -156,6 +222,18 @@
   if (ret == OK)
     {
       ret = dhcp_setup_result(ifname, &ds);
+#ifdef CONFIG_NETUTILS_NTPCLIENT
+      if (ret == OK)
+        {
+          ret = dhcp_set_ntp_servers(&ds);
+          if (ret < 0)
+            {
+              nwarn("WARNING: failed to update DHCP NTP server list: %d\n",
+                    ret);
+              ret = OK;
+            }
+        }
+#endif
     }
   else
     {
diff --git a/netutils/ntpclient/ntpclient.c b/netutils/ntpclient/ntpclient.c
index ce7d2f0..ec002a7 100644
--- a/netutils/ntpclient/ntpclient.c
+++ b/netutils/ntpclient/ntpclient.c
@@ -126,6 +126,14 @@
   NTP_STOPPED
 };
 
+enum ntpc_server_source_e
+{
+  NTP_SERVER_SOURCE_NONE = 0,
+  NTP_SERVER_SOURCE_CONFIG,
+  NTP_SERVER_SOURCE_DHCP,
+  NTP_SERVER_SOURCE_EXPLICIT
+};
+
 /* This type describes the state of the NTP client daemon.  Only one
  * instance of the NTP daemon is permitted in this implementation.
  */
@@ -133,11 +141,17 @@
 struct ntpc_daemon_s
 {
   uint8_t state;       /* See enum ntpc_daemon_e */
+  uint8_t source;      /* See enum ntpc_server_source_e */
+  bool dhcp_registered;
   sem_t lock;          /* Used to protect the whole structure */
   sem_t sync;          /* Used to synchronize start and stop events */
   pid_t pid;           /* Task ID of the NTP daemon */
   sq_queue_t kod_list; /* KoD excluded server addresses */
   int family;          /* Allowed address family */
+
+  /* DHCP-provided server list */
+
+  FAR char *dhcp_servers;
 };
 
 union ntp_addr_u
@@ -194,11 +208,14 @@
 static struct ntpc_daemon_s g_ntpc_daemon =
 {
   NTP_NOT_RUNNING,
+  NTP_SERVER_SOURCE_NONE,
+  false,
   SEM_INITIALIZER(1),
   SEM_INITIALIZER(0),
   -1,
   { NULL, NULL },
   AF_UNSPEC,         /* Default is both IPv4 and IPv6 */
+  NULL,
 };
 
 static struct ntp_sample_s g_last_samples
@@ -209,6 +226,115 @@
  * Private Functions
  ****************************************************************************/
 
+static int ntpc_daemon(int argc, FAR char **argv);
+static int ntpc_set_servers_from_dhcp(FAR const char *ntp_server_list);
+
+static FAR char *ntpc_dup_server_list(FAR const char *ntp_server_list)
+{
+  if (ntp_server_list == NULL || ntp_server_list[0] == '\0')
+    {
+      return NULL;
+    }
+
+  return strdup(ntp_server_list);
+}
+
+static FAR const char *ntpc_select_server_list(FAR uint8_t *source)
+{
+  if (g_ntpc_daemon.dhcp_servers != NULL &&
+      g_ntpc_daemon.dhcp_servers[0] != '\0')
+    {
+      *source = NTP_SERVER_SOURCE_DHCP;
+      return g_ntpc_daemon.dhcp_servers;
+    }
+
+  if (CONFIG_NETUTILS_NTPCLIENT_SERVER[0] != '\0')
+    {
+      *source = NTP_SERVER_SOURCE_CONFIG;
+      return CONFIG_NETUTILS_NTPCLIENT_SERVER;
+    }
+
+  *source = NTP_SERVER_SOURCE_NONE;
+  return NULL;
+}
+
+#ifdef CONFIG_NETUTILS_DHCPC
+static void ntpc_dhcp_notify(FAR const char *ntp_server_list, FAR void *arg)
+{
+  int ret;
+
+  UNUSED(arg);
+
+  ret = ntpc_set_servers_from_dhcp(ntp_server_list);
+  if (ret < 0)
+    {
+      nwarn("WARNING: failed to update DHCP NTP server list: %d\n", ret);
+    }
+}
+
+static int ntpc_register_dhcp(void)
+{
+  int ret;
+
+  sem_wait(&g_ntpc_daemon.lock);
+  if (g_ntpc_daemon.dhcp_registered)
+    {
+      sem_post(&g_ntpc_daemon.lock);
+      return OK;
+    }
+
+  g_ntpc_daemon.dhcp_registered = true;
+  sem_post(&g_ntpc_daemon.lock);
+
+  ret = netlib_register_dhcp_ntp_callback(ntpc_dhcp_notify, NULL);
+  if (ret < 0)
+    {
+      sem_wait(&g_ntpc_daemon.lock);
+      g_ntpc_daemon.dhcp_registered = false;
+      sem_post(&g_ntpc_daemon.lock);
+      return ret;
+    }
+
+  return OK;
+}
+#endif
+
+static int ntpc_start_selected(FAR const char *ntp_server_list,
+                               uint8_t source)
+{
+  FAR char *task_argv[] =
+    {
+      (FAR char *)ntp_server_list,
+      NULL
+    };
+
+  g_ntpc_daemon.state = NTP_STARTED;
+  g_ntpc_daemon.source = source;
+  g_ntpc_daemon.pid =
+    task_create("NTP daemon", CONFIG_NETUTILS_NTPCLIENT_SERVERPRIO,
+                CONFIG_NETUTILS_NTPCLIENT_STACKSIZE, ntpc_daemon,
+                task_argv);
+
+  if (g_ntpc_daemon.pid < 0)
+    {
+      int errval = errno;
+      DEBUGASSERT(errval > 0);
+
+      g_ntpc_daemon.state = NTP_STOPPED;
+      g_ntpc_daemon.source = NTP_SERVER_SOURCE_NONE;
+      nerr("ERROR: Failed to start the NTP daemon: %d\n", errval);
+      return -errval;
+    }
+
+  do
+    {
+      sem_wait(&g_ntpc_daemon.sync);
+    }
+  while (g_ntpc_daemon.state == NTP_STARTED);
+
+  return g_ntpc_daemon.pid;
+}
+
 /****************************************************************************
  * Name: sample_cmp
  ****************************************************************************/
@@ -1490,11 +1616,12 @@
 
 int ntpc_start_with_list(FAR const char *ntp_server_list)
 {
-  FAR char *task_argv[] =
+  int ret;
+
+  if (ntp_server_list == NULL || ntp_server_list[0] == '\0')
     {
-      (FAR char *)ntp_server_list,
-      NULL
-    };
+      return -EINVAL;
+    }
 
   /* Is the NTP in a non-running state? */
 
@@ -1502,34 +1629,10 @@
   if (g_ntpc_daemon.state == NTP_NOT_RUNNING ||
       g_ntpc_daemon.state == NTP_STOPPED)
     {
-      /* Start the NTP daemon */
-
-      g_ntpc_daemon.state = NTP_STARTED;
-      g_ntpc_daemon.pid =
-        task_create("NTP daemon", CONFIG_NETUTILS_NTPCLIENT_SERVERPRIO,
-                    CONFIG_NETUTILS_NTPCLIENT_STACKSIZE, ntpc_daemon,
-                    task_argv);
-
-      /* Handle failures to start the NTP daemon */
-
-      if (g_ntpc_daemon.pid < 0)
-        {
-          int errval = errno;
-          DEBUGASSERT(errval > 0);
-
-          g_ntpc_daemon.state = NTP_STOPPED;
-          nerr("ERROR: Failed to start the NTP daemon: %d\n", errval);
-          sem_post(&g_ntpc_daemon.lock);
-          return -errval;
-        }
-
-      /* Wait for any daemon state change */
-
-      do
-        {
-          sem_wait(&g_ntpc_daemon.sync);
-        }
-      while (g_ntpc_daemon.state == NTP_STARTED);
+      ret = ntpc_start_selected(ntp_server_list,
+                                NTP_SERVER_SOURCE_EXPLICIT);
+      sem_post(&g_ntpc_daemon.lock);
+      return ret;
     }
 
   sem_post(&g_ntpc_daemon.lock);
@@ -1550,7 +1653,101 @@
 
 int ntpc_start(void)
 {
-  return ntpc_start_with_list(CONFIG_NETUTILS_NTPCLIENT_SERVER);
+  FAR const char *ntp_server_list;
+  uint8_t source;
+  int ret;
+
+#ifdef CONFIG_NETUTILS_DHCPC
+  ret = ntpc_register_dhcp();
+  if (ret < 0)
+    {
+      return ret;
+    }
+#endif
+
+  sem_wait(&g_ntpc_daemon.lock);
+  if (g_ntpc_daemon.state != NTP_NOT_RUNNING &&
+      g_ntpc_daemon.state != NTP_STOPPED)
+    {
+      ret = g_ntpc_daemon.pid;
+      sem_post(&g_ntpc_daemon.lock);
+      return ret;
+    }
+
+  ntp_server_list = ntpc_select_server_list(&source);
+  if (ntp_server_list == NULL)
+    {
+      g_ntpc_daemon.source = NTP_SERVER_SOURCE_NONE;
+      sem_post(&g_ntpc_daemon.lock);
+      return -ENOENT;
+    }
+
+  ret = ntpc_start_selected(ntp_server_list, source);
+  sem_post(&g_ntpc_daemon.lock);
+  return ret;
+}
+
+static int ntpc_set_servers_from_dhcp(FAR const char *ntp_server_list)
+{
+  FAR char *new_servers;
+  FAR char *old_servers;
+  bool start = false;
+  bool restart = false;
+  int ret = OK;
+
+  new_servers = ntpc_dup_server_list(ntp_server_list);
+  if (ntp_server_list != NULL && ntp_server_list[0] != '\0' &&
+      new_servers == NULL)
+    {
+      return -ENOMEM;
+    }
+
+  sem_wait(&g_ntpc_daemon.lock);
+
+  if ((g_ntpc_daemon.dhcp_servers == NULL && new_servers == NULL) ||
+      (g_ntpc_daemon.dhcp_servers != NULL && new_servers != NULL &&
+       strcmp(g_ntpc_daemon.dhcp_servers, new_servers) == 0))
+    {
+      sem_post(&g_ntpc_daemon.lock);
+      free(new_servers);
+      return OK;
+    }
+
+  old_servers = g_ntpc_daemon.dhcp_servers;
+  g_ntpc_daemon.dhcp_servers = new_servers;
+
+  if ((g_ntpc_daemon.source == NTP_SERVER_SOURCE_DHCP ||
+       g_ntpc_daemon.source == NTP_SERVER_SOURCE_CONFIG) &&
+      (g_ntpc_daemon.state == NTP_STARTED ||
+       g_ntpc_daemon.state == NTP_RUNNING))
+    {
+      restart = true;
+    }
+  else if (g_ntpc_daemon.source == NTP_SERVER_SOURCE_NONE &&
+           new_servers != NULL &&
+           (g_ntpc_daemon.state == NTP_NOT_RUNNING ||
+            g_ntpc_daemon.state == NTP_STOPPED))
+    {
+      start = true;
+    }
+
+  sem_post(&g_ntpc_daemon.lock);
+  free(old_servers);
+
+  if (restart)
+    {
+      ret = ntpc_stop();
+      if (ret >= 0 && new_servers != NULL)
+        {
+          ret = ntpc_start();
+        }
+    }
+  else if (start)
+    {
+      ret = ntpc_start();
+    }
+
+  return ret;
 }
 
 /****************************************************************************