From: W.C.A. Wijngaards Date: Fri, 13 Mar 2026 15:25:42 +0000 (+0100) Subject: - For #278: fast_reload can reload tls-service-key, tls-service-pem X-Git-Tag: release-1.25.0rc1~50 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=4484dc3954afb8c852bc6828c21144ec8151c750;p=thirdparty%2Funbound.git - For #278: fast_reload can reload tls-service-key, tls-service-pem and tls-cert-bundle changes. It checks the modification time of the tls-service-key and tls-service-pem files for update. --- diff --git a/daemon/daemon.c b/daemon/daemon.c index c8ec56afa..2ae7d6d05 100644 --- a/daemon/daemon.c +++ b/daemon/daemon.c @@ -221,12 +221,73 @@ setup_listen_sslctx(void** ctx, int is_dot, int is_doh, } #endif /* HAVE_SSL */ +#ifdef HAVE_SSL +void* daemon_setup_listen_dot_sslctx(struct daemon* daemon, + struct config_file* cfg) +{ + void* ctx; + (void)setup_listen_sslctx(&ctx, 1, 0, cfg, daemon->chroot); + return ctx; +} +#endif /* HAVE_SSL */ + +#ifdef HAVE_SSL +#ifdef HAVE_NGHTTP2_NGHTTP2_H +void* daemon_setup_listen_doh_sslctx(struct daemon* daemon, + struct config_file* cfg) +{ + void* ctx; + (void)setup_listen_sslctx(&ctx, 0, 1, cfg, daemon->chroot); + return ctx; +} +#endif /* HAVE_NGHTTP2_NGHTTP2_H */ +#endif /* HAVE_SSL */ + +#ifdef HAVE_SSL +#ifdef HAVE_NGTCP2 +void* daemon_setup_listen_quic_sslctx(struct daemon* daemon, + struct config_file* cfg) +{ + void* ctx; + char* chroot = daemon->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 = quic_sslctx_create(key, pem, NULL))) { + fatal_exit("could not set up quic SSL_CTX"); + } + return ctx; +} +#endif /* HAVE_NGTCP2 */ +#endif /* HAVE_SSL */ + +#ifdef HAVE_SSL +void* daemon_setup_connect_dot_sslctx(struct daemon* daemon, + struct config_file* cfg) +{ + void* ctx; + char* bundle, *chroot = daemon->chroot; + bundle = cfg->tls_cert_bundle; + if(chroot && bundle && strncmp(bundle, chroot, strlen(chroot)) == 0) + bundle += strlen(chroot); + + if(!(ctx = connect_sslctx_create(NULL, NULL, bundle, + cfg->tls_win_cert))) + fatal_exit("could not set up connect SSL_CTX"); + return 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; + char* 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; @@ -244,20 +305,18 @@ daemon_setup_sslctxs(struct daemon* daemon, struct config_file* cfg) fatal_exit("could not set session ticket SSL_CTX"); } } - (void)setup_listen_sslctx(&daemon->listen_dot_sslctx, 1, 0, - cfg, chroot); + daemon->listen_dot_sslctx = daemon_setup_listen_dot_sslctx( + daemon, cfg); #ifdef HAVE_NGHTTP2_NGHTTP2_H if(cfg_has_https(cfg)) { - (void)setup_listen_sslctx(&daemon->listen_doh_sslctx, - 0, 1, cfg, chroot); + daemon->listen_doh_sslctx = + daemon_setup_listen_doh_sslctx(daemon, cfg); } #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"); - } + daemon->listen_quic_sslctx = + daemon_setup_listen_quic_sslctx(daemon, cfg); } #endif /* HAVE_NGTCP2 */ @@ -279,12 +338,8 @@ daemon_setup_sslctxs(struct daemon* daemon, struct config_file* cfg) 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"); + daemon->connect_dot_sslctx = daemon_setup_connect_dot_sslctx( + daemon, cfg); #else /* HAVE_SSL */ (void)daemon;(void)cfg; #endif /* HAVE_SSL */ @@ -315,20 +370,28 @@ daemon_delete_sslctxs(struct daemon* daemon) #endif } -/** See if the SSL cert files have changed */ -static int +int ssl_cert_changed(struct daemon* daemon, struct config_file* cfg) { time_t mtime = 0; long ns = 0; + char* chroot = daemon->chroot; + char* key = cfg->ssl_service_key; + char* pem = cfg->ssl_service_pem; log_assert(daemon->ssl_service_key && cfg->ssl_service_key); + if(chroot && strncmp(key, chroot, strlen(chroot)) == 0) + key += strlen(chroot); + if(chroot && pem && strncmp(pem, chroot, strlen(chroot)) == 0) + pem += strlen(chroot); + if(strcmp(daemon->ssl_service_key, cfg->ssl_service_key) != 0) return 1; - if(strcmp(daemon->ssl_service_pem, cfg->ssl_service_pem) != 0) + if(daemon->ssl_service_pem && cfg->ssl_service_pem && + strcmp(daemon->ssl_service_pem, cfg->ssl_service_pem) != 0) return 1; - if(!file_get_mtime(daemon->ssl_service_key, &mtime, &ns, NULL)) { + if(!file_get_mtime(key, &mtime, &ns, NULL)) { log_err("Could not stat(%s): %s", - daemon->ssl_service_key, strerror(errno)); + key, strerror(errno)); /* It has probably changed, but file read is likely going to * fail. */ return 0; @@ -336,9 +399,9 @@ ssl_cert_changed(struct daemon* daemon, struct config_file* cfg) 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)) { + if(!file_get_mtime(pem, &mtime, &ns, NULL)) { log_err("Could not stat(%s): %s", - daemon->ssl_service_pem, strerror(errno)); + pem, strerror(errno)); /* It has probably changed, but file read is likely going to * fail. */ return 0; diff --git a/daemon/daemon.h b/daemon/daemon.h index d38cd90d8..2be8759a4 100644 --- a/daemon/daemon.h +++ b/daemon/daemon.h @@ -244,4 +244,23 @@ 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); +/** See if the SSL cert files have changed */ +int ssl_cert_changed(struct daemon* daemon, struct config_file* cfg); + +/** Setup the listening DoT SSL_CTX, returns the ssl ctx. */ +void* daemon_setup_listen_dot_sslctx(struct daemon* daemon, + struct config_file* cfg); + +/** Setup the listening DoH SSL_CTX, returns the ssl ctx. */ +void* daemon_setup_listen_doh_sslctx(struct daemon* daemon, + struct config_file* cfg); + +/** Setup the listening Quic SSL_CTX, returns the ssl ctx */ +void* daemon_setup_listen_quic_sslctx(struct daemon* daemon, + struct config_file* cfg); + +/** Setup the connect DoT SSL_CTX, returns the ssl ctx */ +void* daemon_setup_connect_dot_sslctx(struct daemon* daemon, + struct config_file* cfg); + #endif /* DAEMON_H */ diff --git a/daemon/remote.c b/daemon/remote.c index 292a7f6fa..eccbe745f 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -4633,6 +4633,26 @@ fr_init_time(struct timeval* time_start, struct timeval* time_read, * are kept in here. They can then be deleted. */ struct fast_reload_construct { + /** ssl context for listening to dnstcp over ssl */ + void* listen_dot_sslctx; + /** ssl context for connecting to dnstcp over ssl */ + void* connect_dot_sslctx; + /** ssl context for listening to DoH */ + 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; /** construct for views */ struct views* views; /** construct for auth zones */ @@ -4936,9 +4956,6 @@ fr_check_compat_cfg(struct fast_reload_thread* fr, struct config_file* newcfg) FR_CHECK_CHANGED_CFG("http_notls_downstream", http_notls_downstream, changed_str); FR_CHECK_CHANGED_CFG("https-port", https_port, changed_str); FR_CHECK_CHANGED_CFG("tls-port", ssl_port, changed_str); - FR_CHECK_CHANGED_CFG_STR("tls-service-key", ssl_service_key, changed_str); - FR_CHECK_CHANGED_CFG_STR("tls-service-pem", ssl_service_pem, changed_str); - FR_CHECK_CHANGED_CFG_STR("tls-cert-bundle", tls_cert_bundle, changed_str); FR_CHECK_CHANGED_CFG_STRLIST("proxy-protocol-port", proxy_protocol_port, changed_str); FR_CHECK_CHANGED_CFG_STRLIST("tls-additional-port", tls_additional_port, changed_str); FR_CHECK_CHANGED_CFG_STR("interface-automatic-ports", if_automatic_ports, changed_str); @@ -5047,6 +5064,19 @@ fr_construct_clear(struct fast_reload_construct* ct) wait_limits_free(&ct->wait_limits_netblock); wait_limits_free(&ct->wait_limits_cookie_netblock); domain_limits_free(&ct->domain_limits); +#ifdef HAVE_SSL + /* The SSL contexts can be SSL_CTX_free here. It is reference + * counted. So ongoing transfers with can continue. + * Once they are done, the context is freed. */ + SSL_CTX_free((SSL_CTX*)ct->listen_dot_sslctx); + SSL_CTX_free((SSL_CTX*)ct->connect_dot_sslctx); + SSL_CTX_free((SSL_CTX*)ct->listen_doh_sslctx); +#endif /* HAVE_SSL */ +#ifdef HAVE_NGTCP2 + SSL_CTX_free((SSL_CTX*)ct->listen_quic_sslctx); +#endif + free(ct->ssl_service_key); + free(ct->ssl_service_pem); /* Delete the log identity here so that the global value is not * reset by config_delete. */ if(ct->oldcfg && ct->oldcfg->log_identity) { @@ -5528,6 +5558,96 @@ auth_zones_check_changes(struct fast_reload_thread* fr, return 1; } +/** Check if the sslctxs have changed. */ +static int +fr_check_sslctx_change(struct fast_reload_thread* fr, + struct config_file* newcfg) +{ +#ifdef HAVE_SSL + struct daemon* daemon = fr->worker->daemon; + if(newcfg->ssl_service_key && newcfg->ssl_service_key[0]) { + if(!daemon->ssl_service_key || + ssl_cert_changed(daemon, newcfg)) + return 1; + } else { + if(daemon->ssl_service_key) + return 1; /* it is removed */ + } + if((daemon->cfg->tls_cert_bundle && !newcfg->tls_cert_bundle) || + (!daemon->cfg->tls_cert_bundle && newcfg->tls_cert_bundle) || + (daemon->cfg->tls_cert_bundle && newcfg->tls_cert_bundle && + strcmp(daemon->cfg->tls_cert_bundle, newcfg->tls_cert_bundle)!=0)) + return 1; /* The tls-cert-bundle has changed and return + true here makes it reload the connect_dot_sslctx. */ +#else + (void)fr; (void)newcfg; +#endif /* HAVE_SSL */ + return 0; +} + +/** Create the SSL CTXs when they have changed. */ +static int +ct_create_sslctxs(struct fast_reload_construct* ct, + struct config_file* newcfg, struct daemon* daemon) +{ +#ifdef HAVE_SSL + char* chroot = daemon->chroot; + char* key = newcfg->ssl_service_key; + char* pem = newcfg->ssl_service_pem; + + if(!(newcfg->ssl_service_key && newcfg->ssl_service_key[0])) { + /* Leave listen ctxs and file str at NULL */ + ct->connect_dot_sslctx = daemon_setup_connect_dot_sslctx( + daemon, newcfg); + return 1; + } + + if(chroot && strncmp(key, chroot, strlen(chroot)) == 0) + key += strlen(chroot); + if(chroot && pem && strncmp(pem, chroot, strlen(chroot)) == 0) + pem += strlen(chroot); + + ct->listen_dot_sslctx = daemon_setup_listen_dot_sslctx(daemon, newcfg); +#ifdef HAVE_NGHTTP2_NGHTTP2_H + if(cfg_has_https(newcfg)) { + ct->listen_doh_sslctx = daemon_setup_listen_doh_sslctx( + daemon, newcfg); + } +#endif +#ifdef HAVE_NGTCP2 + if(cfg_has_quic(newcfg)) { + ct->listen_quic_sslctx = daemon_setup_listen_quic_sslctx( + daemon, newcfg); + } +#endif /* HAVE_NGTCP2 */ + ct->connect_dot_sslctx = daemon_setup_connect_dot_sslctx(daemon, + newcfg); + + /* Store mtime and names */ + ct->ssl_service_key = strdup(newcfg->ssl_service_key); + if(!ct->ssl_service_key) { + log_err("ct_create_sslctxs: out of memory"); + return 0; + } + ct->ssl_service_pem = strdup(newcfg->ssl_service_pem); + if(!ct->ssl_service_pem) { + log_err("ct_create_sslctxs: out of memory"); + return 0; + } + if(!file_get_mtime(key, &ct->mtime_ssl_service_key, + &ct->mtime_ns_ssl_service_key, NULL)) + log_err("Could not stat(%s): %s", + key, strerror(errno)); + if(!file_get_mtime(pem, &ct->mtime_ssl_service_pem, + &ct->mtime_ns_ssl_service_pem, NULL)) + log_err("Could not stat(%s): %s", + pem, strerror(errno)); +#else + (void)ct; (void)newcfg; (void)daemon; +#endif /* HAVE_SSL */ + return 1; +} + /** fast reload thread, construct from config the new items */ static int fr_construct_from_config(struct fast_reload_thread* fr, @@ -5535,6 +5655,13 @@ fr_construct_from_config(struct fast_reload_thread* fr, { int have_view_respip_cfg = 0; + fr->sslctxs_changed = fr_check_sslctx_change(fr, newcfg); + if(fr->sslctxs_changed) { + if(!ct_create_sslctxs(ct, newcfg, fr->worker->daemon)) { + fr_construct_clear(ct); + return 0; + } + } if(!(ct->views = views_create())) { fr_construct_clear(ct); return 0; @@ -5812,6 +5939,44 @@ auth_zones_swap(struct auth_zones* az, struct auth_zones* data) * the xfer elements can continue to be their callbacks. */ } +/** Swap two void* */ +static void +void_ptr_swap(void** a, void **b) +{ + void* tmp = *a; + *a = *b; + *b = tmp; +} + +/** Swap two char* */ +static void +char_ptr_swap(char** a, char **b) +{ + char* tmp = *a; + *a = *b; + *b = tmp; +} + +/** Swap and set ssl ctx information */ +static void +sslctxs_swap(struct daemon* daemon, struct fast_reload_construct* ct) +{ + void_ptr_swap(&daemon->listen_dot_sslctx, &ct->listen_dot_sslctx); + void_ptr_swap(&daemon->connect_dot_sslctx, &ct->connect_dot_sslctx); +#ifdef HAVE_NGHTTP2_NGHTTP2_H + void_ptr_swap(&daemon->listen_doh_sslctx, &ct->listen_doh_sslctx); +#endif +#ifdef HAVE_NGTCP2 + void_ptr_swap(&daemon->listen_quic_sslctx, &ct->listen_quic_sslctx); +#endif /* HAVE_NGTCP2 */ + char_ptr_swap(&daemon->ssl_service_key, &ct->ssl_service_key); + char_ptr_swap(&daemon->ssl_service_pem, &ct->ssl_service_pem); + daemon->mtime_ssl_service_key = ct->mtime_ssl_service_key; + daemon->mtime_ns_ssl_service_key = ct->mtime_ns_ssl_service_key; + daemon->mtime_ssl_service_pem = ct->mtime_ssl_service_pem; + daemon->mtime_ns_ssl_service_pem = ct->mtime_ns_ssl_service_pem; +} + #if defined(ATOMIC_POINTER_LOCK_FREE) && defined(HAVE_LINK_ATOMIC_STORE) /** Fast reload thread, if atomics are available, copy the config items * one by one with atomic store operations. */ @@ -6420,6 +6585,9 @@ fr_reload_config(struct fast_reload_thread* fr, struct config_file* newcfg, daemon->env->cachedb_enabled = cachedb_is_enabled(&daemon->mods, daemon->env); #endif + if(fr->sslctxs_changed) { + sslctxs_swap(daemon, ct); + } #ifdef USE_DNSTAP if(env->cfg->dnstap) { if(!fr->fr_nopause) @@ -7616,6 +7784,40 @@ fr_worker_pickup_auth_changes(struct worker* worker, } } +/** Fast reload, the worker picks up changes in listen_dnsport. */ +static void +fr_worker_pickup_listen_dnsport(struct worker* worker) +{ + struct listen_dnsport* front = worker->front; + struct daemon* daemon = worker->daemon; + if(worker->daemon->fast_reload_thread->sslctxs_changed) { + struct listen_list* ll; + void* dot_sslctx = daemon->listen_dot_sslctx; + void* doh_sslctx = daemon->listen_doh_sslctx; + void* quic_sslctx = daemon->listen_quic_sslctx; + for(ll = front->cps; ll; ll = ll->next) { + struct comm_point* cp = ll->com; + if(cp->type == comm_tcp_accept && + cp->pp2_enabled /* true for http */) { + if(cp->ssl) + cp->ssl = doh_sslctx; + } else if(cp->type == comm_tcp_accept) { + if(cp->ssl) + cp->ssl = dot_sslctx; +#ifdef HAVE_NGTCP2 + } else if(cp->type == comm_doq) { + if(cp->ssl) { + cp->ssl = quic_sslctx; + if(cp->doq_socket) + cp->doq_socket->ctx = + (SSL_CTX*)quic_sslctx; + } +#endif + } + } + } +} + /** Fast reload, the worker picks up changes in outside_network. */ static void fr_worker_pickup_outside_network(struct worker* worker) @@ -7631,6 +7833,8 @@ fr_worker_pickup_outside_network(struct worker* worker) outnet->tcp_reuse_timeout = cfg->tcp_reuse_timeout; outnet->tcp_auth_query_timeout = cfg->tcp_auth_query_timeout; outnet->delayclose = cfg->delay_close; + if(worker->daemon->fast_reload_thread->sslctxs_changed) + outnet->sslctx = worker->daemon->connect_dot_sslctx; if(outnet->delayclose) { #ifndef S_SPLINT_S outnet->delay_tv.tv_sec = cfg->delay_close/1000; @@ -7702,6 +7906,7 @@ fast_reload_worker_pickup_changes(struct worker* worker) worker->env.cachedb_enabled = worker->daemon->env->cachedb_enabled; #endif fr_worker_pickup_outside_network(worker); + fr_worker_pickup_listen_dnsport(worker); #ifdef USE_DNSTAP fr_worker_pickup_dnstap_changes(worker); #endif diff --git a/daemon/remote.h b/daemon/remote.h index 77c00a597..e9c392fd8 100644 --- a/daemon/remote.h +++ b/daemon/remote.h @@ -255,6 +255,8 @@ struct fast_reload_thread { struct fast_reload_auth_change* auth_zone_change_list; /** the old tree of auth zones, to lookup. */ struct auth_zones* old_auth_zones; + /** If the ssl ctxs have changed. */ + int sslctxs_changed; }; /** diff --git a/doc/Changelog b/doc/Changelog index 37b98f670..f9b6df967 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -8,6 +8,9 @@ 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. - iana portlist updated. + - For #278: fast_reload can reload tls-service-key, tls-service-pem + and tls-cert-bundle changes. It checks the modification time of + the tls-service-key and tls-service-pem files for update. 9 March 2026: Wouter - Fix compile failure in unbound-checkconf for older gcc compiler. diff --git a/doc/unbound-control.rst b/doc/unbound-control.rst index 2f5520824..d95d6dd0b 100644 --- a/doc/unbound-control.rst +++ b/doc/unbound-control.rst @@ -170,6 +170,8 @@ There are several commands that the server understands. :ref:`tcp-auth-query-timeout`, :ref:`delay-close`. :ref:`iter-scrub-promiscuous`. + :ref:`tls-service-key`. + :ref:`tls-service-pem`. It does not work with :ref:`interface` and diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index f02087598..8f7b24e83 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -1048,9 +1048,13 @@ These options are part of the ``server:`` section. certificate is in the :ref:`tls-service-pem` file and it must also be specified if :ref:`tls-service-key` is specified. - Enabling or disabling this service requires a restart (a reload is not - enough), because the key is read while root permissions are held and before - chroot (if any). + If the key is stored with root permissions or outside of chroot, then + a change or enabling or disabling requires a restart (a reload is not + enough). + But if the key file (and tls-service-pem file) are accessible, then they + are read in on reload, and fast_reload. + The server checks the modification time of the file (and the filename) + to see if the file has changed for reload. The ports enabled implicitly or explicitly via :ref:`tls-port` and :ref:`https-port` do not provide normal DNS TCP