diff --git a/.github/workflows/simulation-1.1.yml b/.github/workflows/simulation-1.1.yml index 0cff6d00b..5889015ec 100644 --- a/.github/workflows/simulation-1.1.yml +++ b/.github/workflows/simulation-1.1.yml @@ -62,6 +62,7 @@ jobs: export ASAN_SYMBOLIZER_PATH=`which llvm-symbolizer` export ASAN_OPTIONS=symbolize=1 export DISTCHECK_CONFIGURE_FLAGS= CPPFLAGS=-DOPENTHREAD_SIMULATION_VIRTUAL_TIME=1 + export DISTCHECK_BUILD=1 ./bootstrap VERBOSE=1 make -f examples/Makefile-simulation distcheck diff --git a/Android.mk b/Android.mk index baf2ea22c..af425f36f 100644 --- a/Android.mk +++ b/Android.mk @@ -178,6 +178,7 @@ LOCAL_SRC_FILES := \ src/core/api/entropy_api.cpp \ src/core/api/error_api.cpp \ src/core/api/heap_api.cpp \ + src/core/api/history_tracker_api.cpp \ src/core/api/icmp6_api.cpp \ src/core/api/instance_api.cpp \ src/core/api/ip6_api.cpp \ @@ -342,6 +343,7 @@ LOCAL_SRC_FILES := \ src/core/utils/child_supervision.cpp \ src/core/utils/flash.cpp \ src/core/utils/heap.cpp \ + src/core/utils/history_tracker.cpp \ src/core/utils/jam_detector.cpp \ src/core/utils/lookup_table.cpp \ src/core/utils/otns.cpp \ @@ -498,6 +500,7 @@ LOCAL_SRC_FILES := \ src/cli/cli_coap_secure.cpp \ src/cli/cli_commissioner.cpp \ src/cli/cli_dataset.cpp \ + src/cli/cli_history.cpp \ src/cli/cli_joiner.cpp \ src/cli/cli_network_data.cpp \ src/cli/cli_srp_client.cpp \ diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index d9dcb0843..1d6230a17 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -211,6 +211,11 @@ if(OT_EXTERNAL_HEAP) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1") endif() +option(OT_HISTORY_TRACKER "enable history tracker support") +if(OT_HISTORY_TRACKER) + target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1") +endif() + option(OT_IP6_FRAGM "enable ipv6 fragmentation support") if(OT_IP6_FRAGM) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1") diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation index 39e61f151..8bdcb3fe3 100644 --- a/examples/Makefile-simulation +++ b/examples/Makefile-simulation @@ -52,6 +52,7 @@ DIAGNOSTIC ?= 1 DNS_CLIENT ?= 1 DNSSD_SERVER ?= 1 ECDSA ?= 1 +HISTORY_TRACKER ?= 1 IP6_FRAGM ?= 1 JAM_DETECTION ?= 1 JOINER ?= 1 diff --git a/examples/README.md b/examples/README.md index ea9b0c309..85db8188c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -37,6 +37,7 @@ This page lists the available common switches with description. Unless stated ot | ECDSA | OT_ECDSA | Enables support for Elliptic Curve Digital Signature Algorithm. Enable this switch if ECDSA digital signature is used by application. | | EXTERNAL_HEAP | OT_EXTERNAL_HEAP | Enables support for external heap. Enable this switch if the platform uses its own heap. Make sure to specify the external heap Calloc and Free functions to be used by the OpenThread stack. | | FULL_LOGS | OT_FULL_LOGS | Enables all log levels and regions. This switch sets the log level to OT_LOG_LEVEL_DEBG and turns on all region flags. See [Logging guide](https://openthread.io/guides/build/logs) to learn more. | +| HISTORY_TRACKER | OT_HISTORY_TRACKER | Enables support for History Tracker. | | IP6_FRAGM | OT_IP6_FRAGM | Enables support for IPv6 fragmentation. | | JAM_DETECTION | OT_JAM_DETECTION | Enables support for [Jam Detection](https://openthread.io/guides/build/features/jam-detection). Enable this switch if a device requires the ability to detect signal jamming on a specific channel. | | JOINER | OT_JOINER | Enables [support for Joiner](https://openthread.io/reference/group/api-joiner). Enable this switch on a device that has to be commissioned to join the network. | diff --git a/examples/common-switches.mk b/examples/common-switches.mk index 2040530c1..a3ae15b47 100644 --- a/examples/common-switches.mk +++ b/examples/common-switches.mk @@ -55,6 +55,7 @@ DUA ?= 0 DYNAMIC_LOG_LEVEL ?= 0 ECDSA ?= 0 EXTERNAL_HEAP ?= 0 +HISTORY_TRACKER ?= 0 IP6_FRAGM ?= 0 JAM_DETECTION ?= 0 JOINER ?= 0 @@ -211,6 +212,10 @@ ifeq ($(EXTERNAL_HEAP),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1 endif +ifeq ($(HISTORY_TRACKER),1) +COMMONCFLAGS += -DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1 +endif + ifeq ($(IP6_FRAGM),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1 endif diff --git a/include/Makefile.am b/include/Makefile.am index 9cf92de93..6feb4469e 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -58,6 +58,7 @@ openthread_headers = \ openthread/entropy.h \ openthread/error.h \ openthread/heap.h \ + openthread/history_tracker.h \ openthread/icmp6.h \ openthread/instance.h \ openthread/ip6.h \ diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn index 1bc5d55d0..6a365fd9c 100644 --- a/include/openthread/BUILD.gn +++ b/include/openthread/BUILD.gn @@ -79,6 +79,7 @@ source_set("openthread") { "entropy.h", "error.h", "heap.h", + "history_tracker.h", "icmp6.h", "instance.h", "ip6.h", diff --git a/include/openthread/history_tracker.h b/include/openthread/history_tracker.h new file mode 100644 index 000000000..a9d1608ec --- /dev/null +++ b/include/openthread/history_tracker.h @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + */ + +#ifndef OPENTHREAD_HISTORY_TRACKER_H_ +#define OPENTHREAD_HISTORY_TRACKER_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief + * This header defines the public API for History Tracker. + * + * History Tracker module records history of different events (e.g. RX and TX messages or network info changes, etc.) + * as the Thread network operates. All tracked entries are timestamped. + * + * The functions in this module are available when `OPENTHREAD_CONFIG_HISTOR_TRACKER_ENABLE` is enabled. + * + */ + +/** + * This constant specifies the maximum age of entries which is 49 days (in msec). + * + * Entries older than the max age will give this value as their age. + * + */ +#define OT_HISTORY_TRACKER_MAX_AGE (49 * 24 * 60 * 60 * 1000u) + +/** + * This type represents an iterator to iterate through a history list. + * + * The fields in this type are opaque (intended for use by OpenThread core) and therefore should not be accessed/used + * by caller. + * + * Before using an iterator, it MUST be initialized using `otHistoryTrackerInitIterator()`, + * + */ +typedef struct otHistoryTrackerIterator +{ + uint32_t mData32; + uint16_t mData16; +} otHistoryTrackerIterator; + +/** + * This structure represents Thread network info. + * + */ +typedef struct otHistoryTrackerNetworkInfo +{ + otDeviceRole mRole; ///< Device Role. + otLinkModeConfig mMode; ///< Device Mode. + uint16_t mRloc16; ///< Device RLOC16. + uint32_t mPartitionId; ///< Partition ID (valid when attached). +} otHistoryTrackerNetworkInfo; + +/** + * Constants representing message priority used in `otHistoryTrackerMessageInfo` struct. + * + */ +enum +{ + OT_HISTORY_TRACKER_MSG_PRIORITY_LOW = OT_MESSAGE_PRIORITY_LOW, ///< Low priority level. + OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL = OT_MESSAGE_PRIORITY_NORMAL, ///< Normal priority level. + OT_HISTORY_TRACKER_MSG_PRIORITY_HIGH = OT_MESSAGE_PRIORITY_HIGH, ///< High priority level. + OT_HISTORY_TRACKER_MSG_PRIORITY_NET = OT_MESSAGE_PRIORITY_HIGH + 1, ///< Network Control priority level. +}; + +/** + * This structure represents a RX/TX IPv6 message info. + * + * Some of the fields in this struct are applicable to a RX message or a TX message only, e.g., `mAveRxRss` is the + * average RSS of all fragment frames that form a received message and is only applicable for a RX message. + * + */ +typedef struct otHistoryTrackerMessageInfo +{ + uint16_t mPayloadLength; ///< IPv6 payload length (exclude IP6 header itself). + uint16_t mNeighborRloc16; ///< RLOC16 of neighbor which sent/received the msg (`0xfffe` if no RLOC16). + otSockAddr mSource; ///< Source IPv6 address and port (if UDP/TCP) + otSockAddr mDestination; ///< Destination IPv6 address and port (if UDP/TCP). + uint16_t mChecksum; ///< Message checksum (valid only for UDP/TCP/ICMP6). + uint8_t mIpProto; ///< IP Protocol number (`OT_IP6_PROTO_*` enumeration). + uint8_t mIcmp6Type; ///< ICMP6 type if msg is ICMP6, zero otherwise (`OT_ICMP6_TYPE_*` enumeration). + int8_t mAveRxRss; ///< RSS of received message or OT_RADIO_INVALI_RSSI if not known. + bool mLinkSecurity : 1; ///< Indicates whether msg used link security. + bool mTxSuccess : 1; ///< Indicates TX success (e.g., ack received). Applicable for TX msg only. + uint8_t mPriority : 2; ///< Message priority (`OT_HISTORY_TRACKER_MSG_PRIORITY_*` enumeration). + bool mRadioIeee802154 : 1; ///< Indicates whether msg was sent/received over a 15.4 radio link. + bool mRadioTrelUdp6 : 1; ///< Indicates whether msg was sent/received over a TREL radio link. +} otHistoryTrackerMessageInfo; + +/** + * This function initializes an `otHistoryTrackerIterator`. + * + * An iterator MUST be initialized before it is used. + * + * An iterator can be initialized again to start from the beginning of the list. + * + * When iterating over entries in a list, to ensure the entry ages are consistent, the age is given relative to the + * time the iterator was initialized, i.e., the entry age is provided as the duration (in milliseconds) from the event + * (when entry was recorded) to the iterator initialization time. + * + * @param[in] aIterator A pointer to the iterator to initialize (MUST NOT be NULL). + * + */ +void otHistoryTrackerInitIterator(otHistoryTrackerIterator *aIterator); + +/** + * This function iterates over the entries in the network info history list. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[inout] aIterator A pointer to an iterator. MUST be initialized or the behavior is undefined. + * @param[out] aEntryAge A pointer to a variable to output the entry's age. MUST NOT be NULL. + * Age is provided as the duration (in milliseconds) from when entry was recorded to + * @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries + * older than max age. + * + * @returns A pointer to `otHistoryTrackerNetworkInfo` entry or `NULL` if no more entries in the list. + * + */ +const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance * aInstance, + otHistoryTrackerIterator *aIterator, + uint32_t * aEntryAge); + +/** + * This function iterates over the entries in the RX message history list. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[inout] aIterator A pointer to an iterator. MUST be initialized or the behavior is undefined. + * @param[out] aEntryAge A pointer to a variable to output the entry's age. MUST NOT be NULL. + * Age is provided as the duration (in milliseconds) from when entry was recorded to + * @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries + * older than max age. + * + * @returns The `otHistoryTrackerMessageInfo` entry or `NULL` if no more entries in the list. + * + */ +const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance * aInstance, + otHistoryTrackerIterator *aIterator, + uint32_t * aEntryAge); + +/** + * This function iterates over the entries in the TX message history list. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[inout] aIterator A pointer to an iterator. MUST be initialized or the behavior is undefined. + * @param[out] aEntryAge A pointer to a variable to output the entry's age. MUST NOT be NULL. + * Age is provided as the duration (in milliseconds) from when entry was recorded to + * @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries + * older than max age. + * + * @returns The `otHistoryTrackerMessageInfo` entry or `NULL` if no more entries in the list. + * + */ +const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance * aInstance, + otHistoryTrackerIterator *aIterator, + uint32_t * aEntryAge); + +#define OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE 21 ///< Recommended size for string representation of an entry age. + +/** + * This function converts a given entry age to a human-readable string. + * + * The entry age string follows the format "::." for hours, minutes, seconds and millisecond (if + * shorter than one day) or "
days ::." (if longer than one day). + * + * If the resulting string does not fit in @p aBuffer (within its @p aSize characters), the string will be truncated + * but the outputted string is always null-terminated. + * + * @param[in] aEntryAge The entry age (duration in msec). + * @param[out] aBuffer A pointer to a char array to output the string (MUST NOT be NULL). + * @param[in] aSize The size of @p aBuffer (in bytes). Recommended to use `OT_IP6_ADDRESS_STRING_SIZE`. + * + */ +void otHistoryTrackerEntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // OPENTHREAD_HISTORY_TRACKER_H_ diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 9f9ea2522..2a4d21d2e 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (150) +#define OPENTHREAD_API_VERSION (151) /** * @addtogroup api-instance diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h index 33bf301cc..cd8ad538e 100644 --- a/include/openthread/ip6.h +++ b/include/openthread/ip6.h @@ -226,6 +226,23 @@ typedef struct otMessageInfo bool mMulticastLoop : 1; ///< TRUE to allow looping back multicast, FALSE otherwise. } otMessageInfo; +/** + * Internet Protocol Numbers. + * + */ +enum +{ + OT_IP6_PROTO_HOP_OPTS = 0, ///< IPv6 Hop-by-Hop Option + OT_IP6_PROTO_TCP = 6, ///< Transmission Control Protocol + OT_IP6_PROTO_UDP = 17, ///< User Datagram + OT_IP6_PROTO_IP6 = 41, ///< IPv6 encapsulation + OT_IP6_PROTO_ROUTING = 43, ///< Routing Header for IPv6 + OT_IP6_PROTO_FRAGMENT = 44, ///< Fragment Header for IPv6 + OT_IP6_PROTO_ICMP6 = 58, ///< ICMP for IPv6 + OT_IP6_PROTO_NONE = 59, ///< No Next Header for IPv6 + OT_IP6_PROTO_DST_OPTS = 60, ///< Destination Options for IPv6 +}; + /** * This function brings up/down the IPv6 interface. * @@ -797,6 +814,16 @@ otError otIp6RegisterMulticastListeners(otInstance * */ otError otIp6SetMeshLocalIid(otInstance *aInstance, const otIp6InterfaceIdentifier *aIid); +/** + * This function converts a given IP protocol number to a human-readable string. + * + * @param[in] aIpProto An IP protocol number (`OT_IP6_PROTO_*` enumeration). + * + * @returns A string representing @p aIpProto. + * + */ +const char *otIp6ProtoToString(uint8_t aIpProto); + /** * @} * diff --git a/include/openthread/thread.h b/include/openthread/thread.h index b3968ff3f..6f87a7bce 100644 --- a/include/openthread/thread.h +++ b/include/openthread/thread.h @@ -709,6 +709,16 @@ otError otThreadGetNextNeighborInfo(otInstance *aInstance, otNeighborInfoIterato */ otDeviceRole otThreadGetDeviceRole(otInstance *aInstance); +/** + * Convert the device role to human-readable string. + * + * @param[in] aRole The device role to convert. + * + * @returns A string representing @p aRole. + * + */ +const char *otThreadDeviceRoleToString(otDeviceRole aRole); + /** * This function get the Thread Leader Data. * diff --git a/script/check-scan-build b/script/check-scan-build index 7f4d161de..ccbc52e03 100755 --- a/script/check-scan-build +++ b/script/check-scan-build @@ -50,6 +50,7 @@ do_scan_build() "-DOPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1" "-DOPENTHREAD_CONFIG_ECDSA_ENABLE=1" "-DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1" + "-DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1" "-DOPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1" "-DOPENTHREAD_CONFIG_IP6_SLAAC_ENABLE=1" "-DOPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1" diff --git a/script/check-simulation-build-autotools b/script/check-simulation-build-autotools index 0f7fbc9eb..4b17fe667 100755 --- a/script/check-simulation-build-autotools +++ b/script/check-simulation-build-autotools @@ -55,6 +55,7 @@ build_all_features() "-DOPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1" "-DOPENTHREAD_CONFIG_ECDSA_ENABLE=1" "-DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1" + "-DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1" "-DOPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1" "-DOPENTHREAD_CONFIG_IP6_SLAAC_ENABLE=1" "-DOPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1" diff --git a/script/cmake-build b/script/cmake-build index 8f7793bec..8e1401f94 100755 --- a/script/cmake-build +++ b/script/cmake-build @@ -82,6 +82,7 @@ readonly OT_POSIX_SIM_COMMON_OPTIONS=( "-DOT_DIAGNOSTIC=ON" "-DOT_DNS_CLIENT=ON" "-DOT_ECDSA=ON" + "-DOT_HISTORY_TRACKER=ON" "-DOT_IP6_FRAGM=ON" "-DOT_JAM_DETECTION=ON" "-DOT_JOINER=ON" diff --git a/script/make-pretty b/script/make-pretty index 8f4ec67c7..9c07fcf42 100755 --- a/script/make-pretty +++ b/script/make-pretty @@ -99,6 +99,7 @@ readonly OT_CLANG_TIDY_BUILD_OPTS=( '-DOT_DUA=ON' '-DOT_MLR=ON' '-DOT_ECDSA=ON' + '-DOT_HISTORY_TRACKER=ON' '-DOT_IP6_FRAGM=ON' '-DOT_JAM_DETECTION=ON' '-DOT_JOINER=ON' diff --git a/script/test b/script/test index c0f15bfe8..3f042be2a 100755 --- a/script/test +++ b/script/test @@ -59,6 +59,7 @@ build_simulation() "-DOT_DNS_CLIENT=ON" "-DOT_DNSSD_SERVER=ON" "-DOT_ECDSA=ON" + "-DOT_HISTORY_TRACKER=ON" "-DOT_MESSAGE_USE_HEAP=ON" "-DOT_NETDATA_PUBLISHER=ON" "-DOT_PING_SENDER=ON" diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn index ce2314c1c..4ead712c7 100644 --- a/src/cli/BUILD.gn +++ b/src/cli/BUILD.gn @@ -39,6 +39,8 @@ openthread_cli_sources = [ "cli_config.h", "cli_dataset.cpp", "cli_dataset.hpp", + "cli_history.cpp", + "cli_history.hpp", "cli_joiner.cpp", "cli_joiner.hpp", "cli_network_data.cpp", diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 77a01ce8b..aacef8e97 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -37,6 +37,7 @@ set(COMMON_SOURCES cli_coap_secure.cpp cli_commissioner.cpp cli_dataset.cpp + cli_history.cpp cli_joiner.cpp cli_network_data.cpp cli_srp_client.cpp diff --git a/src/cli/Makefile.am b/src/cli/Makefile.am index 3fdc14e23..7d8b03188 100644 --- a/src/cli/Makefile.am +++ b/src/cli/Makefile.am @@ -145,6 +145,7 @@ SOURCES_COMMON = \ cli_coap_secure.cpp \ cli_commissioner.cpp \ cli_dataset.cpp \ + cli_history.cpp \ cli_joiner.cpp \ cli_network_data.cpp \ cli_srp_client.cpp \ @@ -168,6 +169,7 @@ noinst_HEADERS = \ cli_commissioner.hpp \ cli_config.h \ cli_dataset.hpp \ + cli_history.hpp \ cli_joiner.hpp \ cli_network_data.hpp \ cli_srp_client.hpp \ diff --git a/src/cli/README.md b/src/cli/README.md index 56bee0ea8..de08bae70 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -52,6 +52,7 @@ Done - [factoryreset](#factoryreset) - [fake](#fake) - [fem](#fem) +- [history](README_HISTORY.md) - [ifconfig](#ifconfig) - [ipaddr](#ipaddr) - [ipmaddr](#ipmaddr) diff --git a/src/cli/README_HISTORY.md b/src/cli/README_HISTORY.md new file mode 100644 index 000000000..be503653c --- /dev/null +++ b/src/cli/README_HISTORY.md @@ -0,0 +1,340 @@ +# OpenThread CLI - History Tracker + +History Tracker module records history of different events (e.g., RX and TX IPv6 messages or network info changes, etc.) as the Thread network operates. All tracked entries are timestamped. + +All commands under `history` require `OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE` feature to be enabled. + +The number of entries recorded for each history list is configurable through a set of OpenThread config options, e.g. `OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE` specifies the number of entries in Network Info history list. The History Tracker will keep the most recent entries overwriting oldest one when the list gets full. + +## Command List + +Usage : `history [command] ...` + +- [help](#help) +- [netinfo](#netinfo) +- [rx](#rx) +- [rxtx](#rxtx) +- [tx](#tx) + +## Timestamp Format + +Recorded entries are timestamped. When the history list is printed, the timestamps are shown relative the time the command was issues (i.e., when the list was printed) indicating how long ago the entry was recorded. + +```bash +> history netinfo +| Age | Role | Mode | RLOC16 | Partition ID | ++----------------------+----------+------+--------+--------------+ +| 02:31:50.628 | leader | rdn | 0x2000 | 151029327 | +| 02:31:53.262 | detached | rdn | 0xfffe | 0 | +| 02:31:54.663 | detached | rdn | 0x2000 | 0 | +Done +``` + +For example `02:31:50.628` indicates the event was recorded "2 hours, 31 minutes, 50 seconds, and 628 milliseconds ago". Number of days is added for events that are older than 24 hours, e.g., `1 day 11:25:31.179`, or `31 days 03:00:23.931`. + +Timestamps use millisecond accuracy and are tacked up to 49 days. If the event is older than 49 days, the entry is still tracked in the list but the timestamp is shown as `more than 49 days`. + +## Command Details + +### help + +Usage: `history help` + +Print SRP client help menu. + +```bash +> history help +help +netinfo +rx +rxtx +tx +Done +> +``` + +### netinfo + +Usage `history netinfo [list] []` + +Print the Network Info history. Each Network Info provides + +- Device Role +- MLE Link Mode +- RLOC16 +- Partition ID + +Print the Network Info history as a table. + +```bash +> history netinfo +| Age | Role | Mode | RLOC16 | Partition ID | ++----------------------+----------+------+--------+--------------+ +| 00:00:10.069 | router | rdn | 0x6000 | 151029327 | +| 00:02:09.337 | child | rdn | 0x2001 | 151029327 | +| 00:02:09.338 | child | rdn | 0x2001 | 151029327 | +| 00:07:40.806 | child | - | 0x2001 | 151029327 | +| 00:07:42.297 | detached | - | 0x6000 | 0 | +| 00:07:42.968 | disabled | - | 0x6000 | 0 | +Done +``` + +Print the Network Info history as a list. + +```bash +> history netinfo list +00:00:59.467 -> role:router mode:rdn rloc16:0x6000 partition-id:151029327 +00:02:58.735 -> role:child mode:rdn rloc16:0x2001 partition-id:151029327 +00:02:58.736 -> role:child mode:rdn rloc16:0x2001 partition-id:151029327 +00:08:30.204 -> role:child mode:- rloc16:0x2001 partition-id:151029327 +00:08:31.695 -> role:detached mode:- rloc16:0x6000 partition-id:0 +00:08:32.366 -> role:disabled mode:- rloc16:0x6000 partition-id:0 +Done +``` + +Print only the latest 2 entries. + +```bash +> history netinfo 2 +| Age | Role | Mode | RLOC16 | Partition ID | ++----------------------+----------+------+--------+--------------+ +| 00:02:05.451 | router | rdn | 0x6000 | 151029327 | +| 00:04:04.719 | child | rdn | 0x2001 | 151029327 | +Done +``` + +### rx + +Usage `history rx [list] []` + +Print the IPv6 message RX history in either table or list format. Entries provide same information and follow same format as in `history rxtx` command. + +Print the IPv6 message RX history as a table: + +```bash +> history rx +| Age | Type | Len | Chksum | Sec | Prio | RSS |Dir | Neighb | Radio | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0xbd26 | no | net | -20 | RX | 0x4800 | 15.4 | +| 00:00:07.640 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | HopOpts | 44 | 0x0000 | yes | norm | -20 | RX | 0x4800 | 15.4 | +| 00:00:09.263 | src: [fdde:ad00:beef:0:0:ff:fe00:4800]:0 | +| | dst: [ff03:0:0:0:0:0:0:2]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 12 | 0x3f7d | yes | net | -20 | RX | 0x4800 | 15.4 | +| 00:00:09.302 | src: [fdde:ad00:beef:0:0:ff:fe00:4800]:61631 | +| | dst: [fdde:ad00:beef:0:0:ff:fe00:4801]:61631 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | ICMP6(EchoReqst) | 16 | 0x942c | yes | norm | -20 | RX | 0x4800 | 15.4 | +| 00:00:09.304 | src: [fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0 | +| | dst: [fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | HopOpts | 44 | 0x0000 | yes | norm | -20 | RX | 0x4800 | 15.4 | +| 00:00:09.304 | src: [fdde:ad00:beef:0:0:ff:fe00:4800]:0 | +| | dst: [ff03:0:0:0:0:0:0:2]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0x2e37 | no | net | -20 | RX | 0x4800 | 15.4 | +| 00:00:21.622 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0xe177 | no | net | -20 | RX | 0x4800 | 15.4 | +| 00:00:26.640 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 165 | 0x82ee | yes | net | -20 | RX | 0x4800 | 15.4 | +| 00:00:30.000 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | +| | dst: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 93 | 0x52df | no | net | -20 | RX | unknwn | 15.4 | +| 00:00:30.480 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | +| | dst: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0x5ccf | no | net | -20 | RX | unknwn | 15.4 | +| 00:00:30.772 | src: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | +Done + +``` + +Print the latest 5 entries of the IPv6 message RX history as a list: + +```bash +> history rx list 4 +00:00:13.368 + type:UDP len:50 cheksum:0xbd26 sec:no prio:net rss:-20 from:0x4800 radio:15.4 + src:[fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 + dst:[ff02:0:0:0:0:0:0:1]:19788 +00:00:14.991 + type:HopOpts len:44 cheksum:0x0000 sec:yes prio:norm rss:-20 from:0x4800 radio:15.4 + src:[fdde:ad00:beef:0:0:ff:fe00:4800]:0 + dst:[ff03:0:0:0:0:0:0:2]:0 +00:00:15.030 + type:UDP len:12 cheksum:0x3f7d sec:yes prio:net rss:-20 from:0x4800 radio:15.4 + src:[fdde:ad00:beef:0:0:ff:fe00:4800]:61631 + dst:[fdde:ad00:beef:0:0:ff:fe00:4801]:61631 +00:00:15.032 + type:ICMP6(EchoReqst) len:16 cheksum:0x942c sec:yes prio:norm rss:-20 from:0x4800 radio:15.4 + src:[fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0 + dst:[fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0 +Done +``` + +### rxtx + +Usage `history rxtx [list] []` + +Print the combined IPv6 message RX and TX history in either table or list format. Each entry provides: + +- IPv6 message type: UDP, TCP, ICMP6 (and its subtype), etc. +- IPv6 payload length (excludes the IPv6 header). +- Source IPv6 address and port number. +- Destination IPv6 address and port number (port number is valid for UDP/TCP, it is zero otherwise). +- Whether or not link-layer security was used. +- Message priority: low, norm, high, net (for Thread control messages). +- Message checksum (valid for UDP, TCP, or ICMP6 message) +- RSS: Received Signal Strength (in dBm) - averaged over all received fragment frames that formed the message. For TX history `NA` (not applicable) is used. +- Whether the message was sent or received (`TX` or `RX`). A failed transmission (e.g., if tx was aborted or no ack from peer for any of the message fragments) is indicated with `TX-F` in the table format or `tx-success:no` in the list format. +- Short address (RLOC16) of neighbor to/from which the message was sent/received. If the frame is broadcast, it is shown as `bcast` in table format or `0xffff` in the list format. If the short address of neighbor is not available, it is shown as `unknwn` in the table format or `0xfffe` in the list format. +- Radio link on which the message was sent/received (useful when `OPENTHREAD_CONFIG_MULTI_RADIO` is enabled). Can be `15.4`, `trel`, or `all` (if sent on all radio links). + +Print the IPv6 message RX and TX history as a table: + +```bash +> history rxtx +| Age | Type | Len | Chksum | Sec | Prio | RSS |Dir | Neighb | Radio | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | HopOpts | 44 | 0x0000 | yes | norm | -20 | RX | 0x0800 | 15.4 | +| 00:00:09.267 | src: [fdde:ad00:beef:0:0:ff:fe00:800]:0 | +| | dst: [ff03:0:0:0:0:0:0:2]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 12 | 0x6c6b | yes | net | -20 | RX | 0x0800 | 15.4 | +| 00:00:09.290 | src: [fdde:ad00:beef:0:0:ff:fe00:800]:61631 | +| | dst: [fdde:ad00:beef:0:0:ff:fe00:801]:61631 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | ICMP6(EchoReqst) | 16 | 0xc6a2 | yes | norm | -20 | RX | 0x0800 | 15.4 | +| 00:00:09.292 | src: [fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0 | +| | dst: [fdde:ad00:beef:0:af4c:3644:882a:3698]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | ICMP6(EchoReply) | 16 | 0xc5a2 | yes | norm | NA | TX | 0x0800 | 15.4 | +| 00:00:09.292 | src: [fdde:ad00:beef:0:af4c:3644:882a:3698]:0 | +| | dst: [fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0xaa0d | yes | net | NA | TX | 0x0800 | 15.4 | +| 00:00:09.294 | src: [fdde:ad00:beef:0:0:ff:fe00:801]:61631 | +| | dst: [fdde:ad00:beef:0:0:ff:fe00:800]:61631 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | HopOpts | 44 | 0x0000 | yes | norm | -20 | RX | 0x0800 | 15.4 | +| 00:00:09.296 | src: [fdde:ad00:beef:0:0:ff:fe00:800]:0 | +| | dst: [ff03:0:0:0:0:0:0:2]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0xc1d8 | no | net | -20 | RX | 0x0800 | 15.4 | +| 00:00:09.569 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0x3cb1 | no | net | -20 | RX | 0x0800 | 15.4 | +| 00:00:16.519 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0xeda0 | no | net | -20 | RX | 0x0800 | 15.4 | +| 00:00:20.599 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788 | +| | dst: [ff02:0:0:0:0:0:0:1]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 165 | 0xbdfa | yes | net | -20 | RX | 0x0800 | 15.4 | +| 00:00:21.059 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788 | +| | dst: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 64 | 0x1c11 | no | net | NA | TX | 0x0800 | 15.4 | +| 00:00:21.062 | src: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788 | +| | dst: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 93 | 0xedff | no | net | -20 | RX | unknwn | 15.4 | +| 00:00:21.474 | src: [fe80:0:0:0:54d9:5153:ffc6:df26]:19788 | +| | dst: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 44 | 0xd383 | no | net | NA | TX | bcast | 15.4 | +| 00:00:21.811 | src: [fe80:0:0:0:8893:c2cc:d983:1e1c]:19788 | +| | dst: [ff02:0:0:0:0:0:0:2]:19788 | +Done +``` + +Print the latest 5 entries of the IPv6 message RX history as a list: + +```bash +> history rxtx list 5 + +00:00:02.100 + type:UDP len:50 cheksum:0xd843 sec:no prio:net rss:-20 from:0x0800 radio:15.4 + src:[fe80:0:0:0:54d9:5153:ffc6:df26]:19788 + dst:[ff02:0:0:0:0:0:0:1]:19788 +00:00:15.331 + type:HopOpts len:44 cheksum:0x0000 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4 + src:[fdde:ad00:beef:0:0:ff:fe00:800]:0 + dst:[ff03:0:0:0:0:0:0:2]:0 +00:00:15.354 + type:UDP len:12 cheksum:0x6c6b sec:yes prio:net rss:-20 from:0x0800 radio:15.4 + src:[fdde:ad00:beef:0:0:ff:fe00:800]:61631 + dst:[fdde:ad00:beef:0:0:ff:fe00:801]:61631 +00:00:15.356 + type:ICMP6(EchoReqst) len:16 cheksum:0xc6a2 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4 + src:[fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0 + dst:[fdde:ad00:beef:0:af4c:3644:882a:3698]:0 +00:00:15.356 + type:ICMP6(EchoReply) len:16 cheksum:0xc5a2 sec:yes prio:norm tx-success:yes to:0x0800 radio:15.4 + src:[fdde:ad00:beef:0:af4c:3644:882a:3698]:0 + dst:[fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0 +``` + +### tx + +Usage `history tx [list] []` + +Print the IPv6 message TX history in either table or list format. Entries provide same information and follow same format as in `history rxtx` command. + +Print the IPv6 message TX history as a table (10 latest entries): + +```bash +> history tx +| Age | Type | Len | Chksum | Sec | Prio | RSS |Dir | Neighb | Radio | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | ICMP6(EchoReply) | 16 | 0x932c | yes | norm | NA | TX | 0x4800 | 15.4 | +| 00:00:18.798 | src: [fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0 | +| | dst: [fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 50 | 0xce87 | yes | net | NA | TX | 0x4800 | 15.4 | +| 00:00:18.800 | src: [fdde:ad00:beef:0:0:ff:fe00:4801]:61631 | +| | dst: [fdde:ad00:beef:0:0:ff:fe00:4800]:61631 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 64 | 0xf7ba | no | net | NA | TX | 0x4800 | 15.4 | +| 00:00:39.499 | src: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788 | +| | dst: [fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 | ++----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ +| | UDP | 44 | 0x26d4 | no | net | NA | TX | bcast | 15.4 | +| 00:00:40.256 | src: [fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788 | +| | dst: [ff02:0:0:0:0:0:0:2]:19788 | +Done +``` + +Print the IPv6 message TX history as a list: + +```bash +history tx list +00:00:23.957 + type:ICMP6(EchoReply) len:16 cheksum:0x932c sec:yes prio:norm tx-success:yes to:0x4800 radio:15.4 + src:[fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0 + dst:[fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0 +00:00:23.959 + type:UDP len:50 cheksum:0xce87 sec:yes prio:net tx-success:yes to:0x4800 radio:15.4 + src:[fdde:ad00:beef:0:0:ff:fe00:4801]:61631 + dst:[fdde:ad00:beef:0:0:ff:fe00:4800]:61631 +00:00:44.658 + type:UDP len:64 cheksum:0xf7ba sec:no prio:net tx-success:yes to:0x4800 radio:15.4 + src:[fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788 + dst:[fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788 +00:00:45.415 + type:UDP len:44 cheksum:0x26d4 sec:no prio:net tx-success:yes to:0xffff radio:15.4 + src:[fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788 + dst:[ff02:0:0:0:0:0:0:2]:19788 +Done +``` diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 7f6e97adb..9c8e5493c 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -128,6 +128,9 @@ Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, voi #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE , mSrpServer(*this) #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + , mHistory(*this) +#endif #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE , mOutputLength(0) , mIsLogging(false) @@ -162,6 +165,35 @@ void Interpreter::OutputBytes(const uint8_t *aBytes, uint16_t aLength) } } +const char *Interpreter::LinkModeToString(const otLinkModeConfig &aLinkMode, char (&aStringBuffer)[kLinkModeStringSize]) +{ + char *flagsPtr = &aStringBuffer[0]; + + if (aLinkMode.mRxOnWhenIdle) + { + *flagsPtr++ = 'r'; + } + + if (aLinkMode.mDeviceType) + { + *flagsPtr++ = 'd'; + } + + if (aLinkMode.mNetworkData) + { + *flagsPtr++ = 'n'; + } + + if (flagsPtr == &aStringBuffer[0]) + { + *flagsPtr++ = '-'; + } + + *flagsPtr = '\0'; + + return aStringBuffer; +} + void Interpreter::OutputEnabledDisabledStatus(bool aEnabled) { OutputLine(aEnabled ? "Enabled" : "Disabled"); @@ -202,7 +234,11 @@ void Interpreter::OutputTableHeader(uint8_t aNumColumns, const char *const aTitl } OutputLine("|"); + OutputTableSeperator(aNumColumns, aWidths); +} +void Interpreter::OutputTableSeperator(uint8_t aNumColumns, const uint8_t aWidths[]) +{ for (uint8_t index = 0; index < aNumColumns; index++) { OutputFormat("+"); @@ -324,6 +360,13 @@ otError Interpreter::ProcessHelp(Arg aArgs[]) return OT_ERROR_NONE; } +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +otError Interpreter::ProcessHistory(Arg aArgs[]) +{ + return mHistory.Process(aArgs); +} +#endif + #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE otError Interpreter::ProcessBorderAgent(Arg aArgs[]) { @@ -866,10 +909,12 @@ exit: #if OPENTHREAD_FTD otError Interpreter::ProcessChild(Arg aArgs[]) { - otError error = OT_ERROR_NONE; - otChildInfo childInfo; - uint16_t childId; - bool isTable; + otError error = OT_ERROR_NONE; + otChildInfo childInfo; + uint16_t childId; + bool isTable; + otLinkModeConfig linkMode; + char linkModeString[kLinkModeStringSize]; isTable = (aArgs[0] == "table"); @@ -936,32 +981,10 @@ otError Interpreter::ProcessChild(Arg aArgs[]) OutputFormat("Ext Addr: "); OutputExtAddress(childInfo.mExtAddress); OutputLine(""); - OutputFormat("Mode: "); - - if (!(childInfo.mRxOnWhenIdle || childInfo.mFullThreadDevice || childInfo.mFullNetworkData)) - { - OutputFormat("-"); - } - else - { - if (childInfo.mRxOnWhenIdle) - { - OutputFormat("r"); - } - - if (childInfo.mFullThreadDevice) - { - OutputFormat("d"); - } - - if (childInfo.mFullNetworkData) - { - OutputFormat("n"); - } - } - - OutputLine(""); - + linkMode.mRxOnWhenIdle = childInfo.mRxOnWhenIdle; + linkMode.mDeviceType = childInfo.mFullThreadDevice; + linkMode.mNetworkData = childInfo.mFullThreadDevice; + OutputLine("Mode: %s", LinkModeToString(linkMode, linkModeString)); OutputLine("Net Data: %d", childInfo.mNetworkDataVersion); OutputLine("Timeout: %d", childInfo.mTimeout); OutputLine("Age: %d", childInfo.mAge); @@ -2663,32 +2686,9 @@ otError Interpreter::ProcessMode(Arg aArgs[]) if (aArgs[0].IsEmpty()) { - linkMode = otThreadGetLinkMode(mInstance); - - if (!(linkMode.mRxOnWhenIdle || linkMode.mDeviceType || linkMode.mNetworkData)) - { - OutputFormat("-"); - } - else - { - if (linkMode.mRxOnWhenIdle) - { - OutputFormat("r"); - } - - if (linkMode.mDeviceType) - { - OutputFormat("d"); - } - - if (linkMode.mNetworkData) - { - OutputFormat("n"); - } - } - - OutputLine(""); + char linkModeString[kLinkModeStringSize]; + OutputLine("%s", LinkModeToString(otThreadGetLinkMode(mInstance), linkModeString)); ExitNow(); } @@ -4026,34 +4026,7 @@ otError Interpreter::ProcessState(Arg aArgs[]) if (aArgs[0].IsEmpty()) { - switch (otThreadGetDeviceRole(mInstance)) - { - case OT_DEVICE_ROLE_DISABLED: - OutputLine("disabled"); - break; - - case OT_DEVICE_ROLE_DETACHED: - OutputLine("detached"); - break; - - case OT_DEVICE_ROLE_CHILD: - OutputLine("child"); - break; - -#if OPENTHREAD_FTD - case OT_DEVICE_ROLE_ROUTER: - OutputLine("router"); - break; - - case OT_DEVICE_ROLE_LEADER: - OutputLine("leader"); - break; -#endif - - default: - OutputLine("invalid state"); - break; - } + OutputLine("%s", otThreadDeviceRoleToString(otThreadGetDeviceRole(mInstance))); } else { diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index 2eafb7fd4..f2ca60e89 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -56,6 +56,7 @@ #include "cli/cli_commissioner.hpp" #include "cli/cli_dataset.hpp" +#include "cli/cli_history.hpp" #include "cli/cli_joiner.hpp" #include "cli/cli_network_data.hpp" #include "cli/cli_srp_client.hpp" @@ -100,6 +101,7 @@ class Interpreter friend class CoapSecure; friend class Commissioner; friend class Dataset; + friend class History; friend class Joiner; friend class NetworkData; friend class SrpClient; @@ -306,6 +308,22 @@ public: */ void SetUserCommands(const otCliCommand *aCommands, uint8_t aLength, void *aContext); + static constexpr uint8_t kLinkModeStringSize = sizeof("rdn"); ///< Size of string buffer for a MLE Link Mode. + + /** + * This method converts a given MLE Link Mode to flag string. + * + * The characters 'r', 'd', and 'n' are respectively used for `mRxOnWhenIdle`, `mDeviceType` and `mNetworkData` + * flags. If all flags are `false`, then "-" is returned. + * + * @param[in] aLinkMode The MLE Link Mode to convert. + * @param[out] aStringBuffer A reference to an string array to place the string. + * + * @returns A pointer @p aStringBuffer which contains the converted string. + * + */ + static const char *LinkModeToString(const otLinkModeConfig &aLinkMode, char (&aStringBuffer)[kLinkModeStringSize]); + protected: static Interpreter *sInterpreter; @@ -401,6 +419,7 @@ private: } void OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[]); + void OutputTableSeperator(uint8_t aNumColumns, const uint8_t aWidths[]); template void OutputTableHeader(const char *const (&aTitles)[kTableNumColumns], const uint8_t (&aWidths)[kTableNumColumns]) @@ -408,6 +427,11 @@ private: OutputTableHeader(kTableNumColumns, &aTitles[0], aWidths); } + template void OutputTableSeperator(const uint8_t (&aWidths)[kTableNumColumns]) + { + OutputTableSeperator(kTableNumColumns, aWidths); + } + #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE otError ParsePingInterval(const Arg &aArg, uint32_t &aInterval); #endif @@ -419,6 +443,7 @@ private: otError ProcessUserCommands(Arg aArgs[]); otError ProcessHelp(Arg aArgs[]); + otError ProcessHistory(Arg aArgs[]); otError ProcessCcaThreshold(Arg aArgs[]); otError ProcessBufferInfo(Arg aArgs[]); otError ProcessChannel(Arg aArgs[]); @@ -804,6 +829,9 @@ private: #endif {"fem", &Interpreter::ProcessFem}, {"help", &Interpreter::ProcessHelp}, +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + {"history", &Interpreter::ProcessHistory}, +#endif {"ifconfig", &Interpreter::ProcessIfconfig}, {"ipaddr", &Interpreter::ProcessIpAddr}, {"ipmaddr", &Interpreter::ProcessIpMulticastAddr}, @@ -953,6 +981,10 @@ private: SrpServer mSrpServer; #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + History mHistory; +#endif + #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE char mOutputString[OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LOG_STRING_SIZE]; uint16_t mOutputLength; diff --git a/src/cli/cli_history.cpp b/src/cli/cli_history.cpp new file mode 100644 index 000000000..d5ede5529 --- /dev/null +++ b/src/cli/cli_history.cpp @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements CLI for the History Tracker. + */ + +#include "cli_history.hpp" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#include + +#include "cli/cli.hpp" + +namespace ot { +namespace Cli { + +constexpr History::Command History::sCommands[]; + +otError History::ProcessHelp(Arg aArgs[]) +{ + OT_UNUSED_VARIABLE(aArgs); + + for (const Command &command : sCommands) + { + mInterpreter.OutputLine(command.mName); + } + + return OT_ERROR_NONE; +} + +otError History::ParseArgs(Arg aArgs[], bool &aIsList, uint16_t &aNumEntries) const +{ + if (*aArgs == "list") + { + aArgs++; + aIsList = true; + } + else + { + aIsList = false; + } + + if (aArgs->ParseAsUint16(aNumEntries) == OT_ERROR_NONE) + { + aArgs++; + } + else + { + aNumEntries = 0; + } + + return aArgs[0].IsEmpty() ? OT_ERROR_NONE : OT_ERROR_INVALID_ARGS; +} + +otError History::ProcessNetInfo(Arg aArgs[]) +{ + otError error; + bool isList; + uint16_t numEntries; + otHistoryTrackerIterator iterator; + const otHistoryTrackerNetworkInfo *info; + uint32_t entryAge; + char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE]; + char linkModeString[Interpreter::kLinkModeStringSize]; + + SuccessOrExit(error = ParseArgs(aArgs, isList, numEntries)); + + if (!isList) + { + // | Age | Role | Mode | RLOC16 | Partition ID | + // +----------------------+----------+------+--------+--------------+ + + static const char *const kNetInfoTitles[] = {"Age", "Role", "Mode", "RLOC16", "Partition ID"}; + static const uint8_t kNetInfoColumnWidths[] = {22, 10, 6, 8, 14}; + + mInterpreter.OutputTableHeader(kNetInfoTitles, kNetInfoColumnWidths); + } + + otHistoryTrackerInitIterator(&iterator); + + for (uint16_t index = 0; (numEntries == 0) || (index < numEntries); index++) + { + info = otHistoryTrackerIterateNetInfoHistory(mInterpreter.mInstance, &iterator, &entryAge); + VerifyOrExit(info != nullptr); + + otHistoryTrackerEntryAgeToString(entryAge, ageString, sizeof(ageString)); + + mInterpreter.OutputLine( + isList ? "%s -> role:%s mode:%s rloc16:0x%04x partition-id:%u" : "| %20s | %-8s | %-4s | 0x%04x | %12u |", + ageString, otThreadDeviceRoleToString(info->mRole), + Interpreter::LinkModeToString(info->mMode, linkModeString), info->mRloc16, info->mPartitionId); + } + +exit: + return error; +} + +otError History::ProcessRx(Arg aArgs[]) +{ + return ProcessRxTxHistory(kRx, aArgs); +} + +otError History::ProcessRxTx(Arg aArgs[]) +{ + return ProcessRxTxHistory(kRxTx, aArgs); +} + +otError History::ProcessTx(Arg aArgs[]) +{ + return ProcessRxTxHistory(kTx, aArgs); +} + +const char *History::MessagePriorityToString(uint8_t aPriority) +{ + const char *str = "unkn"; + + switch (aPriority) + { + case OT_HISTORY_TRACKER_MSG_PRIORITY_LOW: + str = "low"; + break; + + case OT_HISTORY_TRACKER_MSG_PRIORITY_NORMAL: + str = "norm"; + break; + + case OT_HISTORY_TRACKER_MSG_PRIORITY_HIGH: + str = "high"; + break; + + case OT_HISTORY_TRACKER_MSG_PRIORITY_NET: + str = "net"; + break; + + default: + break; + } + + return str; +} + +const char *History::RadioTypeToString(const otHistoryTrackerMessageInfo &aInfo) +{ + const char *str = "none"; + + if (aInfo.mRadioTrelUdp6 && aInfo.mRadioIeee802154) + { + str = "all"; + } + else if (aInfo.mRadioIeee802154) + { + str = "15.4"; + } + else if (aInfo.mRadioTrelUdp6) + { + str = "trel"; + } + + return str; +} + +const char *History::MessageTypeToString(const otHistoryTrackerMessageInfo &aInfo) +{ + const char *str = otIp6ProtoToString(aInfo.mIpProto); + + if (aInfo.mIpProto == OT_IP6_PROTO_ICMP6) + { + switch (aInfo.mIcmp6Type) + { + case OT_ICMP6_TYPE_DST_UNREACH: + str = "ICMP6(Unreach)"; + break; + case OT_ICMP6_TYPE_PACKET_TO_BIG: + str = "ICMP6(TooBig)"; + break; + case OT_ICMP6_TYPE_ECHO_REQUEST: + str = "ICMP6(EchoReqst)"; + break; + case OT_ICMP6_TYPE_ECHO_REPLY: + str = "ICMP6(EchoReply)"; + break; + case OT_ICMP6_TYPE_ROUTER_SOLICIT: + str = "ICMP6(RouterSol)"; + break; + case OT_ICMP6_TYPE_ROUTER_ADVERT: + str = "ICMP6(RouterAdv)"; + break; + default: + str = "ICMP6(Other)"; + break; + } + } + + return str; +} + +otError History::ProcessRxTxHistory(RxTx aRxTx, Arg aArgs[]) +{ + otError error; + bool isList; + uint16_t numEntries; + otHistoryTrackerIterator rxIterator; + otHistoryTrackerIterator txIterator; + bool isRx = false; + const otHistoryTrackerMessageInfo *info = nullptr; + const otHistoryTrackerMessageInfo *rxInfo = nullptr; + const otHistoryTrackerMessageInfo *txInfo = nullptr; + uint32_t entryAge; + uint32_t rxEntryAge; + uint32_t txEntryAge; + + // | Age | Type | Len | Chksum | Sec | Prio | RSS |Dir | Neighb | Radio | + // +----------------------+------------------+-------+--------+-----+------+------+----+--------+-------+ + + static const char *const kTableTitles[] = {"Age", "Type", "Len", "Chksum", "Sec", + "Prio", "RSS", "Dir", "Neighb", "Radio"}; + + static const uint8_t kTableColumnWidths[] = {22, 18, 7, 8, 5, 6, 6, 4, 8, 7}; + + SuccessOrExit(error = ParseArgs(aArgs, isList, numEntries)); + + if (!isList) + { + mInterpreter.OutputTableHeader(kTableTitles, kTableColumnWidths); + } + + otHistoryTrackerInitIterator(&txIterator); + otHistoryTrackerInitIterator(&rxIterator); + + for (uint16_t index = 0; (numEntries == 0) || (index < numEntries); index++) + { + switch (aRxTx) + { + case kRx: + info = otHistoryTrackerIterateRxHistory(mInterpreter.mInstance, &rxIterator, &entryAge); + isRx = true; + break; + + case kTx: + info = otHistoryTrackerIterateTxHistory(mInterpreter.mInstance, &txIterator, &entryAge); + isRx = false; + break; + + case kRxTx: + // Iterate through both RX and TX lists and determine the entry + // with earlier age. + + if (rxInfo == nullptr) + { + rxInfo = otHistoryTrackerIterateRxHistory(mInterpreter.mInstance, &rxIterator, &rxEntryAge); + } + + if (txInfo == nullptr) + { + txInfo = otHistoryTrackerIterateTxHistory(mInterpreter.mInstance, &txIterator, &txEntryAge); + } + + if ((rxInfo != nullptr) && ((txInfo == nullptr) || (rxEntryAge <= txEntryAge))) + { + info = rxInfo; + entryAge = rxEntryAge; + isRx = true; + rxInfo = nullptr; + } + else + { + info = txInfo; + entryAge = txEntryAge; + isRx = false; + txInfo = nullptr; + } + + break; + } + + VerifyOrExit(info != nullptr); + + if (isList) + { + OutputRxTxEntryListFormat(*info, entryAge, isRx); + } + else + { + if (index != 0) + { + mInterpreter.OutputTableSeperator(kTableColumnWidths); + } + + OutputRxTxEntryTableFormat(*info, entryAge, isRx); + } + } + +exit: + return error; +} + +void History::OutputRxTxEntryListFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx) +{ + constexpr uint8_t kIndentSize = 4; + + char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE]; + char addrString[OT_IP6_SOCK_ADDR_STRING_SIZE]; + + otHistoryTrackerEntryAgeToString(aEntryAge, ageString, sizeof(ageString)); + + mInterpreter.OutputLine("%s", ageString); + mInterpreter.OutputFormat(kIndentSize, "type:%s len:%u cheksum:0x%04x sec:%s prio:%s ", MessageTypeToString(aInfo), + aInfo.mPayloadLength, aInfo.mChecksum, aInfo.mLinkSecurity ? "yes" : "no", + MessagePriorityToString(aInfo.mPriority)); + if (aIsRx) + { + mInterpreter.OutputFormat("rss:%d", aInfo.mAveRxRss); + } + else + { + mInterpreter.OutputFormat("tx-success:%s", aInfo.mTxSuccess ? "yes" : "no"); + } + + mInterpreter.OutputLine(" %s:0x%04x radio:%s", aIsRx ? "from" : "to", aInfo.mNeighborRloc16, + RadioTypeToString(aInfo)); + + otIp6SockAddrToString(&aInfo.mSource, addrString, sizeof(addrString)); + mInterpreter.OutputLine(kIndentSize, "src:%s", addrString); + + otIp6SockAddrToString(&aInfo.mDestination, addrString, sizeof(addrString)); + mInterpreter.OutputLine(kIndentSize, "dst:%s", addrString); +} + +void History::OutputRxTxEntryTableFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx) +{ + char ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE]; + char addrString[OT_IP6_SOCK_ADDR_STRING_SIZE]; + + otHistoryTrackerEntryAgeToString(aEntryAge, ageString, sizeof(ageString)); + + mInterpreter.OutputFormat("| %20s | %-16.16s | %5u | 0x%04x | %3s | %4s | ", "", MessageTypeToString(aInfo), + aInfo.mPayloadLength, aInfo.mChecksum, aInfo.mLinkSecurity ? "yes" : "no", + MessagePriorityToString(aInfo.mPriority)); + + if (aIsRx) + { + mInterpreter.OutputFormat("%4d | RX ", aInfo.mAveRxRss); + } + else + { + mInterpreter.OutputFormat(" NA |"); + mInterpreter.OutputFormat(aInfo.mTxSuccess ? " TX " : "TX-F"); + } + + if (aInfo.mNeighborRloc16 == kShortAddrBroadcast) + { + mInterpreter.OutputFormat("| bcast "); + } + else if (aInfo.mNeighborRloc16 == kShortAddrInvalid) + { + mInterpreter.OutputFormat("| unknwn "); + } + else + { + mInterpreter.OutputFormat("| 0x%04x ", aInfo.mNeighborRloc16); + } + + mInterpreter.OutputLine("| %5.5s |", RadioTypeToString(aInfo)); + + otIp6SockAddrToString(&aInfo.mSource, addrString, sizeof(addrString)); + mInterpreter.OutputLine("| %20s | src: %-70s |", ageString, addrString); + + otIp6SockAddrToString(&aInfo.mDestination, addrString, sizeof(addrString)); + mInterpreter.OutputLine("| %20s | dst: %-70s |", "", addrString); +} + +otError History::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_INVALID_COMMAND; + const Command *command; + + if (aArgs[0].IsEmpty()) + { + IgnoreError(ProcessHelp(aArgs)); + ExitNow(); + } + + command = Utils::LookupTable::Find(aArgs[0].GetCString(), sCommands); + VerifyOrExit(command != nullptr); + + error = (this->*command->mHandler)(aArgs + 1); + +exit: + return error; +} + +} // namespace Cli +} // namespace ot + +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE diff --git a/src/cli/cli_history.hpp b/src/cli/cli_history.hpp new file mode 100644 index 000000000..223a705be --- /dev/null +++ b/src/cli/cli_history.hpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file contains definitions for CLI to control History Tracker + */ + +#ifndef CLI_HISTORY_HPP_ +#define CLI_HISTORY_HPP_ + +#include "openthread-core-config.h" + +#include + +#include "cli/cli_config.h" +#include "utils/lookup_table.hpp" +#include "utils/parse_cmdline.hpp" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +namespace ot { +namespace Cli { + +class Interpreter; + +/** + * This class implements the History Tracker CLI interpreter. + * + */ +class History +{ +public: + typedef Utils::CmdLineParser::Arg Arg; + + /** + * Constructor + * + * @param[in] aInterpreter The CLI interpreter. + * + */ + explicit History(Interpreter &aInterpreter) + : mInterpreter(aInterpreter) + { + } + + /** + * This method interprets a list of CLI arguments. + * + * @param[in] aArgs A pointer an array of command line arguments. + * + */ + otError Process(Arg aArgs[]); + +private: + static constexpr uint16_t kShortAddrInvalid = 0xfffe; + static constexpr uint16_t kShortAddrBroadcast = 0xffff; + static constexpr int8_t kInvalidRss = OT_RADIO_RSSI_INVALID; + + struct Command + { + const char *mName; + otError (History::*mHandler)(Arg aArgs[]); + }; + + enum RxTx : uint8_t + { + kRx, + kTx, + kRxTx, + }; + + otError ProcessHelp(Arg aArgs[]); + otError ProcessNetInfo(Arg aArgs[]); + otError ProcessRx(Arg aArgs[]); + otError ProcessRxTx(Arg aArgs[]); + otError ProcessTx(Arg aArgs[]); + + otError ParseArgs(Arg aArgs[], bool &aIsList, uint16_t &aNumEntries) const; + otError ProcessRxTxHistory(RxTx aRxTx, Arg aArgs[]); + void OutputRxTxEntryListFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx); + void OutputRxTxEntryTableFormat(const otHistoryTrackerMessageInfo &aInfo, uint32_t aEntryAge, bool aIsRx); + + static const char *MessagePriorityToString(uint8_t aPriority); + static const char *RadioTypeToString(const otHistoryTrackerMessageInfo &aInfo); + static const char *MessageTypeToString(const otHistoryTrackerMessageInfo &aInfo); + + static constexpr Command sCommands[] = { + {"help", &History::ProcessHelp}, {"netinfo", &History::ProcessNetInfo}, {"rx", &History::ProcessRx}, + {"rxtx", &History::ProcessRxTx}, {"tx", &History::ProcessTx}, + }; + + static_assert(Utils::LookupTable::IsSorted(sCommands), "Command Table is not sorted"); + + Interpreter &mInterpreter; +}; + +} // namespace Cli +} // namespace ot + +#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#endif // CLI_HISTORY_HPP_ diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index e860d863f..30456d643 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -315,6 +315,7 @@ openthread_core_files = [ "api/entropy_api.cpp", "api/error_api.cpp", "api/heap_api.cpp", + "api/history_tracker_api.cpp", "api/icmp6_api.cpp", "api/instance_api.cpp", "api/ip6_api.cpp", @@ -638,6 +639,8 @@ openthread_core_files = [ "utils/flash.hpp", "utils/heap.cpp", "utils/heap.hpp", + "utils/history_tracker.cpp", + "utils/history_tracker.hpp", "utils/jam_detector.cpp", "utils/jam_detector.hpp", "utils/lookup_table.cpp", @@ -710,6 +713,7 @@ source_set("libopenthread_core_config") { "config/dns_client.h", "config/dnssd_server.h", "config/dtls.h", + "config/history_tracker.h", "config/ip6.h", "config/joiner.h", "config/link_quality.h", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5655f909a..b6929fd18 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -51,6 +51,7 @@ set(COMMON_SOURCES api/entropy_api.cpp api/error_api.cpp api/heap_api.cpp + api/history_tracker_api.cpp api/icmp6_api.cpp api/instance_api.cpp api/ip6_api.cpp @@ -215,6 +216,7 @@ set(COMMON_SOURCES utils/child_supervision.cpp utils/flash.cpp utils/heap.cpp + utils/history_tracker.cpp utils/jam_detector.cpp utils/lookup_table.cpp utils/otns.cpp diff --git a/src/core/Makefile.am b/src/core/Makefile.am index 2d7a3eac2..0f0d08885 100644 --- a/src/core/Makefile.am +++ b/src/core/Makefile.am @@ -128,6 +128,7 @@ SOURCES_COMMON = \ api/entropy_api.cpp \ api/error_api.cpp \ api/heap_api.cpp \ + api/history_tracker_api.cpp \ api/icmp6_api.cpp \ api/instance_api.cpp \ api/ip6_api.cpp \ @@ -292,6 +293,7 @@ SOURCES_COMMON = \ utils/child_supervision.cpp \ utils/flash.cpp \ utils/heap.cpp \ + utils/history_tracker.cpp \ utils/jam_detector.cpp \ utils/lookup_table.cpp \ utils/otns.cpp \ @@ -430,6 +432,7 @@ HEADERS_COMMON = \ config/dns_client.h \ config/dnssd_server.h \ config/dtls.h \ + config/history_tracker.h \ config/ip6.h \ config/joiner.h \ config/link_quality.h \ @@ -562,6 +565,7 @@ HEADERS_COMMON = \ utils/child_supervision.hpp \ utils/flash.hpp \ utils/heap.hpp \ + utils/history_tracker.hpp \ utils/jam_detector.hpp \ utils/lookup_table.hpp \ utils/otns.hpp \ diff --git a/src/core/api/history_tracker_api.cpp b/src/core/api/history_tracker_api.cpp new file mode 100644 index 000000000..e7a65d3d7 --- /dev/null +++ b/src/core/api/history_tracker_api.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the History Tracker public APIs. + */ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#include + +#include "common/instance.hpp" +#include "common/locator_getters.hpp" +#include "utils/history_tracker.hpp" + +using namespace ot; + +void otHistoryTrackerInitIterator(otHistoryTrackerIterator *aIterator) +{ + static_cast(aIterator)->Init(); +} + +const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance * aInstance, + otHistoryTrackerIterator *aIterator, + uint32_t * aEntryAge) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().IterateNetInfoHistory( + *static_cast(aIterator), *aEntryAge); +} + +const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance * aInstance, + otHistoryTrackerIterator *aIterator, + uint32_t * aEntryAge) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().IterateRxHistory( + *static_cast(aIterator), *aEntryAge); +} + +const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance * aInstance, + otHistoryTrackerIterator *aIterator, + uint32_t * aEntryAge) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().IterateTxHistory( + *static_cast(aIterator), *aEntryAge); +} + +void otHistoryTrackerEntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize) +{ + Utils::HistoryTracker::EntryAgeToString(aEntryAge, aBuffer, aSize); +} + +#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE diff --git a/src/core/api/ip6_api.cpp b/src/core/api/ip6_api.cpp index 5263dc3e1..59096a940 100644 --- a/src/core/api/ip6_api.cpp +++ b/src/core/api/ip6_api.cpp @@ -333,3 +333,8 @@ otError otIp6SetMeshLocalIid(otInstance *aInstance, const otIp6InterfaceIdentifi } #endif + +const char *otIp6ProtoToString(uint8_t aIpProto) +{ + return Ip6::Ip6::IpProtoToString(aIpProto); +} diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp index 1ea74fc46..0a9018489 100644 --- a/src/core/api/thread_api.cpp +++ b/src/core/api/thread_api.cpp @@ -336,6 +336,11 @@ otDeviceRole otThreadGetDeviceRole(otInstance *aInstance) return static_cast(instance.Get().GetRole()); } +const char *otThreadDeviceRoleToString(otDeviceRole aRole) +{ + return Mle::Mle::RoleToString(static_cast(aRole)); +} + otError otThreadGetLeaderData(otInstance *aInstance, otLeaderData *aLeaderData) { Instance &instance = *static_cast(aInstance); diff --git a/src/core/common/instance.cpp b/src/core/common/instance.cpp index 213749cf3..0d3842cb2 100644 --- a/src/core/common/instance.cpp +++ b/src/core/common/instance.cpp @@ -87,6 +87,9 @@ Instance::Instance(void) #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD , mChannelManager(*this) #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + , mHistoryTracker(*this) +#endif #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD , mDatasetUpdater(*this) #endif diff --git a/src/core/common/instance.hpp b/src/core/common/instance.hpp index e97d5af1f..8d056c6cd 100644 --- a/src/core/common/instance.hpp +++ b/src/core/common/instance.hpp @@ -82,6 +82,9 @@ #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE #include "utils/channel_monitor.hpp" #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +#include "utils/history_tracker.hpp" +#endif #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2) #include "backbone_router/bbr_leader.hpp" @@ -388,6 +391,10 @@ private: Utils::ChannelManager mChannelManager; #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Utils::HistoryTracker mHistoryTracker; +#endif + #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD MeshCoP::DatasetUpdater mDatasetUpdater; #endif @@ -828,6 +835,13 @@ template <> inline Utils::ChannelManager &Instance::Get(void) } #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +template <> inline Utils::HistoryTracker &Instance::Get(void) +{ + return mHistoryTracker; +} +#endif + #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD template <> inline MeshCoP::DatasetUpdater &Instance::Get(void) { diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp index f28114537..a77660ac2 100644 --- a/src/core/common/notifier.cpp +++ b/src/core/common/notifier.cpp @@ -177,6 +177,9 @@ void Notifier::EmitEvents(void) #if OPENTHREAD_CONFIG_OTNS_ENABLE Get().HandleNotifierEvents(events); #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().HandleNotifierEvents(events); +#endif #if OPENTHREAD_ENABLE_VENDOR_EXTENSION Get().HandleNotifierEvents(events); #endif diff --git a/src/core/config/history_tracker.h b/src/core/config/history_tracker.h new file mode 100644 index 000000000..963c912be --- /dev/null +++ b/src/core/config/history_tracker.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes compile-time configurations for History Tracker module. + * + */ + +#ifndef CONFIG_HISTORY_TRACKER_H_ +#define CONFIG_HISTORY_TRACKER_H_ + +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + * + * Define as 1 to enable History Tracker module. + * + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 0 +#endif + +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE + * + * Specifies the maximum number of entries in Network Info (role, mode, partition ID, RLOC16) history list. + * + * Can be set to zero to configure History Tracker module not to collect any entries. + * + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE 32 +#endif + +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE + * + * Specifies the maximum number of entries in RX history list. + * + * Can be set to zero to configure History Tracker module not to collect any RX history. + * + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE 32 +#endif + +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE + * + * Specifies the maximum number of entries in TX history list. + * + * Can be set to zero to configure History Tracker module not to collect any TX history. + * + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE 32 +#endif + +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES + * + * Define as 1 to exclude Thread Control message (e.g., MLE, TMF) from TX and RX history. + * + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES 1 +#endif + +#endif // CONFIG_HISTORY_TRACKER_H_ diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp index 63ff981f0..0789b49df 100644 --- a/src/core/net/ip6.hpp +++ b/src/core/net/ip6.hpp @@ -298,9 +298,11 @@ public: const PriorityQueue &GetSendQueue(void) const { return mSendQueue; } /** - * This static method converts an `IpProto` enumeration to a string. + * This static method converts an IP protocol number to a string. * - * @returns The string representation of an IP protocol enumeration. + * @param[in] aIpPorto An IP protocol number. + * + * @returns The string representation of @p aIpProto. * */ static const char *IpProtoToString(uint8_t aIpProto); diff --git a/src/core/net/ip6_headers.hpp b/src/core/net/ip6_headers.hpp index efe90cf60..2ddc2196b 100644 --- a/src/core/net/ip6_headers.hpp +++ b/src/core/net/ip6_headers.hpp @@ -86,15 +86,15 @@ using ot::Encoding::BigEndian::HostSwap32; */ // Internet Protocol Numbers -static constexpr uint8_t kProtoHopOpts = 0; ///< IPv6 Hop-by-Hop Option -static constexpr uint8_t kProtoTcp = 6; ///< Transmission Control Protocol -static constexpr uint8_t kProtoUdp = 17; ///< User Datagram -static constexpr uint8_t kProtoIp6 = 41; ///< IPv6 encapsulation -static constexpr uint8_t kProtoRouting = 43; ///< Routing Header for IPv6 -static constexpr uint8_t kProtoFragment = 44; ///< Fragment Header for IPv6 -static constexpr uint8_t kProtoIcmp6 = 58; ///< ICMP for IPv6 -static constexpr uint8_t kProtoNone = 59; ///< No Next Header for IPv6 -static constexpr uint8_t kProtoDstOpts = 60; ///< Destination Options for IPv6 +static constexpr uint8_t kProtoHopOpts = OT_IP6_PROTO_HOP_OPTS; ///< IPv6 Hop-by-Hop Option +static constexpr uint8_t kProtoTcp = OT_IP6_PROTO_TCP; ///< Transmission Control Protocol +static constexpr uint8_t kProtoUdp = OT_IP6_PROTO_UDP; ///< User Datagram +static constexpr uint8_t kProtoIp6 = OT_IP6_PROTO_IP6; ///< IPv6 encapsulation +static constexpr uint8_t kProtoRouting = OT_IP6_PROTO_ROUTING; ///< Routing Header for IPv6 +static constexpr uint8_t kProtoFragment = OT_IP6_PROTO_FRAGMENT; ///< Fragment Header for IPv6 +static constexpr uint8_t kProtoIcmp6 = OT_IP6_PROTO_ICMP6; ///< ICMP for IPv6 +static constexpr uint8_t kProtoNone = OT_IP6_PROTO_NONE; ///< No Next Header for IPv6 +static constexpr uint8_t kProtoDstOpts = OT_IP6_PROTO_DST_OPTS; ///< Destination Options for IPv6 /** * Class Selectors diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h index 423b35a82..0e9e71401 100644 --- a/src/core/openthread-core-config.h +++ b/src/core/openthread-core-config.h @@ -71,6 +71,7 @@ #include "config/dns_client.h" #include "config/dnssd_server.h" #include "config/dtls.h" +#include "config/history_tracker.h" #include "config/ip6.h" #include "config/joiner.h" #include "config/link_quality.h" diff --git a/src/core/thread/mesh_forwarder.cpp b/src/core/thread/mesh_forwarder.cpp index dcca5f25c..c77b05bce 100644 --- a/src/core/thread/mesh_forwarder.cpp +++ b/src/core/thread/mesh_forwarder.cpp @@ -1042,6 +1042,10 @@ void MeshForwarder::UpdateSendMessage(Error aFrameTxError, Mac::Address &aMacDes } #endif +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordTxMessage(*mSendMessage, aMacDest); +#endif + LogMessage(kMessageTransmit, *mSendMessage, &aMacDest, txError); if (mSendMessage->GetType() == Message::kTypeIp6) @@ -1441,6 +1445,10 @@ Error MeshForwarder::HandleDatagram(Message &aMessage, const ThreadLinkInfo &aLi { ThreadNetif &netif = Get(); +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordRxMessage(aMessage, aMacSource); +#endif + LogMessage(kMessageReceive, aMessage, &aMacSource, kErrorNone); if (aMessage.GetType() == Message::kTypeIp6) @@ -1620,8 +1628,6 @@ uint16_t MeshForwarder::CalcFrameVersion(const Neighbor *aNeighbor, bool aIePres // LCOV_EXCL_START -#if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_NOTE) && (OPENTHREAD_CONFIG_LOG_MAC == 1) - Error MeshForwarder::ParseIp6UdpTcpHeader(const Message &aMessage, Ip6::Header & aIp6Header, uint16_t & aChecksum, @@ -1668,6 +1674,8 @@ exit: return error; } +#if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_NOTE) && (OPENTHREAD_CONFIG_LOG_MAC == 1) + const char *MeshForwarder::MessageActionToString(MessageAction aAction, Error aError) { static const char *const kMessageActionStrings[] = { diff --git a/src/core/thread/mesh_forwarder.hpp b/src/core/thread/mesh_forwarder.hpp index 255b3bc6f..7d288afb5 100644 --- a/src/core/thread/mesh_forwarder.hpp +++ b/src/core/thread/mesh_forwarder.hpp @@ -58,6 +58,10 @@ namespace Mle { class DiscoverScanner; } +namespace Utils { +class HistoryTracker; +} + /** * @addtogroup core-mesh-forwarding * @@ -154,6 +158,7 @@ class MeshForwarder : public InstanceLocator, private NonCopyable friend class IndirectSender; friend class Mle::DiscoverScanner; friend class TimeTicker; + friend class Utils::HistoryTracker; public: /** @@ -503,15 +508,16 @@ private: const Mac::Address &aMacDest, bool aIsSecure); + static Error ParseIp6UdpTcpHeader(const Message &aMessage, + Ip6::Header & aIp6Header, + uint16_t & aChecksum, + uint16_t & aSourcePort, + uint16_t & aDestPort); + #if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_NOTE) && (OPENTHREAD_CONFIG_LOG_MAC == 1) const char *MessageActionToString(MessageAction aAction, Error aError); const char *MessagePriorityToString(const Message &aMessage); - Error ParseIp6UdpTcpHeader(const Message &aMessage, - Ip6::Header & aIp6Header, - uint16_t & aChecksum, - uint16_t & aSourcePort, - uint16_t & aDestPort); #if OPENTHREAD_FTD Error DecompressIp6UdpTcpHeader(const Message & aMessage, uint16_t aOffset, diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index 8a98c8acf..dc7bce5b4 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -748,6 +748,10 @@ Error Mle::SetDeviceMode(DeviceMode aDeviceMode) VerifyOrExit(mDeviceMode != aDeviceMode); mDeviceMode = aDeviceMode; +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordNetworkInfo(); +#endif + #if OPENTHREAD_CONFIG_OTNS_ENABLE Get().EmitDeviceMode(mDeviceMode); #endif @@ -4406,11 +4410,11 @@ const char *Mle::MessageTypeActionToSuffixString(MessageType aType, MessageActio const char *Mle::RoleToString(DeviceRole aRole) { static const char *const kRoleStrings[] = { - "Disabled", // (0) kRoleDisabled - "Detached", // (1) kRoleDetached - "Child", // (2) kRoleChild - "Router", // (3) kRoleRouter - "Leader", // (4) kRoleLeader + "disabled", // (0) kRoleDisabled + "detached", // (1) kRoleDetached + "child", // (2) kRoleChild + "router", // (3) kRoleRouter + "leader", // (4) kRoleLeader }; static_assert(kRoleDisabled == 0, "kRoleDisabled value is incorrect"); @@ -4419,7 +4423,7 @@ const char *Mle::RoleToString(DeviceRole aRole) static_assert(kRoleRouter == 3, "kRoleRouter value is incorrect"); static_assert(kRoleLeader == 4, "kRoleLeader value is incorrect"); - return kRoleStrings[aRole]; + return (aRole <= OT_ARRAY_LENGTH(kRoleStrings)) ? kRoleStrings[aRole] : "invalid"; } // LCOV_EXCL_START diff --git a/src/core/utils/history_tracker.cpp b/src/core/utils/history_tracker.cpp new file mode 100644 index 000000000..341422090 --- /dev/null +++ b/src/core/utils/history_tracker.cpp @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the History Tracker module. + */ + +#include "history_tracker.hpp" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#include "common/code_utils.hpp" +#include "common/debug.hpp" +#include "common/instance.hpp" +#include "common/locator_getters.hpp" +#include "common/string.hpp" +#include "common/timer.hpp" + +namespace ot { +namespace Utils { + +//--------------------------------------------------------------------------------------------------------------------- +// HistoryTracker + +HistoryTracker::HistoryTracker(Instance &aInstance) + : InstanceLocator(aInstance) + , mTimer(aInstance, HandleTimer) +{ + mTimer.Start(kAgeCheckPeriod); +} + +void HistoryTracker::RecordNetworkInfo(void) +{ + NetworkInfo * entry = mNetInfoHistory.AddNewEntry(); + Mle::DeviceMode mode; + + VerifyOrExit(entry != nullptr); + + entry->mRole = static_cast(Get().GetRole()); + entry->mRloc16 = Get().GetRloc16(); + entry->mPartitionId = Get().GetLeaderData().GetPartitionId(); + mode = Get().GetDeviceMode(); + mode.Get(entry->mMode); + +exit: + return; +} + +void HistoryTracker::RecordMessage(const Message &aMessage, const Mac::Address &aMacAddresss, MessageType aType) +{ + MessageInfo * entry = nullptr; + Ip6::Header ip6Header; + Ip6::Icmp::Header icmp6Header; + uint8_t ip6Proto; + uint16_t checksum; + uint16_t sourcePort; + uint16_t destPort; + + VerifyOrExit(aMessage.GetType() == Message::kTypeIp6); + + SuccessOrExit(MeshForwarder::ParseIp6UdpTcpHeader(aMessage, ip6Header, checksum, sourcePort, destPort)); + + ip6Proto = ip6Header.GetNextHeader(); + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_EXCLUDE_THREAD_CONTROL_MESSAGES + if (ip6Proto == Ip6::kProtoUdp) + { + uint16_t port = 0; + + switch (aType) + { + case kRxMessage: + port = destPort; + break; + + case kTxMessage: + port = sourcePort; + break; + } + + VerifyOrExit((port != Mle::kUdpPort) && (port != Tmf::kUdpPort)); + } +#endif + + if (ip6Proto == Ip6::kProtoIcmp6) + { + SuccessOrExit(aMessage.Read(sizeof(Ip6::Header), icmp6Header)); + checksum = icmp6Header.GetChecksum(); + } + else + { + icmp6Header.Clear(); + } + + switch (aType) + { + case kRxMessage: + entry = mRxHistory.AddNewEntry(); + break; + + case kTxMessage: + entry = mTxHistory.AddNewEntry(); + break; + } + + VerifyOrExit(entry != nullptr); + + entry->mPayloadLength = ip6Header.GetPayloadLength(); + entry->mNeighborRloc16 = aMacAddresss.IsShort() ? aMacAddresss.GetShort() : kInvalidRloc16; + entry->mSource.mAddress = ip6Header.GetSource(); + entry->mSource.mPort = sourcePort; + entry->mDestination.mAddress = ip6Header.GetDestination(); + entry->mDestination.mPort = destPort; + entry->mChecksum = checksum; + entry->mIpProto = ip6Proto; + entry->mIcmp6Type = icmp6Header.GetType(); + entry->mAveRxRss = (aType == kRxMessage) ? aMessage.GetRssAverager().GetAverage() : kInvalidRss; + entry->mLinkSecurity = aMessage.IsLinkSecurityEnabled(); + entry->mTxSuccess = (aType == kTxMessage) ? aMessage.GetTxSuccess() : true; + entry->mPriority = aMessage.GetPriority(); + + if (aMacAddresss.IsExtended()) + { + Neighbor *neighbor = Get().FindNeighbor(aMacAddresss, Neighbor::kInStateAnyExceptInvalid); + + if (neighbor != nullptr) + { + entry->mNeighborRloc16 = neighbor->GetRloc16(); + } + } + +#if OPENTHREAD_CONFIG_MULTI_RADIO + if (aMessage.IsRadioTypeSet()) + { + switch (aMessage.GetRadioType()) + { +#if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE + case Mac::kRadioTypeIeee802154: + entry->mRadioIeee802154 = true; + break; +#endif +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + case Mac::kRadioTypeTrel: + entry->mRadioTrelUdp6 = true; + break; +#endif + } + + // Radio type may not be set on a tx message indicating that it + // was sent over all radio types (e.g., for broadcast frame). + // In such a case, we set all supported radios from `else` + // block below. + } + else +#endif // OPENTHREAD_CONFIG_MULTI_RADIO + { +#if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE + entry->mRadioIeee802154 = true; +#endif + +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + entry->mRadioTrelUdp6 = true; +#endif + } + +exit: + return; +} + +void HistoryTracker::HandleNotifierEvents(Events aEvents) +{ + if (aEvents.ContainsAny(kEventThreadRoleChanged | kEventThreadRlocAdded | kEventThreadRlocRemoved | + kEventThreadPartitionIdChanged)) + { + RecordNetworkInfo(); + } +} + +void HistoryTracker::HandleTimer(Timer &aTimer) +{ + aTimer.Get().HandleTimer(); +} + +void HistoryTracker::HandleTimer(void) +{ + mNetInfoHistory.UpdateAgedEntries(); + mRxHistory.UpdateAgedEntries(); + mTxHistory.UpdateAgedEntries(); + + mTimer.Start(kAgeCheckPeriod); +} + +void HistoryTracker::EntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize) +{ + StringWriter writer(aBuffer, aSize); + + if (aEntryAge >= kMaxAge) + { + writer.Append("more than %u days", kMaxAge / kOneDayInMsec); + } + else + { + uint32_t days = aEntryAge / kOneDayInMsec; + + if (days > 0) + { + writer.Append("%u day%s ", days, (days == 1) ? "" : "s"); + aEntryAge -= days * kOneDayInMsec; + } + + writer.Append("%02u:%02u:%02u.%03u", (aEntryAge / kOneHourInMsec), + (aEntryAge % kOneDayInMsec) / kOneMinuteInMsec, (aEntryAge % kOneMinuteInMsec) / kOneSecondInMsec, + (aEntryAge % kOneSecondInMsec)); + } +} + +//--------------------------------------------------------------------------------------------------------------------- +// HistoryTracker::Timestamp + +void HistoryTracker::Timestamp::SetToNow(void) +{ + mTime = TimerMilli::GetNow(); + + // If the current time happens to be the special value which we + // use to indicate "distant past", decrement the time by one. + + if (mTime.GetValue() == kDistantPast) + { + mTime.SetValue(mTime.GetValue() - 1); + } +} + +uint32_t HistoryTracker::Timestamp::GetDurationTill(TimeMilli aTime) const +{ + return IsDistantPast() ? kMaxAge : OT_MIN(aTime - mTime, kMaxAge); +} + +//--------------------------------------------------------------------------------------------------------------------- +// HistoryTracker::List + +HistoryTracker::List::List(void) + : mStartIndex(0) + , mSize(0) +{ +} + +void HistoryTracker::List::Clear(void) +{ + mStartIndex = 0; + mSize = 0; +} + +uint16_t HistoryTracker::List::Add(uint16_t aMaxSize, Timestamp aTimestamps[]) +{ + // Add a new entry and return its list index. Overwrites the + // oldest entry if list is full. + // + // Entries are saved in the order they are added such that + // `mStartIndex` is the newest entry and the entries after up + // to `mSize` are the previously added entries. + + mStartIndex = (mStartIndex == 0) ? aMaxSize - 1 : mStartIndex - 1; + mSize += (mSize == aMaxSize) ? 0 : 1; + + aTimestamps[mStartIndex].SetToNow(); + + return mStartIndex; +} + +Error HistoryTracker::List::Iterate(uint16_t aMaxSize, + const Timestamp aTimestamps[], + Iterator & aIterator, + uint16_t & aListIndex, + uint32_t & aEntryAge) const +{ + Error error = kErrorNone; + + VerifyOrExit(aIterator.GetEntryNumber() < mSize, error = kErrorNotFound); + + aListIndex = MapEntryNumberToListIndex(aIterator.GetEntryNumber(), aMaxSize); + aEntryAge = aTimestamps[aListIndex].GetDurationTill(aIterator.GetInitTime()); + + aIterator.IncrementEntryNumber(); + +exit: + return error; +} + +uint16_t HistoryTracker::List::MapEntryNumberToListIndex(uint16_t aEntryNumber, uint16_t aMaxSize) const +{ + // Map the `aEntryNumber` to the list index. `aEntryNumber` value + // of zero corresponds to the newest (the most recently added) + // entry and value one to next one and so on. List index + // warps at the end of array to start of array. Caller MUST + // ensure `aEntryNumber` is smaller than `mSize`. + + uint32_t index; + + OT_ASSERT(aEntryNumber < mSize); + + index = static_cast(aEntryNumber) + mStartIndex; + index -= (index >= aMaxSize) ? aMaxSize : 0; + + return static_cast(index); +} + +void HistoryTracker::List::UpdateAgedEntries(uint16_t aMaxSize, Timestamp aTimestamps[]) +{ + TimeMilli now = TimerMilli::GetNow(); + + // We go through the entries in reverse (starting with the oldest + // entry) and check if the entry's age is larger than `kMaxAge` + // and if so mark it as "distant past". We can stop as soon as we + // get to an entry with age smaller than max. + // + // The `for()` loop condition is `(entryNumber < mSize)` which + // ensures that we go through the loop body for `entryNumber` + // value of zero and then in the next iteration (when the + // `entryNumber` rolls over) we stop. + + for (uint16_t entryNumber = mSize - 1; entryNumber < mSize; entryNumber--) + { + uint16_t index = MapEntryNumberToListIndex(entryNumber, aMaxSize); + + if (aTimestamps[index].GetDurationTill(now) < kMaxAge) + { + break; + } + + aTimestamps[index].MarkAsDistantPast(); + } +} + +} // namespace Utils +} // namespace ot + +#endif // #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE diff --git a/src/core/utils/history_tracker.hpp b/src/core/utils/history_tracker.hpp new file mode 100644 index 000000000..96f3d761f --- /dev/null +++ b/src/core/utils/history_tracker.hpp @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions to support History Tracker module. + */ + +#ifndef HISTORY_TRACKER_HPP_ +#define HISTORY_TRACKER_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#include +#include + +#include "common/clearable.hpp" +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "common/notifier.hpp" +#include "common/timer.hpp" +#include "net/socket.hpp" +#include "thread/mesh_forwarder.hpp" +#include "thread/mle.hpp" +#include "thread/mle_types.hpp" + +namespace ot { +namespace Utils { + +/** + * This class implements History Tracker. + * + */ +class HistoryTracker : public InstanceLocator, private NonCopyable +{ + friend class ot::MeshForwarder; + friend class ot::Notifier; + friend class ot::Mle::Mle; + +public: + /** + * This constant specifies the maximum age of entries which is 49 days (value in msec). + * + * Entries older than the max age will give this value as their age. + * + */ + static constexpr uint32_t kMaxAge = OT_HISTORY_TRACKER_MAX_AGE; + + /** + * This constant specifies the recommend string size to represent an entry age + * + */ + static constexpr uint16_t kEntryAgeStringSize = OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE; + + /** + * This type represents an iterator to iterate through a history list. + * + */ + class Iterator : public otHistoryTrackerIterator + { + friend class HistoryTracker; + + public: + /** + * This method initializes an `Iterator` + * + * An iterator MUST be initialized before it is used. An iterator can be initialized again to start from + * the beginning of the list. + * + */ + void Init(void) { ResetEntryNumber(), SetInitTime(); } + + private: + uint16_t GetEntryNumber(void) const { return mData16; } + void ResetEntryNumber(void) { mData16 = 0; } + void IncrementEntryNumber(void) { mData16++; } + TimeMilli GetInitTime(void) const { return TimeMilli(mData32); } + void SetInitTime(void) { mData32 = TimerMilli::GetNow().GetValue(); } + }; + + /** + * This type represents Thread network info. + * + */ + typedef otHistoryTrackerNetworkInfo NetworkInfo; + + /** + * This type represents a RX/TX IPv6 message info. + * + */ + typedef otHistoryTrackerMessageInfo MessageInfo; + + /** + * This constructor initializes the `HistoryTracker`. + * + * @param[in] aInstance A reference to the OpenThread instance. + * + */ + explicit HistoryTracker(Instance &aInstance); + + /** + * This method iterates over the entries in the network info history list. + * + * @param[inout] aIterator An iterator. MUST be initialized. + * @param[out] aEntryAge A reference to a variable to output the entry's age. + * Age is provided as the duration (in milliseconds) from when entry was recorded to + * @p aIterator initialization time. It is set to `kMaxAge` for entries older than max age. + * + * @returns A pointer to `NetworkInfo` entry or `nullptr` if no more entries in the list. + * + */ + const NetworkInfo *IterateNetInfoHistory(Iterator &aIterator, uint32_t &aEntryAge) const + { + return mNetInfoHistory.Iterate(aIterator, aEntryAge); + } + + /** + * This method iterates over the entries in the RX history list. + * + * @param[inout] aIterator An iterator. MUST be initialized. + * @param[out] aEntryAge A reference to a variable to output the entry's age. + * Age is provided as the duration (in milliseconds) from when entry was recorded to + * @p aIterator initialization time. It is set to `kMaxAge` for entries older than max age. + * + * @returns A pointer to `MessageInfo` entry or `nullptr` if no more entries in the list. + * + */ + const MessageInfo *IterateRxHistory(Iterator &aIterator, uint32_t &aEntryAge) const + { + return mRxHistory.Iterate(aIterator, aEntryAge); + } + + /** + * This method iterates over the entries in the TX history list. + * + * @param[inout] aIterator An iterator. MUST be initialized. + * @param[out] aEntryAge A reference to a variable to output the entry's age. + * Age is provided as the duration (in milliseconds) from when entry was recorded to + * @p aIterator initialization time. It is set to `kMaxAge` for entries older than max age. + * + * @returns A pointer to `MessageInfo` entry or `nullptr` if no more entries in the list. + * + */ + const MessageInfo *IterateTxHistory(Iterator &aIterator, uint32_t &aEntryAge) const + { + return mTxHistory.Iterate(aIterator, aEntryAge); + } + + /** + * This static method converts a given entry age to a human-readable string. + * + * The entry age string follows the format "::." for hours, minutes, seconds and millisecond + * (if shorter than one day) or "
days ::." (if longer than one day). + * + * If the resulting string does not fit in @p aBuffer (within its @p aSize characters), the string will be + * truncated but the outputted string is always null-terminated. + * + * @param[in] aEntryAge The entry age (duration in msec). + * @param[out] aBuffer A pointer to a char array to output the string (MUST NOT be NULL). + * @param[in] aSize The size of @p aBuffer (in bytes). Recommended to use `OT_IP6_ADDRESS_STRING_SIZE`. + * + */ + static void EntryAgeToString(uint32_t aEntryAge, char *aBuffer, uint16_t aSize); + +private: + static constexpr uint32_t kOneSecondInMsec = 1000; + static constexpr uint32_t kOneMinuteInMsec = 60 * kOneSecondInMsec; + static constexpr uint32_t kOneHourInMsec = 60 * kOneMinuteInMsec; + static constexpr uint32_t kOneDayInMsec = 24 * kOneHourInMsec; + + // `Timestamp` uses `uint32_t` value. `2^32` msec is 49 days, 17 + // hours, 2 minutes and 47 seconds and 296 msec. We use 49 days + // as `kMaxAge` and check for aged entries every 16 hours. + + static constexpr uint32_t kAgeCheckPeriod = 16 * kOneHourInMsec; + + static constexpr uint16_t kNetInfoListSize = OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_INFO_LIST_SIZE; + static constexpr uint16_t kRxListSize = OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE; + static constexpr uint16_t kTxListSize = OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE; + + static constexpr int8_t kInvalidRss = OT_RADIO_RSSI_INVALID; + static constexpr uint16_t kInvalidRloc16 = Mac::kShortAddrInvalid; + + class Timestamp + { + public: + void SetToNow(void); + uint32_t GetDurationTill(TimeMilli aTime) const; + bool IsDistantPast(void) const { return (mTime.GetValue() == kDistantPast); } + void MarkAsDistantPast(void) { return mTime.SetValue(kDistantPast); } + + private: + static constexpr uint32_t kDistantPast = 0; + + TimeMilli mTime; + }; + + // An ordered list of timestamped items (base class of `EntryList`). + class List : private NonCopyable + { + public: + void Clear(void); + uint16_t GetSize(void) const { return mSize; } + + protected: + List(void); + uint16_t Add(uint16_t aMaxSize, Timestamp aTimestamps[]); + void UpdateAgedEntries(uint16_t aMaxSize, Timestamp aTimestamps[]); + uint16_t MapEntryNumberToListIndex(uint16_t aEntryNumber, uint16_t aMaxSize) const; + Error Iterate(uint16_t aMaxSize, + const Timestamp aTimestamps[], + Iterator & aIterator, + uint16_t & aListIndex, + uint32_t & aEntryAge) const; + + private: + uint16_t mStartIndex; + uint16_t mSize; + }; + + // A history list (with given max size) of timestamped `Entry` items. + template class EntryList : public List + { + public: + // Adds a new entry to the list or overwrites the oldest entry + // if list is full. First version returns a pointer to the + // new `Entry` (for caller to populate). Second version copies + // the given `aEntry`. + Entry *AddNewEntry(void) { return &mEntries[Add(kMaxSize, mTimestamps)]; } + void AddNewEntry(const Entry &aEntry) { mEntries[Add(kMaxSize, mTimestamps)] = aEntry; } + + void UpdateAgedEntries(void) { List::UpdateAgedEntries(kMaxSize, mTimestamps); } + + const Entry *Iterate(Iterator &aIterator, uint32_t &aEntryAge) const + { + uint16_t index; + + return (List::Iterate(kMaxSize, mTimestamps, aIterator, index, aEntryAge) == kErrorNone) ? &mEntries[index] + : nullptr; + } + + private: + Timestamp mTimestamps[kMaxSize]; + Entry mEntries[kMaxSize]; + }; + + // Partial specialization for `kMaxSize` zero. + template class EntryList : private NonCopyable + { + public: + void Clear(void) {} + uint16_t GetSize(void) const { return 0; } + Entry * AddNewEntry(void) { return nullptr; } + void AddNewEntry(const Entry &) {} + const Entry *Iterate(Iterator &, uint32_t &) const { return nullptr; } + void RemoveAgedEntries(void) {} + }; + + enum MessageType : uint8_t + { + kRxMessage, + kTxMessage, + }; + + void RecordRxMessage(const Message &aMessage, const Mac::Address &aMacSource) + { + RecordMessage(aMessage, aMacSource, kRxMessage); + } + + void RecordTxMessage(const Message &aMessage, const Mac::Address &aMacDest) + { + RecordMessage(aMessage, aMacDest, kTxMessage); + } + + void RecordNetworkInfo(void); + void RecordMessage(const Message &aMessage, const Mac::Address &aMacAddress, MessageType aType); + void HandleNotifierEvents(Events aEvents); + static void HandleTimer(Timer &aTimer); + void HandleTimer(void); + + EntryList mNetInfoHistory; + EntryList mRxHistory; + EntryList mTxHistory; + TimerMilli mTimer; +}; + +} // namespace Utils +} // namespace ot + +#endif // OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + +#endif // HISTORY_TRACKER_HPP_ diff --git a/src/posix/Makefile-posix b/src/posix/Makefile-posix index 9d37ea140..07a1f43ba 100644 --- a/src/posix/Makefile-posix +++ b/src/posix/Makefile-posix @@ -53,6 +53,7 @@ DNS_CLIENT ?= 1 DNSSD_SERVER ?= 1 DYNAMIC_LOG_LEVEL ?= 1 ECDSA ?= 1 +HISTORY_TRACKER ?= 1 IP6_FRAGM ?= 1 JAM_DETECTION ?= 1 JOINER ?= 1 diff --git a/src/posix/platform/openthread-core-posix-config.h b/src/posix/platform/openthread-core-posix-config.h index 35c13a74a..d1ceb9208 100644 --- a/src/posix/platform/openthread-core-posix-config.h +++ b/src/posix/platform/openthread-core-posix-config.h @@ -217,6 +217,16 @@ #define OPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS 8 #endif +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + * + * Define as 1 to enable History Tracker module. + * + */ +#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 1 +#endif + /** * @def OPENTHREAD_CONFIG_HEAP_INTERNAL_SIZE * diff --git a/tests/fuzz/oss-fuzz-build b/tests/fuzz/oss-fuzz-build index 37b4291a5..7b2103671 100755 --- a/tests/fuzz/oss-fuzz-build +++ b/tests/fuzz/oss-fuzz-build @@ -54,6 +54,7 @@ -DOT_DHCP6_SERVER=ON \ -DOT_DNS_CLIENT=ON \ -DOT_ECDSA=ON \ + -DOT_HISTORY_TRACKER=ON \ -DOT_IP6_FRAGM=ON \ -DOT_JAM_DETECTION=ON \ -DOT_JOINER=ON \ diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am index f873f331f..1a94f5740 100644 --- a/tests/scripts/thread-cert/Makefile.am +++ b/tests/scripts/thread-cert/Makefile.am @@ -162,6 +162,7 @@ EXTRA_DIST = \ test_diag.py \ test_dns_client_config_auto_start.py \ test_dnssd.py \ + test_history_tracker.py \ test_ipv6.py \ test_ipv6_fragmentation.py \ test_ipv6_source_selection.py \ @@ -228,6 +229,7 @@ check_SCRIPTS = \ test_diag.py \ test_dns_client_config_auto_start.py \ test_dnssd.py \ + test_history_tracker.py \ test_ipv6.py \ test_ipv6_fragmentation.py \ test_ipv6_source_selection.py \ diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 04cdbb127..61a97e85a 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -1331,6 +1331,10 @@ class NodeImpl: self.send_command(cmd) self._expect_done() + def get_partition_id(self): + self.send_command('partitionid') + return self._expect_result(r'\d+') + def get_preferred_partition_id(self): self.send_command('partitionid preferred') return self._expect_result(r'\d+') @@ -2720,6 +2724,112 @@ class NodeImpl: self.send_command(cmd) self._expect_command_output(cmd) + def history_netinfo(self, num_entries=0): + """ + Get the `netinfo` history list, parse each entry and return + a list of dictionary (string key and string value) entries. + + Example of return value: + [ + { + 'age': '00:00:00.000 ago', + 'role': 'disabled', + 'mode': 'rdn', + 'rloc16': '0x7400', + 'partition-id': '1318093703' + }, + { + 'age': '00:00:02.588 ago', + 'role': 'leader', + 'mode': 'rdn', + 'rloc16': '0x7400', + 'partition-id': '1318093703' + } + ] + """ + cmd = f'history netinfo list {num_entries}' + self.send_command(cmd) + output = self._expect_command_output(cmd) + netinfos = [] + for entry in output: + netinfo = {} + age, info = entry.split(' -> ') + netinfo['age'] = age + for item in info.split(' '): + k, v = item.split(':') + netinfo[k] = v + netinfos.append(netinfo) + return netinfos + + def history_rx(self, num_entries=0): + """ + Get the IPv6 RX history list, parse each entry and return + a list of dictionary (string key and string value) entries. + + Example of return value: + [ + { + 'age': '00:00:01.999', + 'type': 'ICMP6(EchoReqst)', + 'len': '16', + 'sec': 'yes', + 'prio': 'norm', + 'rss': '-20', + 'from': '0xac00', + 'radio': '15.4', + 'src': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0', + 'dst': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0', + } + ] + """ + cmd = f'history rx list {num_entries}' + self.send_command(cmd) + return self._parse_history_rx_tx_ouput(self._expect_command_output(cmd)) + + def history_tx(self, num_entries=0): + """ + Get the IPv6 TX history list, parse each entry and return + a list of dictionary (string key and string value) entries. + + Example of return value: + [ + { + 'age': '00:00:01.999', + 'type': 'ICMP6(EchoReply)', + 'len': '16', + 'sec': 'yes', + 'prio': 'norm', + 'to': '0xac00', + 'tx-success': 'yes', + 'radio': '15.4', + 'src': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0', + 'dst': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0', + + } + ] + """ + cmd = f'history tx list {num_entries}' + self.send_command(cmd) + return self._parse_history_rx_tx_ouput(self._expect_command_output(cmd)) + + def _parse_history_rx_tx_ouput(self, lines): + rxtx_list = [] + for line in lines: + if line.strip().startswith('type:'): + for item in line.strip().split(' '): + k, v = item.split(':') + entry[k] = v + elif line.strip().startswith('src:'): + entry['src'] = line[4:] + elif line.strip().startswith('dst:'): + entry['dst'] = line[4:] + rxtx_list.append(entry) + else: + entry = {} + entry['age'] = line + + return rxtx_list + class Node(NodeImpl, OtCli): pass diff --git a/tests/scripts/thread-cert/test_history_tracker.py b/tests/scripts/thread-cert/test_history_tracker.py new file mode 100755 index 000000000..6d2efeec9 --- /dev/null +++ b/tests/scripts/thread-cert/test_history_tracker.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import os +import unittest +import sys +import thread_cert + +# Test description: +# This test verifies History Tracker behavior. +# +# Topology: +# +# LEADER +# | +# | +# CHILD +# + +LEADER = 1 +CHILD = 2 + +SHORT_WAIT = 5 +ONE_DAY = 24 * 60 * 60 +MAX_AGE_IN_DAYS = 49 + + +class TestHistoryTracker(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + SUPPORT_NCP = False + + TOPOLOGY = { + LEADER: { + 'name': 'Leader', + 'mode': 'rdn', + }, + CHILD: { + 'name': 'Child', + 'mode': 'n', + }, + } + + def test(self): + leader = self.nodes[LEADER] + child = self.nodes[CHILD] + + # Start the leader and verify that 'netinfo' history + # is updated correctly. + + leader.start() + self.simulator.go(SHORT_WAIT) + self.assertEqual(leader.get_state(), 'leader') + + netinfo = leader.history_netinfo() + self.assertEqual(len(netinfo), 2) + self.assertEqual(netinfo[0]['role'], 'leader') + self.assertEqual(netinfo[0]['mode'], 'rdn') + self.assertEqual(int(netinfo[0]['rloc16'], 16), leader.get_addr16()) + self.assertEqual(netinfo[0]['partition-id'], leader.get_partition_id()) + self.assertEqual(netinfo[1]['role'], 'detached') + + # Stop the leader + + leader.thread_stop() + leader.interface_down() + self.simulator.go(SHORT_WAIT) + netinfo = leader.history_netinfo(2) + self.assertEqual(len(netinfo), 2) + self.assertEqual(netinfo[0]['role'], 'disabled') + self.assertEqual(netinfo[1]['role'], 'leader') + + # Wait for one day, two days, then up to max age and verify that + # `netinfo` entry age is updated correctly. + # + # Since we want to wait for long duration (49 days), to speed up + # the simulation time, we disable leader to avoid the need to + # to simulate all the message/events (e.g. MLE adv) while thread + # is operational. + + self.simulator.go(ONE_DAY) + netinfo = leader.history_netinfo(1) + self.assertTrue(netinfo[0]['age'].startswith('1 day')) + + self.simulator.go(ONE_DAY) + netinfo = leader.history_netinfo(1) + self.assertTrue(netinfo[0]['age'].startswith('2 days')) + + self.simulator.go((MAX_AGE_IN_DAYS - 3) * ONE_DAY) + netinfo = leader.history_netinfo(1) + self.assertTrue(netinfo[0]['age'].startswith('{} days'.format(MAX_AGE_IN_DAYS - 1))) + + self.simulator.go(ONE_DAY) + netinfo = leader.history_netinfo(1) + self.assertTrue(netinfo[0]['age'].startswith('more than {} days'.format(MAX_AGE_IN_DAYS))) + + self.simulator.go(2 * ONE_DAY) + netinfo = leader.history_netinfo(1) + self.assertTrue(netinfo[0]['age'].startswith('more than {} days'.format(MAX_AGE_IN_DAYS))) + + # Start leader and child + + leader.start() + self.simulator.go(SHORT_WAIT) + self.assertEqual(leader.get_state(), 'leader') + + child.start() + self.simulator.go(SHORT_WAIT) + self.assertEqual(child.get_state(), 'child') + + child_rloc16 = child.get_addr16() + leader_rloc16 = leader.get_addr16() + + # Verify the `netinfo` history on child + + netinfo = child.history_netinfo(2) + self.assertEqual(len(netinfo), 2) + self.assertEqual(netinfo[0]['role'], 'child') + self.assertEqual(netinfo[0]['mode'], 'n') + self.assertEqual(int(netinfo[0]['rloc16'], 16), child_rloc16) + self.assertEqual(netinfo[0]['partition-id'], leader.get_partition_id()) + self.assertEqual(netinfo[1]['role'], 'detached') + + # Change the child mode and verify that `netinfo` history + # records this change. + + child.set_mode('rn') + self.simulator.go(SHORT_WAIT) + netinfo = child.history_netinfo(1) + self.assertEqual(len(netinfo), 1) + self.assertEqual(netinfo[0]['mode'], 'rn') + + # Ping from leader to child and check the RX and TX history + # on child and leader. + + child_mleid = child.get_mleid() + leader_mleid = leader.get_mleid() + + ping_sizes = [10, 100, 1000] + num_msgs = len(ping_sizes) + + for size in ping_sizes: + leader.ping(child_mleid, size=size) + + leader_tx = leader.history_tx(num_msgs) + leader_rx = leader.history_rx(num_msgs) + child_tx = child.history_tx(num_msgs) + child_rx = child.history_rx(num_msgs) + + for index in range(num_msgs): + self.assertEqual(leader_tx[index]['type'], 'ICMP6(EchoReqst)') + self.assertEqual(leader_tx[index]['sec'], 'yes') + self.assertEqual(leader_tx[index]['prio'], 'norm') + self.assertEqual(leader_tx[index]['tx-success'], 'yes') + self.assertEqual(leader_tx[index]['radio'], '15.4') + self.assertEqual(int(leader_tx[index]['to'], 16), child_rloc16) + self.assertEqual(leader_tx[index]['src'][1:-3], leader_mleid) + self.assertEqual(leader_tx[index]['dst'][1:-3], child_mleid) + + self.assertEqual(child_rx[index]['type'], 'ICMP6(EchoReqst)') + self.assertEqual(child_rx[index]['sec'], 'yes') + self.assertEqual(child_rx[index]['prio'], 'norm') + self.assertEqual(child_rx[index]['radio'], '15.4') + self.assertEqual(int(child_rx[index]['from'], 16), leader_rloc16) + self.assertEqual(child_rx[index]['src'][1:-3], leader_mleid) + self.assertEqual(child_rx[index]['dst'][1:-3], child_mleid) + + self.assertEqual(leader_rx[index]['type'], 'ICMP6(EchoReply)') + self.assertEqual(child_tx[index]['type'], 'ICMP6(EchoReply)') + + self.assertEqual(leader_tx[index]['len'], child_rx[index]['len']) + self.assertEqual(leader_rx[index]['len'], child_tx[index]['len']) + + +if __name__ == '__main__': + # FIXME: We skip the test under distcheck build (the simulation + # under this build for some reason cannot seem to handle longer + # wait times - days up to 50 days in this test). We return error + # code 77 which indicates that this test case was skipped (in + # automake). + + if os.getenv('DISTCHECK_BUILD') == '1': + sys.exit(77) + + unittest.main() diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index 9f1a4ced2..00f5dd8a4 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -486,6 +486,14 @@ */ #define OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE 1 +/** + * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + * + * Define as 1 to enable History Tracker module. + * + */ +#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE 1 + #if OPENTHREAD_RADIO /** * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE