1 /* MIT License
2 *
3 * Copyright (c) 2023 Brad House
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice (including the next
13 * paragraph) shall be included in all copies or substantial portions of the
14 * Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 *
24 * SPDX-License-Identifier: MIT
25 */
26 #include "ares_private.h"
27
28 typedef struct {
29 char *name;
30 size_t name_len;
31 size_t idx;
32 } ares_nameoffset_t;
33
ares_nameoffset_free(void * arg)34 static void ares_nameoffset_free(void *arg)
35 {
36 ares_nameoffset_t *off = arg;
37 if (off == NULL) {
38 return; /* LCOV_EXCL_LINE: DefensiveCoding */
39 }
40 ares_free(off->name);
41 ares_free(off);
42 }
43
ares_nameoffset_create(ares_llist_t ** list,const char * name,size_t idx)44 static ares_status_t ares_nameoffset_create(ares_llist_t **list,
45 const char *name, size_t idx)
46 {
47 ares_status_t status;
48 ares_nameoffset_t *off = NULL;
49
50 if (list == NULL || name == NULL || ares_strlen(name) == 0 ||
51 ares_strlen(name) > 255) {
52 return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
53 }
54
55 if (*list == NULL) {
56 *list = ares_llist_create(ares_nameoffset_free);
57 }
58 if (*list == NULL) {
59 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
60 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
61 }
62
63 off = ares_malloc_zero(sizeof(*off));
64 if (off == NULL) {
65 return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
66 }
67
68 off->name = ares_strdup(name);
69 off->name_len = ares_strlen(off->name);
70 off->idx = idx;
71
72 if (ares_llist_insert_last(*list, off) == NULL) {
73 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
74 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
75 }
76
77 return ARES_SUCCESS;
78
79 /* LCOV_EXCL_START: OutOfMemory */
80 fail:
81 ares_nameoffset_free(off);
82 return status;
83 /* LCOV_EXCL_STOP */
84 }
85
ares_nameoffset_find(ares_llist_t * list,const char * name)86 static const ares_nameoffset_t *ares_nameoffset_find(ares_llist_t *list,
87 const char *name)
88 {
89 size_t name_len = ares_strlen(name);
90 ares_llist_node_t *node;
91 const ares_nameoffset_t *longest_match = NULL;
92
93 if (list == NULL || name == NULL || name_len == 0) {
94 return NULL;
95 }
96
97 for (node = ares_llist_node_first(list); node != NULL;
98 node = ares_llist_node_next(node)) {
99 const ares_nameoffset_t *val = ares_llist_node_val(node);
100 size_t prefix_len;
101
102 /* Can't be a match if the stored name is longer */
103 if (val->name_len > name_len) {
104 continue;
105 }
106
107 /* Can't be the longest match if our existing longest match is longer */
108 if (longest_match != NULL && longest_match->name_len > val->name_len) {
109 continue;
110 }
111
112 prefix_len = name_len - val->name_len;
113
114 /* Due to DNS 0x20, lets not inadvertently mangle things, use case-sensitive
115 * matching instead of case-insensitive. This may result in slightly
116 * larger DNS queries overall. */
117 if (!ares_streq(val->name, name + prefix_len)) {
118 continue;
119 }
120
121 /* We need to make sure if `val->name` is "example.com" that name is
122 * is separated by a label, e.g. "myexample.com" is not ok, however
123 * "my.example.com" is, so we look for the preceding "." */
124 if (prefix_len != 0 && name[prefix_len - 1] != '.') {
125 continue;
126 }
127
128 longest_match = val;
129 }
130
131 return longest_match;
132 }
133
ares_dns_labels_free_cb(void * arg)134 static void ares_dns_labels_free_cb(void *arg)
135 {
136 ares_buf_t **buf = arg;
137 if (buf == NULL) {
138 return;
139 }
140
141 ares_buf_destroy(*buf);
142 }
143
ares_dns_labels_add(ares_array_t * labels)144 static ares_buf_t *ares_dns_labels_add(ares_array_t *labels)
145 {
146 ares_buf_t **buf;
147
148 if (labels == NULL) {
149 return NULL; /* LCOV_EXCL_LINE: DefensiveCoding */
150 }
151
152 if (ares_array_insert_last((void **)&buf, labels) != ARES_SUCCESS) {
153 return NULL;
154 }
155
156 *buf = ares_buf_create();
157 if (*buf == NULL) {
158 ares_array_remove_last(labels);
159 return NULL;
160 }
161
162 return *buf;
163 }
164
ares_dns_labels_get_last(ares_array_t * labels)165 static ares_buf_t *ares_dns_labels_get_last(ares_array_t *labels)
166 {
167 ares_buf_t **buf = ares_array_last(labels);
168
169 if (buf == NULL) {
170 return NULL;
171 }
172
173 return *buf;
174 }
175
ares_dns_labels_get_at(ares_array_t * labels,size_t idx)176 static ares_buf_t *ares_dns_labels_get_at(ares_array_t *labels, size_t idx)
177 {
178 ares_buf_t **buf = ares_array_at(labels, idx);
179
180 if (buf == NULL) {
181 return NULL;
182 }
183
184 return *buf;
185 }
186
ares_dns_name_labels_del_last(ares_array_t * labels)187 static void ares_dns_name_labels_del_last(ares_array_t *labels)
188 {
189 ares_array_remove_last(labels);
190 }
191
ares_parse_dns_name_escape(ares_buf_t * namebuf,ares_buf_t * label,ares_bool_t validate_hostname)192 static ares_status_t ares_parse_dns_name_escape(ares_buf_t *namebuf,
193 ares_buf_t *label,
194 ares_bool_t validate_hostname)
195 {
196 ares_status_t status;
197 unsigned char c;
198
199 status = ares_buf_fetch_bytes(namebuf, &c, 1);
200 if (status != ARES_SUCCESS) {
201 return ARES_EBADNAME;
202 }
203
204 /* If next character is a digit, read 2 more digits */
205 if (ares_isdigit(c)) {
206 size_t i;
207 unsigned int val = 0;
208
209 val = c - '0';
210
211 for (i = 0; i < 2; i++) {
212 status = ares_buf_fetch_bytes(namebuf, &c, 1);
213 if (status != ARES_SUCCESS) {
214 return ARES_EBADNAME;
215 }
216
217 if (!ares_isdigit(c)) {
218 return ARES_EBADNAME;
219 }
220 val *= 10;
221 val += c - '0';
222 }
223
224 /* Out of range */
225 if (val > 255) {
226 return ARES_EBADNAME;
227 }
228
229 if (validate_hostname && !ares_is_hostnamech((unsigned char)val)) {
230 return ARES_EBADNAME;
231 }
232
233 return ares_buf_append_byte(label, (unsigned char)val);
234 }
235
236 /* We can just output the character */
237 if (validate_hostname && !ares_is_hostnamech(c)) {
238 return ARES_EBADNAME;
239 }
240
241 return ares_buf_append_byte(label, c);
242 }
243
ares_split_dns_name(ares_array_t * labels,ares_bool_t validate_hostname,const char * name)244 static ares_status_t ares_split_dns_name(ares_array_t *labels,
245 ares_bool_t validate_hostname,
246 const char *name)
247 {
248 ares_status_t status;
249 ares_buf_t *label = NULL;
250 ares_buf_t *namebuf = NULL;
251 size_t i;
252 size_t total_len = 0;
253 unsigned char c;
254
255 if (name == NULL || labels == NULL) {
256 return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
257 }
258
259 /* Put name into a buffer for parsing */
260 namebuf = ares_buf_create();
261 if (namebuf == NULL) {
262 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
263 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
264 }
265
266 if (*name != '\0') {
267 status =
268 ares_buf_append(namebuf, (const unsigned char *)name, ares_strlen(name));
269 if (status != ARES_SUCCESS) {
270 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
271 }
272 }
273
274 /* Start with 1 label */
275 label = ares_dns_labels_add(labels);
276 if (label == NULL) {
277 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
278 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
279 }
280
281 while (ares_buf_fetch_bytes(namebuf, &c, 1) == ARES_SUCCESS) {
282 /* New label */
283 if (c == '.') {
284 label = ares_dns_labels_add(labels);
285 if (label == NULL) {
286 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
287 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
288 }
289 continue;
290 }
291
292 /* Escape */
293 if (c == '\\') {
294 status = ares_parse_dns_name_escape(namebuf, label, validate_hostname);
295 if (status != ARES_SUCCESS) {
296 goto done;
297 }
298 continue;
299 }
300
301 /* Output direct character */
302 if (validate_hostname && !ares_is_hostnamech(c)) {
303 status = ARES_EBADNAME;
304 goto done;
305 }
306
307 status = ares_buf_append_byte(label, c);
308 if (status != ARES_SUCCESS) {
309 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
310 }
311 }
312
313 /* Remove trailing blank label */
314 if (ares_buf_len(ares_dns_labels_get_last(labels)) == 0) {
315 ares_dns_name_labels_del_last(labels);
316 }
317
318 /* If someone passed in "." there could have been 2 blank labels, check for
319 * that */
320 if (ares_array_len(labels) == 1 &&
321 ares_buf_len(ares_dns_labels_get_last(labels)) == 0) {
322 ares_dns_name_labels_del_last(labels);
323 }
324
325 /* Scan to make sure label lengths are valid */
326 for (i = 0; i < ares_array_len(labels); i++) {
327 const ares_buf_t *buf = ares_dns_labels_get_at(labels, i);
328 size_t len = ares_buf_len(buf);
329 /* No 0-length labels, and no labels over 63 bytes */
330 if (len == 0 || len > 63) {
331 status = ARES_EBADNAME;
332 goto done;
333 }
334 total_len += len;
335 }
336
337 /* Can't exceed maximum (unescaped) length */
338 if (ares_array_len(labels) && total_len + ares_array_len(labels) - 1 > 255) {
339 status = ARES_EBADNAME;
340 goto done;
341 }
342
343 status = ARES_SUCCESS;
344
345 done:
346 ares_buf_destroy(namebuf);
347 return status;
348 }
349
ares_dns_name_write(ares_buf_t * buf,ares_llist_t ** list,ares_bool_t validate_hostname,const char * name)350 ares_status_t ares_dns_name_write(ares_buf_t *buf, ares_llist_t **list,
351 ares_bool_t validate_hostname,
352 const char *name)
353 {
354 const ares_nameoffset_t *off = NULL;
355 size_t name_len;
356 size_t orig_name_len;
357 size_t pos = ares_buf_len(buf);
358 ares_array_t *labels = NULL;
359 char name_copy[512];
360 ares_status_t status;
361
362 if (buf == NULL || name == NULL) {
363 return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
364 }
365
366 labels = ares_array_create(sizeof(ares_buf_t *), ares_dns_labels_free_cb);
367 if (labels == NULL) {
368 return ARES_ENOMEM;
369 }
370
371 /* NOTE: due to possible escaping, name_copy buffer is > 256 to allow for
372 * this */
373 name_len = ares_strcpy(name_copy, name, sizeof(name_copy));
374 orig_name_len = name_len;
375
376 /* Find longest match */
377 if (list != NULL) {
378 off = ares_nameoffset_find(*list, name_copy);
379 if (off != NULL && off->name_len != name_len) {
380 /* truncate */
381 name_len -= (off->name_len + 1);
382 name_copy[name_len] = 0;
383 }
384 }
385
386 /* Output labels */
387 if (off == NULL || off->name_len != orig_name_len) {
388 size_t i;
389
390 status = ares_split_dns_name(labels, validate_hostname, name_copy);
391 if (status != ARES_SUCCESS) {
392 goto done;
393 }
394
395 for (i = 0; i < ares_array_len(labels); i++) {
396 size_t len = 0;
397 const ares_buf_t *lbuf = ares_dns_labels_get_at(labels, i);
398 const unsigned char *ptr = ares_buf_peek(lbuf, &len);
399
400 status = ares_buf_append_byte(buf, (unsigned char)(len & 0xFF));
401 if (status != ARES_SUCCESS) {
402 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
403 }
404
405 status = ares_buf_append(buf, ptr, len);
406 if (status != ARES_SUCCESS) {
407 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
408 }
409 }
410
411 /* If we are NOT jumping to another label, output terminator */
412 if (off == NULL) {
413 status = ares_buf_append_byte(buf, 0);
414 if (status != ARES_SUCCESS) {
415 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
416 }
417 }
418 }
419
420 /* Output name compression offset jump */
421 if (off != NULL) {
422 unsigned short u16 =
423 (unsigned short)0xC000 | (unsigned short)(off->idx & 0x3FFF);
424 status = ares_buf_append_be16(buf, u16);
425 if (status != ARES_SUCCESS) {
426 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
427 }
428 }
429
430 /* Store pointer for future jumps as long as its not an exact match for
431 * a prior entry */
432 if (list != NULL && (off == NULL || off->name_len != orig_name_len) &&
433 name_len > 0) {
434 status = ares_nameoffset_create(list, name /* not truncated copy! */, pos);
435 if (status != ARES_SUCCESS) {
436 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
437 }
438 }
439
440 status = ARES_SUCCESS;
441
442 done:
443 ares_array_destroy(labels);
444 return status;
445 }
446
447 /* Reserved characters for names that need to be escaped */
is_reservedch(int ch)448 static ares_bool_t is_reservedch(int ch)
449 {
450 switch (ch) {
451 case '"':
452 case '.':
453 case ';':
454 case '\\':
455 case '(':
456 case ')':
457 case '@':
458 case '$':
459 return ARES_TRUE;
460 default:
461 break;
462 }
463
464 return ARES_FALSE;
465 }
466
ares_fetch_dnsname_into_buf(ares_buf_t * buf,ares_buf_t * dest,size_t len,ares_bool_t is_hostname)467 static ares_status_t ares_fetch_dnsname_into_buf(ares_buf_t *buf,
468 ares_buf_t *dest, size_t len,
469 ares_bool_t is_hostname)
470 {
471 size_t remaining_len;
472 const unsigned char *ptr = ares_buf_peek(buf, &remaining_len);
473 ares_status_t status;
474 size_t i;
475
476 if (buf == NULL || len == 0 || remaining_len < len) {
477 return ARES_EBADRESP;
478 }
479
480 for (i = 0; i < len; i++) {
481 unsigned char c = ptr[i];
482
483 /* Hostnames have a very specific allowed character set. Anything outside
484 * of that (non-printable and reserved included) are disallowed */
485 if (is_hostname && !ares_is_hostnamech(c)) {
486 status = ARES_EBADRESP;
487 goto fail;
488 }
489
490 /* NOTE: dest may be NULL if the user is trying to skip the name. validation
491 * still occurs above. */
492 if (dest == NULL) {
493 continue;
494 }
495
496 /* Non-printable characters need to be output as \DDD */
497 if (!ares_isprint(c)) {
498 unsigned char escape[4];
499
500 escape[0] = '\\';
501 escape[1] = '0' + (c / 100);
502 escape[2] = '0' + ((c % 100) / 10);
503 escape[3] = '0' + (c % 10);
504
505 status = ares_buf_append(dest, escape, sizeof(escape));
506 if (status != ARES_SUCCESS) {
507 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
508 }
509
510 continue;
511 }
512
513 /* Reserved characters need to be escaped, otherwise normal */
514 if (is_reservedch(c)) {
515 status = ares_buf_append_byte(dest, '\\');
516 if (status != ARES_SUCCESS) {
517 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
518 }
519 }
520
521 status = ares_buf_append_byte(dest, c);
522 if (status != ARES_SUCCESS) {
523 return status; /* LCOV_EXCL_LINE: OutOfMemory */
524 }
525 }
526
527 return ares_buf_consume(buf, len);
528
529 fail:
530 return status;
531 }
532
ares_dns_name_parse(ares_buf_t * buf,char ** name,ares_bool_t is_hostname)533 ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name,
534 ares_bool_t is_hostname)
535 {
536 size_t save_offset = 0;
537 unsigned char c;
538 ares_status_t status;
539 ares_buf_t *namebuf = NULL;
540 size_t label_start = ares_buf_get_position(buf);
541
542 if (buf == NULL) {
543 return ARES_EFORMERR;
544 }
545
546 if (name != NULL) {
547 namebuf = ares_buf_create();
548 if (namebuf == NULL) {
549 status = ARES_ENOMEM;
550 goto fail;
551 }
552 }
553
554 /* The compression scheme allows a domain name in a message to be
555 * represented as either:
556 *
557 * - a sequence of labels ending in a zero octet
558 * - a pointer
559 * - a sequence of labels ending with a pointer
560 */
561 while (1) {
562 /* Keep track of the minimum label starting position to prevent forward
563 * jumping */
564 if (label_start > ares_buf_get_position(buf)) {
565 label_start = ares_buf_get_position(buf);
566 }
567
568 status = ares_buf_fetch_bytes(buf, &c, 1);
569 if (status != ARES_SUCCESS) {
570 goto fail;
571 }
572
573 /* Pointer/Redirect */
574 if ((c & 0xc0) == 0xc0) {
575 /* The pointer takes the form of a two octet sequence:
576 *
577 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
578 * | 1 1| OFFSET |
579 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
580 *
581 * The first two bits are ones. This allows a pointer to be distinguished
582 * from a label, since the label must begin with two zero bits because
583 * labels are restricted to 63 octets or less. (The 10 and 01
584 * combinations are reserved for future use.) The OFFSET field specifies
585 * an offset from the start of the message (i.e., the first octet of the
586 * ID field in the domain header). A zero offset specifies the first byte
587 * of the ID field, etc.
588 */
589 size_t offset = (size_t)((c & 0x3F) << 8);
590
591 /* Fetch second byte of the redirect length */
592 status = ares_buf_fetch_bytes(buf, &c, 1);
593 if (status != ARES_SUCCESS) {
594 goto fail;
595 }
596
597 offset |= ((size_t)c);
598
599 /* According to RFC 1035 4.1.4:
600 * In this scheme, an entire domain name or a list of labels at
601 * the end of a domain name is replaced with a pointer to a prior
602 * occurrence of the same name.
603 * Note the word "prior", meaning it must go backwards. This was
604 * confirmed via the ISC BIND code that it also prevents forward
605 * pointers.
606 */
607 if (offset >= label_start) {
608 status = ARES_EBADNAME;
609 goto fail;
610 }
611
612 /* First time we make a jump, save the current position */
613 if (save_offset == 0) {
614 save_offset = ares_buf_get_position(buf);
615 }
616
617 status = ares_buf_set_position(buf, offset);
618 if (status != ARES_SUCCESS) {
619 status = ARES_EBADNAME;
620 goto fail;
621 }
622
623 continue;
624 } else if ((c & 0xc0) != 0) {
625 /* 10 and 01 are reserved */
626 status = ARES_EBADNAME;
627 goto fail;
628 } else if (c == 0) {
629 /* termination via zero octet*/
630 break;
631 }
632
633 /* New label */
634
635 /* Labels are separated by periods */
636 if (ares_buf_len(namebuf) != 0 && name != NULL) {
637 status = ares_buf_append_byte(namebuf, '.');
638 if (status != ARES_SUCCESS) {
639 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
640 }
641 }
642
643 status = ares_fetch_dnsname_into_buf(buf, namebuf, c, is_hostname);
644 if (status != ARES_SUCCESS) {
645 goto fail;
646 }
647 }
648
649 /* Restore offset read after first redirect/pointer as this is where the DNS
650 * message continues */
651 if (save_offset) {
652 ares_buf_set_position(buf, save_offset);
653 }
654
655 if (name != NULL) {
656 *name = ares_buf_finish_str(namebuf, NULL);
657 if (*name == NULL) {
658 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
659 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
660 }
661 }
662
663 return ARES_SUCCESS;
664
665 fail:
666 /* We want badname response if we couldn't parse */
667 if (status == ARES_EBADRESP) {
668 status = ARES_EBADNAME;
669 }
670
671 ares_buf_destroy(namebuf);
672 return status;
673 }
674