diff --git a/Android.mk b/Android.mk index 30369f494..6abd706d0 100644 --- a/Android.mk +++ b/Android.mk @@ -186,6 +186,7 @@ LOCAL_SRC_FILES := \ src/core/api/random_crypto_api.cpp \ src/core/api/random_noncrypto_api.cpp \ src/core/api/server_api.cpp \ + src/core/api/srp_client_api.cpp \ src/core/api/tasklet_api.cpp \ src/core/api/thread_api.cpp \ src/core/api/thread_ftd_api.cpp \ @@ -262,6 +263,7 @@ LOCAL_SRC_FILES := \ src/core/net/ip6_headers.cpp \ src/core/net/ip6_mpl.cpp \ src/core/net/netif.cpp \ + src/core/net/srp_client.cpp \ src/core/net/udp6.cpp \ src/core/radio/radio.cpp \ src/core/radio/radio_callbacks.cpp \ diff --git a/doc/ot_api_doc.h b/doc/ot_api_doc.h index e8111cd3e..9ab5e1947 100644 --- a/doc/ot_api_doc.h +++ b/doc/ot_api_doc.h @@ -56,6 +56,7 @@ * @defgroup api-dns DNSv6 * @defgroup api-icmp6 ICMPv6 * @defgroup api-ip6 IPv6 + * @defgroup api-srp SRP * @defgroup api-udp-group UDP * * @{ diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 8ad3f65d5..330fb68c8 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -167,6 +167,11 @@ if(OT_ECDSA) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_ECDSA_ENABLE=1") endif() +option(OT_SRP_CLIENT "enable SRP client support") +if (OT_SRP_CLIENT) + target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE=1") +endif() + option(OT_DUA "enable Domain Unicast Address feature for Thread 1.2") if(OT_DUA) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DUA_ENABLE=1") diff --git a/examples/README.md b/examples/README.md index 0ad87a1df..22ad5d853 100644 --- a/examples/README.md +++ b/examples/README.md @@ -56,6 +56,7 @@ This page lists the available common switches with description. Unless stated ot | SLAAC | OT_SLAAC | Enables support for adding auto-configured SLAAC addresses by OpenThread. This feature is enabled by default. | | SNTP_CLIENT | OT_SNTP_CLIENT | Enables support for SNTP Client. | | SPINEL_ENCRYPTER_LIBS | not implemented | Specifies library files (absolute paths) for implementing the NCP Spinel Encrypter. | +| SRP_CLIENT | OT_SRP_CLIENT | Enable support for SRP client. | | THREAD_VERSION | OT_THREAD_VERSION | Enables the chosen Thread version (1.1 (default) / 1.2). For example, set to `1.2` for Thread 1.2. | | TIME_SYNC | OT_TIME_SYNC | Enables the time synchronization service feature. **Note: Enabling this feature breaks conformance to the Thread Specification.** | | | UDP_FORWARD | OT_UDP_FORWARD | Enables support for UDP forward. | Enable this switch on the Border Router device (running on the NCP design) with External Commissioning support to service Thread Commissioner packets on the NCP side. | diff --git a/examples/common-switches.mk b/examples/common-switches.mk index 2f041f90c..34ce908d4 100644 --- a/examples/common-switches.mk +++ b/examples/common-switches.mk @@ -76,6 +76,7 @@ SETTINGS_RAM ?= 0 # SLAAC is enabled by default SLAAC ?= 1 SNTP_CLIENT ?= 0 +SRP_CLIENT ?= 0 THREAD_VERSION ?= 1.1 TIME_SYNC ?= 0 UDP_FORWARD ?= 0 @@ -268,6 +269,10 @@ ifeq ($(SNTP_CLIENT),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE=1 endif +ifeq ($(SRP_CLIENT),1) +COMMONCFLAGS += -DOPENTHREAD_CONFIG_SRP_CLIENT_ENABLE=1 +endif + ifeq ($(THREAD_VERSION),1.1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_THREAD_VERSION=2 else ifeq ($(THREAD_VERSION),1.2) diff --git a/include/Makefile.am b/include/Makefile.am index 57e582996..df80474e7 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -74,6 +74,7 @@ openthread_headers = \ openthread/random_noncrypto.h \ openthread/server.h \ openthread/sntp.h \ + openthread/srp_client.h \ openthread/tasklet.h \ openthread/thread.h \ openthread/thread_ftd.h \ diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn index fa64a47c2..605252376 100644 --- a/include/openthread/BUILD.gn +++ b/include/openthread/BUILD.gn @@ -114,6 +114,7 @@ source_set("openthread") { "random_noncrypto.h", "server.h", "sntp.h", + "srp_client.h", "tasklet.h", "thread.h", "thread_ftd.h", diff --git a/include/openthread/srp_client.h b/include/openthread/srp_client.h new file mode 100644 index 000000000..58928bb90 --- /dev/null +++ b/include/openthread/srp_client.h @@ -0,0 +1,503 @@ +/* + * Copyright (c) 2020, 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 defines the OpenThread SRP (Service Registration Protocol) client APIs. + */ + +#ifndef OPENTHREAD_SRP_CLIENT_H_ +#define OPENTHREAD_SRP_CLIENT_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup api-srp + * + * @brief + * This module includes functions that control SRP client behavior. + * + * @{ + * + */ + +/** + * This structure represents a TXT record entry representing a key/value pair (RFC 6763 - section 6.3). + * + * The strings buffers pointed to by `mKey` and `mValue` MUST persist and remain unchanged after an instance of such a + * structure is passed to OpenThread (as part of `otSrpClientService` instance). + * + * An array of `otSrpTxtEntry` entries is used in `otSrpClientService` to specify the full TXT record (a list of + * entries). + * + */ +typedef struct otSrpTxtEntry +{ + /** + * The TXT record key string. + * + * If `mKey` is not NULL, then the entry is treated as key/value pair with `mValue` buffer providing the value. + * - The entry is encoded as follows: + * - A single string length byte followed by "key=value" format (without the quotation marks). + - In this case, the overall encoded length must be 255 bytes or less. + * - If `mValue` is NULL, then key is treated as a boolean attribute and encoded as "key" (with no `=`). + * - If `mValue` is not NULL but `mValueLength` is zero, then it is treated as empty value and encoded as "key=". + * + * If `mKey` is NULL, then `mValue` buffer is treated as an already encoded TXT-DATA and is appended as is in the + * DNS message. + * + */ + const char * mKey; + const uint8_t *mValue; ///< The TXT record value or already encoded TXT-DATA (depending on `mKey`). + uint16_t mValueLength; ///< Number of bytes in `mValue` buffer. +} otSrpTxtEntry; + +/** + * This enumeration specifies an SRP client item (service or host info) state. + * + */ +typedef enum +{ + OT_SRP_CLIENT_ITEM_STATE_TO_ADD, ///< Item to be added/registered. + OT_SRP_CLIENT_ITEM_STATE_ADDING, ///< Item is being added/registered. + OT_SRP_CLIENT_ITEM_STATE_TO_REFRESH, ///< Item to be refreshed (re-register to renew lease). + OT_SRP_CLIENT_ITEM_STATE_REFRESHING, ///< Item is being refreshed. + OT_SRP_CLIENT_ITEM_STATE_TO_REMOVE, ///< Item to be removed. + OT_SRP_CLIENT_ITEM_STATE_REMOVING, ///< Item is being removed. + OT_SRP_CLIENT_ITEM_STATE_REGISTERED, ///< Item is registered with server. + OT_SRP_CLIENT_ITEM_STATE_REMOVED, ///< Item is removed. +} otSrpClientItemState; + +/** + * This structure represents an SRP client host info. + * + */ +typedef struct otSrpClientHostInfo +{ + const char * mName; ///< Host name (label) string (NULL if not yet set). + const otIp6Address * mAddresses; ///< Pointer to an array of host IPv6 addresses (NULL if not yet set). + uint8_t mNumAddresses; ///< Number of IPv6 addresses in `mAddresses` array. + otSrpClientItemState mState; ///< Host info state. +} otSrpClientHostInfo; + +/** + * This structure represents an SRP client service. + * + * The values in this structure, including the string buffers for the names and the TXT record entries, MUST persist + * and stay constant after an instance of this structure is passed to OpenThread from `otSrpClientAddService()` or + * `otSrpClientRemoveService()`. + * + */ +typedef struct otSrpClientService +{ + const char * mName; ///< The service name labels (e.g., "_chip._udp", not the full domain name). + const char * mInstanceName; ///< The service instance name label (not the full name). + const otSrpTxtEntry *mTxtEntries; ///< Array of TXT entries (number of entries is given by `mNumTxtEntries`). + uint16_t mPort; ///< The service port number. + uint16_t mPriority; ///< The service priority. + uint16_t mWeight; ///< The service weight. + uint8_t mNumTxtEntries; ///< Number of entries in the `mTxtEntries` array. + + /** + * @note The following fields are used/managed by OT core only. Their values do not matter and are ignored when an + * instance of `otSrpClientService` is passed in `otSrpClientAddService()` or `otSrpClientRemoveService()`. The + * user should not modify these fields. + * + */ + + otSrpClientItemState mState; ///< Service state (managed by OT core). + uint32_t mData; ///< Internal data (used by OT core). + struct otSrpClientService *mNext; ///< Pointer to next entry in a linked-list (managed by OT core). +} otSrpClientService; + +/** + * This function pointer type defines the callback used by SRP client to notify user of changes/events/errors. + * + * This callback is invoked on a successful registration of an update (i.e., add/remove of host-info and/or some + * service(s)) with the SRP server, or if there is a failure or error (e.g., server rejects a update request or client + * times out waiting for response, etc). + * + * In case of a successful reregistration of an update, `aError` parameter would be `OT_ERROR_NONE` and the host info + * and the full list of services is provided as input parameters to the callback. Note that host info and services each + * track its own state in the corresponding `mState` member variable of the related data structure (the state + * indicating whether the host-info/service is registered or removed or still being added/removed, etc). + * + * The list of removed services is passed as its own linked-list `aRemovedServices` in the callback. Note that when the + * callback is invoked, the SRP client (OpenThread implementation) is done with the removed service instances listed in + * `aRemovedServices` and no longer tracks/stores them (i.e., if from the callback we call `otSrpClientGetServices()` + * the removed services will not be present in the returned list). Providing a separate list of removed services in + * the callback helps indicate to user which items are now removed and allow user to re-claim/reuse the instances. + * + * If the server rejects an SRP update request, the DNS response code (RFC 2136) is mapped to the following errors: + * + * (0) NOERROR Success (no error condition) -> OT_ERROR_NONE + * (1) FORMERR Server unable to interpret due to format error -> OT_ERROR_PARSE + * (2) SERVFAIL Server encountered an internal failure -> OT_ERROR_FAILED + * (3) NXDOMAIN Name that ought to exist, does not exist -> OT_ERROR_NOT_FOUND + * (4) NOTIMP Server does not support the query type (OpCode) -> OT_ERROR_NOT_IMPLEMENTED + * (5) REFUSED Server refused for policy/security reasons -> OT_ERROR_SECURITY + * (6) YXDOMAIN Some name that ought not to exist, does exist -> OT_ERROR_DUPLICATED + * (7) YXRRSET Some RRset that ought not to exist, does exist -> OT_ERROR_DUPLICATED + * (8) NXRRSET Some RRset that ought to exist, does not exist -> OT_ERROR_NOT_FOUND + * (9) NOTAUTH Service is not authoritative for zone -> OT_ERROR_SECURITY + * (10) NOTZONE A name is not in the zone -> OT_ERROR_PARSE + * (20) BADNAME Bad name -> OT_ERROR_PARSE + * (21) BADALG Bad algorithm -> OT_ERROR_SECURITY + * (22) BADTRUN Bad truncation -> OT_ERROR_PARSE + * Other response codes -> OT_ERROR_FAILED + * + * The following errors are also possible: + * + * OT_ERROR_RESPONSE_TIMEOUT : Timed out waiting for response from server (client would continue to retry). + * OT_ERROR_INVALID_ARGS : The provided service structure is invalid (e.g., bad service name or `otSrpTxtEntry`). + * OT_ERROR_NO_BUFS : Insufficient buffer to prepare or send the update message. + * + * Note that in case of any failure, the client continues the operation, i.e. it prepares and (re)transmits the SRP + * update message to the server, after some wait interval. The retry wait interval starts from the minimum value and + * is increased by the growth factor every failure up to the max value (please see configuration parameter + * `OPENTHREAD_CONFIG_SRP_CLIENT_MIN_RETRY_WAIT_INTERVAL` and the related ones for more details). + * + * @param[in] aError The error (see above). + * @param[in] aHostInfo A pointer to host info. + * @param[in] aService The head of linked-list containing all services (excluding the ones removed). NULL if + * the list is empty. + * @param[in] aRemovedServices The head of linked-list containing all removed services. NULL if the list is empty. + * @param[in] aContext A pointer to an arbitrary context (provided when callback was registered). + * + */ +typedef void (*otSrpClientCallback)(otError aError, + const otSrpClientHostInfo *aHostInfo, + const otSrpClientService * aServices, + const otSrpClientService * aRemovedServices, + void * aContext); + +/** + * This function starts the SRP client operation. + * + * SRP client will prepare and send "SRP Update" message to the SRP server once all the following conditions are met: + * + * - The SRP client is started - `otSrpClientStart()` is called. + * - Host name is set - `otSrpClientSetHostName()` is called. + * - At least one host IPv6 address is set - `otSrpClientSetHostName()` is called. + * - At least one service is added - `otSrpClientAddService()` is called. + * + * It does not matter in which order these functions are called. When all conditions are met, the SRP client will + * wait for a short delay before preparing an "SRP Update" message and sending it to server. This delay allows for user + * to add multiple services and/or IPv6 addresses before the first SRP Update message is sent (ensuring a single SRP + * Update is sent containing all the info). The config `OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_DELAY` specifies the + * delay interval. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aServerSockAddr The socket address (IPv6 address and port number) of the SRP server. + * @param[in] aCallback The callback which is used to notify events and changes. Can be NULL if not needed. + * @param[in] aContext An arbitrary context used with @p aCallback. + * + * @retval OT_ERROR_NONE SRP client operation started successfully or it is already running with same server + * socket address and callback. + * @retval OT_ERROR_BUSY SRP client is busy running with a different socket address and/or callback. + * @retval OT_ERROR_FAILED Failed to open/connect the client's UDP socket. + * + */ +otError otSrpClientStart(otInstance * aInstance, + const otSockAddr * aServerSockAddr, + otSrpClientCallback aCallback, + void * aContext); + +/** + * This function stops the SRP client operation. + * + * This function stops any further interactions with the SRP server. Note that it does not remove or clear host info + * and/or list of services. It marks all services to be added/removed again once the client is (re)started. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + */ +void otSrpClientStop(otInstance *aInstance); + +/** + * This function gets the lease interval used in SRP update requests. + * + * Note that this is the lease duration requested by the SRP client. The server may choose to accept a different lease + * interval. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + * @returns The lease interval (in seconds). + * + */ +uint32_t otSrpClientGetLeaseInterval(otInstance *aInstance); + +/** + * This function sets the lease interval used in SRP update requests. + * + * Changing the lease interval does not impact the accepted lease interval of already registered services/host-info. + * It only affects any future SRP update messages (i.e., adding new services and/or refreshes of the existing services). + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aInterval The lease interval (in seconds). If zero, the default value specified by + * `OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_LEASE` would be used. + * + */ +void otSrpClientSetLeaseInterval(otInstance *aInstance, uint32_t aInterval); + +/** + * This function gets the key lease interval used in SRP update requests. + * + * Note that this is the lease duration requested by the SRP client. The server may choose to accept a different lease + * interval. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + * @returns The key lease interval (in seconds). + * + */ +uint32_t otSrpClientGetKeyLeaseInterval(otInstance *aInstance); + +/** + * This function sets the key lease interval used in SRP update requests. + * + * Changing the lease interval does not impact the accepted lease interval of already registered services/host-info. + * It only affects any future SRP update messages (i.e., adding new services and/or refreshes of existing services). + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aInterval The key lease interval (in seconds). If zero, the default value specified by + * `OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_KEY_LEASE` would be used. + * + */ +void otSrpClientSetKeyLeaseInterval(otInstance *aInstance, uint32_t aInterval); + +/** + * This function gets the host info. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + * @returns A pointer to host info structure. + * + */ +const otSrpClientHostInfo *otSrpClientGetHostInfo(otInstance *aInstance); + +/** + * This function sets the host name label. + * + * After a successful call to this function, `otSrpClientCallback` will be called to report the status of host info + * registration with SRP server. + * + * The name string buffer pointed to by @p aName MUST persist and stay unchanged after returning from this function. + * OpenThread will keep the pointer to the string. + * + * The host name can be set before client is started or after start but before host info is registered with server + * (host info should be in either `STATE_TO_ADD` or `STATE_REMOVED`). + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aName A pointer to host name label string (MUST NOT be NULL). Pointer to the string buffer MUST + * persist and remain valid and constant after return from this function. + * + * @retval OT_ERROR_NONE The host name label was set successfully. + * @retval OT_ERROR_INVALID_ARGS The @p aName is NULL. + * @retval OT_ERROR_INVALID_STATE The host name is already set and registered with the server. + * + */ +otError otSrpClientSetHostName(otInstance *aInstance, const char *aName); + +/** + * This function sets/updates the list of host IPv6 address. + * + * Host IPv6 addresses can be set/changed before start or during operation of SRP client (e.g. to add/remove or change + * a previously registered host address), except when the host info is being removed (client is busy handling a remove + * request from an earlier call to `otSrpClientRemoveHostAndServices()` and host info still being in either + * `STATE_TO_REMOVE` or `STATE_REMOVING` states). + * + * The host IPv6 address array pointed to by @p aAddresses MUST persist and remain unchanged after returning from this + * function (with `OT_ERROR_NONE`). OpenThread will save the pointer to the array. + * + * After a successful call to this function, `otSrpClientCallback` will be called to report the status of the address + * registration with SRP server. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aAddresses A pointer to the an array containing the host IPv6 addresses. + * @param[in] aNumAddresses The number of addresses in the @p aAddresses array. + * + * @retval OT_ERROR_NONE The host IPv6 address list change started successfully. The `otSrpClientCallback` + * will be called to report the status of registering addresses with server. + * @retval OT_ERROR_INVALID_ARGS The address list is invalid (e.g., must contain at least one address). + * @retval OT_ERROR_INVALID_STATE Host is being removed and therefore cannot change host address. + * + */ +otError otSrpClientSetHostAddresses(otInstance *aInstance, const otIp6Address *aIp6Addresses, uint8_t aNumAddresses); + +/** + * This function adds a service to be registered with server. + * + * After a successful call to this function, `otSrpClientCallback` will be called to report the status of the service + * addition/registration with SRP server. + * + * The `otSrpClientService` instance being pointed to by @p aService MUST persist and remain unchanged after returning + * from this function (with `OT_ERROR_NONE`). OpenThread will save the pointer to the service instance. + * + * The `otSrpClientService` instance is not longer tracked by OpenThread and can be reclaimed only when + * + * - It is removed explicitly by a call to `otSrpClientRemoveService()` or removed along with other services by a + * call to `otSrpClientRemoveHostAndServices() and only after the `otSrpClientCallback` is called indicating the + * service was removed. Or, + * - A call to `otSrpClientClearHostAndServices()` which removes the host and all related services immediately. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aService A pointer to a `otSrpClientService` instance to add. + + * @retval OT_ERROR_NONE The addition of service started successfully. The `otSrpClientCallback` will be + * called to report the status. + * @retval OT_ERROR_ALREADY The same service is already in the list. + * @retval OT_ERROR_INVALID_ARGS The service structure is invalid (e.g., bad service name or `otSrpTxtEntry`). + * + */ +otError otSrpClientAddService(otInstance *aInstance, otSrpClientService *aService); + +/** + * This function requests a service to be unregistered with server. + * + * After a successful call to this function, `otSrpClientCallback` will be called to report the status of remove + * request with SRP server. + + * The `otSrpClientService` instance being pointed to by @p aService MUST persist and remain unchanged after returning + * from this function (with `OT_ERROR_NONE`). OpenThread will keep the service instance during the remove process. + * Only after the `otSrpClientCallback` is called indicating the service instance is removed from SRP client + * service list and can be be freed/reused. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aService A pointer to a `otSrpClientService` instance to remove. + * + * @retval OT_ERROR_NONE The removal of service started successfully. The `otSrpClientCallback` will be called to + * report the status. + * @retval OT_ERROR_NOT_FOUND The service could not be found in the list. + * + */ +otError otSrpClientRemoveService(otInstance *aInstance, otSrpClientService *aService); + +/** + * This function gets the list of services being managed by client. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + * @returns A pointer to the head of linked-list of all services or NULL if the list is empty. + * + */ +const otSrpClientService *otSrpClientGetServices(otInstance *aInstance); + +/** + * This function starts the remove process of the host info and all services. + * + * After returning from this function, `otSrpClientCallback` will be called to report the status of remove request with + * SRP server. + * + * If the host info is to be permanently removed from server, @p aRemoveKeyLease should be set to `true` which removes + * the key lease associated with host on server. Otherwise, the key lease record is kept as before, which ensures + * that the server holds the host name in reserve for when the client is once again able to provide and register its + * service(s). + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aRemoveKeyLease A boolean indicating whether or not the host key lease should also be removed. + * + * @retval OT_ERROR_NONE The removal of host info and services started successfully. The `otSrpClientCallback` + * will be called to report the status. + * @retval OT_ERROR_ALREADY The host info is already removed. + * + */ +otError otSrpClientRemoveHostAndServices(otInstance *aInstance, bool aRemoveKeyLease); + +/** + * This function clears all host info and all the services. + * + * Unlike `otSrpClientRemoveHostAndServices()` which sends an update message to server to remove/unregister all the + * info, this function clears all the info immediately without any interaction with server. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + */ +void otSrpClientClearHostAndServices(otInstance *aInstance); + +/** + * This function gets the domain name being used by SRP client. + * + * This function requires `OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE` to be enabled. + * + * If domain name is not set, "default.service.arpa" will be used. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + * @returns The domain name string. + * + */ +const char *otSrpClientGetDomainName(otInstance *aInstance); + +/** + * This function sets the domain name to be used by SRP client. + * + * This function requires `OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE` to be enabled. + * + * If not set "default.service.arpa" will be used. + * + * The name string buffer pointed to by @p aName MUST persist and stay unchanged after returning from this function. + * OpenThread will keep the pointer to the string. + * + * The domain name can be set before client is started or after start but before host info is registered with server + * (host info should be in either `STATE_TO_ADD` or `STATE_TO_REMOVE`). + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aName A pointer to the domain name string. If NULL sets it to default "default.service.arpa". + * + * @retval OT_ERROR_NONE The domain name label was set successfully. + * @retval OT_ERROR_INVALID_STATE The host info is already registered with server. + * + */ +otError otSrpClientSetDomainName(otInstance *aInstance, const char *aName); + +/** + * This function converts a `otSrpClientItemState` to a string. + * + * @param[in] aItemState An item state. + * + * @returns A string representation of @p aItemState. + * + */ +const char *otSrpClientItemStateToString(otSrpClientItemState aItemState); + +/** + * @} + * + */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // OPENTHREAD_SRP_CLIENT_H_ diff --git a/script/check-arm-build-autotools b/script/check-arm-build-autotools index 831fc86a3..a741f4c96 100755 --- a/script/check-arm-build-autotools +++ b/script/check-arm-build-autotools @@ -174,6 +174,7 @@ build_nrf52833() "SERVICE=1" "SLAAC=1" "SNTP_CLIENT=1" + "SRP_CLIENT=1" "UDP_FORWARD=1" ) @@ -216,6 +217,7 @@ build_nrf52840() "SERVICE=1" "SLAAC=1" "SNTP_CLIENT=1" + "SRP_CLIENT=1" "UDP_FORWARD=1" ) diff --git a/script/check-scan-build b/script/check-scan-build index 3c38f1ea0..3695321fd 100755 --- a/script/check-scan-build +++ b/script/check-scan-build @@ -73,6 +73,7 @@ do_scan_build() "-DOPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE=1" "-DOPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE=1" "-DOPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE=1" + "-DOPENTHREAD_CONFIG_SRP_CLIENT_ENABLE=1" "-DOPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE=1" "-DOPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE=1" "-DOPENTHREAD_CONFIG_UDP_FORWARD_ENABLE=1" diff --git a/script/check-simulation-build-autotools b/script/check-simulation-build-autotools index 590c3c072..636248f32 100755 --- a/script/check-simulation-build-autotools +++ b/script/check-simulation-build-autotools @@ -80,6 +80,7 @@ build_all_features() "-DOPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE=1" "-DOPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE=1" "-DOPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE=1" + "-DOPENTHREAD_CONFIG_SRP_CLIENT_ENABLE=1" "-DOPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE=1" "-DOPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE=1" "-DOPENTHREAD_CONFIG_UDP_FORWARD_ENABLE=1" diff --git a/script/check-size b/script/check-size index b4c589797..2fc597ed0 100755 --- a/script/check-size +++ b/script/check-size @@ -126,6 +126,7 @@ size_nrf52840_version() "SERVICE=1" "SLAAC=1" "SNTP_CLIENT=1" + "SRP_CLIENT=1" "TIME_SYNC=1" "UDP_FORWARD=1" ) diff --git a/script/cmake-build b/script/cmake-build index e8f3d942a..0489aebb2 100755 --- a/script/cmake-build +++ b/script/cmake-build @@ -90,6 +90,7 @@ readonly OT_POSIX_SIM_COMMON_OPTIONS=( "-DOT_REFERENCE_DEVICE=ON" "-DOT_SERVICE=ON" "-DOT_SNTP_CLIENT=ON" + "-DOT_SRP_CLIENT=ON" "-DOT_COVERAGE=ON" "-DOT_LOG_LEVEL_DYNAMIC=ON" "-DOT_COMPILE_WARNING_AS_ERROR=ON" diff --git a/script/make-pretty b/script/make-pretty index 63f131d3d..0e2200b3e 100755 --- a/script/make-pretty +++ b/script/make-pretty @@ -109,6 +109,7 @@ readonly OT_CLANG_TIDY_BUILD_OPTS=( '-DOT_SERVICE=ON' '-DOT_SLAAC=ON' '-DOT_SNTP_CLIENT=ON' + '-DOT_SRP_CLIENT=ON' '-DOT_THREAD_VERSION=1.2' '-DOT_COVERAGE=ON' '-DOT_LOG_LEVEL_DYNAMIC=ON' diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 6890ab189..18f3e3320 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -333,6 +333,7 @@ openthread_core_files = [ "api/random_noncrypto_api.cpp", "api/server_api.cpp", "api/sntp_api.cpp", + "api/srp_client_api.cpp", "api/tasklet_api.cpp", "api/thread_api.cpp", "api/thread_ftd_api.cpp", @@ -503,6 +504,8 @@ openthread_core_files = [ "net/sntp_client.cpp", "net/sntp_client.hpp", "net/socket.hpp", + "net/srp_client.cpp", + "net/srp_client.hpp", "net/tcp.hpp", "net/udp6.cpp", "net/udp6.hpp", @@ -683,6 +686,7 @@ source_set("libopenthread_core_config") { "config/platform.h", "config/radio_link.h", "config/sntp_client.h", + "config/srp_client.h", "config/time_sync.h", "config/tmf.h", "openthread-core-config.h", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c8f0541d8..982f734a4 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -66,6 +66,7 @@ set(COMMON_SOURCES api/random_noncrypto_api.cpp api/server_api.cpp api/sntp_api.cpp + api/srp_client_api.cpp api/tasklet_api.cpp api/thread_api.cpp api/thread_ftd_api.cpp @@ -144,6 +145,7 @@ set(COMMON_SOURCES net/ip6_mpl.cpp net/netif.cpp net/sntp_client.cpp + net/srp_client.cpp net/udp6.cpp radio/radio.cpp radio/radio_callbacks.cpp diff --git a/src/core/Makefile.am b/src/core/Makefile.am index db17fc1c0..7e05b477d 100644 --- a/src/core/Makefile.am +++ b/src/core/Makefile.am @@ -143,6 +143,7 @@ SOURCES_COMMON = \ api/random_noncrypto_api.cpp \ api/server_api.cpp \ api/sntp_api.cpp \ + api/srp_client_api.cpp \ api/tasklet_api.cpp \ api/thread_api.cpp \ api/thread_ftd_api.cpp \ @@ -221,6 +222,7 @@ SOURCES_COMMON = \ net/ip6_mpl.cpp \ net/netif.cpp \ net/sntp_client.cpp \ + net/srp_client.cpp \ net/udp6.cpp \ radio/radio.cpp \ radio/radio_callbacks.cpp \ @@ -412,6 +414,7 @@ HEADERS_COMMON = \ config/platform.h \ config/radio_link.h \ config/sntp_client.h \ + config/srp_client.h \ config/time_sync.h \ config/tmf.h \ crypto/aes_ccm.hpp \ @@ -463,6 +466,7 @@ HEADERS_COMMON = \ net/netif.hpp \ net/sntp_client.hpp \ net/socket.hpp \ + net/srp_client.hpp \ net/tcp.hpp \ net/udp6.hpp \ radio/radio.hpp \ diff --git a/src/core/api/srp_client_api.cpp b/src/core/api/srp_client_api.cpp new file mode 100644 index 000000000..08b307a0e --- /dev/null +++ b/src/core/api/srp_client_api.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the OpenThread SRP client APIs. + */ + +#include "openthread-core-config.h" + +#include + +#include "common/instance.hpp" +#include "common/locator-getters.hpp" +#include "net/srp_client.hpp" + +using namespace ot; + +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + +otError otSrpClientStart(otInstance * aInstance, + const otSockAddr * aServerSockAddr, + otSrpClientCallback aCallback, + void * aContext) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().Start(*static_cast(aServerSockAddr), aCallback, aContext); +} + +void otSrpClientStop(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().Stop(); +} + +uint32_t otSrpClientGetLeaseInterval(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().GetLeaseInterval(); +} + +void otSrpClientSetLeaseInterval(otInstance *aInstance, uint32_t aInterval) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().SetLeaseInterval(aInterval); +} + +uint32_t otSrpClientGetKeyLeaseInterval(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().GetKeyLeaseInterval(); +} + +void otSrpClientSetKeyLeaseInterval(otInstance *aInstance, uint32_t aInterval) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().SetKeyLeaseInterval(aInterval); +} + +const otSrpClientHostInfo *otSrpClientGetHostInfo(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return &instance.Get().GetHostInfo(); +} + +otError otSrpClientSetHostName(otInstance *aInstance, const char *aName) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().SetHostName(aName); +} + +otError otSrpClientSetHostAddresses(otInstance *aInstance, const otIp6Address *aIp6Addresses, uint8_t aNumAddresses) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().SetHostAddresses(static_cast(aIp6Addresses), + aNumAddresses); +} + +otError otSrpClientAddService(otInstance *aInstance, otSrpClientService *aService) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().AddService(*static_cast(aService)); +} + +otError otSrpClientRemoveService(otInstance *aInstance, otSrpClientService *aService) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().RemoveService(*static_cast(aService)); +} + +const otSrpClientService *otSrpClientGetServices(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().GetServices().GetHead(); +} + +otError otSrpClientRemoveHostAndServices(otInstance *aInstance, bool aRemoveKeyLease) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().RemoveHostAndServices(aRemoveKeyLease); +} + +void otSrpClientClearHostAndServices(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + instance.Get().ClearHostAndServices(); +} + +#if OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE +const char *otSrpClientGetDomainName(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().GetDomainName(); +} + +otError otSrpClientSetDomainName(otInstance *aInstance, const char *aDomainName) +{ + Instance &instance = *static_cast(aInstance); + + return instance.Get().SetDomainName(aDomainName); +} +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE + +const char *otSrpClientItemStateToString(otSrpClientItemState aItemState) +{ + OT_ASSERT(aItemState <= OT_SRP_CLIENT_ITEM_STATE_REMOVED); + + return Srp::Client::ItemStateToString(static_cast(aItemState)); +} + +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE diff --git a/src/core/common/instance.hpp b/src/core/common/instance.hpp index 78f5472dc..57b244cfb 100644 --- a/src/core/common/instance.hpp +++ b/src/core/common/instance.hpp @@ -696,6 +696,13 @@ template <> inline Dns::Client &Instance::Get(void) } #endif +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE +template <> inline Srp::Client &Instance::Get(void) +{ + return mThreadNetif.mSrpClient; +} +#endif + #if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE template <> inline NetworkDiagnostic::NetworkDiagnostic &Instance::Get(void) { diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp index 63ff56468..583df96a0 100644 --- a/src/core/common/notifier.cpp +++ b/src/core/common/notifier.cpp @@ -183,6 +183,9 @@ void Notifier::EmitEvents(void) #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE Get().HandleNotifierEvents(events); #endif +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + Get().HandleNotifierEvents(events); +#endif for (ExternalCallback &callback : mExternalCallbacks) { diff --git a/src/core/common/settings.cpp b/src/core/common/settings.cpp index fc408261d..12a3f4aa1 100644 --- a/src/core/common/settings.cpp +++ b/src/core/common/settings.cpp @@ -516,6 +516,48 @@ exit: } #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + +otError Settings::SaveSrpKey(const Crypto::Ecdsa::P256::KeyPair &aKeyPair) +{ + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = Save(kKeySrpEcdsaKey, aKeyPair.GetDerBytes(), aKeyPair.GetDerLength())); + otLogInfoCore("Non-volatile: Saved SRP key"); + +exit: + LogFailure(error, "saving SRP key", false); + return error; +} + +otError Settings::ReadSrpKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair) const +{ + otError error; + uint16_t length = Crypto::Ecdsa::P256::KeyPair::kMaxDerSize; + + SuccessOrExit(error = Read(kKeySrpEcdsaKey, aKeyPair.GetDerBytes(), length)); + VerifyOrExit(length <= Crypto::Ecdsa::P256::KeyPair::kMaxDerSize, error = OT_ERROR_NOT_FOUND); + aKeyPair.SetDerLength(static_cast(length)); + otLogInfoCore("Non-volatile: Read SRP key"); + +exit: + return error; +} + +otError Settings::DeleteSrpKey(void) +{ + otError error; + + SuccessOrExit(error = Delete(kKeySrpEcdsaKey)); + otLogInfoCore("Non-volatile: Deleted SRP key"); + +exit: + LogFailure(error, "deleting SRP key", true); + return error; +} + +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + otError Settings::Read(Key aKey, void *aBuffer, uint16_t &aSize) const { return Get().Get(aKey, 0, reinterpret_cast(aBuffer), &aSize); diff --git a/src/core/common/settings.hpp b/src/core/common/settings.hpp index 8a4449e6d..e863db5d5 100644 --- a/src/core/common/settings.hpp +++ b/src/core/common/settings.hpp @@ -47,6 +47,9 @@ #if OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE #include "utils/slaac_address.hpp" #endif +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE +#include "crypto/ecdsa.hpp" +#endif namespace ot { @@ -591,6 +594,7 @@ public: kKeyDadInfo = 0x0008, ///< Duplicate Address Detection (DAD) information. kKeyOmrPrefix = 0x0009, ///< Off-mesh routable (OMR) prefix. kKeyOnLinkPrefix = 0x000a, ///< On-link prefix for infrastructure link. + kKeySrpEcdsaKey = 0x000b, ///< SRP client ECDSA public/private key pair. }; protected: @@ -1056,6 +1060,40 @@ public: otError ReadOnLinkPrefix(Ip6::Prefix &aOnLinkPrefix) const; #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + /** + * This method saves SRP client ECDSA key pair. + * + * @param[in] aKeyPair A reference to an SRP ECDSA key-pair to save. + * + * @retval OT_ERROR_NONE Successfully saved key-pair information in settings. + * @retval OT_ERROR_NOT_IMPLEMENTED The platform does not implement settings functionality. + * + */ + otError SaveSrpKey(const Crypto::Ecdsa::P256::KeyPair &aKeyPair); + + /** + * This method reads SRP client ECDSA key pair. + * + * @param[out] aKeyPair A reference to a ECDA `KeyPair` to output the read content. + * + * @retval OT_ERROR_NONE Successfully read key-pair information. + * @retval OT_ERROR_NOT_FOUND No corresponding value in the setting store. + * @retval OT_ERROR_NOT_IMPLEMENTED The platform does not implement settings functionality. + * + */ + otError ReadSrpKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair) const; + + /** + * This method deletes SRP client ECDSA key pair from settings. + * + * @retval OT_ERROR_NONE Successfully deleted the value. + * @retval OT_ERROR_NOT_IMPLEMENTED The platform does not implement settings functionality. + * + */ + otError DeleteSrpKey(void); +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + private: class ChildInfoIteratorBuilder : public InstanceLocator { diff --git a/src/core/config/srp_client.h b/src/core/config/srp_client.h new file mode 100644 index 000000000..09c7856e4 --- /dev/null +++ b/src/core/config/srp_client.h @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes compile-time configurations for the SRP (Service Registration Protocol) Client. + * + */ + +#ifndef CONFIG_SRP_CLIENT_H_ +#define CONFIG_SRP_CLIENT_H_ + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + * + * Define to 1 to enable SRP Client support. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE +#define OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE 0 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE + * + * Define to 1 for the SRP client implementation to provide APIs that get/set the domain name. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE +#define OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE 0 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_LEASE + * + * Specifies the default requested lease interval (in seconds). Set to two hours. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_LEASE +#define OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_LEASE (2 * 60 * 60) +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_KEY_LEASE + * + * Specifies the default requested key lease interval (in seconds). Set to 14 days. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_KEY_LEASE +#define OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_KEY_LEASE (14 * 24 * 60 * 60) +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_LEASE_RENEW_GUARD_INTERVAL + * + * Specifies the guard interval (in seconds) for lease renew time. The guard interval determines how much earlier + * (relative to the lease expiration time) the SRP client will send an SRP update for lease renewal. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_LEASE_RENEW_GUARD_INTERVAL +#define OPENTHREAD_CONFIG_SRP_CLIENT_LEASE_RENEW_GUARD_INTERVAL 120 // two minutes in seconds +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_NUMERATOR + * + * Specifies the numerator of early lease renewal factor. + * + * This value is used for opportunistic early refresh behave. When sending an SRP update, the services that are not yet + * expired but are close, are allowed to refresh early and are included in the SRP update. + * + * The "early lease renewal interval" is used to determine if a service can renew early. The interval is calculated by + * multiplying the accepted lease interval by the "early lease renewal factor" which is given as a fraction (numerator + * and denominator). + * + * If the factor is set to zero (numerator=0, denominator=1), the opportunistic early refresh behavior is disabled. + * If denominator is set to zero (the factor is set to infinity), then all services (including previously registered + * ones) are always included in SRP update message. + * + * Default value is 1/2 (i.e., services that are within half of the lease interval are allowed to refresh early). + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_NUMERATOR +#define OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_NUMERATOR 1 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR + * + * Specifies the denominator of early lease renewal factor. + * + * Please see OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_NUMERATOR for more details. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR +#define OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR 2 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_DELAY + * + * Specifies the (short) delay (in msec) after an update is required before SRP client sends the update message. + * + * When there is a change (e.g., a new service is added/removed) that requires an update, the SRP client will wait for + * a short delay before preparing and sending an SRP update message to server. This allows user to provide more change + * that are then all sent in same update message. The delay is only applied on the first change that triggers an + * update message transmission. Subsequent changes (API calls) while waiting for the tx to start will not reset the + * delay timer. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_DELAY +#define OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_DELAY 10 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_MIN_RETRY_WAIT_INTERVAL + * + * Specifies the minimum wait interval (in msec) between SRP update message retries. + * + * The update message is retransmitted if there is no response from server or if server rejects the update. The wait + * interval starts from the minimum value and is increased by the growth factor every failure up to the max value. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_MIN_RETRY_WAIT_INTERVAL +#define OPENTHREAD_CONFIG_SRP_CLIENT_MIN_RETRY_WAIT_INTERVAL 1800 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_MAX_RETRY_WAIT_INTERVAL + * + * Specifies the maximum wait interval (in msec) between SRP update message retries. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_MAX_RETRY_WAIT_INTERVAL +#define OPENTHREAD_CONFIG_SRP_CLIENT_MAX_RETRY_WAIT_INTERVAL (1 * 60 * 60 * 1000) // 1 hour in ms. +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_WAIT_INTERVAL_JITTER + * + * Specifies jitter (in msec) for retry wait interval. If the current retry wait interval is smaller than the jitter + * then the the wait interval itself is used as jitter (e.g., with jitter 500 msec and if retry interval is 300ms + * the retry interval is then randomly selected from [0, 2*300] ms). + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_WAIT_INTERVAL_JITTER +#define OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_WAIT_INTERVAL_JITTER 500 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_NUMERATOR + * + * Specifies the numerator of the retry wait interval growth factor fraction. The growth factor is represented as + * a fraction (e.g., for 1.5, we can use 15 as the numerator and 10 as the denominator). + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_NUMERATOR +#define OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_NUMERATOR 17 +#endif + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_DENOMINATOR + * + * Specifies the denominator of the retry wait interval growth factor fraction. + * + */ +#ifndef OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_DENOMINATOR +#define OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_DENOMINATOR 10 +#endif + +#endif // CONFIG_SRP_CLIENT_H_ diff --git a/src/core/net/socket.hpp b/src/core/net/socket.hpp index 9c9a2ef1e..bff21c48d 100644 --- a/src/core/net/socket.hpp +++ b/src/core/net/socket.hpp @@ -282,6 +282,40 @@ public: * */ const Address &GetAddress(void) const { return *static_cast(&mAddress); } + + /** + * This method returns the socket address port number. + * + * @returns The port number + * + */ + uint16_t GetPort(void) const { return mPort; } + + /** + * This method overloads operator `==` to evaluate whether or not two `SockAddr` instances are equal (same address + * and port number). + * + * @param[in] aOther The other `SockAddr` instance to compare with. + * + * @retval TRUE If the two `SockAddr` instances are equal. + * @retval FALSE If the two `SockAddr` instances not equal. + * + */ + bool operator==(const SockAddr &aOther) const + { + return (GetPort() == aOther.GetPort()) && (GetAddress() == aOther.GetAddress()); + } + + /** + * This method overloads operator `!=` to evaluate whether or not two `SockAddr` instances are unequal. + * + * @param[in] aOther The other `SockAddr` instance to compare with. + * + * @retval TRUE If the two `SockAddr` instances are not equal. + * @retval FALSE If the two `SockAddr` instances are equal. + * + */ + bool operator!=(const SockAddr &aOther) const { return !(*this == aOther); } }; /** diff --git a/src/core/net/srp_client.cpp b/src/core/net/srp_client.cpp new file mode 100644 index 000000000..2796e90a8 --- /dev/null +++ b/src/core/net/srp_client.cpp @@ -0,0 +1,1547 @@ +/* + * Copyright (c) 2020, 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 "srp_client.hpp" + +#include "common/code_utils.hpp" +#include "common/debug.hpp" +#include "common/instance.hpp" +#include "common/locator-getters.hpp" +#include "common/logging.hpp" +#include "common/settings.hpp" +#include "common/string.hpp" + +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + +/** + * @file + * This file implements the SRP client. + */ + +namespace ot { +namespace Srp { + +//--------------------------------------------------------------------- +// Client::TxtEntry + +otError Client::TxtEntry::AppendTo(Message &aMessage) const +{ + otError error = OT_ERROR_NONE; + uint8_t length; + + if (mKey == nullptr) + { + VerifyOrExit(mValue != nullptr); + error = aMessage.AppendBytes(mValue, mValueLength); + ExitNow(); + } + + length = static_cast(StringLength(mKey, kMaxKeyLength + 1)); + + VerifyOrExit(length <= kMaxKeyLength, error = OT_ERROR_INVALID_ARGS); + + if (mValue == nullptr) + { + // Treat as a boolean attribute and encoded as "key" (with no `=`). + SuccessOrExit(error = aMessage.Append(length)); + error = aMessage.AppendBytes(mKey, length); + ExitNow(); + } + + // Treat as key/value and encode as "key=value", value may be empty. + + VerifyOrExit(mValueLength + length + sizeof(char) <= kMaxKeyValueEncodedSize, error = OT_ERROR_INVALID_ARGS); + + length += static_cast(mValueLength + sizeof(char)); + + SuccessOrExit(error = aMessage.Append(length)); + SuccessOrExit(error = aMessage.AppendBytes(mKey, length)); + SuccessOrExit(error = aMessage.Append(kKeyValueSeparator)); + error = aMessage.AppendBytes(mValue, mValueLength); + +exit: + return error; +} + +otError Client::TxtEntry::AppendEntries(const TxtEntry *aEntries, uint8_t aNumEntries, Message &aMessage) +{ + otError error = OT_ERROR_NONE; + uint16_t startOffset = aMessage.GetLength(); + + for (uint8_t index = 0; index < aNumEntries; index++) + { + SuccessOrExit(error = aEntries[index].AppendTo(aMessage)); + } + + if (aMessage.GetLength() == startOffset) + { + error = aMessage.Append(0); + } + +exit: + return error; +} + +//--------------------------------------------------------------------- +// Client::HostInfo + +void Client::HostInfo::Init(void) +{ + Clearable::Clear(); + + // State is directly set on `mState` instead of using `SetState()` + // to avoid logging. + mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED; +} + +void Client::HostInfo::Clear(void) +{ + Clearable::Clear(); + SetState(kRemoved); +} + +void Client::HostInfo::SetState(ItemState aState) +{ + if (aState != GetState()) + { + otLogInfoSrp("[client] HostInfo %s -> %s", ItemStateToString(GetState()), ItemStateToString(aState)); + mState = static_cast(aState); + } +} + +void Client::HostInfo::SetAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses) +{ + mAddresses = aAddresses; + mNumAddresses = aNumAddresses; + + otLogInfoSrp("[client] HostInfo set %d addrs", GetNumAddresses()); + + for (uint8_t index = 0; index < GetNumAddresses(); index++) + { + otLogInfoSrp("[client] %s", GetAddress(index).ToString().AsCString()); + } +} + +//--------------------------------------------------------------------- +// Client::Service + +otError Client::Service::Init(void) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit((GetName() != nullptr) && (GetInstanceName() != nullptr), error = OT_ERROR_INVALID_ARGS); + VerifyOrExit((GetTxtEntries() != nullptr) || (GetNumTxtEntries() == 0), error = OT_ERROR_INVALID_ARGS); + + // State is directly set on `mState` instead of using `SetState()` + // to avoid logging. + mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED; + +exit: + return error; +} + +void Client::Service::SetState(ItemState aState) +{ + VerifyOrExit(GetState() != aState); + + otLogInfoSrp("[client] Service %s -> %s, \"%s\" \"%s\"", ItemStateToString(GetState()), ItemStateToString(aState), + GetInstanceName(), GetName()); + + if (aState == kToAdd) + { + // Log more details only when entering `kToAdd` state. + + otLogInfoSrp("[client] port:%d weight:%d prio:%d txts:%d", GetPort(), GetWeight(), GetPriority(), + GetNumTxtEntries()); + } + + mState = static_cast(aState); + +exit: + return; +} + +//--------------------------------------------------------------------- +// Client + +const char Client::kDefaultDomainName[] = "default.service.arpa"; + +Client::Client(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStateStopped) + , mTxFailureRetryCount(0) + , mShouldRemoveKeyLease(false) + , mUpdateMessageId(0) + , mRetryWaitInterval(kMinRetryWaitInterval) + , mAcceptedLeaseInterval(0) + , mLeaseInterval(kDefaultLease) + , mKeyLeaseInterval(kDefaultKeyLease) + , mSocket(aInstance) + , mCallback(nullptr) + , mCallbackContext(nullptr) + , mDomainName(kDefaultDomainName) + , mTimer(aInstance, Client::HandleTimer, this) +{ + mHostInfo.Init(); + + // The `Client` implementation uses different constant array of + // `ItemState` to define transitions between states in `Pause()`, + // `Stop()`, `SendUpdate`, and `ProcessResponse()`, or to convert + // an `ItemState` to string. Here, we assert that the enumeration + // values are correct. + + static_assert(kToAdd == 0, "kToAdd value is not correct"); + static_assert(kAdding == 1, "kAdding value is not correct"); + static_assert(kToRefresh == 2, "kToRefresh value is not correct"); + static_assert(kRefreshing == 3, "kRefreshing value is not correct"); + static_assert(kToRemove == 4, "kToRemove value is not correct"); + static_assert(kRemoving == 5, "kRemoving value is not correct"); + static_assert(kRegistered == 6, "kRegistered value is not correct"); + static_assert(kRemoved == 7, "kRemoved value is not correct"); +} + +otError Client::Start(const Ip6::SockAddr &aServerSockAddr, Callback aCallback, void *aContext) +{ + otError error = OT_ERROR_NONE; + + if (GetState() != kStateStopped) + { + VerifyOrExit(aServerSockAddr == mSocket.GetPeerName(), error = OT_ERROR_BUSY); + VerifyOrExit((mCallback == aCallback) && (mCallbackContext == aContext), error = OT_ERROR_BUSY); + ExitNow(); + } + + SuccessOrExit(error = mSocket.Open(Client::HandleUdpReceive, this)); + SuccessOrExit(error = mSocket.Connect(aServerSockAddr)); + + otLogInfoSrp("[client] Starting, server [%s]:%d", aServerSockAddr.GetAddress().ToString().AsCString(), + aServerSockAddr.mPort); + + mCallback = aCallback; + mCallbackContext = aContext; + Resume(); + +exit: + return error; +} + +void Client::Stop(void) +{ + // Change the state of host info and services so that they are + // added/removed again once the client is started back. In the + // case of `kAdding`, we intentionally move to `kToRefresh` + // instead of `kToAdd` since the server may receive our add + // request and the item may be registered on the server. This + // ensures that if we are later asked to remove the item, we do + // notify server. + + static const ItemState kNewStateOnStop[]{ + /* (0) kToAdd -> */ kToAdd, + /* (1) kAdding -> */ kToRefresh, + /* (2) kToRefresh -> */ kToRefresh, + /* (3) kRefreshing -> */ kToRefresh, + /* (4) kToRemove -> */ kToRemove, + /* (5) kRemoving -> */ kToRemove, + /* (6) kRegistered -> */ kToRefresh, + /* (7) kRemoved -> */ kRemoved, + }; + + VerifyOrExit(GetState() != kStateStopped); + + // State changes: + // kAdding -> kToRefresh + // kRefreshing -> kToRefresh + // kRemoving -> kToRemove + // kRegistered -> kToRefresh + + ChangeHostAndServiceStates(kNewStateOnStop); + + IgnoreError(mSocket.Close()); + mShouldRemoveKeyLease = false; + mTxFailureRetryCount = 0; + ResetRetryWaitInterval(); + SetState(kStateStopped); + +exit: + return; +} + +void Client::Resume(void) +{ + SetState(kStateUpdated); + UpdateState(); +} + +void Client::Pause(void) +{ + // Change the state of host info and services that are are being + // added or removed so that they are added/removed again once the + // client is resumed or started back. + + static const ItemState kNewStateOnPause[]{ + /* (0) kToAdd -> */ kToAdd, + /* (1) kAdding -> */ kToRefresh, + /* (2) kToRefresh -> */ kToRefresh, + /* (3) kRefreshing -> */ kToRefresh, + /* (4) kToRemove -> */ kToRemove, + /* (5) kRemoving -> */ kToRemove, + /* (6) kRegistered -> */ kRegistered, + /* (7) kRemoved -> */ kRemoved, + }; + + // State changes: + // kAdding -> kToRefresh + // kRefreshing -> kToRefresh + // kRemoving -> kToRemove + + ChangeHostAndServiceStates(kNewStateOnPause); + + SetState(kStatePaused); +} + +void Client::HandleNotifierEvents(Events aEvents) +{ + VerifyOrExit(aEvents.Contains(kEventThreadRoleChanged)); + + if (Get().IsDisabled()) + { + Stop(); + ExitNow(); + } + + if (Get().IsAttached()) + { + VerifyOrExit(GetState() == kStatePaused); + Resume(); + } + else + { + VerifyOrExit(GetState() != kStateStopped); + Pause(); + } + +exit: + return; +} + +#if OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE +otError Client::SetDomainName(const char *aName) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit((mHostInfo.GetState() == kToAdd) || (mHostInfo.GetState() == kRemoved), + error = OT_ERROR_INVALID_STATE); + + mDomainName = (aName != nullptr) ? aName : kDefaultDomainName; + otLogInfoSrp("[client] Domain name \"%s\"", mDomainName); + +exit: + return error; +} +#endif + +otError Client::SetHostName(const char *aName) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit(aName != nullptr, error = OT_ERROR_INVALID_ARGS); + + VerifyOrExit((mHostInfo.GetState() == kToAdd) || (mHostInfo.GetState() == kRemoved), + error = OT_ERROR_INVALID_STATE); + + otLogInfoSrp("[client] Host name \"%s\"", aName); + mHostInfo.SetName(aName); + mHostInfo.SetState(kToAdd); + UpdateState(); + +exit: + return error; +} + +otError Client::SetHostAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit((aAddresses != nullptr) && (aNumAddresses > 0), error = OT_ERROR_INVALID_ARGS); + + VerifyOrExit((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving), + error = OT_ERROR_INVALID_STATE); + + if (mHostInfo.GetState() == kRemoved) + { + mHostInfo.SetState(kToAdd); + } + else if (mHostInfo.GetState() != kToAdd) + { + mHostInfo.SetState(kToRefresh); + } + + mHostInfo.SetAddresses(aAddresses, aNumAddresses); + UpdateState(); + +exit: + return error; +} + +otError Client::AddService(Service &aService) +{ + otError error; + + VerifyOrExit(!mServices.Contains(aService), error = OT_ERROR_ALREADY); + + SuccessOrExit(error = aService.Init()); + mServices.Push(aService); + + aService.SetState(kToAdd); + UpdateState(); + +exit: + return error; +} + +otError Client::RemoveService(Service &aService) +{ + otError error = OT_ERROR_NONE; + LinkedList removedServices; + + VerifyOrExit(mServices.Contains(aService), error = OT_ERROR_NOT_FOUND); + + UpdateServiceStateToRemove(aService); + + // Check if the service was removed immediately, if so + // invoke the callback to report the removed service. + GetRemovedServices(removedServices); + + if (!removedServices.IsEmpty()) + { + InvokeCallback(OT_ERROR_NONE, mHostInfo, removedServices.GetHead()); + } + + UpdateState(); + +exit: + return error; +} + +void Client::UpdateServiceStateToRemove(Service &aService) +{ + if (aService.GetState() == kToAdd) + { + // If the service has not been added yet, we can remove it immediately. + aService.SetState(kRemoved); + } + else if (aService.GetState() != kRemoving) + { + aService.SetState(kToRemove); + } +} + +otError Client::RemoveHostAndServices(bool aShouldRemoveKeyLease) +{ + otError error = OT_ERROR_NONE; + + otLogInfoSrp("[client] Remove host & services"); + + VerifyOrExit(mHostInfo.GetState() != kRemoved, error = OT_ERROR_ALREADY); + + if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving)) + { + // Host info remove is already ongoing, if "key lease" remove mode is + // the same, there is no need to send a new update message. + VerifyOrExit(mShouldRemoveKeyLease != aShouldRemoveKeyLease); + } + + mShouldRemoveKeyLease = aShouldRemoveKeyLease; + + for (Service *service = mServices.GetHead(); service != nullptr; service = service->GetNext()) + { + UpdateServiceStateToRemove(*service); + } + + if (mHostInfo.GetState() == kToAdd) + { + // Host info is not added yet (not yet registered with + // server), so we can remove it and all services immediately. + mHostInfo.SetState(kRemoved); + HandleUpdateDone(); + ExitNow(); + } + + mHostInfo.SetState(kToRemove); + UpdateState(); + +exit: + return error; +} + +void Client::ClearHostAndServices(void) +{ + otLogInfoSrp("[client] Clear host & services"); + + switch (GetState()) + { + case kStateStopped: + case kStatePaused: + break; + + case kStateToUpdate: + case kStateUpdating: + case kStateUpdated: + case kStateToRetry: + SetState(kStateUpdated); + break; + } + + mTxFailureRetryCount = 0; + ResetRetryWaitInterval(); + + mServices.Clear(); + mHostInfo.Clear(); +} + +void Client::SetState(State aState) +{ + VerifyOrExit(aState != mState); + + otLogInfoSrp("[client] State %s -> %s", StateToString(mState), StateToString(aState)); + mState = aState; + + switch (mState) + { + case kStateStopped: + case kStatePaused: + case kStateUpdated: + mTimer.Stop(); + break; + + case kStateToUpdate: + mTimer.Start(kUpdateTxDelay); + break; + + case kStateUpdating: + mTimer.Start(GetRetryWaitInterval()); + break; + + case kStateToRetry: + break; + } +exit: + return; +} + +void Client::ChangeHostAndServiceStates(const ItemState *aNewStates) +{ + mHostInfo.SetState(aNewStates[mHostInfo.GetState()]); + + for (Service *service = mServices.GetHead(); service != nullptr; service = service->GetNext()) + { + service->SetState(aNewStates[service->GetState()]); + } +} + +void Client::InvokeCallback(otError aError) const +{ + InvokeCallback(aError, mHostInfo, nullptr); +} + +void Client::InvokeCallback(otError aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const +{ + VerifyOrExit(GetState() != kStateStopped); + VerifyOrExit(mCallback != nullptr); + mCallback(aError, &aHostInfo, mServices.GetHead(), aRemovedServices, mCallbackContext); + +exit: + return; +} + +void Client::SendUpdate(void) +{ + static const ItemState kNewStateOnMessageTx[]{ + /* (0) kToAdd -> */ kAdding, + /* (1) kAdding -> */ kAdding, + /* (2) kToRefresh -> */ kRefreshing, + /* (3) kRefreshing -> */ kRefreshing, + /* (4) kToRemove -> */ kRemoving, + /* (5) kRemoving -> */ kRemoving, + /* (6) kRegistered -> */ kRegistered, + /* (7) kRemoved -> */ kRemoved, + }; + + otError error = OT_ERROR_NONE; + Message *message = mSocket.NewMessage(0); + + VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS); + SuccessOrExit(error = PrepareUpdateMessage(*message)); + SuccessOrExit(error = mSocket.SendTo(*message, Ip6::MessageInfo())); + + otLogInfoSrp("[client] Send update"); + + // State changes: + // kToAdd -> kAdding + // kToRefresh -> kRefreshing + // kToRemove -> kRemoving + + ChangeHostAndServiceStates(kNewStateOnMessageTx); + + // Remember the update message tx time to use later to determine the + // lease renew time. + mLeaseRenewTime = TimerMilli::GetNow(); + mTxFailureRetryCount = 0; + + SetState(kStateUpdating); + + if (!Get().IsRxOnWhenIdle()) + { + // If device is sleepy send fast polls while waiting for + // the response from server. + Get().SendFastPolls(kFastPollsAfterUpdateTx); + } + +exit: + if (error != OT_ERROR_NONE) + { + // If there is an error in preparation or transmission of the + // update message (e.g., no buffer to allocate message), up to + // `kMaxTxFailureRetries` times, we wait for a short interval + // `kTxFailureRetryInterval` and try again. After this, we + // continue to retry using the `mRetryWaitInterval` (which keeps + // growing on each failure). + + otLogInfoSrp("[client] Failed to send update: %s", otThreadErrorToString(error)); + + FreeMessage(message); + + SetState(kStateToRetry); + + if (mTxFailureRetryCount < kMaxTxFailureRetries) + { + uint32_t interval; + + mTxFailureRetryCount++; + interval = Random::NonCrypto::AddJitter(kTxFailureRetryInterval, kTxFailureRetryJitter); + mTimer.Start(interval); + + otLogInfoSrp("[client] Quick retry %d in %u msec", mTxFailureRetryCount, interval); + + // Do not report message preparation errors to user + // until `kMaxTxFailureRetries` are exhausted. + } + else + { + LogRetryWaitInterval(); + mTimer.Start(Random::NonCrypto::AddJitter(GetRetryWaitInterval(), kRetryIntervalJitter)); + GrowRetryWaitInterval(); + InvokeCallback(error); + } + } +} + +otError Client::PrepareUpdateMessage(Message &aMessage) +{ + enum : uint16_t + { + kHeaderOffset = 0, + }; + + otError error = OT_ERROR_NONE; + Dns::UpdateHeader header; + Info info; + + info.Clear(); + + SuccessOrExit(error = ReadOrGenerateKey(info.mKeyPair)); + + // Generate random Message ID and ensure it is different from last one + do + { + SuccessOrExit(error = header.SetRandomMessageId()); + } while (header.GetMessageId() == mUpdateMessageId); + + mUpdateMessageId = header.GetMessageId(); + + // SRP Update (DNS Update) message must have exactly one record in + // Zone section, no records in Prerequisite Section, can have + // multiple records in Update Section (tracked as they are added), + // and two records in Additional Data Section (OPT and SIG records). + // The SIG record itself should not be included in calculation of + // SIG(0) signature, so the addition record count is set to one + // here. After signature calculation and appending of SIG record, + // the additional record count is updated to two and the header is + // rewritten in the message. + + header.SetZoneRecordCount(1); + header.SetAdditionalRecordCount(1); + SuccessOrExit(error = aMessage.Append(header)); + + // Prepare Zone section + + info.mDomainNameOffset = aMessage.GetLength(); + SuccessOrExit(error = Dns::Name::AppendName(mDomainName, aMessage)); + SuccessOrExit(error = aMessage.Append(Dns::Zone())); + + // Prepare Update section + + if (mHostInfo.GetState() != kToRemove) + { + for (Service *service = mServices.GetHead(); service != nullptr; service = service->GetNext()) + { + SuccessOrExit(error = AppendServiceInstructions(*service, aMessage, info)); + } + } + + SuccessOrExit(error = AppendHostDescriptionInstruction(aMessage, info)); + + header.SetUpdateRecordCount(info.mRecordCount); + aMessage.Write(kHeaderOffset, header); + + // Prepare Additional Data section + + SuccessOrExit(error = AppendUpdateLeaseOptRecord(aMessage)); + SuccessOrExit(error = AppendSignature(aMessage, info)); + + header.SetAdditionalRecordCount(2); // Lease OPT and SIG RRs + aMessage.Write(kHeaderOffset, header); + +exit: + return error; +} + +otError Client::ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair) +{ + otError error; + + error = Get().ReadSrpKey(aKeyPair); + + if (error == OT_ERROR_NONE) + { + Crypto::Ecdsa::P256::PublicKey publicKey; + + if (aKeyPair.GetPublicKey(publicKey) == OT_ERROR_NONE) + { + ExitNow(); + } + } + + SuccessOrExit(error = aKeyPair.Generate()); + IgnoreError(Get().SaveSrpKey(aKeyPair)); + +exit: + return error; +} + +otError Client::AppendServiceInstructions(Service &aService, Message &aMessage, Info &aInfo) +{ + otError error = OT_ERROR_NONE; + Dns::ResourceRecord rr; + Dns::SrvRecord srv; + bool removing; + uint16_t serviceNameOffset; + uint16_t instanceNameOffset; + uint16_t offset; + + if (aService.GetState() == kRegistered) + { + // If the lease needs to be renewed or if we are close to the + // renewal time of a registered service, we refresh the service + // early and include it in this update. This helps put more + // services on the same lease refresh schedule. + + VerifyOrExit(ShouldRenewEarly(aService)); + aService.SetState(kToRefresh); + } + + removing = ((aService.GetState() == kToRemove) || (aService.GetState() == kRemoving)); + + //---------------------------------- + // Service Discovery Instruction + + // PTR record + + // "service name labels" + (pointer to) domain name. + serviceNameOffset = aMessage.GetLength(); + SuccessOrExit(error = Dns::Name::AppendMultipleLabels(aService.GetName(), aMessage)); + SuccessOrExit(error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, aMessage)); + + // On remove, we use "Delete an RR from an RRSet" where class is set + // to NONE and TTL to zero (RFC 2136 - section 2.5.4). + + rr.Init(Dns::ResourceRecord::kTypePtr, removing ? Dns::PtrRecord::kClassNone : Dns::PtrRecord::kClassInternet); + rr.SetTtl(removing ? 0 : mLeaseInterval); + offset = aMessage.GetLength(); + SuccessOrExit(error = aMessage.Append(rr)); + + // "Instance name" + (pointer to) service name. + instanceNameOffset = aMessage.GetLength(); + SuccessOrExit(error = Dns::Name::AppendLabel(aService.GetInstanceName(), aMessage)); + SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, aMessage)); + + UpdateRecordLengthInMessage(rr, offset, aMessage); + aInfo.mRecordCount++; + + //---------------------------------- + // Service Description Instruction + + // "Delete all RRsets from a name" for Instance Name. + + SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage)); + SuccessOrExit(error = AppendDeleteAllRrsets(aMessage)); + aInfo.mRecordCount++; + + VerifyOrExit(!removing); + + // SRV RR + + SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage)); + srv.Init(); + srv.SetTtl(mLeaseInterval); + srv.SetPriority(aService.GetPriority()); + srv.SetWeight(aService.GetWeight()); + srv.SetPort(aService.GetPort()); + offset = aMessage.GetLength(); + SuccessOrExit(error = aMessage.Append(srv)); + SuccessOrExit(error = AppendHostName(aMessage, aInfo)); + UpdateRecordLengthInMessage(srv, offset, aMessage); + aInfo.mRecordCount++; + + // TXT RR + + SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage)); + rr.Init(Dns::ResourceRecord::kTypeTxt); + offset = aMessage.GetLength(); + SuccessOrExit(error = aMessage.Append(rr)); + SuccessOrExit(error = TxtEntry::AppendEntries(aService.GetTxtEntries(), aService.GetNumTxtEntries(), aMessage)); + UpdateRecordLengthInMessage(rr, offset, aMessage); + aInfo.mRecordCount++; + +exit: + return error; +} + +otError Client::AppendHostDescriptionInstruction(Message &aMessage, Info &aInfo) const +{ + otError error = OT_ERROR_NONE; + Dns::ResourceRecord rr; + Dns::KeyRecord key; + Crypto::Ecdsa::P256::PublicKey publicKey; + + //---------------------------------- + // Host Description Instruction + + // "Delete all RRsets from a name" for Host Name. + + SuccessOrExit(error = AppendHostName(aMessage, aInfo)); + SuccessOrExit(error = AppendDeleteAllRrsets(aMessage)); + aInfo.mRecordCount++; + + // AAAA RRs + + rr.Init(Dns::ResourceRecord::kTypeAaaa); + rr.SetTtl(mLeaseInterval); + rr.SetLength(sizeof(Ip6::Address)); + + for (uint8_t index = 0; index < mHostInfo.GetNumAddresses(); index++) + { + SuccessOrExit(error = AppendHostName(aMessage, aInfo)); + SuccessOrExit(error = aMessage.Append(rr)); + SuccessOrExit(error = aMessage.Append(mHostInfo.GetAddress(index))); + aInfo.mRecordCount++; + } + + // KEY RR + + SuccessOrExit(error = AppendHostName(aMessage, aInfo)); + key.Init(); + key.SetTtl(mLeaseInterval); + key.SetFlags(Dns::KeyRecord::kAuthConfidPermitted, Dns::KeyRecord::kOwnerNonZone, + Dns::KeyRecord::kSignatoryFlagGeneral); + key.SetProtocol(Dns::KeyRecord::kProtocolDnsSec); + key.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256); + key.SetLength(sizeof(Dns::KeyRecord) - sizeof(Dns::ResourceRecord) + sizeof(Crypto::Ecdsa::P256::PublicKey)); + SuccessOrExit(error = aMessage.Append(key)); + SuccessOrExit(error = aInfo.mKeyPair.GetPublicKey(publicKey)); + SuccessOrExit(error = aMessage.Append(publicKey)); + aInfo.mRecordCount++; + +exit: + return error; +} + +otError Client::AppendDeleteAllRrsets(Message &aMessage) const +{ + // "Delete all RRsets from a name" (RFC 2136 - 2.5.3) + // Name should be already appended in the message. + + Dns::ResourceRecord rr; + + rr.Init(Dns::ResourceRecord::kTypeAny, Dns::ResourceRecord::kClassAny); + rr.SetTtl(0); + rr.SetLength(0); + + return aMessage.Append(rr); +} + +otError Client::AppendHostName(Message &aMessage, Info &aInfo, bool aDoNotCompress) const +{ + otError error; + + if (aDoNotCompress) + { + // Uncompressed (canonical form) of host name is used for SIG(0) + // calculation. + SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), aMessage)); + error = Dns::Name::AppendName(mDomainName, aMessage); + ExitNow(); + } + + // If host name was previously added in the message, add it + // compressed as pointer to the previous one. Otherwise, + // append it and remember the offset. + + if (aInfo.mHostNameOffset != Info::kUnknownOffset) + { + ExitNow(error = Dns::Name::AppendPointerLabel(aInfo.mHostNameOffset, aMessage)); + } + + aInfo.mHostNameOffset = aMessage.GetLength(); + SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), aMessage)); + error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, aMessage); + +exit: + return error; +} + +otError Client::AppendUpdateLeaseOptRecord(Message &aMessage) const +{ + otError error; + Dns::OptRecord optRecord; + Dns::LeaseOption leaseOption; + + // Append empty (root domain) as OPT RR name. + SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage)); + + // `Init()` sets the type and clears (set to zero) the extended + // Response Code, version and all flags. + optRecord.Init(); + optRecord.SetUdpPayloadSize(kUdpPayloadSize); + optRecord.SetDnsSecurityFlag(); + optRecord.SetLength(sizeof(Dns::LeaseOption)); + + SuccessOrExit(error = aMessage.Append(optRecord)); + + leaseOption.Init(); + + if (mHostInfo.GetState() == kToRemove) + { + leaseOption.SetLeaseInterval(0); + leaseOption.SetKeyLeaseInterval(mShouldRemoveKeyLease ? 0 : mKeyLeaseInterval); + } + else + { + leaseOption.SetLeaseInterval(mLeaseInterval); + leaseOption.SetKeyLeaseInterval(mKeyLeaseInterval); + } + + error = aMessage.Append(leaseOption); + +exit: + return error; +} + +otError Client::AppendSignature(Message &aMessage, Info &aInfo) +{ + otError error; + Dns::SigRecord sig; + Crypto::Sha256 sha256; + Crypto::Sha256::Hash hash; + Crypto::Ecdsa::P256::Signature signature; + uint16_t offset; + uint16_t len; + + // Prepare SIG RR: TTL, type covered, labels count should be set + // to zero. Since we have no clock, inception and expiration time + // are also set to zero. The RDATA length will be set later (not + // yet known due to variably (and possible compression) of signer's + // name. + + sig.Clear(); + sig.Init(Dns::ResourceRecord::kClassAny); + sig.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256); + + // Append the SIG RR with full uncompressed form of the host name + // as the signer's name. This is used for SIG(0) calculation only. + // It will be overwritten with host name compressed. + + offset = aMessage.GetLength(); + SuccessOrExit(error = aMessage.Append(sig)); + SuccessOrExit(error = AppendHostName(aMessage, aInfo, /* aDoNotCompress */ true)); + + // Calculate signature (RFC 2931): Calculated over "data" which is + // concatenation of (1) the SIG RR RDATA wire format (including + // the canonical form of the signer's name), entirely omitting the + // signature subfield, (2) DNS query message, including DNS header + // but not UDP/IP header before the header RR counts have been + // adjusted for the inclusion of SIG(0). + + sha256.Start(); + + // (1) SIG RR RDATA wire format + len = aMessage.GetLength() - offset - sizeof(Dns::ResourceRecord); + sha256.Update(aMessage, offset + sizeof(Dns::ResourceRecord), len); + + // (2) Message from DNS header before SIG + sha256.Update(aMessage, 0, offset); + + sha256.Finish(hash); + SuccessOrExit(error = aInfo.mKeyPair.Sign(hash, signature)); + + // Move back in message and append SIG RR now with compressed host + // name (as signer's name) along with the calculated signature. + + IgnoreError(aMessage.SetLength(offset)); + + // SIG(0) uses owner name of root (single zero byte). + SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage)); + + offset = aMessage.GetLength(); + SuccessOrExit(error = aMessage.Append(sig)); + SuccessOrExit(error = AppendHostName(aMessage, aInfo)); + SuccessOrExit(error = aMessage.Append(signature)); + UpdateRecordLengthInMessage(sig, offset, aMessage); + +exit: + return error; +} + +void Client::UpdateRecordLengthInMessage(Dns::ResourceRecord &aRecord, uint16_t aOffset, Message &aMessage) const +{ + // This method is used to calculate an RR DATA length and update + // (rewrite) it in a message. This should be called immediately + // after all the fields in the record are written in the message. + // `aOffset` gives the offset in the message to the start of the + // record. + + aRecord.SetLength(aMessage.GetLength() - aOffset - sizeof(Dns::ResourceRecord)); + aMessage.Write(aOffset, aRecord); +} + +void Client::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo) +{ + OT_UNUSED_VARIABLE(aMessageInfo); + + static_cast(aContext)->ProcessResponse(*static_cast(aMessage)); +} + +void Client::ProcessResponse(Message &aMessage) +{ + static const ItemState kNewStateOnUpdateDone[]{ + /* (0) kToAdd -> */ kToAdd, + /* (1) kAdding -> */ kRegistered, + /* (2) kToRefresh -> */ kToRefresh, + /* (3) kRefreshing -> */ kRegistered, + /* (4) kToRemove -> */ kToRemove, + /* (5) kRemoving -> */ kRemoved, + /* (6) kRegistered -> */ kRegistered, + /* (7) kRemoved -> */ kRemoved, + }; + + otError error = OT_ERROR_NONE; + Dns::UpdateHeader header; + uint16_t offset = aMessage.GetOffset(); + uint16_t recordCount; + LinkedList removedServices; + + VerifyOrExit(GetState() == kStateUpdating); + + SuccessOrExit(error = aMessage.Read(offset, header)); + + VerifyOrExit(header.GetType() == Dns::Header::kTypeResponse, error = OT_ERROR_PARSE); + VerifyOrExit(header.GetQueryType() == Dns::Header::kQueryTypeUpdate, error = OT_ERROR_PARSE); + VerifyOrExit(header.GetMessageId() == mUpdateMessageId, error = OT_ERROR_DROP); + + if (!Get().IsRxOnWhenIdle()) + { + Get().StopFastPolls(); + } + + // Response is for the earlier request message. + + otLogInfoSrp("[client] Received response"); + + error = Dns::Header::ResponseCodeToError(header.GetResponseCode()); + + if (error != OT_ERROR_NONE) + { + otLogInfoSrp("[client] Server rejected %s code:%d", otThreadErrorToString(error), header.GetResponseCode()); + + if (mHostInfo.GetState() == kAdding) + { + // Since server rejected the update message, we go back to + // `kToAdd` state to allow user to give a new name using + // `SetHostName()`. + mHostInfo.SetState(kToAdd); + } + + // Wait for the timer to expire to retry. Note that timer is + // already scheduled for the current wait interval when state + // was changed to `kStateUpdating`. + + LogRetryWaitInterval(); + GrowRetryWaitInterval(); + SetState(kStateToRetry); + InvokeCallback(error); + ExitNow(error = OT_ERROR_NONE); + } + + offset += sizeof(header); + + // Skip over all sections till Additional Data section + // SPEC ENHANCEMENT: Sever can echo the request back or not + // include any of RRs. Would be good to explicitly require SRP server + // to not echo back RRs. + + if (header.GetZoneRecordCount() != 0) + { + VerifyOrExit(header.GetZoneRecordCount() == 1, error = OT_ERROR_PARSE); + SuccessOrExit(error = Dns::Name::ParseName(aMessage, offset)); + VerifyOrExit(offset + sizeof(Dns::Zone) <= aMessage.GetLength(), error = OT_ERROR_PARSE); + offset += sizeof(Dns::Zone); + } + + // Check for Update Lease OPT RR. This determines the lease + // interval accepted by server. If not present, then use the + // transmitted lease interval from the update request message. + + mAcceptedLeaseInterval = mLeaseInterval; + recordCount = + header.GetPrerequisiteRecordCount() + header.GetUpdateRecordCount() + header.GetAdditionalRecordCount(); + + while (recordCount > 0) + { + uint16_t startOffset = offset; + Dns::ResourceRecord rr; + + SuccessOrExit(error = ReadResourceRecord(aMessage, offset, rr)); + recordCount--; + + if (rr.GetType() == Dns::ResourceRecord::kTypeOpt) + { + SuccessOrExit(error = ProcessOptRecord(aMessage, startOffset, static_cast(rr))); + } + } + + // Calculate the lease renew time based on update message tx time + // and the lease time. `kLeaseRenewGuardInterval` is used to + // ensure that we renew the lease before server expires it. In the + // unlikely (but maybe useful for testing) case where the accepted + // lease interval is too short (shorter than the guard time) we + // just use half of the accepted lease interval. + + if (mAcceptedLeaseInterval > kLeaseRenewGuardInterval) + { + mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval - kLeaseRenewGuardInterval); + } + else + { + mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval) / 2; + } + + for (Service *service = mServices.GetHead(); service != nullptr; service = service->GetNext()) + { + if ((service->GetState() == kAdding) || (service->GetState() == kRefreshing)) + { + service->SetLeaseRenewTime(mLeaseRenewTime); + } + } + + // State changes: + // kAdding -> kRegistered + // kRefreshing -> kRegistered + // kRemoving -> kRemoved + + ChangeHostAndServiceStates(kNewStateOnUpdateDone); + + HandleUpdateDone(); + UpdateState(); + +exit: + if (error != OT_ERROR_NONE) + { + otLogInfoSrp("[clinet] Failed to process response %s", otThreadErrorToString(error)); + } +} + +void Client::HandleUpdateDone(void) +{ + HostInfo hostInfoCopy = mHostInfo; + LinkedList removedServices; + + if (mHostInfo.GetState() == kRemoved) + { + mHostInfo.Clear(); + } + + ResetRetryWaitInterval(); + SetState(kStateUpdated); + + GetRemovedServices(removedServices); + InvokeCallback(OT_ERROR_NONE, hostInfoCopy, removedServices.GetHead()); +} + +void Client::GetRemovedServices(LinkedList &aRemovedServices) +{ + Service *service; + Service *prev; + Service *next; + + for (prev = nullptr, service = mServices.GetHead(); service != nullptr; service = next) + { + next = service->GetNext(); + + if (service->GetState() == kRemoved) + { + mServices.PopAfter(prev); + aRemovedServices.Push(*service); + + // When the service is removed from the list + // we keep the `prev` pointer same as before. + } + else + { + prev = service; + } + } +} + +otError Client::ReadResourceRecord(const Message &aMessage, uint16_t &aOffset, Dns::ResourceRecord &aRecord) +{ + // Reads and skips over a Resource Record (RR) from message at + // given offset. On success, `aOffset` is updated to point to end + // of RR. + + otError error; + + SuccessOrExit(error = Dns::Name::ParseName(aMessage, aOffset)); + SuccessOrExit(error = aMessage.Read(aOffset, aRecord)); + VerifyOrExit(aOffset + aRecord.GetSize() <= aMessage.GetLength(), error = OT_ERROR_PARSE); + aOffset += static_cast(aRecord.GetSize()); + +exit: + return error; +} + +otError Client::ProcessOptRecord(const Message &aMessage, uint16_t aOffset, const Dns::OptRecord &aOptRecord) +{ + // Read and process all options (in an OPT RR) from a message. + // The `aOffset` points to beginning of record in `aMessage`. + + otError error = OT_ERROR_NONE; + uint16_t len; + + IgnoreError(Dns::Name::ParseName(aMessage, aOffset)); + aOffset += sizeof(Dns::OptRecord); + + len = aOptRecord.GetLength(); + + while (len > 0) + { + Dns::LeaseOption leaseOption; + Dns::Option & option = leaseOption; + uint16_t size; + + SuccessOrExit(error = aMessage.Read(aOffset, option)); + + VerifyOrExit(aOffset + option.GetSize() <= aMessage.GetLength(), error = OT_ERROR_PARSE); + + if ((option.GetOptionCode() == Dns::Option::kUpdateLease) && + (option.GetOptionLength() >= Dns::LeaseOption::kOptionLength)) + { + SuccessOrExit(error = aMessage.Read(aOffset, leaseOption)); + + mAcceptedLeaseInterval = leaseOption.GetLeaseInterval(); + + if (mAcceptedLeaseInterval > kMaxLease) + { + mAcceptedLeaseInterval = kMaxLease; + } + } + + size = static_cast(option.GetSize()); + aOffset += size; + len -= size; + } + +exit: + return error; +} + +void Client::UpdateState(void) +{ + TimeMilli now = TimerMilli::GetNow(); + TimeMilli earliestRenewTime = now.GetDistantFuture(); + bool shouldUpdate = false; + + VerifyOrExit((GetState() != kStateStopped) && (GetState() != kStatePaused)); + VerifyOrExit((mHostInfo.GetName() != nullptr) && (mHostInfo.GetNumAddresses() > 0)); + + // Go through the host info and all the services to check if there + // are any new changes (i.e., anything new to add or remove). This + // is used to determine whether to send an SRP update message or + // not. Also keep track of the earliest renew time among the + // previously registered services. This is used to schedule the + // timer for next refresh. + + switch (mHostInfo.GetState()) + { + case kAdding: + case kRefreshing: + case kRemoving: + break; + + case kRegistered: + if (now < mLeaseRenewTime) + { + break; + } + + mHostInfo.SetState(kToRefresh); + + // Fall through + + case kToAdd: + case kToRefresh: + // Make sure we have at least one service otherwise no need to + // send SRP update message with host info only. The exception + // is when removing host info where we allow for empty + // service list. + VerifyOrExit(!mServices.IsEmpty()); + + // Fall through + + case kToRemove: + shouldUpdate = true; + break; + + case kRemoved: + ExitNow(); + } + + // If host info is being removed, we skip over checking service list + // for new adds (or removes). This handles the situation where while + // remove is ongoing and before we get a response from the server, + // user adds a new service to be registered. We wait for remove to + // finish (receive response from server) before starting with a new + // service adds. + + if (mHostInfo.GetState() != kRemoving) + { + for (Service *service = mServices.GetHead(); service != nullptr; service = service->GetNext()) + { + switch (service->GetState()) + { + case kToAdd: + case kToRefresh: + case kToRemove: + shouldUpdate = true; + break; + + case kRegistered: + if (service->GetLeaseRenewTime() <= now) + { + service->SetState(kToRefresh); + shouldUpdate = true; + } + else if (service->GetLeaseRenewTime() < earliestRenewTime) + { + earliestRenewTime = service->GetLeaseRenewTime(); + } + + break; + + case kAdding: + case kRefreshing: + case kRemoving: + case kRemoved: + break; + } + } + } + + if (shouldUpdate) + { + SetState(kStateToUpdate); + ExitNow(); + } + + if ((GetState() == kStateUpdated) && (earliestRenewTime != now.GetDistantFuture())) + { + mTimer.FireAt(earliestRenewTime); + } + +exit: + return; +} + +void Client::GrowRetryWaitInterval(void) +{ + mRetryWaitInterval = + mRetryWaitInterval / kRetryIntervalGrowthFactorDenominator * kRetryIntervalGrowthFactorNumerator; + + if (mRetryWaitInterval > kMaxRetryWaitInterval) + { + mRetryWaitInterval = kMaxRetryWaitInterval; + } +} + +uint32_t Client::GetBoundedLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const +{ + uint32_t boundedInterval = aDefaultInterval; + + if (aInterval != 0) + { + boundedInterval = OT_MIN(aInterval, static_cast(kMaxLease)); + } + + return boundedInterval; +} + +bool Client::ShouldRenewEarly(const Service &aService) const +{ + // Check if we reached the service renew time or close to it. The + // "early renew interval" is used to allow early refresh. It is + // calculated as a factor of the `mAcceptedLeaseInterval`. The + // "early lease renew factor" is given as a fraction (numerator and + // denominator). If the denominator is set to zero (i.e., factor is + // set to infinity), then service is always included in all SRP + // update messages. + + bool shouldRenew; + +#if OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR != 0 + uint32_t earlyRenewInterval = + Time::SecToMsec(mAcceptedLeaseInterval) / kEarlyLeaseRenewFactorDenominator * kEarlyLeaseRenewFactorNumerator; + + shouldRenew = (aService.GetLeaseRenewTime() <= TimerMilli::GetNow() + earlyRenewInterval); +#else + OT_UNUSED_VARIABLE(aService); + shouldRenew = true; +#endif + + return shouldRenew; +} + +void Client::HandleTimer(Timer &aTimer) +{ + aTimer.GetOwner().HandleTimer(); +} + +void Client::HandleTimer(void) +{ + switch (GetState()) + { + case kStateStopped: + case kStatePaused: + break; + + case kStateToUpdate: + case kStateToRetry: + SendUpdate(); + break; + + case kStateUpdating: + LogRetryWaitInterval(); + otLogInfoSrp("[client] Timed out, no response"); + GrowRetryWaitInterval(); + SetState(kStateToUpdate); + InvokeCallback(OT_ERROR_RESPONSE_TIMEOUT); + break; + + case kStateUpdated: + UpdateState(); + break; + } +} + +const char *Client::ItemStateToString(ItemState aState) +{ + static const char *const kItemStateStrings[] = { + "ToAdd", // kToAdd (0) + "Adding", // kAdding (1) + "ToRefresh", // kToRefresh (2) + "Refreshing", // kRefreshing (3) + "ToRemove", // kToRemove (4) + "Removing", // kRemoving (5) + "Registered", // kRegistered (6) + "Removed", // kRemoved (7) + }; + + return kItemStateStrings[aState]; +} + +#if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO) && (OPENTHREAD_CONFIG_LOG_SRP == 1) + +const char *Client::StateToString(State aState) +{ + static const char *const kStateStrings[] = { + "Stopped", // kStateStopped (0) + "Paused", // kStatePaused (1) + "ToUpdate", // kStateToUpdate (2) + "Updating", // kStateUpdating (3) + "Updated", // kStateUpdated (4) + "ToRetry", // kStateToRetry (5) + }; + + static_assert(kStateStopped == 0, "kStateStopped value is not correct"); + static_assert(kStatePaused == 1, "kStatePaused value is not correct"); + static_assert(kStateToUpdate == 2, "kStateToUpdate value is not correct"); + static_assert(kStateUpdating == 3, "kStateUpdating value is not correct"); + static_assert(kStateUpdated == 4, "kStateUpdated value is not correct"); + static_assert(kStateToRetry == 5, "kStateToRetry value is not correct"); + + return kStateStrings[aState]; +} + +void Client::LogRetryWaitInterval(void) const +{ + enum : uint16_t + { + kLogInMsecLimit = 5000, // Max interval (in msec) to log the value in msec unit + kMsecInSec = 1000, + }; + + uint32_t interval = GetRetryWaitInterval(); + + otLogInfoSrp("[client] Retry interval %u %s", (interval < kLogInMsecLimit) ? interval : Time::MsecToSec(interval), + (interval < kLogInMsecLimit) ? "ms" : "sec"); +} + +#endif // #if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO) && (OPENTHREAD_CONFIG_LOG_SRP == 1) + +} // namespace Srp +} // namespace ot + +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE diff --git a/src/core/net/srp_client.hpp b/src/core/net/srp_client.hpp new file mode 100644 index 000000000..033ed2b04 --- /dev/null +++ b/src/core/net/srp_client.hpp @@ -0,0 +1,732 @@ +/* + * Copyright (c) 2020, 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. + */ + +#ifndef SRP_CLIENT_HPP_ +#define SRP_CLIENT_HPP_ + +#include "openthread-core-config.h" + +#include + +#include "common/clearable.hpp" +#include "common/linked_list.hpp" +#include "common/locator.hpp" +#include "common/message.hpp" +#include "common/non_copyable.hpp" +#include "common/notifier.hpp" +#include "common/timer.hpp" +#include "crypto/ecdsa.hpp" +#include "net/dns_headers.hpp" +#include "net/ip6.hpp" +#include "net/udp6.hpp" + +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + +/** + * @file + * This file includes definitions for the SRP (Service Registration Protocol) client. + */ + +namespace ot { +namespace Srp { + +#if !OPENTHREAD_CONFIG_ECDSA_ENABLE +#error "SRP Client feature requires ECDSA support (OPENTHREAD_CONFIG_ECDSA_ENABLE)." +#endif + +/** + * This class implements SRP client. + * + */ +class Client : public InstanceLocator, private NonCopyable +{ + friend class ot::Notifier; + +public: + /** + * This enumeration types represents an SRP client item (service or host info) state. + * + */ + enum ItemState : uint8_t + { + kToAdd = OT_SRP_CLIENT_ITEM_STATE_TO_ADD, ///< Item to be added/registered. + kAdding = OT_SRP_CLIENT_ITEM_STATE_ADDING, ///< Item is being added/registered. + kToRefresh = OT_SRP_CLIENT_ITEM_STATE_TO_REFRESH, ///< Item to be refreshed (renew lease). + kRefreshing = OT_SRP_CLIENT_ITEM_STATE_REFRESHING, ///< Item is being refreshed. + kToRemove = OT_SRP_CLIENT_ITEM_STATE_TO_REMOVE, ///< Item to be removed. + kRemoving = OT_SRP_CLIENT_ITEM_STATE_REMOVING, ///< Item is being removed. + kRegistered = OT_SRP_CLIENT_ITEM_STATE_REGISTERED, ///< Item is registered with server. + kRemoved = OT_SRP_CLIENT_ITEM_STATE_REMOVED, ///< Item is removed. + }; + + /** + * This function pointer type defines the callback used by SRP client to notify user of a changes/events/errors. + * + * Please see `otSrpClientCallback` for more details. + * + */ + typedef otSrpClientCallback Callback; + + /** + * This type represents a TXT record entry representing a key/value pair (RFC 6763 - section 6.3). + * + */ + class TxtEntry : public otSrpTxtEntry + { + public: + /** + * This method encodes and appends the `TxtEntry` to a message. + * + * @param[in] aMessage The message to append to. + * + * @retval OT_ERROR_NONE Entry was appended successfully to @p aMessage. + * @retval OT_ERROR_INVALID_ARGS The `TxEntry` info is not valid. + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + otError AppendTo(Message &aMessage) const; + + /** + * This static method appends an array of `TxtEntry` items to a message. + * + * @param[in] aEntries A pointer to array of `TxtEntry` items. + * @param[in] aNumEntries The number of entries in @p aEntries array. + * @param[in] aMessage The message to append to. + * + * + * @retval OT_ERROR_NONE Entries appended successfully to @p aMessage. + * @retval OT_ERROR_INVALID_ARGS The `TxEntry` info is not valid. + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + static otError AppendEntries(const TxtEntry *aEntries, uint8_t aNumEntries, Message &aMessage); + + private: + enum : char + { + kKeyValueSeparator = '=', + }; + + enum : uint8_t + { + kMinKeyLength = 1, + kMaxKeyLength = 9, + kMaxKeyValueEncodedSize = 255, + }; + }; + + /** + * This type represents an SRP client host info. + * + */ + class HostInfo : public otSrpClientHostInfo, public Clearable + { + friend class Client; + + public: + /** + * This method initializes the `HostInfo` object. + * + */ + void Init(void); + + /** + * This method clears the `HostInfo` object. + * + */ + void Clear(void); + + /** + * This method gets the host name (label) string. + * + * @returns The host name (label) string, or nullptr if not yet set. + * + */ + const char *GetName(void) const { return mName; } + + /** + * This method gets the number of host IPv6 addresses. + * + * @returns The number of host IPv6 addresses. + * + */ + uint8_t GetNumAddresses(void) const { return mNumAddresses; } + + /** + * This method gets the host IPv6 address at a given index. + * + * @param[in] aIndex The index to get (MUST be smaller than `GetNumAddresses()`). + * + * @returns The host IPv6 address at index @p aIndex. + * + */ + const Ip6::Address &GetAddress(uint8_t aIndex) const + { + return static_cast(mAddresses[aIndex]); + } + + /** + * This method gets the state of `HostInfo`. + * + * @returns The `HostInfo` state. + * + */ + ItemState GetState(void) const { return static_cast(mState); } + + private: + void SetName(const char *aName) { mName = aName; } + void SetState(ItemState aState); + void SetAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses); + }; + + /** + * This type represents an SRP client service. + * + */ + class Service : public otSrpClientService, public LinkedListEntry + { + friend class Client; + + public: + /** + * This method initializes and validates the `Service` object and its fields. + * + * @retval OT_ERROR_NONE Successfully initialized and validated the `Service` object. + * @retval OT_ERROR_INVALID_ARGS The info in `Service` object is not valid (e.g. null name or bad `TxtEntry`). + * + */ + otError Init(void); + + /** + * This method gets the service name labels string. + * + * @returns The service name label string (e.g., "_chip._udp", not the full domain name). + * + */ + const char *GetName(void) const { return mName; } + + /** + * This method gets the service instance name label (not the full name). + * + * @returns The service instance name label string. + * + */ + const char *GetInstanceName(void) const { return mInstanceName; } + + /** + * This method gets the service port number. + * + * @returns The service port number. + * + */ + uint16_t GetPort(void) const { return mPort; } + + /** + * This method gets the service priority. + * + * @returns The service priority. + * + */ + uint16_t GetPriority(void) const { return mPriority; } + + /** + * This method gets the service weight. + * + * @returns The service weight. + * + */ + uint16_t GetWeight(void) const { return mWeight; } + + /** + * This method gets the array of service TXT entries. + * + * @returns A pointer to an array of service TXT entries. + * + */ + const TxtEntry *GetTxtEntries(void) const { return static_cast(mTxtEntries); } + + /** + * This method gets the number of entries in the service TXT entry array. + * + * @returns The number of entries in the service TXT entry array. + * + */ + uint8_t GetNumTxtEntries(void) const { return mNumTxtEntries; } + + /** + * This method get the state of service. + * + * @returns The service state. + * + */ + ItemState GetState(void) const { return static_cast(mState); } + + private: + void SetState(ItemState aState); + TimeMilli GetLeaseRenewTime(void) const { return TimeMilli(mData); } + void SetLeaseRenewTime(TimeMilli aTime) { mData = aTime.GetValue(); } + }; + + /** + * This constructor initializes the SRP `Client` object. + * + * @param[in] aInstance A reference to the OpenThread instance. + * + */ + explicit Client(Instance &aInstance); + + /** + * This method starts the SRP client operation. + * + * SRP client will prepare and send "SRP Update" message to the SRP server once all the following conditions are + * met: + * + * - The SRP client is started - `Start()` is called + * - Host name is set - `SetHostName()` is called. + * - At least one host IPv6 address is set - `SetHostAddresses()` is called. + * - At least one service is added - `AddService()` is called. + * + * It does not matter in which order these methods are called. When all conditions are met, the SRP client will + * wait for a short delay before preparing an "SRP Update" message and sending it to server. This delay allows for + * user to add multiple services and/or IPv6 addresses before the first SRP Update message is sent (ensuring a + * single SRP Update is sent containing all the info). + * + * @param[in] aServerSockAddr The socket address (IPv6 address and port number) of the SRP server. + * @param[in] aCallback The callback to notify of events and changes. Can be nullptr if not needed. + * @param[in] aContext An arbitrary context used with @p aCallback. + * + * @retval OT_ERROR_NONE SRP client operation started successfully or it is already running with same server + * socket address and callback. + * @retval OT_ERROR_BUSY SRP client is busy running with a different socket address and/or callback. + * @retval OT_ERROR_FAILED Failed to open/connect the client's UDP socket. + * + */ + otError Start(const Ip6::SockAddr &aServerSockAddr, Callback aCallback, void *aContext); + + /** + * This method stops the SRP client operation. + * + * This method stops any further interactions with the SRP server. Note that it does not remove or clear host info + * and/or list of services. It marks all services to be added/removed again once the client is started again. + * + */ + void Stop(void); + + /** + * This method gets the lease interval used in SRP update requests. + * + * Note that this is lease duration that would be requested by the SRP client. Server may choose to accept a + * different lease interval. + * + * @returns The lease interval (in seconds). + * + */ + uint32_t GetLeaseInterval(void) const { return mLeaseInterval; } + + /** + * This method sets the lease interval used in SRP update requests. + * + * Changing the lease interval does not impact the accepted lease interval of already registered services/host-info. + * It only changes any future SRP update messages (i.e adding new services and/or refreshes of existing services). + * + * @param[in] The lease interval (in seconds). If zero, the default value `kDefaultLease` would be used. + * + */ + void SetLeaseInterval(uint32_t aInterval) { mLeaseInterval = GetBoundedLeaseInterval(aInterval, kDefaultLease); } + + /** + * This method gets the key lease interval used in SRP update requests. + * + * @returns The key lease interval (in seconds). + * + */ + uint32_t GetKeyLeaseInterval(void) const { return mKeyLeaseInterval; } + + /** + * This method sets the key lease interval used in SRP update requests. + * + * Changing the lease interval does not impact the accepted lease interval of already registered services/host-info. + * It only changes any future SRP update messages (i.e adding new services and/or refreshes of existing services). + * + * @param[in] The key lease interval (in seconds). If zero, the default value `kDefaultKeyLease` would be used. + * + */ + void SetKeyLeaseInterval(uint32_t aInterval) + { + mKeyLeaseInterval = GetBoundedLeaseInterval(aInterval, kDefaultKeyLease); + } + + /** + * This method gets the host info. + * + * @returns A reference to host info structure. + * + */ + const HostInfo &GetHostInfo(void) const { return mHostInfo; } + + /** + * This function sets the host name label. + * + * After a successful call to this function, `Callback` will be called to report the status of host info + * registration with SRP server. + * + * The host name can be set before client is started or after start but before host info is registered with server + * (host info should be in either `kToAdd` or `kRemoved`). + * + * @param[in] aName A pointer to host name label string (MUST NOT be NULL). Pointer the string buffer MUST + * persist and remain valid and constant after return from this function. + * + * @retval OT_ERROR_NONE The host name label was set successfully. + * @retval OT_ERROR_INVALID_ARGS The @p aName is NULL. + * @retval OT_ERROR_INVALID_STATE The host name is already set and registered with the server. + * + */ + otError SetHostName(const char *aName); + + /** + * This method sets/updates the list of host IPv6 address. + * + * Host IPv6 addresses can be set/changed before start or even during operation of SRP client (e.g. to add/remove + * or change a previously registered host address), except when the host info is being removed (client is busy + * handling a remove request from an earlier call to `RemoveHostAndServices()` and host info still being in either + * `kStateToRemove` or `kStateRemoving` states). + * + * After a successful call to this method, `Callback` will be called to report the status of the address + * registration with SRP server. + * + * @param[in] aAddresses A pointer to the an array containing the host IPv6 addresses. + * @param[in] aNumAddresses The number of addresses in the @p aAddresses array. + * + * @retval OT_ERROR_NONE The host IPv6 address list change started successfully. The `Callback` + * will be called to report the status of registering addresses with server. + * @retval OT_ERROR_INVALID_ARGS The address list is invalid (e.g., must contain at least one address). + * @retval OT_ERROR_INVALID_STATE Host is being removed and therefore cannot change host address. + * + */ + otError SetHostAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses); + + /** + * This method adds a service to be registered with server. + * + * After a successful call to this method, `Callback` will be called to report the status of the service + * addition/registration with SRP server. + * + * @param[in] aService A `Service` to add (the instance must persist and remain unchanged after + * successful return from this method). + * + * @retval OT_ERROR_NONE The addition of service started successfully. The `Callback` will be + * called to report the status. + * @retval OT_ERROR_ALREADY The same service is already in the list. + * @retval OT_ERROR_INVALID_ARGS The service structure is invalid (e.g., bad service name or `TxEntry`). + * + */ + otError AddService(Service &aService); + + /** + * This method removes a service to be unregistered with server. + * + * @param[in] aService A `Service` to remove (the instance must persist and remain unchanged after + * successful return from this method). + * + * @retval OT_ERROR_NONE The removal of service started successfully. The `Callback` will be called to + * report the status. + * @retval OT_ERROR_NOT_FOUND The service could not be found in the list. + * + */ + + otError RemoveService(Service &aService); + + /** + * This method gets the list of services being managed by client. + * + * @returns The list of services. + * + */ + const LinkedList &GetServices(void) const { return mServices; } + + /** + * This method starts the remove process of the host info and all services. + * + * After retuning from this method, `Callback` will be called to report the status of remove request with + * SRP server. + * + * If the host info is to be permanently removed from server, @p aRemoveKeyLease should be set to `true` which + * removes the key lease associated with host on server. Otherwise, the key lease record is kept as before, which + * ensures that the server holds the host name in reserve for when the client once again able to provide and + * register its service(s). + * + * @param[in] aRemoveKeyLease A boolean indicating whether or not the host key lease should also be removed. + * + * @retval OT_ERROR_NONE The removal of host and services started successfully. The `Callback` will be called + * to report the status. + * @retval OT_ERROR_ALREADY The host is already removed. + * + */ + otError RemoveHostAndServices(bool aShouldRemoveKeyLease); + + /** + * This method clears all host info and all the services. + * + * Unlike `RemoveHostAndServices()` which sends an update message to server to remove/unregister all the info, this + * method clears all the info immediately without any interaction with server. + * + */ + void ClearHostAndServices(void); + +#if OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE + /** + * This method gets the domain name being used by SRP client. + * + * If domain name is not set, "default.service.arpa" will be used. + * + * @returns The domain name string. + * + */ + const char *GetDomainName(void) const { return mDomainName; } + + /** + * This method sets the domain name to be used by SRP client. + * + * This is an optional method. If not set "default.service.arpa" will be used. + * + * The domain name can be set before client is started or after start but before host info is registered with server + * (host info should be in either `kToAdd` or `kToRemove`). + * + * @param[in] aName A pointer to the domain name string. If NULL sets it to default "default.service.arpa". + * + * @retval OT_ERROR_NONE The domain name label was set successfully. + * @retval OT_ERROR_INVALID_STATE The host info is already registered with server. + * + */ + otError SetDomainName(const char *aName); +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE + + /** + * This static method converts a `ItemState` to a string. + * + * @param[in] aState An `ItemState`. + * + * @returns A string representation of @p aState. + * + */ + static const char *ItemStateToString(ItemState aState); + +private: + enum : uint8_t + { + kFastPollsAfterUpdateTx = 11, // Number of fast data polls after SRP Update tx (11x 188ms = ~2 seconds) + }; + + enum : uint16_t + { + kUdpPayloadSize = Ip6::Ip6::kMaxDatagramLength - sizeof(Ip6::Udp::Header), // Max UDP payload size + }; + + enum : uint32_t + { + // ------------------------------- + // Lease related constants + + kDefaultLease = OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_LEASE, // in seconds. + kDefaultKeyLease = OPENTHREAD_CONFIG_SRP_CLIENT_DEFAULT_KEY_LEASE, // in seconds. + + // The guard interval determines how much earlier (relative to + // the lease expiration time) the client will send an update + // to renew the lease. + kLeaseRenewGuardInterval = OPENTHREAD_CONFIG_SRP_CLIENT_LEASE_RENEW_GUARD_INTERVAL, // in seconds. + + // Max allowed lease time to avoid timer roll-over (~24.8 days). + kMaxLease = (Timer::kMaxDelay / 1000) - 1, + + // Opportunistic early refresh: When sending an SRP update, the + // services that are not yet expired but are close, are allowed + // to refresh early and are included in the SRP update. This + // helps place more services on the same lease refresh schedule + // reducing number of messages sent to the SRP server. The + // "early lease renewal interval" is used to determine if a + // service can renew early. The interval is calculated by + // multiplying the accepted lease interval by the"early lease + // renewal factor" which is given as a fraction (numerator and + // denominator). + // + // If the factor is set to zero (numerator=0, denominator=1), + // the opportunistic early refresh behavior is disabled. If + // denominator is set to zero (the factor is set to infinity), + // then all services (including previously registered ones) + // are always included in SRP update message. + + kEarlyLeaseRenewFactorNumerator = OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_NUMERATOR, + kEarlyLeaseRenewFactorDenominator = OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR, + + // ------------------------------- + // When there is a change (e.g., a new service is added/removed) + // that requires an update, the SRP client will wait for a short + // delay as specified by `kUpdateTxDelay` before sending an SRP + // update to server. This allows the user to provide more change + // that are then all sent in same update message. + kUpdateTxDelay = OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_DELAY, // in msec. + + // ------------------------------- + // Retry related constants + // + // If the preparation or transmission of an SRP update message + // fails (e.g., no buffer to allocate the message), SRP client + // will retry after a short interval `kTxFailureRetryInterval` + // up to `kMaxTxFailureRetries` attempts. After this, the retry + // wait interval will be used (which keeps growing on each failure + // - please see bellow). + // + // If the update message is sent successfully but there is no + // response from server or if server rejects the update, the + // client will retransmit the update message after some wait + // interval. The wait interval starts from the minimum value and + // is increased by the growth factor on back-to-back failures up + // to the max value. The growth factor is given as a fraction + // (e.g., for 1.5, we can use 15 as the numerator and 10 as the + // denominator). A random jitter is added to the retry interval. + // If the current wait interval value is smaller than the jitter + // interval, then wait interval value itself is used as the + // jitter value. For example, with jitter interval of 2 seconds + // if the current retry interval is 800ms, then a random wait + // interval in [0,2*800] ms will be used. + + kTxFailureRetryInterval = 250, // in ms + kMaxTxFailureRetries = 8, // num of quick retries after tx failure + kMinRetryWaitInterval = OPENTHREAD_CONFIG_SRP_CLIENT_MIN_RETRY_WAIT_INTERVAL, // in ms + kMaxRetryWaitInterval = OPENTHREAD_CONFIG_SRP_CLIENT_MAX_RETRY_WAIT_INTERVAL, // in ms + kRetryIntervalGrowthFactorNumerator = OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_NUMERATOR, + kRetryIntervalGrowthFactorDenominator = OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_INTERVAL_GROWTH_FACTOR_DENOMINATOR, + }; + + enum : uint16_t + { + kTxFailureRetryJitter = 10, // in ms + kRetryIntervalJitter = OPENTHREAD_CONFIG_SRP_CLIENT_RETRY_WAIT_INTERVAL_JITTER, // in ms + }; + + static_assert(kDefaultLease <= static_cast(kMaxLease), "kDefaultLease is larger than max"); + static_assert(kDefaultKeyLease <= static_cast(kMaxLease), "kDefaultKeyLease is larger than max"); + + enum State : uint8_t + { + kStateStopped, // Client is stopped. + kStatePaused, // Client is paused (due to device being detached). + kStateToUpdate, // Waiting to send SRP update + kStateUpdating, // SRP update is sent, waiting for response from server. + kStateUpdated, // SRP update response received from server. + kStateToRetry, // SRP update tx failed, waiting to retry. + }; + + struct Info : public Clearable + { + enum : uint16_t + { + kUnknownOffset = 0, // Unknown offset value (used when offset is not yet set). + }; + + uint16_t mDomainNameOffset; // Offset of domain name serialization + uint16_t mHostNameOffset; // Offset of host name serialization. + uint16_t mRecordCount; // Number of resource records in Update section. + Crypto::Ecdsa::P256::KeyPair mKeyPair; // The ECDSA key pair. + }; + + void Resume(void); + void Pause(void); + void HandleNotifierEvents(Events aEvents); + void UpdateServiceStateToRemove(Service &aService); + State GetState(void) const { return mState; } + void SetState(State aState); + void ChangeHostAndServiceStates(const ItemState *aNewStates); + void InvokeCallback(otError aError) const; + void InvokeCallback(otError aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const; + void ClearHostInfoAndServices(void); + void HandleHostInfoOrServiceChange(void); + void SendUpdate(void); + otError PrepareUpdateMessage(Message &aMessage); + otError ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair); + otError AppendServiceInstructions(Service &aService, Message &aMessage, Info &aInfo); + otError AppendHostDescriptionInstruction(Message &aMessage, Info &aInfo) const; + otError AppendDeleteAllRrsets(Message &aMessage) const; + otError AppendHostName(Message &aMessage, Info &aInfo, bool aDoNotCompress = false) const; + otError AppendUpdateLeaseOptRecord(Message &aMessage) const; + otError AppendSignature(Message &aMessage, Info &aInfo); + void UpdateRecordLengthInMessage(Dns::ResourceRecord &aRecord, uint16_t aOffset, Message &aMessage) const; + static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo); + void ProcessResponse(Message &aMessage); + void HandleUpdateDone(void); + void GetRemovedServices(LinkedList &aRemovedServices); + static otError ReadResourceRecord(const Message &aMessage, uint16_t &aOffset, Dns::ResourceRecord &aRecord); + otError ProcessOptRecord(const Message &aMessage, uint16_t aOffset, const Dns::OptRecord &aOptRecord); + void UpdateState(void); + uint32_t GetRetryWaitInterval(void) const { return mRetryWaitInterval; } + void ResetRetryWaitInterval(void) { mRetryWaitInterval = kMinRetryWaitInterval; } + void GrowRetryWaitInterval(void); + uint32_t GetBoundedLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const; + bool ShouldRenewEarly(const Service &aService) const; + static void HandleTimer(Timer &aTimer); + void HandleTimer(void); + +#if (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO) && (OPENTHREAD_CONFIG_LOG_SRP == 1) + static const char *StateToString(State aState); + void LogRetryWaitInterval(void) const; +#else + void LogRetryWaitInterval(void) const {} +#endif + + static const char kDefaultDomainName[]; + + static_assert(kMaxTxFailureRetries < 128, "kMaxTxFailureRetries exceed the range of mTxFailureRetryCount (7-bit)"); + + State mState; + uint8_t mTxFailureRetryCount : 7; + bool mShouldRemoveKeyLease : 1; + + uint16_t mUpdateMessageId; + uint32_t mRetryWaitInterval; + + TimeMilli mLeaseRenewTime; + uint32_t mAcceptedLeaseInterval; + uint32_t mLeaseInterval; + uint32_t mKeyLeaseInterval; + + Ip6::Udp::Socket mSocket; + + Callback mCallback; + void * mCallbackContext; + const char * mDomainName; + HostInfo mHostInfo; + LinkedList mServices; + TimerMilli mTimer; +}; + +} // namespace Srp +} // namespace ot + +#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + +#endif // SRP_CLIENT_HPP_ diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h index 18f82451e..b4b2585bb 100644 --- a/src/core/openthread-core-config.h +++ b/src/core/openthread-core-config.h @@ -76,6 +76,7 @@ #include "config/platform.h" #include "config/radio_link.h" #include "config/sntp_client.h" +#include "config/srp_client.h" #include "config/time_sync.h" #include "config/tmf.h" diff --git a/src/core/thread/thread_netif.cpp b/src/core/thread/thread_netif.cpp index faf611b9f..341a50658 100644 --- a/src/core/thread/thread_netif.cpp +++ b/src/core/thread/thread_netif.cpp @@ -62,6 +62,9 @@ ThreadNetif::ThreadNetif(Instance &aInstance) #if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE , mDnsClient(aInstance) #endif +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + , mSrpClient(aInstance) +#endif #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE , mSntpClient(aInstance) #endif diff --git a/src/core/thread/thread_netif.hpp b/src/core/thread/thread_netif.hpp index 7a6183050..71810b33f 100644 --- a/src/core/thread/thread_netif.hpp +++ b/src/core/thread/thread_netif.hpp @@ -79,6 +79,7 @@ #include "net/ip6_filter.hpp" #include "net/netif.hpp" #include "net/sntp_client.hpp" +#include "net/srp_client.hpp" #include "thread/address_resolver.hpp" #include "thread/announce_begin_server.hpp" #include "thread/discover_scanner.hpp" @@ -196,10 +197,13 @@ private: #endif #if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE Dns::Client mDnsClient; -#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE +#endif +#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + Srp::Client mSrpClient; +#endif #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE Sntp::Client mSntpClient; -#endif // OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE +#endif MeshCoP::ActiveDataset mActiveDataset; MeshCoP::PendingDataset mPendingDataset; Ip6::Filter mIp6Filter; diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index 0d0c328dd..474452879 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -470,6 +470,22 @@ */ #define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1 +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE + * + * Define to 1 to enable SRP Client support. + * + */ +#define OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE 1 + +/** + * @def OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_CHANGE_ENABLE + * + * Define to 1 for the SRP client implementation to provides APIs to allow domain name to be set/changed. + * + */ +#define OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE 1 + #if OPENTHREAD_RADIO /** * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE