From 8f96ae7acf8275d40b49e06ac41242dc02c191ac Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Thu, 5 Mar 2026 09:47:13 +0100 Subject: [PATCH 01/18] - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound. Thanks to Kunta Chu, School of Software, Tsinghua University, Taofei Guo, Peking University, and Jianjun Chen, Institute for Network Sciences and Cyberspace, Tsinghua University for the report. The private-address option is fixed to also elide SVCB and HTTPS records that match the filter. --- doc/Changelog | 8 + doc/unbound.conf.rst | 5 + iterator/iter_priv.c | 168 ++++++++++++++++++++- iterator/iter_scrub.c | 6 +- testdata/iter_priv_svcb.rpl | 283 ++++++++++++++++++++++++++++++++++++ 5 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 testdata/iter_priv_svcb.rpl diff --git a/doc/Changelog b/doc/Changelog index 19137495b..0230be806 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,11 @@ +5 March 2026: Wouter + - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound. + Thanks to Kunta Chu, School of Software, Tsinghua University, + Taofei Guo, Peking University, and Jianjun Chen, Institute for + Network Sciences and Cyberspace, Tsinghua University for the + report. The private-address option is fixed to also elide + SVCB and HTTPS records that match the filter. + 4 March 2026: Yorgos - For #1411: Introduce a failing case in the rpl test so that it only passes with the fix in place. diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index 0d406f689..fd4a7969c 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -2015,6 +2015,11 @@ These options are part of the ``server:`` section. turned into a network proxy, allowing remote access through the browser to other parts of your private network. + The option removes resource records of types A, AAAA, SVCB and HTTPS + that match the filter. + Inside the SVCB and HTTPS records, the svcparams of type ipv4hint + and ipv6hint are checked for matches. + Some names can be allowed to contain your private addresses, by default all the :ref:`local-data` that you configured is allowed to, and you can specify additional names using diff --git a/iterator/iter_priv.c b/iterator/iter_priv.c index be4219216..6f885cec7 100644 --- a/iterator/iter_priv.c +++ b/iterator/iter_priv.c @@ -207,6 +207,168 @@ size_t priv_get_mem(struct iter_priv* priv) return sizeof(*priv) + regional_get_mem(priv->region); } +/** + * Check if svcparam ipv4hint contains a private address. + * @param priv: private address lookup struct. + * @param d: the data bytes. + * @param data_len: number of data bytes in the svcparam. + * @param addr: address to return the private address to log in to. + * It has space for IPv4 and IPv6 addresses. + * @param addrlen: length of the addr. Returns the correct size for the addr. + * @return true if the rdata contains a private address. + */ +static int svcb_ipv4hint_contains_priv_addr(struct iter_priv* priv, + uint8_t* d, uint16_t data_len, struct sockaddr_storage* addr, + socklen_t* addrlen) +{ + struct sockaddr_in sa; + *addrlen = (socklen_t)sizeof(struct sockaddr_in); + memset(&sa, 0, sizeof(struct sockaddr_in)); + sa.sin_family = AF_INET; + sa.sin_port = (in_port_t)htons(UNBOUND_DNS_PORT); + + while(data_len >= LDNS_IP4ADDRLEN) { + memmove(&sa.sin_addr, d, LDNS_IP4ADDRLEN); + memmove(addr, &sa, *addrlen); + if(priv_lookup_addr(priv, addr, *addrlen)) + return 1; + + d += LDNS_IP4ADDRLEN; + data_len -= LDNS_IP4ADDRLEN; + } + /* if data_len != 0 here, then the svcparam is malformed. */ + return 0; +} + +/** + * Check if svcparam ipv6hint contains a private address. + * @param priv: private address lookup struct. + * @param d: the data bytes. + * @param data_len: number of data bytes in the svcparam. + * @param addr: address to return the private address to log in to. + * It has space for IPv4 and IPv6 addresses. + * @param addrlen: length of the addr. Returns the correct size for the addr. + * @return true if the rdata contains a private address. + */ +static int svcb_ipv6hint_contains_priv_addr(struct iter_priv* priv, + uint8_t* d, uint16_t data_len, struct sockaddr_storage* addr, + socklen_t* addrlen) +{ + struct sockaddr_in6 sa; + *addrlen = (socklen_t)sizeof(struct sockaddr_in6); + memset(&sa, 0, sizeof(struct sockaddr_in6)); + sa.sin6_family = AF_INET6; + sa.sin6_port = (in_port_t)htons(UNBOUND_DNS_PORT); + + while(data_len >= LDNS_IP6ADDRLEN) { + memmove(&sa.sin6_addr, d, LDNS_IP6ADDRLEN); + memmove(addr, &sa, *addrlen); + if(priv_lookup_addr(priv, addr, *addrlen)) + return 1; + + d += LDNS_IP6ADDRLEN; + data_len -= LDNS_IP6ADDRLEN; + } + /* if data_len != 0 here, then the svcparam is malformed. */ + return 0; +} + +/** + * Check if type SVCB and HTTPS rdata contains a private address. + * @param priv: private address lookup struct. + * @param pkt: the packet. + * @param rr: the rr with rdata to check. + * @param addr: address to return the private address to log in to. + * @param addrlen: length of the addr. Initially the total size, on + * return the correct size for the addr. + * @return true if the rdata contains a private address. + */ +static int svcb_rr_contains_priv_addr(struct iter_priv* priv, + sldns_buffer* pkt, struct rr_parse* rr, struct sockaddr_storage* addr, + socklen_t* addrlen) +{ + uint8_t* d = rr->ttl_data; + uint16_t svcparamkey, data_len, rdatalen; + size_t oldpos, dname_len, dname_start, dname_compr_len; + d += 4; /* skip TTL */ + rdatalen = sldns_read_uint16(d); /* read rdata length */ + d += 2; + + if(rdatalen < 2 /* priority */ + 1 /* 1 length target */) + return 0; /* malformed, too short */ + d += 2; /* skip priority */ + rdatalen -= 2; + oldpos = sldns_buffer_position(pkt); + sldns_buffer_set_position(pkt, (size_t)(d - sldns_buffer_begin(pkt))); + dname_start = sldns_buffer_position(pkt); + dname_len = pkt_dname_len(pkt); + dname_compr_len = sldns_buffer_position(pkt) - dname_start; + sldns_buffer_set_position(pkt, oldpos); + if(dname_len == 0) + return 0; /* dname malformed */ + if(dname_compr_len > rdatalen) + return 0; /* malformed */ + d += dname_compr_len; /* skip target */ + rdatalen -= dname_compr_len; + + while(rdatalen >= 4) { + svcparamkey = sldns_read_uint16(d); + data_len = sldns_read_uint16(d+2); + d += 4; + rdatalen -= 4; + + /* verify that we have data_len data */ + if(data_len > rdatalen) { + /* It is malformed, but if there are addresses + * in there it can be rejected. */ + data_len = rdatalen; + } + + if(!data_len) + continue; /* no data for the svcparamkey */ + + if(svcparamkey == SVCB_KEY_IPV4HINT) { + if(svcb_ipv4hint_contains_priv_addr(priv, d, data_len, + addr, addrlen)) + return 1; + } else if(svcparamkey == SVCB_KEY_IPV6HINT) { + if(svcb_ipv6hint_contains_priv_addr(priv, d, data_len, + addr, addrlen)) + return 1; + } + d += data_len; + rdatalen -= data_len; + } + /* If rdatalen != 0 here, then the svcb rdata is malformed. */ + return 0; +} + +/** + * Check if the SVCB and HTTPS rrset is bad. + * @param priv: private address lookup struct. + * @param pkt: the packet. + * @param rrset: the rrset to check. + * @return 1 if the entire rrset has to be removed. 0 if not. + * It removes RRs if they have private addresses, and log that. + */ +static int priv_svcb_rrset_bad(struct iter_priv* priv, sldns_buffer* pkt, + struct rrset_parse* rrset) +{ + struct rr_parse* rr, *prev = NULL; + struct sockaddr_storage addr; + socklen_t addrlen = (socklen_t)sizeof(addr); + for(rr = rrset->rr_first; rr; rr = rr->next) { + if(svcb_rr_contains_priv_addr(priv, pkt, rr, &addr, + &addrlen)) { + if(msgparse_rrset_remove_rr("sanitize: removing public name with private address", pkt, rrset, prev, rr, &addr, addrlen)) + return 1; + continue; + } + prev = rr; + } + return 0; +} + int priv_rrset_bad(struct iter_priv* priv, sldns_buffer* pkt, struct rrset_parse* rrset) { @@ -268,7 +430,11 @@ int priv_rrset_bad(struct iter_priv* priv, sldns_buffer* pkt, } prev = rr; } - } + } else if(rrset->type == LDNS_RR_TYPE_SVCB || + rrset->type == LDNS_RR_TYPE_HTTPS) { + if(priv_svcb_rrset_bad(priv, pkt, rrset)) + return 1; + } } return 0; } diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index 8507a3fb6..a4b98375b 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -972,8 +972,10 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg, } /* remove private addresses */ - if( (rrset->type == LDNS_RR_TYPE_A || - rrset->type == LDNS_RR_TYPE_AAAA)) { + if(rrset->type == LDNS_RR_TYPE_A || + rrset->type == LDNS_RR_TYPE_AAAA || + rrset->type == LDNS_RR_TYPE_SVCB || + rrset->type == LDNS_RR_TYPE_HTTPS) { /* do not set servfail since this leads to too * many drops of other people using rfc1918 space */ diff --git a/testdata/iter_priv_svcb.rpl b/testdata/iter_priv_svcb.rpl new file mode 100644 index 000000000..5deae4250 --- /dev/null +++ b/testdata/iter_priv_svcb.rpl @@ -0,0 +1,283 @@ +; config options +server: + target-fetch-policy: "0 0 0 0 0" + qname-minimisation: no + minimal-responses: yes + iter-scrub-promiscuous: yes + + private-address: 10.0.0.0/8 + private-address: 172.16.0.0/12 + private-address: 192.168.0.0/16 + private-address: 169.254.0.0/16 + private-address: fd00::/8 + private-address: fe80::/10 + + private-domain: "example.net" + +stub-zone: + name: "." + stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET. + +CONFIG_END + +SCENARIO_BEGIN Test iterator scrubber with private addresses in SVCB. + +; K.ROOT-SERVERS.NET. +RANGE_BEGIN 0 100 + ADDRESS 193.0.14.129 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +. IN NS +SECTION ANSWER +. IN NS K.ROOT-SERVERS.NET. +SECTION ADDITIONAL +K.ROOT-SERVERS.NET. IN A 193.0.14.129 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode subdomain +ADJUST copy_id copy_query +REPLY QR NOERROR +SECTION QUESTION +com. IN A +SECTION AUTHORITY +com. IN NS a.gtld-servers.net. +SECTION ADDITIONAL +a.gtld-servers.net. IN A 192.5.6.30 +ENTRY_END + +; root server authoritative for example.net too. +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +mail.example.net. IN SVCB +SECTION ANSWER +mail.example.net. IN SVCB 1 foo.example.net. ipv4hint=10.20.30.40 +ENTRY_END +RANGE_END + +; a.gtld-servers.net. +RANGE_BEGIN 0 100 + ADDRESS 192.5.6.30 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +com. IN NS +SECTION ANSWER +com. IN NS a.gtld-servers.net. +SECTION ADDITIONAL +a.gtld-servers.net. IN A 192.5.6.30 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode subdomain +ADJUST copy_id copy_query +REPLY QR NOERROR +SECTION QUESTION +example.com. IN A +SECTION AUTHORITY +example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 1.2.3.4 +ENTRY_END +RANGE_END + +; ns.example.com. +RANGE_BEGIN 0 100 + ADDRESS 1.2.3.4 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +example.com. IN NS +SECTION ANSWER +example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 1.2.3.4 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +ns.example.com. IN A +SECTION ANSWER +ns.example.com. IN A 1.2.3.4 +SECTION AUTHORITY +example.com. IN NS ns.example.com. +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +ns.example.com. IN AAAA +SECTION ANSWER +SECTION AUTHORITY +example.com. IN SOA ns.example.com. root.example.com. 4 14400 3600 604800 3600 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +www.example.com. IN SVCB +SECTION ANSWER +www.example.com. IN SVCB 1 foo.example.com. ipv4hint=192.20.30.40 +SECTION AUTHORITY +example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 1.2.3.4 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +mail.example.com. IN SVCB +SECTION ANSWER +mail.example.com. IN SVCB 1 foo.example.com. ipv6hint=fe80::15 +SECTION AUTHORITY +example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 1.2.3.4 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +foo.example.com. IN SVCB +SECTION ANSWER +foo.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.40 +SECTION AUTHORITY +example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 1.2.3.4 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +toss.example.com. IN SVCB +SECTION ANSWER +toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.40 +toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.40 +toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=1.2.3.4 +toss.example.com. IN SVCB 1 foo.example.com. ipv6hint=fe80::15 +toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=10.20.30.41 +toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=192.0.2.1,10.20.30.42,192.0.2.2 +SECTION AUTHORITY +example.com. IN NS ns.example.com. +SECTION ADDITIONAL +ns.example.com. IN A 1.2.3.4 +ENTRY_END +RANGE_END + +; public address is not scrubbed +STEP 1 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +www.example.com. IN SVCB +ENTRY_END + +; recursion happens here. +STEP 2 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +www.example.com. IN SVCB +SECTION ANSWER +www.example.com. IN SVCB 1 foo.example.com. ipv4hint=192.20.30.40 +ENTRY_END + +; IPv4 address is scrubbed +STEP 3 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +foo.example.com. IN SVCB +ENTRY_END + +; recursion happens here. +STEP 10 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +foo.example.com. IN SVCB +SECTION ANSWER +; scrubbed away +ENTRY_END + +; IPv6 address is scrubbed +STEP 20 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +mail.example.com. IN SVCB +ENTRY_END + +STEP 30 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +mail.example.com. IN SVCB +SECTION ANSWER +ENTRY_END + +; allowed domain is not scrubbed. +STEP 40 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +mail.example.net. IN SVCB +ENTRY_END + +STEP 50 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +mail.example.net. IN SVCB +SECTION ANSWER +mail.example.net. IN SVCB 1 foo.example.net. ipv4hint=10.20.30.40 +ENTRY_END + +; rest of RRset intact, only 10/8 tossed away. +STEP 60 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +toss.example.com. IN SVCB +ENTRY_END + +STEP 70 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +toss.example.com. IN SVCB +SECTION ANSWER +toss.example.com. IN SVCB 1 foo.example.com. ipv4hint=1.2.3.4 +ENTRY_END + +SCENARIO_END From 1bd7c8dfeedba3087c88c75d1b58c2c58986792d Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Thu, 5 Mar 2026 12:15:54 +0100 Subject: [PATCH 02/18] - Update generated man pages. --- doc/Changelog | 1 + doc/unbound.conf.5.in | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index 0230be806..2581a3cdc 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -5,6 +5,7 @@ Network Sciences and Cyberspace, Tsinghua University for the report. The private-address option is fixed to also elide SVCB and HTTPS records that match the filter. + - Update generated man pages. 4 March 2026: Yorgos - For #1411: Introduce a failing case in the rpl test so that it only diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index 194a3c076..b8c601c79 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -2275,6 +2275,11 @@ This protects against so\-called DNS Rebinding, where a user browser is turned into a network proxy, allowing remote access through the browser to other parts of your private network. .sp +The option removes resource records of types A, AAAA, SVCB and HTTPS +that match the filter. +Inside the SVCB and HTTPS records, the svcparams of type ipv4hint +and ipv6hint are checked for matches. +.sp Some names can be allowed to contain your private addresses, by default all the \fI\%local\-data\fP that you configured is allowed to, and you can specify additional names using From da3812953ac96acb23dea03a692f043ccf354d51 Mon Sep 17 00:00:00 2001 From: Andy Warner Date: Fri, 6 Mar 2026 00:57:54 -0700 Subject: [PATCH 03/18] Add lock unlock for view in memory error handling (#1415) view->lock would be skipped during an out of memory error bailout. --- respip/respip.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/respip/respip.c b/respip/respip.c index f39ce5805..ff12114de 100644 --- a/respip/respip.c +++ b/respip/respip.c @@ -973,6 +973,9 @@ respip_rewrite_reply(const struct query_info* qinfo, lock_rw_unlock(&raddr->lock); lock_rw_unlock(&a->lock); lock_rw_unlock(&az->rpz_lock); + if(view) { + lock_rw_unlock(&view->lock); + } return 0; } if(rpz_used) { From 92ab54641e2b20c08bc89363b185bd3f3ea2e221 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 6 Mar 2026 08:58:33 +0100 Subject: [PATCH 04/18] Changelog entry for #1415 - Merge #1415: Add lock unlock for view in memory error handling. --- doc/Changelog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index 2581a3cdc..7a44ffb90 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,6 @@ +6 March 2026: Wouter + - Merge #1415: Add lock unlock for view in memory error handling. + 5 March 2026: Wouter - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound. Thanks to Kunta Chu, School of Software, Tsinghua University, From 683241a2f5a2965063e44f1bd0515d06bf508631 Mon Sep 17 00:00:00 2001 From: Yorgos Thessalonikefs Date: Fri, 6 Mar 2026 17:01:41 +0100 Subject: [PATCH 05/18] - Document the suggestion for a higher value for 'outgoing-range'; helps when the request list is full. --- doc/Changelog | 4 ++++ doc/unbound.conf.5.in | 4 ++++ doc/unbound.conf.rst | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index 7a44ffb90..eaa8e6f83 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,6 +1,10 @@ 6 March 2026: Wouter - Merge #1415: Add lock unlock for view in memory error handling. +6 March 2026: Yorgos + - Document the suggestion for a higher value for 'outgoing-range'; + helps when the request list is full. + 5 March 2026: Wouter - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound. Thanks to Kunta Chu, School of Software, Tsinghua University, diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index b8c601c79..593a76189 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -382,6 +382,10 @@ Default depends on compile options. Larger numbers need extra resources from the operating system. For performance a very large value is best, use libevent to make this possible. +Should be higher (preferably double) than the value of +\fI\%num\-queries\-per\-thread\fP to +account for cases where the request list is full and avoid file descriptor +starvation. .sp Default: 4096 (libevent) / 960 (minievent) / 48 (windows) .UNINDENT diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index fd4a7969c..f02087598 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -366,6 +366,10 @@ These options are part of the ``server:`` section. Larger numbers need extra resources from the operating system. For performance a very large value is best, use libevent to make this possible. + Should be higher (preferably double) than the value of + :ref:`num-queries-per-thread` to + account for cases where the request list is full and avoid file descriptor + starvation. Default: 4096 (libevent) / 960 (minievent) / 48 (windows) From 94ef1a8feeb918bc25325de5f3d4354bbef774a4 Mon Sep 17 00:00:00 2001 From: Yorgos Thessalonikefs Date: Fri, 6 Mar 2026 17:05:57 +0100 Subject: [PATCH 06/18] - Warn for unused 'nodefault' local-zone configuration in unbound-checkconf (related to #1416). --- doc/Changelog | 2 + services/localzone.c | 126 +++++++++++++++++++---------------- services/localzone.h | 3 + smallapp/unbound-checkconf.c | 45 +++++++++++++ util/configparser.y | 2 +- 5 files changed, 118 insertions(+), 60 deletions(-) diff --git a/doc/Changelog b/doc/Changelog index eaa8e6f83..90223791c 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -4,6 +4,8 @@ 6 March 2026: Yorgos - Document the suggestion for a higher value for 'outgoing-range'; helps when the request list is full. + - Warn for unused 'nodefault' local-zone configuration in + unbound-checkconf (related to #1416). 5 March 2026: Wouter - Fix for DNS Rebinding Bypass via SVCB/HTTPS Records in Unbound. diff --git a/services/localzone.c b/services/localzone.c index ccbe0d522..52166ae2d 100644 --- a/services/localzone.c +++ b/services/localzone.c @@ -56,6 +56,24 @@ * with 16 bytes for an A record, a 64K packet has about 4000 max */ #define LOCALZONE_RRSET_COUNT_MAX 4096 +static const char* default_zones_reverse_array[] = { + "127.in-addr.arpa.", /* reverse ip4 zone */ + "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.", /* reverse ip6 zone */ + 0 +}; +const char** local_zones_default_reverse = default_zones_reverse_array; + +static const char* default_zones_special_array[] = { + "test.", /* RFC 6761 */ + "invalid.", /* RFC 6761 */ + "onion.", /* RFC 7686 */ + "home.arpa.", /* RFC 8375 */ + "resolver.arpa.", /* RFC 9462 */ + "service.arpa.", /* RFC 9665 */ + 0 +}; +const char** local_zones_default_special = default_zones_special_array; + /** print all RRsets in local zone */ static void local_zone_out(struct local_zone* z) @@ -834,7 +852,7 @@ lz_nodefault(struct config_file* cfg, const char* name) for(p = cfg->local_zones_nodefault; p; p = p->next) { /* compare zone name, lowercase, compare without ending . */ - if(strncasecmp(p->str, name, len) == 0 && + if(strncasecmp(p->str, name, len) == 0 && (strlen(p->str) == len || (strlen(p->str)==len+1 && p->str[len] == '.'))) return 1; @@ -842,6 +860,45 @@ lz_nodefault(struct config_file* cfg, const char* name) return 0; } +/** enter reverse default zone */ +static int +add_reverse_default(struct local_zones* zones, struct config_file* cfg, + const char* name) +{ + struct local_zone* z; + char str[1024]; /* known long enough */ + if(lz_exists(zones, name) || lz_nodefault(cfg, name)) + return 1; /* do not enter default content */ + if(!(z=lz_enter_zone(zones, name, "static", LDNS_RR_CLASS_IN))) + return 0; + snprintf(str, sizeof(str), "%s 10800 IN SOA localhost. " + "nobody.invalid. 1 3600 1200 604800 10800", name); + if(!lz_enter_rr_into_zone(z, str)) { + lock_rw_unlock(&z->lock); + return 0; + } + snprintf(str, sizeof(str), "%s 10800 IN NS localhost. ", name); + if(!lz_enter_rr_into_zone(z, str)) { + lock_rw_unlock(&z->lock); + return 0; + } + if(strncasecmp("127.in-addr.arpa.", name, 17) == 0) { + if(!lz_enter_rr_into_zone(z, + "1.0.0.127.in-addr.arpa. 10800 IN PTR localhost.")) { + lock_rw_unlock(&z->lock); + return 0; + } + } else if(strncasecmp("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.", name, 73) == 0) { + snprintf(str, sizeof(str), "%s 10800 IN PTR localhost.", name); + if(!lz_enter_rr_into_zone(z, str)) { + lock_rw_unlock(&z->lock); + return 0; + } + } + lock_rw_unlock(&z->lock); + return 1; +} + /** enter (AS112) empty default zone */ static int add_empty_default(struct local_zones* zones, struct config_file* cfg, @@ -902,72 +959,23 @@ int local_zone_enter_defaults(struct local_zones* zones, struct config_file* cfg } lock_rw_unlock(&z->lock); } - /* reverse ip4 zone */ - if(!lz_exists(zones, "127.in-addr.arpa.") && - !lz_nodefault(cfg, "127.in-addr.arpa.")) { - if(!(z=lz_enter_zone(zones, "127.in-addr.arpa.", "static", - LDNS_RR_CLASS_IN)) || - !lz_enter_rr_into_zone(z, - "127.in-addr.arpa. 10800 IN NS localhost.") || - !lz_enter_rr_into_zone(z, - "127.in-addr.arpa. 10800 IN SOA localhost. " - "nobody.invalid. 1 3600 1200 604800 10800") || - !lz_enter_rr_into_zone(z, - "1.0.0.127.in-addr.arpa. 10800 IN PTR localhost.")) { + + /* ip4 and ip6 reverse */ + for(zstr = local_zones_default_reverse; *zstr; zstr++) { + if(!add_reverse_default(zones, cfg, *zstr)) { log_err("out of memory adding default zone"); - if(z) { lock_rw_unlock(&z->lock); } return 0; } - lock_rw_unlock(&z->lock); } - /* reverse ip6 zone */ - if(!lz_exists(zones, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.") && - !lz_nodefault(cfg, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.")) { - if(!(z=lz_enter_zone(zones, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.", "static", - LDNS_RR_CLASS_IN)) || - !lz_enter_rr_into_zone(z, - "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 10800 IN NS localhost.") || - !lz_enter_rr_into_zone(z, - "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 10800 IN SOA localhost. " - "nobody.invalid. 1 3600 1200 604800 10800") || - !lz_enter_rr_into_zone(z, - "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 10800 IN PTR localhost.")) { + + /* special-use zones */ + for(zstr = local_zones_default_special; *zstr; zstr++) { + if(!add_empty_default(zones, cfg, *zstr)) { log_err("out of memory adding default zone"); - if(z) { lock_rw_unlock(&z->lock); } return 0; } - lock_rw_unlock(&z->lock); - } - /* home.arpa. zone (RFC 8375) */ - if(!add_empty_default(zones, cfg, "home.arpa.")) { - log_err("out of memory adding default zone"); - return 0; - } - /* resolver.arpa. zone (RFC 9462) */ - if(!add_empty_default(zones, cfg, "resolver.arpa.")) { - log_err("out of memory adding default zone"); - return 0; - } - /* service.arpa. zone (draft-ietf-dnssd-srp-25) */ - if(!add_empty_default(zones, cfg, "service.arpa.")) { - log_err("out of memory adding default zone"); - return 0; - } - /* onion. zone (RFC 7686) */ - if(!add_empty_default(zones, cfg, "onion.")) { - log_err("out of memory adding default zone"); - return 0; - } - /* test. zone (RFC 6761) */ - if(!add_empty_default(zones, cfg, "test.")) { - log_err("out of memory adding default zone"); - return 0; - } - /* invalid. zone (RFC 6761) */ - if(!add_empty_default(zones, cfg, "invalid.")) { - log_err("out of memory adding default zone"); - return 0; } + /* block AS112 zones, unless asked not to */ if(!cfg->unblock_lan_zones) { for(zstr = as112_zones; *zstr; zstr++) { diff --git a/services/localzone.h b/services/localzone.h index 3dc89b058..76c011836 100644 --- a/services/localzone.h +++ b/services/localzone.h @@ -57,6 +57,9 @@ struct sldns_buffer; struct comm_reply; struct config_strlist; +extern const char** local_zones_default_special; +extern const char** local_zones_default_reverse; + /** * Local zone type * This type determines processing for queries that did not match diff --git a/smallapp/unbound-checkconf.c b/smallapp/unbound-checkconf.c index 91bc558dd..399c2fce9 100644 --- a/smallapp/unbound-checkconf.c +++ b/smallapp/unbound-checkconf.c @@ -44,6 +44,7 @@ #include "config.h" #include +#include "util/as112.h" #include "util/log.h" #include "util/config_file.h" #include "util/module.h" @@ -188,11 +189,55 @@ donotquerylocalhostcheck(struct config_file* cfg) } } +static void +nodefaultzonescheck(struct config_file* cfg) +{ + struct config_strlist* d; + const char** zstr; + size_t len; + +#define COMPARE_ZONE_NAME(confname, builtname, len) \ + (strncasecmp(confname, builtname, (len)) == 0 && \ + (strlen(confname) == (len) || \ + (strlen(confname) == (len) + 1 \ + && confname[(len)] == '.'))) + + for(d = cfg->local_zones_nodefault; d; d = d->next) { + if(!cfg->unblock_lan_zones) { + for(zstr = as112_zones; *zstr; zstr++) { + len = strlen(*zstr) - 1; /* trailing '.' */ + if(COMPARE_ZONE_NAME(d->str, *zstr, len)) + goto default_continue; + } + } + for(zstr = local_zones_default_special; *zstr; zstr++) { + len = strlen(*zstr) - 1; /* trailing '.' */ + if(COMPARE_ZONE_NAME(d->str, *zstr, len)) + goto default_continue; + } + for(zstr = local_zones_default_reverse; *zstr; zstr++) { + len = strlen(*zstr) - 1; /* trailing '.' */ + if(COMPARE_ZONE_NAME(d->str, *zstr, len)) + goto default_continue; + } + if(COMPARE_ZONE_NAME(d->str, "localhost.", 10 - 1)) + goto default_continue; + fprintf(stderr, "unbound-checkconf: warning: local-zone: '%s' " + "is configured as 'nodefault' but there is no such " + "default local-zone. Check the unbound.conf " + "documentation for default configured local-zones.\n", + d->str); +default_continue: + } +#undef COMPARE_ZONE_NAME +} + /** check localzones */ static void localzonechecks(struct config_file* cfg) { struct local_zones* zs; + nodefaultzonescheck(cfg); if(!(zs = local_zones_create())) fatal_exit("out of memory"); if(!local_zones_apply_cfg(zs, cfg)) diff --git a/util/configparser.y b/util/configparser.y index d9a7cd839..aa787fdce 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -2399,7 +2399,7 @@ server_local_zone: VAR_LOCAL_ZONE STRING_ARG STRING_ARG yyerror("local-zone type: expected static, deny, " "refuse, redirect, transparent, " "typetransparent, inform, inform_deny, " - "inform_redirect, always_transparent, block_a," + "inform_redirect, always_transparent, block_a, " "always_refuse, always_nxdomain, " "always_nodata, always_deny, always_null, " "noview, nodefault or ipset"); From 76ef8c58037b2ac979a1fe68a914cef5dab25f16 Mon Sep 17 00:00:00 2001 From: Yorgos Thessalonikefs Date: Fri, 6 Mar 2026 17:27:21 +0100 Subject: [PATCH 07/18] - Constrain the explicit macros for remote.c:fr_atomic_copy_cfg(). --- daemon/remote.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/daemon/remote.c b/daemon/remote.c index 00e7dd21d..292a7f6fa 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -6186,6 +6186,15 @@ fr_atomic_copy_cfg(struct config_file* oldcfg, struct config_file* cfg, COPY_VAR_int(iter_scrub_cname); COPY_VAR_int(max_global_quota); COPY_VAR_int(iter_scrub_promiscuous); + +#undef COPY_VAR_int +#undef COPY_VAR_ptr +#undef COPY_VAR_unsigned_int +#undef COPY_VAR_size_t +#undef COPY_VAR_uint8_t +#undef COPY_VAR_uint16_t +#undef COPY_VAR_uint32_t +#undef COPY_VAR_int32_t } #endif /* ATOMIC_POINTER_LOCK_FREE && HAVE_LINK_ATOMIC_STORE */ From 9af29c3ed1085d7eeb943a2e476a5331682450a0 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Mon, 9 Mar 2026 09:18:43 +0100 Subject: [PATCH 08/18] - Fix compile failure in unbound-checkconf for older gcc compiler. --- doc/Changelog | 3 +++ smallapp/unbound-checkconf.c | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index 90223791c..83a016609 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,6 @@ +9 March 2026: Wouter + - Fix compile failure in unbound-checkconf for older gcc compiler. + 6 March 2026: Wouter - Merge #1415: Add lock unlock for view in memory error handling. diff --git a/smallapp/unbound-checkconf.c b/smallapp/unbound-checkconf.c index 399c2fce9..a8e19241f 100644 --- a/smallapp/unbound-checkconf.c +++ b/smallapp/unbound-checkconf.c @@ -228,6 +228,7 @@ nodefaultzonescheck(struct config_file* cfg) "documentation for default configured local-zones.\n", d->str); default_continue: + ; /* statement to jump to, for older gcc. */ } #undef COMPARE_ZONE_NAME } From 5c6f56f8f1c917c19deca17dea15b95b76c47556 Mon Sep 17 00:00:00 2001 From: Arunabha Das Date: Mon, 9 Mar 2026 18:23:52 +0530 Subject: [PATCH 09/18] Apply cache TTL policy to DNAME and synthesized CNAME on wire path (#1418) When the scrubber synthesizes a CNAME from a DNAME (authority omits CNAME), apply cache-min-ttl/cache-max-ttl to both DNAME and synthesized CNAME in msg_parse so they stay equal and respect config (RFC 6672). - iterator/iter_scrub.c: In synth_cname_rrset(), clamp TTL to [MIN_TTL, MAX_TTL] when !SERVE_ORIGINAL_TTL and write back to both synth CNAME and DNAME rrset. Removes FIXME. --- iterator/iter_scrub.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index a4b98375b..147b1f05c 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -285,6 +285,17 @@ synth_cname_rrset(uint8_t** sname, size_t* snamelen, uint8_t* alias, return NULL; memmove(cn->rr_first->ttl_data, rrset->rr_first->ttl_data, sizeof(uint32_t)); /* RFC6672: synth CNAME TTL == DNAME TTL */ + /* Apply cache TTL policy so DNAME and synthesized CNAME stay equal + * and respect cache-min-ttl/cache-max-ttl (same as rdata_copy path). */ + if(!SERVE_ORIGINAL_TTL) { + uint32_t ttl = sldns_read_uint32(cn->rr_first->ttl_data); + time_t ttl_t = (time_t)ttl; + if(ttl_t < MIN_TTL) ttl_t = MIN_TTL; + if(ttl_t > MAX_TTL) ttl_t = MAX_TTL; + ttl = (uint32_t)ttl_t; + sldns_write_uint32(cn->rr_first->ttl_data, ttl); + sldns_write_uint32(rrset->rr_first->ttl_data, ttl); + } sldns_write_uint16(cn->rr_first->ttl_data+4, aliaslen); memmove(cn->rr_first->ttl_data+6, alias, aliaslen); cn->rr_first->size = sizeof(uint16_t)+aliaslen; @@ -502,8 +513,6 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, log_err("out of memory synthesizing CNAME"); return 0; } - /* FIXME: resolve the conflict between synthesized - * CNAME ttls and the cache. */ rrset = nx; continue; From 18029fc44f7ff076878a20be53855e901502cad4 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Mon, 9 Mar 2026 13:54:37 +0100 Subject: [PATCH 10/18] Changelog comment for #1418 - Merge #1418: Apply cache TTL policy to DNAME and synthesized CNAME on wire path. --- doc/Changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index 83a016609..e24e55557 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,5 +1,7 @@ 9 March 2026: Wouter - Fix compile failure in unbound-checkconf for older gcc compiler. + - Merge #1418: Apply cache TTL policy to DNAME and synthesized + CNAME on wire path. 6 March 2026: Wouter - Merge #1415: Add lock unlock for view in memory error handling. From 4672fa5b5347677c034a81314ef82ccec8f950c0 Mon Sep 17 00:00:00 2001 From: Yorgos Thessalonikefs Date: Fri, 13 Mar 2026 11:28:32 +0100 Subject: [PATCH 11/18] - Fix to ignore out-of-zone DNAME records for CNAME synthesis. Thanks to Yuxiao Wu, Yiyi Wang, Zhang Chao, Baojun Liu, and Haixin Duan from Tsinghua University. --- doc/Changelog | 5 + iterator/iter_scrub.c | 8 +- testdata/iter_scrub_dname_out_of_zone.rpl | 122 ++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 testdata/iter_scrub_dname_out_of_zone.rpl diff --git a/doc/Changelog b/doc/Changelog index e24e55557..b8252d7e4 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,8 @@ +13 March 2026: Yorgos + - Fix to ignore out-of-zone DNAME records for CNAME synthesis. Thanks + to Yuxiao Wu, Yiyi Wang, Zhang Chao, Baojun Liu, and Haixin Duan from + Tsinghua University. + 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/iterator/iter_scrub.c b/iterator/iter_scrub.c index 147b1f05c..757d95a3d 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -466,8 +466,9 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, pkt, msg, prev, &rrset); continue; } - if(rrset->type == LDNS_RR_TYPE_DNAME && - pkt_strict_sub(pkt, sname, rrset->dname)) { + if(rrset->type == LDNS_RR_TYPE_DNAME && + pkt_strict_sub(pkt, sname, rrset->dname) && + pkt_sub(pkt, rrset->dname, zonename)) { /* check if next rrset is correct CNAME. else, * synthesize a CNAME */ struct rrset_parse* nx = rrset->rrset_all_next; @@ -534,7 +535,8 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, if(nx && nx->section == LDNS_SECTION_ANSWER && nx->type == LDNS_RR_TYPE_DNAME && nx->rr_count == 1 && - pkt_strict_sub(pkt, sname, nx->dname)) { + pkt_strict_sub(pkt, sname, nx->dname) && + pkt_sub(pkt, nx->dname, zonename)) { /* there is a DNAME after this CNAME, it * is in the ANSWER section, and the DNAME * applies to the name we cover */ diff --git a/testdata/iter_scrub_dname_out_of_zone.rpl b/testdata/iter_scrub_dname_out_of_zone.rpl new file mode 100644 index 000000000..8dbafdf72 --- /dev/null +++ b/testdata/iter_scrub_dname_out_of_zone.rpl @@ -0,0 +1,122 @@ +; config options +server: + harden-referral-path: no + target-fetch-policy: "0 0 0 0 0" + qname-minimisation: "no" + minimal-responses: no + iter-scrub-promiscuous: no + +stub-zone: + name: "." + stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET. +CONFIG_END + +SCENARIO_BEGIN Test scrub of out-of-zone DNAME in answer section + +STEP 10 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +x.y.example.com. IN A +ENTRY_END + +; root prime is sent +STEP 20 CHECK_OUT_QUERY +ENTRY_BEGIN +MATCH qname qtype opcode +SECTION QUESTION +. IN NS +ENTRY_END +STEP 30 REPLY +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +. IN NS +SECTION ANSWER +. IN NS K.ROOT-SERVERS.NET. +SECTION ADDITIONAL +K.ROOT-SERVERS.NET. IN A 193.0.14.129 +ENTRY_END + +; query sent to root server +STEP 40 CHECK_OUT_QUERY +ENTRY_BEGIN +MATCH qname qtype opcode +SECTION QUESTION +x.y.example.com. IN A +ENTRY_END +STEP 50 REPLY +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +x.y.example.com. IN A +SECTION AUTHORITY +com. IN NS a.gtld-servers.net. +SECTION ADDITIONAL +a.gtld-servers.net. IN A 192.5.6.30 +ENTRY_END + +; query sent to .com server +STEP 60 CHECK_OUT_QUERY +ENTRY_BEGIN +MATCH qname qtype opcode +SECTION QUESTION +x.y.example.com. IN A +ENTRY_END + +STEP 70 REPLY +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +x.y.example.com. IN A +SECTION AUTHORITY +example.com. IN NS ns1.example.com. +SECTION ADDITIONAL +ns1.example.com. IN A 168.192.2.2 +ENTRY_END + +STEP 80 CHECK_OUT_QUERY +ENTRY_BEGIN +MATCH qname qtype opcode +SECTION QUESTION +x.y.example.com. IN A +ENTRY_END + +STEP 90 REPLY +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR AA NOERROR +SECTION QUESTION +x.y.example.com. IN A +SECTION ANSWER +com. DNAME z.example.com. +SECTION AUTHORITY +example.com. IN NS ns1.example.com. +SECTION ADDITIONAL +ns1.example.com. IN A 168.192.2.2 +ENTRY_END + +; answer to first query +; nodata answer since the DNAME is ignored for synthesis and scrubbed +; all together. +STEP 120 CHECK_ANSWER +ENTRY_BEGIN +MATCH all ttl +REPLY QR RD RA +SECTION QUESTION +x.y.example.com. IN A +SECTION ANSWER +SECTION AUTHORITY +example.com. IN NS ns1.example.com. +SECTION ADDITIONAL +ns1.example.com. IN A 168.192.2.2 +ENTRY_END + +SCENARIO_END From 2eff1d8ab54f14345f28eb7e3050a44a63a1c008 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 13 Mar 2026 11:42:34 +0100 Subject: [PATCH 12/18] - 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. --- config.h.in | 6 ++ configure | 17 +++++ configure.ac | 1 + daemon/daemon.c | 186 ++++++++++++++++++++++++++++++++++++++++++--- daemon/daemon.h | 15 ++++ daemon/unbound.c | 51 +------------ doc/Changelog | 5 ++ util/config_file.c | 27 +++++++ util/config_file.h | 3 + util/net_help.c | 14 ++-- util/net_help.h | 4 +- 11 files changed, 267 insertions(+), 62 deletions(-) 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 */ +/** setup the remote and ticket keys */ 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 */ -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); From eb3bba0724e5e9b5e54b9b7a0280faa2b2704c2e Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 13 Mar 2026 12:05:33 +0100 Subject: [PATCH 13/18] - iana portlist updated. --- doc/Changelog | 1 + util/iana_ports.inc | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index cccb21c57..37b98f670 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -7,6 +7,7 @@ - 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. + - iana portlist updated. 9 March 2026: Wouter - Fix compile failure in unbound-checkconf for older gcc compiler. diff --git a/util/iana_ports.inc b/util/iana_ports.inc index a60f2307e..60db889a4 100644 --- a/util/iana_ports.inc +++ b/util/iana_ports.inc @@ -3978,6 +3978,7 @@ 4791, 4792, 4793, +4794, 4800, 4801, 4802, @@ -4947,6 +4948,7 @@ 9162, 9163, 9164, +9183, 9191, 9200, 9201, @@ -5431,6 +5433,8 @@ 34962, 34963, 34964, +34965, +34966, 34980, 35001, 35004, From 4484dc3954afb8c852bc6828c21144ec8151c750 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 13 Mar 2026 16:25:42 +0100 Subject: [PATCH 14/18] - 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. --- daemon/daemon.c | 107 +++++++++++++++----- daemon/daemon.h | 19 ++++ daemon/remote.c | 211 +++++++++++++++++++++++++++++++++++++++- daemon/remote.h | 2 + doc/Changelog | 3 + doc/unbound-control.rst | 2 + doc/unbound.conf.rst | 10 +- 7 files changed, 326 insertions(+), 28 deletions(-) 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 From 8f44ddb7de66ed617ba8915f7c5ff15c0199501f Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 13 Mar 2026 16:37:41 +0100 Subject: [PATCH 15/18] - Fix detection of http listening port in fast_reload. --- daemon/remote.c | 6 ++++-- doc/Changelog | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/daemon/remote.c b/daemon/remote.c index eccbe745f..3984ef6e4 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -7798,7 +7798,9 @@ fr_worker_pickup_listen_dnsport(struct worker* worker) 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 */) { + cp->tcp_handlers && + cp->max_tcp_count > 0 && + cp->tcp_handlers[0]->type == comm_http) { if(cp->ssl) cp->ssl = doh_sslctx; } else if(cp->type == comm_tcp_accept) { @@ -7905,8 +7907,8 @@ fast_reload_worker_pickup_changes(struct worker* worker) #ifdef USE_CACHEDB worker->env.cachedb_enabled = worker->daemon->env->cachedb_enabled; #endif - fr_worker_pickup_outside_network(worker); fr_worker_pickup_listen_dnsport(worker); + fr_worker_pickup_outside_network(worker); #ifdef USE_DNSTAP fr_worker_pickup_dnstap_changes(worker); #endif diff --git a/doc/Changelog b/doc/Changelog index f9b6df967..94f2eb53c 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -11,6 +11,7 @@ - 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. + - Fix detection of http listening port in fast_reload. 9 March 2026: Wouter - Fix compile failure in unbound-checkconf for older gcc compiler. From 8656cfd4c82a2db6de59b08eae4300ae39ceecc3 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 13 Mar 2026 16:49:34 +0100 Subject: [PATCH 16/18] - Fix to add tls-service-key to memory printout for fast_reload. --- daemon/remote.c | 2 ++ doc/Changelog | 1 + 2 files changed, 3 insertions(+) diff --git a/daemon/remote.c b/daemon/remote.c index 3984ef6e4..dd0e8f299 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -5325,6 +5325,8 @@ fr_printmem(struct fast_reload_thread* fr, size_t mem = 0; if(fr_poll_for_quit(fr)) return 1; + mem += getmem_str(ct->ssl_service_key); + mem += getmem_str(ct->ssl_service_pem); mem += views_get_mem(ct->views); mem += respip_set_get_mem(ct->respip_set); mem += auth_zones_get_mem(ct->auth_zones); diff --git a/doc/Changelog b/doc/Changelog index 94f2eb53c..396a2c3b6 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -12,6 +12,7 @@ and tls-cert-bundle changes. It checks the modification time of the tls-service-key and tls-service-pem files for update. - Fix detection of http listening port in fast_reload. + - Fix to add tls-service-key to memory printout for fast_reload. 9 March 2026: Wouter - Fix compile failure in unbound-checkconf for older gcc compiler. From 535d899beff4e14a409aecb499a840d349dc1c7b Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Fri, 20 Mar 2026 15:57:46 +0100 Subject: [PATCH 17/18] - Fix for testcode pktview to check buffer size and log errors. --- doc/Changelog | 3 +++ testcode/pktview.c | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/doc/Changelog b/doc/Changelog index 396a2c3b6..f92de7fd0 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,6 @@ +20 March 2026: Wouter + - Fix for testcode pktview to check buffer size and log errors. + 13 March 2026: Yorgos - Fix to ignore out-of-zone DNAME records for CNAME synthesis. Thanks to Yuxiao Wu, Yiyi Wang, Zhang Chao, Baojun Liu, and Haixin Duan from diff --git a/testcode/pktview.c b/testcode/pktview.c index 12e0d8edb..4cada926b 100644 --- a/testcode/pktview.c +++ b/testcode/pktview.c @@ -59,12 +59,16 @@ static void usage(char* argv[]) /** read hex input */ static void read_input(sldns_buffer* pkt, FILE* in) { - char buf[102400]; + /* Buffer for 64Kib packet, in hex, with spaces and comments. */ + char buf[1024000]; char* np = buf; while(fgets(np, (int)sizeof(buf) - (np-buf), in)) { if(buf[0] == ';') /* comment */ continue; np = &np[strlen(np)]; + if((size_t)(np-buf) >= sizeof(buf)-1) + fatal_exit("input too large (%lu bytes)", + (unsigned long)sizeof(buf)); } hex_to_buf(pkt, buf); } @@ -188,10 +192,16 @@ static void analyze(sldns_buffer* pkt) /** main program for pktview */ int main(int argc, char* argv[]) { - sldns_buffer* pkt = sldns_buffer_new(65553); + sldns_buffer* pkt; + + log_init(NULL, 0, NULL); + log_ident_set("pktview"); + if(argc != 1) { usage(argv); } + + pkt = sldns_buffer_new(65553); if(!pkt) fatal_exit("out of memory"); read_input(pkt, stdin); From 315077b9e655ac60b0aa9fbd3e7eec343e2c47da Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Tue, 24 Mar 2026 08:45:52 +0100 Subject: [PATCH 18/18] - Fix to check for invalid http content length and chunk size, and to check the RR rdata field lengths when decompressing and inserting RRs from an authority zone transfer. This stops large memory use and heap buffer-overflow read errors. Thanks to Haruto Kimura (Stella) for the report. --- doc/Changelog | 7 +++++++ services/authzone.c | 5 +++++ util/netevent.c | 23 +++++++++++++++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/doc/Changelog b/doc/Changelog index f92de7fd0..5981ac2fa 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,3 +1,10 @@ +24 March 2026: Wouter + - Fix to check for invalid http content length and chunk size, + and to check the RR rdata field lengths when decompressing and + inserting RRs from an authority zone transfer. This stops + large memory use and heap buffer-overflow read errors. Thanks + to Haruto Kimura (Stella) for the report. + 20 March 2026: Wouter - Fix for testcode pktview to check buffer size and log errors. diff --git a/services/authzone.c b/services/authzone.c index 3965b5e17..9778cf1b7 100644 --- a/services/authzone.c +++ b/services/authzone.c @@ -1384,6 +1384,9 @@ decompress_rr_into_buffer(struct sldns_buffer* buf, uint8_t* pkt, len = 0; break; case LDNS_RDF_TYPE_STR: + /* Check rdlen for resilience, because it is + * checked above, that rdlen > 0 */ + if(rdlen < 1) return 0; /* malformed */ len = rd[0] + 1; break; default: @@ -1391,6 +1394,8 @@ decompress_rr_into_buffer(struct sldns_buffer* buf, uint8_t* pkt, break; } if(len) { + if(len > rdlen) + return 0; /* malformed */ if(!sldns_buffer_available(buf, len)) return 0; /* too long for buffer */ sldns_buffer_write(buf, rd, len); diff --git a/util/netevent.c b/util/netevent.c index 01ef54dfe..a86e22518 100644 --- a/util/netevent.c +++ b/util/netevent.c @@ -4870,8 +4870,17 @@ http_process_initial_header(struct comm_point* c) return 0; } } else if(strncasecmp(line, "Content-Length: ", 16) == 0) { - if(!c->http_is_chunked) - c->tcp_byte_count = (size_t)atoi(line+16); + if(!c->http_is_chunked) { + char* end = NULL; + long long cl; + errno = 0; + cl = strtoll(line+16, &end, 10); + if(end == line+16 || errno != 0 || cl < 0) { + verbose(VERB_ALGO, "http invalid Content-Length: " ARG_LL "d", cl); + return 0; /* reject */ + } + c->tcp_byte_count = (size_t)cl; + } } else if(strncasecmp(line, "Transfer-Encoding: chunked", 19+7) == 0) { c->tcp_byte_count = 0; c->http_is_chunked = 1; @@ -4927,9 +4936,15 @@ http_process_chunk_header(struct comm_point* c) if(c->http_in_chunk_headers == 1) { /* read chunked start line */ char* end = NULL; - c->tcp_byte_count = (size_t)strtol(line, &end, 16); - if(end == line) + long chunk_sz; + errno = 0; + chunk_sz = strtol(line, &end, 16); + if(end == line || errno != 0 || chunk_sz < 0) { + verbose(VERB_ALGO, "http invalid chunk size: %ld", + chunk_sz); return 0; + } + c->tcp_byte_count = (size_t)chunk_sz; c->http_in_chunk_headers = 0; /* remove header text from front of buffer */ http_moveover_buffer(c->buffer);