diff --git a/doc/ot_api_doc.h b/doc/ot_api_doc.h index 3863a5a73ea..ac6ec5a8359 100644 --- a/doc/ot_api_doc.h +++ b/doc/ot_api_doc.h @@ -172,8 +172,10 @@ * @defgroup plat-ble BLE * @defgroup plat-crypto Crypto - Platform * @defgroup plat-dns DNS - Platform + * @defgroup plat-dns-sd DNS-SD (mDNS) * @defgroup plat-entropy Entropy * @defgroup plat-factory-diagnostics Factory Diagnostics - Platform + * @defgroup plat-infra-if Infrastructure Interface * @defgroup plat-logging Logging - Platform * @defgroup plat-memory Memory * @defgroup plat-messagepool Message Pool @@ -186,7 +188,6 @@ * @defgroup plat-time Time Service * @defgroup plat-toolchain Toolchain * @defgroup plat-trel TREL - Platform - * @defgroup plat-infra-if Infrastructure Interface * * @} * diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 05176dea26f..bad9193d797 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -238,6 +238,7 @@ ot_option(OT_SERVICE OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE "Network Data ot_option(OT_SETTINGS_RAM OPENTHREAD_SETTINGS_RAM "volatile-only storage of settings") ot_option(OT_SLAAC OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE "SLAAC address") ot_option(OT_SNTP_CLIENT OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE "SNTP client") +ot_option(OT_SRP_ADV_PROXY OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE "SRP advertising proxy") ot_option(OT_SRP_CLIENT OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE "SRP client") ot_option(OT_SRP_SERVER OPENTHREAD_CONFIG_SRP_SERVER_ENABLE "SRP server") ot_option(OT_TCP OPENTHREAD_CONFIG_TCP_ENABLE "TCP") diff --git a/examples/platforms/simulation/CMakeLists.txt b/examples/platforms/simulation/CMakeLists.txt index 945583d70db..41a1dbff6e9 100644 --- a/examples/platforms/simulation/CMakeLists.txt +++ b/examples/platforms/simulation/CMakeLists.txt @@ -63,6 +63,7 @@ add_library(openthread-simulation crypto.c diag.c dns.c + dnssd.c dso_transport.c entropy.c flash.c diff --git a/examples/platforms/simulation/dnssd.c b/examples/platforms/simulation/dnssd.c new file mode 100644 index 00000000000..7967dd48715 --- /dev/null +++ b/examples/platforms/simulation/dnssd.c @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023, 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. + */ + +#include "platform-simulation.h" + +#include + +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE +otPlatDnssdState otPlatDnssdGetState(otInstance *aInstance) +{ + OT_UNUSED_VARIABLE(aInstance); + + return OT_PLAT_DNSSD_STOPPED; +} + +void otPlatDnssdRegisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aService); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +void otPlatDnssdUnregisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aService); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +void otPlatDnssdRegisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aHost); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +void otPlatDnssdUnregisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aHost); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +void otPlatDnssdRegisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aKey); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +void otPlatDnssdUnregisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aKey); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn index f779443aab9..5948fb73a39 100644 --- a/include/openthread/BUILD.gn +++ b/include/openthread/BUILD.gn @@ -91,6 +91,7 @@ source_set("openthread") { "platform/debug_uart.h", "platform/diag.h", "platform/dns.h", + "platform/dnssd.h", "platform/dso_transport.h", "platform/entropy.h", "platform/flash.h", diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 8eeb08f431a..3351997079a 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 (394) +#define OPENTHREAD_API_VERSION (395) /** * @addtogroup api-instance diff --git a/include/openthread/platform/dnssd.h b/include/openthread/platform/dnssd.h new file mode 100644 index 00000000000..e7a2a7f0b0f --- /dev/null +++ b/include/openthread/platform/dnssd.h @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2023, 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 + * @brief + * This file includes the platform abstraction for DNS-SD (e.g., mDNS) on the infrastructure network. + * + */ + +#ifndef OPENTHREAD_PLATFORM_DNSSD_H_ +#define OPENTHREAD_PLATFORM_DNSSD_H_ + +#include + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup plat-dns-sd + * + * @brief + * This module includes the platform abstraction for DNS-SD (e.g., mDNS) on the infrastructure network. + * + * @{ + * + * The DNS-SD platform APIs are used only when `OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE` is enabled. + * + */ + +/** + * Represents the state of the DNS-SD platform. + * + */ +typedef enum otPlatDnssdState +{ + OT_PLAT_DNSSD_STOPPED, ///< Stopped and unable to register any service or host. + OT_PLAT_DNSSD_READY, ///< Running and ready to register service or host. +} otPlatDnssdState; + +/** + * Represents a request ID for registering/unregistering a service or host. + * + */ +typedef uint32_t otPlatDnssdRequestId; + +/** + * Represents the callback function used when registering/unregistering a host or service. + * + * See `otPlatDnssdRegisterService()`, `otPlatDnssdUnregisterService()`, `otPlatDnssdRegisterHost()`, and + * `otPlatDnssdUnregisterHost()` for more details about when to invoke the callback and the `aError` values that can + * be returned in each case. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aRequestId The request ID. + * @param[in] aError Error indicating the outcome of request. + * + */ +typedef void (*otPlatDnssdRegisterCallback)(otInstance *aInstance, otPlatDnssdRequestId aRequestId, otError aError); + +/** + * Represents a DNS-SD service. + * + * See `otPlatDnssdRegisterService()`, `otPlatDnssdUnregisterService()` for more details about fields in each case. + * + */ +typedef struct otPlatDnssdService +{ + const char *mHostName; ///< The host name (does not include domain name). + const char *mServiceInstance; ///< The service instance name label (not the full name). + const char *mServiceType; ///< The service type (e.g., "_mt._udp", does not include domain name). + const char *const *mSubTypeLabels; ///< Array of sub-type labels (can be NULL if no label). + uint16_t mSubTypeLabelsLength; ///< Length of array of sub-type labels. + const uint8_t *mTxtData; ///< Encoded TXT data bytes. + uint16_t mTxtDataLength; ///< Length of TXT data. + uint16_t mPort; ///< The service port number. + uint16_t mPriority; ///< The service priority. + uint16_t mWeight; ///< The service weight. + uint32_t mTtl; ///< The service TTL in seconds. + uint32_t mInfraIfIndex; ///< The infrastructure network interface index. +} otPlatDnssdService; + +/** + * Represents a DNS-SD host. + * + * See `otPlatDnssdRegisterHost()`, `otPlatDnssdUnregisterHost()` for more details about fields in each case. + * + */ +typedef struct otPlatDnssdHost +{ + const char *mHostName; ///< The host name (does not include domain name). + const otIp6Address *mAddresses; ///< Array of IPv6 host addresses. + uint16_t mAddressesLength; ///< Number of entries in @p mAddresses array. + uint32_t mTtl; ///< The host TTL in seconds. + uint32_t mInfraIfIndex; ///< The infrastructure network interface index. +} otPlatDnssdHost; + +/** + * Represents a DNS-SD key record. + * + * See `otPlatDnssdRegisterKey()`, `otPlatDnssdUnregisterKey()` for more details about fields in each case. + * + */ +typedef struct otPlatDnssdKey +{ + const char *mName; ///< A host or a service instance name (does not include domain name). + const char *mServiceType; ///< The service type if key is for a service (does not include domain name). + const uint8_t *mKeyData; ///< Byte array containing the key record data. + uint16_t mKeyDataLength; ///< Length of @p mKeyData in bytes. + uint16_t mClass; ///< The resource record class. + uint32_t mTtl; ///< The TTL in seconds. + uint32_t mInfraIfIndex; ///< The infrastructure network interface index. +} otPlatDnssdKey; + +/** + * Callback to notify state changes of the DNS-SD platform. + * + * The OpenThread stack will call `otPlatDnssdGetState()` (from this callback or later) to get the new state. The + * platform MUST therefore ensure that the returned state from `otPlatDnssdGetState()` is updated before calling this. + * + * @param[in] aInstance The OpenThread instance structure. + * + */ +extern void otPlatDnssdStateHandleStateChange(otInstance *aInstance); + +/** + * Gets the current state of the DNS-SD module. + * + * The platform MUST notify the OpenThread stack whenever its state gets changed by invoking + * `otPlatDnssdStateHandleStateChange()`. + * + * @param[in] aInstance The OpenThread instance. + * + * @returns The current state of the DNS-SD module. + * + */ +otPlatDnssdState otPlatDnssdGetState(otInstance *aInstance); + +/** + * Registers or updates a service on the infrastructure network's DNS-SD module. + * + * The @p aService and all its contained information (strings and buffers) are only valid during this call. The + * platform MUST save a copy of the information if it wants to retain the information after returning from this + * function. + * + * The fields in @p aService follow these rules: + * + * - The `mServiceInstance` and `mServiceType` fields specify the service instance label and service type name, + * respectively. They are never NULL. + * - The `mHostName` field specifies the host name of the service if it is not NULL. Otherwise, if it is NULL, it + * indicates that this service is for the device itself and leaves the host name selection to DNS-SD platform. + * - The `mSubTypeLabels` is an array of strings representing sub-types associated with the service. It can be NULL + * if there are no sub-types. Otherwise, the array length is specified by `mSubTypeLabelsLength`. + * - The `mTxtData` and `mTxtDataLength` fields specify the encoded TXT data. + * - The `mPort`, `mWeight`, and `mPriority` fields specify the service's parameters (as specified in DNS SRV record). + * - The `mTtl` field specifies the TTL if non-zero. If zero, the platform can choose the TTL to use. + * - The `mInfraIfIndex` field, if non-zero, specifies the infrastructure network interface index to use for this + * request. If zero, the platform implementation can decided the interface. + * + * When the `mHostName` field in @p aService is not NULL (indicating that this registration is on behalf of another + * host), the OpenThread stack will ensure that `otPlatDnssdRegisterHost()` is also called for the same host before any + * service registration requests for the same host. + * + * Once the registration request is finished, either successfully or failed, the platform reports the outcome by + * invoking the @p aCallback and passing the same @p aRequestId in the callback. The @p aCallback function pointer can + * be NULL, which indicates that the OpenThread stack does not need to be notified of the outcome of the request. + * If the outcome is determined, the platform implementation may invoke the @p aCallback before returning from this + * function. The OpenThread stack will ensure to handle such a situation. + * + * On success, the @p aCallback MUST be called (if non-NULL) with `OT_ERROR_NONE` as the `aError` input parameter. If + * the registration causes a name conflict on DNS-SD domain (the service instance name is already claimed by another + * host), the `OT_ERROR_DUPLICATED` error MUST be used. The platform implementation can use other `OT_ERROR` types for + * other types of errors. + * + * The platform implementation MUST not assume that the @p aRequestId used in subsequent requests will be different. + * OpenThread may reuse the same request ID again for a different request. + * + * The OpenThread stack will not register the same service (with no changes) that was registered successfully earlier. + * Therefore, the platform implementation does not need to check for duplicate/same service and can assume that calls + * to this function are either registering a new entry or changing some parameter in a previously registered item. As + * a result, these changes always need to be synced on the infrastructure DNS-SD module. + * + * The OpenThread stack does not require the platform implementation to always invoke the @p aCallback function. + * The OpenThread stack has its own mechanism to time out an aged request with no response. This relaxes the + * requirement for platform implementations. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aService Information about the service to register. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ +void otPlatDnssdRegisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback); + +/** + * Unregisters a service on the infrastructure network's DNS-SD module. + * + * The @p aService and all its contained information (strings and buffers) are only valid during this call. The + * platform MUST save a copy of the information if it wants to retain the information after returning from this + * function. + * + * The fields in @p aService follow these rules: + * + * - The `mServiceInstance` and `mServiceType` fields specify the service instance label and service type name, + * respectively. They are never NULL. + * - The `mHostName` field specifies the host name of the service if it is not NULL. Otherwise, if it is NULL, it + * indicates that this service is for the device itself and leaves the host name selection to DNS-SD platform. + * - The `mInfraIfIndex` field, if non-zero, specifies the infrastructure network interface index to use for this + * request. If zero, the platform implementation can decided the interface. + * - The rest of the fields in @p aService structure MUST be ignored in `otPlatDnssdUnregisterService()` call and may + * be set to zero by the OpenThread stack. + * + * Regarding the invocation of the @p aCallback and the reuse of the @p aRequestId, this function follows the same + * rules as described in `otPlatDnssdRegisterService()`. + * + * The OpenThread stack may request the unregistration of a service that was not previously registered, and the + * platform implementation MUST handle this case. In such a case, the platform can use either `OT_ERROR_NOT_FOUND` to + * indicate that there was no such registration, or `OT_ERROR_NONE` when invoking the @p aCallback function. The + * OpenThread stack will handle either case correctly. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aService Information about the service to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ +void otPlatDnssdUnregisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback); + +/** + * Registers or updates a host on the infrastructure network's DNS-SD module. + * + * The @p aHost and all its contained information (strings and arrays) are only valid during this call. The + * platform MUST save a copy of the information if it wants to retain the information after returning from this + * function. + * + * The fields in @p aHost follow these rules: + * + * - The `mHostName` field specifies the host name to register. It is never NULL. + * - The `mAddresses` field is an array of IPv6 addresses to register with the host. `mAddressesLength` field provides + * the number of entries in `mAddresses` array. The platform implementation MUST not filter or remove any of + * addresses in the list. + * The OpenThread stack will already ensure that the given addresses are externally reachable. For example, when + * registering host from an SRP registration, link-local or mesh-local addresses associated with the host which are + * intended for use within Thread mesh are not included in `mAddresses` array passed to this API. The `mAddresses` + * array can be empty with zero `mAddressesLength`. In such a case, the platform MUST stop advertising any addresses + * for this host name on the infrastructure DNS-SD. + * - The `mTtl` field specifies the TTL if non-zero. If zero, the platform can choose the TTL to use. + * - The `mInfraIfIndex` field, if non-zero, specifies the infrastructure network interface index to use for this + * request. If zero, the platform implementation can decided the interface. + * + * Regarding the invocation of the @p aCallback and the reuse of the @p aRequestId, this function follows the same + * rules as described in `otPlatDnssdRegisterService()`. + * + * The OpenThread stack will not register the same host (with no changes) that was registered successfully earlier. + * Therefore, the platform implementation does not need to check for duplicate/same host and can assume that calls + * to this function are either registering a new entry or changing some parameter in a previously registered item. As + * a result, these changes always need to be synced on the infrastructure DNS-SD module. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aHost Information about the host to register. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ +void otPlatDnssdRegisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback); + +/** + * Unregisters a host on the infrastructure network's DNS-SD module. + * + * The @p aHost and all its contained information (strings and arrays) are only valid during this call. The + * platform MUST save a copy of the information if it wants to retain the information after returning from this + * function. + * + * The fields in @p aHost follow these rules: + * + * - The `mHostName` field specifies the host name to unregister. It is never NULL. + * - The `mInfraIfIndex` field, if non-zero, specifies the infrastructure network interface index to use for this + * request. If zero, the platform implementation can decided the interface. + * - The rest of the fields in @p aHost structure MUST be ignored in `otPlatDnssdUnregisterHost()` call and may + * be set to zero by the OpenThread stack. + * + * Regarding the invocation of the @p aCallback and the reuse of the @p aRequestId, this function follows the same + * rules as described in `otPlatDnssdRegisterService()`. + * + * The OpenThread stack may request the unregistration of a host that was not previously registered, and the platform + * implementation MUST handle this case. In such a case, the platform can use either `OT_ERROR_NOT_FOUND` to indicate + * that there was no such registration, or `OT_ERROR_NONE` when invoking the @p aCallback function. OpenThread stack + * will handle either case correctly. + * + * When unregistering a host, the OpenThread stack will also unregister any previously registered services + * associated with the same host (by calling `otPlatDnssdUnregisterService()`). However, the platform implementation + * MAY assume that unregistering a host also unregisters all its associated services. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aHost Information about the host to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ +void otPlatDnssdUnregisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback); + +/** + * Registers or updates a key record on the infrastructure network's DNS-SD module. + * + * The @p aKey and all its contained information (strings and arrays) are only valid during this call. The + * platform MUST save a copy of the information if it wants to retain the information after returning from this + * function. + * + * The fields in @p aKey follow these rules: + * + * - If the key is associated with a host, `mName` field specifies the host name and `mServiceType` will be NULL. + * - If the key is associated with a service, `mName` field specifies the service instance label and `mServiceType` + * field specifies the service type. In this case the DNS name for key record is `{mName}.{mServiceTye}`. + * - The `mKeyData` field contains the key record's data with `mKeyDataLength` as its length in byes. It is never NULL. + * - The `mClass` fields specifies the resource record class to use when registering key record. + * - The `mTtl` field specifies the TTL if non-zero. If zero, the platform can choose the TTL to use. + * - The `mInfraIfIndex` field, if non-zero, specifies the infrastructure network interface index to use for this + * request. If zero, the platform implementation can decided the interface. + * + * Regarding the invocation of the @p aCallback and the reuse of the @p aRequestId, this function follows the same + * rules as described in `otPlatDnssdRegisterService()`. + * + * The OpenThread stack will not register the same key (with no changes) that was registered successfully earlier. + * Therefore, the platform implementation does not need to check for duplicate/same name and can assume that calls + * to this function are either registering a new key or changing the key data in a previously registered one. As + * a result, these changes always need to be synced on the infrastructure DNS-SD module. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aHost Information about the key record to register. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ +void otPlatDnssdRegisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback); + +/** + * Unregisters a key record on the infrastructure network's DNS-SD module. + * + * The @p aKey and all its contained information (strings and arrays) are only valid during this call. The + * platform MUST save a copy of the information if it wants to retain the information after returning from this + * function. + * + * The fields in @p aKey follow these rules: + * + * - If the key is associated with a host, `mName` field specifies the host name and `mServiceType` will be NULL. + * - If the key is associated with a service, `mName` field specifies the service instance label and `mServiceType` + * field specifies the service type. In this case the DNS name for key record is `{mName}.{mServiceTye}`. + * - The `mInfraIfIndex` field, if non-zero, specifies the infrastructure network interface index to use for this + * request. If zero, the platform implementation can decided the interface. + * - The rest of the fields in @p aKey structure MUST be ignored in `otPlatDnssdUnregisterKey()` call and may + * be set to zero by the OpenThread stack. + * + * Regarding the invocation of the @p aCallback and the reuse of the @p aRequestId, this function follows the same + * rules as described in `otPlatDnssdRegisterService()`. + * + * The OpenThread stack may request the unregistration of a key that was not previously registered, and the platform + * implementation MUST handle this case. In such a case, the platform can use either `OT_ERROR_NOT_FOUND` to indicate + * that there was no such registration, or `OT_ERROR_NONE` when invoking the @p aCallback function. the OpenThread + * stack will handle either case correctly. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aKey Information about the key to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ +void otPlatDnssdUnregisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback); + +/** + * @} + * + */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // OPENTHREAD_PLATFORM_DNSSD_H_ diff --git a/script/check-scan-build b/script/check-scan-build index 7e6ad30ff55..2e1463c9443 100755 --- a/script/check-scan-build +++ b/script/check-scan-build @@ -74,6 +74,7 @@ OT_BUILD_OPTIONS=( "-DOT_SERVICE=ON" "-DOT_SLAAC=ON" "-DOT_SNTP_CLIENT=ON" + "-DOT_SRP_ADV_PROXY=ON" "-DOT_SRP_CLIENT=ON" "-DOT_SRP_SERVER=ON" "-DOT_VENDOR_NAME=OpenThread" diff --git a/script/make-pretty b/script/make-pretty index 086ba4c04ce..491d4acb91f 100755 --- a/script/make-pretty +++ b/script/make-pretty @@ -131,6 +131,7 @@ OT_CLANG_TIDY_BUILD_OPTS=( '-DOT_SERVICE=ON' '-DOT_SLAAC=ON' '-DOT_SNTP_CLIENT=ON' + '-DOT_SRP_ADV_PROXY=ON' '-DOT_SRP_CLIENT=ON' '-DOT_SRP_SERVER=ON' '-DOT_THREAD_VERSION=1.3' diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index f4fa5db94a5..a866cd0d4ba 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -553,6 +553,8 @@ openthread_core_files = [ "net/dns_platform.cpp", "net/dns_types.cpp", "net/dns_types.hpp", + "net/dnssd.cpp", + "net/dnssd.hpp", "net/dnssd_server.cpp", "net/dnssd_server.hpp", "net/icmp6.cpp", @@ -582,6 +584,8 @@ openthread_core_files = [ "net/sntp_client.hpp", "net/socket.cpp", "net/socket.hpp", + "net/srp_advertising_proxy.cpp", + "net/srp_advertising_proxy.hpp", "net/srp_client.cpp", "net/srp_client.hpp", "net/srp_server.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 24ceb22715b..5e6cee13567 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -168,6 +168,7 @@ set(COMMON_SOURCES net/dns_dso.cpp net/dns_platform.cpp net/dns_types.cpp + net/dnssd.cpp net/dnssd_server.cpp net/icmp6.cpp net/ip4_types.cpp @@ -182,6 +183,7 @@ set(COMMON_SOURCES net/netif.cpp net/sntp_client.cpp net/socket.cpp + net/srp_advertising_proxy.cpp net/srp_client.cpp net/srp_server.cpp net/tcp6.cpp diff --git a/src/core/border_router/infra_if.cpp b/src/core/border_router/infra_if.cpp index 07b5dc50931..a152e620468 100644 --- a/src/core/border_router/infra_if.cpp +++ b/src/core/border_router/infra_if.cpp @@ -152,6 +152,10 @@ Error InfraIf::HandleStateChanged(uint32_t aIfIndex, bool aIsRunning) Get().HandleInfraIfStateChanged(); +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + Get().HandleInfraIfStateChanged(); +#endif + exit: return error; } diff --git a/src/core/common/heap_data.hpp b/src/core/common/heap_data.hpp index f0b0823ed22..2e3118ffbc7 100644 --- a/src/core/common/heap_data.hpp +++ b/src/core/common/heap_data.hpp @@ -181,6 +181,17 @@ class Data */ bool Matches(const uint8_t *aBuffer, uint16_t aLength) const; + /** + * Overloads operator `==` to compare the `Data` content with the content from another one. + * + * @param[in] aOtherData The other `Data` to compare with. + * + * @retval TRUE The two `Data` instances have matching content (same length and same bytes). + * @retval FALSE The two `Data` instances do not have matching content. + * + */ + bool operator==(const Data &aOtherData) const { return mData == aOtherData.mData; } + /** * Frees any buffer allocated by the `Heap::Data`. * diff --git a/src/core/config/platform.h b/src/core/config/platform.h index 927abb0864b..a3ebf231c8f 100644 --- a/src/core/config/platform.h +++ b/src/core/config/platform.h @@ -35,6 +35,8 @@ #ifndef CONFIG_PLATFORM_H_ #define CONFIG_PLATFORM_H_ +#include "config/srp_server.h" + /** * @addtogroup config-platform * @@ -107,6 +109,16 @@ #define OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + * + * Define as 1 to enable DNSSD (mDNS) platform module. + * + */ +#ifndef OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE +#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE +#endif + /** * @def OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE * diff --git a/src/core/config/srp_server.h b/src/core/config/srp_server.h index 0ae6ef9a946..7e6cd18d2ec 100644 --- a/src/core/config/srp_server.h +++ b/src/core/config/srp_server.h @@ -119,6 +119,16 @@ #define OPENTHREAD_CONFIG_SRP_SERVER_SERVICE_UPDATE_TIMEOUT ((4 * 250u) + 250u) #endif +/** + * @def OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + * + * Define to 1 to enable Advertising Proxy for SRP Sever. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE +#define OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE 0 +#endif + /** * @} * diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index 81e3324a1e1..667741b7bef 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -87,6 +87,11 @@ Instance::Instance(void) , mSettings(*this) , mSettingsDriver(*this) , mMessagePool(*this) +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + // DNS-SD (mDNS) platform is initialized early to + // allow other modules to use it. + , mDnssd(*this) +#endif , mIp6(*this) , mThreadNetif(*this) , mTmfAgent(*this) @@ -185,6 +190,9 @@ Instance::Instance(void) #endif #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE , mSrpServer(*this) +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + , mSrpAdvertisingProxy(*this) +#endif #endif #if OPENTHREAD_FTD , mChildSupervisor(*this) diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index efde7fffbc1..b98b8d8ed23 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -88,6 +88,7 @@ #include "net/dhcp6_server.hpp" #include "net/dns_client.hpp" #include "net/dns_dso.hpp" +#include "net/dnssd.hpp" #include "net/dnssd_server.hpp" #include "net/ip6.hpp" #include "net/ip6_filter.hpp" @@ -95,6 +96,7 @@ #include "net/nd_agent.hpp" #include "net/netif.hpp" #include "net/sntp_client.hpp" +#include "net/srp_advertising_proxy.hpp" #include "net/srp_client.hpp" #include "net/srp_server.hpp" #include "radio/ble_secure.hpp" @@ -458,6 +460,12 @@ class Instance : public otInstance, private NonCopyable SettingsDriver mSettingsDriver; MessagePool mMessagePool; +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + // DNS-SD (mDNS) platform is initialized early to + // allow other modules to use it. + Dnssd mDnssd; +#endif + Ip6::Ip6 mIp6; ThreadNetif mThreadNetif; Tmf::Agent mTmfAgent; @@ -584,6 +592,9 @@ class Instance : public otInstance, private NonCopyable #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE Srp::Server mSrpServer; +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + Srp::AdvertisingProxy mSrpAdvertisingProxy; +#endif #endif #if OPENTHREAD_FTD @@ -861,6 +872,10 @@ template <> inline EnergyScanClient &Instance::Get(void) { return mCommissioner. template <> inline PanIdQueryClient &Instance::Get(void) { return mCommissioner.GetPanIdQueryClient(); } #endif +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE +template <> inline Dnssd &Instance::Get(void) { return mDnssd; } +#endif + #if OPENTHREAD_CONFIG_JOINER_ENABLE template <> inline MeshCoP::Joiner &Instance::Get(void) { return mJoiner; } #endif @@ -1012,7 +1027,7 @@ template <> inline Utils::Otns &Instance::Get(void) { return mOtns; } template <> inline BorderRouter::RoutingManager &Instance::Get(void) { return mRoutingManager; } template <> inline BorderRouter::InfraIf &Instance::Get(void) { return mRoutingManager.mInfraIf; } -#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE +#endif #if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE template <> inline Nat64::Translator &Instance::Get(void) { return mNat64Translator; } @@ -1020,7 +1035,10 @@ template <> inline Nat64::Translator &Instance::Get(void) { return mNat64Transla #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE template <> inline Srp::Server &Instance::Get(void) { return mSrpServer; } +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE +template <> inline Srp::AdvertisingProxy &Instance::Get(void) { return mSrpAdvertisingProxy; } #endif +#endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE template <> inline Ble::BleSecure &Instance::Get(void) { return mApplicationBleSecure; } diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp index 5428713aa27..e590a4831ce 100644 --- a/src/core/net/dns_types.hpp +++ b/src/core/net/dns_types.hpp @@ -2254,13 +2254,21 @@ class Ecdsa256KeyRecord : public KeyRecord, public Clearable, bool IsValid(void) const; /** - * Returns the ECDSA P-256 public kek. + * Returns the ECDSA P-256 public key. * * @returns A reference to the public key. * */ const Crypto::Ecdsa::P256::PublicKey &GetKey(void) const { return mKey; } + /** + * Sets the ECDSA P-256 public key. + * + * @param[in] aKey The public key. + * + */ + void SetKey(const Crypto::Ecdsa::P256::PublicKey &aKey) { mKey = aKey; } + private: Crypto::Ecdsa::P256::PublicKey mKey; } OT_TOOL_PACKED_END; diff --git a/src/core/net/dnssd.cpp b/src/core/net/dnssd.cpp new file mode 100644 index 00000000000..c795fa830b0 --- /dev/null +++ b/src/core/net/dnssd.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023, 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 infrastructure DNS-SD (mDNS) platform APIs. + */ + +#include "dnssd.hpp" + +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + +#include "common/code_utils.hpp" +#include "common/locator_getters.hpp" +#include "instance/instance.hpp" + +namespace ot { + +//--------------------------------------------------------------------------------------------------------------------- +// Dnssd::RequestIdRange + +void Dnssd::RequestIdRange::Add(RequestId aId) +{ + if (IsEmpty()) + { + mStart = aId; + mEnd = aId + 1; + } + else if (SerialNumber::IsLess(aId, mStart)) // Equivalent to (aId < mStart) + { + mStart = aId; + } + else if (!SerialNumber::IsLess(aId, mEnd)) // Equivalent to !(aId < mEd) -> (aId >= mEnd) + { + mEnd = aId + 1; + } +} + +void Dnssd::RequestIdRange::Remove(RequestId aId) +{ + if (!IsEmpty()) + { + if (aId == mStart) + { + mStart++; + } + else if (aId + 1 == mEnd) + { + mEnd--; + } + } +} + +bool Dnssd::RequestIdRange::Contains(RequestId aId) const +{ + // Equivalent to `(aId >= mStart) && (aId < mEnd)` + + return (!SerialNumber::IsLess(aId, mStart) && SerialNumber::IsLess(aId, mEnd)); +} + +//--------------------------------------------------------------------------------------------------------------------- +// Dnssd + +Dnssd::Dnssd(Instance &aInstance) + : InstanceLocator(aInstance) +{ +} + +Dnssd::State Dnssd::GetState(void) const { return MapEnum(otPlatDnssdGetState(&GetInstance())); } + +void Dnssd::RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback) +{ + if (IsReady()) + { + otPlatDnssdRegisterService(&GetInstance(), &aService, aRequestId, aCallback); + } +} + +void Dnssd::UnregisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback) +{ + if (IsReady()) + { + otPlatDnssdUnregisterService(&GetInstance(), &aService, aRequestId, aCallback); + } +} + +void Dnssd::RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback) +{ + if (IsReady()) + { + otPlatDnssdRegisterHost(&GetInstance(), &aHost, aRequestId, aCallback); + } +} + +void Dnssd::UnregisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback) +{ + if (IsReady()) + { + otPlatDnssdUnregisterHost(&GetInstance(), &aHost, aRequestId, aCallback); + } +} + +void Dnssd::RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback) +{ + if (IsReady()) + { + otPlatDnssdRegisterKey(&GetInstance(), &aKey, aRequestId, aCallback); + } +} + +void Dnssd::UnregisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback) +{ + if (IsReady()) + { + otPlatDnssdUnregisterKey(&GetInstance(), &aKey, aRequestId, aCallback); + } +} + +void Dnssd::HandleStateChange(void) +{ +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + Get().HandleDnssdPlatformStateChange(); +#endif +} + +extern "C" void otPlatDnssdStateHandleStateChange(otInstance *aInstance) +{ + AsCoreType(aInstance).Get().HandleStateChange(); +} + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE diff --git a/src/core/net/dnssd.hpp b/src/core/net/dnssd.hpp new file mode 100644 index 00000000000..dc6f6b6cad2 --- /dev/null +++ b/src/core/net/dnssd.hpp @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2023, 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 for infrastructure DNS-SD (mDNS) platform. + */ + +#ifndef DNSSD_HPP_ +#define DNSSD_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + +#include + +#include "common/clearable.hpp" +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "net/ip6_address.hpp" + +namespace ot { + +/** + * @addtogroup core-dns + * + * @brief + * This module includes definitions for DNS-SD (mDNS) platform. + * + * @{ + * + */ + +extern "C" void otPlatDnssdStateHandleStateChange(otInstance *aInstance); + +/** + * Represents DNS-SD (mDNS) platform. + * + */ +class Dnssd : public InstanceLocator, private NonCopyable +{ + friend void otPlatDnssdStateHandleStateChange(otInstance *aInstance); + +public: + /** + * Represents state of DNS-SD platform. + * + */ + enum State : uint8_t + { + kStopped = OT_PLAT_DNSSD_STOPPED, ///< Stopped and unable to register any service or host. + kReady = OT_PLAT_DNSSD_READY ///< Running and ready to register service or host. + }; + + typedef otPlatDnssdRequestId RequestId; ///< A request ID. + typedef otPlatDnssdRegisterCallback RegisterCallback; ///< The registration request callback + + class Host : public otPlatDnssdHost, public Clearable ///< Host information. + { + }; + + class Service : public otPlatDnssdService, public Clearable ///< Service information. + { + }; + + class Key : public otPlatDnssdKey, public Clearable ///< Key information + { + }; + + /** + * Represents a range of `RequestId` values. + * + * The range is stored using start and end ID values. The implementation handles the case when ID values roll over. + * + */ + struct RequestIdRange : public Clearable + { + /** + * Initializes a range as empty. + * + */ + RequestIdRange(void) + : mStart(0) + , mEnd(0) + { + } + + /** + * Adds a request ID to the range. + * + * @param[in] aId The ID to add to the range. + * + */ + void Add(RequestId aId); + + /** + * Removes a request ID from the range. + * + * @param[in] aId The ID to remove from the range. + * + */ + void Remove(RequestId aId); + + /** + * Indicates whether or not a given ID is contained within the range. + * + * @param[in] aId The ID to check. + * + * @retval TRUE The @p aID is contained within the range. + * @retval FALSE The @p aId is not contained within the range. + * + */ + bool Contains(RequestId aId) const; + + /** + * Indicates whether or not the range is empty. + * + * @retval TRUE The range is empty. + * @retval FALSE The range is not empty. + * + */ + bool IsEmpty(void) const { return (mStart == mEnd); } + + private: + // The range is represented as all `RequestId` values from + // `mStart` up to, but not including, `mEnd`. It uses serial + // number arithmetic logic when comparing `RequestId` values, + // so `Contains()` and other methods work correctly even when + // the ID value rolls over. + + RequestId mStart; + RequestId mEnd; + }; + + /** + * Initializes `Dnssd` object. + * + * @param[in] aInstance The OpenThread instance. + * + */ + explicit Dnssd(Instance &aInstance); + + /** + * Gets the current state of DNS-SD platform module. + * + * @returns The current state of DNS-SD platform. + * + */ + State GetState(void) const; + + /** + * Indicates whether or not DNS-SD platform is ready (in `kReady` state). + * + * @retval TRUE The DNS-SD platform is ready. + * @retval FALSE The DNS-SD platform is not ready. + * + */ + bool IsReady(void) const { return GetState() == kReady; } + + /** + * Registers or updates a service on the infrastructure network's DNS-SD module. + * + * Refer to the documentation for `otPlatDnssdRegisterService()`, for a more detailed description of the behavior + * of this method. + * + * @param[in] aService Information about service to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be `nullptr`). + * + */ + void RegisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback); + + /** + * Unregisters a service on the infrastructure network's DNS-SD module. + * + * Refer to the documentation for `otPlatDnssdUnregisterService()`, for a more detailed description of the behavior + * of this method. + * + * @param[in] aService Information about service to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be `nullptr`). + * + */ + void UnregisterService(const Service &aService, RequestId aRequestId, RegisterCallback aCallback); + + /** + * Registers or updates a host on the infrastructure network's DNS-SD module. + * + * Refer to the documentation for `otPlatDnssdRegisterHost()`, for a more detailed description of the behavior + * of this method. + * + * @param[in] aHost Information about host to register. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be `nullptr`). + * + */ + void RegisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback); + + /** + * Unregisters a host on the infrastructure network's DNS-SD module. + * + * Refer to the documentation for `otPlatDnssdUnregisterHost()`, for a more detailed description of the behavior + * of this method. + * + * @param[in] aHost Information about the host to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ + void UnregisterHost(const Host &aHost, RequestId aRequestId, RegisterCallback aCallback); + + /** + * Registers or updates a key record on the infrastructure network's DNS-SD module. + * + * Refer to the documentation for `otPlatDnssdRegisterKey()`, for a more detailed description of the behavior + * of this method. + * + * @param[in] aKey Information about the key to register. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be `nullptr`). + * + */ + void RegisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback); + + /** + * Unregisters a key record on the infrastructure network's DNS-SD module. + * + * Refer to the documentation for `otPlatDnssdUnregisterKey()`, for a more detailed description of the behavior + * of this method. + * + * @param[in] aKey Information about the key to unregister. + * @param[in] aRequestId The ID associated with this request. + * @param[in] aCallback The callback function pointer to report the outcome (may be NULL if no callback needed). + * + */ + void UnregisterKey(const Key &aKey, RequestId aRequestId, RegisterCallback aCallback); + +private: + void HandleStateChange(void); +}; + +/** + * @} + * + */ + +DefineMapEnum(otPlatDnssdState, Dnssd::State); +DefineCoreType(otPlatDnssdService, Dnssd::Service); +DefineCoreType(otPlatDnssdHost, Dnssd::Host); +DefineCoreType(otPlatDnssdKey, Dnssd::Key); + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + +#endif // DNSSD_HPP_ diff --git a/src/core/net/srp_advertising_proxy.cpp b/src/core/net/srp_advertising_proxy.cpp new file mode 100644 index 00000000000..8a2451d1f0b --- /dev/null +++ b/src/core/net/srp_advertising_proxy.cpp @@ -0,0 +1,1393 @@ +/* + * Copyright (c) 2023, 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 implementation of SRP Advertising Proxy. + */ + +#include "srp_advertising_proxy.hpp" + +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + +#include "common/as_core_type.hpp" +#include "common/debug.hpp" +#include "common/locator_getters.hpp" +#include "common/log.hpp" +#include "common/serial_number.hpp" +#include "common/type_traits.hpp" +#include "instance/instance.hpp" + +namespace ot { +namespace Srp { + +RegisterLogModule("SrpAdvProxy"); + +//--------------------------------------------------------------------------------------------------------------------- +// AdvertisingProxy + +AdvertisingProxy::AdvertisingProxy(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStateStopped) + , mCurrentRequestId(0) + , mAdvTimeout(kAdvTimeout) + , mTimer(aInstance) + , mTasklet(aInstance) +{ + mCounters.Clear(); +} + +void AdvertisingProxy::Start(void) +{ + VerifyOrExit(mState != kStateRunning); + + mState = kStateRunning; + mCounters.mStateChanges++; + LogInfo("Started"); + + // Advertise all existing and committed entries on SRP sever. + + for (Host &host : Get().mHosts) + { + LogInfo("Adv existing host '%s'", host.GetFullName()); + Advertise(host); + } + +exit: + return; +} + +void AdvertisingProxy::Stop(void) +{ + VerifyOrExit(mState != kStateStopped); + + mState = kStateStopped; + mCounters.mStateChanges++; + + while (true) + { + OwnedPtr advPtr = mAdvInfoList.Pop(); + + if (advPtr.IsNull()) + { + break; + } + + mCounters.mAdvRejected++; + + UnregisterHostAndItsServicesAndKeys(advPtr->mHost); + + advPtr->mError = kErrorAbort; + advPtr->mHost.mAdvIdRange.Clear(); + advPtr->mBlockingAdv = nullptr; + advPtr->SignalServerToCommit(); + } + + for (Host &host : Get().GetHosts()) + { + UnregisterHostAndItsServicesAndKeys(host); + + host.mAdvIdRange.Clear(); + host.mAdvId = kInvalidRequestId; + host.mIsRegistered = false; + + for (Service &service : host.mServices) + { + service.mAdvId = kInvalidRequestId; + service.mIsRegistered = false; + } + } + + LogInfo("Stopped"); + +exit: + return; +} + +void AdvertisingProxy::UpdateState(void) +{ + if (!Get().IsReady() || !Get().IsRunning()) + { + Stop(); + ExitNow(); + } + + switch (Get().GetState()) + { + case Server::kStateDisabled: + case Server::kStateStopped: + Stop(); + break; + + case Server::kStateRunning: + Start(); + break; + } + +exit: + return; +} + +AdvertisingProxy::RequestId AdvertisingProxy::AllocateNextRequestId(void) +{ + mCurrentRequestId++; + + if (kInvalidRequestId == mCurrentRequestId) + { + mCurrentRequestId++; + } + + return mCurrentRequestId; +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name) +template <> void AdvertisingProxy::UpdateAdvIdRangeOn(Host &aHost) +{ + // Determine and update `mAdvIdRange` on `aHost` based on + // `mAdvId` and `mKeyAdvId` of host and its services. + + aHost.mAdvIdRange.Clear(); + + for (const Service &service : aHost.mServices) + { + if (service.mKeyAdvId != kInvalidRequestId) + { + aHost.mAdvIdRange.Add(service.mKeyAdvId); + } + + if (service.mAdvId != kInvalidRequestId) + { + aHost.mAdvIdRange.Add(service.mAdvId); + } + } + + if (aHost.mKeyAdvId != kInvalidRequestId) + { + aHost.mAdvIdRange.Add(aHost.mKeyAdvId); + } + + if (aHost.mAdvId != kInvalidRequestId) + { + aHost.mAdvIdRange.Add(aHost.mAdvId); + } + + if (aHost.mAdvIdRange.IsEmpty()) + { + mTasklet.Post(); + } +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name) +template <> void AdvertisingProxy::UpdateAdvIdRangeOn(Service &aService) +{ + // Updates `mAdvIdRange` on the `Host` associated with + // `aService`. + + UpdateAdvIdRangeOn(*aService.mHost); +} + +void AdvertisingProxy::AdvertiseRemovalOf(Host &aHost) +{ + LogInfo("Adv removal of host '%s'", aHost.GetFullName()); + mCounters.mAdvHostRemovals++; + + VerifyOrExit(mState == kStateRunning); + VerifyOrExit(aHost.IsDeleted()); + + aHost.mShouldAdvertise = aHost.mIsRegistered; + + for (Service &service : aHost.mServices) + { + if (!service.mIsDeleted) + { + service.mIsDeleted = true; + } + + service.mShouldAdvertise = service.mIsRegistered; + } + + // Reject any outstanding `AdvInfo` that matches `aHost` that is + // being removed. + + for (AdvInfo &adv : mAdvInfoList) + { + Host &advHost = adv.mHost; + + if (!aHost.Matches(advHost.GetFullName()) || advHost.IsDeleted()) + { + continue; + } + + for (Service &advService : advHost.mServices) + { + Service *service; + + service = aHost.FindService(advService.GetInstanceName()); + + if (service == nullptr) + { + // `AdvInfo` contains a service that is not present in + // `aHost`, we unregister the service and its key. + + if (!advService.IsDeleted()) + { + UnregisterService(advService); + } + + UnregisterKey(advService); + } + else + { + service->mShouldAdvertise = true; + + if (aHost.mKeyLease == 0) + { + advService.mIsKeyRegistered = false; + } + } + + advService.mAdvId = kInvalidRequestId; + advService.mKeyAdvId = kInvalidRequestId; + advService.mIsReplaced = true; + } + + if (aHost.mKeyLease == 0) + { + advHost.mIsKeyRegistered = false; + } + + advHost.mAdvId = kInvalidRequestId; + advHost.mKeyAdvId = kInvalidRequestId; + advHost.mIsReplaced = true; + advHost.mAdvIdRange.Clear(); + + adv.mError = kErrorAbort; + mTasklet.Post(); + } + + for (Service &service : aHost.mServices) + { + if (service.mShouldAdvertise) + { + UnregisterService(service); + } + + if (aHost.mKeyLease == 0) + { + UnregisterKey(service); + } + } + + if (aHost.mShouldAdvertise) + { + UnregisterHost(aHost); + } + + if (aHost.mKeyLease == 0) + { + UnregisterKey(aHost); + } + +exit: + return; +} + +void AdvertisingProxy::AdvertiseRemovalOf(Service &aService) +{ + LogInfo("Adv removal of service '%s' '%s'", aService.GetInstanceLabel(), aService.GetServiceName()); + mCounters.mAdvServiceRemovals++; + + VerifyOrExit(mState == kStateRunning); + + aService.mShouldAdvertise = aService.mIsRegistered; + + // Check if any outstanding `AdvInfo` is re-adding the `aService` + // (which is being removed), and if so, skip unregistering the + // service and its key. + + for (const AdvInfo &adv : mAdvInfoList) + { + const Host &advHost = adv.mHost; + const Service *advService; + + if (!aService.mHost->Matches(advHost.GetFullName())) + { + continue; + } + + if (advHost.IsDeleted()) + { + break; + } + + advService = advHost.FindService(aService.GetInstanceName()); + + if ((advService != nullptr) && !advService->IsDeleted()) + { + ExitNow(); + } + } + + if (aService.mShouldAdvertise) + { + UnregisterService(aService); + } + + if (aService.mKeyLease == 0) + { + UnregisterKey(aService); + } + +exit: + return; +} + +void AdvertisingProxy::Advertise(Host &aHost, const Server::MessageMetadata &aMetadata) +{ + AdvInfo *advPtr = nullptr; + Host *existingHost; + + LogInfo("Adv update for '%s'", aHost.GetFullName()); + + mCounters.mAdvTotal++; + + VerifyOrExit(mState == kStateRunning); + + advPtr = AdvInfo::Allocate(aHost, aMetadata, mAdvTimeout); + VerifyOrExit(advPtr != nullptr); + mAdvInfoList.Push(*advPtr); + + // Compare the new `aHost` with outstanding advertisements and + // already committed entries on server. + + for (AdvInfo &adv : mAdvInfoList) + { + if (!aHost.Matches(adv.mHost.GetFullName())) + { + continue; + } + + if (CompareAndUpdateHostAndServices(aHost, adv.mHost)) + { + // If the new `aHost` replaces an entry in the outstanding + // `adv`, we mark the new advertisement as blocked so + // that it is not committed before the earlier one. This + // ensures that SRP Updates are committed in the order + // they are advertised, avoiding issues such as re-adding + // a removed entry due to a delay in registration on + // infra DNS-SD. + + if (advPtr->mBlockingAdv == nullptr) + { + mCounters.mAdvReplaced++; + advPtr->mBlockingAdv = &adv; + } + } + } + + existingHost = Get().mHosts.FindMatching(aHost.GetFullName()); + + if (existingHost != nullptr) + { + CompareAndUpdateHostAndServices(aHost, *existingHost); + } + + Advertise(aHost); + +exit: + if (advPtr != nullptr) + { + if (advPtr->IsCompleted()) + { + mTasklet.Post(); + } + else + { + mTimer.FireAtIfEarlier(advPtr->mExpireTime); + } + } + else + { + LogInfo("Adv skipped '%s'", aHost.GetFullName()); + mCounters.mAdvSkipped++; + Get().CommitSrpUpdate(kErrorNone, aHost, aMetadata); + } +} + +template bool AdvertisingProxy::IsKeyRegisteredOrRegistering(const Entry &aEntry) const +{ + return (aEntry.mIsKeyRegistered || (aEntry.mKeyAdvId != kInvalidRequestId)); +} + +template bool AdvertisingProxy::IsRegisteredOrRegistering(const Entry &aEntry) const +{ + return (aEntry.mIsRegistered || (aEntry.mAdvId != kInvalidRequestId)); +} + +template +void AdvertisingProxy::DecideToAdvertise(Entry &aEntry, bool aUnregisterEntry, bool aUnregisterKey) +{ + // Decides whether to advertise `aEntry` or register its key. + + if (!aUnregisterKey && !IsKeyRegisteredOrRegistering(aEntry)) + { + aEntry.mShouldRegisterKey = true; + aEntry.mKeyAdvId = AllocateNextRequestId(); + } + + VerifyOrExit(!aEntry.mShouldAdvertise); + + if (aUnregisterEntry || aEntry.IsDeleted()) + { + aEntry.mShouldAdvertise = aEntry.mIsRegistered; + } + else if (!IsRegisteredOrRegistering(aEntry)) + { + aEntry.mShouldAdvertise = true; + aEntry.mAdvId = AllocateNextRequestId(); + } + +exit: + return; +} + +void AdvertisingProxy::Advertise(Host &aHost) +{ + bool shouldUnregisterHostAndServices = aHost.IsDeleted(); + bool shouldUnregisterKeys = (aHost.mKeyLease == 0); + + DecideToAdvertise(aHost, shouldUnregisterHostAndServices, shouldUnregisterKeys); + + for (Service &service : aHost.mServices) + { + DecideToAdvertise(service, shouldUnregisterHostAndServices, shouldUnregisterKeys); + } + + // We call `UpdateAdvIdRangeOn()` to determine the `mAdvIdRange` + // on `aHost` before we call any of `UnregisterHost()`, + // `UnregisterService()`, or `UnregisterKey()` methods, and + // and receive any `HandleRegistered()` callbacks. The DNS-SD + // platform may invoke `HandleRegistered()` callbacks from within + // the `Register{Host/Service/Key}()` calls. + + UpdateAdvIdRangeOn(aHost); + + if (shouldUnregisterKeys) + { + UnregisterKey(aHost); + } + else if (aHost.mShouldRegisterKey) + { + RegisterKey(aHost); + } + + // We register host first before any of its services. + // But if we need to unregister host, it is done after + // all services. + + if (aHost.mShouldAdvertise && !shouldUnregisterHostAndServices) + { + RegisterHost(aHost); + } + + for (Service &service : aHost.mServices) + { + if (shouldUnregisterKeys) + { + UnregisterKey(service); + } + else if (service.mShouldRegisterKey) + { + RegisterKey(service); + } + + if (service.mShouldAdvertise) + { + if (shouldUnregisterHostAndServices || service.IsDeleted()) + { + UnregisterService(service); + } + else + { + RegisterService(service); + } + } + } + + if (aHost.mShouldAdvertise && shouldUnregisterHostAndServices) + { + UnregisterHost(aHost); + } +} + +void AdvertisingProxy::UnregisterHostAndItsServicesAndKeys(Host &aHost) +{ + for (Service &service : aHost.mServices) + { + if (service.mIsKeyRegistered) + { + UnregisterKey(service); + } + + if (!service.mIsReplaced && IsRegisteredOrRegistering(service)) + { + UnregisterService(service); + } + } + + if (aHost.mIsKeyRegistered) + { + UnregisterKey(aHost); + } + + if (!aHost.mIsReplaced && IsRegisteredOrRegistering(aHost)) + { + UnregisterHost(aHost); + } +} + +bool AdvertisingProxy::CompareAndUpdateHostAndServices(Host &aHost, Host &aExistingHost) +{ + // This method compares and updates flags used by `AdvertisingProxy` + // on new `aHost` and `aExistingHost` and their services. + // + // It returns a boolean indicating whether the new `aHost` replaced + // any of the entries on the `aExistingHost`. + // + // The `AdvertisingProxy` uses the following flags and variables + // on `Host` and `Service` entries: + // + // - `mIsRegistered` indicates whether or not the entry has been + // successfully registered by the proxy. + // + // - `mIsKeyRegistered` indicates whether or not a key record + // associated with the entry name has been successfully + // registered by the proxy on infrastructure DNS-SD. + // + // - `mAdvId` specifies the ongoing registration request ID + // associated with this entry by the proxy. A value of zero or + // `kInvalidRequestId` indicates that there is no ongoing + // registration for this entry. + // + // - `mKeyAdvId` is similar to `mAdvId` but for registering the + // key record. + // + // - `mIsReplaced` tracks whether this entry has been replaced by + // a newer advertisement request that changes some of its + // parameters. For example, the address list could have been + // changed on a `Host`, or TXT Data, or the list of sub-types, + // or port number could have been changed on a `Service`. + // + // - `mShouldAdvertise` is only used in the `Advertise()` call + // chain to track whether we need to advertise the entry. + // + // - `mShouldRegisterKey` is similar to `mShouldAdvertise` and + // only used in `Advertise()` call chain. + + bool replaced = false; + + VerifyOrExit(&aHost != &aExistingHost); + + replaced = CompareAndUpdateHost(aHost, aExistingHost); + + // Compare services of `aHost` against services of + // `aExistingHost`. + + for (Service &service : aHost.mServices) + { + Service *existingService = aExistingHost.mServices.FindMatching(service.GetInstanceName()); + + if (existingService != nullptr) + { + replaced |= CompareAndUpdateService(service, *existingService); + } + } + +exit: + return replaced; +} + +template void AdvertisingProxy::UpdateKeyRegistrationStatus(Entry &aEntry, const Entry &aExistingEntry) +{ + // Updates key registration status on `aEntry` based + // on its state on `aExistingEntry`. + + static_assert(TypeTraits::IsSame::kValue || TypeTraits::IsSame::kValue, + "`Entry` must be `Host` or `Service` types"); + + // If the new `aEntry` has a zero key lease, we always unregister + // it, just to be safe. Therefore, there is no need to check the + // key registration status of the existing `aExistingEntry`. + + VerifyOrExit(aEntry.GetKeyLease() != 0); + + VerifyOrExit(!IsKeyRegisteredOrRegistering(aEntry)); + + if (aExistingEntry.mIsKeyRegistered) + { + aEntry.mIsKeyRegistered = true; + } + else + { + // Use the key registration request ID by `aExistingEntry` for + // the new `aEntry` if there is any. If there is none the + // `mKeyAdvId` remains as `kInvalidRequestId`. + + aEntry.mKeyAdvId = aExistingEntry.mKeyAdvId; + } + +exit: + return; +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name) +template <> bool AdvertisingProxy::EntriesMatch(const Host &aFirstHost, const Host &aSecondHost) +{ + bool match = false; + + VerifyOrExit(aFirstHost.IsDeleted() == aSecondHost.IsDeleted()); + + if (aFirstHost.IsDeleted()) + { + match = true; + ExitNow(); + } + + VerifyOrExit(aFirstHost.mAddresses.GetLength() == aSecondHost.mAddresses.GetLength()); + + for (const Ip6::Address &address : aFirstHost.mAddresses) + { + VerifyOrExit(aSecondHost.mAddresses.Contains(address)); + } + + match = true; + +exit: + return match; +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name) +template <> bool AdvertisingProxy::EntriesMatch(const Service &aFirstService, const Service &aSecondService) +{ + bool match = false; + + VerifyOrExit(aFirstService.IsDeleted() == aSecondService.IsDeleted()); + + if (aFirstService.IsDeleted()) + { + match = true; + ExitNow(); + } + + VerifyOrExit(aFirstService.GetPort() == aSecondService.GetPort()); + VerifyOrExit(aFirstService.GetWeight() == aSecondService.GetWeight()); + VerifyOrExit(aFirstService.GetPriority() == aSecondService.GetPriority()); + VerifyOrExit(aFirstService.GetTtl() == aSecondService.GetTtl()); + + VerifyOrExit(aFirstService.GetNumberOfSubTypes() == aSecondService.GetNumberOfSubTypes()); + + for (uint16_t index = 0; index < aFirstService.GetNumberOfSubTypes(); index++) + { + VerifyOrExit(aSecondService.HasSubTypeServiceName(aFirstService.GetSubTypeServiceNameAt(index))); + } + + VerifyOrExit(aFirstService.GetTxtDataLength() == aSecondService.GetTxtDataLength()); + VerifyOrExit(!memcmp(aFirstService.GetTxtData(), aSecondService.GetTxtData(), aFirstService.GetTxtDataLength())); + + match = true; + +exit: + return match; +} + +template bool AdvertisingProxy::CompareAndUpdate(Entry &aEntry, Entry &aExistingEntry) +{ + // This is called when the new `aEntry` is not deleted. + + bool replaced = false; + + // If we previously determined that `aEntry` is registered, + // nothing else to do. + + VerifyOrExit(!aEntry.mIsRegistered); + + if (aEntry.mShouldAdvertise || aExistingEntry.mIsReplaced || !EntriesMatch(aEntry, aExistingEntry)) + { + // If we previously determined that we should advertise the + // new `aEntry`, we enter this block to mark `aExistingEntry` + // as being replaced. + // + // If `aExistingEntry` was already marked as replaced, we + // cannot compare it to the new `aEntry`. Therefore, we assume + // that there may be a change and always advertise the new + // `aEntry`. Otherwise, we compare it to the new `aEntry` using + // `EntriesMatch()` and only if there are any differences, we + // mark that `aEntry` needs to be advertised. + + aExistingEntry.mIsReplaced = true; + replaced = true; + + if (aEntry.mAdvId == kInvalidRequestId) + { + aEntry.mShouldAdvertise = true; + aEntry.mAdvId = AllocateNextRequestId(); + } + + // If there is an outstanding registration request for + // `aExistingEntry` we replace it with the request ID of the + // new `aEntry` registration. + + if (aExistingEntry.mAdvId != kInvalidRequestId) + { + aExistingEntry.mAdvId = aEntry.mAdvId; + UpdateAdvIdRangeOn(aExistingEntry); + } + + ExitNow(); + } + + // `aEntry` fully matches `aExistingEntry` and `aExistingEntry` was + // not replaced. + + VerifyOrExit(aEntry.mAdvId == kInvalidRequestId); + + if (aExistingEntry.mIsRegistered) + { + aEntry.mIsRegistered = true; + } + else if (aExistingEntry.mAdvId != kInvalidRequestId) + { + // There is an outstanding registration request for + // `aExistingEntry`. We use the same ID for the new `aEntry`. + + aEntry.mAdvId = aExistingEntry.mAdvId; + } + else + { + // The earlier advertisement of `aExistingEntry` seems to have + // failed since there is no outstanding registration request + // (no ID) and it is not marked as registered. We mark the + // new `aEntry` to be advertised (to try again). + + aEntry.mShouldAdvertise = true; + aEntry.mAdvId = AllocateNextRequestId(); + aExistingEntry.mIsReplaced = true; + } + +exit: + return replaced; +} + +bool AdvertisingProxy::CompareAndUpdateHost(Host &aHost, Host &aExistingHost) +{ + bool replaced = false; + + UpdateKeyRegistrationStatus(aHost, aExistingHost); + + if (!aHost.IsDeleted()) + { + replaced = CompareAndUpdate(aHost, aExistingHost); + ExitNow(); + } + + // The new `aHost` is removing the host and all its services. + + if (aExistingHost.IsDeleted()) + { + // If `aHost` has zero key-lease (fully removed), + // we need to unregister keys for any services on + // existing host that are not present in `aHost`. + + if (aHost.mKeyLease == 0) + { + for (Service &existingService : aExistingHost.mServices) + { + if (!aHost.HasService(existingService.GetInstanceName())) + { + UnregisterKey(existingService); + } + } + } + + ExitNow(); + } + + // `aExistingHost` is updating the same host that is being + // removed by the new `aHost` update. We need to advertise + // the new `aHost` to make sure it is unregistered. + + aHost.mShouldAdvertise = true; + + // We unregister any services that were registered by + // `aExistingHost` but are not included in the now being + // removed `aHost`, and unregister any registered keys when + // `aHost` has zero key lease. + + for (Service &existingService : aExistingHost.mServices) + { + if (existingService.IsDeleted()) + { + if (aHost.GetKeyLease() == 0) + { + existingService.mIsReplaced = true; + UnregisterKey(existingService); + } + + continue; + } + + if (aHost.HasService(existingService.GetInstanceName())) + { + // The `existingService` that are contained in `aHost` + // are updated in `CompareAndUpdateService()`. + continue; + } + + UnregisterService(existingService); + + existingService.mIsReplaced = true; + + if (aHost.GetKeyLease() == 0) + { + UnregisterKey(existingService); + } + } + + aExistingHost.mAdvId = kInvalidRequestId; + aExistingHost.mIsReplaced = true; + replaced = true; + + if (aHost.GetKeyLease() == 0) + { + UnregisterKey(aExistingHost); + } + + UpdateAdvIdRangeOn(aExistingHost); + +exit: + return replaced; +} + +bool AdvertisingProxy::CompareAndUpdateService(Service &aService, Service &aExistingService) +{ + bool replaced = false; + + UpdateKeyRegistrationStatus(aService, aExistingService); + + if (!aService.IsDeleted()) + { + replaced = CompareAndUpdate(aService, aExistingService); + ExitNow(); + } + + if (aExistingService.IsDeleted()) + { + ExitNow(); + } + + aService.mShouldAdvertise = true; + + aExistingService.mIsReplaced = true; + replaced = true; + + if (aExistingService.mAdvId != kInvalidRequestId) + { + // If there is an outstanding registration request for the + // existing service, clear its request ID. + + aExistingService.mAdvId = kInvalidRequestId; + + UpdateAdvIdRangeOn(*aExistingService.mHost); + } + +exit: + return replaced; +} + +void AdvertisingProxy::RegisterHost(Host &aHost) +{ + Error error = kErrorNone; + Dnssd::Host hostInfo; + DnsName hostName; + Heap::Array hostAddresses; + + aHost.mShouldAdvertise = false; + + CopyNameAndRemoveDomain(hostName, aHost.GetFullName()); + + SuccessOrExit(error = hostAddresses.ReserveCapacity(aHost.mAddresses.GetLength())); + + for (const Ip6::Address &address : aHost.mAddresses) + { + if (!address.IsLinkLocal() && !Get().IsMeshLocalAddress(address)) + { + IgnoreError(hostAddresses.PushBack(address)); + } + } + + LogInfo("Registering host '%s', id:%lu", hostName, ToUlong(aHost.mAdvId)); + + hostInfo.Clear(); + hostInfo.mHostName = hostName; + hostInfo.mAddresses = hostAddresses.AsCArray(); + hostInfo.mAddressesLength = hostAddresses.GetLength(); + hostInfo.mTtl = aHost.GetTtl(); + hostInfo.mInfraIfIndex = Get().GetIfIndex(); + + Get().RegisterHost(hostInfo, aHost.mAdvId, HandleRegistered); + +exit: + if (error != kErrorNone) + { + LogWarn("Error %s registering host '%s'", ErrorToString(error), hostName); + } +} + +void AdvertisingProxy::UnregisterHost(Host &aHost) +{ + Dnssd::Host hostInfo; + DnsName hostName; + + aHost.mShouldAdvertise = false; + aHost.mIsRegistered = false; + aHost.mAdvId = false; + + CopyNameAndRemoveDomain(hostName, aHost.GetFullName()); + + LogInfo("Unregistering host '%s'", hostName); + + hostInfo.Clear(); + hostInfo.mHostName = hostName; + hostInfo.mInfraIfIndex = Get().GetIfIndex(); + + Get().UnregisterHost(hostInfo, 0, nullptr); +} + +void AdvertisingProxy::RegisterService(Service &aService) +{ + Error error = kErrorNone; + Dnssd::Service serviceInfo; + DnsName hostName; + DnsName serviceName; + Heap::Array subTypeHeapStrings; + Heap::Array subTypeLabels; + + aService.mShouldAdvertise = false; + + CopyNameAndRemoveDomain(hostName, aService.GetHost().GetFullName()); + CopyNameAndRemoveDomain(serviceName, aService.GetServiceName()); + + SuccessOrExit(error = subTypeHeapStrings.ReserveCapacity(aService.mSubTypes.GetLength())); + SuccessOrExit(error = subTypeLabels.ReserveCapacity(aService.mSubTypes.GetLength())); + + for (const Heap::String &subTypeName : aService.mSubTypes) + { + char label[Dns::Name::kMaxLabelSize]; + Heap::String labelString; + + IgnoreError(Server::Service::ParseSubTypeServiceName(subTypeName.AsCString(), label, sizeof(label))); + SuccessOrExit(error = labelString.Set(label)); + IgnoreError(subTypeHeapStrings.PushBack(static_cast(labelString))); + IgnoreError(subTypeLabels.PushBack(subTypeHeapStrings.Back()->AsCString())); + } + + LogInfo("Registering service '%s' '%s' on '%s', id:%lu", aService.GetInstanceLabel(), serviceName, hostName, + ToUlong(aService.mAdvId)); + + serviceInfo.Clear(); + serviceInfo.mHostName = hostName; + serviceInfo.mServiceInstance = aService.GetInstanceLabel(); + serviceInfo.mServiceType = serviceName; + serviceInfo.mSubTypeLabels = subTypeLabels.AsCArray(); + serviceInfo.mSubTypeLabelsLength = subTypeLabels.GetLength(); + serviceInfo.mTxtData = aService.GetTxtData(); + serviceInfo.mTxtDataLength = aService.GetTxtDataLength(); + serviceInfo.mPort = aService.GetPort(); + serviceInfo.mWeight = aService.GetWeight(); + serviceInfo.mPriority = aService.GetPriority(); + serviceInfo.mTtl = aService.GetTtl(); + serviceInfo.mInfraIfIndex = Get().GetIfIndex(); + + Get().RegisterService(serviceInfo, aService.mAdvId, HandleRegistered); + +exit: + if (error != kErrorNone) + { + LogWarn("Error %s registering service '%s' '%s'", ErrorToString(error), aService.GetInstanceLabel(), + serviceName); + } +} + +void AdvertisingProxy::UnregisterService(Service &aService) +{ + Dnssd::Service serviceInfo; + DnsName hostName; + DnsName serviceName; + + aService.mShouldAdvertise = false; + aService.mIsRegistered = false; + aService.mAdvId = kInvalidRequestId; + + CopyNameAndRemoveDomain(hostName, aService.GetHost().GetFullName()); + CopyNameAndRemoveDomain(serviceName, aService.GetServiceName()); + + LogInfo("Unregistering service '%s' '%s' on '%s'", aService.GetInstanceLabel(), serviceName, hostName); + + serviceInfo.Clear(); + serviceInfo.mHostName = hostName; + serviceInfo.mServiceInstance = aService.GetInstanceLabel(); + serviceInfo.mServiceType = serviceName; + serviceInfo.mInfraIfIndex = Get().GetIfIndex(); + + Get().UnregisterService(serviceInfo, 0, nullptr); +} + +void AdvertisingProxy::RegisterKey(Host &aHost) +{ + DnsName hostName; + + aHost.mShouldRegisterKey = false; + + CopyNameAndRemoveDomain(hostName, aHost.GetFullName()); + + LogInfo("Registering key for host '%s', id:%lu", hostName, ToUlong(aHost.mKeyAdvId)); + + RegisterKey(hostName, /* aServiceType */ nullptr, aHost.mKey, aHost.mKeyAdvId, aHost.GetTtl()); +} + +void AdvertisingProxy::RegisterKey(Service &aService) +{ + DnsName serviceType; + + aService.mShouldRegisterKey = false; + + CopyNameAndRemoveDomain(serviceType, aService.GetServiceName()); + + LogInfo("Registering key for service '%s' '%s', id:%lu", aService.GetInstanceLabel(), serviceType, + ToUlong(aService.mKeyAdvId)); + + RegisterKey(aService.GetInstanceLabel(), serviceType, aService.mHost->mKey, aService.mKeyAdvId, aService.GetTtl()); +} + +void AdvertisingProxy::RegisterKey(const char *aName, + const char *aServiceType, + const Host::Key &aKey, + RequestId aRequestId, + uint32_t aTtl) +{ + Dnssd::Key keyInfo; + Dns::Ecdsa256KeyRecord keyRecord; + + keyRecord.Init(); + keyRecord.SetFlags(Dns::KeyRecord::kAuthConfidPermitted, Dns::KeyRecord::kOwnerNonZone, + Dns::KeyRecord::kSignatoryFlagGeneral); + keyRecord.SetProtocol(Dns::KeyRecord::kProtocolDnsSec); + keyRecord.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256); + keyRecord.SetLength(sizeof(Dns::Ecdsa256KeyRecord) - sizeof(Dns::ResourceRecord)); + keyRecord.SetKey(aKey); + + keyInfo.Clear(); + keyInfo.mName = aName; + keyInfo.mServiceType = aServiceType; + keyInfo.mKeyData = reinterpret_cast(&keyRecord) + sizeof(Dns::ResourceRecord); + keyInfo.mKeyDataLength = keyRecord.GetLength(); + keyInfo.mClass = Dns::ResourceRecord::kClassInternet; + keyInfo.mTtl = aTtl; + keyInfo.mInfraIfIndex = Get().GetIfIndex(); + + Get().RegisterKey(keyInfo, aRequestId, HandleRegistered); +} + +void AdvertisingProxy::UnregisterKey(Host &aHost) +{ + DnsName hostName; + + aHost.mIsKeyRegistered = false; + aHost.mKeyAdvId = kInvalidRequestId; + + CopyNameAndRemoveDomain(hostName, aHost.GetFullName()); + + LogInfo("Unregistering key for host '%s'", hostName); + + UnregisterKey(hostName, /* aServiceType */ nullptr); +} + +void AdvertisingProxy::UnregisterKey(Service &aService) +{ + DnsName serviceType; + + aService.mIsKeyRegistered = false; + aService.mKeyAdvId = kInvalidRequestId; + + CopyNameAndRemoveDomain(serviceType, aService.GetServiceName()); + + LogInfo("Unregistering key for service '%s' '%s'", aService.GetInstanceLabel(), serviceType); + + UnregisterKey(aService.GetInstanceLabel(), serviceType); +} + +void AdvertisingProxy::UnregisterKey(const char *aName, const char *aServiceType) +{ + Dnssd::Key keyInfo; + + keyInfo.Clear(); + keyInfo.mName = aName; + keyInfo.mServiceType = aServiceType; + keyInfo.mInfraIfIndex = Get().GetIfIndex(); + + Get().UnregisterKey(keyInfo, 0, nullptr); +} + +void AdvertisingProxy::CopyNameAndRemoveDomain(DnsName &aName, const char *aFullName) +{ + IgnoreError(Dns::Name::ExtractLabels(aFullName, Get().GetDomain(), aName, sizeof(aName))); +} + +void AdvertisingProxy::HandleRegistered(otInstance *aInstance, otPlatDnssdRequestId aRequestId, otError aError) +{ + AsCoreType(aInstance).Get().HandleRegistered(aRequestId, aError); +} + +void AdvertisingProxy::HandleRegistered(RequestId aRequestId, Error aError) +{ + LogInfo("Register callback, id:%lu, error:%s", ToUlong(aRequestId), ErrorToString(aError)); + + VerifyOrExit(mState == kStateRunning); + + for (Host &host : Get().mHosts) + { + HandleRegisteredRequestIdOn(host, aRequestId, aError); + } + + for (AdvInfo &adv : mAdvInfoList) + { + if (HandleRegisteredRequestIdOn(adv.mHost, aRequestId, aError)) + { + if (adv.mError == kErrorNone) + { + adv.mError = aError; + } + + if (adv.IsCompleted()) + { + mTasklet.Post(); + } + } + } + +exit: + return; +} + +bool AdvertisingProxy::HandleRegisteredRequestIdOn(Host &aHost, RequestId aRequestId, Error aError) +{ + // Handles "registration request callback" for `aRequestId` on a + // given `aHost`. Returns `true`, if the ID matched an entry + // on `aHost` and `aHost` was updated, `false` otherwise. + + bool didUpdate = false; + + VerifyOrExit(aHost.mAdvIdRange.Contains(aRequestId)); + + if (aHost.mAdvId == aRequestId) + { + aHost.mAdvId = kInvalidRequestId; + aHost.mIsRegistered = (aError == kErrorNone); + didUpdate = true; + } + + if (aHost.mKeyAdvId == aRequestId) + { + aHost.mKeyAdvId = kInvalidRequestId; + aHost.mIsKeyRegistered = true; + didUpdate = true; + } + + for (Service &service : aHost.mServices) + { + if (service.mAdvId == aRequestId) + { + service.mAdvId = kInvalidRequestId; + service.mIsRegistered = (aError == kErrorNone); + didUpdate = true; + } + + if (service.mKeyAdvId == aRequestId) + { + service.mKeyAdvId = kInvalidRequestId; + service.mIsKeyRegistered = true; + didUpdate = true; + } + } + + UpdateAdvIdRangeOn(aHost); + +exit: + return didUpdate; +} + +void AdvertisingProxy::HandleTimer(void) +{ + TimeMilli now = TimerMilli::GetNow(); + TimeMilli nextTime = now.GetDistantFuture(); + OwningList expiredList; + + VerifyOrExit(mState == kStateRunning); + + mAdvInfoList.RemoveAllMatching(AdvInfo::ExpirationChecker(now), expiredList); + + for (AdvInfo &adv : mAdvInfoList) + { + nextTime = Min(adv.mExpireTime, nextTime); + } + + if (nextTime != now.GetDistantFuture()) + { + mTimer.FireAtIfEarlier(nextTime); + } + + for (AdvInfo &adv : expiredList) + { + adv.mError = kErrorResponseTimeout; + adv.mBlockingAdv = nullptr; + adv.mHost.mAdvIdRange.Clear(); + SignalAdvCompleted(adv); + } + +exit: + return; +} + +void AdvertisingProxy::HandleTasklet(void) +{ + VerifyOrExit(mState == kStateRunning); + + while (true) + { + OwningList completedList; + + mAdvInfoList.RemoveAllMatching(AdvInfo::CompletionChecker(), completedList); + + VerifyOrExit(!completedList.IsEmpty()); + + // `RemoveAllMatching()` reverses the order of removed entries + // from `mAdvInfoList` (which itself keeps the later requests + // towards the head of the list). This means that the + // `completedList` will be sorted from earliest request to + // latest and this is the order that we want to notify + // `Srp::Server`. + + for (AdvInfo &adv : completedList) + { + SignalAdvCompleted(adv); + } + + completedList.Clear(); + } + +exit: + return; +} + +void AdvertisingProxy::SignalAdvCompleted(AdvInfo &aAdvInfo) +{ + // Check if any outstanding advertisements in the list + // is blocked by `aAdvInfo` and unblock. + + for (AdvInfo &adv : mAdvInfoList) + { + if (adv.mBlockingAdv == &aAdvInfo) + { + adv.mBlockingAdv = nullptr; + + if (adv.IsCompleted()) + { + mTasklet.Post(); + } + } + } + + switch (aAdvInfo.mError) + { + case kErrorNone: + mCounters.mAdvSuccessful++; + break; + case kErrorResponseTimeout: + mCounters.mAdvTimeout++; + break; + default: + mCounters.mAdvRejected++; + break; + } + + aAdvInfo.SignalServerToCommit(); +} + +//--------------------------------------------------------------------------------------------------------------------- +// AdvertisingProxy::AdvInfo + +AdvertisingProxy::AdvInfo::AdvInfo(Host &aHost, const Server::MessageMetadata &aMetadata, uint32_t aTimeout) + : mNext(nullptr) + , mBlockingAdv(nullptr) + , mHost(aHost) + , mExpireTime(TimerMilli::GetNow() + aTimeout) + , mMessageMetadata(aMetadata) + , mError(kErrorNone) +{ + if (aMetadata.mMessageInfo != nullptr) + { + // If `mMessageInfo` is not null in the given `aMetadata` keep + // a copy of it in `AdvInfo` structure and update the + // `mMessageMetadata` to point to the local copy instead. + + mMessageInfo = *aMetadata.mMessageInfo; + mMessageMetadata.mMessageInfo = &mMessageInfo; + } +} + +void AdvertisingProxy::AdvInfo::SignalServerToCommit(void) +{ + LogInfo("Adv done '%s', error:%s", mHost.GetFullName(), ErrorToString(mError)); + Get().CommitSrpUpdate(mError, mHost, mMessageMetadata); +} + +bool AdvertisingProxy::AdvInfo::IsCompleted(void) const +{ + bool isCompleted = false; + + VerifyOrExit(mBlockingAdv == nullptr); + isCompleted = (mError != kErrorNone) || mHost.mAdvIdRange.IsEmpty(); + +exit: + return isCompleted; +} + +} // namespace Srp +} // namespace ot + +#endif // OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE diff --git a/src/core/net/srp_advertising_proxy.hpp b/src/core/net/srp_advertising_proxy.hpp new file mode 100644 index 00000000000..19fa82dbe34 --- /dev/null +++ b/src/core/net/srp_advertising_proxy.hpp @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2023, 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 for Advertising Proxy. + */ + +#ifndef SRP_ADVERTISING_PROXY_HPP_ +#define SRP_ADVERTISING_PROXY_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + +#if !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE +#error "OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE requires OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE" +#endif + +#if !OPENTHREAD_CONFIG_SRP_SERVER_ENABLE +#error "OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE requires OPENTHREAD_CONFIG_SRP_SERVER_ENABLE" +#endif + +#if !OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE +#error "OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE requires OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE" +#endif + +#include "common/clearable.hpp" +#include "common/heap_allocatable.hpp" +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "common/owning_list.hpp" +#include "common/tasklet.hpp" +#include "common/timer.hpp" +#include "net/dnssd.hpp" +#include "net/srp_server.hpp" + +namespace ot { +namespace Srp { + +/** + * Implements SRP Advertising Proxy. + * + */ +class AdvertisingProxy : public InstanceLocator, private NonCopyable +{ +public: + typedef Server::Host Host; ///< An SRP server host registration. + typedef Server::Service Service; ///< An SRP server service registration. + + /** + * Represents counters for Advertising Proxy. + * + */ + struct Counters : public Clearable + { + uint32_t mAdvTotal; ///< Total number of advertisement requests, i.e., calls to `Advertise()`. + uint32_t mAdvReplaced; ///< Number of advertisements that were replaced by a newer one. + uint32_t mAdvSkipped; ///< Number of advertisement that were skipped (DNS-SD platform not yet ready). + uint32_t mAdvSuccessful; ///< Number of successful adv (all requests registered successfully). + uint32_t mAdvRejected; ///< Number of rejected adv (at least one request was rejected by DNS-SD plat). + uint32_t mAdvTimeout; ///< Number of advertisements that timed out (no response from DNS-SD platform). + uint32_t mAdvHostRemovals; ///< Number of host removal adv, i.e., calls to `AdvertiseRemovalOf(Host &)` + uint32_t mAdvServiceRemovals; ///< Number of service removal adv, i.e., calls to `AdvertiseRemovalOf(Service &)` + uint32_t mStateChanges; ///< Number of state changes of Advertising Proxy. + }; + + /** + * Initializes the `AdvertisingProxy` object. + * + * @param[in] aInstance The OpenThread instance + * + */ + explicit AdvertisingProxy(Instance &aInstance); + + /** + * Indicates whether or not the Advertising Proxy is running. + * + * @retval TRUE The Advertising Proxy is running. + * @retval FALSE The Advertising Proxy is not running (it is stopped). + * + */ + bool IsRunning(void) const { return mState == kStateRunning; } + + /** + * Requests advertisement of a newly received SRP Update message. + * + * Once advertisement is completed, `AdvertisingProxy` notifies server by invoking `Server::CommitSrpUpdate()` + * using the same `aHost` and `aMetadata` as input parameters along with an `Error` indicating the outcome of the + * advertisement. + * + * The `aHost` instance ownership is passed to `AdvertisingProxy` until it is passed back to the `Server` in the + * `CommitSrpUpdate()` call. The call to `CommitSrpUpdate()` may happen before this method returns, for example, + * if the proxy is not running and therefore the advertisement is skipped. + * + * @param[in] aHost The `aHost` instance constructed from processing a newly received SRP Update message. + * @param[in] aMetadata The `MessageMetadata` associated with the received SRP Update message by server. + * + */ + void Advertise(Host &aHost, const Server::MessageMetadata &aMetadata); + + /** + * Requests advertisement of removal of an already committed host and all its services, for example, due to its + * lease expiration. + * + * The removal does not use any callback to notify the SRP server since the server always immediately commits the + * removed entries. + * + * If there is an outstanding advertisement request (an earlier call to `Advertise()` that has not yet completed) + * that is registering the same host name as @p aHost that is being removed, the outstanding advertisement is + * rejected using the `kErrorAbort` error. This situation can happen if the client tries to refresh or update its + * registration close to its lease expiration time. By rejecting any outstanding advertisements, we ensure that an + * expired host is not re-added by mistake due to a delay in registration by the DNS-SD platform. The error is + * passed back to the client, triggering it to retry its registration. + * + * @param[in] aHost The host which is being removed. + * + */ + void AdvertiseRemovalOf(Host &aHost); + + /** + * Requests advertisement of removal of an already committed service, for example, due to its lease expiration. + * + * The removal does not use any callback to notify the SRP server since the server always immediately commits the + * removed services. + * + * If there is an outstanding advertisement request (an earlier call to `Advertise()` that has not yet completed) + * that is registering the same service as @p aService that is being removed, we skip the advertisement of service + * removal (do not unregister the service on infrastructure DNS-SD). This ensures that when the outstanding + * advertisement is completed, the service is re-added successfully (and it is still being advertised by proxy). + * This behavior is different from `AdvertiseRemovalOf(Host &)`, where the outstanding advertisement is rejected + * because service removals are individual, compared to when removing a host where the host and all its associated + * services are removed. + * + * @param[in] aHost The host which is being removed. + * + */ + void AdvertiseRemovalOf(Service &aService); + + /** + * Gets the set of counters. + * + * @returns The `AdvertisingProxy` counter. + * + */ + const Counters &GetCounters(void) const { return mCounters; } + + /** + * Resets the counters + * + */ + void ResetCounters(void) { mCounters.Clear(); } + + /** + * Gets the advertisement timeout (in msec). + * + * The default value of `OPENTHREAD_CONFIG_SRP_SERVER_SERVICE_UPDATE_TIMEOUT` is used when not explicitly set. + * + * @returns The advertisement timeout (in msec). + * + */ + uint32_t GetAdvTimeout(void) const { return mAdvTimeout; } + + /** + * Sets the advertisement timeout. + * + * Changing the timeout is intended for testing purposes only. This allows tests to use a long timeout to validate + * the behavior of `AdvertisingProxy` when new `Advertise()` requests replace entries in earlier requests. + * + * @param[in] aTimeout The advertisement timeout (in msec). + * + */ + void SetAdvTimeout(uint32_t aTimeout) { mAdvTimeout = Max(aTimeout, kAdvTimeout); } + + /** + * Notifies `AdvertisingProxy` that SRP sever state changed. + * + */ + void HandleServerStateChange(void) { UpdateState(); } + + /** + * Notifies `AdvertisingProxy` that DND-SD platform state changed. + * + */ + void HandleDnssdPlatformStateChange(void) { UpdateState(); } + + /** + * Notifies `AdvertisingProxy` that `InfraIf` state changed. + * + */ + void HandleInfraIfStateChanged(void) { UpdateState(); } + +private: + typedef Dnssd::RequestId RequestId; + typedef char DnsName[Dns::Name::kMaxNameSize]; + + static constexpr RequestId kInvalidRequestId = Server::kInvalidRequestId; + + static constexpr uint32_t kAdvTimeout = OPENTHREAD_CONFIG_SRP_SERVER_SERVICE_UPDATE_TIMEOUT; // in msec + + enum State : uint8_t + { + kStateStopped, + kStateRunning, + }; + + struct AdvInfo : public Heap::Allocatable, public LinkedListEntry, public GetProvider + { + struct CompletionChecker + { + // Used in `Matches()` to check if advertisement is + // completed (successfully or failed). + }; + + struct ExpirationChecker + { + explicit ExpirationChecker(TimeMilli aNow) + : mNow(aNow) + { + } + + TimeMilli mNow; + }; + + AdvInfo(Host &aHost, const Server::MessageMetadata &aMetadata, uint32_t aTimeout); + void SignalServerToCommit(void); + bool IsCompleted(void) const; + bool Matches(const CompletionChecker &) const { return IsCompleted(); } + bool Matches(const ExpirationChecker &aChecker) const { return (mExpireTime <= aChecker.mNow); } + Instance &GetInstance(void) const { return mHost.GetInstance(); } + + AdvInfo *mNext; + AdvInfo *mBlockingAdv; + Host &mHost; + TimeMilli mExpireTime; + Server::MessageMetadata mMessageMetadata; + Ip6::MessageInfo mMessageInfo; + Error mError; + }; + + template void UpdateAdvIdRangeOn(Entry &aEntry); + template bool IsRegisteredOrRegistering(const Entry &aEntry) const; + template bool IsKeyRegisteredOrRegistering(const Entry &aEntry) const; + template void DecideToAdvertise(Entry &aEntry, bool aUnregisterEntry, bool aUnregisterKey); + template void UpdateKeyRegistrationStatus(Entry &aEntry, const Entry &aExistingEntry); + template bool CompareAndUpdate(Entry &aEntry, Entry &aExistingEntry); + template bool EntriesMatch(const Entry &aFirstEntry, const Entry &aSecondEntry); + + void Start(void); + void Stop(void); + void UpdateState(void); + RequestId AllocateNextRequestId(void); + void Advertise(Host &aHost); + void UnregisterHostAndItsServicesAndKeys(Host &aHost); + bool CompareAndUpdateHostAndServices(Host &aHost, Host &aExistingHost); + bool CompareAndUpdateHost(Host &aHost, Host &aExistingHost); + bool CompareAndUpdateService(Service &aService, Service &aExistingService); + void RegisterHost(Host &aHost); + void UnregisterHost(Host &aHost); + void RegisterService(Service &aService); + void UnregisterService(Service &aService); + void RegisterKey(Host &aHost); + void RegisterKey(Service &aService); + void RegisterKey(const char *aName, + const char *aServiceType, + const Host::Key &aKey, + RequestId aRequestId, + uint32_t aTtl); + void UnregisterKey(Service &aService); + void UnregisterKey(Host &aHost); + void UnregisterKey(const char *aName, const char *aServiceType); + void CopyNameAndRemoveDomain(DnsName &aName, const char *aFullName); + static void HandleRegistered(otInstance *aInstance, otPlatDnssdRequestId aRequestId, otError aError); + void HandleRegistered(RequestId aRequestId, Error aError); + bool HandleRegisteredRequestIdOn(Host &aHost, RequestId aRequestId, Error aError); + void HandleTimer(void); + void HandleTasklet(void); + void SignalAdvCompleted(AdvInfo &aAdvInfo); + + using AdvTimer = TimerMilliIn; + using AdvTasklet = TaskletIn; + + State mState; + RequestId mCurrentRequestId; + uint32_t mAdvTimeout; + OwningList mAdvInfoList; + AdvTimer mTimer; + AdvTasklet mTasklet; + Counters mCounters; +}; + +} // namespace Srp +} // namespace ot + +#endif // OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + +#endif // SRP_ADVERTISING_PROXY_HPP_ diff --git a/src/core/net/srp_server.cpp b/src/core/net/srp_server.cpp index 946b84163e6..3d6550daed3 100644 --- a/src/core/net/srp_server.cpp +++ b/src/core/net/srp_server.cpp @@ -329,7 +329,7 @@ void Server::RemoveHost(Host *aHost, RetainName aRetainName) if (mServiceUpdateHandler.IsSet()) { - uint32_t updateId = AllocateId(); + uint32_t updateId = AllocateServiceUpdateId(); LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(updateId)); mServiceUpdateHandler.Invoke(updateId, aHost, static_cast(kDefaultEventsHandlerTimeout)); @@ -339,6 +339,12 @@ void Server::RemoveHost(Host *aHost, RetainName aRetainName) // only when there is system failure of the platform mDNS implementation // and in which case the host is not expected to be still registered. } +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + else + { + Get().AdvertiseRemovalOf(*aHost); + } +#endif if (!aRetainName) { @@ -456,7 +462,7 @@ void Server::CommitSrpUpdate(Error aError, uint32_t grantedKeyLease = 0; bool useShortLease = aHost.ShouldUseShortLeaseOption(); - if (aError != kErrorNone) + if (aError != kErrorNone || (mState != kStateRunning)) { aHost.Free(); ExitNow(); @@ -525,7 +531,27 @@ void Server::CommitSrpUpdate(Error aError, if (!aHost.HasService(existingService->GetInstanceName())) { aHost.AddService(*existingService); - existingService->Log(Service::kKeepUnchanged); + + // If host is deleted we make sure to add any existing + // service that is not already included as deleted. + // When processing an SRP update message that removes + // a host, we construct `aHost` and add any existing + // services that we have for the same host name as + // deleted. However, due to asynchronous nature + // of "update handler" (advertising proxy), by the + // time we get the callback to commit `aHost`, there + // may be new services added that were not included + // when constructing `aHost`. + + if (aHost.IsDeleted() && !existingService->IsDeleted()) + { + existingService->mIsDeleted = true; + existingService->Log(Service::kRemoveButRetainName); + } + else + { + existingService->Log(Service::kKeepUnchanged); + } } else { @@ -599,6 +625,10 @@ void Server::Start(void) PrepareSocket(); LogInfo("Start listening on port %u", mPort); +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + Get().HandleServerStateChange(); +#endif + exit: return; } @@ -708,6 +738,10 @@ void Server::Stop(void) IgnoreError(mSocket.Close()); mHasRegisteredAnyService = false; +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + Get().HandleServerStateChange(); +#endif + exit: return; } @@ -1202,18 +1236,25 @@ Error Server::ProcessAdditionalSection(Host *aHost, const Message &aMessage, Mes aHost->SetLease(leaseOption.GetLeaseInterval()); aHost->SetKeyLease(leaseOption.GetKeyLeaseInterval()); + for (Service &service : aHost->mServices) + { + if (aHost->GetLease() == 0) + { + service.mIsDeleted = true; + } + + service.mLease = service.mIsDeleted ? 0 : leaseOption.GetLeaseInterval(); + service.mKeyLease = leaseOption.GetKeyLeaseInterval(); + } + // If the client included the short variant of Lease Option, // server must also use the short variant in its response. aHost->SetUseShortLeaseOption(leaseOption.IsShortVariant()); if (aHost->GetLease() > 0) { - uint8_t hostAddressesNum; - - aHost->GetAddresses(hostAddressesNum); - // There MUST be at least one valid address if we have nonzero lease. - VerifyOrExit(hostAddressesNum > 0, error = kErrorFailed); + VerifyOrExit(aHost->mAddresses.GetLength() > 0, error = kErrorFailed); } // SIG(0). @@ -1340,6 +1381,7 @@ void Server::HandleUpdate(Host &aHost, const MessageMetadata &aMetadata) SuccessOrExit(error = service->mServiceName.Set(existingService.GetServiceName())); service->mIsDeleted = true; + service->mKeyLease = existingService.mKeyLease; } exit: @@ -1410,6 +1452,17 @@ void Server::InformUpdateHandlerOrCommit(Error aError, Host &aHost, const Messag aError = kErrorNoBufs; } +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + else if (aError == kErrorNone) + { + // Ask `AdvertisingProxy` to advertise the new update. + // Proxy will report the outcome by calling + // `Server::CommitSrpUpdate()` directly. + + Get().Advertise(aHost, aMetadata); + ExitNow(); + } +#endif CommitSrpUpdate(aError, aHost, aMetadata); @@ -1421,10 +1474,12 @@ void Server::SendResponse(const Dns::UpdateHeader &aHeader, Dns::UpdateHeader::Response aResponseCode, const Ip6::MessageInfo &aMessageInfo) { - Error error; + Error error = kErrorNone; Message *response = nullptr; Dns::UpdateHeader header; + VerifyOrExit(mState == kStateRunning, error = kErrorInvalidState); + response = GetSocket().NewMessage(); VerifyOrExit(response != nullptr, error = kErrorNoBufs); @@ -1752,6 +1807,15 @@ Error Server::Service::Init(const char *aInstanceName, const char *aInstanceLabe mUpdateTime = aUpdateTime; mIsDeleted = false; mIsCommitted = false; +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + mIsRegistered = false; + mIsKeyRegistered = false; + mIsReplaced = false; + mShouldAdvertise = false; + mShouldRegisterKey = false; + mAdvId = kInvalidRequestId; + mKeyAdvId = kInvalidRequestId; +#endif mParsedDeleteAllRrset = false; mParsedSrv = false; @@ -1930,6 +1994,15 @@ Server::Host::Host(Instance &aInstance, TimeMilli aUpdateTime) , mUpdateTime(aUpdateTime) , mParsedKey(false) , mUseShortLeaseOption(false) +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + , mIsRegistered(false) + , mIsKeyRegistered(false) + , mIsReplaced(false) + , mShouldAdvertise(false) + , mShouldRegisterKey(false) + , mAdvId(kInvalidRequestId) + , mKeyAdvId(kInvalidRequestId) +#endif { } @@ -2041,12 +2114,18 @@ void Server::Host::RemoveService(Service *aService, RetainName aRetainName, Noti VerifyOrExit(aService != nullptr); aService->mIsDeleted = true; + aService->mLease = 0; + + if (!aRetainName) + { + aService->mKeyLease = 0; + } aService->Log(aRetainName ? Service::kRemoveButRetainName : Service::kFullyRemove); if (aNotifyServiceHandler && server.mServiceUpdateHandler.IsSet()) { - uint32_t updateId = server.AllocateId(); + uint32_t updateId = server.AllocateServiceUpdateId(); LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(updateId)); server.mServiceUpdateHandler.Invoke(updateId, this, static_cast(kDefaultEventsHandlerTimeout)); @@ -2056,6 +2135,12 @@ void Server::Host::RemoveService(Service *aService, RetainName aRetainName, Noti // failure of the platform mDNS implementation and in which case the // service is not expected to be still registered. } +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + else if (aNotifyServiceHandler) + { + Get().AdvertiseRemovalOf(*aService); + } +#endif if (!aRetainName) { @@ -2119,7 +2204,7 @@ Server::UpdateMetadata::UpdateMetadata(Instance &aInstance, Host &aHost, const M , mNext(nullptr) , mExpireTime(TimerMilli::GetNow() + kDefaultEventsHandlerTimeout) , mDnsHeader(aMessageMetadata.mDnsHeader) - , mId(Get().AllocateId()) + , mId(Get().AllocateServiceUpdateId()) , mTtlConfig(aMessageMetadata.mTtlConfig) , mLeaseConfig(aMessageMetadata.mLeaseConfig) , mHost(aHost) diff --git a/src/core/net/srp_server.hpp b/src/core/net/srp_server.hpp index c506416eca4..6b5556d4ef0 100644 --- a/src/core/net/srp_server.hpp +++ b/src/core/net/srp_server.hpp @@ -72,6 +72,7 @@ #include "common/timer.hpp" #include "crypto/ecdsa.hpp" #include "net/dns_types.hpp" +#include "net/dnssd.hpp" #include "net/ip6.hpp" #include "net/ip6_address.hpp" #include "net/udp6.hpp" @@ -101,6 +102,8 @@ class RoutingManager; namespace Srp { +class AdvertisingProxy; + /** * Implements the SRP server. * @@ -112,6 +115,7 @@ class Server : public InstanceLocator, private NonCopyable friend class Service; friend class Host; friend class Dns::ServiceDiscovery::Server; + friend class AdvertisingProxy; #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE friend class BorderRouter::RoutingManager; #endif @@ -128,6 +132,10 @@ class Server : public InstanceLocator, private NonCopyable kNotifyServiceHandler = true, }; +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + static constexpr Dnssd::RequestId kInvalidRequestId = 0; +#endif + public: static constexpr uint16_t kUdpPortMin = OPENTHREAD_CONFIG_SRP_SERVER_UDP_PORT_MIN; ///< The reserved min port. static constexpr uint16_t kUdpPortMax = OPENTHREAD_CONFIG_SRP_SERVER_UDP_PORT_MAX; ///< The reserved max port. @@ -185,6 +193,7 @@ class Server : public InstanceLocator, private NonCopyable friend class LinkedList; friend class LinkedListEntry; friend class Heap::Allocatable; + friend class AdvertisingProxy; public: /** @@ -432,6 +441,15 @@ class Server : public InstanceLocator, private NonCopyable bool mParsedDeleteAllRrset : 1; bool mParsedSrv : 1; bool mParsedTxt : 1; +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + bool mIsRegistered : 1; + bool mIsKeyRegistered : 1; + bool mIsReplaced : 1; + bool mShouldAdvertise : 1; + bool mShouldRegisterKey : 1; + Dnssd::RequestId mAdvId; + Dnssd::RequestId mKeyAdvId; +#endif }; /** @@ -447,6 +465,7 @@ class Server : public InstanceLocator, private NonCopyable friend class Server; friend class LinkedListEntry; friend class Heap::Allocatable; + friend class AdvertisingProxy; public: typedef Crypto::Ecdsa::P256::PublicKey Key; ///< Host key (public ECDSA P256 key). @@ -603,6 +622,16 @@ class Server : public InstanceLocator, private NonCopyable LinkedList mServices; bool mParsedKey : 1; bool mUseShortLeaseOption : 1; // Use short lease option (lease only 4 bytes). +#if OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE + bool mIsRegistered : 1; + bool mIsKeyRegistered : 1; + bool mIsReplaced : 1; + bool mShouldAdvertise : 1; + bool mShouldRegisterKey : 1; + Dnssd::RequestId mAdvId; + Dnssd::RequestId mKeyAdvId; + Dnssd::RequestIdRange mAdvIdRange; +#endif }; /** @@ -945,6 +974,7 @@ class Server : public InstanceLocator, private NonCopyable void SelectPort(void); void PrepareSocket(void); Ip6::Udp::Socket &GetSocket(void); + LinkedList &GetHosts(void) { return mHosts; } #if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE void HandleDnssdServerStateChange(void); @@ -953,7 +983,7 @@ class Server : public InstanceLocator, private NonCopyable void HandleNetDataPublisherEvent(NetworkData::Publisher::Event aEvent); - ServiceUpdateId AllocateId(void) { return mServiceUpdateId++; } + ServiceUpdateId AllocateServiceUpdateId(void) { return mServiceUpdateId++; } void InformUpdateHandlerOrCommit(Error aError, Host &aHost, const MessageMetadata &aMetadata); void CommitSrpUpdate(Error aError, Host &aHost, const MessageMetadata &aMessageMetadata); diff --git a/tests/fuzz/oss-fuzz-build b/tests/fuzz/oss-fuzz-build index c8bb40789b7..e83c5c80976 100755 --- a/tests/fuzz/oss-fuzz-build +++ b/tests/fuzz/oss-fuzz-build @@ -68,6 +68,7 @@ set -euxo pipefail -DOT_SERVICE=ON \ -DOT_SLAAC=ON \ -DOT_SNTP_CLIENT=ON \ + -DOT_SRN_ADV_PROXY=ON \ -DOT_SRP_CLIENT=ON \ -DOT_SRP_SERVER=ON \ -DOT_THREAD_VERSION=1.3 \ diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index facfce9de0b..bc933d17f11 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -61,6 +61,10 @@ #define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 1 +#define OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE 1 + +#define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 1 + #define OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index f44ffaeea73..1a9b1822832 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -1116,6 +1116,27 @@ target_link_libraries(ot-test-serial-number add_test(NAME ot-test-serial-number COMMAND ot-test-serial-number) +add_executable(ot-test-srp-adv-proxy + test_srp_adv_proxy.cpp +) + +target_include_directories(ot-test-srp-adv-proxy + PRIVATE + ${COMMON_INCLUDES} +) + +target_compile_options(ot-test-srp-adv-proxy + PRIVATE + ${COMMON_COMPILE_OPTIONS} +) + +target_link_libraries(ot-test-srp-adv-proxy + PRIVATE + ${COMMON_LIBS} +) + +add_test(NAME ot-test-srp-adv-proxy COMMAND ot-test-srp-adv-proxy) + add_executable(ot-test-srp-server test_srp_server.cpp ) diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp index 43f6224cbeb..40e23ceb462 100644 --- a/tests/unit/test_platform.cpp +++ b/tests/unit/test_platform.cpp @@ -738,4 +738,81 @@ otError otPlatBleGattServerIndicate(otInstance *aInstance, uint16_t aHandle, con } #endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE +#if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + +OT_TOOL_WEAK otPlatDnssdState otPlatDnssdGetState(otInstance *aInstance) +{ + OT_UNUSED_VARIABLE(aInstance); + + return OT_PLAT_DNSSD_STOPPED; +} + +OT_TOOL_WEAK void otPlatDnssdRegisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aService); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +OT_TOOL_WEAK void otPlatDnssdUnregisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aService); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +OT_TOOL_WEAK void otPlatDnssdRegisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aHost); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +OT_TOOL_WEAK void otPlatDnssdUnregisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aHost); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +OT_TOOL_WEAK void otPlatDnssdRegisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aKey); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +OT_TOOL_WEAK void otPlatDnssdUnregisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aKey); + OT_UNUSED_VARIABLE(aRequestId); + OT_UNUSED_VARIABLE(aCallback); +} + +#endif // OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE + } // extern "C" diff --git a/tests/unit/test_platform.h b/tests/unit/test_platform.h index 7197feb7e35..f1c7eb4f47d 100644 --- a/tests/unit/test_platform.h +++ b/tests/unit/test_platform.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include diff --git a/tests/unit/test_srp_adv_proxy.cpp b/tests/unit/test_srp_adv_proxy.cpp new file mode 100644 index 00000000000..9fe8809bf2d --- /dev/null +++ b/tests/unit/test_srp_adv_proxy.cpp @@ -0,0 +1,2781 @@ +/* + * Copyright (c) 2023, 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. + */ + +#include + +#include "test_platform.h" +#include "test_util.hpp" + +#include +#include +#include +#include + +#include "common/arg_macros.hpp" +#include "common/array.hpp" +#include "common/string.hpp" +#include "common/time.hpp" +#include "instance/instance.hpp" + +#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE && \ + OPENTHREAD_CONFIG_SRP_SERVER_ADVERTISING_PROXY_ENABLE && !OPENTHREAD_CONFIG_TIME_SYNC_ENABLE && \ + !OPENTHREAD_PLATFORM_POSIX +#define ENABLE_ADV_PROXY_TEST 1 +#else +#define ENABLE_ADV_PROXY_TEST 0 +#endif + +#if ENABLE_ADV_PROXY_TEST + +using namespace ot; + +// Logs a message and adds current time (sNow) as "::." +#define Log(...) \ + printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 36000000), (sNow / 60000) % 60, \ + (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__)) + +static constexpr uint16_t kMaxRaSize = 800; + +static ot::Instance *sInstance; + +static uint32_t sNow = 0; +static uint32_t sAlarmTime; +static bool sAlarmOn = false; + +static otRadioFrame sRadioTxFrame; +static uint8_t sRadioTxFramePsdu[OT_RADIO_FRAME_MAX_SIZE]; +static bool sRadioTxOngoing = false; + +//---------------------------------------------------------------------------------------------------------------------- +// Function prototypes + +void ProcessRadioTxAndTasklets(void); +void AdvanceTime(uint32_t aDuration); + +//---------------------------------------------------------------------------------------------------------------------- +// `otPlatRadio` + +extern "C" { + +otRadioCaps otPlatRadioGetCaps(otInstance *) { return OT_RADIO_CAPS_ACK_TIMEOUT | OT_RADIO_CAPS_CSMA_BACKOFF; } + +otError otPlatRadioTransmit(otInstance *, otRadioFrame *) +{ + sRadioTxOngoing = true; + + return OT_ERROR_NONE; +} + +otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { return &sRadioTxFrame; } + +//---------------------------------------------------------------------------------------------------------------------- +// `otPlatAlarm` + +void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; } + +void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt) +{ + sAlarmOn = true; + sAlarmTime = aT0 + aDt; +} + +uint32_t otPlatAlarmMilliGetNow(void) { return sNow; } + +//---------------------------------------------------------------------------------------------------------------------- +// `otPlatDnssd` + +static constexpr uint16_t kDnssdArraySize = 128; + +struct DnssdRequest +{ + DnssdRequest(void) = default; + + DnssdRequest(otPlatDnssdRequestId aId, otPlatDnssdRegisterCallback aCallback) + : mId(aId) + , mCallback(aCallback) + { + } + + otPlatDnssdRequestId mId; + otPlatDnssdRegisterCallback mCallback; +}; + +static Array sDnssdRegHostRequests; +static Array sDnssdUnregHostRequests; +static Array sDnssdRegServiceRequests; +static Array sDnssdUnregServiceRequests; +static Array sDnssdRegKeyRequests; +static Array sDnssdUnregKeyRequests; + +static bool sDnssdShouldCheckWithClient = true; +static Error sDnssdCallbackError = kErrorPending; +static otPlatDnssdState sDnssdState = OT_PLAT_DNSSD_READY; +static uint16_t sDnssdNumHostAddresses = 0; + +constexpr uint32_t kInfraIfIndex = 1; + +otPlatDnssdState otPlatDnssdGetState(otInstance *aInstance) +{ + OT_UNUSED_VARIABLE(aInstance); + + return sDnssdState; +} + +void otPlatDnssdRegisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + Log("otPlatDnssdRegisterService(aRequestId: %lu)", ToUlong(aRequestId)); + Log(" hostName : %s", aService->mHostName); + Log(" serviceInstance: %s", aService->mServiceInstance); + Log(" serviceType : %s", aService->mServiceType); + Log(" num sub-types : %u", aService->mSubTypeLabelsLength); + + for (uint16_t index = 0; index < aService->mSubTypeLabelsLength; index++) + { + Log(" sub-type %-4u : %s", index, aService->mSubTypeLabels[index]); + } + + Log(" TXT data len : %u", aService->mTxtDataLength); + Log(" port : %u", aService->mPort); + Log(" priority : %u", aService->mPriority); + Log(" weight : %u", aService->mWeight); + Log(" TTL : %u", aService->mTtl); + Log(" Infra-if index : %u", aService->mInfraIfIndex); + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aService->mInfraIfIndex == kInfraIfIndex); + + if (sDnssdShouldCheckWithClient) + { + Srp::Client &srpClient = AsCoreType(aInstance).Get(); + bool didFind = false; + + VerifyOrQuit(StringMatch(srpClient.GetHostInfo().GetName(), aService->mHostName)); + + didFind = false; + + for (const Srp::Client::Service &service : srpClient.GetServices()) + { + if (StringMatch(service.GetInstanceName(), aService->mServiceInstance)) + { + didFind = true; + VerifyOrQuit(StringMatch(service.GetName(), aService->mServiceType)); + VerifyOrQuit(service.GetPort() == aService->mPort); + VerifyOrQuit(service.GetWeight() == aService->mWeight); + VerifyOrQuit(service.GetPriority() == aService->mPriority); + VerifyOrQuit(service.HasSubType() == (aService->mSubTypeLabelsLength != 0)); + } + } + + VerifyOrQuit(didFind); + } + + SuccessOrQuit(sDnssdRegServiceRequests.PushBack(DnssdRequest(aRequestId, aCallback))); + + if ((sDnssdCallbackError != kErrorPending) && (aCallback != nullptr)) + { + aCallback(aInstance, aRequestId, sDnssdCallbackError); + } +} + +void otPlatDnssdUnregisterService(otInstance *aInstance, + const otPlatDnssdService *aService, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + Log("otPlatDnssdUnregisterService(aRequestId: %lu)", ToUlong(aRequestId)); + Log(" hostName : %s", aService->mHostName); + Log(" serviceInstance: %s", aService->mServiceInstance); + Log(" serviceName : %s", aService->mServiceType); + Log(" Infra-if index : %u", aService->mInfraIfIndex); + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aService->mInfraIfIndex == kInfraIfIndex); + + if (sDnssdShouldCheckWithClient) + { + // Validate the received service info matches one of the services + // on SRP client. + + Srp::Client &srpClient = AsCoreType(aInstance).Get(); + bool didFind = false; + + VerifyOrQuit(StringMatch(srpClient.GetHostInfo().GetName(), aService->mHostName)); + + for (const Srp::Client::Service &service : srpClient.GetServices()) + { + if (StringMatch(service.GetInstanceName(), aService->mServiceInstance)) + { + didFind = true; + VerifyOrQuit(StringMatch(service.GetName(), aService->mServiceType)); + } + } + + VerifyOrQuit(didFind); + } + + SuccessOrQuit(sDnssdUnregServiceRequests.PushBack(DnssdRequest(aRequestId, aCallback))); + + if ((sDnssdCallbackError != kErrorPending) && (aCallback != nullptr)) + { + aCallback(aInstance, aRequestId, sDnssdCallbackError); + } +} + +void otPlatDnssdRegisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + Log("otPlatDnssdRegisterHost(aRequestId: %lu)", ToUlong(aRequestId)); + Log(" hostName : %s", aHost->mHostName); + Log(" numAddresses : %u", aHost->mAddressesLength); + + for (uint16_t index = 0; index < aHost->mAddressesLength; index++) + { + Log(" Address %-4u : %s", index, AsCoreType(&aHost->mAddresses[index]).ToString().AsCString()); + } + + Log(" TTL : %u", aHost->mTtl); + Log(" Infra-if index : %u", aHost->mInfraIfIndex); + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aHost->mInfraIfIndex == kInfraIfIndex); + + sDnssdNumHostAddresses = aHost->mAddressesLength; + + if (sDnssdShouldCheckWithClient) + { + VerifyOrQuit(StringMatch(AsCoreType(aInstance).Get().GetHostInfo().GetName(), aHost->mHostName)); + } + + SuccessOrQuit(sDnssdRegHostRequests.PushBack(DnssdRequest(aRequestId, aCallback))); + + if ((sDnssdCallbackError != kErrorPending) && (aCallback != nullptr)) + { + aCallback(aInstance, aRequestId, sDnssdCallbackError); + } +} + +void otPlatDnssdUnregisterHost(otInstance *aInstance, + const otPlatDnssdHost *aHost, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + Log("otPlatDnssdUnregisterHost(aRequestId: %lu)", ToUlong(aRequestId)); + Log(" hostName : %s", aHost->mHostName); + Log(" Infra-if index : %u", aHost->mInfraIfIndex); + + VerifyOrQuit(sInstance == aInstance); + VerifyOrQuit(aHost->mInfraIfIndex == kInfraIfIndex); + + if (sDnssdShouldCheckWithClient) + { + VerifyOrQuit(StringMatch(AsCoreType(aInstance).Get().GetHostInfo().GetName(), aHost->mHostName)); + } + + SuccessOrQuit(sDnssdUnregHostRequests.PushBack(DnssdRequest(aRequestId, aCallback))); + + if ((sDnssdCallbackError != kErrorPending) && (aCallback != nullptr)) + { + aCallback(aInstance, aRequestId, sDnssdCallbackError); + } +} + +void otPlatDnssdRegisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + Log("otPlatDnssdRegisterKey(aRequestId: %lu)", ToUlong(aRequestId)); + Log(" name : %s", aKey->mName); + Log(" serviceType : %s", aKey->mServiceType == nullptr ? "(null)" : aKey->mServiceType); + Log(" key data-len : %u", aKey->mKeyDataLength); + Log(" TTL : %u", aKey->mTtl); + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aKey->mInfraIfIndex == kInfraIfIndex); + + if (sDnssdShouldCheckWithClient) + { + if (aKey->mServiceType == nullptr) + { + VerifyOrQuit(StringMatch(AsCoreType(aInstance).Get().GetHostInfo().GetName(), aKey->mName)); + } + else + { + bool didFind = false; + + for (const Srp::Client::Service &service : AsCoreType(aInstance).Get().GetServices()) + { + if (StringMatch(service.GetInstanceName(), aKey->mName)) + { + didFind = true; + VerifyOrQuit(StringMatch(service.GetName(), aKey->mServiceType)); + } + } + + VerifyOrQuit(didFind); + } + } + + SuccessOrQuit(sDnssdRegKeyRequests.PushBack(DnssdRequest(aRequestId, aCallback))); + + if ((sDnssdCallbackError != kErrorPending) && (aCallback != nullptr)) + { + aCallback(aInstance, aRequestId, sDnssdCallbackError); + } +} + +void otPlatDnssdUnregisterKey(otInstance *aInstance, + const otPlatDnssdKey *aKey, + otPlatDnssdRequestId aRequestId, + otPlatDnssdRegisterCallback aCallback) +{ + Log("otPlatDnssdUnregisterKey(aRequestId: %lu)", ToUlong(aRequestId)); + Log(" name : %s", aKey->mName); + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aKey->mInfraIfIndex == kInfraIfIndex); + + if (sDnssdShouldCheckWithClient) + { + if (aKey->mServiceType == nullptr) + { + VerifyOrQuit(StringMatch(AsCoreType(aInstance).Get().GetHostInfo().GetName(), aKey->mName)); + } + else + { + bool didFind = false; + + for (const Srp::Client::Service &service : AsCoreType(aInstance).Get().GetServices()) + { + if (StringMatch(service.GetInstanceName(), aKey->mName)) + { + didFind = true; + VerifyOrQuit(StringMatch(service.GetName(), aKey->mServiceType)); + } + } + + VerifyOrQuit(didFind); + } + } + + SuccessOrQuit(sDnssdUnregKeyRequests.PushBack(DnssdRequest(aRequestId, aCallback))); + + if ((sDnssdCallbackError != kErrorPending) && (aCallback != nullptr)) + { + aCallback(aInstance, aRequestId, sDnssdCallbackError); + } +} + +// Number of times we expect each `otPlatDnssd` request to be called +struct DnssdRequestCounts : public Clearable +{ + DnssdRequestCounts(void) { Clear(); } + + uint16_t mKeyReg; + uint16_t mHostReg; + uint16_t mServiceReg; + uint16_t mKeyUnreg; + uint16_t mHostUnreg; + uint16_t mServiceUnreg; +}; + +void VerifyDnnsdRequests(const DnssdRequestCounts &aRequestCounts, bool aAllowMoreUnregs = false) +{ + VerifyOrQuit(sDnssdRegKeyRequests.GetLength() == aRequestCounts.mKeyReg); + VerifyOrQuit(sDnssdRegHostRequests.GetLength() == aRequestCounts.mHostReg); + VerifyOrQuit(sDnssdRegServiceRequests.GetLength() == aRequestCounts.mServiceReg); + + if (aAllowMoreUnregs) + { + VerifyOrQuit(sDnssdUnregKeyRequests.GetLength() >= aRequestCounts.mKeyUnreg); + VerifyOrQuit(sDnssdUnregHostRequests.GetLength() >= aRequestCounts.mHostUnreg); + VerifyOrQuit(sDnssdUnregServiceRequests.GetLength() >= aRequestCounts.mServiceUnreg); + } + else + { + VerifyOrQuit(sDnssdUnregKeyRequests.GetLength() == aRequestCounts.mKeyUnreg); + VerifyOrQuit(sDnssdUnregHostRequests.GetLength() == aRequestCounts.mHostUnreg); + VerifyOrQuit(sDnssdUnregServiceRequests.GetLength() == aRequestCounts.mServiceUnreg); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +Array sHeapAllocatedPtrs; + +#if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE +void *otPlatCAlloc(size_t aNum, size_t aSize) +{ + void *ptr = calloc(aNum, aSize); + + SuccessOrQuit(sHeapAllocatedPtrs.PushBack(ptr)); + + return ptr; +} + +void otPlatFree(void *aPtr) +{ + if (aPtr != nullptr) + { + void **entry = sHeapAllocatedPtrs.Find(aPtr); + + VerifyOrQuit(entry != nullptr, "A heap allocated item is freed twice"); + sHeapAllocatedPtrs.Remove(*entry); + } + + free(aPtr); +} +#endif + +#if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED +void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...) +{ + OT_UNUSED_VARIABLE(aLogLevel); + OT_UNUSED_VARIABLE(aLogRegion); + + va_list args; + + printf(" "); + va_start(args, aFormat); + vprintf(aFormat, args); + va_end(args); + printf("\n"); +} +#endif + +} // extern "C" + +//--------------------------------------------------------------------------------------------------------------------- + +void ProcessRadioTxAndTasklets(void) +{ + do + { + if (sRadioTxOngoing) + { + sRadioTxOngoing = false; + otPlatRadioTxStarted(sInstance, &sRadioTxFrame); + otPlatRadioTxDone(sInstance, &sRadioTxFrame, nullptr, OT_ERROR_NONE); + } + + otTaskletsProcess(sInstance); + } while (otTaskletsArePending(sInstance)); +} + +void AdvanceTime(uint32_t aDuration) +{ + uint32_t time = sNow + aDuration; + + Log("AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000); + + while (TimeMilli(sAlarmTime) <= TimeMilli(time)) + { + ProcessRadioTxAndTasklets(); + sNow = sAlarmTime; + otPlatAlarmMilliFired(sInstance); + } + + ProcessRadioTxAndTasklets(); + sNow = time; +} + +void InitTest(void) +{ + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Initialize OT instance. + + sNow = 0; + sAlarmOn = false; + sInstance = static_cast(testInitInstance()); + + memset(&sRadioTxFrame, 0, sizeof(sRadioTxFrame)); + sRadioTxFrame.mPsdu = sRadioTxFramePsdu; + sRadioTxOngoing = false; + + sDnssdShouldCheckWithClient = true; + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdCallbackError = kErrorPending; + sDnssdRegHostRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Initialize Border Router and start Thread operation. + + otOperationalDataset dataset; + otOperationalDatasetTlvs datasetTlvs; + + SuccessOrQuit(otDatasetCreateNewNetwork(sInstance, &dataset)); + SuccessOrQuit(otDatasetConvertToTlvs(&dataset, &datasetTlvs)); + SuccessOrQuit(otDatasetSetActiveTlvs(sInstance, &datasetTlvs)); + + SuccessOrQuit(otIp6SetEnabled(sInstance, true)); + SuccessOrQuit(otThreadSetEnabled(sInstance, true)); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Ensure device starts as leader. + + AdvanceTime(10000); + + VerifyOrQuit(otThreadGetDeviceRole(sInstance) == OT_DEVICE_ROLE_LEADER); +} + +void FinalizeTest(void) +{ + SuccessOrQuit(otIp6SetEnabled(sInstance, false)); + SuccessOrQuit(otThreadSetEnabled(sInstance, false)); + SuccessOrQuit(otInstanceErasePersistentInfo(sInstance)); + testFreeInstance(sInstance); +} + +//--------------------------------------------------------------------------------------------------------------------- +// SRP Client callback + +static bool sProcessedClientCallback = false; +static Error sLastClientCallbackError = kErrorNone; + +void HandleSrpClientCallback(otError aError, + const otSrpClientHostInfo *aHostInfo, + const otSrpClientService *aServices, + const otSrpClientService *aRemovedServices, + void *aContext) +{ + Log("HandleSrpClientCallback() called with error %s", ErrorToString(aError)); + + VerifyOrQuit(aContext == sInstance); + + sProcessedClientCallback = true; + sLastClientCallbackError = aError; + + OT_UNUSED_VARIABLE(aHostInfo); + OT_UNUSED_VARIABLE(aServices); + OT_UNUSED_VARIABLE(aRemovedServices); +} + +static const char kHostName[] = "awesomehost"; + +void PrepareService1(Srp::Client::Service &aService) +{ + static const char kServiceName[] = "_srv1._udp"; + static const char kInstanceLabel[] = "service1"; + static const char kSub1[] = "_sub1"; + static const char kSub2[] = "_sub2"; + static const char kSub3[] = "_sub3"; + static const char *kSubLabels[] = {kSub1, kSub2, kSub3, nullptr}; + static const char kTxtKey1[] = "ABCD"; + static const uint8_t kTxtValue1[] = {'a', '0'}; + static const char kTxtKey2[] = "Z0"; + static const uint8_t kTxtValue2[] = {'1', '2', '3'}; + static const char kTxtKey3[] = "D"; + static const uint8_t kTxtValue3[] = {0}; + static const otDnsTxtEntry kTxtEntries[] = { + {kTxtKey1, kTxtValue1, sizeof(kTxtValue1)}, + {kTxtKey2, kTxtValue2, sizeof(kTxtValue2)}, + {kTxtKey3, kTxtValue3, sizeof(kTxtValue3)}, + }; + + memset(&aService, 0, sizeof(aService)); + aService.mName = kServiceName; + aService.mInstanceName = kInstanceLabel; + aService.mSubTypeLabels = kSubLabels; + aService.mTxtEntries = kTxtEntries; + aService.mNumTxtEntries = 3; + aService.mPort = 777; + aService.mWeight = 1; + aService.mPriority = 2; +} + +void PrepareService2(Srp::Client::Service &aService) +{ + static const char kService2Name[] = "_matter._udp"; + static const char kInstance2Label[] = "service2"; + static const char kSub4[] = "_44444444"; + static const char *kSubLabels2[] = {kSub4, nullptr}; + + memset(&aService, 0, sizeof(aService)); + aService.mName = kService2Name; + aService.mInstanceName = kInstance2Label; + aService.mSubTypeLabels = kSubLabels2; + aService.mTxtEntries = nullptr; + aService.mNumTxtEntries = 0; + aService.mPort = 555; + aService.mWeight = 0; + aService.mPriority = 3; +} + +//---------------------------------------------------------------------------------------------------------------------- + +typedef Dnssd::RequestId RequestId; +typedef Dnssd::RequestIdRange RequestIdRange; + +void ValidateRequestIdRange(const RequestIdRange &aIdRange, RequestId aStart, RequestId aEnd) +{ + RequestId maxId = NumericLimits::kMax; + bool shouldContain = false; + + VerifyOrQuit(!aIdRange.IsEmpty()); + + for (RequestId id = aStart - 5; id != aEnd + 6; id++) + { + // `idRange` should contain IDs within range `[aStart, aEnd]` + + if (id == aStart) + { + shouldContain = true; + } + + if (id == aEnd + 1) + { + shouldContain = false; + } + + VerifyOrQuit(aIdRange.Contains(id) == shouldContain); + } + + // Test values that half the range apart + + for (RequestId id = aStart + maxId / 2 - 10; id != aEnd + maxId / 2 + 10; id++) + { + VerifyOrQuit(!aIdRange.Contains(id)); + } +} + +void TestDnssdRequestIdRange(void) +{ + RequestId maxId = NumericLimits::kMax; + RequestIdRange idRange; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDnssdRequestIdRange"); + + VerifyOrQuit(idRange.IsEmpty()); + + idRange.Add(5); + ValidateRequestIdRange(idRange, 5, 5); + + idRange.Remove(4); + ValidateRequestIdRange(idRange, 5, 5); + + idRange.Remove(6); + ValidateRequestIdRange(idRange, 5, 5); + + idRange.Remove(5); + VerifyOrQuit(idRange.IsEmpty()); + VerifyOrQuit(!idRange.Contains(5)); + + // Adding and removing multiple IDs + + idRange.Add(10); + idRange.Add(15); + ValidateRequestIdRange(idRange, 10, 15); + + idRange.Add(12); + ValidateRequestIdRange(idRange, 10, 15); + idRange.Add(15); + ValidateRequestIdRange(idRange, 10, 15); + idRange.Add(10); + ValidateRequestIdRange(idRange, 10, 15); + + idRange.Add(9); + ValidateRequestIdRange(idRange, 9, 15); + idRange.Add(16); + ValidateRequestIdRange(idRange, 9, 16); + + idRange.Remove(10); + ValidateRequestIdRange(idRange, 9, 16); + idRange.Remove(15); + ValidateRequestIdRange(idRange, 9, 16); + + idRange.Remove(8); + ValidateRequestIdRange(idRange, 9, 16); + idRange.Remove(17); + ValidateRequestIdRange(idRange, 9, 16); + + idRange.Remove(9); + ValidateRequestIdRange(idRange, 10, 16); + idRange.Remove(16); + ValidateRequestIdRange(idRange, 10, 15); + + idRange.Clear(); + VerifyOrQuit(idRange.IsEmpty()); + VerifyOrQuit(!idRange.Contains(10)); + + // Ranges close to roll-over max value + + idRange.Add(maxId); + ValidateRequestIdRange(idRange, maxId, maxId); + + idRange.Remove(0); + ValidateRequestIdRange(idRange, maxId, maxId); + idRange.Remove(maxId - 1); + ValidateRequestIdRange(idRange, maxId, maxId); + + idRange.Add(0); + ValidateRequestIdRange(idRange, maxId, 0); + + idRange.Add(maxId - 2); + ValidateRequestIdRange(idRange, maxId - 2, 0); + + idRange.Add(3); + ValidateRequestIdRange(idRange, maxId - 2, 3); + idRange.Add(3); + ValidateRequestIdRange(idRange, maxId - 2, 3); + + idRange.Remove(4); + ValidateRequestIdRange(idRange, maxId - 2, 3); + idRange.Remove(maxId - 3); + ValidateRequestIdRange(idRange, maxId - 2, 3); + + idRange.Remove(3); + ValidateRequestIdRange(idRange, maxId - 2, 2); + + idRange.Remove(maxId - 2); + ValidateRequestIdRange(idRange, maxId - 1, 2); + + Log("End of TestDnssdRequestIdRange"); +} + +void TestSrpAdvProxy(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxy"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add an on-mesh prefix (with SLAAC) to network data"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:cafe:beef::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorNone; // Invoke callback directly from dnssd APIs + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + srpClient->SetLeaseInterval(180); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a service"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + dnssdCounts.mKeyReg += 2; + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a second service"); + + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + // This time we should only see the new service and its key being + // registered as the host is same as before and already registered + + dnssdCounts.mKeyReg++; + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait for longer than lease interval for client to refresh"); + + sProcessedClientCallback = false; + + AdvanceTime(181 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + + // Validate that adv-proxy does not update any of registration on + // DNS-SD platform since there is no change. + + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal >= 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == advProxy->GetCounters().mAdvTotal); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add a new on-mesh prefix so to get a new host address"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:abba::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + sProcessedClientCallback = false; + + AdvanceTime(5 * 1000); + + // This time we should only see new host registration + // since that's the only thing that changes + + dnssdCounts.mHostReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove the first service on client"); + + SuccessOrQuit(srpClient->RemoveService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + // We should see the service being unregistered + // by advertising proxy on DNS-SD platform but its key + // remains registered. + + dnssdCounts.mServiceUnreg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + // Wait for more than lease interval again and make sure + // there is no change in DNS-SD platform API calls. + + sProcessedClientCallback = false; + + AdvanceTime(181 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Change service 2 on client, remove its sub-type"); + + SuccessOrQuit(srpClient->ClearService(service2)); + PrepareService2(service2); + service2.mSubTypeLabels = nullptr; + + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + // Since the service is now changed, advertising proxy + // should update it (re-register it) on DNS-SD APIs. + + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove the host on client"); + + SuccessOrQuit(srpClient->RemoveHostAndServices(/* aShouldRemoveKeyLease */ false)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + // We should see the host and service being unregistered + // on DNS-SD APIs but keys remain unchanged. + + dnssdCounts.mHostUnreg++; + dnssdCounts.mServiceUnreg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved); + VerifyOrQuit(service2.GetState() == Srp::Client::kRemoved); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove the host on client again and force an update to be sent to server"); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->RemoveHostAndServices(/* aShouldRemoveKeyLease */ false, /* aSendUnregToServer */ true)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + // We should see no changes (no calls) to DNS-SD APIs. + + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Re-add service 1 on client and register with server"); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + PrepareService1(service1); + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + // We should see one host register and one service register + // on DNS-SD APIs. Keys are already registered. + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + // Wait for more than lease interval again and make sure + // there is no change in DNS-SD platform API calls. + + sProcessedClientCallback = false; + + AdvanceTime(181 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP client and wait for lease time to expire"); + + srpClient->ClearHostAndServices(); // does not signal removal to server + + // Since we clear everything on SRP client, we disable + // matching the services with client from `otPlatDnssd` + // APIs. + sDnssdShouldCheckWithClient = false; + + AdvanceTime(181 * 1000); + + // Make sure host and service are unregistered. + + dnssdCounts.mHostUnreg++; + dnssdCounts.mServiceUnreg++; + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + VerifyOrQuit(!advProxy->IsRunning()); + + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == advProxy->GetCounters().mAdvTotal); + VerifyOrQuit(advProxy->GetCounters().mAdvTimeout == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvRejected == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvSkipped == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + dnssdCounts.mKeyUnreg += 3; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxy"); +} + +void TestSrpAdvProxyDnssdStateChange(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxyDnssdStateChange"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add an on-mesh prefix (with SLAAC) to network data"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:cafe:beef::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_STOPPED; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorNone; // Invoke callback directly + + VerifyOrQuit(!advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(!advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + srpClient->SetLeaseInterval(180); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a services"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a second service"); + + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + // None of the DNS-SD APIs should be called since its state + // `OT_PLAT_DNSSD_STOPPED` (`dnssdCounts` is all zeros). + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Update DNS-SD state and signal that state is changed"); + + sDnssdState = OT_PLAT_DNSSD_READY; + otPlatDnssdStateHandleStateChange(sInstance); + + AdvanceTime(5); + + VerifyOrQuit(advProxy->IsRunning()); + VerifyOrQuit(advProxy->GetCounters().mStateChanges == 1); + + // Now the host and two services should be registered on + // DNS-SD platform + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg += 2; + dnssdCounts.mKeyReg += 3; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait for longer than lease interval for client to refresh"); + + sProcessedClientCallback = false; + + AdvanceTime(181 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + + // Validate that adv-proxy does not update any of registration on + // DNS-SD platform since there is no change. + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Update DNS-SD state to `STOPPED` and signal its change"); + + sDnssdState = OT_PLAT_DNSSD_STOPPED; + otPlatDnssdStateHandleStateChange(sInstance); + + AdvanceTime(5); + + VerifyOrQuit(!advProxy->IsRunning()); + VerifyOrQuit(advProxy->GetCounters().mStateChanges == 2); + + // Since DNS-SD platform signal that it is stopped, + // there should be no calls to any of APIs. + + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait for longer than lease interval for client to refresh"); + + sProcessedClientCallback = false; + + AdvanceTime(181 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + // The DNS-SD API counters should remain unchanged + + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Update DNS-SD state to `READY` and signal its change"); + + sDnssdState = OT_PLAT_DNSSD_READY; + otPlatDnssdStateHandleStateChange(sInstance); + + AdvanceTime(5); + + VerifyOrQuit(advProxy->IsRunning()); + VerifyOrQuit(advProxy->GetCounters().mStateChanges == 3); + + // Check that the host and two services are again registered + // on DNS-SD platform by advertising proxy. + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg += 2; + dnssdCounts.mKeyReg += 3; + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Update DNS-SD state to `STOPPED` and signal its change"); + + sDnssdState = OT_PLAT_DNSSD_STOPPED; + otPlatDnssdStateHandleStateChange(sInstance); + + AdvanceTime(5); + + VerifyOrQuit(!advProxy->IsRunning()); + VerifyOrQuit(advProxy->GetCounters().mStateChanges == 4); + + // Since DNS-SD platform signal that it is stopped, + // there should be no calls to any of APIs. + + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove the first service on client"); + + SuccessOrQuit(srpClient->RemoveService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + // No changes to DNS-SD API counters (since it is stopped) + + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Update DNS-SD state to `READY` and signal its change #2"); + + // Since the already removed `service1` is no longer available + // on SRP client, we disable checking the services with client + // from `otPlatDnssd` APIs. + sDnssdShouldCheckWithClient = false; + + sDnssdState = OT_PLAT_DNSSD_READY; + otPlatDnssdStateHandleStateChange(sInstance); + + AdvanceTime(5); + + VerifyOrQuit(advProxy->IsRunning()); + VerifyOrQuit(advProxy->GetCounters().mStateChanges == 5); + + // We should see the host and `service2` registered again. + // And all 3 keys (even for removed `service1`) to be + // registered. + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg += 3; + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + + VerifyOrQuit(!advProxy->IsRunning()); + VerifyOrQuit(advProxy->GetCounters().mStateChanges == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvSkipped > 0); + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == + (advProxy->GetCounters().mAdvSuccessful + advProxy->GetCounters().mAdvSkipped)); + VerifyOrQuit(advProxy->GetCounters().mAdvTimeout == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvRejected == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + dnssdCounts.mHostUnreg++; + dnssdCounts.mServiceUnreg++; + dnssdCounts.mKeyUnreg += 3; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxyDnssdStateChange"); +} + +void TestSrpAdvProxyDelayedCallback(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + const DnssdRequest *request; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxyDelayedCallback"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add an on-mesh prefix (with SLAAC) to network data"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:cafe:beef::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorPending; // Do not call the callbacks directly + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + srpClient->SetLeaseInterval(180); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a service, invoke the registration callback after some delay"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(1000); + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg += 2; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + + VerifyOrQuit(!sProcessedClientCallback); + VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr); + + // Invoke the service and key callbacks first + request = &sDnssdRegServiceRequests[0]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + request = &sDnssdRegKeyRequests[0]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + request = &sDnssdRegKeyRequests[1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(10); + VerifyOrQuit(!sProcessedClientCallback); + VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr); + + // Invoke the host registration callback next + request = &sDnssdRegHostRequests[0]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(10); + VerifyOrQuit(srpServer->GetNextHost(nullptr) != nullptr); + + AdvanceTime(100); + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a second service, invoke registration callback with `kErrorDuplicated`"); + + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1000); + + VerifyOrQuit(!sProcessedClientCallback); + + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + + // Invoke the service callback with kErrorDuplicated + + request = &sDnssdRegServiceRequests[1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorDuplicated); + + AdvanceTime(100); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorDuplicated); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvRejected == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Try registering service again from client, invoke callback with success"); + + SuccessOrQuit(srpClient->ClearService(service2)); + PrepareService2(service2); + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1000); + + VerifyOrQuit(!sProcessedClientCallback); + + // We should see a new service registration request. + + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvRejected == 1); + + // Invoked the service and key callback with success. + + request = &sDnssdRegKeyRequests[sDnssdRegKeyRequests.GetLength() - 1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + request = &sDnssdRegServiceRequests[sDnssdRegServiceRequests.GetLength() - 1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(100); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvRejected == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Change the service and register again, but ignore the registration callback"); + + SuccessOrQuit(srpClient->ClearService(service2)); + PrepareService2(service2); + service2.mSubTypeLabels = nullptr; + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1000); + + VerifyOrQuit(!sProcessedClientCallback); + + // We should see a new service registration request. + + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + // Wait for advertising proxy timeout (there will be no callback from + // platform) so validate that registration failure is reported to + // the SRP client. + + AdvanceTime(2 * 1000); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError != kErrorNone); + + VerifyOrQuit(advProxy->GetCounters().mAdvTimeout == 1); + + // Wait for longer than client retry time. + + AdvanceTime(3 * 1000); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + + // Make sure the host and two services are unregistered + // (even though the second service was not successfully + // registered yet). + + VerifyOrQuit(sDnssdRegHostRequests.GetLength() == 1); + VerifyOrQuit(sDnssdRegServiceRequests.GetLength() >= 4); + VerifyOrQuit(sDnssdRegKeyRequests.GetLength() >= 3); + VerifyOrQuit(sDnssdUnregHostRequests.GetLength() == 1); + VerifyOrQuit(sDnssdUnregServiceRequests.GetLength() == 2); + VerifyOrQuit(sDnssdUnregKeyRequests.GetLength() == 3); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxyDelayedCallback"); +} + +void TestSrpAdvProxyReplacedEntries(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + const DnssdRequest *request; + uint16_t numServices; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxyReplacedEntries"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add an on-mesh prefix (with SLAAC) to network data"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:cafe:beef::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorPending; // Do not call the callbacks directly + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Set AdvTimeout to 5 minutes on AdvProxy"); + + // Change the timeout on AvdertisingProxy to 5 minutes + // so that we can send multiple SRP updates and create + // situations where previous advertisement are replaced. + + advProxy->SetAdvTimeout(5 * 60 * 1000); + VerifyOrQuit(advProxy->GetAdvTimeout() == 5 * 60 * 1000); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a service and do not invoke the registration request callbacks"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(1200); + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg += 2; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + VerifyOrQuit(!sProcessedClientCallback); + VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr); + + // SRP client min retry is 1800 msec, we wait for longer + // to make sure client retries. + + AdvanceTime(2000); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + // We should see no new service or host registrations on + // DNS-SD platform APIs as the requests should be same + // and fully matching the outstanding ones. + + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke the DNS-SD API callbacks"); + + request = &sDnssdRegServiceRequests[0]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + request = &sDnssdRegHostRequests[0]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + for (uint16_t index = 0; index < 2; index++) + { + request = &sDnssdRegKeyRequests[index]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + } + + AdvanceTime(100); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + VerifyOrQuit(srpServer->GetNextHost(nullptr) != nullptr); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Check outstanding Adv being replaced"); + + // Change service 1 + SuccessOrQuit(srpClient->ClearService(service1)); + PrepareService1(service1); + service1.mSubTypeLabels = nullptr; // No sub-types + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(1200); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + // We should see the changed service registered on DNS-SD + // platform APIs. + + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + // Change service 1 again (add sub-types back). + SuccessOrQuit(srpClient->ClearService(service1)); + PrepareService1(service1); + SuccessOrQuit(srpClient->AddService(service1)); + + AdvanceTime(1200); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + // We should see the changed service registered on DNS-SD + // platform APIs again. + + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke the replaced entry DNS-SD API callback"); + + request = &sDnssdRegServiceRequests[1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(100); + + // Since adv is replaced invoking the old registration callback + // should not complete it. + + VerifyOrQuit(!sProcessedClientCallback); + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke the new entry DNS-SD API callback"); + + request = &sDnssdRegServiceRequests[2]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(100); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + // Make sure the service entry on the SRP server is the + // last (most recent) request with three sub-types + + VerifyOrQuit(srpServer->GetNextHost(nullptr)->GetServices().GetHead() != nullptr); + VerifyOrQuit(srpServer->GetNextHost(nullptr)->GetServices().GetHead()->GetNumberOfSubTypes() == 3); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Check replacing Adv being blocked till old Adv is completed"); + + // Change service 1 and add service 2 + SuccessOrQuit(srpClient->ClearService(service1)); + PrepareService1(service1); + service1.mSubTypeLabels = nullptr; // No sub-types + SuccessOrQuit(srpClient->AddService(service1)); + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1200); + + // We should see a new Adv with two new service registrations + // on DNS-SD APIs. + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 5); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + dnssdCounts.mServiceReg += 2; + dnssdCounts.mKeyReg++; + VerifyDnnsdRequests(dnssdCounts); + + // Invoke the key registration callback + + request = &sDnssdRegKeyRequests[sDnssdRegKeyRequests.GetLength() - 1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + // Now have SRP client send a new SRP update message + // just changing `service2`. We clear `servcie1` on client + // so it is not included in new SRP update message. + + SuccessOrQuit(srpClient->ClearService(service1)); + SuccessOrQuit(srpClient->ClearService(service2)); + PrepareService2(service2); + service2.mPort = 2222; // Use a different port number + SuccessOrQuit(srpClient->AddService(service2)); + + AdvanceTime(1200); + + // We should see the new Adv (total increasing) and + // also replacing the outstanding one + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 2); + + // We should see new registration for the changed `service2` + + dnssdCounts.mServiceReg++; + VerifyDnnsdRequests(dnssdCounts); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke the callback for new registration replacing old one first"); + + request = &sDnssdRegServiceRequests[5]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(100); + + // This should not change anything, since the new Avd should + // be still blocked by the earlier Adv that it replaced. + + VerifyOrQuit(!sProcessedClientCallback); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 4); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke the callback for replaced Adv services"); + + request = &sDnssdRegServiceRequests[4]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + request = &sDnssdRegServiceRequests[3]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(100); + + // This should trigger both Adv to complete. + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 2); + + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + // Make sure the `service2` entry on the SRP server is the + // last (most recent) request with new port number. + + VerifyOrQuit(srpServer->GetNextHost(nullptr)->GetServices().GetHead() != nullptr); + + numServices = 0; + + for (const Srp::Server::Service &service : srpServer->GetNextHost(nullptr)->GetServices()) + { + numServices++; + + if (StringMatch(service.GetInstanceLabel(), service2.GetInstanceName(), kStringCaseInsensitiveMatch)) + { + VerifyOrQuit(service.GetPort() == service2.GetPort()); + } + else if (StringMatch(service.GetInstanceLabel(), service1.GetInstanceName(), kStringCaseInsensitiveMatch)) + { + // Service 1 was changed to have no sub-types + VerifyOrQuit(service.GetPort() == service1.GetPort()); + VerifyOrQuit(service.GetNumberOfSubTypes() == 0); + } + else + { + VerifyOrQuit(false); // Unexpected extra service on SRP server. + } + } + + VerifyOrQuit(numServices == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Check replacing Adv being blocked till old Adv is completed when removing services"); + + // Change and re-add both services so they are both + // included in a new SRP update message from client. + + SuccessOrQuit(srpClient->ClearService(service2)); + PrepareService1(service1); + PrepareService2(service2); + SuccessOrQuit(srpClient->AddService(service1)); + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1200); + + // We should see a new Adv with two new service registrations + // on DNS-SD APIs. + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 7); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 2); + + dnssdCounts.mServiceReg += 2; + VerifyDnnsdRequests(dnssdCounts); + + // Now have SRP client send a new SRP update message + // just removing `service1`. We clear `servcie2` on client + // so it is not included in new SRP update message. + + SuccessOrQuit(srpClient->RemoveService(service1)); + SuccessOrQuit(srpClient->ClearService(service2)); + + AdvanceTime(1200); + + // We should see a new Adv added replacing the outstanding + // one. + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 8); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 6); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 3); + + dnssdCounts.mServiceUnreg++; + VerifyDnnsdRequests(dnssdCounts); + + // Even though the new SRP update which removed `servcie2` + // is already unregistered, it should be blocked by the + // earlier Adv. + + VerifyOrQuit(!sProcessedClientCallback); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke the callback for replaced Adv services"); + + request = &sDnssdRegServiceRequests[6]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + request = &sDnssdRegServiceRequests[7]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(100); + + // This should trigger both Adv to complete, and first one + // should be committed before the second one removing the + // `service2`. + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 8); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 8); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 3); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved); + + // Check services on server and make sure `service2` + // is marked as deleted. + + VerifyOrQuit(srpServer->GetNextHost(nullptr)->GetServices().GetHead() != nullptr); + + numServices = 0; + + for (const Srp::Server::Service &service : srpServer->GetNextHost(nullptr)->GetServices()) + { + numServices++; + + if (StringMatch(service.GetInstanceLabel(), service1.GetInstanceName(), kStringCaseInsensitiveMatch)) + { + VerifyOrQuit(service.IsDeleted()); + } + else if (StringMatch(service.GetInstanceLabel(), service2.GetInstanceName(), kStringCaseInsensitiveMatch)) + { + VerifyOrQuit(!service.IsDeleted()); + } + else + { + VerifyOrQuit(false); // Unexpected extra service on SRP server. + } + } + + VerifyOrQuit(numServices == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + sDnssdShouldCheckWithClient = false; + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxyReplacedEntries"); +} + +void TestSrpAdvProxyHostWithOffMeshRoutableAddress(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + const DnssdRequest *request; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxyHostWithOffMeshRoutableAddress"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorNone; // Invoke callback directly from dnssd APIs + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + srpClient->SetLeaseInterval(400); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a service"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg += 2; + + VerifyDnnsdRequests(dnssdCounts); + VerifyOrQuit(sDnssdNumHostAddresses == 0); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register a second service"); + + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(2 * 1000); + + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + VerifyOrQuit(!advProxy->IsRunning()); + + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == advProxy->GetCounters().mAdvTotal); + VerifyOrQuit(advProxy->GetCounters().mAdvTimeout == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvRejected == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvSkipped == 0); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxyHostWithOffMeshRoutableAddress"); +} + +void TestSrpAdvProxyRemoveBeforeCommitted(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + const DnssdRequest *request; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxyRemoveBeforeCommitted"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add an on-mesh prefix (with SLAAC) to network data"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:cafe:beef::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorNone; // Do not call the callbacks directly + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register host and one service"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2000); + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg += 2; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Set AdvTimeout to 5 minutes on AdvProxy"); + + // Change the timeout on AvdertisingProxy to 5 minutes + // so that we can send multiple SRP updates and create + // situations where previous advertisement are replaced. + + advProxy->SetAdvTimeout(5 * 60 * 1000); + VerifyOrQuit(advProxy->GetAdvTimeout() == 5 * 60 * 1000); + + sDnssdCallbackError = kErrorPending; // Do not invoke callback + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove service1 while adding a new service2 and do not invoke callback from DNSSD plat"); + + SuccessOrQuit(srpClient->RemoveService(service1)); + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1000); + + dnssdCounts.mServiceReg++; + dnssdCounts.mServiceUnreg++; + dnssdCounts.mKeyReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + VerifyOrQuit(!sProcessedClientCallback); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove host and its services without removing key-lease"); + + SuccessOrQuit(srpClient->RemoveHostAndServices(/* aShouldRemoveKeyLease */ false)); + + AdvanceTime(1000); + + // Proxy will unregister both services again + // (to be safe). + + dnssdCounts.mHostUnreg++; + dnssdCounts.mServiceUnreg++; + VerifyDnnsdRequests(dnssdCounts, /* aAllowMoreUnregs */ true); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + VerifyOrQuit(!sProcessedClientCallback); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke callback for last key registration"); + + // This should be enough for all `AdvInfo` entries to be finished. + + request = &sDnssdRegKeyRequests[sDnssdRegKeyRequests.GetLength() - 1]; + VerifyOrQuit(request->mCallback != nullptr); + request->mCallback(sInstance, request->mId, kErrorNone); + + AdvanceTime(50); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + VerifyOrQuit(sProcessedClientCallback); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + sDnssdShouldCheckWithClient = false; + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxyRemoveBeforeCommitted"); +} + +void TestSrpAdvProxyFullyRemoveBeforeCommitted(void) +{ + NetworkData::OnMeshPrefixConfig prefixConfig; + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::AdvertisingProxy *advProxy; + Srp::Client::Service service1; + Srp::Client::Service service2; + DnssdRequestCounts dnssdCounts; + uint16_t heapAllocations; + const DnssdRequest *request; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestSrpAdvProxyFullyRemoveBeforeCommitted"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + advProxy = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Add an on-mesh prefix (with SLAAC) to network data"); + + prefixConfig.Clear(); + SuccessOrQuit(AsCoreType(&prefixConfig.mPrefix.mPrefix).FromString("fd00:cafe:beef::")); + prefixConfig.mPrefix.mLength = 64; + prefixConfig.mStable = true; + prefixConfig.mSlaac = true; + prefixConfig.mPreferred = true; + prefixConfig.mOnMesh = true; + prefixConfig.mDefaultRoute = false; + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + + SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig)); + SuccessOrQuit(otBorderRouterRegister(sInstance)); + + // Configure Dnssd platform API behavior + + sDnssdRegHostRequests.Clear(); + sDnssdRegServiceRequests.Clear(); + sDnssdUnregHostRequests.Clear(); + sDnssdUnregServiceRequests.Clear(); + sDnssdRegKeyRequests.Clear(); + sDnssdUnregKeyRequests.Clear(); + + sDnssdState = OT_PLAT_DNSSD_READY; + sDnssdShouldCheckWithClient = true; + sDnssdCallbackError = kErrorNone; // Do not call the callbacks directly + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP server"); + + SuccessOrQuit(otBorderRoutingInit(sInstance, /* aInfraIfIndex */ kInfraIfIndex, /* aInfraIfIsRunning */ true)); + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast); + + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetServiceHandler(nullptr, sInstance); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + VerifyOrQuit(advProxy->IsRunning()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start SRP client"); + + srpClient->SetCallback(HandleSrpClientCallback, sInstance); + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Register host and one service"); + + SuccessOrQuit(srpClient->AddService(service1)); + + sProcessedClientCallback = false; + + AdvanceTime(2000); + + dnssdCounts.mHostReg++; + dnssdCounts.mServiceReg++; + dnssdCounts.mKeyReg += 2; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + VerifyOrQuit(sProcessedClientCallback); + VerifyOrQuit(sLastClientCallbackError == kErrorNone); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Set AdvTimeout to 5 minutes on AdvProxy"); + + // Change the timeout on AvdertisingProxy to 5 minutes + // so that we can send multiple SRP updates and create + // situations where previous advertisement are replaced. + + advProxy->SetAdvTimeout(5 * 60 * 1000); + VerifyOrQuit(advProxy->GetAdvTimeout() == 5 * 60 * 1000); + + sDnssdCallbackError = kErrorPending; // Do not invoke callback + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove service1 while adding a new service2 and do not invoke callback from DNSSD plat"); + + SuccessOrQuit(srpClient->RemoveService(service1)); + SuccessOrQuit(srpClient->AddService(service2)); + + sProcessedClientCallback = false; + + AdvanceTime(1000); + + dnssdCounts.mServiceReg++; + dnssdCounts.mServiceUnreg++; + dnssdCounts.mKeyReg++; + VerifyDnnsdRequests(dnssdCounts); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 2); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 1); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 0); + + VerifyOrQuit(!sProcessedClientCallback); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Remove host and its services and remove key-lease"); + + SuccessOrQuit(srpClient->RemoveHostAndServices(/* aShouldRemoveKeyLease */ true)); + + AdvanceTime(1000); + + // Proxy should unregister everything. + // Keys may be unregistered multiple times. + + dnssdCounts.mHostUnreg++; + dnssdCounts.mServiceUnreg++; + dnssdCounts.mKeyUnreg += 3; + VerifyDnnsdRequests(dnssdCounts, /* aAllowMoreUnregs */ true); + + VerifyOrQuit(advProxy->GetCounters().mAdvTotal == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvSuccessful == 3); + VerifyOrQuit(advProxy->GetCounters().mAdvReplaced == 1); + + VerifyOrQuit(sProcessedClientCallback); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Disable SRP server"); + + sDnssdShouldCheckWithClient = false; + + // Verify that all heap allocations by SRP server + // and Advertising Proxy are freed. + + srpServer->SetEnabled(false); + AdvanceTime(100); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finalize OT instance and validate all heap allocations are freed"); + + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestSrpAdvProxyFullyRemoveBeforeCommitted"); +} + +#endif // ENABLE_ADV_PROXY_TEST + +int main(void) +{ +#if ENABLE_ADV_PROXY_TEST + TestDnssdRequestIdRange(); + TestSrpAdvProxy(); + TestSrpAdvProxyDnssdStateChange(); + TestSrpAdvProxyDelayedCallback(); + TestSrpAdvProxyReplacedEntries(); + TestSrpAdvProxyHostWithOffMeshRoutableAddress(); + TestSrpAdvProxyRemoveBeforeCommitted(); + TestSrpAdvProxyFullyRemoveBeforeCommitted(); + + printf("All tests passed\n"); +#else + printf("SRP_ADV_PROXY feature is not enabled\n"); +#endif + + return 0; +}