|
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) |
3 | 4 | #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) |
6 | 7 |
|
7 | 8 | #include "dns_server.h" |
8 | 9 | #include "logging.h" |
@@ -51,7 +52,7 @@ static void watcher_cb(struct ev_loop __attribute__((unused)) *loop, |
51 | 52 | return; |
52 | 53 | } |
53 | 54 |
|
54 | | - if (len < (int)sizeof(uint16_t)) { |
| 55 | + if (len < DNS_HEADER_LENGTH) { |
55 | 56 | WLOG("Malformed request received, too short: %d", len); |
56 | 57 | return; |
57 | 58 | } |
@@ -80,9 +81,127 @@ void dns_server_init(dns_server_t *d, struct ev_loop *loop, |
80 | 81 | ev_io_start(d->loop, &d->watcher); |
81 | 82 | } |
82 | 83 |
|
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); |
86 | 205 | if(len == -1) { |
87 | 206 | DLOG("sendto failed: %s", strerror(errno)); |
88 | 207 | } |
|
0 commit comments