diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 71a0fe3c3..6549a0719 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (497) +#define OPENTHREAD_API_VERSION (498) /** * @addtogroup api-instance diff --git a/include/openthread/mdns.h b/include/openthread/mdns.h index c0ed1cfe6..cdad91e53 100644 --- a/include/openthread/mdns.h +++ b/include/openthread/mdns.h @@ -836,10 +836,16 @@ otError otMdnsStopIp4AddressResolver(otInstance *aInstance, const otMdnsAddressR * MUST NOT include the domain name. The reason for a separate first label is to allow it to include a dot `.` * character (as allowed for service instance labels). * - * Discovered results are reported through the `mCallback` function in @p aQuerier, providing the raw record - * data bytes. A removed record data is indicated with a TTL value of zero. The callback may be invoked immediately - * with cached information (if available) and potentially before this function returns. When cached results are used, - * the reported TTL value will reflect the original TTL from the last received response. + * Discovered results are reported through the `mCallback` function in @p aQuerier, providing the record data bytes + * (RDATA). For NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC record types, the RDATA format + * contains one or more DNS names (which may use DNS name compression). For the above list, the reported record data + * bytes via @p mCallback will be decompressed to contain the full DNS name(s). For all other record types, the record + * data bytes are provided exactly as they appear in the received mDNS response. This aligns the implementation with + * RFC 6762 (section 18.14) regarding the use of name compression. + * + * A removed record data is indicated with a TTL value of zero. The callback may be invoked immediately with cached + * information (if available) and potentially before this function returns. When cached results are used, the reported + * TTL value will reflect the original TTL from the last received response. * * Multiple querier instances can be started for the same name, provided they use different callback functions. * diff --git a/src/core/net/dns_types.cpp b/src/core/net/dns_types.cpp index 3f2fc3632..a4ab14596 100644 --- a/src/core/net/dns_types.cpp +++ b/src/core/net/dns_types.cpp @@ -1036,6 +1036,101 @@ exit: return error; } +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. + + 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}, + {kTypeSoa, 0, 2, 5 * sizeof(uint32_t)}, // mname, rname, followed by five 32-bit values. + {kTypePtr, 0, 1, 0}, + {kTypeMx, sizeof(uint16_t), 1, 0}, // `preference` 16-bit field, exchange name [RFC 1035] + {kTypeRp, 0, 2, 0}, /// `mbox-dname` `txt-dname` [RFC 1183] + {kTypeAfsdb, sizeof(uint16_t), 1, 0}, // `sub-type` 16-bit field, host name [RFC 1183] + {kTypeRt, sizeof(uint16_t), 1, 0}, // `preference` 16-bit field, host name [RFC 1183] + {kTypePx, sizeof(uint16_t), 2, 0}, // `preference` 16-bit field, two names [RFC 2163] + {kTypeSrv, sizeof(SrvRecord) - sizeof(ResourceRecord), 1, 0}, + {kTypeKx, sizeof(uint16_t), 1, 0}, // `preference` 16-bit field, name [RFC 2230] + {kTypeDname, 0, 1, 0}, + {kTypeNsec, 0, 1, NsecRecord::TypeBitMap::kMinSize}, + }; + + static_assert(BinarySearch::IsSorted(kRecipes), "kRecipes is not sorted"); + + Error error; + ResourceRecord record; + const DataRecipe *recipe; + uint16_t startOffset; + uint16_t remainingLength; + + SuccessOrExit(error = record.ReadFrom(aMessage, aOffset)); + aOffset += sizeof(ResourceRecord); + + recipe = BinarySearch::Find(record.GetType(), kRecipes); + + if (recipe == nullptr) + { + aDataMsg.Free(); + error = kErrorNone; + ExitNow(); + } + + aDataMsg.Reset(aMessage.Get().Allocate(Message::kTypeOther)); + VerifyOrExit(!aDataMsg.IsNull(), error = kErrorNoBufs); + + startOffset = aOffset; + + // Check and copy the prefix bytes in the record data. + + VerifyOrExit(record.GetLength() >= recipe->mNumPrefixBytes, error = kErrorParse); + SuccessOrExit(error = aDataMsg->AppendBytesFromMessage(aMessage, aOffset, recipe->mNumPrefixBytes)); + aOffset += recipe->mNumPrefixBytes; + + // Read and decompress embedded DNS names in the record data. + + for (uint8_t numNames = 0; numNames < recipe->mNumNames; numNames++) + { + Name name(aMessage, aOffset); + + // ParseName() updates `aOffset` to point to the byte after + // the end of name field. + + SuccessOrExit(error = Name::ParseName(aMessage, aOffset)); + SuccessOrExit(error = name.AppendTo(*aDataMsg)); + } + + // Determine the remaining length after the names in the record + // data. Ensure we have at least `mMinNumSuffixBytes` and copy + // them into `aDataMsg`. + + VerifyOrExit(aOffset - startOffset <= record.GetLength(), error = kErrorParse); + remainingLength = record.GetLength() - (aOffset - startOffset); + + VerifyOrExit(remainingLength >= recipe->mMinNumSuffixBytes, error = kErrorParse); + + SuccessOrExit(error = aDataMsg->AppendBytesFromMessage(aMessage, aOffset, remainingLength)); + +exit: + return error; +} + void TxtEntry::Iterator::Init(const uint8_t *aTxtData, uint16_t aTxtDataLength) { SetTxtData(aTxtData); diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp index 54633854c..a95edec94 100644 --- a/src/core/net/dns_types.hpp +++ b/src/core/net/dns_types.hpp @@ -45,6 +45,7 @@ #include "common/encoding.hpp" #include "common/equatable.hpp" #include "common/message.hpp" +#include "common/owned_ptr.hpp" #include "crypto/ecdsa.hpp" #include "net/ip4_types.hpp" #include "net/ip6_address.hpp" @@ -1268,14 +1269,20 @@ public: static constexpr uint16_t kTypeZero = 0; ///< Zero as special indicator for the SIG RR (SIG(0) from RFC 2931). static constexpr uint16_t kTypeA = 1; ///< Address record (IPv4). static constexpr uint16_t kTypeNs = 2; ///< NS record (an authoritative name server). - static constexpr uint16_t kTypeSoa = 6; ///< Start of (zone of) authority. static constexpr uint16_t kTypeCname = 5; ///< CNAME record. + static constexpr uint16_t kTypeSoa = 6; ///< SOA record (start of (zone of) authority). static constexpr uint16_t kTypePtr = 12; ///< PTR record. + static constexpr uint16_t kTypeMx = 15; ///< MAX record (mail exchange). static constexpr uint16_t kTypeTxt = 16; ///< TXT record. + static constexpr uint16_t kTypeRp = 17; ///< RP record (Responsible Person). + static constexpr uint16_t kTypeAfsdb = 18; ///< AFSDB record (AFS Data Base location). + static constexpr uint16_t kTypeRt = 21; ///< RT record (Route Through). static constexpr uint16_t kTypeSig = 24; ///< SIG record. static constexpr uint16_t kTypeKey = 25; ///< KEY record. + static constexpr uint16_t kTypePx = 26; ///< PX record (X.400 mail mapping information). static constexpr uint16_t kTypeAaaa = 28; ///< IPv6 address record. static constexpr uint16_t kTypeSrv = 33; ///< SRV locator record. + static constexpr uint16_t kTypeKx = 36; ///< KX record (Key Exchanger). static constexpr uint16_t kTypeDname = 39; ///< DNAME record. static constexpr uint16_t kTypeOpt = 41; ///< Option record. static constexpr uint16_t kTypeNsec = 47; ///< NSEC record. @@ -1506,6 +1513,38 @@ public: return ReadRecord(aMessage, aOffset, RecordType::kType, aRecord, sizeof(RecordType)); } + /** + * Parses and decompresses record data for specific record types where the record data format can contain one or + * more compressed DNS names. + * + * The following record types are handled: NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC. + * + * If the record type is not in the list above, this method returns `kErrorNone`, with @p aDataMsg remaining + * as `nullptr`. + * + * If the record type is in the above list (requires decompression) and is processed successfully, a new message + * is allocated where the decompressed record data is placed. The allocated message is returned via @p aDataMsg + * (its ownership is passed to the caller as indicated by the use of `OwnedPtr`). The allocated message contains + * the decompressed record data. Importantly it does not include the `ResourceRecord` header. + * + * When decompressing the record data, this method ensures any embedded DNS names are well-formed and parsable. + * It also verifies the record data meets the expected data format (e.g., minimum length based on the record type). + * Any additional bytes beyond the expected record data format are also copied to `aDataMsg`. No deep semantic + * validation of the record data content is performed. + * + * @param[in] aMessage The message to read from. `aMessage.GetOffset()` MUST point to the start of DNS header + * (this is used to handle compressed names). + * @param[in] aOffset The offset in @p aMessage pointing to the byte after the record name and the start of + * `ResourceRecord` fields. + * @param[out] aDataMsg A reference to an `OwnedPtr` to output the allocated message containing the + * decompressed record data. On input, it should be set to `nullptr`. + * + * @retval kErrorNone The record type did not require decompression, or decompression was successful. + * @retval kErrorNoBufs Failed to allocate a buffer to return the decompressed data. + * @retval kErrorParse The record data format is invalid. + */ + static Error DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr &aDataMsg); + protected: Error ReadName(const Message &aMessage, uint16_t &aOffset, diff --git a/src/core/net/mdns.cpp b/src/core/net/mdns.cpp index 604f442ac..245215778 100644 --- a/src/core/net/mdns.cpp +++ b/src/core/net/mdns.cpp @@ -7025,15 +7025,30 @@ void Core::RecordCache::ProcessResponseRecord(const Message &aMessage, { // Name and record type in `aMessage` are already matched. - // Adds a new record data to `mNewEntries` list. This called as - // the records in a received response are processed one by one. - // Once all records are processed `CommitNewResponseEntries()` is - // called to update the list. + // First, checks if the record data needs to be decompressed + // (the record data format can contain one or more compressed DNS + // names). This check applies to records: NS, CNAME, SOA, PTR, + // MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC. + // + // Then, adds the new record data to the `mNewEntries` list. This + // step occurs as the records in a received response are + // processed one by one. Once all records are processed, + // `CommitNewResponseEntries()` is called to update the list. - Heap::Data data; - NewRecordEntry *entry; + OwnedPtr dataMsg; + Heap::Data data; + NewRecordEntry *entry; - SuccessOrAssert(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength())); + SuccessOrExit(ResourceRecord::DecompressRecordData(aMessage, aRecordOffset, dataMsg)); + + if (dataMsg != nullptr) + { + SuccessOrAssert(data.SetFrom(*dataMsg)); + } + else + { + SuccessOrAssert(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength())); + } // Check for duplicates in the same response. If there // are exact duplicates, we remember the last one in the @@ -7053,6 +7068,9 @@ void Core::RecordCache::ProcessResponseRecord(const Message &aMessage, mNewEntries.Push(*entry); } + +exit: + return; } void Core::RecordCache::CommitNewResponseEntries(void) diff --git a/tests/unit/test_dns.cpp b/tests/unit/test_dns.cpp index c11545dcd..0bd856706 100644 --- a/tests/unit/test_dns.cpp +++ b/tests/unit/test_dns.cpp @@ -1122,7 +1122,7 @@ void TestHeaderAndResourceRecords(void) kHeaderOffset = 0, kQuestionCount = 1, kAnswerCount = 2, - kAdditionalCount = 5, + kAdditionalCount = 6, kTtl = 7200, kTxtTtl = 7300, kSrvPort = 1234, @@ -1146,30 +1146,34 @@ void TestHeaderAndResourceRecords(void) const char *kInstanceLabels[] = {kInstance1Label, kInstance2Label}; const char *kInstanceNames[] = {kInstance1Name, kInstance2Name}; - Instance *instance; - MessagePool *messagePool; - Message *message; - Dns::Header header; - uint16_t messageId; - uint16_t headerOffset; - uint16_t offset; - uint16_t numRecords; - uint16_t len; - uint16_t serviceNameOffset; - uint16_t hostNameOffset; - uint16_t answerSectionOffset; - uint16_t additionalSectionOffset; - uint16_t index; - Dns::PtrRecord ptrRecord; - Dns::SrvRecord srvRecord; - Dns::TxtRecord txtRecord; - Dns::AaaaRecord aaaaRecord; - Dns::ResourceRecord record; - Ip6::Address hostAddress; - - Dns::Name::LabelBuffer label; - Dns::Name::Buffer name; - uint8_t buffer[kMaxSize]; + Instance *instance; + MessagePool *messagePool; + Message *message; + Dns::Header header; + uint16_t messageId; + uint16_t headerOffset; + uint16_t offset; + uint16_t numRecords; + uint16_t len; + uint16_t serviceNameOffset; + uint16_t hostNameOffset; + uint16_t answerSectionOffset; + uint16_t additionalSectionOffset; + uint16_t index; + Dns::PtrRecord ptrRecord; + Dns::SrvRecord srvRecord; + Dns::TxtRecord txtRecord; + Dns::AaaaRecord aaaaRecord; + Dns::NsecRecord nsecRecord; + Dns::NsecRecord::TypeBitMap nsecBitmap; + Dns::ResourceRecord record; + Ip6::Address hostAddress; + Dns::Name::LabelBuffer label; + Dns::Name::Buffer name; + uint8_t buffer[kMaxSize]; + uint8_t *bytes; + OwnedPtr dataMsg; + uint16_t dataOffset; printf("================================================================\n"); printf("TestHeaderAndResourceRecords()\n"); @@ -1259,6 +1263,19 @@ void TestHeaderAndResourceRecords(void) aaaaRecord.SetAddress(hostAddress); SuccessOrQuit(message->Append(aaaaRecord)); + nsecRecord.Init(); + nsecRecord.SetTtl(kTtl); + nsecBitmap.Clear(); + nsecBitmap.AddType(Dns::ResourceRecord::kTypeAaaa); + + SuccessOrQuit(Dns::Name::AppendPointerLabel(hostNameOffset, *message)); + offset = message->GetLength(); + SuccessOrQuit(message->Append(nsecRecord)); + SuccessOrQuit(Dns::Name::AppendPointerLabel(hostNameOffset, *message)); + SuccessOrQuit(message->AppendBytes(&nsecBitmap, nsecBitmap.GetSize())); + nsecRecord.SetLength(message->GetLength() - offset - sizeof(Dns::ResourceRecord)); + message->Write(offset, nsecRecord); + // Dump the entire message VerifyOrQuit(message->GetLength() < kMaxSize, "Message is too long"); @@ -1301,6 +1318,13 @@ void TestHeaderAndResourceRecords(void) for (const char *instanceLabel : kInstanceLabels) { SuccessOrQuit(Dns::Name::CompareName(*message, offset, kServiceName)); + + SuccessOrQuit(Dns::ResourceRecord::DecompressRecordData(*message, offset, dataMsg)); + VerifyOrQuit(dataMsg != nullptr); + dataOffset = 0; + VerifyOrQuit(Dns::Name::CompareName(*dataMsg, dataOffset, kServiceName)); + VerifyOrQuit(dataOffset == dataMsg->GetLength()); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, ptrRecord)); VerifyOrQuit(ptrRecord.GetTtl() == kTtl, "Read PTR is incorrect"); @@ -1374,6 +1398,7 @@ void TestHeaderAndResourceRecords(void) // SRV record SuccessOrQuit(Dns::Name::CompareName(*message, offset, instanceName)); + savedOffset = offset; SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, srvRecord)); VerifyOrQuit(srvRecord.GetTtl() == kTtl); VerifyOrQuit(srvRecord.GetPort() == kSrvPort); @@ -1384,8 +1409,24 @@ void TestHeaderAndResourceRecords(void) printf(" \"%s\" SRV %u %d %d %d %d \"%s\"\n", instanceName, srvRecord.GetTtl(), srvRecord.GetLength(), srvRecord.GetPort(), srvRecord.GetWeight(), srvRecord.GetPriority(), name); + // SRV record again using `DecompressRecordData()` + dataMsg.Free(); + SuccessOrQuit(Dns::ResourceRecord::DecompressRecordData(*message, savedOffset, dataMsg)); + VerifyOrQuit(dataMsg != nullptr); + dataOffset = 0; + len = sizeof(Dns::SrvRecord) - sizeof(Dns::ResourceRecord); + bytes = reinterpret_cast(&srvRecord); + bytes += sizeof(Dns::ResourceRecord); + VerifyOrQuit(dataMsg->CompareBytes(dataOffset, bytes, len)); + dataOffset += len; + SuccessOrQuit(Dns::Name::CompareName(*dataMsg, dataOffset, kHostName)); + VerifyOrQuit(dataOffset == dataMsg->GetLength()); + // TXT record SuccessOrQuit(Dns::Name::CompareName(*message, offset, instanceName)); + dataMsg.Free(); + SuccessOrQuit(Dns::ResourceRecord::DecompressRecordData(*message, offset, dataMsg)); + VerifyOrQuit(dataMsg == nullptr); SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, txtRecord)); VerifyOrQuit(txtRecord.GetTtl() == kTxtTtl); savedOffset = offset; @@ -1405,13 +1446,34 @@ void TestHeaderAndResourceRecords(void) VerifyOrQuit(savedOffset == offset); } + // AAAA record SuccessOrQuit(Dns::Name::CompareName(*message, offset, kHostName)); + dataMsg.Free(); + SuccessOrQuit(Dns::ResourceRecord::DecompressRecordData(*message, offset, dataMsg)); + VerifyOrQuit(dataMsg == nullptr); SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, aaaaRecord)); VerifyOrQuit(aaaaRecord.GetTtl() == kTtl); VerifyOrQuit(aaaaRecord.GetAddress() == hostAddress); printf(" \"%s\" AAAA %u %d \"%s\"\n", kHostName, aaaaRecord.GetTtl(), aaaaRecord.GetLength(), aaaaRecord.GetAddress().ToString().AsCString()); + // NSEC record + SuccessOrQuit(Dns::Name::CompareName(*message, offset, kHostName)); + dataMsg.Free(); + SuccessOrQuit(Dns::ResourceRecord::DecompressRecordData(*message, offset, dataMsg)); + VerifyOrQuit(dataMsg != nullptr); + dataOffset = 0; + SuccessOrQuit(Dns::Name::CompareName(*dataMsg, dataOffset, kHostName)); + VerifyOrQuit(dataMsg->CompareBytes(dataOffset, &nsecBitmap, nsecBitmap.GetSize())); + dataOffset += nsecBitmap.GetSize(); + VerifyOrQuit(dataOffset == dataMsg->GetLength()); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, record)); + VerifyOrQuit(record.GetType() == Dns::ResourceRecord::kTypeNsec); + VerifyOrQuit(nsecBitmap.ContainsType(Dns::ResourceRecord::kTypeAaaa)); + printf(" \"%s\" NSEC %u %d bitmap-size:%d\n", kHostName, record.GetTtl(), record.GetLength(), + nsecBitmap.GetSize()); + offset += record.GetLength(); + VerifyOrQuit(offset == message->GetLength(), "offset is incorrect after additional section parse"); // Use `ParseRecords()` to parse all records @@ -1451,6 +1513,12 @@ void TestHeaderAndResourceRecords(void) SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, record)); VerifyOrQuit(record.GetType() == Dns::ResourceRecord::kTypeAaaa); offset += record.GetLength(); + + SuccessOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, Dns::Name(kHostName))); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, record)); + VerifyOrQuit(record.GetType() == Dns::ResourceRecord::kTypeNsec); + offset += record.GetLength(); + VerifyOrQuit(offset == message->GetLength(), "offset is incorrect after additional section parse"); printf("Use FindRecord() to search for specific records:\n");