diff --git a/src/core/net/dns_types.cpp b/src/core/net/dns_types.cpp index 92c0444d6..3e45233e5 100644 --- a/src/core/net/dns_types.cpp +++ b/src/core/net/dns_types.cpp @@ -1036,26 +1036,8 @@ exit: return error; } -Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr &aDataMsg) +const ResourceRecord::DataRecipe *ResourceRecord::FindDataRecipeFor(uint16_t aRecordType) { - // Reads the `ResourceRecord` header to identify the record type - // and uses a predefined recipe to parse the record data. - - struct DataRecipe - { - int Compare(uint16_t aRecordType) const { return (aRecordType - mRecordType); } - - constexpr static bool AreInOrder(const DataRecipe &aFirst, const DataRecipe &aSecond) - { - return (aFirst.mRecordType < aSecond.mRecordType); - } - - uint16_t mRecordType; // The record type. - uint8_t mNumPrefixBytes; // Number of bytes in RDATA before the first name. - uint8_t mNumNames; // Number of DNS names embedded in the RDATA. - uint16_t mMinNumSuffixBytes; // Minimum number of expected bytes in RDATA after the last name. - }; - static constexpr DataRecipe kRecipes[] = { {kTypeNs, 0, 1, 0}, {kTypeCname, 0, 1, 0}, @@ -1074,6 +1056,14 @@ Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOf static_assert(BinarySearch::IsSorted(kRecipes), "kRecipes is not sorted"); + return BinarySearch::Find(aRecordType, kRecipes); +} + +Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr &aDataMsg) +{ + // Reads the `ResourceRecord` header to identify the record type + // and uses a predefined recipe to parse the record data. + Error error; ResourceRecord record; const DataRecipe *recipe; @@ -1083,7 +1073,7 @@ Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOf SuccessOrExit(error = record.ReadFrom(aMessage, aOffset)); aOffset += sizeof(ResourceRecord); - recipe = BinarySearch::Find(record.GetType(), kRecipes); + recipe = FindDataRecipeFor(record.GetType()); if (recipe == nullptr) { @@ -1131,6 +1121,89 @@ exit: return error; } +Error ResourceRecord::AppendTranslatedRecordDataTo(Message &aMessage, + uint16_t aRecordType, + const Data &aData, + const char *aOriginalDomain, + uint16_t aTranslatedDomainOffset) +{ + Error error = kErrorNone; + const DataRecipe *recipe = FindDataRecipeFor(aRecordType); + OwnedPtr dataMsg; + uint16_t offset; + uint16_t remainingLength; + + if (recipe == nullptr) + { + error = aMessage.AppendData(aData); + ExitNow(); + } + + dataMsg.Reset(aMessage.Get().Allocate(Message::kTypeOther)); + VerifyOrExit(dataMsg != nullptr, error = kErrorNoBufs); + SuccessOrExit(error = dataMsg->AppendData(aData)); + + // Append the prefix bytes in the record data. + + offset = 0; + SuccessOrExit(error = aMessage.AppendBytesFromMessage(*dataMsg, offset, recipe->mNumPrefixBytes)); + offset += recipe->mNumPrefixBytes; + + // Translate and append the embedded DNS names + + for (uint8_t numNames = 0; numNames < recipe->mNumNames; numNames++) + { + Name::LabelBuffer label; + uint8_t labelLength; + uint16_t labelOffset; + + // Read labels one by one and append them to `aMessage`. + // First, check if the remaining labels match the original + // domain name and if so, append the translated domain name + // (as a compressed pointer label) instead. + + labelOffset = offset; + + while (true) + { + uint16_t compareOffset = labelOffset; + + if (Name::CompareName(*dataMsg, compareOffset, aOriginalDomain) == kErrorNone) + { + SuccessOrExit(error = Name::AppendPointerLabel(aTranslatedDomainOffset, aMessage)); + break; + } + + labelLength = sizeof(label); + error = Name::ReadLabel(*dataMsg, labelOffset, label, labelLength); + + if (error == kErrorNotFound) + { + // Reached end of the label + break; + } + + SuccessOrExit(error); + + SuccessOrExit(error = Name::AppendLabel(label, aMessage)); + } + + // Parse name and update `offset` to the end of name field. + SuccessOrExit(error = Name::ParseName(*dataMsg, offset)); + } + + // Append the extra bytes after the name(s). + + VerifyOrExit(offset <= dataMsg->GetLength(), error = kErrorParse); + remainingLength = dataMsg->GetLength() - offset; + + VerifyOrExit(remainingLength >= recipe->mMinNumSuffixBytes, error = kErrorParse); + SuccessOrExit(error = aMessage.AppendBytesFromMessage(*dataMsg, offset, remainingLength)); + +exit: + return error; +} + ResourceRecord::TypeInfoString ResourceRecord::TypeToString(uint16_t aRecordType) { static constexpr Stringify::Entry kRecordTypeTable[] = { diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp index 41e8d59da..d9c231bb7 100644 --- a/src/core/net/dns_types.hpp +++ b/src/core/net/dns_types.hpp @@ -42,6 +42,7 @@ #include "common/appender.hpp" #include "common/as_core_type.hpp" #include "common/clearable.hpp" +#include "common/data.hpp" #include "common/encoding.hpp" #include "common/equatable.hpp" #include "common/message.hpp" @@ -1550,6 +1551,35 @@ public: */ static Error DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr &aDataMsg); + /** + * Translates embedded DNS names in record data (if needed) and appends the translated data to a given message. + * + * For records with type NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, or NSEC, the record data + * includes one or more embedded DNS names. For these record types, if the embedded DNS name uses the given + * @p aOriginalDomain, it is replaced with the translated domain name before appending it to @p aMessage. + * Otherwise, the name is appended as it appears in the record data. This is intended for use by the Discovery + * Proxy where the RDATA from mDNS will use the `.local.` domain name, which then needs to be translated to the + * Thread network domain name (`default.service.arpa.`). + * + * For other record types, the record data @p aData is appended as is. + * + * @param[in] aMessage The message to append the (translated) record data to. + * @param[in] aRecordType The record type. + * @param[in] aData The record data (to translate and append). + * @param[in] aOriginalDomain The original domain name. + * @param[in] aTranslatedDomainOffset The offset of the translated domain name in @p aMessage. + * + * @retval kErrorNone The (translated) record data was successfully appended to @p aMessage. + * @retval kErrorNoBufs Failed to allocate new buffers. + * @retval kErrorParse The given @p aData format is not valid. + * + */ + static Error AppendTranslatedRecordDataTo(Message &aMessage, + uint16_t aRecordType, + const Data &aData, + const char *aOriginalDomain, + uint16_t aTranslatedDomainOffset); + /** * Returns a human-readable string representation of a given resource record type. * @@ -1571,6 +1601,21 @@ protected: private: static constexpr uint16_t kType = kTypeAny; // This is intended for used by `ReadRecord()` only. + struct DataRecipe // RDATA recipe for record types that contain one or more embedded DNS names + { + int Compare(uint16_t aRecordType) const { return (aRecordType - mRecordType); } + + constexpr static bool AreInOrder(const DataRecipe &aFirst, const DataRecipe &aSecond) + { + return (aFirst.mRecordType < aSecond.mRecordType); + } + + uint16_t mRecordType; // The record type. + uint8_t mNumPrefixBytes; // Number of bytes in RDATA before the first name. + uint8_t mNumNames; // Number of DNS names embedded in the RDATA. + uint16_t mMinNumSuffixBytes; // Minimum number of expected bytes in RDATA after the last name. + }; + static Error FindRecord(const Message &aMessage, uint16_t &aOffset, uint16_t aNumRecords, @@ -1586,6 +1631,8 @@ private: ResourceRecord &aRecord, uint16_t aMinRecordSize); + static const DataRecipe *FindDataRecipeFor(uint16_t aRecordType); + Error CheckRecord(const Message &aMessage, uint16_t aOffset) const; Error ReadFrom(const Message &aMessage, uint16_t aOffset); diff --git a/src/core/net/dnssd_server.cpp b/src/core/net/dnssd_server.cpp index 701a879ba..3cdb1d617 100644 --- a/src/core/net/dnssd_server.cpp +++ b/src/core/net/dnssd_server.cpp @@ -45,6 +45,7 @@ RegisterLogModule("DnssdServer"); const char Server::kDefaultDomainName[] = "default.service.arpa."; const char Server::kSubLabel[] = "_sub"; +const char Server::kMdnsDomainName[] = "local."; #if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE const char *Server::kBlockedDomains[] = {"ipv4only.arpa."}; @@ -678,6 +679,7 @@ exit: Error Server::Response::AppendKeyRecord(const Srp::Server::Host &aHost) { Ecdsa256KeyRecord keyRecord; + RecordData keyData; uint32_t ttl; keyRecord.Init(); @@ -686,25 +688,31 @@ Error Server::Response::AppendKeyRecord(const Srp::Server::Host &aHost) keyRecord.SetAlgorithm(KeyRecord::kAlgorithmEcdsaP256Sha256); keyRecord.SetLength(sizeof(Ecdsa256KeyRecord) - sizeof(ResourceRecord)); keyRecord.SetKey(aHost.GetKey()); + keyData.InitFrom(keyRecord); ttl = TimeMilli::MsecToSec(aHost.GetExpireTime() - TimerMilli::GetNow()); - return AppendGenericRecord(Ecdsa256KeyRecord::kType, &keyRecord, sizeof(keyRecord), ttl); + return AppendGenericRecord(Ecdsa256KeyRecord::kType, keyData, ttl); } #endif -Error Server::Response::AppendGenericRecord(uint16_t aRrType, const void *aData, uint16_t aDataLength, uint32_t aTtl) +Error Server::Response::AppendGenericRecord(uint16_t aRrType, const RecordData &aData, uint32_t aTtl) { Error error = kErrorNone; ResourceRecord record; + uint16_t recordOffset; record.Init(aRrType); record.SetTtl(aTtl); - record.SetLength(aDataLength); SuccessOrExit(error = Name::AppendPointerLabel(mOffsets.mHostName, *mMessage)); + + recordOffset = mMessage->GetLength(); SuccessOrExit(error = mMessage->Append(record)); - SuccessOrExit(error = mMessage->AppendBytes(aData, aDataLength)); + + SuccessOrExit(error = ResourceRecord::AppendTranslatedRecordDataTo(*mMessage, aRrType, aData, kMdnsDomainName, + mOffsets.mDomainName)); + ResourceRecord::UpdateRecordLengthInMessage(*mMessage, recordOffset); IncResourceRecordCount(); @@ -2322,10 +2330,13 @@ exit: Error Server::Response::AppendGenericRecord(const ProxyResult &aResult) { const Dnssd::RecordResult *result = aResult.mRecordResult; + RecordData data; mSection = kAnswerSection; - return AppendGenericRecord(result->mRecordType, result->mRecordData, result->mRecordDataLength, result->mTtl); + data.Init(result->mRecordData, result->mRecordDataLength); + + return AppendGenericRecord(result->mRecordType, data, result->mTtl); } bool Server::IsProxyAddressValid(const Ip6::Address &aAddress) diff --git a/src/core/net/dnssd_server.hpp b/src/core/net/dnssd_server.hpp index 3fb91fa09..5fb2b26e5 100644 --- a/src/core/net/dnssd_server.hpp +++ b/src/core/net/dnssd_server.hpp @@ -350,6 +350,8 @@ private: }; #endif + typedef Data RecordData; + struct Questions { Questions(void) { mFirstRrType = 0, mSecondRrType = 0; } @@ -420,7 +422,7 @@ private: uint16_t aPort); Error AppendTxtRecord(const ServiceInstanceInfo &aInstanceInfo); Error AppendTxtRecord(const void *aTxtData, uint16_t aTxtLength, uint32_t aTtl); - Error AppendGenericRecord(uint16_t aRrType, const void *aData, uint16_t aDataLength, uint32_t aTtl); + Error AppendGenericRecord(uint16_t aRrType, const RecordData &aData, uint32_t aTtl); Error AppendHostAddresses(AddrType aAddrType, const HostInfo &aHostInfo); Error AppendHostAddresses(const ServiceInstanceInfo &aInstanceInfo); Error AppendHostAddresses(AddrType aAddrType, const Ip6::Address *aAddrs, uint16_t aAddrsLength, uint32_t aTtl); @@ -592,6 +594,7 @@ private: static const char kDefaultDomainName[]; static const char kSubLabel[]; + static const char kMdnsDomainName[]; #if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE static const char *kBlockedDomains[]; #endif diff --git a/tests/unit/test_dnssd_discovery_proxy.cpp b/tests/unit/test_dnssd_discovery_proxy.cpp index 7470b3127..3d5a97418 100644 --- a/tests/unit/test_dnssd_discovery_proxy.cpp +++ b/tests/unit/test_dnssd_discovery_proxy.cpp @@ -1041,6 +1041,20 @@ void TestProxyBasic(void) const uint8_t kTxtData[] = {3, 'A', '=', '1', 0}; const uint8_t kKeyData[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99}; + const uint8_t kCnameData[] = { + 10, 'p', 'r', 'o', 't', 'e', 'c', 't', 'o', 'r', 's', /* protectors */ + 5, 'l', 'o', 'c', 'a', 'l', /* local */ + 0, + }; + + const uint8_t kTranslatedCnameData[] = { + 10, 'p', 'r', 'o', 't', 'e', 'c', 't', 'o', 'r', 's', /* protectors */ + 7, 'd', 'e', 'f', 'a', 'u', 'l', 't', /* default */ + 7, 's', 'e', 'r', 'v', 'i', 'c', 'e', /* service */ + 4, 'a', 'r', 'p', 'a', /* arpa */ + 0, + }; + Srp::Server *srpServer; Srp::Client *srpClient; Dns::Client *dnsClient; @@ -1758,6 +1772,85 @@ void TestProxyBasic(void) VerifyOrQuit(!memcmp(sQueryRecordInfo.mRecords[0].mDataBuffer, kKeyData, sizeof(kKeyData))); VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer); + Log("--------------------------------------------------------------------------------------------"); + + ResetPlatDnssdApiInfo(); + sQueryRecordInfo.Reset(); + + Log("QueryRecord() for CNAME that requires RDATA translation"); + SuccessOrQuit(dnsClient->QueryRecord(Dns::ResourceRecord::kTypeCname, "avengers", "default.service.arpa.", + RecordCallback, sInstance)); + AdvanceTime(10); + + // Check that a record querier is started + + VerifyOrQuit(sStartBrowserInfo.mCallCount == 0); + VerifyOrQuit(sStopBrowserInfo.mCallCount == 0); + VerifyOrQuit(sStartSrvResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopSrvResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartTxtResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopTxtResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartIp6AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopIp6AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartIp4AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopIp4AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartRecordQuerierInfo.mCallCount == 1); + VerifyOrQuit(sStopRecordQuerierInfo.mCallCount == 0); + + VerifyOrQuit(sStartRecordQuerierInfo.NameMatches("avengers", nullptr)); + + VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Invoke Record Querier callback"); + + recordResult.mFirstLabel = "avengers"; + recordResult.mNextLabels = nullptr; + recordResult.mRecordType = Dns::ResourceRecord::kTypeCname; + recordResult.mRecordData = kCnameData; + recordResult.mRecordDataLength = sizeof(kCnameData); + recordResult.mTtl = kTtl; + recordResult.mInfraIfIndex = kInfraIfIndex; + + InvokeRecordQuerierCallback(sStartRecordQuerierInfo.mCallback, recordResult); + + AdvanceTime(10); + + // Check that the record querier is stopped + + VerifyOrQuit(sStartBrowserInfo.mCallCount == 0); + VerifyOrQuit(sStopBrowserInfo.mCallCount == 0); + VerifyOrQuit(sStartSrvResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopSrvResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartTxtResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopTxtResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartIp6AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopIp6AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartIp4AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStopIp4AddrResolverInfo.mCallCount == 0); + VerifyOrQuit(sStartRecordQuerierInfo.mCallCount == 1); + VerifyOrQuit(sStopRecordQuerierInfo.mCallCount == 1); + + VerifyOrQuit(sStopRecordQuerierInfo.NameMatches("avengers", nullptr)); + VerifyOrQuit(sStopRecordQuerierInfo.mCallback == sStartRecordQuerierInfo.mCallback); + + // Check that response is sent to client and validate + // that the CNAME RDATA is correctly translated. + + VerifyOrQuit(sQueryRecordInfo.mCallbackCount == 1); + SuccessOrQuit(sQueryRecordInfo.mError); + + VerifyOrQuit(!strcmp(sQueryRecordInfo.mQueryName, "avengers.default.service.arpa.")); + VerifyOrQuit(sQueryRecordInfo.mNumRecords == 1); + + VerifyOrQuit(!strcmp(sQueryRecordInfo.mRecords[0].mNameBuffer, "avengers.default.service.arpa.")); + VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordType == Dns::ResourceRecord::kTypeCname); + VerifyOrQuit(sQueryRecordInfo.mRecords[0].mRecordLength == sizeof(kTranslatedCnameData)); + VerifyOrQuit(sQueryRecordInfo.mRecords[0].mTtl == kTtl); + VerifyOrQuit(sQueryRecordInfo.mRecords[0].mDataBufferSize == sizeof(kTranslatedCnameData)); + VerifyOrQuit(!memcmp(sQueryRecordInfo.mRecords[0].mDataBuffer, kTranslatedCnameData, sizeof(kTranslatedCnameData))); + VerifyOrQuit(MapEnum(sQueryRecordInfo.mRecords[0].mSection) == Dns::Client::RecordInfo::kSectionAnswer); + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); Log("Stop DNS-SD server");