/* * Copyright (C) Internet Systems Consortium, Inc. ("ISC") * * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at https://mozilla.org/MPL/2.0/. * * See the COPYRIGHT file distributed with this work for additional * information regarding copyright ownership. */ #pragma once #include #include typedef struct isc_dnsstream_assembler isc_dnsstream_assembler_t; /*!< * \brief The 'isc_dnsstream_assembler_t' object is built on top of * 'isc_buffer_t' and intended to encapsulate the state machine * used for handling DNS messages received in the format used for * messages transmitted over TCP. * * The idea is that the object accepts the input data received from a * socket (or anywhere else, for that matter), tries to assemble DNS * messages from the incoming data and calls the callback passing it * the status of the incoming data as well as a pointer to the memory * region referencing the data of the assembled message (in the case * there is enough data to assemble the message). It is capable of * assembling DNS messages no matter how "torn apart" they are when * sent over network. * * The implementation is completely decoupled from the networking code * itself makes it trivial to write unit tests for it, leading to * better verification of its correctness. Another important aspect * of its functioning is directly related to the fact that it is built * on top of 'isc_buffer_t', which tries to manage memory in a * smart way. In particular: * *\li It tries to use a static buffer for smaller messages, reducing * pressure on the memory manager (hot path); * *\li When allocating dynamic memory for larger messages, it tries to * allocate memory conservatively (generic path). * * That is, when using 'isc_dnsstream_assembler_t', we allocate memory * conservatively, avoiding any allocations whatsoever for small DNS * messages (whose size is lesser of equal to 512 bytes). The last * characteristic is important in the context of DNS, as most of DNS * messages are small. */ typedef bool (*isc_dnsstream_assembler_cb_t)(isc_dnsstream_assembler_t *dnsasm, const isc_result_t result, isc_region_t *restrict region, void *cbarg, void *userarg); /*!< * /brief The type of callback called when processing the data passed to a * 'isc_dnsstream_assembler_t' type. * * The callback accepts the following arguments: * *\li 'isc_dnsstream_assembler_t *dnsasm' - a pointer to the * 'isc_dnsstream_assembler_t' object in use; *\li 'isc_result_t result' - processing status; *\li 'isc_region_t *region' - the region referencing the DNS message if * assembled, empty otherwise; *\li 'void *cbarg' - the callback argument, set during the object * initialisation or when setting the callback; *\li 'void *userarg' - the callback argument passed to it when processing the * current chunk of data; * * Return values: * *\li 'true' - continue processing data, if there is any non-processed data * left; *\li 'false' - stop processing data regardless of non-processed data * availability. * * Processing status values: * *\li 'ISC_R_SUCCESS' - a message has been successfully assembled; *\li 'ISC_R_NOMORE' - not enough data to assemble a DNS message, need to get more; *\li 'ISC_R_RANGE' - there was an attempt to process a zero-sized DNS message (i.e. someone attempts to send us junk data). */ #define ISC_DNSSTREAM_STATIC_BUFFER_SIZE (512) struct isc_dnsstream_assembler { isc_buffer_t dnsbuf; /*!< Internal buffer for assembling DNS messages. */ uint8_t buf[ISC_DNSSTREAM_STATIC_BUFFER_SIZE]; isc_buffer_t *current; /*!< Pointer to the currently used data buffer. Most of the time it point to the 'dnsbuf' except when dealing with data in place (when it points to a temporary buffer) */ isc_dnsstream_assembler_cb_t onmsg_cb; /*!< Data processing callback. */ void *cbarg; /*!< Callback argument. */ bool calling_cb; /*mctx); isc_buffer_init(&dnsasm->dnsbuf, dnsasm->buf, sizeof(dnsasm->buf)); isc_buffer_setmctx(&dnsasm->dnsbuf, dnsasm->mctx); dnsasm->current = &dnsasm->dnsbuf; } static inline void isc_dnsstream_assembler_uninit(isc_dnsstream_assembler_t *restrict dnsasm) { REQUIRE(dnsasm != NULL); /* * Uninitialising the object from withing the callback does not * make any sense. */ INSIST(dnsasm->calling_cb == false); isc_buffer_clearmctx(&dnsasm->dnsbuf); isc_buffer_invalidate(&dnsasm->dnsbuf); if (dnsasm->mctx != NULL) { isc_mem_detach(&dnsasm->mctx); } dnsasm->current = NULL; } static inline isc_dnsstream_assembler_t * isc_dnsstream_assembler_new(isc_mem_t *memctx, isc_dnsstream_assembler_cb_t cb, void *cbarg) { isc_dnsstream_assembler_t *newasm; REQUIRE(memctx != NULL); REQUIRE(cb != NULL); newasm = isc_mem_get(memctx, sizeof(*newasm)); isc_dnsstream_assembler_init(newasm, memctx, cb, cbarg); return newasm; } static inline void isc_dnsstream_assembler_free(isc_dnsstream_assembler_t **restrict dnsasm) { isc_dnsstream_assembler_t *restrict oldasm = NULL; isc_mem_t *memctx = NULL; REQUIRE(dnsasm != NULL && *dnsasm != NULL); oldasm = *dnsasm; isc_mem_attach(oldasm->mctx, &memctx); isc_dnsstream_assembler_uninit(oldasm); isc_mem_putanddetach(&memctx, oldasm, sizeof(*oldasm)); *dnsasm = NULL; } static inline void isc_dnsstream_assembler_setcb(isc_dnsstream_assembler_t *restrict dnsasm, isc_dnsstream_assembler_cb_t cb, void *cbarg) { REQUIRE(dnsasm != NULL); REQUIRE(cb != NULL); dnsasm->onmsg_cb = cb; dnsasm->cbarg = cbarg; } static inline bool isc__dnsstream_assembler_callcb(isc_dnsstream_assembler_t *restrict dnsasm, const isc_result_t result, isc_region_t *restrict region, void *userarg) { bool ret; dnsasm->result = result; dnsasm->calling_cb = true; ret = dnsasm->onmsg_cb(dnsasm, result, region, dnsasm->cbarg, userarg); dnsasm->calling_cb = false; return ret; } static inline bool isc__dnsstream_assembler_handle_message( isc_dnsstream_assembler_t *restrict dnsasm, void *userarg) { bool cont = false; isc_region_t region = { 0 }; uint16_t dnslen = 0; isc_result_t result; INSIST(dnsasm->calling_cb == false); result = isc_buffer_peekuint16(dnsasm->current, &dnslen); switch (result) { case ISC_R_SUCCESS: if (dnslen == 0) { /* * Someone seems to send us binary junk or output from * /dev/zero */ result = ISC_R_RANGE; isc_dnsstream_assembler_clear(dnsasm); break; } if (dnslen > (isc_buffer_remaininglength(dnsasm->current) - sizeof(uint16_t))) { result = ISC_R_NOMORE; break; } break; case ISC_R_NOMORE: break; default: UNREACHABLE(); } if (result == ISC_R_SUCCESS) { (void)isc_buffer_getuint16(dnsasm->current); isc_buffer_remainingregion(dnsasm->current, ®ion); region.length = dnslen; cont = isc__dnsstream_assembler_callcb(dnsasm, result, ®ion, userarg); if (isc_buffer_remaininglength(dnsasm->current) >= dnslen) { isc_buffer_forward(dnsasm->current, dnslen); } } else { cont = false; (void)isc__dnsstream_assembler_callcb(dnsasm, result, NULL, userarg); } return cont; } static inline void isc__dnsstream_assembler_processing(isc_dnsstream_assembler_t *restrict dnsasm, void *userarg) { while (isc__dnsstream_assembler_handle_message(dnsasm, userarg)) { if (isc_buffer_remaininglength(dnsasm->current) == 0) { break; } } } static inline void isc__dnsstream_assembler_incoming_direct( isc_dnsstream_assembler_t *restrict dnsasm, void *userarg, void *restrict buf, const unsigned int buf_size) { isc_buffer_t data = { 0 }; isc_region_t remaining = { 0 }; INSIST(dnsasm->current == &dnsasm->dnsbuf); isc_buffer_init(&data, buf, buf_size); isc_buffer_add(&data, buf_size); /* * Replace the internal buffer within the assembler * object with a temporary buffer referring to the * passed data directly. */ dnsasm->current = &data; /* process the data internally */ isc__dnsstream_assembler_processing(dnsasm, userarg); /* set the internal buffer back */ dnsasm->current = &dnsasm->dnsbuf; isc_buffer_remainingregion(&data, &remaining); if (remaining.length != 0) { /* * Some unprocessed data left - let's put it * into the internal buffer for processing * later. */ isc_buffer_putmem(dnsasm->current, remaining.base, remaining.length); } } static inline bool isc__dnsstream_assembler_incoming_direct_non_empty( isc_dnsstream_assembler_t *restrict dnsasm, void *userarg, void *restrict buf, unsigned int buf_size) { size_t remaining; uint16_t dnslen = 0; size_t until_complete = 0; size_t remaining_no_len; if (isc_buffer_peekuint16(dnsasm->current, &dnslen) != ISC_R_SUCCESS) { return false; } remaining = isc_buffer_remaininglength(dnsasm->current); remaining_no_len = remaining - sizeof(uint16_t); /* * We have data for more than one DNS message - that means that on * previous iteration we stopped prematurely intentionally. */ if (remaining_no_len >= dnslen) { return false; } /* * At this point we know that we have incomplete message in the * internal buffer, let's find how much data do we need to * complete the message and then check if we have enough data to * handle it. */ until_complete = dnslen - remaining_no_len; if (buf_size >= until_complete) { bool cont; uint8_t *unprocessed_buf = NULL; size_t unprocessed_size; isc_buffer_putmem(dnsasm->current, buf, until_complete); unprocessed_buf = ((uint8_t *)buf + until_complete); unprocessed_size = buf_size - until_complete; /* handle the message */ cont = isc__dnsstream_assembler_handle_message(dnsasm, userarg); isc_buffer_trycompact(dnsasm->current); INSIST(isc_buffer_remaininglength(dnsasm->current) == 0); if (unprocessed_size == 0) { return true; } if (cont) { /* * The callback logic told us to continue processing * messages, let's try to process the rest directly. */ isc__dnsstream_assembler_incoming_direct( dnsasm, userarg, unprocessed_buf, unprocessed_size); } else { /* * The callback logic told us to stop, let's copy the * remaining data into the internal buffer to process it * later. */ isc_buffer_putmem(dnsasm->current, unprocessed_buf, unprocessed_size); } return true; } return false; } static inline void isc_dnsstream_assembler_incoming(isc_dnsstream_assembler_t *restrict dnsasm, void *userarg, void *restrict buf, const unsigned int buf_size) { REQUIRE(dnsasm != NULL); INSIST(!dnsasm->calling_cb); if (buf != NULL && buf_size > 0) { size_t remaining; remaining = isc_buffer_remaininglength(&dnsasm->dnsbuf); if (remaining == 0) { /* * We can try to handle messages in-place (without * memory copying/re-allocation) in the case we have no * other data in the internal buffer and have received * one or more complete messages at once. This way we * can avoid copying memory into the assembler's * internal buffer. */ isc__dnsstream_assembler_incoming_direct( dnsasm, userarg, buf, buf_size); return; } else if (isc__dnsstream_assembler_incoming_direct_non_empty( dnsasm, userarg, buf, buf_size)) { /* * We had incomplete message in the buffer, but received * enough data to handle it. After that we handle the . * rest (if any) of the messages directly without * copying into the internal buffer. Any data, belonging * to incomplete messages at the end of the buffer, was * copied into the internal buffer to be processed later * when receiving the next batch of data. */ return; } else if (remaining == 1) { /* Mostly the same case as above, but we have incomplete * message length in the buffer and received at least * one byte to complete it. */ void *unprocessed_buf = NULL; size_t unprocessed_size; isc_buffer_putmem(dnsasm->current, buf, 1); unprocessed_buf = (uint8_t *)buf + 1; unprocessed_size = buf_size - 1; if (isc__dnsstream_assembler_incoming_direct_non_empty( dnsasm, userarg, unprocessed_buf, unprocessed_size)) { return; } if (unprocessed_size > 0) { isc_buffer_putmem(dnsasm->current, unprocessed_buf, unprocessed_size); } /* let's continue processing via the generic path */ } else { /* * Put the data into the internal buffer for * processing. */ isc_buffer_putmem(dnsasm->current, buf, buf_size); } } isc__dnsstream_assembler_processing(dnsasm, userarg); isc_buffer_trycompact(dnsasm->current); } static inline isc_result_t isc_dnsstream_assembler_result( const isc_dnsstream_assembler_t *restrict dnsasm) { REQUIRE(dnsasm != NULL); return dnsasm->result; } static inline size_t isc_dnsstream_assembler_remaininglength( const isc_dnsstream_assembler_t *restrict dnsasm) { REQUIRE(dnsasm != NULL); return isc_buffer_remaininglength(dnsasm->current); } static inline void isc_dnsstream_assembler_clear(isc_dnsstream_assembler_t *restrict dnsasm) { REQUIRE(dnsasm != NULL); isc_buffer_clear(dnsasm->current); if (dnsasm->current != &dnsasm->dnsbuf) { isc_buffer_clear(&dnsasm->dnsbuf); } dnsasm->result = ISC_R_UNSET; }