Skip to content

Commit 866a916

Browse files
committed
UDP truncation feature
1 parent df97cee commit 866a916

File tree

5 files changed

+184
-24
lines changed

5 files changed

+184
-24
lines changed

src/dns_server.c

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
#include <ares.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
2-
#include <errno.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
1+
#include <ares.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
2+
#include <ares_dns_record.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
3+
#include <errno.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
34
#include <stdint.h>
4-
#include <string.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
5-
#include <unistd.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
5+
#include <string.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
6+
#include <unistd.h> // NOLINT(llvmlibc-restrict-system-libc-headers)
67

78
#include "dns_server.h"
89
#include "logging.h"
@@ -51,7 +52,7 @@ static void watcher_cb(struct ev_loop __attribute__((unused)) *loop,
5152
return;
5253
}
5354

54-
if (len < (int)sizeof(uint16_t)) {
55+
if (len < DNS_HEADER_LENGTH) {
5556
WLOG("Malformed request received, too short: %d", len);
5657
return;
5758
}
@@ -80,9 +81,127 @@ void dns_server_init(dns_server_t *d, struct ev_loop *loop,
8081
ev_io_start(d->loop, &d->watcher);
8182
}
8283

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

src/dns_server.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
#include <stdint.h>
99
#include <ev.h>
1010

11+
enum {
12+
DNS_HEADER_LENGTH = 12, // RFC1035 4.1.1 header size
13+
DNS_SIZE_LIMIT = 512
14+
};
15+
1116
struct dns_server_s;
1217

1318
typedef void (*dns_req_received_cb)(void *dns_server, uint8_t is_tcp, void *data,
@@ -27,8 +32,8 @@ void dns_server_init(dns_server_t *d, struct ev_loop *loop,
2732
dns_req_received_cb cb, void *data);
2833

2934
// Sends a DNS response 'buf' of length 'blen' to 'raddr'.
30-
void dns_server_respond(dns_server_t *d, struct sockaddr *raddr, char *buf,
31-
size_t blen);
35+
void dns_server_respond(dns_server_t *d, struct sockaddr *raddr,
36+
const char *dns_req, const size_t dns_req_len, char *dns_resp, size_t dns_resp_len);
3237

3338
void dns_server_stop(dns_server_t *d);
3439

src/dns_server_tcp.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
enum {
2121
LISTEN_BACKLOG = 5,
22-
MIN_DNS_LENGTH = 12, // RFC1035 4.1.1 header size
2322
IDLE_TIMEOUT_S = 120, // "two minutes" according to RFC1035 4.2.2
2423
RESEND_DELAY_US = 500, // 0.0005 sec
2524
};
@@ -159,7 +158,7 @@ static void read_cb(struct ev_loop __attribute__((unused)) *loop,
159158
uint16_t req_size = 0;
160159
uint8_t request_received = 0;
161160
while (get_dns_request(client, &dns_req, &req_size)) {
162-
if (req_size < MIN_DNS_LENGTH) {
161+
if (req_size < DNS_HEADER_LENGTH) {
163162
WLOG_CLIENT("Malformed request received, too short: %u", req_size);
164163
free(dns_req);
165164
remove_client(client);
@@ -305,7 +304,7 @@ dns_server_tcp_t * dns_server_tcp_create(
305304
void dns_server_tcp_respond(dns_server_tcp_t *d,
306305
struct sockaddr *raddr, char *resp, size_t resp_len)
307306
{
308-
if (resp_len < MIN_DNS_LENGTH || resp_len > UINT16_MAX) {
307+
if (resp_len < DNS_HEADER_LENGTH || resp_len > UINT16_MAX) {
309308
WLOG("Malformed request received, invalid length: %u", resp_len);
310309
return;
311310
}

src/main.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ typedef struct {
3333
void *dns_server;
3434
uint8_t is_tcp;
3535
char* dns_req;
36+
size_t dns_req_len;
3637
stat_t *stat;
3738
ev_tstamp start_tstamp;
3839
uint16_t tx_id;
@@ -87,27 +88,28 @@ static void https_resp_cb(void *data, char *buf, size_t buflen) {
8788
if (req == NULL) {
8889
FLOG("%04hX: data NULL", req->tx_id);
8990
}
90-
free((void*)req->dns_req);
9191
if (buf != NULL) { // May be NULL for timeout, DNS failure, or something similar.
92-
if (buflen < (int)sizeof(uint16_t)) {
93-
WLOG("%04hX: Malformed response received (too short)", req->tx_id);
92+
if (buflen < DNS_HEADER_LENGTH) {
93+
WLOG("%04hX: Malformed response received, too short: %u", req->tx_id, buflen);
9494
} else {
95-
uint16_t response_id = ntohs(*((uint16_t*)buf));
95+
const uint16_t response_id = ntohs(*((uint16_t*)buf));
9696
if (req->tx_id != response_id) {
9797
WLOG("DNS request and response IDs are not matching: %hX != %hX",
9898
req->tx_id, response_id);
9999
} else {
100100
if (req->is_tcp) {
101101
dns_server_tcp_respond((dns_server_tcp_t *)req->dns_server, (struct sockaddr*)&req->raddr, buf, buflen);
102102
} else {
103-
dns_server_respond((dns_server_t *)req->dns_server, (struct sockaddr*)&req->raddr, buf, buflen);
103+
dns_server_respond((dns_server_t *)req->dns_server, (struct sockaddr*)&req->raddr,
104+
req->dns_req, req->dns_req_len, buf, buflen);
104105
}
105106
if (req->stat) {
106107
stat_request_end(req->stat, buflen, ev_now(req->stat->loop) - req->start_tstamp, req->is_tcp);
107108
}
108109
}
109110
}
110111
}
112+
free((void*)req->dns_req);
111113
free(req);
112114
}
113115

@@ -137,6 +139,7 @@ static void dns_server_cb(void *dns_server, uint8_t is_tcp, void *data,
137139
req->dns_server = dns_server;
138140
req->is_tcp = is_tcp;
139141
req->dns_req = dns_req; // To free buffer after https request is complete.
142+
req->dns_req_len = dns_req_len;
140143
req->stat = app->stat;
141144

142145
if (req->stat) {

tests/robot/functional_tests.robot

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Start Proxy
3131
Set Test Variable ${dig_timeout} 2
3232
Set Test Variable ${dig_retry} 0
3333
Sleep 0.5
34+
Is Process Running ${proxy}
3435
Common Test Setup
3536

3637
Start Proxy With Valgrind
@@ -46,6 +47,7 @@ Start Proxy With Valgrind
4647
Set Test Variable ${dig_timeout} 10
4748
Set Test Variable ${dig_retry} 2
4849
Sleep 6 # wait for valgrind to fire up the proxy
50+
Is Process Running ${proxy}
4951
Common Test Setup
5052

5153
Stop Proxy
@@ -74,17 +76,18 @@ Start Dig
7476
RETURN ${handle}
7577

7678
Stop Dig
77-
[Arguments] ${handle}
79+
[Arguments] ${handle} ${expect}=${None}
7880
${result} = Wait For Process ${handle} timeout=20 secs
7981
Log ${result.stdout}
8082
Should Be Equal As Integers ${result.rc} 0
81-
Should Contain ${result.stdout} ANSWER SECTION
83+
${expect}= Set Variable If $expect is None ANSWER SECTION ${expect}
84+
Should Contain ${result.stdout} ${expect}
8285
RETURN ${result.stdout}
8386

8487
Run Dig
85-
[Arguments] ${domain}=google.com
88+
[Arguments] ${domain}=google.com ${expect}=${None}
8689
${handle} = Start Dig ${domain}
87-
${dig_output} = Stop Dig ${handle}
90+
${dig_output} = Stop Dig ${handle} ${expect}
8891
RETURN ${dig_output}
8992

9093
Run Dig Parallel
@@ -97,11 +100,24 @@ Run Dig Parallel
97100
Stop Dig ${handle}
98101
END
99102

103+
100104
Large Response Test
101105
[Documentation] https://dnscheck.tools/#more
102-
Set Test Variable @{dig_options} @{dig_options} -t txt # ask for TXT response
106+
# use large buffer not to fragment UDP response, and ask for TXT response
107+
Set Test Variable @{dig_options} @{dig_options} +bufsize=5000 -t txt
103108
${dig_output} = Run Dig txtfill4096.test.dnscheck.tools
104-
Should Contain ${dig_output} MSG SIZE \ rcvd: 4185 # expecting more than 4k large response
109+
Should Match Regexp ${dig_output} MSG SIZE\\s+rcvd: 4\\d{3}$ # expecting more than 4k large response
110+
111+
Verify Truncation
112+
[Arguments] ${domain} ${udp_buffer_size} ${result_bytes_min} ${result_bytes_max} ${expect}=${None}
113+
# ask for TXT response
114+
Set Test Variable @{dig_options} +notcp +ignore +bufsize=${udp_buffer_size} -t txt
115+
${dig_output} = Run Dig ${domain} ${expect}
116+
Should Contain ${dig_output} flags: qr tc
117+
# expecting response to be ${result_bytes_min} byte (could be flaky)
118+
@{res} = Should Match Regexp ${dig_output} MSG SIZE\\s+rcvd: (\\d+)$ # expecting more than 4k large response
119+
Should Be True ${res}[1] >= ${result_bytes_min}
120+
Should Be True ${res}[1] <= ${result_bytes_max}
105121

106122

107123
*** Test Cases ***
@@ -167,3 +183,21 @@ Send TCP Requests Fragmented
167183
Should Contain ${dns_reply} google
168184

169185
Close Tcp Client Connection
186+
187+
Truncate UDP Small
188+
Start Proxy
189+
Wait Until Keyword Succeeds 5x 200ms
190+
# too small buffer will be overridden to 512, so expecting more than 300 bytes
191+
... Verify Truncation microsoft.com 256 300 512
192+
193+
Truncate UDP Large
194+
Start Proxy
195+
Wait Until Keyword Succeeds 5x 200ms
196+
# expecting more than 1500 byte large response
197+
... Verify Truncation microsoft.com 2000 1500 2000
198+
199+
Truncate UDP Impossible
200+
Start Proxy
201+
Wait Until Keyword Succeeds 5x 200ms
202+
# the only TXT answer record has to be dropped to met limit
203+
... Verify Truncation txtfill4096.test.dnscheck.tools 4096 12 100 ANSWER: 0

0 commit comments

Comments
 (0)