Skip to content

Commit 346509a

Browse files
committed
Extract DNS response truncation into its own module
~110 lines of EDNS0/c-ares juggling (parse OPT, drop additional+authority, drop answers until under the limit, set TC) was buried as static functions in the UDP listener — only reachable by booting a real UDP server. Move to dns_truncate.{c,h} with one entry point: dns_truncate_for_udp(). The UDP listener's respond path collapses to validate-length, truncate-for-fit, sendto. The module is now unit-testable with synthetic byte buffers.
1 parent 38b24a0 commit 346509a

3 files changed

Lines changed: 161 additions & 121 deletions

File tree

src/dns_listener_udp.c

Lines changed: 2 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#include <ares.h>
21
#include <arpa/inet.h>
32
#include <errno.h>
43
#include <stdint.h>
@@ -9,6 +8,7 @@
98

109
#include "dns_common.h"
1110
#include "dns_listener_udp.h"
11+
#include "dns_truncate.h"
1212
#include "logging.h"
1313

1414
typedef struct dns_listener_udp_s {
@@ -86,116 +86,6 @@ static void watcher_cb(struct ev_loop __attribute__((unused)) *loop,
8686
d->cb(d->cb_data, &d->base, (struct sockaddr*)&tmp_raddr, dns_req, (size_t)len);
8787
}
8888

89-
static uint16_t get_edns_udp_size(const char *dns_req, const size_t dns_req_len) {
90-
ares_dns_record_t *dnsrec = NULL;
91-
ares_status_t parse_status = ares_dns_parse((const unsigned char *)dns_req, dns_req_len, 0, &dnsrec);
92-
if (parse_status != ARES_SUCCESS) {
93-
WLOG("Failed to parse DNS request: %s", ares_strerror((int)parse_status));
94-
return DNS_SIZE_LIMIT;
95-
}
96-
const uint16_t tx_id = ares_dns_record_get_id(dnsrec);
97-
uint16_t udp_size = 0;
98-
const size_t record_count = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ADDITIONAL);
99-
for (size_t i = 0; i < record_count; ++i) {
100-
const ares_dns_rr_t *rr = ares_dns_record_rr_get(dnsrec, ARES_SECTION_ADDITIONAL, i);
101-
if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) {
102-
udp_size = ares_dns_rr_get_u16(rr, ARES_RR_OPT_UDP_SIZE);
103-
if (udp_size > 0) {
104-
DLOG("%04hX: Found EDNS0 UDP buffer size: %u", tx_id, udp_size);
105-
}
106-
break;
107-
}
108-
}
109-
ares_dns_record_destroy(dnsrec);
110-
if (udp_size < DNS_SIZE_LIMIT) {
111-
DLOG("%04hX: EDNS0 UDP buffer size %u overruled to %d", tx_id, udp_size, DNS_SIZE_LIMIT);
112-
return DNS_SIZE_LIMIT; // RFC6891 4.3 "Values lower than 512 MUST be treated as equal to 512."
113-
}
114-
return udp_size;
115-
}
116-
117-
static void truncate_dns_response(char *buf, size_t *buflen, const uint16_t size_limit) {
118-
const size_t old_size = *buflen;
119-
buf[2] |= 0x02; // anyway: set truncation flag
120-
121-
ares_dns_record_t *dnsrec = NULL;
122-
ares_status_t status = ares_dns_parse((const unsigned char *)buf, *buflen, 0, &dnsrec);
123-
if (status != ARES_SUCCESS) {
124-
WLOG("Failed to parse DNS response: %s", ares_strerror((int)status));
125-
return;
126-
}
127-
const uint16_t tx_id = ares_dns_record_get_id(dnsrec);
128-
129-
// NOTE: according to current c-ares implementation, removing first or last elements are the fastest!
130-
131-
// remove every additional and authority record
132-
while (ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ADDITIONAL) > 0) {
133-
status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_ADDITIONAL, 0);
134-
if (status != ARES_SUCCESS) {
135-
WLOG("%04hX: Could not remove additional record: %s", tx_id, ares_strerror((int)status));
136-
}
137-
}
138-
while (ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_AUTHORITY) > 0) {
139-
status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_AUTHORITY, 0);
140-
if (status != ARES_SUCCESS) {
141-
WLOG("%04hX: Could not remove authority record: %s", tx_id, ares_strerror((int)status));
142-
}
143-
}
144-
145-
// rough estimate to reach size limit
146-
size_t answers = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER);
147-
size_t answers_to_keep = ((size_limit - DNS_HEADER_LENGTH) * answers) / old_size;
148-
answers_to_keep = answers_to_keep > 0 ? answers_to_keep : 1; // try to keep 1 answer
149-
150-
// remove answer records until fit size limit or running out of answers
151-
unsigned char *new_resp = NULL;
152-
size_t new_resp_len = 0;
153-
for (uint8_t g = 0; g < UINT8_MAX; ++g) { // endless loop guard
154-
status = ares_dns_write(dnsrec, &new_resp, &new_resp_len);
155-
if (status != ARES_SUCCESS) {
156-
WLOG("%04hX: Failed to create truncated DNS response: %s", tx_id, ares_strerror((int)status));
157-
new_resp = NULL; // just to be sure
158-
break;
159-
}
160-
if (new_resp_len < size_limit || answers == 0) {
161-
break;
162-
}
163-
if (new_resp_len >= old_size) {
164-
WLOG("%04hX: Truncated DNS response size larger or equal to original: %u >= %u",
165-
tx_id, new_resp_len, old_size); // impossible?
166-
}
167-
ares_free_string(new_resp);
168-
new_resp = NULL;
169-
170-
DLOG("%04hX: DNS response size truncated from %u to %u but to keep %u limit reducing answers from %u to %u",
171-
tx_id, old_size, new_resp_len, size_limit, answers, answers_to_keep);
172-
173-
while (answers > answers_to_keep) {
174-
status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_ANSWER, answers - 1);
175-
if (status != ARES_SUCCESS) {
176-
WLOG("%04hX: Could not remove answer record: %s", tx_id, ares_strerror((int)status));
177-
break;
178-
}
179-
--answers;
180-
}
181-
answers = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER); // update to be sure!
182-
answers_to_keep /= 2;
183-
}
184-
ares_dns_record_destroy(dnsrec);
185-
186-
if (new_resp != NULL) {
187-
if (new_resp_len < old_size) {
188-
memcpy(buf, new_resp, new_resp_len);
189-
*buflen = new_resp_len;
190-
buf[2] |= 0x02; // set truncation flag
191-
ILOG("%04hX: DNS response size truncated from %u to %u to keep %u limit",
192-
tx_id, old_size, new_resp_len, size_limit);
193-
}
194-
ares_free_string(new_resp);
195-
new_resp = NULL;
196-
}
197-
}
198-
19989
static void udp_respond(dns_listener_t *self, struct sockaddr *raddr,
20090
const char *dns_req, size_t dns_req_len,
20191
char *dns_resp, size_t dns_resp_len) {
@@ -205,16 +95,7 @@ static void udp_respond(dns_listener_t *self, struct sockaddr *raddr,
20595
WLOG("Malformed response received, invalid length: %u", dns_resp_len);
20696
return;
20797
}
208-
if (dns_resp_len > DNS_SIZE_LIMIT) {
209-
const uint16_t udp_size = get_edns_udp_size(dns_req, dns_req_len);
210-
if (dns_resp_len > udp_size) {
211-
truncate_dns_response(dns_resp, &dns_resp_len, udp_size);
212-
} else {
213-
uint16_t tx_id = ntohs(*((uint16_t*)dns_req));
214-
DLOG("%04hX: DNS response size %u larger than %d but EDNS0 UDP buffer size %u allows it",
215-
tx_id, dns_resp_len, DNS_SIZE_LIMIT, udp_size);
216-
}
217-
}
98+
dns_truncate_for_udp(dns_req, dns_req_len, dns_resp, &dns_resp_len);
21899

219100
ssize_t len = sendto(d->sock, dns_resp, dns_resp_len, 0, raddr, d->addrlen);
220101
if (len == -1) {

src/dns_truncate.c

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#include <ares.h>
2+
#include <arpa/inet.h>
3+
#include <stdint.h>
4+
#include <stdlib.h>
5+
#include <string.h>
6+
7+
#include "dns_common.h"
8+
#include "dns_truncate.h"
9+
#include "logging.h"
10+
11+
// Returns the size limit the request peer is willing to accept. Reads the
12+
// EDNS0 OPT record from the request's additional section. Falls back to the
13+
// RFC1035 4.2.1 default of 512 if the request can't be parsed or the OPT
14+
// advertises a smaller size (RFC6891 4.3 mandates that values below 512
15+
// MUST be treated as 512).
16+
static uint16_t get_edns_udp_size(const char *dns_req, const size_t dns_req_len) {
17+
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);
19+
if (parse_status != ARES_SUCCESS) {
20+
WLOG("Failed to parse DNS request: %s", ares_strerror((int)parse_status));
21+
return DNS_SIZE_LIMIT;
22+
}
23+
const uint16_t tx_id = ares_dns_record_get_id(dnsrec);
24+
uint16_t udp_size = 0;
25+
const size_t record_count = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ADDITIONAL);
26+
for (size_t i = 0; i < record_count; ++i) {
27+
const ares_dns_rr_t *rr = ares_dns_record_rr_get(dnsrec, ARES_SECTION_ADDITIONAL, i);
28+
if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) {
29+
udp_size = ares_dns_rr_get_u16(rr, ARES_RR_OPT_UDP_SIZE);
30+
if (udp_size > 0) {
31+
DLOG("%04hX: Found EDNS0 UDP buffer size: %u", tx_id, udp_size);
32+
}
33+
break;
34+
}
35+
}
36+
ares_dns_record_destroy(dnsrec);
37+
if (udp_size < DNS_SIZE_LIMIT) {
38+
DLOG("%04hX: EDNS0 UDP buffer size %u overruled to %d", tx_id, udp_size, DNS_SIZE_LIMIT);
39+
return DNS_SIZE_LIMIT;
40+
}
41+
return udp_size;
42+
}
43+
44+
static void truncate_to_size_limit(char *buf, size_t *buflen, const uint16_t size_limit) {
45+
const size_t old_size = *buflen;
46+
buf[2] |= 0x02; // anyway: set truncation flag
47+
48+
ares_dns_record_t *dnsrec = NULL;
49+
ares_status_t status = ares_dns_parse((const unsigned char *)buf, *buflen, 0, &dnsrec);
50+
if (status != ARES_SUCCESS) {
51+
WLOG("Failed to parse DNS response: %s", ares_strerror((int)status));
52+
return;
53+
}
54+
const uint16_t tx_id = ares_dns_record_get_id(dnsrec);
55+
56+
// NOTE: according to current c-ares implementation, removing first or last elements are the fastest!
57+
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);
61+
if (status != ARES_SUCCESS) {
62+
WLOG("%04hX: Could not remove additional record: %s", tx_id, ares_strerror((int)status));
63+
}
64+
}
65+
while (ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_AUTHORITY) > 0) {
66+
status = ares_dns_record_rr_del(dnsrec, ARES_SECTION_AUTHORITY, 0);
67+
if (status != ARES_SUCCESS) {
68+
WLOG("%04hX: Could not remove authority record: %s", tx_id, ares_strerror((int)status));
69+
}
70+
}
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;
89+
}
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?
93+
}
94+
ares_free_string(new_resp);
95+
new_resp = NULL;
96+
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);
99+
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+
}
111+
ares_dns_record_destroy(dnsrec);
112+
113+
if (new_resp != NULL) {
114+
if (new_resp_len < old_size) {
115+
memcpy(buf, new_resp, new_resp_len);
116+
*buflen = new_resp_len;
117+
buf[2] |= 0x02; // set truncation flag
118+
ILOG("%04hX: DNS response size truncated from %u to %u to keep %u limit",
119+
tx_id, old_size, new_resp_len, size_limit);
120+
}
121+
ares_free_string(new_resp);
122+
new_resp = NULL;
123+
}
124+
}
125+
126+
void dns_truncate_for_udp(const char *dns_req, size_t dns_req_len,
127+
char *resp, size_t *resp_len) {
128+
if (*resp_len <= DNS_SIZE_LIMIT) {
129+
return; // always fits
130+
}
131+
const uint16_t udp_size = get_edns_udp_size(dns_req, dns_req_len);
132+
if (*resp_len <= udp_size) {
133+
uint16_t tx_id = ntohs(*((uint16_t*)dns_req));
134+
DLOG("%04hX: DNS response size %zu larger than %d but EDNS0 UDP buffer size %u allows it",
135+
tx_id, *resp_len, DNS_SIZE_LIMIT, udp_size);
136+
return;
137+
}
138+
truncate_to_size_limit(resp, resp_len, udp_size);
139+
}

src/dns_truncate.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#ifndef _DNS_TRUNCATE_H_
2+
#define _DNS_TRUNCATE_H_
3+
4+
#include <sys/types.h>
5+
6+
// Fit a DNS response into the size limit advertised by the request.
7+
//
8+
// If `resp` exceeds the request's EDNS0 UDP buffer size (or RFC1035 4.2.1's
9+
// 512-byte default when no EDNS0 OPT record is present), shrink it in place
10+
// by dropping additional and authority records, then answer records, until
11+
// it fits. The TC flag is set on truncation. A response that already fits
12+
// is left untouched.
13+
//
14+
// Mutates `resp` and `*resp_len`. Caller retains ownership of both buffers.
15+
//
16+
// DNS-over-TCP has no per-message size cap and never needs this.
17+
void dns_truncate_for_udp(const char *dns_req, size_t dns_req_len,
18+
char *resp, size_t *resp_len);
19+
20+
#endif // _DNS_TRUNCATE_H_

0 commit comments

Comments
 (0)