]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- Fix #278: DoT: complete unbound restart required on certificate
authorW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Fri, 13 Mar 2026 10:42:34 +0000 (11:42 +0100)
committerW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Fri, 13 Mar 2026 10:42:34 +0000 (11:42 +0100)
  renew. Fix so that a reload checks if the files have changed, and
  if so, reload the contexts. Also for DoH, DoQ and outgoing DoT.

config.h.in
configure
configure.ac
daemon/daemon.c
daemon/daemon.h
daemon/unbound.c
doc/Changelog
util/config_file.c
util/config_file.h
util/net_help.c
util/net_help.h

index 5ffb7c43c680d1a26c68bbdd01f23db98321a8b0..3372ae4dc2418fab389b2425baf7d4034c7de80d 100644 (file)
 /* Define to 1 if `sun_len' is a member of `struct sockaddr_un'. */
 #undef HAVE_STRUCT_SOCKADDR_UN_SUN_LEN
 
+/* Define to 1 if `st_mtimensec' is a member of `struct stat'. */
+#undef HAVE_STRUCT_STAT_ST_MTIMENSEC
+
+/* Define to 1 if `st_mtim.tv_nsec' is a member of `struct stat'. */
+#undef HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC
+
 /* Define if you have Swig libraries and header files. */
 #undef HAVE_SWIG
 
index 3e4e922005d46b10990894be424ffb68af22aee5..62bc2496c6819ebb17818d938576828af8f73a62 100755 (executable)
--- a/configure
+++ b/configure
@@ -23268,6 +23268,23 @@ printf "%s\n" "no" >&6; }
 
 fi
 
+fi
+
+ac_fn_c_check_member "$LINENO" "struct stat" "st_mtimensec" "ac_cv_member_struct_stat_st_mtimensec" "$ac_includes_default"
+if test "x$ac_cv_member_struct_stat_st_mtimensec" = xyes
+then :
+
+printf "%s\n" "#define HAVE_STRUCT_STAT_ST_MTIMENSEC 1" >>confdefs.h
+
+
+fi
+ac_fn_c_check_member "$LINENO" "struct stat" "st_mtim.tv_nsec" "ac_cv_member_struct_stat_st_mtim_tv_nsec" "$ac_includes_default"
+if test "x$ac_cv_member_struct_stat_st_mtim_tv_nsec" = xyes
+then :
+
+printf "%s\n" "#define HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC 1" >>confdefs.h
+
+
 fi
 
 ac_fn_c_check_member "$LINENO" "struct sockaddr_un" "sun_len" "ac_cv_member_struct_sockaddr_un_sun_len" "
index 7317aad608b699892efd587b6433544db266ac17..bc1274c087358ec000722c25c651570df5e4f44a 100644 (file)
@@ -1769,6 +1769,7 @@ if test $ac_cv_func_daemon = yes; then
 ])
 fi
 
+AC_CHECK_MEMBERS([struct stat.st_mtimensec, struct stat.st_mtim.tv_nsec])
 AC_CHECK_MEMBERS([struct sockaddr_un.sun_len],,,[
 AC_INCLUDES_DEFAULT
 #ifdef HAVE_SYS_UN_H
index 5ee12e0dbb1c584396ce8e5e912c9504ad29a75e..c8ec56afa6ed3022dfecbe53dd8e5b6c99ecd139 100644 (file)
@@ -199,6 +199,181 @@ signal_handling_playback(struct worker* wrk)
        sig_record_reload = 0;
 }
 
+#ifdef HAVE_SSL
+/* setup a listening ssl context, fatal_exit() on any failure */
+static void
+setup_listen_sslctx(void** ctx, int is_dot, int is_doh,
+       struct config_file* cfg, char* chroot)
+{
+       char* key = cfg->ssl_service_key;
+       char* pem = cfg->ssl_service_pem;
+       if(chroot && strncmp(key, chroot, strlen(chroot)) == 0)
+               key += strlen(chroot);
+       if(chroot && pem && strncmp(pem, chroot, strlen(chroot)) == 0)
+               pem += strlen(chroot);
+       if(!(*ctx = listen_sslctx_create(key, pem, NULL,
+               cfg->tls_ciphers, cfg->tls_ciphersuites,
+               (cfg->tls_session_ticket_keys.first &&
+               cfg->tls_session_ticket_keys.first->str[0] != 0),
+               is_dot, is_doh, cfg->tls_use_system_policy_versions))) {
+               fatal_exit("could not set up listen SSL_CTX");
+       }
+}
+#endif /* HAVE_SSL */
+
+/* setups the needed ssl contexts, fatal_exit() on any failure */
+void
+daemon_setup_sslctxs(struct daemon* daemon, struct config_file* cfg)
+{
+#ifdef HAVE_SSL
+       char* bundle, *chroot = daemon->chroot;
+       if(cfg->ssl_service_key && cfg->ssl_service_key[0]) {
+               char* key = cfg->ssl_service_key;
+               char* pem = cfg->ssl_service_pem;
+               if(chroot && strncmp(key, chroot, strlen(chroot)) == 0)
+                       key += strlen(chroot);
+               if(chroot && pem && strncmp(pem, chroot, strlen(chroot)) == 0)
+                       pem += strlen(chroot);
+
+               /* setup the session keys; the callback to use them will be
+                * attached to each sslctx separately */
+               if(cfg->tls_session_ticket_keys.first &&
+                       cfg->tls_session_ticket_keys.first->str[0] != 0) {
+                       if(!listen_sslctx_setup_ticket_keys(
+                               cfg->tls_session_ticket_keys.first, chroot)) {
+                               fatal_exit("could not set session ticket SSL_CTX");
+                       }
+               }
+               (void)setup_listen_sslctx(&daemon->listen_dot_sslctx, 1, 0,
+                       cfg, chroot);
+#ifdef HAVE_NGHTTP2_NGHTTP2_H
+               if(cfg_has_https(cfg)) {
+                       (void)setup_listen_sslctx(&daemon->listen_doh_sslctx,
+                               0, 1, cfg, chroot);
+               }
+#endif
+#ifdef HAVE_NGTCP2
+               if(cfg_has_quic(cfg)) {
+                       if(!(daemon->listen_quic_sslctx = quic_sslctx_create(
+                               key, pem, NULL))) {
+                               fatal_exit("could not set up quic SSL_CTX");
+                       }
+               }
+#endif /* HAVE_NGTCP2 */
+
+               /* Store the file name and mtime to detect changes later. */
+               daemon->ssl_service_key = strdup(cfg->ssl_service_key);
+               if(!daemon->ssl_service_key)
+                       fatal_exit("could not setup ssl ctx: out of memory");
+               daemon->ssl_service_pem = strdup(cfg->ssl_service_pem);
+               if(!daemon->ssl_service_pem)
+                       fatal_exit("could not setup ssl ctx: out of memory");
+               if(!file_get_mtime(key,
+                       &daemon->mtime_ssl_service_key,
+                       &daemon->mtime_ns_ssl_service_key, NULL))
+                       log_err("Could not stat(%s): %s",
+                               key, strerror(errno));
+               if(!file_get_mtime(pem,
+                       &daemon->mtime_ssl_service_pem,
+                       &daemon->mtime_ns_ssl_service_pem, NULL))
+                       log_err("Could not stat(%s): %s",
+                               pem, strerror(errno));
+       }
+       bundle = cfg->tls_cert_bundle;
+       if(chroot && bundle && strncmp(bundle, chroot, strlen(chroot)) == 0)
+               bundle += strlen(chroot);
+       if(!(daemon->connect_dot_sslctx = connect_sslctx_create(NULL, NULL,
+               bundle, cfg->tls_win_cert)))
+               fatal_exit("could not set up connect SSL_CTX");
+#else /* HAVE_SSL */
+       (void)daemon;(void)cfg;
+#endif /* HAVE_SSL */
+}
+
+/** Delete the ssl ctxs */
+static void
+daemon_delete_sslctxs(struct daemon* daemon)
+{
+#ifdef HAVE_SSL
+       listen_sslctx_delete_ticket_keys();
+       SSL_CTX_free((SSL_CTX*)daemon->listen_dot_sslctx);
+       daemon->listen_dot_sslctx = NULL;
+       SSL_CTX_free((SSL_CTX*)daemon->listen_doh_sslctx);
+       daemon->listen_doh_sslctx = NULL;
+       SSL_CTX_free((SSL_CTX*)daemon->connect_dot_sslctx);
+       daemon->connect_dot_sslctx = NULL;
+       free(daemon->ssl_service_key);
+       daemon->ssl_service_key = NULL;
+       free(daemon->ssl_service_pem);
+       daemon->ssl_service_pem = NULL;
+#else
+       (void)daemon;
+#endif
+#ifdef HAVE_NGTCP2
+       SSL_CTX_free((SSL_CTX*)daemon->listen_quic_sslctx);
+       daemon->listen_quic_sslctx = NULL;
+#endif
+}
+
+/** See if the SSL cert files have changed */
+static int
+ssl_cert_changed(struct daemon* daemon, struct config_file* cfg)
+{
+       time_t mtime = 0;
+       long ns = 0;
+       log_assert(daemon->ssl_service_key && cfg->ssl_service_key);
+       if(strcmp(daemon->ssl_service_key, cfg->ssl_service_key) != 0)
+               return 1;
+       if(strcmp(daemon->ssl_service_pem, cfg->ssl_service_pem) != 0)
+               return 1;
+       if(!file_get_mtime(daemon->ssl_service_key, &mtime, &ns, NULL)) {
+               log_err("Could not stat(%s): %s",
+                       daemon->ssl_service_key, strerror(errno));
+               /* It has probably changed, but file read is likely going to
+                * fail. */
+               return 0;
+       }
+       if(mtime != daemon->mtime_ssl_service_key ||
+               ns != daemon->mtime_ns_ssl_service_key)
+               return 1;
+       if(!file_get_mtime(daemon->ssl_service_pem, &mtime, &ns, NULL)) {
+               log_err("Could not stat(%s): %s",
+                       daemon->ssl_service_pem, strerror(errno));
+               /* It has probably changed, but file read is likely going to
+                * fail. */
+               return 0;
+       }
+       if(mtime != daemon->mtime_ssl_service_pem ||
+               ns != daemon->mtime_ns_ssl_service_pem)
+               return 1;
+       return 0;
+}
+
+/** Reload the sslctxs if they have changed */
+static void
+daemon_reload_sslctxs(struct daemon* daemon)
+{
+#ifdef HAVE_SSL
+       if(daemon->cfg->ssl_service_key && daemon->cfg->ssl_service_key[0]) {
+               /* See if changed */
+               if(!daemon->ssl_service_key ||
+                       ssl_cert_changed(daemon,daemon->cfg)) {
+                       verbose(VERB_ALGO, "Reloading certificates");
+                       daemon_delete_sslctxs(daemon);
+                       daemon_setup_sslctxs(daemon, daemon->cfg);
+               }
+       } else {
+               /* See if sslctxs are removed from config. */
+               if(daemon->ssl_service_key) {
+                       verbose(VERB_ALGO, "Removing certificates");
+                       daemon_delete_sslctxs(daemon);
+               }
+       }
+#else
+       (void)daemon;
+#endif
+}
+
 struct daemon* 
 daemon_init(void)
 {
@@ -745,6 +920,7 @@ daemon_fork(struct daemon* daemon)
 #endif
 
        log_assert(daemon);
+       daemon_reload_sslctxs(daemon);
        if(!(daemon->env->views = views_create()))
                fatal_exit("Could not create views: out of memory");
        /* create individual views and their localzone/data trees */
@@ -991,15 +1167,7 @@ daemon_delete(struct daemon* daemon)
        free(daemon->pidfile);
        free(daemon->cfgfile);
        free(daemon->env);
-#ifdef HAVE_SSL
-       listen_sslctx_delete_ticket_keys();
-       SSL_CTX_free((SSL_CTX*)daemon->listen_dot_sslctx);
-       SSL_CTX_free((SSL_CTX*)daemon->listen_doh_sslctx);
-       SSL_CTX_free((SSL_CTX*)daemon->connect_dot_sslctx);
-#endif
-#ifdef HAVE_NGTCP2
-       SSL_CTX_free((SSL_CTX*)daemon->listen_quic_sslctx);
-#endif
+       daemon_delete_sslctxs(daemon);
        free(daemon);
        /* lex cleanup */
        ub_c_lex_destroy();
index 2295761ab7e30316c3779e7f606aa260824c3d35..d38cd90d8d80337463f83a06a382a3da414a187d 100644 (file)
@@ -107,6 +107,18 @@ struct daemon {
        void* listen_doh_sslctx;
        /** ssl context for listening to quic */
        void* listen_quic_sslctx;
+       /** the file name that the ssl context is made with, private key. */
+       char* ssl_service_key;
+       /** the file name that the ssl context is made with, certificate. */
+       char* ssl_service_pem;
+       /** modification time for ssl_service_key, in sec and ns. Like
+        * in a struct timespec, but without that for portability. */
+       time_t mtime_ssl_service_key;
+       long mtime_ns_ssl_service_key;
+       /** modification time for ssl_service_pem, in sec and ns. Like
+        * in a struct timespec, but without that for portability. */
+       time_t mtime_ssl_service_pem;
+       long mtime_ns_ssl_service_pem;
        /** num threads allocated */
        int num;
        /** num threads allocated in the previous config or 0 at first */
@@ -229,4 +241,7 @@ void daemon_apply_cfg(struct daemon* daemon, struct config_file* cfg);
  */
 int setup_acl_for_ports(struct acl_list* list, struct listen_port* port_list);
 
+/* setups the needed ssl contexts, fatal_exit() on any failure */
+void daemon_setup_sslctxs(struct daemon* daemon, struct config_file* cfg);
+
 #endif /* DAEMON_H */
index 6888047435c962009855a5ce7b9c66dfe3ff7afa..a787b76fead45640bfe58969e3af018efe975a30 100644 (file)
@@ -463,57 +463,13 @@ detach(void)
 #endif /* HAVE_DAEMON */
 }
 
-#ifdef HAVE_SSL
-/* setup a listening ssl context, fatal_exit() on any failure */
-static void
-setup_listen_sslctx(void** ctx, int is_dot, int is_doh, struct config_file* cfg)
-{
-       if(!(*ctx = listen_sslctx_create(
-               cfg->ssl_service_key, cfg->ssl_service_pem, NULL,
-               cfg->tls_ciphers, cfg->tls_ciphersuites,
-               (cfg->tls_session_ticket_keys.first &&
-               cfg->tls_session_ticket_keys.first->str[0] != 0),
-               is_dot, is_doh, cfg->tls_use_system_policy_versions))) {
-               fatal_exit("could not set up listen SSL_CTX");
-       }
-}
-#endif /* HAVE_SSL */
-
-/* setups the needed ssl contexts, fatal_exit() on any failure */
+/** setup the remote and ticket keys */
 static void
-setup_sslctxs(struct daemon* daemon, struct config_file* cfg)
+setup_sslctx_remote(struct daemon* daemon, struct config_file* cfg)
 {
 #ifdef HAVE_SSL
        if(!(daemon->rc = daemon_remote_create(cfg)))
                fatal_exit("could not set up remote-control");
-       if(cfg->ssl_service_key && cfg->ssl_service_key[0]) {
-               /* setup the session keys; the callback to use them will be
-                * attached to each sslctx separately */
-               if(cfg->tls_session_ticket_keys.first &&
-                       cfg->tls_session_ticket_keys.first->str[0] != 0) {
-                       if(!listen_sslctx_setup_ticket_keys(
-                               cfg->tls_session_ticket_keys.first)) {
-                               fatal_exit("could not set session ticket SSL_CTX");
-                       }
-               }
-               (void)setup_listen_sslctx(&daemon->listen_dot_sslctx, 1, 0, cfg);
-#ifdef HAVE_NGHTTP2_NGHTTP2_H
-               if(cfg_has_https(cfg)) {
-                       (void)setup_listen_sslctx(&daemon->listen_doh_sslctx, 0, 1, cfg);
-               }
-#endif
-#ifdef HAVE_NGTCP2
-               if(cfg_has_quic(cfg)) {
-                       if(!(daemon->listen_quic_sslctx = quic_sslctx_create(
-                               cfg->ssl_service_key, cfg->ssl_service_pem, NULL))) {
-                               fatal_exit("could not set up quic SSL_CTX");
-                       }
-               }
-#endif /* HAVE_NGTCP2 */
-       }
-       if(!(daemon->connect_dot_sslctx = connect_sslctx_create(NULL, NULL,
-               cfg->tls_cert_bundle, cfg->tls_win_cert)))
-               fatal_exit("could not set up connect SSL_CTX");
 #else /* HAVE_SSL */
        (void)daemon;(void)cfg;
 #endif /* HAVE_SSL */
@@ -545,7 +501,8 @@ perform_setup(struct daemon* daemon, struct config_file* cfg, int debug_mode,
 #endif
 
        /* read ssl keys while superuser and outside chroot */
-       (void)setup_sslctxs(daemon, cfg);
+       setup_sslctx_remote(daemon, cfg);
+       daemon_setup_sslctxs(daemon, cfg);
 
        /* init syslog (as root) if needed, before daemonize, otherwise
         * a fork error could not be printed since daemonize closed stderr.*/
index b8252d7e4efc59906377ff2498ce0cde703ac677..cccb21c57de67db534a60547302e437b4c9fbb9d 100644 (file)
@@ -3,6 +3,11 @@
          to Yuxiao Wu, Yiyi Wang, Zhang Chao, Baojun Liu, and Haixin Duan from
          Tsinghua University.
 
+13 March 2026: Wouter
+       - Fix #278: DoT: complete unbound restart required on certificate
+         renew. Fix so that a reload checks if the files have changed, and
+         if so, reload the contexts. Also for DoH, DoQ and outgoing DoT.
+
 9 March 2026: Wouter
        - Fix compile failure in unbound-checkconf for older gcc compiler.
        - Merge #1418: Apply cache TTL policy to DNAME and synthesized
index 94c9b5edd87e530ae04617e6346342ae8fc63e00..b1d89f0932f265425a349f8f3c09f33af2f5ccea 100644 (file)
@@ -62,6 +62,9 @@
 #include "sldns/wire2str.h"
 #include "sldns/parseutil.h"
 #include "iterator/iterator.h"
+#ifdef HAVE_SYS_STAT_H
+#include <sys/stat.h>
+#endif
 #ifdef HAVE_GLOB_H
 # include <glob.h>
 #endif
@@ -2984,3 +2987,27 @@ cfg_has_quic(struct config_file* cfg)
        return 0;
 #endif
 }
+
+int
+file_get_mtime(const char* file, time_t* mtime, long* ns, int* nonexist)
+{
+       struct stat s;
+       if(stat(file, &s) != 0) {
+               *mtime = 0;
+               *ns = 0;
+               if(nonexist)
+                       *nonexist = (errno == ENOENT);
+               return 0;
+       }
+       if(nonexist)
+               *nonexist = 0;
+       *mtime = s.st_mtime;
+#ifdef HAVE_STRUCT_STAT_ST_MTIMENSEC
+       *ns = s.st_mtimensec;
+#elif defined(HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC)
+       *ns = s.st_mtim.tv_nsec;
+#else
+       *ns = 0;
+#endif
+       return 1;
+}
index aff3fd78b43ace6cdcbf689ee95c4aa899284fd9..f214d3361b075fadc7401c7ad644bac8c12a9750 100644 (file)
@@ -1493,4 +1493,7 @@ size_t getmem_str(char* str);
  */
 int cfg_ports_list_contains(char* ports, int p);
 
+/** get the file mtime stat (or error, with errno and nonexist) */
+int file_get_mtime(const char* file, time_t* mtime, long* ns, int* nonexist);
+
 #endif /* UTIL_CONFIG_FILE_H */
index 9ad9a3bb8854834bfa98db5430ce94349bbeba7a..36ed7a05e30559e664df2079a4470f121b252b16 100644 (file)
@@ -1799,7 +1799,7 @@ void ub_openssl_lock_delete(void)
 #endif /* OPENSSL_THREADS */
 }
 
-int listen_sslctx_setup_ticket_keys(struct config_strlist* tls_session_ticket_keys) {
+int listen_sslctx_setup_ticket_keys(struct config_strlist* tls_session_ticket_keys, char* chroot) {
 #ifdef HAVE_SSL
        size_t s = 1;
        struct config_strlist* p;
@@ -1817,14 +1817,18 @@ int listen_sslctx_setup_ticket_keys(struct config_strlist* tls_session_ticket_ke
                size_t n;
                unsigned char *data;
                FILE *f;
+               char* fstr;
 
                data = (unsigned char *)malloc(80);
                if(!data)
                        return 0;
 
-               f = fopen(p->str, "rb");
+               fstr = p->str;
+               if(chroot && strncmp(fstr, chroot, strlen(chroot)) == 0)
+                       fstr += strlen(chroot);
+               f = fopen(fstr, "rb");
                if(!f) {
-                       log_err("could not read tls-session-ticket-key %s: %s", p->str, strerror(errno));
+                       log_err("could not read tls-session-ticket-key %s: %s", fstr, strerror(errno));
                        free(data);
                        return 0;
                }
@@ -1832,11 +1836,11 @@ int listen_sslctx_setup_ticket_keys(struct config_strlist* tls_session_ticket_ke
                fclose(f);
 
                if(n != 80) {
-                       log_err("tls-session-ticket-key %s is %d bytes, must be 80 bytes", p->str, (int)n);
+                       log_err("tls-session-ticket-key %s is %d bytes, must be 80 bytes", fstr, (int)n);
                        free(data);
                        return 0;
                }
-               verbose(VERB_OPS, "read tls-session-ticket-key: %s", p->str);
+               verbose(VERB_OPS, "read tls-session-ticket-key: %s", fstr);
 
                keys->key_name = data;
                keys->aes_key = data + 16;
index 1f31a89c3c06d17a7299b40fa347ecd8bff09ba8..757aff8556f1e1383627329bb54b1e569cf9a2e7 100644 (file)
@@ -567,9 +567,11 @@ void ub_openssl_lock_delete(void);
 /**
  * setup TLS session ticket
  * @param tls_session_ticket_keys: TLS ticket secret filenames
+ * @param chroot: if not NULL, the chroot that is in use.
  * @return false on failure (alloc failure).
  */
-int listen_sslctx_setup_ticket_keys(struct config_strlist* tls_session_ticket_keys);
+int listen_sslctx_setup_ticket_keys(
+       struct config_strlist* tls_session_ticket_keys, char* chroot);
 
 /** Free memory used for TLS session ticket keys */
 void listen_sslctx_delete_ticket_keys(void);