33#include <stdint.h>
44#include <stdlib.h>
55#include <string.h>
6+ #include <sys/types.h>
67
78#include "dns_common.h"
89#include "dns_truncate.h"
1314// RFC1035 4.2.1 default of 512 if the request can't be parsed or the OPT
1415// advertises a smaller size (RFC6891 4.3 mandates that values below 512
1516// MUST be treated as 512).
17+ // Using c-ares' DNS parser for convenience and robustness, since the client's
18+ // request can not be trusted.
1619static uint16_t get_edns_udp_size (const char * dns_req , const size_t dns_req_len ) {
1720 ares_dns_record_t * dnsrec = NULL ;
18- ares_status_t parse_status = ares_dns_parse ((const unsigned char * )dns_req , dns_req_len , 0 , & dnsrec );
21+ ares_status_t parse_status = ares_dns_parse ((const unsigned char * )dns_req , dns_req_len ,
22+ ARES_DNS_PARSE_AN_BASE_RAW | ARES_DNS_PARSE_NS_BASE_RAW , // for faster parsing
23+ & dnsrec );
1924 if (parse_status != ARES_SUCCESS ) {
20- WLOG ("Failed to parse DNS request: %s" , ares_strerror ((int )parse_status ));
25+ const uint16_t req_id = ntohs (* ((uint16_t * )dns_req ));
26+ WLOG ("%04hX: Failed to parse DNS request: %s" , req_id , ares_strerror ((int )parse_status ));
2127 return DNS_SIZE_LIMIT ;
2228 }
23- const uint16_t tx_id = ares_dns_record_get_id (dnsrec );
29+ const uint16_t req_id = ares_dns_record_get_id (dnsrec );
2430 uint16_t udp_size = 0 ;
2531 const size_t record_count = ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_ADDITIONAL );
2632 for (size_t i = 0 ; i < record_count ; ++ i ) {
2733 const ares_dns_rr_t * rr = ares_dns_record_rr_get (dnsrec , ARES_SECTION_ADDITIONAL , i );
2834 if (ares_dns_rr_get_type (rr ) == ARES_REC_TYPE_OPT ) {
2935 udp_size = ares_dns_rr_get_u16 (rr , ARES_RR_OPT_UDP_SIZE );
3036 if (udp_size > 0 ) {
31- DLOG ("%04hX: Found EDNS0 UDP buffer size: %u" , tx_id , udp_size );
37+ DLOG ("%04hX: Found EDNS0 UDP buffer size: %u" , req_id , udp_size );
3238 }
3339 break ;
3440 }
3541 }
3642 ares_dns_record_destroy (dnsrec );
3743 if (udp_size < DNS_SIZE_LIMIT ) {
38- DLOG ("%04hX: EDNS0 UDP buffer size %u overruled to %d" , tx_id , udp_size , DNS_SIZE_LIMIT );
44+ DLOG ("%04hX: EDNS0 UDP buffer size %u overruled to %d" , req_id , udp_size , DNS_SIZE_LIMIT );
3945 return DNS_SIZE_LIMIT ;
4046 }
4147 return udp_size ;
4248}
4349
44- static void truncate_to_size_limit (char * buf , size_t * buflen , const uint16_t size_limit ) {
50+ /*
51+ * @brief Truncates a DNS response in-place to a skeleton packet to force an immediate TCP fallback.
52+ *
53+ * @param buf Pointer to the raw DNS message buffer.
54+ * @param buflen The actual size of the data currently in the buffer.
55+ * Will be set to the new truncated size after processing.
56+ * @param size_limit The desired maximum size (e.g. 512).
57+ *
58+ * @section reasoning Architectural Reasoning & RFC Compliance:
59+ *
60+ * 1. TC Bit Enforcement (RFC 1035):
61+ * Sets the Truncation bit (buf[2] |= 0x02) unconditionally when payload data is cleared.
62+ * According to RFC 1035, the primary directive given to a resolver when it catches a packet
63+ * with TC = 1 is that it must discard the UDP response data and immediately retry the query
64+ * over a reliable transport (TCP). Because the client throws away the packet anyway, returning
65+ * an empty data section completely satisfies the protocol's intent.
66+ *
67+ * 2. Total Section Cleardown (Deterministic Atomicity & RFC 2181):
68+ * RFC 2181, Section 5.2, introduces the concept of RRSet Atomicity, stating that all records
69+ * belonging to the same name, class, and type must be treated as a single cohesive unit.
70+ * Instead of complex, error-prone progressive backtracking loops that risk partial RRSet exposure
71+ * (which can cause intermediary resolvers to incorrectly cache incomplete data), this engine
72+ * clears the ANCount, NSCount, and non-OPT ARCount fields to 0. Wiping all records
73+ * uniformly ensures zero data corruption risk, as a set of 0 records cannot violate atomicity.
74+ *
75+ * 3. EDNS0/OPT Preservation (RFC 6891):
76+ * The OPT pseudo-RR (Type 41) is critical for extended error tracking, cookies, and DNSSEC signaling.
77+ * RFC 6891 mandates that OPT records should be preserved in truncated messages if they were present
78+ * in the request. This function scans the Additional section, locates the OPT record, and uses
79+ * memmove() to safely relocate it to sit directly flush against the end of the Question section,
80+ * preserving it in the truncated response stream.
81+ *
82+ * 4. Trusted Data Assumption:
83+ * DoH resolver response is considered trusted input, so assuming that it complies with RFCs
84+ * and is well-formed.
85+ *
86+ */
87+ static void truncate_to_size_limit (uint8_t * buf , size_t * buflen , size_t size_limit ) {
4588 const size_t old_size = * buflen ;
4689 buf [2 ] |= 0x02 ; // anyway: set truncation flag
4790
4891 ares_dns_record_t * dnsrec = NULL ;
49- ares_status_t status = ares_dns_parse ((const unsigned char * )buf , * buflen , 0 , & dnsrec );
92+ ares_status_t status = ares_dns_parse ((const unsigned char * )buf , * buflen ,
93+ ARES_DNS_PARSE_AN_BASE_RAW | ARES_DNS_PARSE_NS_BASE_RAW , // for faster parsing
94+ & dnsrec );
5095 if (status != ARES_SUCCESS ) {
51- WLOG ("Failed to parse DNS response: %s" , ares_strerror ((int )status ));
96+ const uint16_t resp_id = ntohs (* ((uint16_t * )buf ));
97+ WLOG ("%04hX: Failed to parse DNS response: %s" , resp_id , ares_strerror ((int )status ));
5298 return ;
5399 }
54- const uint16_t tx_id = ares_dns_record_get_id (dnsrec );
100+ const uint16_t resp_id = ares_dns_record_get_id (dnsrec );
55101
56- // NOTE: according to current c-ares implementation, removing first or last elements are the fastest!
102+ // NOTE: according to current c-ares implementation, removing last element is the fastest!
57103
58- // remove every additional and authority record
59- while ( ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_ADDITIONAL ) > 0 ) {
60- status = ares_dns_record_rr_del (dnsrec , ARES_SECTION_ADDITIONAL , 0 );
104+ // Remove every answer and authority record
105+ for ( size_t i = ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_ANSWER ); i > 0 ; i -- ) {
106+ status = ares_dns_record_rr_del (dnsrec , ARES_SECTION_ANSWER , i - 1 );
61107 if (status != ARES_SUCCESS ) {
62- WLOG ("%04hX: Could not remove additional record: %s" , tx_id , ares_strerror ((int )status ));
108+ WLOG ("%04hX: Could not remove answer record: %s" , resp_id , ares_strerror ((int )status ));
63109 }
64110 }
65- while ( ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_AUTHORITY ) > 0 ) {
66- status = ares_dns_record_rr_del (dnsrec , ARES_SECTION_AUTHORITY , 0 );
111+ for ( size_t i = ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_AUTHORITY ); i > 0 ; i -- ) {
112+ status = ares_dns_record_rr_del (dnsrec , ARES_SECTION_AUTHORITY , i - 1 );
67113 if (status != ARES_SUCCESS ) {
68- WLOG ("%04hX: Could not remove authority record: %s" , tx_id , ares_strerror ((int )status ));
114+ WLOG ("%04hX: Could not remove authority record: %s" , resp_id , ares_strerror ((int )status ));
69115 }
70116 }
71-
72- // rough estimate to reach size limit
73- size_t answers = ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_ANSWER );
74- size_t answers_to_keep = ((size_limit - DNS_HEADER_LENGTH ) * answers ) / old_size ;
75- answers_to_keep = answers_to_keep > 0 ? answers_to_keep : 1 ; // try to keep 1 answer
76-
77- // remove answer records until fit size limit or running out of answers
78- unsigned char * new_resp = NULL ;
79- size_t new_resp_len = 0 ;
80- for (uint8_t g = 0 ; g < UINT8_MAX ; ++ g ) { // endless loop guard
81- status = ares_dns_write (dnsrec , & new_resp , & new_resp_len );
82- if (status != ARES_SUCCESS ) {
83- WLOG ("%04hX: Failed to create truncated DNS response: %s" , tx_id , ares_strerror ((int )status ));
84- new_resp = NULL ; // just to be sure
85- break ;
86- }
87- if (new_resp_len < size_limit || answers == 0 ) {
88- break ;
117+ // Remove every additional record except OPT
118+ for (size_t i = ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_ADDITIONAL ); i > 0 ; i -- ) {
119+ const ares_dns_rr_t * rr = ares_dns_record_rr_get (dnsrec , ARES_SECTION_ADDITIONAL , i - 1 );
120+ if (ares_dns_rr_get_type (rr ) == ARES_REC_TYPE_OPT ) {
121+ continue ; // skip removing OPT, removing records before will be unoptimal
89122 }
90- if ( new_resp_len >= old_size ) {
91- WLOG ( "%04hX: Truncated DNS response size larger or equal to original: %u >= %u" ,
92- tx_id , new_resp_len , old_size ); // impossible?
123+ status = ares_dns_record_rr_del ( dnsrec , ARES_SECTION_ADDITIONAL , i - 1 );
124+ if ( status != ARES_SUCCESS ) {
125+ WLOG ( "%04hX: Could not remove additional record: %s" , resp_id , ares_strerror (( int ) status ));
93126 }
94- ares_free_string (new_resp );
95- new_resp = NULL ;
127+ }
96128
97- DLOG ("%04hX: DNS response size truncated from %u to %u but to keep %u limit reducing answers from %u to %u" ,
98- tx_id , old_size , new_resp_len , size_limit , answers , answers_to_keep );
129+ unsigned char * new_resp = NULL ;
130+ size_t new_resp_len = 0 ;
131+ status = ares_dns_write (dnsrec , & new_resp , & new_resp_len );
99132
100- while (answers > answers_to_keep ) {
101- status = ares_dns_record_rr_del (dnsrec , ARES_SECTION_ANSWER , answers - 1 );
102- if (status != ARES_SUCCESS ) {
103- WLOG ("%04hX: Could not remove answer record: %s" , tx_id , ares_strerror ((int )status ));
104- break ;
105- }
106- -- answers ;
107- }
108- answers = ares_dns_record_rr_cnt (dnsrec , ARES_SECTION_ANSWER ); // update to be sure!
109- answers_to_keep /= 2 ;
110- }
111133 ares_dns_record_destroy (dnsrec );
112134
113- if (new_resp == NULL ) {
135+ if (status != ARES_SUCCESS || new_resp == NULL || new_resp_len == 0 ) {
136+ WLOG ("%04hX: Failed to create truncated DNS response: %s (new_resp=%p, new_resp_len=%zu)" ,
137+ resp_id , ares_strerror ((int )status ), new_resp , new_resp_len );
114138 return ;
115139 }
116140
@@ -119,7 +143,7 @@ static void truncate_to_size_limit(char *buf, size_t *buflen, const uint16_t siz
119143 * buflen = new_resp_len ;
120144 buf [2 ] |= 0x02 ; // set truncation flag
121145 ILOG ("%04hX: DNS response size truncated from %u to %u to keep %u limit" ,
122- tx_id , old_size , new_resp_len , size_limit );
146+ resp_id , old_size , new_resp_len , size_limit );
123147 }
124148
125149 ares_free_string (new_resp );
@@ -132,10 +156,10 @@ void dns_truncate_for_udp(const char *dns_req, size_t dns_req_len,
132156 }
133157 const uint16_t udp_size = get_edns_udp_size (dns_req , dns_req_len );
134158 if (* resp_len <= udp_size ) {
135- uint16_t tx_id = ntohs (* ((uint16_t * )dns_req ));
159+ uint16_t req_id = ntohs (* ((uint16_t * )dns_req ));
136160 DLOG ("%04hX: DNS response size %zu larger than %d but EDNS0 UDP buffer size %u allows it" ,
137- tx_id , * resp_len , DNS_SIZE_LIMIT , udp_size );
161+ req_id , * resp_len , DNS_SIZE_LIMIT , udp_size );
138162 return ;
139163 }
140- truncate_to_size_limit (resp , resp_len , udp_size );
164+ truncate_to_size_limit (( uint8_t * ) resp , resp_len , udp_size );
141165}
0 commit comments