From 3ffe8516f7d943f0c2e6279a00b1291c046c82b3 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 5 Apr 2023 15:49:08 -0700 Subject: [PATCH] [dns-client] add `ServiceMode` to control service resolution (#8772) This commit updates DNS client to add `otDnsServiceMode` to the `otDnsQueryConfig`. This new config property determines which records to query and allow the API user to control the behavior during service resolution: We can query for SRV record only or TXT record only, or query for both SRV and TXT records in the same message, or in parallel in different messages, or an "optimized" mode where client will first try to query for both records together in the same message but if server responds with an error, it then retries using two parallel separate queries. This gives flexibility and control to the API user. It also helps address situations where the server (DNS resolver) may not accept queries with more than one questions. To support this new feature, this commit updates and enhances the internal design of the `Dns::Client`. A new mechanism is added to allow multiple `Query` instances to be associated with each other under a main `Query` and responses for the related queries are saved until all are received and validated before finalizing the main `Query` and invoking the callback and allowing caller to retrieve the info. This commit also adds a detailed unit test `test_dns_client` which covers DNS client browse and service resolution validating behavior under all service modes. In order to test `Client` functionality, this commit also adds a `TestMode` in `Dns::ServiceDiscovery::Server` allowing us to change server behavior, e.g., reject messages with more than one question in query. --- include/openthread/dns_client.h | 31 +- include/openthread/instance.h | 2 +- src/cli/README.md | 32 +- src/cli/cli.cpp | 77 +- src/cli/cli.hpp | 2 + src/core/config/dns_client.h | 10 + src/core/net/dns_client.cpp | 816 ++++++++++++------ src/core/net/dns_client.hpp | 88 +- src/core/net/dnssd_server.cpp | 10 + src/core/net/dnssd_server.hpp | 25 +- .../expect/tun-dns-over-tcp-client.exp | 2 +- tests/unit/CMakeLists.txt | 21 + tests/unit/test_dns_client.cpp | 682 +++++++++++++++ 13 files changed, 1515 insertions(+), 283 deletions(-) create mode 100644 tests/unit/test_dns_client.cpp diff --git a/include/openthread/dns_client.h b/include/openthread/dns_client.h index c7f25b772..87ed2bac4 100644 --- a/include/openthread/dns_client.h +++ b/include/openthread/dns_client.h @@ -80,6 +80,23 @@ typedef enum OT_DNS_NAT64_DISALLOW = 2, ///< Do not allow NAT64 address translation during DNS client address resolution. } otDnsNat64Mode; +/** + * This enumeration type represents the service resolution mode in an `otDnsQueryConfig`. + * + * This is only used during DNS client service resolution `otDnsClientResolveService()`. It determines which + * record types to query. + * + */ +typedef enum +{ + OT_DNS_SERVICE_MODE_UNSPECIFIED = 0, ///< Mode is not specified. Use default service mode. + OT_DNS_SERVICE_MODE_SRV = 1, ///< Query for SRV record only. + OT_DNS_SERVICE_MODE_TXT = 2, ///< Query for TXT record only. + OT_DNS_SERVICE_MODE_SRV_TXT = 3, ///< Query for both SRV and TXT records in same message. + OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE = 4, ///< Query in parallel for SRV and TXT using separate messages. + OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE = 5, ///< Query for TXT/SRV together first, if fails then query separately. +} otDnsServiceMode; + /** * This enumeration type represents the DNS transport protocol in an `otDnsQueryConfig`. * @@ -102,11 +119,12 @@ typedef enum */ typedef struct otDnsQueryConfig { - otSockAddr mServerSockAddr; ///< Server address (IPv6 address/port). All zero or zero port for unspecified. + otSockAddr mServerSockAddr; ///< Server address (IPv6 addr/port). All zero or zero port for unspecified. uint32_t mResponseTimeout; ///< Wait time (in msec) to rx response. Zero indicates unspecified value. uint8_t mMaxTxAttempts; ///< Maximum tx attempts before reporting failure. Zero for unspecified value. otDnsRecursionFlag mRecursionFlag; ///< Indicates whether the server can resolve the query recursively or not. otDnsNat64Mode mNat64Mode; ///< Allow/Disallow NAT64 address translation during address resolution. + otDnsServiceMode mServiceMode; ///< Determines which records to query during service resolution. otDnsTransportProto mTransportProto; ///< Select default transport protocol. } otDnsQueryConfig; @@ -420,7 +438,8 @@ otError otDnsBrowseResponseGetServiceInstance(const otDnsBrowseResponse *aRespon * (note that it is a SHOULD and not a MUST requirement). This function tries to retrieve this info for a given service * instance when available. * - * - If no matching SRV record is found in @p aResponse, `OT_ERROR_NOT_FOUND` is returned. + * - If no matching SRV record is found in @p aResponse, `OT_ERROR_NOT_FOUND` is returned. In this case, no additional + * records (no TXT and/or AAAA) are read. * - If a matching SRV record is found in @p aResponse, @p aServiceInfo is updated and `OT_ERROR_NONE` is returned. * - If no matching TXT record is found in @p aResponse, `mTxtDataSize` in @p aServiceInfo is set to zero. * - If TXT data length is greater than `mTxtDataSize`, it is read partially and `mTxtDataTruncated` is set to true. @@ -550,8 +569,10 @@ otError otDnsServiceResponseGetServiceName(const otDnsServiceResponse *aResponse * * This function MUST only be used from `otDnsServiceCallback`. * - * - If no matching SRV record is found in @p aResponse, `OT_ERROR_NOT_FOUND` is returned. - * - If a matching SRV record is found in @p aResponse, @p aServiceInfo is updated and `OT_ERROR_NONE` is returned. + * - If a matching SRV record is found in @p aResponse, @p aServiceInfo is updated. + * - If no matching SRV record is found, `OT_ERROR_NOT_FOUND` is returned unless the query config for this query + * used `OT_DNS_SERVICE_MODE_TXT` for `mServiceMode` (meaning the request was only for TXT record). In this case, we + * still try to parse the SRV record from Additional Data Section of response (in case server provided the info). * - If no matching TXT record is found in @p aResponse, `mTxtDataSize` in @p aServiceInfo is set to zero. * - If TXT data length is greater than `mTxtDataSize`, it is read partially and `mTxtDataTruncated` is set to true. * - If no matching AAAA record is found in @p aResponse, `mHostAddress is set to all zero or unspecified address. @@ -562,7 +583,7 @@ otError otDnsServiceResponseGetServiceName(const otDnsServiceResponse *aResponse * @param[out] aServiceInfo A `ServiceInfo` to output the service instance information (MUST NOT be NULL). * * @retval OT_ERROR_NONE The service instance info was read. @p aServiceInfo is updated. - * @retval OT_ERROR_NOT_FOUND Could not find a matching SRV record in @p aResponse. + * @retval OT_ERROR_NOT_FOUND Could not find a required record in @p aResponse. * @retval OT_ERROR_NO_BUFS The host name and/or TXT data could not fit in the given buffers. * @retval OT_ERROR_PARSE Could not parse the records in the @p aResponse. * diff --git a/include/openthread/instance.h b/include/openthread/instance.h index f6eb33de1..367d8e975 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (306) +#define OPENTHREAD_API_VERSION (307) /** * @addtogroup api-instance diff --git a/src/cli/README.md b/src/cli/README.md index 6d8369fd2..22da85b09 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -1113,7 +1113,20 @@ Done Get the default query config used by DNS client. -The config includes the server IPv6 address and port, response timeout in msec (wait time to rx response), maximum tx attempts before reporting failure, boolean flag to indicate whether the server can resolve the query recursively or not. +The config includes + +- Server IPv6 address and port +- Response timeout in msec (wait time to rx response) +- Maximum tx attempts before reporting failure +- Boolean flag to indicate whether the server can resolve the query recursively or not. +- Service resolution mode which specifies which records to query. Possible options are: + - `srv` : Query for SRV record only. + - `txt` : Query for TXT record only. + - `srv_txt` : Query for both SRV and TXT records in the same message. + - `srv_txt_sep`: Query in parallel for SRV and TXT using separate messages. + - `srv_txt_opt`: Query for TXT/SRV together first, if it fails then query separately. +- Whether to allow/disallow NAT64 address translation during address resolution (requires `OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE`) +- Transport protocol UDP or TCP (requires `OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE`) ```bash > dns config @@ -1121,19 +1134,30 @@ Server: [fd00:0:0:0:0:0:0:1]:1234 ResponseTimeout: 5000 ms MaxTxAttempts: 2 RecursionDesired: no +ServiceMode: srv_txt_opt +Nat64Mode: allow TransportProtocol: udp Done > ``` -### dns config \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\] \[transport protocol\] +### dns config \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\] \[service mode] Set the default query config. +Service mode specifies which records to query. Possible options are: + +- `def` : Use default option. +- `srv` : Query for SRV record only. +- `txt` : Query for TXT record only. +- `srv_txt` : Query for both SRV and TXT records in the same message. +- `srv_txt_sep`: Query in parallel for SRV and TXT using separate messages. +- `srv_txt_opt`: Query for TXT/SRV together first, if it fails then query separately. + To set protocol effectively to tcp `OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE` is required. ```bash -> dns config fd00::1 1234 5000 2 0 tcp +> dns config fd00::1 1234 5000 2 0 srv_txt_sep tcp Done > dns config @@ -1141,6 +1165,8 @@ Server: [fd00:0:0:0:0:0:0:1]:1234 ResponseTimeout: 5000 ms MaxTxAttempts: 2 RecursionDesired: no +ServiceMode: srv_txt_sep +Nat64Mode: allow TransportProtocol: tcp Done ``` diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index d0b2eae66..dc248a46a 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -2990,6 +2990,8 @@ template <> otError Interpreter::Process(Arg aArgs[]) * ResponseTimeout: 5000 ms * MaxTxAttempts: 2 * RecursionDesired: no + * ServiceMode: srv + * Nat64Mode: allow * Done * @endcode * @par api_copy @@ -3011,6 +3013,10 @@ template <> otError Interpreter::Process(Arg aArgs[]) OutputLine("MaxTxAttempts: %u", defaultConfig->mMaxTxAttempts); OutputLine("RecursionDesired: %s", (defaultConfig->mRecursionFlag == OT_DNS_FLAG_RECURSION_DESIRED) ? "yes" : "no"); + OutputLine("ServiceMode: %s", DnsConfigServiceModeToString(defaultConfig->mServiceMode)); +#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE + OutputLine("Nat64Mode: %s", (defaultConfig->mNat64Mode == OT_DNS_NAT64_ALLOW) ? "allow" : "disallow"); +#endif #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE OutputLine("TransportProtocol: %s", (defaultConfig->mTransportProto == OT_DNS_TRANSPORT_UDP) ? "udp" : "tcp"); @@ -3046,7 +3052,7 @@ template <> otError Interpreter::Process(Arg aArgs[]) * #otDnsClientSetDefaultConfig * @cparam dns config [@ca{dns-server-IP}] [@ca{dns-server-port}] [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] [@ca{recursion-desired-boolean}] + * --> [@ca{recursion-desired-boolean}] [@ca{service-mode}] * @par * We can leave some of the fields as unspecified (or use value zero). The * unspecified fields are replaced by the corresponding OT config option @@ -3256,11 +3262,69 @@ exit: #if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE +const char *Interpreter::DnsConfigServiceModeToString(otDnsServiceMode aMode) const +{ + static const char *const kServiceModeStrings[] = { + "unspec", // OT_DNS_SERVICE_MODE_UNSPECIFIED (0) + "srv", // OT_DNS_SERVICE_MODE_SRV (1) + "txt", // OT_DNS_SERVICE_MODE_TXT (2) + "srv_txt", // OT_DNS_SERVICE_MODE_SRV_TXT (3) + "srv_txt_sep", // OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE (4) + "srv_txt_opt", // OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE (5) + }; + + static_assert(OT_DNS_SERVICE_MODE_UNSPECIFIED == 0, "OT_DNS_SERVICE_MODE_UNSPECIFIED value is incorrect"); + static_assert(OT_DNS_SERVICE_MODE_SRV == 1, "OT_DNS_SERVICE_MODE_SRV value is incorrect"); + static_assert(OT_DNS_SERVICE_MODE_TXT == 2, "OT_DNS_SERVICE_MODE_TXT value is incorrect"); + static_assert(OT_DNS_SERVICE_MODE_SRV_TXT == 3, "OT_DNS_SERVICE_MODE_SRV_TXT value is incorrect"); + static_assert(OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE == 4, "OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE value is incorrect"); + static_assert(OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE == 5, "OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE value is incorrect"); + + return Stringify(aMode, kServiceModeStrings); +} + +otError Interpreter::ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const +{ + otError error = OT_ERROR_NONE; + + if (aArg == "def") + { + aMode = OT_DNS_SERVICE_MODE_UNSPECIFIED; + } + else if (aArg == "srv") + { + aMode = OT_DNS_SERVICE_MODE_SRV; + } + else if (aArg == "txt") + { + aMode = OT_DNS_SERVICE_MODE_TXT; + } + else if (aArg == "srv_txt") + { + aMode = OT_DNS_SERVICE_MODE_SRV_TXT; + } + else if (aArg == "srv_txt_sep") + { + aMode = OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE; + } + else if (aArg == "srv_txt_opt") + { + aMode = OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE; + } + else + { + error = OT_ERROR_INVALID_ARGS; + } + + return error; +} + otError Interpreter::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig) { // This method gets the optional DNS config from `aArgs[]`. // The format: `[server IP address] [server port] [timeout] - // [max tx attempt] [recursion desired]`. + // [max tx attempt] [recursion desired] [service mode] + // [transport]` otError error = OT_ERROR_NONE; bool recursionDesired; @@ -3292,11 +3356,15 @@ otError Interpreter::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig) aConfig->mRecursionFlag = recursionDesired ? OT_DNS_FLAG_RECURSION_DESIRED : OT_DNS_FLAG_NO_RECURSION; VerifyOrExit(!aArgs[5].IsEmpty()); - if (aArgs[5] == "tcp") + SuccessOrExit(error = ParseDnsServiceMode(aArgs[5], aConfig->mServiceMode)); + + VerifyOrExit(!aArgs[6].IsEmpty()); + + if (aArgs[6] == "tcp") { aConfig->mTransportProto = OT_DNS_TRANSPORT_TCP; } - else if (aArgs[5] == "udp") + else if (aArgs[6] == "udp") { aConfig->mTransportProto = OT_DNS_TRANSPORT_UDP; } @@ -3304,6 +3372,7 @@ otError Interpreter::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig) { error = OT_ERROR_INVALID_ARGS; } + exit: return error; } diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index bb220a8f2..79faa376d 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -459,6 +459,8 @@ private: otError GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig); static void HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext); void HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse); + const char *DnsConfigServiceModeToString(otDnsServiceMode aMode) const; + otError ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const; #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE void OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo); static void HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext); diff --git a/src/core/config/dns_client.h b/src/core/config/dns_client.h index 99a6520cf..d727261d8 100644 --- a/src/core/config/dns_client.h +++ b/src/core/config/dns_client.h @@ -151,6 +151,16 @@ #define OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_RECURSION_DESIRED_FLAG 1 #endif +/** + * @def OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE + * + * Specifies the default `otDnsServiceMode` to use. The value MUST be from `otDnsServiceMode` enumeration. + * + */ +#ifndef OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE +#define OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE +#endif + /** * @def OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE * diff --git a/src/core/net/dns_client.cpp b/src/core/net/dns_client.cpp index e25732ed5..531a6b208 100644 --- a/src/core/net/dns_client.cpp +++ b/src/core/net/dns_client.cpp @@ -70,19 +70,27 @@ Client::QueryConfig::QueryConfig(InitMode aMode) SetResponseTimeout(kDefaultResponseTimeout); SetMaxTxAttempts(kDefaultMaxTxAttempts); SetRecursionFlag(kDefaultRecursionDesired ? kFlagRecursionDesired : kFlagNoRecursion); + SetServiceMode(kDefaultServiceMode); #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE SetNat64Mode(kDefaultNat64Allowed ? kNat64Allow : kNat64Disallow); #endif SetTransportProto(kDnsTransportUdp); } -void Client::QueryConfig::SetFrom(const QueryConfig &aConfig, const QueryConfig &aDefaultConfig) +void Client::QueryConfig::SetFrom(const QueryConfig *aConfig, const QueryConfig &aDefaultConfig) { // This method sets the config from `aConfig` replacing any // unspecified fields (value zero) with the fields from - // `aDefaultConfig`. + // `aDefaultConfig`. If `aConfig` is `nullptr` then + // `aDefaultConfig` is used. - *this = aConfig; + if (aConfig == nullptr) + { + *this = aDefaultConfig; + ExitNow(); + } + + *this = *aConfig; if (GetServerSockAddr().GetAddress().IsUnspecified()) { @@ -115,10 +123,19 @@ void Client::QueryConfig::SetFrom(const QueryConfig &aConfig, const QueryConfig SetNat64Mode(aDefaultConfig.GetNat64Mode()); } #endif + + if (GetServiceMode() == kServiceModeUnspecified) + { + SetServiceMode(aDefaultConfig.GetServiceMode()); + } + if (GetTransportProto() == kDnsTransportUnspecified) { SetTransportProto(aDefaultConfig.GetTransportProto()); } + +exit: + return; } //--------------------------------------------------------------------------------------------------------------------- @@ -230,22 +247,42 @@ exit: #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE -Error Client::Response::FindServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const +void Client::Response::InitServiceInfo(ServiceInfo &aServiceInfo) const { - // This method searches for SRV and TXT records in the given - // section matching the record name against `aName`, and updates - // the `aServiceInfo` accordingly. It also searches for AAAA - // record for host name associated with the service (from SRV - // record). The search for AAAA record is always performed in - // Additional Data section (independent of the value given in - // `aSection`). + // This method initializes `aServiceInfo` setting all + // TTLs to zero and host name to empty string. - Error error; + aServiceInfo.mTtl = 0; + aServiceInfo.mHostAddressTtl = 0; + aServiceInfo.mTxtDataTtl = 0; + aServiceInfo.mTxtDataTruncated = false; + + AsCoreType(&aServiceInfo.mHostAddress).Clear(); + + if ((aServiceInfo.mHostNameBuffer != nullptr) && (aServiceInfo.mHostNameBufferSize > 0)) + { + aServiceInfo.mHostNameBuffer[0] = '\0'; + } +} + +Error Client::Response::ReadServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const +{ + // This method searches for SRV record in the given `aSection` + // matching the record name against `aName`, and updates the + // `aServiceInfo` accordingly. It also searches for AAAA record + // for host name associated with the service (from SRV record). + // The search for AAAA record is always performed in Additional + // Data section (independent of the value given in `aSection`). + + Error error = kErrorNone; uint16_t offset; uint16_t numRecords; Name hostName; SrvRecord srvRecord; - TxtRecord txtRecord; + + // A non-zero `mTtl` indicates that SRV record is already found + // and parsed from a previous response. + VerifyOrExit(aServiceInfo.mTtl == 0); VerifyOrExit(mMessage != nullptr, error = kErrorNotFound); @@ -277,56 +314,97 @@ Error Client::Response::FindServiceInfo(Section aSection, const Name &aName, Ser if (error == kErrorNotFound) { - AsCoreType(&aServiceInfo.mHostAddress).Clear(); - aServiceInfo.mHostAddressTtl = 0; - error = kErrorNone; - } - - SuccessOrExit(error); - - // A null `mTxtData` indicates that caller does not want to retrieve TXT data. - VerifyOrExit(aServiceInfo.mTxtData != nullptr); - - // Search for a matching TXT record. If not found, indicate this by - // setting `aServiceInfo.mTxtDataSize` to zero. - - SelectSection(aSection, offset, numRecords); - - aServiceInfo.mTxtDataTruncated = false; - - error = ResourceRecord::FindRecord(*mMessage, offset, numRecords, /* aIndex */ 0, aName, txtRecord); - - switch (error) - { - case kErrorNone: - error = txtRecord.ReadTxtData(*mMessage, offset, aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize); - - if (error == kErrorNoBufs) - { - error = kErrorNone; - aServiceInfo.mTxtDataTruncated = true; - } - - SuccessOrExit(error); - aServiceInfo.mTxtDataTtl = txtRecord.GetTtl(); - break; - - case kErrorNotFound: - aServiceInfo.mTxtDataSize = 0; - aServiceInfo.mTxtDataTtl = 0; - error = kErrorNone; - break; - - default: - ExitNow(); + error = kErrorNone; } exit: return error; } +Error Client::Response::ReadTxtRecord(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const +{ + // This method searches a TXT record in the given `aSection` + // matching the record name against `aName` and updates the TXT + // related properties in `aServicesInfo`. + // + // If no match is found `mTxtDataTtl` (which is initialized to zero) + // remains unchanged to indicate this. In this case this method still + // returns `kErrorNone`. + + Error error = kErrorNone; + uint16_t offset; + uint16_t numRecords; + TxtRecord txtRecord; + + // A non-zero `mTxtDataTtl` indicates that TXT record is already + // found and parsed from a previous response. + VerifyOrExit(aServiceInfo.mTxtDataTtl == 0); + + // A null `mTxtData` indicates that caller does not want to retrieve + // TXT data. + VerifyOrExit(aServiceInfo.mTxtData != nullptr); + + VerifyOrExit(mMessage != nullptr, error = kErrorNotFound); + + SelectSection(aSection, offset, numRecords); + + aServiceInfo.mTxtDataTruncated = false; + + SuccessOrExit(error = ResourceRecord::FindRecord(*mMessage, offset, numRecords, /* aIndex */ 0, aName, txtRecord)); + + error = txtRecord.ReadTxtData(*mMessage, offset, aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize); + + if (error == kErrorNoBufs) + { + error = kErrorNone; + + // Mark `mTxtDataTruncated` to indicate that we could not read + // the full TXT record into the given `mTxtData` buffer. + aServiceInfo.mTxtDataTruncated = true; + } + + SuccessOrExit(error); + aServiceInfo.mTxtDataTtl = txtRecord.GetTtl(); + +exit: + if (error == kErrorNotFound) + { + error = kErrorNone; + } + + return error; +} + #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE +void Client::Response::PopulateFrom(const Message &aMessage) +{ + // Populate `Response` with info from `aMessage`. + + uint16_t offset = aMessage.GetOffset(); + Header header; + + mMessage = &aMessage; + + IgnoreError(aMessage.Read(offset, header)); + offset += sizeof(Header); + + for (uint16_t num = 0; num < header.GetQuestionCount(); num++) + { + IgnoreError(Name::ParseName(aMessage, offset)); + offset += sizeof(Question); + } + + mAnswerOffset = offset; + IgnoreError(ResourceRecord::ParseRecords(aMessage, offset, header.GetAnswerCount())); + IgnoreError(ResourceRecord::ParseRecords(aMessage, offset, header.GetAuthorityRecordCount())); + mAdditionalOffset = offset; + IgnoreError(ResourceRecord::ParseRecords(aMessage, offset, header.GetAdditionalRecordCount())); + + mAnswerRecordCount = header.GetAnswerCount(); + mAdditionalRecordCount = header.GetAdditionalRecordCount(); +} + //--------------------------------------------------------------------------------------------------------------------- // Client::AddressResponse @@ -400,12 +478,20 @@ Error Client::BrowseResponse::GetServiceInfo(const char *aInstanceLabel, Service Error error; Name instanceName; - // Find a matching PTR record for the service instance label. - // Then search and read SRV, TXT and AAAA records in Additional Data section - // matching the same name to populate `aServiceInfo`. + // Find a matching PTR record for the service instance label. Then + // search and read SRV, TXT and AAAA records in Additional Data + // section matching the same name to populate `aServiceInfo`. SuccessOrExit(error = FindPtrRecord(aInstanceLabel, instanceName)); - error = FindServiceInfo(kAdditionalDataSection, instanceName, aServiceInfo); + + InitServiceInfo(aServiceInfo); + SuccessOrExit(error = ReadServiceInfo(kAdditionalDataSection, instanceName, aServiceInfo)); + SuccessOrExit(error = ReadTxtRecord(kAdditionalDataSection, instanceName, aServiceInfo)); + + if (aServiceInfo.mTxtDataTtl == 0) + { + aServiceInfo.mTxtDataSize = 0; + } exit: return error; @@ -497,10 +583,51 @@ exit: Error Client::ServiceResponse::GetServiceInfo(ServiceInfo &aServiceInfo) const { - // Search and read SRV, TXT records in Answer Section - // matching name from query. + // Search and read SRV, TXT records matching name from query. - return FindServiceInfo(kAnswerSection, Name(*mQuery, kNameOffsetInQuery), aServiceInfo); + Error error = kErrorNotFound; + + InitServiceInfo(aServiceInfo); + + for (const Response *response = this; response != nullptr; response = response->mNext) + { + Name name(*response->mQuery, kNameOffsetInQuery); + QueryInfo info; + Section srvSection; + Section txtSection; + + info.ReadFrom(*response->mQuery); + + // Determine from which section we should try to read the SRV and + // TXT records based on the query type. + // + // In `kServiceQuerySrv` or `kServiceQueryTxt` we expect to see + // only one record (SRV or TXT) in the answer section, but we + // still try to read the other records from additional data + // section in case server provided them. + + srvSection = (info.mQueryType != kServiceQueryTxt) ? kAnswerSection : kAdditionalDataSection; + txtSection = (info.mQueryType != kServiceQuerySrv) ? kAnswerSection : kAdditionalDataSection; + + error = response->ReadServiceInfo(srvSection, name, aServiceInfo); + + if ((srvSection == kAdditionalDataSection) && (error == kErrorNotFound)) + { + error = kErrorNone; + } + + SuccessOrExit(error); + + SuccessOrExit(error = response->ReadTxtRecord(txtSection, name, aServiceInfo)); + } + + if (aServiceInfo.mTxtDataTtl == 0) + { + aServiceInfo.mTxtDataSize = 0; + } + +exit: + return error; } Error Client::ServiceResponse::GetHostAddress(const char *aHostName, @@ -508,7 +635,19 @@ Error Client::ServiceResponse::GetHostAddress(const char *aHostName, Ip6::Address &aAddress, uint32_t &aTtl) const { - return FindHostAddress(kAdditionalDataSection, Name(aHostName), aIndex, aAddress, aTtl); + Error error = kErrorNotFound; + + for (const Response *response = this; response != nullptr; response = response->mNext) + { + error = FindHostAddress(kAdditionalDataSection, Name(aHostName), aIndex, aAddress, aTtl); + + if (error == kErrorNone) + { + break; + } + } + + return error; } #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE @@ -526,24 +665,29 @@ const uint16_t Client::kServiceQueryRecordTypes[] = {ResourceRecord::kTypeSrv, R #endif const uint8_t Client::kQuestionCount[] = { - /* kIp6AddressQuery -> */ GetArrayLength(kIp6AddressQueryRecordTypes), // AAAA records + /* kIp6AddressQuery -> */ GetArrayLength(kIp6AddressQueryRecordTypes), // AAAA record #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE - /* kIp4AddressQuery -> */ GetArrayLength(kIp4AddressQueryRecordTypes), // A records + /* kIp4AddressQuery -> */ GetArrayLength(kIp4AddressQueryRecordTypes), // A record #endif #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE - /* kBrowseQuery -> */ GetArrayLength(kBrowseQueryRecordTypes), // PTR records - /* kServiceQuery -> */ GetArrayLength(kServiceQueryRecordTypes), // SRV and TXT records + /* kBrowseQuery -> */ GetArrayLength(kBrowseQueryRecordTypes), // PTR record + /* kServiceQuerySrvTxt -> */ GetArrayLength(kServiceQueryRecordTypes), // SRV and TXT records + /* kServiceQuerySrv -> */ 1, // SRV record only + /* kServiceQueryTxt -> */ 1, // TXT record only #endif }; -const uint16_t *Client::kQuestionRecordTypes[] = { +const uint16_t *const Client::kQuestionRecordTypes[] = { /* kIp6AddressQuery -> */ kIp6AddressQueryRecordTypes, #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE /* kIp4AddressQuery -> */ kIp4AddressQueryRecordTypes, #endif #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE /* kBrowseQuery -> */ kBrowseQueryRecordTypes, - /* kServiceQuery -> */ kServiceQueryRecordTypes, + /* kServiceQuerySrvTxt -> */ kServiceQueryRecordTypes, + /* kServiceQuerySrv -> */ &kServiceQueryRecordTypes[0], + /* kServiceQueryTxt -> */ &kServiceQueryRecordTypes[1], + #endif }; @@ -564,11 +708,15 @@ Client::Client(Instance &aInstance) static_assert(kIp4AddressQuery == 1, "kIp4AddressQuery value is not correct"); #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE static_assert(kBrowseQuery == 2, "kBrowseQuery value is not correct"); - static_assert(kServiceQuery == 3, "kServiceQuery value is not correct"); + static_assert(kServiceQuerySrvTxt == 3, "kServiceQuerySrvTxt value is not correct"); + static_assert(kServiceQuerySrv == 4, "kServiceQuerySrv value is not correct"); + static_assert(kServiceQueryTxt == 5, "kServiceQueryTxt value is not correct"); #endif #elif OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE static_assert(kBrowseQuery == 1, "kBrowseQuery value is not correct"); - static_assert(kServiceQuery == 2, "kServiceQuery value is not correct"); + static_assert(kServiceQuerySrvTxt == 2, "kServiceQuerySrvTxt value is not correct"); + static_assert(kServiceQuerySrv == 3, "kServiceQuerySrv value is not correct"); + static_assert(kServiceQueryTxt == 4, "kServiceQuerySrv value is not correct"); #endif } @@ -587,7 +735,7 @@ void Client::Stop(void) { Query *query; - while ((query = mQueries.GetHead()) != nullptr) + while ((query = mMainQueries.GetHead()) != nullptr) { FinalizeQuery(*query, kErrorAbort); } @@ -630,7 +778,7 @@ void Client::SetDefaultConfig(const QueryConfig &aQueryConfig) { QueryConfig startingDefault(QueryConfig::kInitFromDefaults); - mDefaultConfig.SetFrom(aQueryConfig, startingDefault); + mDefaultConfig.SetFrom(&aQueryConfig, startingDefault); #if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE mUserDidSetDefaultAddress = !aQueryConfig.GetServerSockAddr().GetAddress().IsUnspecified(); @@ -669,10 +817,12 @@ Error Client::ResolveAddress(const char *aHostName, QueryInfo info; info.Clear(); - info.mQueryType = kIp6AddressQuery; + info.mQueryType = kIp6AddressQuery; + info.mConfig.SetFrom(aConfig, mDefaultConfig); info.mCallback.mAddressCallback = aCallback; + info.mCallbackContext = aContext; - return StartQuery(info, aConfig, nullptr, aHostName, aContext); + return StartQuery(info, nullptr, aHostName); } #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE @@ -684,10 +834,12 @@ Error Client::ResolveIp4Address(const char *aHostName, QueryInfo info; info.Clear(); - info.mQueryType = kIp4AddressQuery; + info.mQueryType = kIp4AddressQuery; + info.mConfig.SetFrom(aConfig, mDefaultConfig); info.mCallback.mAddressCallback = aCallback; + info.mCallbackContext = aContext; - return StartQuery(info, aConfig, nullptr, aHostName, aContext); + return StartQuery(info, nullptr, aHostName); } #endif @@ -698,10 +850,12 @@ Error Client::Browse(const char *aServiceName, BrowseCallback aCallback, void *a QueryInfo info; info.Clear(); - info.mQueryType = kBrowseQuery; + info.mQueryType = kBrowseQuery; + info.mConfig.SetFrom(aConfig, mDefaultConfig); info.mCallback.mBrowseCallback = aCallback; + info.mCallbackContext = aContext; - return StartQuery(info, aConfig, nullptr, aServiceName, aContext); + return StartQuery(info, nullptr, aServiceName); } Error Client::ResolveService(const char *aInstanceLabel, @@ -712,14 +866,40 @@ Error Client::ResolveService(const char *aInstanceLabel, { QueryInfo info; Error error; + QueryType secondQueryType = kNoQuery; VerifyOrExit(aInstanceLabel != nullptr, error = kErrorInvalidArgs); info.Clear(); - info.mQueryType = kServiceQuery; - info.mCallback.mServiceCallback = aCallback; - error = StartQuery(info, aConfig, aInstanceLabel, aServiceName, aContext); + info.mConfig.SetFrom(aConfig, mDefaultConfig); + + switch (info.mConfig.GetServiceMode()) + { + case QueryConfig::kServiceModeSrvTxtSeparate: + secondQueryType = kServiceQueryTxt; + + OT_FALL_THROUGH; + + case QueryConfig::kServiceModeSrv: + info.mQueryType = kServiceQuerySrv; + break; + + case QueryConfig::kServiceModeTxt: + info.mQueryType = kServiceQueryTxt; + break; + + case QueryConfig::kServiceModeSrvTxt: + case QueryConfig::kServiceModeSrvTxtOptimize: + default: + info.mQueryType = kServiceQuerySrvTxt; + break; + } + + info.mCallback.mServiceCallback = aCallback; + info.mCallbackContext = aContext; + + error = StartQuery(info, aInstanceLabel, aServiceName, secondQueryType); exit: return error; @@ -727,37 +907,17 @@ exit: #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE -Error Client::StartQuery(QueryInfo &aInfo, - const QueryConfig *aConfig, - const char *aLabel, - const char *aName, - void *aContext) +Error Client::StartQuery(QueryInfo &aInfo, const char *aLabel, const char *aName, QueryType aSecondType) { - // This method assumes that `mQueryType` and `mCallback` to be - // already set by caller on `aInfo`. The `aLabel` can be `nullptr` - // and then `aName` provides the full name, otherwise the name is - // appended as `{aLabel}.{aName}`. + // The `aLabel` can be `nullptr` and then `aName` provides the + // full name, otherwise the name is appended as `{aLabel}. + // {aName}`. Error error; Query *query; VerifyOrExit(mSocket.IsBound(), error = kErrorInvalidState); - if (aConfig == nullptr) - { - aInfo.mConfig = mDefaultConfig; - } - else - { - // To form the config for this query, replace any unspecified - // fields (zero value) in the given `aConfig` with the fields - // from `mDefaultConfig`. - - aInfo.mConfig.SetFrom(*aConfig, mDefaultConfig); - } - - aInfo.mCallbackContext = aContext; - #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE if (aInfo.mQueryType == kIp4AddressQuery) { @@ -770,10 +930,33 @@ Error Client::StartQuery(QueryInfo &aInfo, #endif SuccessOrExit(error = AllocateQuery(aInfo, aLabel, aName, query)); - mQueries.Enqueue(*query); - if ((error = SendQuery(*query, aInfo, /* aUpdateTimer */ true)) != kErrorNone) + + mMainQueries.Enqueue(*query); + + error = SendQuery(*query, aInfo, /* aUpdateTimer */ true); + VerifyOrExit(error == kErrorNone, FreeQuery(*query)); + + if (aSecondType != kNoQuery) { - FreeQuery(*query); + Query *secondQuery; + + aInfo.mQueryType = aSecondType; + aInfo.mMessageId = 0; + aInfo.mTransmissionCount = 0; + aInfo.mMainQuery = query; + + // We intentionally do not use `error` here so in the unlikely + // case where we cannot allocate the second query we can proceed + // with the first one. + SuccessOrExit(AllocateQuery(aInfo, aLabel, aName, secondQuery)); + + IgnoreError(SendQuery(*secondQuery, aInfo, /* aUpdateTiemr */ true)); + + // Update first query to link to second one by updating + // its `mNextQuery`. + aInfo.ReadFrom(*query); + aInfo.mNextQuery = secondQuery; + UpdateQuery(*query, aInfo); } exit: @@ -805,7 +988,29 @@ exit: return error; } -void Client::FreeQuery(Query &aQuery) { mQueries.DequeueAndFree(aQuery); } +Client::Query &Client::FindMainQuery(Query &aQuery) +{ + QueryInfo info; + + info.ReadFrom(aQuery); + + return (info.mMainQuery == nullptr) ? aQuery : *info.mMainQuery; +} + +void Client::FreeQuery(Query &aQuery) +{ + Query &mainQuery = FindMainQuery(aQuery); + QueryInfo info; + + mMainQueries.Dequeue(mainQuery); + + for (Query *query = &mainQuery; query != nullptr; query = info.mNextQuery) + { + info.ReadFrom(*query); + FreeMessage(info.mSavedResponse); + query->Free(); + } +} Error Client::SendQuery(Query &aQuery, QueryInfo &aInfo, bool aUpdateTimer) { @@ -944,24 +1149,24 @@ Error Client::AppendNameFromQuery(const Query &aQuery, Message &aMessage) void Client::FinalizeQuery(Query &aQuery, Error aError) { - Response response; - QueryInfo info; + Response response; + Query &mainQuery = FindMainQuery(aQuery); response.mInstance = &Get(); - response.mQuery = &aQuery; - info.ReadFrom(aQuery); + response.mQuery = &mainQuery; - FinalizeQuery(response, info.mQueryType, aError); + FinalizeQuery(response, aError); } -void Client::FinalizeQuery(Response &aResponse, QueryType aType, Error aError) +void Client::FinalizeQuery(Response &aResponse, Error aError) { - Callback callback; - void *context; + QueryType type; + Callback callback; + void *context; - GetCallback(*aResponse.mQuery, callback, context); + GetQueryTypeAndCallback(*aResponse.mQuery, type, callback, context); - switch (aType) + switch (type) { case kIp6AddressQuery: #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE @@ -981,24 +1186,29 @@ void Client::FinalizeQuery(Response &aResponse, QueryType aType, Error aError) } break; - case kServiceQuery: + case kServiceQuerySrvTxt: + case kServiceQuerySrv: + case kServiceQueryTxt: if (callback.mServiceCallback != nullptr) { callback.mServiceCallback(aError, &aResponse, context); } break; #endif + case kNoQuery: + break; } FreeQuery(*aResponse.mQuery); } -void Client::GetCallback(const Query &aQuery, Callback &aCallback, void *&aContext) +void Client::GetQueryTypeAndCallback(const Query &aQuery, QueryType &aType, Callback &aCallback, void *&aContext) { QueryInfo info; info.ReadFrom(aQuery); + aType = info.mQueryType; aCallback = info.mCallback; aContext = info.mCallbackContext; } @@ -1008,17 +1218,21 @@ Client::Query *Client::FindQueryById(uint16_t aMessageId) Query *matchedQuery = nullptr; QueryInfo info; - for (Query &query : mQueries) + for (Query &mainQuery : mMainQueries) { - info.ReadFrom(query); - - if (info.mMessageId == aMessageId) + for (Query *query = &mainQuery; query != nullptr; query = info.mNextQuery) { - matchedQuery = &query; - break; + info.ReadFrom(*query); + + if (info.mMessageId == aMessageId) + { + matchedQuery = query; + ExitNow(); + } } } +exit: return matchedQuery; } @@ -1029,58 +1243,78 @@ void Client::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessa static_cast(aContext)->ProcessResponse(AsCoreType(aMessage)); } -void Client::ProcessResponse(const Message &aMessage) +void Client::ProcessResponse(const Message &aResponseMessage) { - Response response; - QueryType type; - Error responseError; + Error responseError; + Query *query; - response.mInstance = &Get(); - response.mMessage = &aMessage; + SuccessOrExit(ParseResponse(aResponseMessage, query, responseError)); - // We intentionally parse the response in a separate method - // `ParseResponse()` to free all the stack allocated variables - // (e.g., `QueryInfo`) used during parsing of the message before - // finalizing the query and invoking the user's callback. + if (responseError != kErrorNone) + { + // Received an error from server, check if we can replace + // the query. - SuccessOrExit(ParseResponse(response, type, responseError)); - FinalizeQuery(response, type, responseError); +#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE + if (ReplaceWithIp4Query(*query) == kErrorNone) + { + ExitNow(); + } +#endif +#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE + if (ReplaceWithSeparateSrvTxtQueries(*query) == kErrorNone) + { + ExitNow(); + } +#endif + + FinalizeQuery(*query, responseError); + ExitNow(); + } + + // Received successful response from server. + + if (!CanFinalizeQuery(*query)) + { + SaveQueryResponse(*query, aResponseMessage); + ExitNow(); + } + + PrepareResponseAndFinalize(FindMainQuery(*query), aResponseMessage, nullptr); exit: return; } -Error Client::ParseResponse(Response &aResponse, QueryType &aType, Error &aResponseError) +Error Client::ParseResponse(const Message &aResponseMessage, Query *&aQuery, Error &aResponseError) { - Error error = kErrorNone; - const Message &message = *aResponse.mMessage; - uint16_t offset = message.GetOffset(); - Header header; - QueryInfo info; - Name queryName; + Error error = kErrorNone; + uint16_t offset = aResponseMessage.GetOffset(); + Header header; + QueryInfo info; + Name queryName; - SuccessOrExit(error = message.Read(offset, header)); + SuccessOrExit(error = aResponseMessage.Read(offset, header)); offset += sizeof(Header); VerifyOrExit((header.GetType() == Header::kTypeResponse) && (header.GetQueryType() == Header::kQueryTypeStandard) && !header.IsTruncationFlagSet(), error = kErrorDrop); - aResponse.mQuery = FindQueryById(header.GetMessageId()); - VerifyOrExit(aResponse.mQuery != nullptr, error = kErrorNotFound); + aQuery = FindQueryById(header.GetMessageId()); + VerifyOrExit(aQuery != nullptr, error = kErrorNotFound); - info.ReadFrom(*aResponse.mQuery); - aType = info.mQueryType; + info.ReadFrom(*aQuery); - queryName.SetFromMessage(*aResponse.mQuery, kNameOffsetInQuery); + queryName.SetFromMessage(*aQuery, kNameOffsetInQuery); // Check the Question Section - if (header.GetQuestionCount() == kQuestionCount[aType]) + if (header.GetQuestionCount() == kQuestionCount[info.mQueryType]) { - for (uint8_t num = 0; num < kQuestionCount[aType]; num++) + for (uint8_t num = 0; num < kQuestionCount[info.mQueryType]; num++) { - SuccessOrExit(error = Name::CompareName(message, offset, queryName)); + SuccessOrExit(error = Name::CompareName(aResponseMessage, offset, queryName)); offset += sizeof(Question); } } @@ -1092,74 +1326,103 @@ Error Client::ParseResponse(Response &aResponse, QueryType &aType, Error &aRespo // Check the answer, authority and additional record sections - aResponse.mAnswerOffset = offset; - SuccessOrExit(error = ResourceRecord::ParseRecords(message, offset, header.GetAnswerCount())); - SuccessOrExit(error = ResourceRecord::ParseRecords(message, offset, header.GetAuthorityRecordCount())); - aResponse.mAdditionalOffset = offset; - SuccessOrExit(error = ResourceRecord::ParseRecords(message, offset, header.GetAdditionalRecordCount())); + SuccessOrExit(error = ResourceRecord::ParseRecords(aResponseMessage, offset, header.GetAnswerCount())); + SuccessOrExit(error = ResourceRecord::ParseRecords(aResponseMessage, offset, header.GetAuthorityRecordCount())); + SuccessOrExit(error = ResourceRecord::ParseRecords(aResponseMessage, offset, header.GetAdditionalRecordCount())); - aResponse.mAnswerRecordCount = header.GetAnswerCount(); - aResponse.mAdditionalRecordCount = header.GetAdditionalRecordCount(); - - // Check the response code from server + // Read the response code aResponseError = Header::ResponseCodeToError(header.GetResponseCode()); -#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE +exit: + return error; +} - if (aType == kIp6AddressQuery) +bool Client::CanFinalizeQuery(Query &aQuery) +{ + // Determines whether we can finalize a main query by checking if + // we have received and saved responses for all other related + // queries associated with `aQuery`. Note that this method is + // called when we receive a response for `aQeury`, so no need to + // check for a saved response for `aQuery` itself. + + bool canFinalize = true; + QueryInfo info; + + for (Query *query = &FindMainQuery(aQuery); query != nullptr; query = info.mNextQuery) { - Ip6::Address ip6ddress; - uint32_t ttl; - ARecord aRecord; + info.ReadFrom(*query); - // If the response does not contain an answer for the IPv6 address - // resolution query and if NAT64 is allowed for this query, we can - // perform IPv4 to IPv6 address translation. - - VerifyOrExit(aResponse.FindHostAddress(Response::kAnswerSection, queryName, /* aIndex */ 0, ip6ddress, ttl) != - kErrorNone); - VerifyOrExit(info.mConfig.GetNat64Mode() == QueryConfig::kNat64Allow); - - // First, we check if the response already contains an A record - // (IPv4 address) for the query name. - - if (aResponse.FindARecord(Response::kAdditionalDataSection, queryName, /* aIndex */ 0, aRecord) == kErrorNone) + if (query == &aQuery) { - aResponse.mIp6QueryResponseRequiresNat64 = true; - aResponseError = kErrorNone; - ExitNow(); + continue; } - // Otherwise, we send a new query for IPv4 address resolution - // for the same host name. We reuse the existing `query` - // instance and keep all the info but clear `mTransmissionCount` - // and `mMessageId` (so that a new random message ID is - // selected). The new `info` will be saved in the query in - // `SendQuery()`. Note that the current query is still in the - // `mQueries` list when `SendQuery()` selects a new random - // message ID, so the existing message ID for this query will - // not be reused. Since the query is not yet resolved, we - // return `kErrorPending`. - - info.mQueryType = kIp4AddressQuery; - info.mMessageId = 0; - info.mTransmissionCount = 0; - - IgnoreReturnValue(SendQuery(*aResponse.mQuery, info, /* aUpdateTimer */ true)); - - error = kErrorPending; + if (info.mSavedResponse == nullptr) + { + canFinalize = false; + ExitNow(); + } } -#endif // OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE - exit: - if (error != kErrorNone) - { - LogInfo("Failed to parse response %s", ErrorToString(error)); - } + return canFinalize; +} - return error; +void Client::SaveQueryResponse(Query &aQuery, const Message &aResponseMessage) +{ + QueryInfo info; + + info.ReadFrom(aQuery); + VerifyOrExit(info.mSavedResponse == nullptr); + + // If `Clone()` fails we let retry or timeout handle the error. + info.mSavedResponse = aResponseMessage.Clone(); + + UpdateQuery(aQuery, info); + +exit: + return; +} + +Client::Query *Client::PopulateResponse(Response &aResponse, Query &aQuery, const Message &aResponseMessage) +{ + // Populate `aResponse` for `aQuery`. If there is a saved response + // message for `aQuery` we use it, otherwise, we use + // `aResponseMessage`. + + QueryInfo info; + + info.ReadFrom(aQuery); + + aResponse.mInstance = &Get(); + aResponse.mQuery = &aQuery; + aResponse.PopulateFrom((info.mSavedResponse == nullptr) ? aResponseMessage : *info.mSavedResponse); + + return info.mNextQuery; +} + +void Client::PrepareResponseAndFinalize(Query &aQuery, const Message &aResponseMessage, Response *aPrevResponse) +{ + // This method prepares a list of chained `Response` instances + // corresponding to all related (chained) queries. It uses + // recursion to go through the queries and construct the + // `Response` chain. + + Response response; + Query *nextQuery; + + nextQuery = PopulateResponse(response, aQuery, aResponseMessage); + response.mNext = aPrevResponse; + + if (nextQuery != nullptr) + { + PrepareResponseAndFinalize(*nextQuery, aResponseMessage, &response); + } + else + { + FinalizeQuery(response, kErrorNone); + } } void Client::HandleTimer(void) @@ -1171,29 +1434,40 @@ void Client::HandleTimer(void) bool hasTcpQuery = false; #endif - for (Query &query : mQueries) + for (Query &mainQuery : mMainQueries) { - info.ReadFrom(query); - - if (now >= info.mRetransmissionTime) + for (Query *query = &mainQuery; query != nullptr; query = info.mNextQuery) { - if (info.mTransmissionCount >= info.mConfig.GetMaxTxAttempts()) + info.ReadFrom(*query); + + if (info.mSavedResponse != nullptr) { - FinalizeQuery(query, kErrorResponseTimeout); continue; } - IgnoreReturnValue(SendQuery(query, info, /* aUpdateTimer */ false)); - } + if (now >= info.mRetransmissionTime) + { + if (info.mTransmissionCount >= info.mConfig.GetMaxTxAttempts()) + { + FinalizeQuery(*query, kErrorResponseTimeout); + continue; + } - nextTime = Min(nextTime, info.mRetransmissionTime); + IgnoreError(SendQuery(*query, info, /* aUpdateTimer */ false)); + } + + if (nextTime > info.mRetransmissionTime) + { + nextTime = info.mRetransmissionTime; + } #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE - if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp) - { - hasTcpQuery = true; - } + if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp) + { + hasTcpQuery = true; + } #endif + } } if (nextTime < now.GetDistantFuture()) @@ -1209,6 +1483,76 @@ void Client::HandleTimer(void) #endif } +#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE + +Error Client::ReplaceWithIp4Query(Query &aQuery) +{ + Error error = kErrorFailed; + QueryInfo info; + + info.ReadFrom(aQuery); + + VerifyOrExit(info.mQueryType == kIp4AddressQuery); + VerifyOrExit(info.mConfig.GetNat64Mode() == QueryConfig::kNat64Allow); + + // We send a new query for IPv4 address resolution + // for the same host name. We reuse the existing `aQuery` + // instance and keep all the info but clear `mTransmissionCount` + // and `mMessageId` (so that a new random message ID is + // selected). The new `info` will be saved in the query in + // `SendQuery()`. Note that the current query is still in the + // `mMainQueries` list when `SendQuery()` selects a new random + // message ID, so the existing message ID for this query will + // not be reused. + + info.mQueryType = kIp4AddressQuery; + info.mMessageId = 0; + info.mTransmissionCount = 0; + + IgnoreError(SendQuery(aQuery, info, /* aUpdateTimer */ true)); + error = kErrorNone; + +exit: + return error; +} + +#endif // OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE + +#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE + +Error Client::ReplaceWithSeparateSrvTxtQueries(Query &aQuery) +{ + Error error = kErrorFailed; + QueryInfo info; + Query *secondQuery; + + info.ReadFrom(aQuery); + + VerifyOrExit(info.mQueryType == kServiceQuerySrvTxt); + VerifyOrExit(info.mConfig.GetServiceMode() == QueryConfig::kServiceModeSrvTxtOptimize); + + secondQuery = aQuery.Clone(); + VerifyOrExit(secondQuery != nullptr); + + info.mQueryType = kServiceQueryTxt; + info.mMessageId = 0; + info.mTransmissionCount = 0; + info.mMainQuery = &aQuery; + IgnoreError(SendQuery(*secondQuery, info, /* aUpdateTimer */ true)); + + info.mQueryType = kServiceQuerySrv; + info.mMessageId = 0; + info.mTransmissionCount = 0; + info.mNextQuery = secondQuery; + IgnoreError(SendQuery(aQuery, info, /* aUpdateTimer */ true)); + error = kErrorNone; + +exit: + return error; +} + +#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE + #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE void Client::PrepareTcpMessage(Message &aMessage) { @@ -1334,24 +1678,7 @@ void Client::HandleTcpReceiveAvailable(otTcpEndpoint *aEndpoint, totalRead += length + sizeof(uint16_t); // Now process the read message as query response. - { - Response response; - QueryType type; - Error responseError; - - response.mInstance = &Get(); - response.mMessage = message; - - if (ParseResponse(response, type, responseError) == kErrorNone) - { - if (responseError == kErrorNone && length > OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE) - { - LogWarn("Dns query over TCP wasn't received - message is too big."); - responseError = kErrorNoBufs; - } - FinalizeQuery(response, type, responseError); - } - } + ProcessResponse(*message); IgnoreError(message->SetLength(0)); @@ -1383,12 +1710,13 @@ void Client::HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedRe mTcpState = kTcpUninitialized; // Abort queries in case of connection failures - for (Query &query : mQueries) + for (Query &mainQuery : mMainQueries) { - info.ReadFrom(query); + info.ReadFrom(mainQuery); + if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp) { - FinalizeQuery(query, kErrorAbort); + FinalizeQuery(mainQuery, kErrorAbort); } } } diff --git a/src/core/net/dns_client.hpp b/src/core/net/dns_client.hpp index e3147455b..74a74036d 100644 --- a/src/core/net/dns_client.hpp +++ b/src/core/net/dns_client.hpp @@ -145,6 +145,20 @@ public: }; #endif + /** + * This enumeration type represents the service resolution mode. + * + */ + enum ServiceMode : uint8_t + { + kServiceModeUnspecified = OT_DNS_SERVICE_MODE_UNSPECIFIED, ///< Unspecified. Use default. + kServiceModeSrv = OT_DNS_SERVICE_MODE_SRV, ///< SRV record only. + kServiceModeTxt = OT_DNS_SERVICE_MODE_TXT, ///< TXT record only. + kServiceModeSrvTxt = OT_DNS_SERVICE_MODE_SRV_TXT, ///< SRV and TXT same msg. + kServiceModeSrvTxtSeparate = OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE, ///< SRV and TXT separate msgs. + kServiceModeSrvTxtOptimize = OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE, ///< Same msg first, if fail separate. + }; + /** * This enumeration type represents the DNS transport protocol selection. * @@ -206,6 +220,13 @@ public: */ Nat64Mode GetNat64Mode(void) const { return static_cast(mNat64Mode); } #endif + /** + * This method gets the service resolution mode. + * + * @returns The service resolution mode. + * + */ + ServiceMode GetServiceMode(void) const { return static_cast(mServiceMode); } /** * This method gets the transport protocol. @@ -220,6 +241,10 @@ public: static constexpr uint16_t kDefaultServerPort = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_PORT; static constexpr uint8_t kDefaultMaxTxAttempts = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_MAX_TX_ATTEMPTS; static constexpr bool kDefaultRecursionDesired = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_RECURSION_DESIRED_FLAG; + static constexpr ServiceMode kDefaultServiceMode = + static_cast(OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE); + + static_assert(kDefaultServiceMode != kServiceModeUnspecified, "Invalid default service mode"); #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE static constexpr bool kDefaultNat64Allowed = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_NAT64_ALLOWED; @@ -239,6 +264,7 @@ public: void SetResponseTimeout(uint32_t aResponseTimeout) { mResponseTimeout = aResponseTimeout; } void SetMaxTxAttempts(uint8_t aMaxTxAttempts) { mMaxTxAttempts = aMaxTxAttempts; } void SetRecursionFlag(RecursionFlag aFlag) { mRecursionFlag = static_cast(aFlag); } + void SetServiceMode(ServiceMode aMode) { mServiceMode = static_cast(aMode); } #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE void SetNat64Mode(Nat64Mode aMode) { mNat64Mode = static_cast(aMode); } #endif @@ -246,7 +272,8 @@ public: { mTransportProto = static_cast(aTransportProto); } - void SetFrom(const QueryConfig &aConfig, const QueryConfig &aDefaultConfig); + + void SetFrom(const QueryConfig *aConfig, const QueryConfig &aDefaultConfig); }; #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE @@ -292,12 +319,16 @@ public: #endif #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE - Error FindServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const; + void InitServiceInfo(ServiceInfo &aServiceInfo) const; + Error ReadServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const; + Error ReadTxtRecord(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const; #endif + void PopulateFrom(const Message &aMessage); Instance *mInstance; // The OpenThread instance. Query *mQuery; // The associated query. const Message *mMessage; // The response message. + Response *mNext; // The next response when we have related queries. uint16_t mAnswerOffset; // Answer section offset in `mMessage`. uint16_t mAnswerRecordCount; // Number of records in answer section. uint16_t mAdditionalOffset; // Additional data section offset in `mMessage`. @@ -720,9 +751,12 @@ private: kIp4AddressQuery, // IPv4 Address resolution #endif #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE - kBrowseQuery, // Browse (service instance enumeration). - kServiceQuery, // Service instance resolution. + kBrowseQuery, // Browse (service instance enumeration). + kServiceQuerySrvTxt, // Service instance resolution both SRV and TXT records. + kServiceQuerySrv, // Service instance resolution SRV record only. + kServiceQueryTxt, // Service instance resolution TXT record only. #endif + kNoQuery, }; #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE @@ -757,30 +791,45 @@ private: TimeMilli mRetransmissionTime; QueryConfig mConfig; uint8_t mTransmissionCount; + Query *mMainQuery; + Query *mNextQuery; + Message *mSavedResponse; // Followed by the name (service, host, instance) encoded as a `Dns::Name`. }; static constexpr uint16_t kNameOffsetInQuery = sizeof(QueryInfo); - Error StartQuery(QueryInfo &aInfo, - const QueryConfig *aConfig, - const char *aLabel, - const char *aName, - void *aContext); + Error StartQuery(QueryInfo &aInfo, const char *aLabel, const char *aName, QueryType aSecondType = kNoQuery); Error AllocateQuery(const QueryInfo &aInfo, const char *aLabel, const char *aName, Query *&aQuery); void FreeQuery(Query &aQuery); void UpdateQuery(Query &aQuery, const QueryInfo &aInfo) { aQuery.Write(0, aInfo); } + Query &FindMainQuery(Query &aQuery); Error SendQuery(Query &aQuery, QueryInfo &aInfo, bool aUpdateTimer); void FinalizeQuery(Query &aQuery, Error aError); - void FinalizeQuery(Response &Response, QueryType aType, Error aError); - static void GetCallback(const Query &aQuery, Callback &aCallback, void *&aContext); + void FinalizeQuery(Response &Response, Error aError); + static void GetQueryTypeAndCallback(const Query &aQuery, QueryType &aType, Callback &aCallback, void *&aContext); Error AppendNameFromQuery(const Query &aQuery, Message &aMessage); Query *FindQueryById(uint16_t aMessageId); static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMsgInfo); - void ProcessResponse(const Message &aMessage); - Error ParseResponse(Response &aResponse, QueryType &aType, Error &aResponseError); + void ProcessResponse(const Message &aResponseMessage); + Error ParseResponse(const Message &aResponseMessage, Query *&aQuery, Error &aResponseError); + bool CanFinalizeQuery(Query &aQuery); + void SaveQueryResponse(Query &aQuery, const Message &aResponseMessage); + Query *PopulateResponse(Response &aResponse, Query &aQuery, const Message &aResponseMessage); + void PrepareResponseAndFinalize(Query &aQuery, const Message &aResponseMessage, Response *aPrevResponse); void HandleTimer(void); +#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE + Error ReplaceWithIp4Query(Query &aQuery); +#endif +#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE + Error ReplaceWithSeparateSrvTxtQueries(Query &aQuery); +#endif + +#if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE + void UpdateDefaultConfigAddress(void); +#endif + #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE static void HandleTcpEstablishedCallback(otTcpEndpoint *aEndpoint); static void HandleTcpSendDoneCallback(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData); @@ -805,15 +854,8 @@ private: void PrepareTcpMessage(Message &aMessage); #endif // OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE -#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE - Error CheckAddressResponse(Response &aResponse, Error aResponseError) const; -#endif -#if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE - void UpdateDefaultConfigAddress(void); -#endif - - static const uint8_t kQuestionCount[]; - static const uint16_t *kQuestionRecordTypes[]; + static const uint8_t kQuestionCount[]; + static const uint16_t *const kQuestionRecordTypes[]; static const uint16_t kIp6AddressQueryRecordTypes[]; #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE @@ -840,7 +882,7 @@ private: TcpState mTcpState; #endif - QueryList mQueries; + QueryList mMainQueries; RetryTimer mTimer; QueryConfig mDefaultConfig; #if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE diff --git a/src/core/net/dnssd_server.cpp b/src/core/net/dnssd_server.cpp index f2c415259..31efd3159 100644 --- a/src/core/net/dnssd_server.cpp +++ b/src/core/net/dnssd_server.cpp @@ -70,6 +70,7 @@ Server::Server(Instance &aInstance) , mEnableUpstreamQuery(false) #endif , mTimer(aInstance) + , mTestMode(kTestModeDisabled) { mCounters.Clear(); } @@ -203,6 +204,15 @@ void Server::ProcessQuery(const Header &aRequestHeader, Message &aRequestMessage VerifyOrExit(!aRequestHeader.IsTruncationFlagSet(), response = Header::kResponseFormatError); VerifyOrExit(aRequestHeader.GetQuestionCount() > 0, response = Header::kResponseFormatError); + switch (mTestMode) + { + case kTestModeDisabled: + break; + case kTestModeSingleQuestionOnly: + VerifyOrExit(aRequestHeader.GetQuestionCount() == 1, response = Header::kResponseFormatError); + break; + } + response = AddQuestions(aRequestHeader, aRequestMessage, responseHeader, *responseMessage, compressInfo); VerifyOrExit(response == Header::kResponseSuccess); diff --git a/src/core/net/dnssd_server.hpp b/src/core/net/dnssd_server.hpp index cf38ffc4f..bebcf41c5 100644 --- a/src/core/net/dnssd_server.hpp +++ b/src/core/net/dnssd_server.hpp @@ -262,6 +262,27 @@ public: */ const Counters &GetCounters(void) const { return mCounters; }; + /** + * This enumeration represents different test modes. + * + * The test mode is intended for testing the client by having server behave in certain ways, e.g., reject messages + * with certain format (e.g., more than one question in query). + * + */ + enum TestMode : uint8_t + { + kTestModeDisabled, ///< Test mode is disabled. + kTestModeSingleQuestionOnly, ///< Allow single question in query message, send `FormatError` for two or more. + }; + + /** + * This method sets the test mode for `Server`. + * + * @param[in] aTestMode The new test mode. + * + */ + void SetTestMode(TestMode aTestMode) { mTestMode = aTestMode; } + private: class NameCompressInfo : public Clearable { @@ -531,8 +552,8 @@ private: #endif ServerTimer mTimer; - - Counters mCounters; + Counters mCounters; + TestMode mTestMode; }; } // namespace ServiceDiscovery diff --git a/tests/scripts/expect/tun-dns-over-tcp-client.exp b/tests/scripts/expect/tun-dns-over-tcp-client.exp index 3e74bfdb8..3da428a58 100755 --- a/tests/scripts/expect/tun-dns-over-tcp-client.exp +++ b/tests/scripts/expect/tun-dns-over-tcp-client.exp @@ -39,7 +39,7 @@ switch_node 1 set addr_1 [get_ipaddr mleid] switch_node 2 -send "dns resolve ipv6.google.com $addr_1 2000 6000 4 1 tcp\n" +send "dns resolve ipv6.google.com $addr_1 2000 6000 4 1 def tcp\n" expect "DNS response for ipv6.google.com" expect_line "Done" diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index aeda3cc73..adc0647a1 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -258,6 +258,27 @@ target_link_libraries(ot-test-dns add_test(NAME ot-test-dns COMMAND ot-test-dns) +add_executable(ot-test-dns-client + test_dns_client.cpp +) + +target_include_directories(ot-test-dns-client + PRIVATE + ${COMMON_INCLUDES} +) + +target_compile_options(ot-test-dns-client + PRIVATE + ${COMMON_COMPILE_OPTIONS} +) + +target_link_libraries(ot-test-dns-client + PRIVATE + ${COMMON_LIBS} +) + +add_test(NAME ot-test-dns-client COMMAND ot-test-dns-client) + add_executable(ot-test-dso test_dso.cpp ) diff --git a/tests/unit/test_dns_client.cpp b/tests/unit/test_dns_client.cpp new file mode 100644 index 000000000..1b27a5904 --- /dev/null +++ b/tests/unit/test_dns_client.cpp @@ -0,0 +1,682 @@ +/* + * Copyright (c) 2023, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "test_platform.h" +#include "test_util.hpp" + +#include +#include +#include +#include + +#include "common/arg_macros.hpp" +#include "common/array.hpp" +#include "common/instance.hpp" +#include "common/string.hpp" +#include "common/time.hpp" + +#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE && \ + OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE && OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE && \ + OPENTHREAD_CONFIG_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE && \ + !OPENTHREAD_CONFIG_TIME_SYNC_ENABLE && !OPENTHREAD_PLATFORM_POSIX +#define ENABLE_DNS_TEST 1 +#else +#define ENABLE_DNS_TEST 0 +#endif + +#if ENABLE_DNS_TEST + +using namespace ot; + +// Logs a message and adds current time (sNow) as "::." +#define Log(...) \ + printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 36000000), (sNow / 60000) % 60, \ + (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__)) + +static constexpr uint16_t kMaxRaSize = 800; + +static ot::Instance *sInstance; + +static uint32_t sNow = 0; +static uint32_t sAlarmTime; +static bool sAlarmOn = false; + +static otRadioFrame sRadioTxFrame; +static uint8_t sRadioTxFramePsdu[OT_RADIO_FRAME_MAX_SIZE]; +static bool sRadioTxOngoing = false; + +//---------------------------------------------------------------------------------------------------------------------- +// Function prototypes + +void ProcessRadioTxAndTasklets(void); +void AdvanceTime(uint32_t aDuration); + +//---------------------------------------------------------------------------------------------------------------------- +// `otPlatRadio` + +extern "C" { + +otError otPlatRadioTransmit(otInstance *, otRadioFrame *) +{ + sRadioTxOngoing = true; + + return OT_ERROR_NONE; +} + +otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { return &sRadioTxFrame; } + +//---------------------------------------------------------------------------------------------------------------------- +// `otPlatAlaram` + +void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; } + +void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt) +{ + sAlarmOn = true; + sAlarmTime = aT0 + aDt; +} + +uint32_t otPlatAlarmMilliGetNow(void) { return sNow; } + +//---------------------------------------------------------------------------------------------------------------------- + +Array sHeapAllocatedPtrs; + +#if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE +void *otPlatCAlloc(size_t aNum, size_t aSize) +{ + void *ptr = calloc(aNum, aSize); + + SuccessOrQuit(sHeapAllocatedPtrs.PushBack(ptr)); + + return ptr; +} + +void otPlatFree(void *aPtr) +{ + if (aPtr != nullptr) + { + void **entry = sHeapAllocatedPtrs.Find(aPtr); + + VerifyOrQuit(entry != nullptr, "A heap allocated item is freed twice"); + sHeapAllocatedPtrs.Remove(*entry); + } + + free(aPtr); +} +#endif + +#if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED +void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...) +{ + OT_UNUSED_VARIABLE(aLogLevel); + OT_UNUSED_VARIABLE(aLogRegion); + + va_list args; + + printf(" "); + va_start(args, aFormat); + vprintf(aFormat, args); + va_end(args); + printf("\n"); +} +#endif + +} // extern "C" + +//--------------------------------------------------------------------------------------------------------------------- + +void ProcessRadioTxAndTasklets(void) +{ + do + { + if (sRadioTxOngoing) + { + sRadioTxOngoing = false; + otPlatRadioTxStarted(sInstance, &sRadioTxFrame); + otPlatRadioTxDone(sInstance, &sRadioTxFrame, nullptr, OT_ERROR_NONE); + } + + otTaskletsProcess(sInstance); + } while (otTaskletsArePending(sInstance)); +} + +void AdvanceTime(uint32_t aDuration) +{ + uint32_t time = sNow + aDuration; + + Log("AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000); + + while (TimeMilli(sAlarmTime) <= TimeMilli(time)) + { + ProcessRadioTxAndTasklets(); + sNow = sAlarmTime; + otPlatAlarmMilliFired(sInstance); + } + + ProcessRadioTxAndTasklets(); + sNow = time; +} + +void InitTest(void) +{ + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Initialize OT instance. + + sNow = 0; + sInstance = static_cast(testInitInstance()); + + memset(&sRadioTxFrame, 0, sizeof(sRadioTxFrame)); + sRadioTxFrame.mPsdu = sRadioTxFramePsdu; + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Initialize Border Router and start Thread operation. + + SuccessOrQuit(otLinkSetPanId(sInstance, 0x1234)); + SuccessOrQuit(otIp6SetEnabled(sInstance, true)); + SuccessOrQuit(otThreadSetEnabled(sInstance, true)); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Ensure device starts as leader. + + AdvanceTime(10000); + + VerifyOrQuit(otThreadGetDeviceRole(sInstance) == OT_DEVICE_ROLE_LEADER); +} + +void FinalizeTest(void) +{ + SuccessOrQuit(otIp6SetEnabled(sInstance, false)); + SuccessOrQuit(otThreadSetEnabled(sInstance, false)); + // Make sure there is no message/buffer leak + VerifyOrQuit(sInstance->Get().GetFreeBufferCount() == + sInstance->Get().GetTotalBufferCount()); + SuccessOrQuit(otInstanceErasePersistentInfo(sInstance)); + testFreeInstance(sInstance); +} + +//--------------------------------------------------------------------------------------------------------------------- + +static const char kHostName[] = "elden"; +static const char kHostFullName[] = "elden.default.service.arpa."; + +static const char kService1Name[] = "_srv._udp"; +static const char kService1FullName[] = "_srv._udp.default.service.arpa."; +static const char kInstance1Label[] = "srv-instance"; + +static const char kService2Name[] = "_game._udp"; +static const char kService2FullName[] = "_game._udp.default.service.arpa."; +static const char kInstance2Label[] = "last-ninja"; + +void PrepareService1(Srp::Client::Service &aService) +{ + static const char kSub1[] = "_sub1"; + static const char kSub2[] = "_V1234567"; + static const char kSub3[] = "_XYZWS"; + static const char *kSubLabels[] = {kSub1, kSub2, kSub3, nullptr}; + static const char kTxtKey1[] = "ABCD"; + static const uint8_t kTxtValue1[] = {'a', '0'}; + static const char kTxtKey2[] = "Z0"; + static const uint8_t kTxtValue2[] = {'1', '2', '3'}; + static const char kTxtKey3[] = "D"; + static const uint8_t kTxtValue3[] = {0}; + static const otDnsTxtEntry kTxtEntries[] = { + {kTxtKey1, kTxtValue1, sizeof(kTxtValue1)}, + {kTxtKey2, kTxtValue2, sizeof(kTxtValue2)}, + {kTxtKey3, kTxtValue3, sizeof(kTxtValue3)}, + }; + + memset(&aService, 0, sizeof(aService)); + aService.mName = kService1Name; + aService.mInstanceName = kInstance1Label; + aService.mSubTypeLabels = kSubLabels; + aService.mTxtEntries = kTxtEntries; + aService.mNumTxtEntries = 3; + aService.mPort = 777; + aService.mWeight = 1; + aService.mPriority = 2; +} + +void PrepareService2(Srp::Client::Service &aService) +{ + static const char kSub4[] = "_44444444"; + static const char *kSubLabels2[] = {kSub4, nullptr}; + + memset(&aService, 0, sizeof(aService)); + aService.mName = kService2Name; + aService.mInstanceName = kInstance2Label; + aService.mSubTypeLabels = kSubLabels2; + aService.mTxtEntries = nullptr; + aService.mNumTxtEntries = 0; + aService.mPort = 555; + aService.mWeight = 0; + aService.mPriority = 3; +} + +void ValidateHost(Srp::Server &aServer, const char *aHostName) +{ + // Validate that only a host with `aHostName` is + // registered on SRP server. + + const Srp::Server::Host *host; + const char *name; + + Log("ValidateHost()"); + + host = aServer.GetNextHost(nullptr); + VerifyOrQuit(host != nullptr); + + name = host->GetFullName(); + Log("Hostname: %s", name); + + VerifyOrQuit(StringStartsWith(name, aHostName, kStringCaseInsensitiveMatch)); + VerifyOrQuit(name[strlen(aHostName)] == '.'); + + // Only one host on server + VerifyOrQuit(aServer.GetNextHost(host) == nullptr); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void LogServiceInfo(const Dns::Client::ServiceInfo &aInfo) +{ + Log(" TTL: %u", aInfo.mTtl); + Log(" Port: %u", aInfo.mPort); + Log(" Weight: %u", aInfo.mWeight); + Log(" HostName: %s", aInfo.mHostNameBuffer); + Log(" HostAddr: %s", AsCoreType(&aInfo.mHostAddress).ToString().AsCString()); + Log(" TxtDataLength: %u", aInfo.mTxtDataSize); + Log(" TxtDataTTL: %u", aInfo.mTxtDataTtl); +} + +const char *ServiceModeToString(Dns::Client::QueryConfig::ServiceMode aMode) +{ + static const char *const kServiceModeStrings[] = { + "unspec", // kServiceModeUnspecified (0) + "srv", // kServiceModeSrv (1) + "txt", // kServiceModeTxt (2) + "srv_txt", // kServiceModeSrvTxt (3) + "srv_txt_sep", // kServiceModeSrvTxtSeparate (4) + "srv_txt_opt", // kServiceModeSrvTxtOptimize (5) + }; + + static_assert(Dns::Client::QueryConfig::kServiceModeUnspecified == 0, "Unspecified value is incorrect"); + static_assert(Dns::Client::QueryConfig::kServiceModeSrv == 1, "Srv value is incorrect"); + static_assert(Dns::Client::QueryConfig::kServiceModeTxt == 2, "Txt value is incorrect"); + static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxt == 3, "SrvTxt value is incorrect"); + static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate == 4, "SrvTxtSeparate value is incorrect"); + static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize == 5, "SrvTxtOptimize value is incorrect"); + + return kServiceModeStrings[aMode]; +} + +//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct BrowseInfo +{ + void Reset(void) { mCallbackCount = 0; } + + uint16_t mCallbackCount; + Error mError; + char mServiceName[Dns::Name::kMaxNameSize]; + uint16_t mNumInstances; +}; + +static BrowseInfo sBrowseInfo; + +void BrowseCallback(otError aError, const otDnsBrowseResponse *aResponse, void *aContext) +{ + const Dns::Client::BrowseResponse &response = AsCoreType(aResponse); + + Log("BrowseCallback"); + Log(" Error: %s", ErrorToString(aError)); + + VerifyOrQuit(aContext == sInstance); + + sBrowseInfo.mCallbackCount++; + sBrowseInfo.mError = aError; + + SuccessOrExit(aError); + + SuccessOrQuit(response.GetServiceName(sBrowseInfo.mServiceName, sizeof(sBrowseInfo.mServiceName))); + Log(" ServiceName: %s", sBrowseInfo.mServiceName); + + for (uint16_t index = 0;; index++) + { + char instLabel[Dns::Name::kMaxLabelSize]; + Error error; + + error = response.GetServiceInstance(index, instLabel, sizeof(instLabel)); + + if (error == kErrorNotFound) + { + sBrowseInfo.mNumInstances = index; + break; + } + + SuccessOrQuit(error); + + Log(" %2u) %s", index + 1, instLabel); + } + +exit: + return; +} + +//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct ResolveServiceInfo +{ + void Reset(void) + { + memset(this, 0, sizeof(*this)); + mInfo.mHostNameBuffer = mNameBuffer; + mInfo.mHostNameBufferSize = sizeof(mNameBuffer); + mInfo.mTxtData = mTxtBuffer; + mInfo.mTxtDataSize = sizeof(mTxtBuffer); + }; + + uint16_t mCallbackCount; + Error mError; + Dns::Client::ServiceInfo mInfo; + char mNameBuffer[Dns::Name::kMaxNameSize]; + uint8_t mTxtBuffer[256]; +}; + +static ResolveServiceInfo sResolveServiceInfo; + +void ServiceCallback(otError aError, const otDnsServiceResponse *aResponse, void *aContext) +{ + const Dns::Client::ServiceResponse &response = AsCoreType(aResponse); + char instLabel[Dns::Name::kMaxLabelSize]; + char serviceName[Dns::Name::kMaxNameSize]; + + Log("ServiceCallback"); + Log(" Error: %s", ErrorToString(aError)); + + VerifyOrQuit(aContext == sInstance); + + SuccessOrQuit(response.GetServiceName(instLabel, sizeof(instLabel), serviceName, sizeof(serviceName))); + Log(" InstLabel: %s", instLabel); + Log(" ServiceName: %s", serviceName); + + sResolveServiceInfo.mCallbackCount++; + sResolveServiceInfo.mError = aError; + + SuccessOrExit(aError); + SuccessOrQuit(response.GetServiceInfo(sResolveServiceInfo.mInfo)); + LogServiceInfo(sResolveServiceInfo.mInfo); + +exit: + return; +} + +//---------------------------------------------------------------------------------------------------------------------- + +void TestDnsClient(void) +{ + const Dns::Client::QueryConfig::ServiceMode kServiceModes[] = { + Dns::Client::QueryConfig::kServiceModeSrv, + Dns::Client::QueryConfig::kServiceModeTxt, + Dns::Client::QueryConfig::kServiceModeSrvTxt, + Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate, + Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize, + }; + + Srp::Server *srpServer; + Srp::Client *srpClient; + Srp::Client::Service service1; + Srp::Client::Service service2; + Dns::Client *dnsClient; + Dns::Client::QueryConfig queryConfig; + Dns::ServiceDiscovery::Server *dnsServer; + uint16_t heapAllocations; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDnsClient"); + + InitTest(); + + srpServer = &sInstance->Get(); + srpClient = &sInstance->Get(); + dnsClient = &sInstance->Get(); + dnsServer = &sInstance->Get(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + PrepareService1(service1); + PrepareService2(service2); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Start SRP server. + + SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast)); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled); + + srpServer->SetEnabled(true); + VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled); + + AdvanceTime(10000); + VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Start SRP client. + + srpClient->EnableAutoStartMode(nullptr, nullptr); + VerifyOrQuit(srpClient->IsAutoStartModeEnabled()); + + AdvanceTime(2000); + VerifyOrQuit(srpClient->IsRunning()); + + SuccessOrQuit(srpClient->SetHostName(kHostName)); + SuccessOrQuit(srpClient->EnableAutoHostAddress()); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Register two services on SRP. + + SuccessOrQuit(srpClient->AddService(service1)); + SuccessOrQuit(srpClient->AddService(service2)); + + AdvanceTime(2 * 1000); + + VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered); + VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered); + ValidateHost(*srpServer, kHostName); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Check DNS Client's default config + + VerifyOrQuit(dnsClient->GetDefaultConfig().GetServiceMode() == + Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Validate DNS Client `Browse()` + + sBrowseInfo.Reset(); + Log("Browse(%s)", kService1FullName); + SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance)); + AdvanceTime(100); + VerifyOrQuit(sBrowseInfo.mCallbackCount == 1); + SuccessOrQuit(sBrowseInfo.mError); + VerifyOrQuit(sBrowseInfo.mNumInstances == 1); + + sBrowseInfo.Reset(); + + Log("Browse(%s)", kService2FullName); + SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance)); + AdvanceTime(100); + VerifyOrQuit(sBrowseInfo.mCallbackCount == 1); + SuccessOrQuit(sBrowseInfo.mError); + VerifyOrQuit(sBrowseInfo.mNumInstances == 1); + + sBrowseInfo.Reset(); + Log("Browse() for unknwon service"); + SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance)); + AdvanceTime(100); + VerifyOrQuit(sBrowseInfo.mCallbackCount == 1); + VerifyOrQuit(sBrowseInfo.mError == kErrorNotFound); + + Log("Issue four parallel `Browse()` at the same time"); + sBrowseInfo.Reset(); + SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance)); + SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance)); + SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance)); + SuccessOrQuit(dnsClient->Browse("_unknown2._udp.default.service.arpa.", BrowseCallback, sInstance)); + AdvanceTime(100); + VerifyOrQuit(sBrowseInfo.mCallbackCount == 4); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Validate DNS Client `ResolveService()` using all service modes + + for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes) + { + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("ResolveService(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName, + ServiceModeToString(mode)); + + queryConfig.Clear(); + queryConfig.mServiceMode = static_cast(mode); + + sResolveServiceInfo.Reset(); + SuccessOrQuit( + dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig)); + AdvanceTime(100); + + VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1); + SuccessOrQuit(sResolveServiceInfo.mError); + + if (mode != Dns::Client::QueryConfig::kServiceModeTxt) + { + VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0); + VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort); + VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight); + VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0); + } + + if (mode != Dns::Client::QueryConfig::kServiceModeSrv) + { + VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0); + VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0); + } + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + Log("Set TestMode on server to only accept single question"); + dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeSingleQuestionOnly); + + Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName, + ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize)); + + queryConfig.Clear(); + queryConfig.mServiceMode = static_cast(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize); + + sResolveServiceInfo.Reset(); + SuccessOrQuit( + dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig)); + AdvanceTime(200); + + VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1); + SuccessOrQuit(sResolveServiceInfo.mError); + + // Use `kServiceModeSrvTxt` and check that server does reject two questions. + + Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName, + ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxt)); + + queryConfig.Clear(); + queryConfig.mServiceMode = static_cast(Dns::Client::QueryConfig::kServiceModeSrvTxt); + + sResolveServiceInfo.Reset(); + SuccessOrQuit( + dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig)); + AdvanceTime(200); + + VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1); + VerifyOrQuit(sResolveServiceInfo.mError != kErrorNone); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + Log("Stop DNS-SD server"); + dnsServer->Stop(); + + Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName, + ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrv)); + + queryConfig.Clear(); + queryConfig.mServiceMode = static_cast(Dns::Client::QueryConfig::kServiceModeSrv); + + sResolveServiceInfo.Reset(); + SuccessOrQuit( + dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig)); + AdvanceTime(25 * 1000); + + VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1); + VerifyOrQuit(sResolveServiceInfo.mError == kErrorResponseTimeout); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Disable SRP server, verify that all heap allocations by SRP server + // and/or by DNS Client are freed. + + Log("Disabling SRP server"); + + srpServer->SetEnabled(false); + AdvanceTime(100); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Finalize OT instance and validate all heap allocations are freed. + + Log("Finalizing OT instance"); + FinalizeTest(); + + VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty()); + + Log("End of TestDnsClient"); +} + +#endif // ENABLE_DNS_TEST + +int main(void) +{ +#if ENABLE_DNS_TEST + TestDnsClient(); + printf("All tests passed\n"); +#else + printf("DNS_CLIENT or DSNSSD_SERVER feature is not enabled\n"); +#endif + + return 0; +}