Skip to content

Commit 2ec2afd

Browse files
committed
- Fix #132: Add harden-cname-follow: yes config option, that
can be used to disable cname scrubbing. That can be useful for when the traffic is on localhost only.
1 parent 7fb79b0 commit 2ec2afd

15 files changed

Lines changed: 297 additions & 8 deletions

daemon/remote.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6373,6 +6373,7 @@ fr_atomic_copy_cfg(struct config_file* oldcfg, struct config_file* cfg,
63736373
COPY_VAR_int(harden_referral_path);
63746374
COPY_VAR_int(harden_algo_downgrade);
63756375
COPY_VAR_int(harden_unknown_additional);
6376+
COPY_VAR_int(harden_cname_follow);
63766377
COPY_VAR_int(use_caps_bits_for_id);
63776378
COPY_VAR_ptr(caps_whitelist);
63786379
COPY_VAR_ptr(private_address);

doc/Changelog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
24 August 2026: Wouter
2+
- Fix #132: Add `harden-cname-follow: yes` config option, that
3+
can be used to disable cname scrubbing. That can be useful
4+
for when the traffic is on localhost only.
5+
16
21 August 2026: Wouter
27
- Fix to defend against double event deletion, that could cause
38
event corruption and use after free, for comm_timer_disable.

doc/example.conf.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,9 @@ server:
582582
# additional section.
583583
# harden-unknown-additional: no
584584

585+
# Harden CNAME redirections by following them.
586+
# harden-cname-follow: yes
587+
585588
# Sent minimum amount of information to upstream servers to enhance
586589
# privacy. Only sent minimum required labels of the QNAME and set QTYPE
587590
# to A when possible.

doc/unbound.conf.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,6 +1952,17 @@ These options are part of the ``server:`` section.
19521952
Default: no
19531953

19541954

1955+
@@UAHL@unbound.conf@harden-cname-follow@@: *<yes or no>*
1956+
Harden CNAME redirections by following them.
1957+
If no, then upstream CNAME and DNAME redirections are allowed in a
1958+
response without checking with further messages if those are valid.
1959+
It can only really be disabled safely on localhost net or encrypted
1960+
connectivity.
1961+
Default is on to protect the cache integrity.
1962+
1963+
Default: yes
1964+
1965+
19551966
@@UAHL@unbound.conf@use-caps-for-id@@: *<yes or no>*
19561967
Use 0x20-encoded random bits in the query to foil spoof attempts.
19571968
This perturbs the lowercase and uppercase of query names sent to authority

iterator/iter_resptype.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "services/cache/dns.h"
4747
#include "util/net_help.h"
4848
#include "util/data/dname.h"
49+
#include "util/config_file.h"
4950
#include "sldns/rrdef.h"
5051
#include "sldns/pkthdr.h"
5152

@@ -107,7 +108,8 @@ response_type_from_cache(struct dns_msg* msg,
107108
enum response_type
108109
response_type_from_server(int rdset,
109110
struct dns_msg* msg, struct query_info* request, struct delegpt* dp,
110-
int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral)
111+
int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral,
112+
struct config_file* cfg)
111113
{
112114
uint8_t* origzone = (uint8_t*)"\000"; /* the default */
113115
struct ub_packed_rrset_key* s;
@@ -130,6 +132,16 @@ response_type_from_server(int rdset,
130132
if( (msg->rep->flags&BIT_RA) &&
131133
!(msg->rep->flags&BIT_AA) && !rdset)
132134
return RESPONSE_TYPE_REC_LAME;
135+
if(!cfg->harden_cname_follow /* follow CNAME chain */) {
136+
/* If the CNAME chain is allowed, see if there is a
137+
* SOA record, if so, the chain is complete to the
138+
* end of it, otherwise it could be a partial chain.*/
139+
for(i=msg->rep->an_numrrsets; i<msg->rep->an_numrrsets+msg->rep->ns_numrrsets; i++) {
140+
s = msg->rep->rrsets[i];
141+
if(ntohs(s->rk.type) == LDNS_RR_TYPE_SOA)
142+
return RESPONSE_TYPE_ANSWER;
143+
}
144+
}
133145
/* it could be a CNAME with NXDOMAIN rcode */
134146
for(i=0; i<msg->rep->an_numrrsets; i++) {
135147
s = msg->rep->rrsets[i];
@@ -162,6 +174,7 @@ response_type_from_server(int rdset,
162174
if(msg->rep->an_numrrsets > 0) {
163175
uint8_t* mname = request->qname;
164176
size_t mname_len = request->qname_len;
177+
int sawanswer = 0;
165178

166179
/* Now look at the answer section first. 3 states: our
167180
* answer is there directly, our answer is there after
@@ -197,6 +210,7 @@ response_type_from_server(int rdset,
197210
* the answer, we only provisionally say
198211
* 'ANSWER' -- it very well could be a
199212
* REFERRAL. */
213+
sawanswer = 1;
200214
break;
201215
}
202216

@@ -214,6 +228,10 @@ response_type_from_server(int rdset,
214228
* still got to here, then it is a CNAME response.
215229
* (This is regardless of the AA bit at this point) */
216230
if(mname != request->qname) {
231+
if(!cfg->harden_cname_follow /* allow CNAMEs */ &&
232+
sawanswer /* The last was not CNAME, and
233+
there is an answer RRset. */ )
234+
return RESPONSE_TYPE_ANSWER;
217235
return RESPONSE_TYPE_CNAME;
218236
}
219237
}

iterator/iter_resptype.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
struct dns_msg;
4646
struct query_info;
4747
struct delegpt;
48+
struct config_file;
4849

4950
/**
5051
* The response type is used to interpret the response.
@@ -115,7 +116,7 @@ enum response_type response_type_from_cache(struct dns_msg* msg,
115116
* detection, mostly).
116117
*
117118
* @param rdset: if RD bit was sent in query sent by unbound.
118-
* @param msg: the message from the cache.
119+
* @param msg: the message.
119120
* @param request: the request that generated the response.
120121
* @param dp: The delegation point that was being queried
121122
* when the response was returned.
@@ -124,10 +125,12 @@ enum response_type response_type_from_cache(struct dns_msg* msg,
124125
* is lame, before it became empty.
125126
* @param msg_lame_referral: returned true if the reply has a referral before
126127
* scrub.
128+
* @param cfg: config with options.
127129
* @return the response type (CNAME or ANSWER).
128130
*/
129131
enum response_type response_type_from_server(int rdset,
130132
struct dns_msg* msg, struct query_info* request, struct delegpt* dp,
131-
int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral);
133+
int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral,
134+
struct config_file* cfg);
132135

133136
#endif /* ITERATOR_ITER_RESPTYPE_H */

iterator/iter_scrub.c

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg,
517517
* server, scrub down the length to something
518518
* shorter. This deletes everything after the limit
519519
* is reached. The iterator is going to look up
520-
* the content one by one anyway. */
520+
* the content one by one, if harden-cname-follow . */
521521
remove_rrset("normalize: removing because too many cnames:",
522522
pkt, msg, prev, &rrset);
523523
continue;
@@ -1021,13 +1021,16 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg,
10211021
uint8_t* ns_rrset_dname = NULL;
10221022
int added_rrlen_ede = 0;
10231023
struct rrset_parse* rrset, *prev;
1024+
uint8_t* sname = qinfo->qname;
1025+
size_t snamelen = qinfo->qname_len;
10241026
prev = NULL;
10251027
rrset = msg->rrset_first;
10261028

10271029
/* the first DNAME is allowed to stay. It needs checking before
10281030
* it can be used from the cache. After normalization, an initial
10291031
* DNAME will have a correctly synthesized CNAME after it. */
10301032
if(rrset && rrset->type == LDNS_RR_TYPE_DNAME &&
1033+
env->cfg->harden_cname_follow /* CNAME chain is cut off, one DNAME is allowed here. */ &&
10311034
rrset->section == LDNS_SECTION_ANSWER &&
10321035
pkt_strict_sub(pkt, qinfo->qname, rrset->dname) &&
10331036
pkt_sub(pkt, rrset->dname, zonename)) {
@@ -1043,12 +1046,33 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg,
10431046
* ANY queries get query name in answer section.
10441047
* Remainders of CNAME chains are cut off and resolved by iterator. */
10451048
while(rrset && rrset->section == LDNS_SECTION_ANSWER) {
1046-
if(dname_pkt_compare(pkt, qinfo->qname, rrset->dname) != 0) {
1049+
if(!env->cfg->harden_cname_follow /* CNAME chain is allowed to stay */ &&
1050+
rrset->type == LDNS_RR_TYPE_DNAME &&
1051+
pkt_strict_sub(pkt, sname, rrset->dname) &&
1052+
pkt_sub(pkt, rrset->dname, zonename)) {
1053+
/* This DNAME is allowed to stay, the synthesized
1054+
* CNAME follows next. */
1055+
prev = rrset;
1056+
rrset = rrset->rrset_all_next;
1057+
continue;
1058+
}
1059+
if(dname_pkt_compare(pkt, sname, rrset->dname) != 0) {
10471060
if(has_additional(rrset->type)) del_addi = 1;
10481061
remove_rrset("sanitize: removing extraneous answer "
10491062
"RRset:", pkt, msg, prev, &rrset);
10501063
continue;
10511064
}
1065+
if(!env->cfg->harden_cname_follow /* CNAME chain is allowed to stay */ &&
1066+
qinfo->qtype != LDNS_RR_TYPE_ANY &&
1067+
rrset->type == LDNS_RR_TYPE_CNAME &&
1068+
dname_pkt_compare(pkt, sname, rrset->dname) == 0) {
1069+
/* Follow the CNAME chain, and allow the elements
1070+
* that match the CNAME chain. Also allow a DNAME
1071+
* in front. */
1072+
if(!parse_get_cname_target(rrset, &sname, &snamelen,
1073+
pkt))
1074+
return 0;
1075+
}
10521076
prev = rrset;
10531077
rrset = rrset->rrset_all_next;
10541078
}

iterator/iter_utils.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,3 +1712,28 @@ deleg_port_number(struct module_env* env)
17121712
return env->cfg->ssl_port;
17131713
return -1;
17141714
}
1715+
1716+
void
1717+
shorten_answer_cname(struct reply_info* rep, uint8_t* cutoff)
1718+
{
1719+
size_t i, found = 0, removenum;
1720+
uint8_t* sname = NULL;
1721+
size_t snamelen = 0;
1722+
for(i=0; i<rep->an_numrrsets; i++) {
1723+
if(query_dname_compare(rep->rrsets[i]->rk.dname, cutoff) == 0) {
1724+
found = 1;
1725+
removenum = i;
1726+
break;
1727+
}
1728+
if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_CNAME) {
1729+
get_cname_target(rep->rrsets[i], &sname, &snamelen);
1730+
if(query_dname_compare(sname, cutoff) == 0) {
1731+
found = 1;
1732+
removenum = i+1;
1733+
break;
1734+
}
1735+
}
1736+
}
1737+
if(!found) return; /* not found */
1738+
val_reply_remove_answers(rep, removenum, rep->an_numrrsets-removenum);
1739+
}

iterator/iter_utils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,7 @@ void iter_make_minimal(struct reply_info* rep);
486486
/** See if we need a different port number */
487487
int deleg_port_number(struct module_env* env);
488488

489+
/** Shorten reply CNAME chain to cutoff, cutoff is excluded. */
490+
void shorten_answer_cname(struct reply_info* rep, uint8_t* cutoff);
491+
489492
#endif /* ITERATOR_ITER_UTILS_H */

iterator/iterator.c

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3189,7 +3189,7 @@ processQueryResponse(struct module_qstate* qstate, struct iter_qstate* iq,
31893189
type = response_type_from_server(
31903190
(int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd),
31913191
iq->response, &iq->qinfo_out, iq->dp, &iq->empty_nodata_found,
3192-
iq->msg_lame_empty, iq->msg_lame_referral);
3192+
iq->msg_lame_empty, iq->msg_lame_referral, qstate->env->cfg);
31933193
iq->chase_to_rd = 0;
31943194
/* remove TC flag, if this is erroneously set by TCP upstream */
31953195
iq->response->rep->flags &= ~BIT_TC;
@@ -3302,6 +3302,47 @@ processQueryResponse(struct module_qstate* qstate, struct iter_qstate* iq,
33023302
* to send another query with a new qtype. */
33033303
type = RESPONSE_TYPE_ANSWER;
33043304
}
3305+
if(type == RESPONSE_TYPE_ANSWER &&
3306+
!qstate->env->cfg->harden_cname_follow /* cname chain from upstream is allowed */ &&
3307+
qstate->env->auth_zones &&
3308+
/* Check for CNAMEs in answer and RPZ after the CNAME. */
3309+
reply_find_rrset_section_an(
3310+
iq->response->rep, iq->qchase.qname,
3311+
iq->qchase.qname_len, LDNS_RR_TYPE_CNAME,
3312+
iq->qchase.qclass) != NULL) {
3313+
/* If this is an answer with CNAMEs in front, and
3314+
* RPZ wants to modify after CNAME(s), cut off, the
3315+
* remainder, and treat as the CNAME response */
3316+
size_t i;
3317+
uint8_t* origname = iq->qchase.qname;
3318+
size_t orignamelen = iq->qchase.qname_len;
3319+
for(i=0; i<iq->response->rep->an_numrrsets; i++) {
3320+
struct dns_msg* forged_response;
3321+
if(ntohs(iq->response->rep->rrsets[i]->rk.type) ==
3322+
LDNS_RR_TYPE_DNAME) {
3323+
continue;
3324+
}
3325+
if(ntohs(iq->response->rep->rrsets[i]->rk.type) !=
3326+
LDNS_RR_TYPE_CNAME) {
3327+
break;
3328+
}
3329+
get_cname_target(iq->response->rep->rrsets[i],
3330+
&iq->qchase.qname, &iq->qchase.qname_len);
3331+
forged_response = rpz_callback_from_iterator_cname(qstate, iq);
3332+
if(forged_response) {
3333+
/* Cut off the answer section at this point.
3334+
* RPZ is going to make an answer, in the
3335+
* processInit after the CNAME(s) in front
3336+
* are handled. */
3337+
shorten_answer_cname(iq->response->rep,
3338+
iq->qchase.qname);
3339+
type = RESPONSE_TYPE_CNAME;
3340+
break;
3341+
}
3342+
}
3343+
iq->qchase.qname = origname;
3344+
iq->qchase.qname_len = orignamelen;
3345+
}
33053346

33063347
/* handle each of the type cases */
33073348
if(type == RESPONSE_TYPE_ANSWER) {
@@ -3804,7 +3845,7 @@ processPrimeResponse(struct module_qstate* qstate, int id)
38043845
type = response_type_from_server(
38053846
(int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd),
38063847
iq->response, &iq->qchase, iq->dp, NULL, iq->msg_lame_empty,
3807-
iq->msg_lame_referral);
3848+
iq->msg_lame_referral, qstate->env->cfg);
38083849
if(type == RESPONSE_TYPE_ANSWER) {
38093850
qstate->return_rcode = LDNS_RCODE_NOERROR;
38103851
qstate->return_msg = iq->response;

0 commit comments

Comments
 (0)