1 /* $NetBSD: dnsstream.h,v 1.2 2025/01/26 16:25:40 christos Exp $ */ 2 3 /* 4 * Copyright (C) Internet Systems Consortium, Inc. ("ISC") 5 * 6 * SPDX-License-Identifier: MPL-2.0 7 * 8 * This Source Code Form is subject to the terms of the Mozilla Public 9 * License, v. 2.0. If a copy of the MPL was not distributed with this 10 * file, you can obtain one at https://mozilla.org/MPL/2.0/. 11 * 12 * See the COPYRIGHT file distributed with this work for additional 13 * information regarding copyright ownership. 14 */ 15 #pragma once 16 17 #include <isc/buffer.h> 18 #include <isc/mem.h> 19 20 typedef struct isc_dnsstream_assembler isc_dnsstream_assembler_t; 21 /*!< 22 * \brief The 'isc_dnsstream_assembler_t' object is built on top of 23 * 'isc_buffer_t' and intended to encapsulate the state machine 24 * used for handling DNS messages received in the format used for 25 * messages transmitted over TCP. 26 * 27 * The idea is that the object accepts the input data received from a 28 * socket (or anywhere else, for that matter), tries to assemble DNS 29 * messages from the incoming data and calls the callback passing it 30 * the status of the incoming data as well as a pointer to the memory 31 * region referencing the data of the assembled message (in the case 32 * there is enough data to assemble the message). It is capable of 33 * assembling DNS messages no matter how "torn apart" they are when 34 * sent over network. 35 * 36 * The implementation is completely decoupled from the networking code 37 * itself makes it trivial to write unit tests for it, leading to 38 * better verification of its correctness. Another important aspect 39 * of its functioning is directly related to the fact that it is built 40 * on top of 'isc_buffer_t', which tries to manage memory in a 41 * smart way. In particular: 42 * 43 *\li It tries to use a static buffer for smaller messages, reducing 44 * pressure on the memory manager (hot path); 45 * 46 *\li When allocating dynamic memory for larger messages, it tries to 47 * allocate memory conservatively (generic path). 48 * 49 * That is, when using 'isc_dnsstream_assembler_t', we allocate memory 50 * conservatively, avoiding any allocations whatsoever for small DNS 51 * messages (whose size is lesser of equal to 512 bytes). The last 52 * characteristic is important in the context of DNS, as most of DNS 53 * messages are small. 54 */ 55 56 typedef bool (*isc_dnsstream_assembler_cb_t)(isc_dnsstream_assembler_t *dnsasm, 57 const isc_result_t result, 58 isc_region_t *restrict region, 59 void *cbarg, void *userarg); 60 /*!< 61 * /brief The type of callback called when processing the data passed to a 62 * 'isc_dnsstream_assembler_t' type. 63 * 64 * The callback accepts the following arguments: 65 * 66 *\li 'isc_dnsstream_assembler_t *dnsasm' - a pointer to the 67 * 'isc_dnsstream_assembler_t' object in use; 68 *\li 'isc_result_t result' - processing status; 69 *\li 'isc_region_t *region' - the region referencing the DNS message if 70 * assembled, empty otherwise; 71 *\li 'void *cbarg' - the callback argument, set during the object 72 * initialisation or when setting the callback; 73 *\li 'void *userarg' - the callback argument passed to it when processing the 74 * current chunk of data; 75 * 76 * Return values: 77 * 78 *\li 'true' - continue processing data, if there is any non-processed data 79 * left; 80 *\li 'false' - stop processing data regardless of non-processed data 81 * availability. 82 * 83 * Processing status values: 84 * 85 *\li 'ISC_R_SUCCESS' - a message has been successfully assembled; 86 *\li 'ISC_R_NOMORE' - not enough data to assemble a DNS message, need to get 87 more; 88 *\li 'ISC_R_RANGE' - there was an attempt to process a zero-sized DNS 89 message (i.e. someone attempts to send us junk data). 90 */ 91 92 #define ISC_DNSSTREAM_STATIC_BUFFER_SIZE (512) 93 94 struct isc_dnsstream_assembler { 95 isc_buffer_t dnsbuf; /*!< Internal buffer for assembling DNS 96 messages. */ 97 uint8_t buf[ISC_DNSSTREAM_STATIC_BUFFER_SIZE]; 98 isc_buffer_t *current; /*!< Pointer to the currently used data buffer. 99 Most of the time it point to the 'dnsbuf' 100 except when dealing with data in place (when 101 it points to a temporary buffer) */ 102 isc_dnsstream_assembler_cb_t onmsg_cb; /*!< Data processing callback. */ 103 void *cbarg; /*!< Callback argument. */ 104 bool calling_cb; /*<! Callback calling marker. Used to detect recursive 105 object uses (changing the data state from withing 106 the callback). */ 107 isc_result_t result; /*<! The last passed to the callback processing 108 status value. */ 109 isc_mem_t *mctx; 110 }; 111 112 static inline void 113 isc_dnsstream_assembler_init(isc_dnsstream_assembler_t *restrict dnsasm, 114 isc_mem_t *memctx, isc_dnsstream_assembler_cb_t cb, 115 void *cbarg); 116 /*!< 117 * \brief Initialise the given 'isc_dnsstream_assembler_t' object, attach 118 * to the memory context. 119 * 120 * Requires: 121 *\li 'dnsasm' is not NULL; 122 *\li 'memctx' is not NULL; 123 *\li 'cb' is not NULL. 124 */ 125 126 static inline void 127 isc_dnsstream_assembler_uninit(isc_dnsstream_assembler_t *restrict dnsasm); 128 /*!< 129 * \brief Un-initialise the given 'isc_dnsstream_assembler_t' object, detach 130 * to the attached memory context. Destroys any internal unprocessed data. 131 * 132 * Requires: 133 *\li 'dnsasm' is not NULL. 134 */ 135 136 static inline isc_dnsstream_assembler_t * 137 isc_dnsstream_assembler_new(isc_mem_t *memctx, isc_dnsstream_assembler_cb_t cb, 138 void *cbarg); 139 /*!< 140 * \brief Allocate and initialise a new 'isc_dnsstream_assembler_t' object, 141 * attach to the memory context. 142 * 143 * Requires: 144 *\li 'dnsasm' is not NULL; 145 *\li 'memctx' is not NULL; 146 *\li 'cb' is not NULL. 147 */ 148 149 static inline void 150 isc_dnsstream_assembler_free(isc_dnsstream_assembler_t **restrict dnsasm); 151 /*!< 152 * \brief Un-initialise the given 'isc_dnsstream_assembler_t' object, detach 153 * to the attached memory context, free the memory consumed by the object. 154 * 155 * Requires: 156 *\li 'dnsasm' is not NULL; 157 *\li 'dnsasm' is not pointing to NULL. 158 */ 159 160 static inline void 161 isc_dnsstream_assembler_setcb(isc_dnsstream_assembler_t *restrict dnsasm, 162 isc_dnsstream_assembler_cb_t cb, void *cbarg); 163 /*!< 164 * \brief Change the data processing callback and its argument within the given 165 * 'isc_dnsstream_assembler_t' object. 166 * 167 * Requires: 168 *\li 'dnsasm' is not NULL; 169 *\li 'cb' is not NULL. 170 */ 171 172 static inline void 173 isc_dnsstream_assembler_incoming(isc_dnsstream_assembler_t *restrict dnsasm, 174 void *userarg, void *restrict buf, 175 const unsigned int buf_size); 176 /*!< 177 * \brief Process the new incoming data to the given 178 * 'isc_dnsstream_assembler_t' or continue processing the currently 179 * unprocessed data (when 'buf' equals NULL and 'buf_size' equals 180 * 0). Call the callback passing a status of data to it. 181 * 182 * To avoid erroneously recursive usage of the object, it is forbidden to call 183 * this function from within the callback. Doing so will abort the program. 184 * 185 * Requires: 186 *\li 'dnsasm' is not NULL. 187 */ 188 189 static inline isc_result_t 190 isc_dnsstream_assembler_result(const isc_dnsstream_assembler_t *restrict dnsasm); 191 /*!< 192 * \brief Return the last data processing status passed to the 193 * callback. 194 * 195 * Requires: 196 *\li 'dnsasm' is not NULL. 197 * 198 * Return values: 199 *\li 'ISC_R_SUCCESS' - a message has been successfully assembled; 200 *\li 'ISC_R_NOMORE' - not enough data to assemble a DNS message, need to get 201 more; 202 *\li 'ISC_R_RANGE' - there was an attempt to process a zero-sized DNS; 203 *\li 'ISC_R_UNSET' - not data has been passed to the object. 204 */ 205 206 static inline size_t 207 isc_dnsstream_assembler_remaininglength( 208 const isc_dnsstream_assembler_t *restrict dnsasm); 209 /*!< 210 * \brief Return the amount of currently unprocessed data within the given 211 * 'isc_dnsstream_assembler_t' object 212 * 213 * Requires: 214 *\li 'dnsasm' is not NULL. 215 */ 216 217 static inline void 218 isc_dnsstream_assembler_clear(isc_dnsstream_assembler_t *restrict dnsasm); 219 /*!< 220 * \brief Clear the given 'isc_dnsstream_assembler_t' object from 221 * any unprocessed data, clear the last data processing status (set it to 222 * 'ISC_R_UNSET'). 223 * 224 * Requires: 225 *\li 'dnsasm' is not NULL. 226 */ 227 228 static inline void 229 isc_dnsstream_assembler_init(isc_dnsstream_assembler_t *restrict dnsasm, 230 isc_mem_t *memctx, isc_dnsstream_assembler_cb_t cb, 231 void *cbarg) { 232 REQUIRE(dnsasm != NULL); 233 REQUIRE(memctx != NULL); 234 REQUIRE(cb != NULL); 235 236 *dnsasm = (isc_dnsstream_assembler_t){ .result = ISC_R_UNSET }; 237 isc_dnsstream_assembler_setcb(dnsasm, cb, cbarg); 238 isc_mem_attach(memctx, &dnsasm->mctx); 239 240 isc_buffer_init(&dnsasm->dnsbuf, dnsasm->buf, sizeof(dnsasm->buf)); 241 isc_buffer_setmctx(&dnsasm->dnsbuf, dnsasm->mctx); 242 243 dnsasm->current = &dnsasm->dnsbuf; 244 } 245 246 static inline void 247 isc_dnsstream_assembler_uninit(isc_dnsstream_assembler_t *restrict dnsasm) { 248 REQUIRE(dnsasm != NULL); 249 /* 250 * Uninitialising the object from withing the callback does not 251 * make any sense. 252 */ 253 INSIST(dnsasm->calling_cb == false); 254 isc_buffer_clearmctx(&dnsasm->dnsbuf); 255 isc_buffer_invalidate(&dnsasm->dnsbuf); 256 if (dnsasm->mctx != NULL) { 257 isc_mem_detach(&dnsasm->mctx); 258 } 259 dnsasm->current = NULL; 260 } 261 262 static inline isc_dnsstream_assembler_t * 263 isc_dnsstream_assembler_new(isc_mem_t *memctx, isc_dnsstream_assembler_cb_t cb, 264 void *cbarg) { 265 isc_dnsstream_assembler_t *newasm; 266 267 REQUIRE(memctx != NULL); 268 REQUIRE(cb != NULL); 269 270 newasm = isc_mem_get(memctx, sizeof(*newasm)); 271 isc_dnsstream_assembler_init(newasm, memctx, cb, cbarg); 272 273 return newasm; 274 } 275 276 static inline void 277 isc_dnsstream_assembler_free(isc_dnsstream_assembler_t **restrict dnsasm) { 278 isc_dnsstream_assembler_t *restrict oldasm = NULL; 279 isc_mem_t *memctx = NULL; 280 REQUIRE(dnsasm != NULL && *dnsasm != NULL); 281 282 oldasm = *dnsasm; 283 284 isc_mem_attach(oldasm->mctx, &memctx); 285 isc_dnsstream_assembler_uninit(oldasm); 286 isc_mem_putanddetach(&memctx, oldasm, sizeof(*oldasm)); 287 288 *dnsasm = NULL; 289 } 290 291 static inline void 292 isc_dnsstream_assembler_setcb(isc_dnsstream_assembler_t *restrict dnsasm, 293 isc_dnsstream_assembler_cb_t cb, void *cbarg) { 294 REQUIRE(dnsasm != NULL); 295 REQUIRE(cb != NULL); 296 dnsasm->onmsg_cb = cb; 297 dnsasm->cbarg = cbarg; 298 } 299 300 static inline bool 301 isc__dnsstream_assembler_callcb(isc_dnsstream_assembler_t *restrict dnsasm, 302 const isc_result_t result, 303 isc_region_t *restrict region, void *userarg) { 304 bool ret; 305 306 dnsasm->result = result; 307 dnsasm->calling_cb = true; 308 ret = dnsasm->onmsg_cb(dnsasm, result, region, dnsasm->cbarg, userarg); 309 dnsasm->calling_cb = false; 310 311 return ret; 312 } 313 314 static inline bool 315 isc__dnsstream_assembler_handle_message( 316 isc_dnsstream_assembler_t *restrict dnsasm, void *userarg) { 317 bool cont = false; 318 isc_region_t region = { 0 }; 319 uint16_t dnslen = 0; 320 isc_result_t result; 321 322 INSIST(dnsasm->calling_cb == false); 323 324 result = isc_buffer_peekuint16(dnsasm->current, &dnslen); 325 326 switch (result) { 327 case ISC_R_SUCCESS: 328 if (dnslen == 0) { 329 /* 330 * Someone seems to send us binary junk or output from 331 * /dev/zero 332 */ 333 result = ISC_R_RANGE; 334 isc_dnsstream_assembler_clear(dnsasm); 335 break; 336 } 337 338 if (dnslen > (isc_buffer_remaininglength(dnsasm->current) - 339 sizeof(uint16_t))) 340 { 341 result = ISC_R_NOMORE; 342 break; 343 } 344 break; 345 case ISC_R_NOMORE: 346 break; 347 default: 348 UNREACHABLE(); 349 } 350 351 if (result == ISC_R_SUCCESS) { 352 (void)isc_buffer_getuint16(dnsasm->current); 353 isc_buffer_remainingregion(dnsasm->current, ®ion); 354 region.length = dnslen; 355 cont = isc__dnsstream_assembler_callcb(dnsasm, result, ®ion, 356 userarg); 357 if (isc_buffer_remaininglength(dnsasm->current) >= dnslen) { 358 isc_buffer_forward(dnsasm->current, dnslen); 359 } 360 } else { 361 cont = false; 362 (void)isc__dnsstream_assembler_callcb(dnsasm, result, NULL, 363 userarg); 364 } 365 366 return cont; 367 } 368 369 static inline void 370 isc__dnsstream_assembler_processing(isc_dnsstream_assembler_t *restrict dnsasm, 371 void *userarg) { 372 while (isc__dnsstream_assembler_handle_message(dnsasm, userarg)) { 373 if (isc_buffer_remaininglength(dnsasm->current) == 0) { 374 break; 375 } 376 } 377 } 378 379 static inline void 380 isc__dnsstream_assembler_incoming_direct( 381 isc_dnsstream_assembler_t *restrict dnsasm, void *userarg, 382 void *restrict buf, const unsigned int buf_size) { 383 isc_buffer_t data = { 0 }; 384 isc_region_t remaining = { 0 }; 385 INSIST(dnsasm->current == &dnsasm->dnsbuf); 386 387 isc_buffer_init(&data, buf, buf_size); 388 isc_buffer_add(&data, buf_size); 389 390 /* 391 * Replace the internal buffer within the assembler 392 * object with a temporary buffer referring to the 393 * passed data directly. 394 */ 395 dnsasm->current = &data; 396 397 /* process the data internally */ 398 isc__dnsstream_assembler_processing(dnsasm, userarg); 399 400 /* set the internal buffer back */ 401 dnsasm->current = &dnsasm->dnsbuf; 402 403 isc_buffer_remainingregion(&data, &remaining); 404 if (remaining.length != 0) { 405 /* 406 * Some unprocessed data left - let's put it 407 * into the internal buffer for processing 408 * later. 409 */ 410 isc_buffer_putmem(dnsasm->current, remaining.base, 411 remaining.length); 412 } 413 } 414 415 static inline bool 416 isc__dnsstream_assembler_incoming_direct_non_empty( 417 isc_dnsstream_assembler_t *restrict dnsasm, void *userarg, 418 void *restrict buf, unsigned int buf_size) { 419 size_t remaining; 420 uint16_t dnslen = 0; 421 size_t until_complete = 0; 422 size_t remaining_no_len; 423 424 if (isc_buffer_peekuint16(dnsasm->current, &dnslen) != ISC_R_SUCCESS) { 425 return false; 426 } 427 428 remaining = isc_buffer_remaininglength(dnsasm->current); 429 remaining_no_len = remaining - sizeof(uint16_t); 430 431 /* 432 * We have data for more than one DNS message - that means that on 433 * previous iteration we stopped prematurely intentionally. 434 */ 435 if (remaining_no_len >= dnslen) { 436 return false; 437 } 438 439 /* 440 * At this point we know that we have incomplete message in the 441 * internal buffer, let's find how much data do we need to 442 * complete the message and then check if we have enough data to 443 * handle it. 444 */ 445 until_complete = dnslen - remaining_no_len; 446 447 if (buf_size >= until_complete) { 448 bool cont; 449 uint8_t *unprocessed_buf = NULL; 450 size_t unprocessed_size; 451 452 isc_buffer_putmem(dnsasm->current, buf, until_complete); 453 unprocessed_buf = ((uint8_t *)buf + until_complete); 454 unprocessed_size = buf_size - until_complete; 455 456 /* handle the message */ 457 cont = isc__dnsstream_assembler_handle_message(dnsasm, userarg); 458 isc_buffer_trycompact(dnsasm->current); 459 460 INSIST(isc_buffer_remaininglength(dnsasm->current) == 0); 461 if (unprocessed_size == 0) { 462 return true; 463 } 464 465 if (cont) { 466 /* 467 * The callback logic told us to continue processing 468 * messages, let's try to process the rest directly. 469 */ 470 isc__dnsstream_assembler_incoming_direct( 471 dnsasm, userarg, unprocessed_buf, 472 unprocessed_size); 473 } else { 474 /* 475 * The callback logic told us to stop, let's copy the 476 * remaining data into the internal buffer to process it 477 * later. 478 */ 479 isc_buffer_putmem(dnsasm->current, unprocessed_buf, 480 unprocessed_size); 481 } 482 483 return true; 484 } 485 486 return false; 487 } 488 489 static inline void 490 isc_dnsstream_assembler_incoming(isc_dnsstream_assembler_t *restrict dnsasm, 491 void *userarg, void *restrict buf, 492 const unsigned int buf_size) { 493 REQUIRE(dnsasm != NULL); 494 INSIST(!dnsasm->calling_cb); 495 496 if (buf != NULL && buf_size > 0) { 497 size_t remaining; 498 499 remaining = isc_buffer_remaininglength(&dnsasm->dnsbuf); 500 501 if (remaining == 0) { 502 /* 503 * We can try to handle messages in-place (without 504 * memory copying/re-allocation) in the case we have no 505 * other data in the internal buffer and have received 506 * one or more complete messages at once. This way we 507 * can avoid copying memory into the assembler's 508 * internal buffer. 509 */ 510 isc__dnsstream_assembler_incoming_direct( 511 dnsasm, userarg, buf, buf_size); 512 return; 513 } else if (isc__dnsstream_assembler_incoming_direct_non_empty( 514 dnsasm, userarg, buf, buf_size)) 515 { 516 /* 517 * We had incomplete message in the buffer, but received 518 * enough data to handle it. After that we handle the 519 . * rest (if any) of the messages directly without 520 * copying into the internal buffer. Any data, belonging 521 * to incomplete messages at the end of the buffer, was 522 * copied into the internal buffer to be processed later 523 * when receiving the next batch of data. 524 */ 525 return; 526 } else if (remaining == 1) { 527 /* Mostly the same case as above, but we have incomplete 528 * message length in the buffer and received at least 529 * one byte to complete it. 530 */ 531 void *unprocessed_buf = NULL; 532 size_t unprocessed_size; 533 534 isc_buffer_putmem(dnsasm->current, buf, 1); 535 unprocessed_buf = (uint8_t *)buf + 1; 536 unprocessed_size = buf_size - 1; 537 538 if (isc__dnsstream_assembler_incoming_direct_non_empty( 539 dnsasm, userarg, unprocessed_buf, 540 unprocessed_size)) 541 { 542 return; 543 } 544 545 if (unprocessed_size > 0) { 546 isc_buffer_putmem(dnsasm->current, 547 unprocessed_buf, 548 unprocessed_size); 549 } 550 /* let's continue processing via the generic path */ 551 } else { 552 /* 553 * Put the data into the internal buffer for 554 * processing. 555 */ 556 isc_buffer_putmem(dnsasm->current, buf, buf_size); 557 } 558 } 559 560 isc__dnsstream_assembler_processing(dnsasm, userarg); 561 562 isc_buffer_trycompact(dnsasm->current); 563 } 564 565 static inline isc_result_t 566 isc_dnsstream_assembler_result( 567 const isc_dnsstream_assembler_t *restrict dnsasm) { 568 REQUIRE(dnsasm != NULL); 569 570 return dnsasm->result; 571 } 572 573 static inline size_t 574 isc_dnsstream_assembler_remaininglength( 575 const isc_dnsstream_assembler_t *restrict dnsasm) { 576 REQUIRE(dnsasm != NULL); 577 578 return isc_buffer_remaininglength(dnsasm->current); 579 } 580 581 static inline void 582 isc_dnsstream_assembler_clear(isc_dnsstream_assembler_t *restrict dnsasm) { 583 REQUIRE(dnsasm != NULL); 584 585 isc_buffer_clear(dnsasm->current); 586 if (dnsasm->current != &dnsasm->dnsbuf) { 587 isc_buffer_clear(&dnsasm->dnsbuf); 588 } 589 dnsasm->result = ISC_R_UNSET; 590 } 591