From 7266e1b909e9c6c147844584ac8518c4c679e86d Mon Sep 17 00:00:00 2001 From: Simon Lin Date: Sat, 29 Aug 2020 23:52:29 +0800 Subject: [PATCH] [mlr] add Commissioner API to register Multicast Listeners (#5346) --- include/openthread/ip6.h | 53 ++++ src/cli/README.md | 27 ++ src/cli/cli.cpp | 91 +++++++ src/cli/cli.hpp | 17 ++ src/core/api/ip6_api.cpp | 15 ++ src/core/backbone_router/bbr_manager.cpp | 117 +++++--- src/core/common/code_utils.hpp | 12 + src/core/config/tmf.h | 5 + src/core/thread/mlr_manager.cpp | 252 +++++++++++++----- src/core/thread/mlr_manager.hpp | 94 +++++-- src/core/thread/thread_tlvs.hpp | 24 +- tests/scripts/thread-cert/node.py | 49 ++++ ..._2_test_multicast_listener_registration.py | 98 +++++++ 13 files changed, 731 insertions(+), 123 deletions(-) diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h index ef169607a..ce9772135 100644 --- a/include/openthread/ip6.h +++ b/include/openthread/ip6.h @@ -653,6 +653,59 @@ typedef bool (*otIp6SlaacPrefixFilter)(otInstance *aInstance, const otIp6Prefix */ void otIp6SetSlaacPrefixFilter(otInstance *aInstance, otIp6SlaacPrefixFilter aFilter); +/** + * This function pointer is called with results of `otIp6RegisterMulticastListeners`. + * + * @param[in] aContext A pointer to the user context. + * @param[in] aError OT_ERROR_NONE when successfully sent MLR.req and received MLR.rsp, + * OT_ERROR_RESPONSE_TIMEOUT when failed to receive MLR.rsp, + * OT_ERROR_PARSE when failed to parse MLR.rsp. + * @param[in] aMlrStatus The Multicast Listener Registration status when @p aError is OT_ERROR_NONE. + * @param[in] aFailedAddresses A pointer to the failed Ip6 addresses when @p aError is OT_ERROR_NONE. + * @param[in] aFailedAddressNum The number of failed Ip6 addresses when @p aError is OT_ERROR_NONE. + * + * @sa otIp6RegisterMulticastListeners + * + */ +typedef void (*otIp6RegisterMulticastListenersCallback)(void * aContext, + otError aError, + uint8_t aMlrStatus, + const otIp6Address *aFailedAddresses, + uint8_t aFailedAddressNum); + +/** + * This function registers Multicast Listeners to Primary Backbone Router. + * + * Note: only available when both `OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE` and + * `OPENTHREAD_CONFIG_COMMISSIONER_ENABLE` are enabled) + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aAddresses A Multicast Address Array to register. + * @param[in] aAddressNum The number of Multicast Address to register (0 if @p aAddresses is NULL). + * @param[in] aTimeout A pointer to the timeout value (in seconds) to be included in MLR.req. A timeout value of 0 + * removes the corresponding Multicast Listener. If NULL, MLR.req would have no Timeout Tlv by + * default. + * @param[in] aCallback A pointer to the callback function. + * @param[in] aContext A pointer to the user context. + * + * @retval OT_ERROR_NONE Successfully sent MLR.req. The @p aCallback will be called iff this method + * returns OT_ERROR_NONE. + * @retval OT_ERROR_BUSY If a previous registration was ongoing. + * @retval OT_ERROR_INVALID_ARGS If one or more arguments are invalid. + * @retval OT_ERROR_INVALID_STATE If the device was not in a valid state to send MLR.req (e.g. Commissioner not + * started, Primary Backbone Router not found). + * @retval OT_ERROR_NO_BUFS If insufficient message buffers available. + * + * @sa otIp6RegisterMulticastListenersCallback + * + */ +otError otIp6RegisterMulticastListeners(otInstance * aInstance, + const otIp6Address * aAddresses, + uint8_t aAddressNum, + const uint32_t * aTimeout, + otIp6RegisterMulticastListenersCallback aCallback, + void * aContext); + /** * @} * diff --git a/src/cli/README.md b/src/cli/README.md index 7dbcca34c..a6c41718b 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -60,6 +60,7 @@ Done - [mac](#mac-retries-direct) - [macfilter](#macfilter) - [masterkey](#masterkey) +- [mlr](#mlr-reg-ipaddr--timeout) - [mode](#mode) - [neighbor](#neighbor-list) - [netdata](#netdata-steeringdata-check-eui64discerner) @@ -1097,6 +1098,32 @@ Set the Thread Master Key value. Done ``` +### mlr reg \ ... [timeout] + +Register Multicast Listeners to Primary Backbone Router, with an optional `timeout` (in seconds). + +Omit `timeout` to use the default MLR timeout on the Primary Backbone Router. + +Use `timeout = 0` to deregister Multicast Listeners. + +NOTE: Only for Thread 1.2 Commissioner FTD device. + +```bash +> mlr reg ff04::1 +status 0, 0 failed +Done +> mlr reg ff04::1 ff04::2 ff02::1 +status 2, 1 failed +ff02:0:0:0:0:0:0:1 +Done +> mlr reg ff04::1 ff04::2 1000 +status 0, 0 failed +Done +> mlr reg ff04::1 ff04::2 0 +status 0, 0 failed +Done +``` + ### mode Get the Thread Device Mode value. diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 38716e9ce..ed4417735 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -184,6 +184,9 @@ const struct Command Interpreter::sCommands[] = { {"macfilter", &Interpreter::ProcessMacFilter}, #endif {"masterkey", &Interpreter::ProcessMasterKey}, +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + {"mlr", &Interpreter::ProcessMlr}, +#endif {"mode", &Interpreter::ProcessMode}, #if OPENTHREAD_FTD {"neighbor", &Interpreter::ProcessNeighbor}, @@ -2070,6 +2073,94 @@ exit: AppendResult(error); } +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + +void Interpreter::ProcessMlr(uint8_t aArgsLength, char **aArgs) +{ + if (aArgsLength == 0) + { + AppendResult(OT_ERROR_INVALID_COMMAND); + } + else if (!strcmp(aArgs[0], "reg")) + { + ProcessMlrReg(aArgsLength - 1, aArgs + 1); + } + else + { + AppendResult(OT_ERROR_INVALID_COMMAND); + } +} + +void Interpreter::ProcessMlrReg(uint8_t aArgsLength, char *aArgs[]) +{ + otError error = OT_ERROR_NONE; + otIp6Address addresses[kIPv6AddressesNumMax]; + uint32_t timeout; + uint8_t i; + + VerifyOrExit(aArgsLength >= 1, error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(aArgsLength <= kIPv6AddressesNumMax + 1, error = OT_ERROR_INVALID_ARGS); + + for (i = 0; i < aArgsLength && i < kIPv6AddressesNumMax; i++) + { + if (otIp6AddressFromString(aArgs[i], &addresses[i]) != OT_ERROR_NONE) + { + break; + } + } + + VerifyOrExit(i > 0 && (i == aArgsLength || i == aArgsLength - 1), error = OT_ERROR_INVALID_ARGS); + + if (i == aArgsLength - 1) + { + // Parse the last argument as a timeout in seconds + unsigned long value; + + SuccessOrExit(error = ParseUnsignedLong(aArgs[i], value)); + + timeout = static_cast(value); + } + + error = otIp6RegisterMulticastListeners(mInstance, addresses, i, i == aArgsLength - 1 ? &timeout : nullptr, + Interpreter::HandleMlrRegResult, this); + +exit: + if (error != OT_ERROR_NONE) + { + AppendResult(error); + } +} + +void Interpreter::HandleMlrRegResult(void * aContext, + otError aError, + uint8_t aMlrStatus, + const otIp6Address *aFailedAddresses, + uint8_t aFailedAddressNum) +{ + static_cast(aContext)->HandleMlrRegResult(aError, aMlrStatus, aFailedAddresses, aFailedAddressNum); +} + +void Interpreter::HandleMlrRegResult(otError aError, + uint8_t aMlrStatus, + const otIp6Address *aFailedAddresses, + uint8_t aFailedAddressNum) +{ + if (aError == OT_ERROR_NONE) + { + OutputFormat("status %d, %d failed\r\n", aMlrStatus, aFailedAddressNum); + + for (uint8_t i = 0; i < aFailedAddressNum; i++) + { + OutputIp6Address(aFailedAddresses[i]); + OutputFormat("\r\n"); + } + } + + AppendResult(aError); +} + +#endif // OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + void Interpreter::ProcessMode(uint8_t aArgsLength, char *aArgs[]) { otError error = OT_ERROR_NONE; diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index 087749dc5..4098957ac 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -354,6 +354,23 @@ private: void ProcessLeaderWeight(uint8_t aArgsLength, char *aArgs[]); #endif void ProcessMasterKey(uint8_t aArgsLength, char *aArgs[]); +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE + void ProcessMlr(uint8_t aArgsLength, char *aArgs[]); + +#if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + void ProcessMlrReg(uint8_t aArgsLength, char *aArgs[]); + + static void HandleMlrRegResult(void * aContext, + otError aError, + uint8_t aMlrStatus, + const otIp6Address *aFailedAddresses, + uint8_t aFailedAddressNum); + void HandleMlrRegResult(otError aError, + uint8_t aMlrStatus, + const otIp6Address *aFailedAddresses, + uint8_t aFailedAddressNum); +#endif +#endif void ProcessMode(uint8_t aArgsLength, char *aArgs[]); #if OPENTHREAD_FTD void ProcessNeighbor(uint8_t aArgsLength, char *aArgs[]); diff --git a/src/core/api/ip6_api.cpp b/src/core/api/ip6_api.cpp index 65e1fca27..51679a5b1 100644 --- a/src/core/api/ip6_api.cpp +++ b/src/core/api/ip6_api.cpp @@ -286,4 +286,19 @@ void otIp6SetSlaacPrefixFilter(otInstance *aInstance, otIp6SlaacPrefixFilter aFi instance.Get().SetFilter(aFilter); } +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE +otError otIp6RegisterMulticastListeners(otInstance * aInstance, + const otIp6Address * aAddresses, + uint8_t aAddressNum, + const uint32_t * aTimeout, + otIp6RegisterMulticastListenersCallback aCallback, + void * aContext) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().RegisterMulticastListeners(aAddresses, aAddressNum, aTimeout, aCallback, + aContext); +} +#endif + #endif // OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE diff --git a/src/core/backbone_router/bbr_manager.cpp b/src/core/backbone_router/bbr_manager.cpp index 9ae17bfda..b7532abbe 100644 --- a/src/core/backbone_router/bbr_manager.cpp +++ b/src/core/backbone_router/bbr_manager.cpp @@ -33,6 +33,8 @@ #include "bbr_manager.hpp" +#include + #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE #include "common/code_utils.hpp" @@ -99,13 +101,19 @@ void Manager::HandleMulticastListenerRegistration(const Coap::Message &aMessage, otError error = OT_ERROR_NONE; bool isPrimary = Get().IsPrimary(); ThreadStatusTlv::MlrStatus status = ThreadStatusTlv::kMlrSuccess; - uint16_t addressesOffset, addressesLength; - Ip6::Address address; - Ip6::Address failedAddresses[kIPv6AddressesNumMax]; - uint8_t failedAddressNum = 0; - TimeMilli expireTime; BackboneRouterConfig config; + uint16_t addressesOffset, addressesLength; + Ip6::Address address; + Ip6::Address failedAddresses[kIPv6AddressesNumMax]; + uint8_t failedAddressNum = 0; + TimeMilli expireTime; + + uint32_t timeout; + uint16_t commissionerSessionId; + bool hasCommissionerSessionIdTlv = false; + bool processTimeoutTlv = false; + VerifyOrExit(aMessage.IsConfirmable() && aMessage.GetCode() == OT_COAP_CODE_POST, error = OT_ERROR_PARSE); #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE @@ -119,8 +127,23 @@ void Manager::HandleMulticastListenerRegistration(const Coap::Message &aMessage, VerifyOrExit(isPrimary, status = ThreadStatusTlv::kMlrBbrNotPrimary); - // TODO: (MLR) handle Commissioner Session TLV - // TODO: (MLR) handle Timeout TLV + // TODO: (MLR) send configured MLR response for Reference Device + + if (ThreadTlv::FindUint16Tlv(aMessage, ThreadTlv::kCommissionerSessionId, commissionerSessionId) == OT_ERROR_NONE) + { + const MeshCoP::CommissionerSessionIdTlv *commissionerSessionIdTlv = + static_cast( + Get().GetCommissioningDataSubTlv(MeshCoP::Tlv::kCommissionerSessionId)); + + VerifyOrExit(commissionerSessionIdTlv != nullptr && + commissionerSessionIdTlv->GetCommissionerSessionId() == commissionerSessionId, + status = ThreadStatusTlv::kMlrGeneralFailure); + + hasCommissionerSessionIdTlv = true; + } + + processTimeoutTlv = hasCommissionerSessionIdTlv && + (ThreadTlv::FindUint32Tlv(aMessage, ThreadTlv::kTimeout, timeout) == OT_ERROR_NONE); VerifyOrExit(ThreadTlv::FindTlvValueOffset(aMessage, IPv6AddressesTlv::kIPv6Addresses, addressesOffset, addressesLength) == OT_ERROR_NONE, @@ -129,39 +152,69 @@ void Manager::HandleMulticastListenerRegistration(const Coap::Message &aMessage, VerifyOrExit(addressesLength / sizeof(Ip6::Address) <= kIPv6AddressesNumMax, status = ThreadStatusTlv::kMlrGeneralFailure); - IgnoreError(Get().GetConfig(config)); - expireTime = TimerMilli::GetNow() + TimeMilli::SecToMsec(config.mMlrTimeout); + if (!processTimeoutTlv) + { + IgnoreError(Get().GetConfig(config)); + + timeout = config.mMlrTimeout; + } + else + { + VerifyOrExit(timeout < std::numeric_limits::max(), status = ThreadStatusTlv::kMlrNoPersistent); + + if (timeout != 0) + { + uint32_t origTimeout = timeout; + + timeout = OT_MIN(timeout, static_cast(Mle::kMlrTimeoutMax)); + timeout = OT_MAX(timeout, static_cast(Mle::kMlrTimeoutMin)); + + if (timeout != origTimeout) + { + otLogNoteBbr("MLR.req: MLR timeout is normalized from %u to %u", origTimeout, timeout); + } + } + } + + expireTime = TimerMilli::GetNow() + TimeMilli::SecToMsec(timeout); for (uint16_t offset = 0; offset < addressesLength; offset += sizeof(Ip6::Address)) { - bool failed = true; - IgnoreReturnValue(aMessage.Read(addressesOffset + offset, sizeof(Ip6::Address), &address)); - switch (mMulticastListenersTable.Add(address, expireTime)) + if (timeout == 0) { - case OT_ERROR_NONE: - failed = false; - break; - case OT_ERROR_INVALID_ARGS: - if (status == ThreadStatusTlv::kMlrSuccess) - { - status = ThreadStatusTlv::kMlrInvalid; - } - break; - case OT_ERROR_NO_BUFS: - if (status == ThreadStatusTlv::kMlrSuccess) - { - status = ThreadStatusTlv::kMlrNoResources; - } - break; - default: - OT_ASSERT(false); + mMulticastListenersTable.Remove(address); } - - if (failed) + else { - failedAddresses[failedAddressNum++] = address; + bool failed = true; + + switch (mMulticastListenersTable.Add(address, expireTime)) + { + case OT_ERROR_NONE: + failed = false; + break; + case OT_ERROR_INVALID_ARGS: + if (status == ThreadStatusTlv::kMlrSuccess) + { + status = ThreadStatusTlv::kMlrInvalid; + } + break; + case OT_ERROR_NO_BUFS: + if (status == ThreadStatusTlv::kMlrSuccess) + { + status = ThreadStatusTlv::kMlrNoResources; + } + break; + default: + OT_ASSERT(false); + } + + if (failed) + { + failedAddresses[failedAddressNum++] = address; + } } } diff --git a/src/core/common/code_utils.hpp b/src/core/common/code_utils.hpp index 61cbfadcc..2921d3068 100644 --- a/src/core/common/code_utils.hpp +++ b/src/core/common/code_utils.hpp @@ -80,6 +80,18 @@ #define OT_DEFINE_ALIGNED_VAR(name, size, align_type) \ align_type name[(((size) + (sizeof(align_type) - 1)) / sizeof(align_type))] +/** + * This macro returns the smaller of @p a and @p b. + * + */ +#define OT_MIN(a, b) ((b) < (a) ? (b) : (a)) + +/** + * This macro returns the greater of @p a and @p b. + * + */ +#define OT_MAX(a, b) ((a) < (b) ? (b) : (a)) + /** * This macro checks for the specified status, which is expected to commonly be successful, and branches to the local * label 'exit' if the status is unsuccessful. diff --git a/src/core/config/tmf.h b/src/core/config/tmf.h index 14491b421..56912737e 100644 --- a/src/core/config/tmf.h +++ b/src/core/config/tmf.h @@ -189,7 +189,12 @@ * This is compulsory for 1.2 FTD. * */ +#ifdef OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE +#error \ + "Don't define `OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE` manually. It's an alias of `((OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2) && OPENTHREAD_FTD)`." +#else #define OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE \ ((OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2) && OPENTHREAD_FTD) +#endif #endif // CONFIG_TMF_H_ diff --git a/src/core/thread/mlr_manager.cpp b/src/core/thread/mlr_manager.cpp index d82fb1023..54f7abdf5 100644 --- a/src/core/thread/mlr_manager.cpp +++ b/src/core/thread/mlr_manager.cpp @@ -48,9 +48,16 @@ namespace ot { MlrManager::MlrManager(Instance &aInstance) : InstanceLocator(aInstance) +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + , mRegisterMulticastListenersCallback(nullptr) + , mRegisterMulticastListenersContext(nullptr) +#endif , mReregistrationDelay(0) , mSendDelay(0) , mMlrPending(false) +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + , mRegisterMulticastListenersPending(false) +#endif { } @@ -218,13 +225,10 @@ void MlrManager::UpdateTimeTickerRegistration(void) void MlrManager::SendMulticastListenerRegistration(void) { - otError error = OT_ERROR_NONE; - Mle::MleRouter & mle = Get(); - Coap::Message * message = nullptr; - Ip6::MessageInfo messageInfo; - IPv6AddressesTlv addressesTlv; - Ip6::Address addresses[kIPv6AddressesNumMax]; - uint8_t addressesNum = 0; + otError error; + Mle::MleRouter &mle = Get(); + Ip6::Address addresses[kIPv6AddressesNumMax]; + uint8_t addressesNum = 0; VerifyOrExit(!mMlrPending, error = OT_ERROR_BUSY); VerifyOrExit(mle.IsAttached(), error = OT_ERROR_INVALID_STATE); @@ -280,6 +284,113 @@ void MlrManager::SendMulticastListenerRegistration(void) #endif VerifyOrExit(addressesNum > 0, error = OT_ERROR_NOT_FOUND); + SuccessOrExit( + error = SendMulticastListenerRegistrationMessage( + addresses, addressesNum, nullptr, &MlrManager::HandleMulticastListenerRegistrationResponse, this)); + + mMlrPending = true; + + // TODO: not enable fast polls for SSED + if (!Get().IsRxOnWhenIdle()) + { + Get().SendFastPolls(DataPollSender::kDefaultFastPolls); + } + +exit: + if (error != OT_ERROR_NONE) + { + SetMulticastAddressMlrState(kMlrStateRegistering, kMlrStateToRegister); + + if (error == OT_ERROR_NO_BUFS) + { + ScheduleSend(1); + } + } + + LogMulticastAddresses(); + CheckInvariants(); +} + +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE +otError MlrManager::RegisterMulticastListeners(const otIp6Address * aAddresses, + uint8_t aAddressNum, + const uint32_t * aTimeout, + otIp6RegisterMulticastListenersCallback aCallback, + void * aContext) +{ + otError error; + + VerifyOrExit(aAddresses != nullptr, error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(aAddressNum > 0 && aAddressNum <= kIPv6AddressesNumMax, error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(aContext == nullptr || aCallback != nullptr, error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(Get().IsActive(), error = OT_ERROR_INVALID_STATE); + + // Only allow one outstanding registration if callback is specified. + VerifyOrExit(!mRegisterMulticastListenersPending, error = OT_ERROR_BUSY); + + SuccessOrExit(error = SendMulticastListenerRegistrationMessage( + aAddresses, aAddressNum, aTimeout, &MlrManager::HandleRegisterMulticastListenersResponse, this)); + + mRegisterMulticastListenersPending = true; + mRegisterMulticastListenersCallback = aCallback; + mRegisterMulticastListenersContext = aContext; + +exit: + return error; +} + +void MlrManager::HandleRegisterMulticastListenersResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aResult) +{ + static_cast(aContext)->HandleRegisterMulticastListenersResponse( + static_cast(aMessage), static_cast(aMessageInfo), aResult); +} + +void MlrManager::HandleRegisterMulticastListenersResponse(otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aResult) +{ + OT_UNUSED_VARIABLE(aMessageInfo); + + uint8_t status; + otError error; + Ip6::Address failedAddresses[kIPv6AddressesNumMax]; + uint8_t failedAddressNum = 0; + otIp6RegisterMulticastListenersCallback callback = mRegisterMulticastListenersCallback; + void * context = mRegisterMulticastListenersContext; + + mRegisterMulticastListenersPending = false; + mRegisterMulticastListenersCallback = nullptr; + mRegisterMulticastListenersContext = nullptr; + + error = ParseMulticastListenerRegistrationResponse(aResult, static_cast(aMessage), status, + failedAddresses, failedAddressNum); + + if (callback != nullptr) + { + callback(context, error, status, failedAddresses, failedAddressNum); + } +} + +#endif // OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + +otError MlrManager::SendMulticastListenerRegistrationMessage(const otIp6Address * aAddresses, + uint8_t aAddressNum, + const uint32_t * aTimeout, + Coap::ResponseHandler aResponseHandler, + void * aResponseContext) +{ + OT_UNUSED_VARIABLE(aTimeout); + + otError error = OT_ERROR_NONE; + Mle::MleRouter & mle = Get(); + Coap::Message * message = nullptr; + Ip6::MessageInfo messageInfo; + IPv6AddressesTlv addressesTlv; + + VerifyOrExit(Get().HasPrimary(), error = OT_ERROR_INVALID_STATE); VerifyOrExit((message = Get().NewMessage()) != nullptr, error = OT_ERROR_NO_BUFS); @@ -289,9 +400,24 @@ void MlrManager::SendMulticastListenerRegistration(void) SuccessOrExit(message->SetPayloadMarker()); addressesTlv.Init(); - addressesTlv.SetLength(sizeof(Ip6::Address) * addressesNum); + addressesTlv.SetLength(sizeof(Ip6::Address) * aAddressNum); SuccessOrExit(error = message->Append(&addressesTlv, sizeof(addressesTlv))); - SuccessOrExit(error = message->Append(&addresses, sizeof(Ip6::Address) * addressesNum)); + SuccessOrExit(error = message->Append(aAddresses, sizeof(Ip6::Address) * aAddressNum)); + +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + if (Get().IsActive()) + { + SuccessOrExit(error = ThreadTlv::AppendUint16Tlv(*message, ThreadTlv::kCommissionerSessionId, + Get().GetSessionId())); + } + + if (aTimeout != nullptr) + { + SuccessOrExit(error = Tlv::AppendUint32Tlv(*message, ThreadTlv::kTimeout, *aTimeout)); + } +#else + OT_ASSERT(aTimeout == nullptr); +#endif if (!mle.IsFullThreadDevice() && mle.GetParent().IsThreadVersion1p1()) { @@ -309,36 +435,26 @@ void MlrManager::SendMulticastListenerRegistration(void) messageInfo.SetPeerPort(Tmf::kUdpPort); messageInfo.SetSockAddr(mle.GetMeshLocal16()); - SuccessOrExit(error = Get().SendMessage( - *message, messageInfo, &MlrManager::HandleMulticastListenerRegistrationResponse, this)); - - mMlrPending = true; - - // TODO: not enable fast polls for SSED - if (!Get().IsRxOnWhenIdle()) - { - Get().SendFastPolls(DataPollSender::kDefaultFastPolls); - } + error = Get().SendMessage(*message, messageInfo, aResponseHandler, aResponseContext); exit: - if (error != OT_ERROR_NONE) + otLogInfoMlr("Send MLR.req: %s, addressNum=%d", otThreadErrorToString(error), aAddressNum); + + if (error != OT_ERROR_NONE && message != nullptr) { - if (message != nullptr) - { - message->Free(); - } - - SetMulticastAddressMlrState(kMlrStateRegistering, kMlrStateToRegister); - - if (error == OT_ERROR_NO_BUFS) - { - ScheduleSend(1); - } + message->Free(); } - otLogInfoMlr("Send MLR.req: %s", otThreadErrorToString(error)); - LogMulticastAddresses(); - CheckInvariants(); + return error; +} + +void MlrManager::HandleMulticastListenerRegistrationResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aResult) +{ + static_cast(aContext)->HandleMulticastListenerRegistrationResponse( + static_cast(aMessage), static_cast(aMessageInfo), aResult); } void MlrManager::HandleMulticastListenerRegistrationResponse(Coap::Message * aMessage, @@ -347,35 +463,12 @@ void MlrManager::HandleMulticastListenerRegistrationResponse(Coap::Message * { OT_UNUSED_VARIABLE(aMessageInfo); - uint8_t status = ThreadStatusTlv::MlrStatus::kMlrGeneralFailure; - otError error = OT_ERROR_NONE; - uint16_t addressesOffset, addressesLength; + uint8_t status; + otError error; Ip6::Address failedAddresses[kIPv6AddressesNumMax]; uint8_t failedAddressNum = 0; - VerifyOrExit(aResult == OT_ERROR_NONE && aMessage != nullptr, error = OT_ERROR_PARSE); - VerifyOrExit(aMessage->GetCode() == OT_COAP_CODE_CHANGED, error = OT_ERROR_PARSE); - - SuccessOrExit(error = Tlv::FindUint8Tlv(*aMessage, ThreadTlv::kStatus, status)); - - if (ThreadTlv::FindTlvValueOffset(*aMessage, IPv6AddressesTlv::kIPv6Addresses, addressesOffset, addressesLength) == - OT_ERROR_NONE) - { - VerifyOrExit(addressesLength % sizeof(Ip6::Address) == 0, error = OT_ERROR_PARSE); - VerifyOrExit(addressesLength / sizeof(Ip6::Address) <= kIPv6AddressesNumMax, error = OT_ERROR_PARSE); - - for (uint16_t offset = 0; offset < addressesLength; offset += sizeof(Ip6::Address)) - { - IgnoreReturnValue( - aMessage->Read(addressesOffset + offset, sizeof(Ip6::Address), &failedAddresses[failedAddressNum])); - failedAddressNum++; - } - } - - VerifyOrExit(failedAddressNum == 0 || status != ThreadStatusTlv::MlrStatus::kMlrSuccess, error = OT_ERROR_PARSE); - -exit: - LogMlrResponse(aResult, error, status, failedAddresses, failedAddressNum); + error = ParseMulticastListenerRegistrationResponse(aResult, aMessage, status, failedAddresses, failedAddressNum); FinishMulticastListenerRegistration(error == OT_ERROR_NONE && status == ThreadStatusTlv::MlrStatus::kMlrSuccess, failedAddresses, failedAddressNum); @@ -403,6 +496,43 @@ exit: } } +otError MlrManager::ParseMulticastListenerRegistrationResponse(otError aResult, + Coap::Message *aMessage, + uint8_t & aStatus, + Ip6::Address * aFailedAddresses, + uint8_t & aFailedAddressNum) +{ + otError error; + uint16_t addressesOffset, addressesLength; + + aStatus = ThreadStatusTlv::MlrStatus::kMlrGeneralFailure; + + VerifyOrExit(aResult == OT_ERROR_NONE && aMessage != nullptr, error = OT_ERROR_PARSE); + VerifyOrExit(aMessage->GetCode() == OT_COAP_CODE_CHANGED, error = OT_ERROR_PARSE); + + SuccessOrExit(error = Tlv::FindUint8Tlv(*aMessage, ThreadTlv::kStatus, aStatus)); + + if (ThreadTlv::FindTlvValueOffset(*aMessage, IPv6AddressesTlv::kIPv6Addresses, addressesOffset, addressesLength) == + OT_ERROR_NONE) + { + VerifyOrExit(addressesLength % sizeof(Ip6::Address) == 0, error = OT_ERROR_PARSE); + VerifyOrExit(addressesLength / sizeof(Ip6::Address) <= kIPv6AddressesNumMax, error = OT_ERROR_PARSE); + + for (uint16_t offset = 0; offset < addressesLength; offset += sizeof(Ip6::Address)) + { + IgnoreReturnValue( + aMessage->Read(addressesOffset + offset, sizeof(Ip6::Address), &aFailedAddresses[aFailedAddressNum])); + aFailedAddressNum++; + } + } + + VerifyOrExit(aFailedAddressNum == 0 || aStatus != ThreadStatusTlv::MlrStatus::kMlrSuccess, error = OT_ERROR_PARSE); + +exit: + LogMlrResponse(aResult, error, aStatus, aFailedAddresses, aFailedAddressNum); + return aResult != OT_ERROR_NONE ? aResult : error; +} + void MlrManager::SetMulticastAddressMlrState(MlrState aFromState, MlrState aToState) { #if OPENTHREAD_CONFIG_MLR_ENABLE @@ -485,6 +615,8 @@ void MlrManager::HandleTimeTick(void) void MlrManager::Reregister(void) { + otLogInfoMlr("MLR Reregister!"); + SetMulticastAddressMlrState(kMlrStateRegistered, kMlrStateToRegister); CheckInvariants(); diff --git a/src/core/thread/mlr_manager.hpp b/src/core/thread/mlr_manager.hpp index 12b2b93ae..c465617df 100644 --- a/src/core/thread/mlr_manager.hpp +++ b/src/core/thread/mlr_manager.hpp @@ -107,23 +107,68 @@ public: uint16_t aOldMlrRegisteredAddressNum); #endif +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + /** + * This method registers Multicast Listeners to Primary Backbone Router. + * + * Note: only available when both `OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE` and + * `OPENTHREAD_CONFIG_COMMISSIONER_ENABLE` are enabled) + * + * @param aAddresses A pointer to Ip6 multicast addresses to register. + * @param aAddressNum The number of Ip6 multicast addresses. + * @param aTimeout A pointer to the timeout (in seconds), or nullptr to use the default MLR timeout. + * A timeout of 0 seconds removes the Multicast Listener addresses. + * @param aCallback A callback function. + * @param aContext A user context pointer. + * + * @retval OT_ERROR_NONE Successfully sent MLR.req. The @p aCallback will be called iff this method + * returns OT_ERROR_NONE. + * @retval OT_ERROR_BUSY If a previous registration was ongoing. + * @retval OT_ERROR_INVALID_ARGS If one or more arguments are invalid. + * @retval OT_ERROR_INVALID_STATE If the device was not in a valid state to send MLR.req (e.g. Commissioner not + * started, Primary Backbone Router not found). + * @retval OT_ERROR_NO_BUFS If insufficient message buffers available. + * + */ + otError RegisterMulticastListeners(const otIp6Address * aAddresses, + uint8_t aAddressNum, + const uint32_t * aTimeout, + otIp6RegisterMulticastListenersCallback aCallback, + void * aContext); +#endif + private: void HandleNotifierEvents(Events aEvents); - void SendMulticastListenerRegistration(void); + void SendMulticastListenerRegistration(void); + otError SendMulticastListenerRegistrationMessage(const otIp6Address * aAddresses, + uint8_t aAddressNum, + const uint32_t * aTimeout, + Coap::ResponseHandler aResponseHandler, + void * aResponseContext); - static void HandleMulticastListenerRegistrationResponse(void * aContext, - otMessage * aMessage, - const otMessageInfo *aMessageInfo, - otError aResult) - { - static_cast(aContext)->HandleMulticastListenerRegistrationResponse( - static_cast(aMessage), static_cast(aMessageInfo), aResult); - } + static void HandleMulticastListenerRegistrationResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aResult); + void HandleMulticastListenerRegistrationResponse(Coap::Message * aMessage, + const Ip6::MessageInfo *aMessageInfo, + otError aResult); + static otError ParseMulticastListenerRegistrationResponse(otError aResult, + Coap::Message *aMessage, + uint8_t & aStatus, + Ip6::Address * aFailedAddresses, + uint8_t & aFailedAddressNum); - void HandleMulticastListenerRegistrationResponse(Coap::Message * aMessage, - const Ip6::MessageInfo *aMessageInfo, - otError aResult); +#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + static void HandleRegisterMulticastListenersResponse(void * aContext, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aResult); + void HandleRegisterMulticastListenersResponse(otMessage * aMessage, + const otMessageInfo *aMessageInfo, + otError aResult); +#endif #if OPENTHREAD_CONFIG_MLR_ENABLE void UpdateLocalSubscriptions(void); @@ -156,17 +201,26 @@ private: void Reregister(void); void HandleTimeTick(void); - void LogMulticastAddresses(void); - void CheckInvariants(void) const; - void LogMlrResponse(otError aResult, - otError aError, - uint8_t aStatus, - const Ip6::Address *aFailedAddresses, - uint8_t aFailedAddressNum); + void LogMulticastAddresses(void); + void CheckInvariants(void) const; + static void LogMlrResponse(otError aResult, + otError aError, + uint8_t aStatus, + const Ip6::Address *aFailedAddresses, + uint8_t aFailedAddressNum); + +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + otIp6RegisterMulticastListenersCallback mRegisterMulticastListenersCallback; + void * mRegisterMulticastListenersContext; +#endif uint32_t mReregistrationDelay; uint16_t mSendDelay; - bool mMlrPending : 1; + + bool mMlrPending : 1; +#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE + bool mRegisterMulticastListenersPending : 1; +#endif }; } // namespace ot diff --git a/src/core/thread/thread_tlvs.hpp b/src/core/thread/thread_tlvs.hpp index 3d986bf51..c82b15909 100644 --- a/src/core/thread/thread_tlvs.hpp +++ b/src/core/thread/thread_tlvs.hpp @@ -69,17 +69,19 @@ public: */ enum Type { - kTarget = 0, ///< Target EID TLV - kExtMacAddress = 1, ///< Extended MAC Address TLV - kRloc16 = 2, ///< RLOC16 TLV - kMeshLocalEid = 3, ///< ML-EID TLV - kStatus = 4, ///< Status TLV - kLastTransactionTime = 6, ///< Time Since Last Transaction TLV - kRouterMask = 7, ///< Router Mask TLV - kNDOption = 8, ///< ND Option TLV - kNDData = 9, ///< ND Data TLV - kThreadNetworkData = 10, ///< Thread Network Data TLV - kIPv6Addresses = 14, ///< IPv6 Addresses TLV + kTarget = 0, ///< Target EID TLV + kExtMacAddress = 1, ///< Extended MAC Address TLV + kRloc16 = 2, ///< RLOC16 TLV + kMeshLocalEid = 3, ///< ML-EID TLV + kStatus = 4, ///< Status TLV + kLastTransactionTime = 6, ///< Time Since Last Transaction TLV + kRouterMask = 7, ///< Router Mask TLV + kNDOption = 8, ///< ND Option TLV + kNDData = 9, ///< ND Data TLV + kThreadNetworkData = 10, ///< Thread Network Data TLV + kTimeout = 11, ///< Timeout TLV + kIPv6Addresses = 14, ///< IPv6 Addresses TLV + kCommissionerSessionId = 15, ///< Commissioner Session ID TLV }; /** diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 97c09580e..caff791e0 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -281,6 +281,37 @@ class Node: return results + def _expect_command_output(self, cmd: str): + lines = [] + cmd_output_started = False + + while True: + self._expect(r"[^\n]+") + line = self.pexpect.match.group(0).decode('utf8').strip() + + if line.startswith('> '): + line = line[2:] + + if line == '': + continue + + if line == cmd: + cmd_output_started = True + continue + + if not cmd_output_started: + continue + + if line == 'Done': + break + elif line.startswith('Error '): + raise Exception(line) + else: + lines.append(line) + + print(f'_expect_command_output({cmd!r}) returns {lines!r}') + return lines + def __init_soc(self, nodeid): """ Initialize a System-on-a-chip node connected via UART. """ import fdpexpect @@ -564,6 +595,24 @@ class Node: self.send_command(cmd) self._expect('Done') + def register_multicast_listener(self, *ipaddrs: Union[ipaddress.IPv6Address, str], timeout=None): + assert len(ipaddrs) > 0, ipaddrs + + cmd = f'mlr reg {" ".join(ipaddrs)}' + if timeout is not None: + cmd += f' {int(timeout)}' + self.send_command(cmd) + self.simulator.go(3) + lines = self._expect_command_output(cmd) + m = re.match(r'status (\d+), (\d+) failed', lines[0]) + assert m is not None, lines + status = int(m.group(1)) + failed_num = int(m.group(2)) + assert failed_num == len(lines) - 1 + failed_ips = list(map(ipaddress.IPv6Address, lines[1:])) + print(f"register_multicast_listener {ipaddrs} => status: {status}, failed ips: {failed_ips}") + return status, failed_ips + def set_link_quality(self, addr, lqi): cmd = 'macfilter rss add-lqi %s %s' % (addr, lqi) self.send_command(cmd) diff --git a/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py b/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py index f32952e87..c6df7808c 100755 --- a/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py +++ b/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py @@ -339,6 +339,104 @@ class TestMulticastListenerRegistration(thread_cert.TestCase): self.simulator.go(WAIT_REDUNDANCE) self.flush_all() + def testCommissionerRegisterMulticastListeners(self): + self._bootstrap() + + # Use ROUTER_1_2 as the Commissioner + commissioiner = self.nodes[ROUTER_1_2] + + self.assertRaisesRegex(Exception, "InvalidState", lambda: commissioiner.register_multicast_listener("ff04::1")) + + commissioiner.commissioner_start() + self.simulator.go(10) + + # Now the Commissioner should be able to register MAs + for ip in ["ff04::1", "ff04::2"]: + status, failed_ips = commissioiner.register_multicast_listener(ip) + self.assertTrue(status == 0 and not failed_ips) + self.__check_multicast_listener(ip, expect_mlr_timeout_range=[290, 300]) + + # Register existing MA with a new timeout should be able to update the timeout + for ip in ["ff04::1", "ff04::2"]: + status, failed_ips = commissioiner.register_multicast_listener(ip, timeout=1000) + self.assertTrue(status == 0 and not failed_ips) + self.__check_multicast_listener(ip, expect_mlr_timeout_range=[990, 1000]) + + # Register MAs with given timeouts + for ip, timeout in [("ff05::1", 400), ("ff05::2", 500), ("ff05::3", 600)]: + status, failed_ips = commissioiner.register_multicast_listener(ip, timeout=timeout) + self.assertTrue(status == 0 and not failed_ips) + self.__check_multicast_listener(ip, expect_mlr_timeout_range=[timeout - 10, timeout]) + + # Register multiple MAs with one call + ips = ["ff05::4", "ff05::5", "ff05::6"] + status, failed_ips = commissioiner.register_multicast_listener(*ips, timeout=700) + self.assertTrue(status == 0 and not failed_ips) + for ip in ips: + self.__check_multicast_listener(ip, expect_mlr_timeout_range=[690, 700]) + + # Register multiple MAs with one call (without timeout) + ips = ["ff05::7", "ff05::8", "ff05::9"] + status, failed_ips = commissioiner.register_multicast_listener(*ips) + self.assertTrue(status == 0 and not failed_ips) + for ip in ips: + self.__check_multicast_listener(ip, expect_mlr_timeout_range=[290, 300]) + + # Unregister MAs using timeout=0 + for ip in ["ff05::1", "ff05::2", "ff05::3"]: + status, failed_ips = commissioiner.register_multicast_listener(ip, timeout=0) + self.assertTrue(status == 0 and not failed_ips) + self.__check_multicast_listener(ip, expect_not_present=True) + + # Unregister multiple MAs + ips = ["ff05::4", "ff05::5", "ff05::6"] + status, failed_ips = commissioiner.register_multicast_listener(*ips, timeout=0) + self.assertTrue(status == 0 and not failed_ips) + self.__check_multicast_listener(ip, expect_not_present=True) + + # Remove MAs that are not subscribed should not fail + ips = ["ff06::1", "ff02::1", "2001::1"] + status, failed_ips = commissioiner.register_multicast_listener(*ips, timeout=0) + self.assertTrue(status == 0 and not failed_ips) + self.__check_multicast_listener(ip, expect_not_present=True) + + # Register invalid MAs should fail + for ip in ["ff02::1", "ff03::1", "2001::1"]: + status, failed_ips = commissioiner.register_multicast_listener(ip) + self.assertEqual(status, 2) + self.assertEqual(set(failed_ips), {ipaddress.IPv6Address(ip)}) + self.__check_multicast_listener(ip, expect_not_present=True) + + # Register valid MAs with invalid MAs should succeed partially + ips = ["ff05::1", "ff05::2", "ff02::1", "2001::1"] + status, failed_ips = commissioiner.register_multicast_listener(*ips) + self.assertEqual(status, 2) + self.assertEqual(set(failed_ips), {ipaddress.IPv6Address("ff02::1"), ipaddress.IPv6Address("2001::1")}) + self.__check_multicast_listener("ff05::1") + self.__check_multicast_listener("ff05::2") + self.__check_multicast_listener("ff02::1", expect_not_present=True) + self.__check_multicast_listener("2001::1", expect_not_present=True) + + # Registering persistent MAs should fail for now + status, failed_ips = commissioiner.register_multicast_listener("ff06::1", timeout=0xffffffff) + self.assertEqual(status, 3) + # "ff06::1" should not be included in failed IPs because all IPs failed + self.assertTrue(not failed_ips == 0) + + def __check_multicast_listener(self, *addrs, expect_mlr_timeout_range=None, expect_not_present=False): + addrs = map(ipaddress.IPv6Address, addrs) + listeners = self.nodes[BBR_1].multicast_listener_list() + logging.info("__check_multicast_listener get listeners: %s", listeners) + + for addr in addrs: + if not expect_not_present: + self.assertIn(addr, listeners) + if expect_mlr_timeout_range is not None: + self.assertGreaterEqual(listeners[addr], expect_mlr_timeout_range[0]) + self.assertLessEqual(listeners[addr], expect_mlr_timeout_range[1]) + else: + self.assertNotIn(addr, listeners) + def __test_multicast_listeners_table_api(self): self.assertTrue(self.nodes[BBR_1].multicast_listener_list() == {})