From: W.C.A. Wijngaards Date: Fri, 13 Mar 2026 10:42:34 +0000 (+0100) Subject: - Fix #278: DoT: complete unbound restart required on certificate X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2eff1d8ab54f14345f28eb7e3050a44a63a1c008;p=thirdparty%2Funbound.git - 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. --- diff --git a/config.h.in b/config.h.in index 5ffb7c43c..3372ae4dc 100644 --- a/config.h.in +++ b/config.h.in @@ -735,6 +735,12 @@ /* 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 diff --git a/configure b/configure index 3e4e92200..62bc2496c 100755 --- 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" " diff --git a/configure.ac b/configure.ac index 7317aad60..bc1274c08 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/daemon/daemon.c b/daemon/daemon.c index 5ee12e0db..c8ec56afa 100644 --- a/daemon/daemon.c +++ b/daemon/daemon.c @@ -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(); diff --git a/daemon/daemon.h b/daemon/daemon.h index 2295761ab..d38cd90d8 100644 --- a/daemon/daemon.h +++ b/daemon/daemon.h @@ -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 */ diff --git a/daemon/unbound.c b/daemon/unbound.c index 688804743..a787b76fe 100644 --- a/daemon/unbound.c +++ b/daemon/unbound.c @@ -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.*/ diff --git a/doc/Changelog b/doc/Changelog index b8252d7e4..cccb21c57 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -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 diff --git a/util/config_file.c b/util/config_file.c index 94c9b5edd..b1d89f093 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -62,6 +62,9 @@ #include "sldns/wire2str.h" #include "sldns/parseutil.h" #include "iterator/iterator.h" +#ifdef HAVE_SYS_STAT_H +#include +#endif #ifdef HAVE_GLOB_H # include #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; +} diff --git a/util/config_file.h b/util/config_file.h index aff3fd78b..f214d3361 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -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 */ diff --git a/util/net_help.c b/util/net_help.c index 9ad9a3bb8..36ed7a05e 100644 --- a/util/net_help.c +++ b/util/net_help.c @@ -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; diff --git a/util/net_help.h b/util/net_help.h index 1f31a89c3..757aff855 100644 --- a/util/net_help.h +++ b/util/net_help.h @@ -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);