Home | History | Annotate | Line # | Download | only in isc
      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, &region);
    354 		region.length = dnslen;
    355 		cont = isc__dnsstream_assembler_callcb(dnsasm, result, &region,
    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