diff --git a/README.md b/README.md index ec1135952..6641c185e 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,8 @@ Please note: LwM2M version 1.0 is only supported by clients, while servers are b - WAKAAMA_COAP_RAW_BLOCK1_REQUESTS For low memory client devices where it is not possible to keep a large post or put request in memory to be parsed (typically a firmware write). This option enable each unprocessed block 1 payload to be passed to the application, typically to be stored to a flash memory. - WAKAAMA_COAP_DEFAULT_BLOCK_SIZE CoAP block size used by CoAP layer when performing block-wise transfers. Possible values: 16, 32, 64, 128, 256, 512 and 1024. Defaults to 1024. - +- WAKAAMA_COAP_MESSAGE_EXCHANGE_LIFETIME "The time from starting to send a Confirmable message to the time when an acknowledgement is no longer expected [...]" rfc7252#section-4.8.2. + Used for message deduplication window. ### Logging diff --git a/coap/message_dedup.c b/coap/message_dedup.c new file mode 100644 index 000000000..6398e3ebd --- /dev/null +++ b/coap/message_dedup.c @@ -0,0 +1,107 @@ +/* + * CoAP message deduplication tracking. + * RFC7252 section 4.2 + */ + +#include + +#include "message_dedup.h" +#include +#include + +void coap_cleanup_message_deduplication_step(coap_msg_dedup_t **message_dedup, const time_t current_time, + time_t *timeout) { + LOG_DBG("Entering"); + coap_msg_dedup_t *message_dedup_check = *message_dedup; + coap_msg_dedup_t *message_dedup_check_prev = *message_dedup; + while (message_dedup_check != NULL) { + time_t diff = current_time - message_dedup_check->timestamp; + if (diff >= LWM2M_COAP_MESSAGE_EXCHANGE_LIFETIME) { + LOG_ARG_DBG("Message %d deduplication period ended", message_dedup_check->mid); + if (message_dedup_check_prev != *message_dedup) { + message_dedup_check_prev->next = message_dedup_check->next; + } else { + *message_dedup = message_dedup_check->next; + message_dedup_check_prev = message_dedup_check->next; + } + coap_msg_dedup_t *message_dedup_check_next = message_dedup_check->next; + lwm2m_free(message_dedup_check); + message_dedup_check = message_dedup_check_next; + } else { + LOG_ARG_DBG("Message %d check deduplication", message_dedup_check->mid); + time_t message_dedup_timeout; + if ((message_dedup_timeout = + (message_dedup_check->timestamp + LWM2M_COAP_MESSAGE_EXCHANGE_LIFETIME) - current_time) < 0) { + message_dedup_timeout = 0; + } + if (message_dedup_timeout < *timeout) { + LOG_ARG_DBG("Message %d check again in %ds deduplication", message_dedup_check->mid, + message_dedup_timeout); + *timeout = message_dedup_timeout; + } + message_dedup_check_prev = message_dedup_check; + message_dedup_check = message_dedup_check->next; + } + } +} + +bool coap_check_message_duplication(coap_msg_dedup_t **message_dedup, const uint16_t mid, const void *session, + uint8_t *dedup_coap_error_code) { + LOG_DBG("Entering"); + coap_msg_dedup_t *message_dedup_check = *message_dedup; + while (message_dedup_check != NULL) { + bool is_equal = lwm2m_session_is_equal(message_dedup_check->session, (void *)session, NULL); + if (message_dedup_check->mid == mid && is_equal) { + LOG_ARG_DBG("Duplicate, ignore mid %d (session: %p)", mid, session); + *dedup_coap_error_code = message_dedup_check->coap_response_code; + return true; + } + message_dedup_check = message_dedup_check->next; + } + LOG_ARG_DBG("Register mid %d (session: %p) for deduplication check", mid, session); + /* The message was not received in the past. Remember for future checks. */ + coap_msg_dedup_t *new_message; + new_message = lwm2m_malloc(sizeof(coap_msg_dedup_t)); + if (new_message == NULL) { + /* Memory allocation failed, mark packet as duplicate. Further allocations during packet processing would fail + * anyway. */ + return true; + } + memset(new_message, 0, sizeof(coap_msg_dedup_t)); + new_message->mid = mid; + new_message->session = (void *)session; + new_message->timestamp = lwm2m_gettime(); + + /* Add message id to deduplication list */ + coap_msg_dedup_t *message_dedup_temp = *message_dedup; + *message_dedup = new_message; + (*message_dedup)->next = message_dedup_temp; + + return false; +} + +bool coap_deduplication_set_response_code(coap_msg_dedup_t **message_dedup, const uint16_t mid, const void *session, + const uint8_t coap_response_code) { + LOG_DBG("Entering"); + coap_msg_dedup_t *message_dedup_check = *message_dedup; + while (message_dedup_check != NULL) { + bool is_equal = lwm2m_session_is_equal(message_dedup_check->session, (void *)session, NULL); + if (message_dedup_check->mid == mid && is_equal) { + LOG_ARG_DBG("Set response code %" PRIu8 " to message mid %" PRIu16, coap_response_code, mid); + message_dedup_check->coap_response_code = coap_response_code; + return true; + } + message_dedup_check = message_dedup_check->next; + } + return false; +} + +void coap_deduplication_free(lwm2m_context_t *ctx) { + LOG_DBG("Remove and free the whole message deduplication list"); + while (ctx->message_dedup != NULL) { + coap_msg_dedup_t *msg_dedup; + msg_dedup = ctx->message_dedup; + ctx->message_dedup = ctx->message_dedup->next; + lwm2m_free(msg_dedup); + } +} diff --git a/coap/message_dedup.h b/coap/message_dedup.h new file mode 100644 index 000000000..d0f5b10d3 --- /dev/null +++ b/coap/message_dedup.h @@ -0,0 +1,54 @@ +#ifndef _COAP_MESSAGE_DEDUP_H_ +#define _COAP_MESSAGE_DEDUP_H_ + +#include +#include + +#include + +typedef struct _coap_msg_dedup_ { + struct _coap_msg_dedup_ *next; + uint16_t mid; + void *session; + uint8_t coap_response_code; + time_t timestamp; +} coap_msg_dedup_t; + +/** + * Cleanup message ids after EXCHANGE_LIFETIME. + * @param message_dedup list of message ids for deduplication + * @param current_time current timestamp + * @param timeout next timeout in main loop + */ +void coap_cleanup_message_deduplication_step(coap_msg_dedup_t **message_dedup, time_t current_time, time_t *timeout); + +/** + * Check whether a message was already received. Add new messages to the deduplication tracking list. + * @param message_dedup list of message ids for deduplication + * @param mid message id + * @param session pointer to the session the message was received from + * @param coap_response_code CoAP response code to be used for answering duplicate messages + * @return true if the message was already seen within in the EXCHANGE_LIFETIME window, false otherwise. + */ +bool coap_check_message_duplication(coap_msg_dedup_t **message_dedup, uint16_t mid, const void *session, + uint8_t *coap_response_code); + +/** + * Set response code to be used in acks to a duplicate message. Acknowledgements to duplicate messages must have the + * same CoAP return code as the relies to the first received message. + * @param message_dedup list of message ids for deduplication + * @param mid message id + * @param session pointer to the session the message was received from + * @param coap_response_code CoAP response code to be used for answering duplicate messages + * @return false if no matching message was found, this is an internal error and should not happen + */ +bool coap_deduplication_set_response_code(coap_msg_dedup_t **message_dedup, uint16_t mid, const void *session, + uint8_t coap_response_code); + +/** + * Remove and free the whole message deduplication list + * @param ctx lwm2m context + */ +void coap_deduplication_free(lwm2m_context_t *ctx); + +#endif // _COAP_MESSAGE_DEDUP_H_ diff --git a/core/liblwm2m.c b/core/liblwm2m.c index 2bff2833e..b98197395 100644 --- a/core/liblwm2m.c +++ b/core/liblwm2m.c @@ -51,6 +51,7 @@ */ #include "internals.h" +#include "message_dedup.h" #include #include @@ -223,6 +224,7 @@ void lwm2m_close(lwm2m_context_t * contextP) #endif prv_deleteTransactionList(contextP); + coap_deduplication_free(contextP); lwm2m_free(contextP); } @@ -493,6 +495,7 @@ int lwm2m_step(lwm2m_context_t * contextP, registration_step(contextP, tv_sec, timeoutP); transaction_step(contextP, tv_sec, timeoutP); + coap_cleanup_message_deduplication_step(&contextP->message_dedup, tv_sec, timeoutP); LOG_ARG_DBG("Final timeoutP: %d", (int)*timeoutP); #ifdef LWM2M_CLIENT_MODE diff --git a/core/packet.c b/core/packet.c index 769d72cb4..320922bd9 100644 --- a/core/packet.c +++ b/core/packet.c @@ -87,6 +87,7 @@ Contains code snippets which are: #include "internals.h" +#include "message_dedup.h" #include #include @@ -493,6 +494,26 @@ void lwm2m_handle_packet(lwm2m_context_t *contextP, uint8_t *buffer, size_t leng message->type, message->token_len, message->code >> 5, message->code & 0x1F, message->mid, message->content_type); LOG_ARG_DBG("Payload: %.*s", (int)message->payload_len, STR_NULL2EMPTY(message->payload)); + + uint8_t dedup_coap_error_code = NO_ERROR; + if (coap_check_message_duplication(&contextP->message_dedup, message->mid, fromSessionH, + &dedup_coap_error_code)) { + if (dedup_coap_error_code == NO_ERROR) { + /* Internal error occurred, return silently. */ + return; + } + LOG_WARN("Message %d already seen in transmission window", message->mid); + coap_init_message(response, COAP_TYPE_ACK, dedup_coap_error_code, message->mid); + if (message->token_len) { + coap_set_header_token(response, message->token, message->token_len); + } + coap_error_code = message_send(contextP, response, fromSessionH); + if (coap_error_code != NO_ERROR) { + LOG_ERR("Warning: Message already seen in transmission window"); + } + return; + } + if (message->code >= COAP_GET && message->code <= COAP_DELETE) { uint32_t block_num = 0; @@ -639,7 +660,10 @@ void lwm2m_handle_packet(lwm2m_context_t *contextP, uint8_t *buffer, size_t leng lwm2m_get_coap_block_size()); coap_set_payload(response, response->payload, lwm2m_get_coap_block_size()); } - + if (!coap_deduplication_set_response_code(&contextP->message_dedup, response->mid, fromSessionH, + response->code)) { + LOG_ARG_ERR("Message %" PRIu16 " duplication not tracked", response->mid); + } coap_error_code = message_send(contextP, response, fromSessionH); lwm2m_free(payload); @@ -664,6 +688,10 @@ void lwm2m_handle_packet(lwm2m_context_t *contextP, uint8_t *buffer, size_t leng } if (1 == coap_set_status_code(response, coap_error_code)) { + if (!coap_deduplication_set_response_code(&contextP->message_dedup, response->mid, fromSessionH, + response->code)) { + LOG_ARG_ERR("Message %" PRIu16 " duplication not tracked", response->mid); + } coap_error_code = message_send(contextP, response, fromSessionH); } } @@ -714,6 +742,10 @@ void lwm2m_handle_packet(lwm2m_context_t *contextP, uint8_t *buffer, size_t leng if (message->payload_len > lwm2m_get_coap_block_size()) { coap_set_status_code(response, COAP_413_ENTITY_TOO_LARGE); } + if (!coap_deduplication_set_response_code(&contextP->message_dedup, response->mid, fromSessionH, + response->code)) { + LOG_ARG_ERR("Message %" PRIu16 " duplication not tracked", response->mid); + } coap_error_code = message_send(contextP, response, fromSessionH); } } @@ -855,6 +887,10 @@ void lwm2m_handle_packet(lwm2m_context_t *contextP, uint8_t *buffer, size_t leng /* Reuse input buffer for error message. */ coap_init_message(message, COAP_TYPE_ACK, coap_error_code, message->mid); coap_set_payload(message, coap_error_message, strlen(coap_error_message)); + if (!coap_deduplication_set_response_code(&contextP->message_dedup, response->mid, fromSessionH, + response->code)) { + LOG_ARG_ERR("Message %" PRIu16 " duplication not tracked", response->mid); + } message_send(contextP, message, fromSessionH); } } @@ -887,4 +923,3 @@ uint8_t message_send(lwm2m_context_t * contextP, return result; } - diff --git a/include/liblwm2m.h b/include/liblwm2m.h index 6e44880b7..eba997af2 100644 --- a/include/liblwm2m.h +++ b/include/liblwm2m.h @@ -817,6 +817,8 @@ typedef int (*lwm2m_bootstrap_callback_t)(lwm2m_context_t *contextP, void *sessi void *userData); #endif +typedef struct _coap_msg_dedup_ coap_msg_dedup_t; + struct _lwm2m_context_ { #ifdef LWM2M_CLIENT_MODE @@ -842,6 +844,7 @@ struct _lwm2m_context_ #endif uint16_t nextMID; lwm2m_transaction_t * transactionList; + coap_msg_dedup_t *message_dedup; void * userData; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1a64830e8..3a14abfec 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,7 @@ set(TEST_SOURCES coap_block1tests.c coap_block2tests.c coap_parse_message.c + coap_message_deduplication.c data_cbor_tests.c core_convert_numbers_test.c core_list_tests.c diff --git a/tests/coap_message_deduplication.c b/tests/coap_message_deduplication.c new file mode 100644 index 000000000..6538ec4b3 --- /dev/null +++ b/tests/coap_message_deduplication.c @@ -0,0 +1,109 @@ +/******************************************************************************* + * + * Copyright (c) 2024 GARDENA GmbH + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * The Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Marc Lasch, GARDENA GmbH - Please refer to git log + * + *******************************************************************************/ + +#include + +#include "CUnit/CUnit.h" +#include "tests.h" + +#include +#include +#include + +/** + * Test insertion of duplication tracking into the dedup message list + */ +static void test_message_deduplication_tracking_entry(void) { + int session = 0xcaffee; + uint8_t coap_response_code = NO_ERROR; + lwm2m_context_t context; + context.message_dedup = NULL; + + coap_check_message_duplication(&context.message_dedup, 1, &session, &coap_response_code); + CU_ASSERT_PTR_NOT_NULL(context.message_dedup); + CU_ASSERT_PTR_NULL(context.message_dedup->next); + CU_ASSERT_EQUAL(context.message_dedup->mid, 1); + CU_ASSERT_EQUAL(*(int *)context.message_dedup->session, 0xcaffee); + CU_ASSERT_EQUAL(context.message_dedup->coap_response_code, NO_ERROR); + + coap_deduplication_free(&context); + CU_ASSERT_PTR_NULL(context.message_dedup); +} + +/** + * Test setting the response code for replies to duplicate messages + */ +static void test_message_deduplication_set_response_code(void) { + int session = 0xcaffee; + uint8_t coap_response_code = NO_ERROR; + uint16_t mid = 123; + lwm2m_context_t context; + context.message_dedup = NULL; + + coap_check_message_duplication(&context.message_dedup, mid, &session, &coap_response_code); + + coap_deduplication_set_response_code(&context.message_dedup, mid, &session, COAP_205_CONTENT); + + CU_ASSERT_EQUAL(context.message_dedup->coap_response_code, COAP_205_CONTENT); + + coap_deduplication_free(&context); + CU_ASSERT_PTR_NULL(context.message_dedup); +} + +/** + * Test duplication step + * + * TODO: Extend test once there is an infrastructure to manipulate time in the tests. + */ +static void test_message_deduplication_step(void) { + int session = 0xcaffee; + uint8_t coap_response_code = NO_ERROR; + lwm2m_context_t context; + context.message_dedup = NULL; + + coap_check_message_duplication(&context.message_dedup, 1, &session, &coap_response_code); + + time_t timeoutP = 10; + coap_cleanup_message_deduplication_step(&context.message_dedup, lwm2m_gettime(), &timeoutP); + CU_ASSERT_PTR_NOT_NULL(context.message_dedup); + CU_ASSERT_PTR_NULL(context.message_dedup->next); + CU_ASSERT_EQUAL(context.message_dedup->mid, 1); + CU_ASSERT_EQUAL(*(int *)context.message_dedup->session, 0xcaffee); + CU_ASSERT_EQUAL(context.message_dedup->coap_response_code, NO_ERROR); + + coap_deduplication_free(&context); + CU_ASSERT_PTR_NULL(context.message_dedup); +} + +static struct TestTable table[] = { + {"test_message_deduplication_tracking_entry", test_message_deduplication_tracking_entry}, + {"test_message_deduplication_set_response_code", test_message_deduplication_set_response_code}, + {"test_message_deduplication_step", test_message_deduplication_step}, + {NULL, NULL}, +}; + +CU_ErrorCode create_message_deduplication_suit(void) { + CU_pSuite pSuite = NULL; + + pSuite = CU_add_suite("Suite_list", NULL, NULL); + if (NULL == pSuite) { + return CU_get_error(); + } + + return add_tests(pSuite, table); +} diff --git a/tests/tests.h b/tests/tests.h index 80ea2a099..55d447f8f 100644 --- a/tests/tests.h +++ b/tests/tests.h @@ -45,6 +45,7 @@ CU_ErrorCode create_cbor_suit(void); CU_ErrorCode create_senml_cbor_suit(void); #endif CU_ErrorCode create_er_coap_parse_message_suit(void); +CU_ErrorCode create_message_deduplication_suit(void); CU_ErrorCode create_list_test_suit(void); #if LWM2M_LOG_LEVEL != LWM2M_LOG_DISABLED CU_ErrorCode create_logging_test_suit(void); diff --git a/tests/unittests.c b/tests/unittests.c index 9ad880473..35783140d 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -91,6 +91,9 @@ int main(void) { if (CUE_SUCCESS != create_er_coap_parse_message_suit()) goto exit; + if (CUE_SUCCESS != create_message_deduplication_suit()) + goto exit; + if (CUE_SUCCESS != create_list_test_suit()) goto exit; #ifdef LWM2M_SERVER_MODE diff --git a/wakaama.cmake b/wakaama.cmake index 6a90d8086..15a9ecfc2 100644 --- a/wakaama.cmake +++ b/wakaama.cmake @@ -31,6 +31,17 @@ set_property( 1024 ) +# EXCHANGE_LIFETIME https://datatracker.ietf.org/doc/html/rfc7252#section-4.8.2 "The time from starting to send a +# Confirmable message to the time when an acknowledgement is no longer expected." Also used as the time to block message +# ids from a client to prevent receiving duplicate packets. Defaults to 247s with default parameters according to +# rfc7252. +set(WAKAAMA_COAP_MESSAGE_EXCHANGE_LIFETIME + 247 + CACHE + STRING + "The time from starting to send a Confirmable message to the time when an acknowledgement is no longer expected" +) + # Logging set(WAKAAMA_LOG_LEVEL LOG_DISABLED @@ -89,6 +100,9 @@ function(set_coap_defines) endif() target_compile_definitions(${target} PUBLIC LWM2M_COAP_DEFAULT_BLOCK_SIZE=${WAKAAMA_COAP_DEFAULT_BLOCK_SIZE}) + target_compile_definitions( + ${target} PUBLIC LWM2M_COAP_MESSAGE_EXCHANGE_LIFETIME=${WAKAAMA_COAP_MESSAGE_EXCHANGE_LIFETIME} + ) endfunction() # Set the defines for logging configuration @@ -135,7 +149,7 @@ function(target_sources_coap target) target_sources( ${target} PRIVATE ${WAKAAMA_TOP_LEVEL_DIRECTORY}/coap/block.c ${WAKAAMA_TOP_LEVEL_DIRECTORY}/coap/er-coap-13/er-coap-13.c - ${WAKAAMA_TOP_LEVEL_DIRECTORY}/coap/transaction.c + ${WAKAAMA_TOP_LEVEL_DIRECTORY}/coap/message_dedup.c ${WAKAAMA_TOP_LEVEL_DIRECTORY}/coap/transaction.c ) # We should not (have to) do this! target_include_directories(${target} PRIVATE ${WAKAAMA_TOP_LEVEL_DIRECTORY}/coap)