ares_dns_name.c (18545B)
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 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 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 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 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 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 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 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 187 static void ares_dns_name_labels_del_last(ares_array_t *labels) 188 { 189 ares_array_remove_last(labels); 190 } 191 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 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 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 */ 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 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 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 }