Repository: openthread/openthread
Pull request #11408: [dns] introduce `DecompressRecordData()` and use it in mDNS
Author: login: abtink
Labels: name: api: user
name: comp: dns
name: comp: mdns
Created: 2025-04-11T00:15:29Z | Merged: 2025-04-21T21:29:09Z

This commit updates the mDNS `RecordQuerier` to handle record types where the RDATA contains one or more potentially compressed DNS names. For these types, the reported record data is now decompressed to include the full DNS names. This enhancement applies to the following record types: NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC.

To achieve this, a helper `ResourceRecord::DecompressRecordData()` method is introduced. This method uses a "recipe" formula specific to each supported record type. The recipe defines the number of prefix bytes before the first embedded name, the number of DNS names, and the minimum number of suffix bytes after the names. A common implementation then uses this recipe to parse and decompress the RDATA. This approach makes the implementation flexible and allows for easier addition of new record types and formats in the future.

Unit test `test_dns` is updated to validate the newly added method.


Diff:
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index 71a0fe3c3c6..6549a0719c5 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 c0ed1cfe663..cdad91e534c 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 3f2fc36326f..a4ab1459630 100644
--- a/src/core/net/dns_types.cpp
+++ b/src/core/net/dns_types.cpp
@@ -1036,6 +1036,101 @@ Error ResourceRecord::ReadFrom(const Message &aMessage, uint16_t aOffset)
     return error;
 }
 
+Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr<Message> &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<MessagePool>().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 54633854c39..a95edec9460 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 @@ class ResourceRecord
     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 @@ class ResourceRecord
         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<Message>` 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<Message> &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 604f442acd1..2452157787f 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<Message> 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 c11545dcd97..0bd856706ad 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<Message>           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<uint8_t *>(&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");


Reviews:
user: login: Vyrastas
body: Just one nit
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-18T16:14:32Z
user: login: abtink
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T18:49:45Z
user: login: Vyrastas
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T18:05:56Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces ResourceRecord::DecompressRecordData to decompress DNS record data that contains compressed DNS names for various record types. Key changes include:
- Adding the new helper method in dns_types.hpp/.cpp with associated recipe logic.
- Updating mDNS record processing in mdns.cpp to use decompression.
- Enhancing DNS unit tests in test_dns.cpp and updating API documentation and version (from 497 to 498).

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                          | Description                                                      |
| ----------------------------- | ---------------------------------------------------------------- |
| tests/unit/test_dns.cpp       | Updated tests to verify decompression of RDATA for several record types. |
| src/core/net/mdns.cpp         | Modified record processing to call DecompressRecordData before reading record entries. |
| src/core/net/dns_types.hpp    | Added new record type constants and declaration/documentation for DecompressRecordData. |
| src/core/net/dns_types.cpp    | Introduced the implementation for DecompressRecordData.         |
| include/openthread/mdns.h     | Updated documentation to reflect decompressed RDATA in callbacks. |
| include/openthread/instance.h | Updated the API version (497 → 498) to reflect changes.            |
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T21:22:37Z
user: login: jwhui
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T21:29:03Z

Review comments:
diff_hunk: @@ -800,10 +800,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 type, the RDATA format
path: include/openthread/mdns.h
user: login: Vyrastas
body: ```suggestion
 * (RDATA). For NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC record types, the RDATA format
```
created_at: 2025-04-18T16:12:09Z
author_association: MEMBER
diff_hunk: @@ -800,10 +800,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 type, the RDATA format
path: include/openthread/mdns.h
user: login: abtink
body: Updated in new pushed commit.
created_at: 2025-04-19T18:49:45Z
author_association: MEMBER

PR comments:
user: login: size-report[bot]
created_at: 2025-04-11T00:20:48Z
body: 

# Size Report of **OpenThread**
> Merging [#11408](https://github.com/openthread/openthread/commit/63d5d75a64c1a632e3c7cdb9f313d8259b0f3b40) into [main](https://github.com/openthread/openthread/commit/0c1dfa0796f8f8978dd2d0432fe8587dec6b2c55)(0c1dfa0796f8f8978dd2d0432fe8587dec6b2c55).

|  name  |  branch  |  text  | data  | bss  | total |
| :----: | :------: | -----: | ----: | ---: | ----: |
| ot-cli-ftd | main | 476960 | 860 | 66372 | 544192 |
|  | #11408 | 476960 | 860 | 66372 | 544192 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-ncp-ftd | main | 443164 | 764 | 61592 | 505520 |
|  | #11408 | 443164 | 764 | 61592 | 505520 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ftd.a | main | 243414 | 95 | 40150 | 283659 |
|  | #11408 | 243776 | 95 | 40150 | 284021 |
|  | +/- | +362 | 0 | 0 | +362 |
| libopenthread-cli-ftd.a | main | 60493 | 0 | 8083 | 68576 |
|  | #11408 | 60493 | 0 | 8083 | 68576 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ncp-ftd.a | main | 32931 | 0 | 5924 | 38855 |
|  | #11408 | 32931 | 0 | 5924 | 38855 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-cli-mtd | main | 370088 | 764 | 50860 | 421712 |
|  | #11408 | 370088 | 764 | 50860 | 421712 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-ncp-mtd | main | 351540 | 764 | 46104 | 398408 |
|  | #11408 | 351540 | 764 | 46104 | 398408 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-mtd.a | main | 162033 | 0 | 24662 | 186695 |
|  | #11408 | 162395 | 0 | 24662 | 187057 |
|  | +/- | +362 | 0 | 0 | +362 |
| libopenthread-cli-mtd.a | main | 40888 | 0 | 8059 | 48947 |
|  | #11408 | 40888 | 0 | 8059 | 48947 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ncp-mtd.a | main | 25581 | 0 | 5924 | 31505 |
|  | #11408 | 25581 | 0 | 5924 | 31505 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-cli-ftd-br | main | 568056 | 868 | 133372 | 702296 |
|  | #11408 | 568056 | 868 | 133372 | 702296 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-ftd-br.a | main | 341160 | 100 | 107118 | 448378 |
|  | #11408 | 341564 | 100 | 107118 | 448782 |
|  | +/- | +404 | 0 | 0 | +404 |
| libopenthread-cli-ftd-br.a | main | 75313 | 0 | 8115 | 83428 |
|  | #11408 | 75313 | 0 | 8115 | 83428 |
|  | +/- | 0 | 0 | 0 | 0 |
| ot-rcp | main | 63168 | 568 | 20804 | 84540 |
|  | #11408 | 63168 | 568 | 20804 | 84540 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-rcp.a | main | 9932 | 0 | 5060 | 14992 |
|  | #11408 | 9932 | 0 | 5060 | 14992 |
|  | +/- | 0 | 0 | 0 | 0 |
| libopenthread-radio.a | main | 19263 | 0 | 238 | 19501 |
|  | #11408 | 19263 | 0 | 238 | 19501 |
|  | +/- | 0 | 0 | 0 | 0 |

author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-11T00:56:28Z
body: ## [Codecov](https://app.codecov.io/gh/openthread/openthread/pull/11408?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread) Report
Attention: Patch coverage is `90.90909%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 72.28%. Comparing base [(`0c1dfa0`)](https://app.codecov.io/gh/openthread/openthread/commit/0c1dfa0796f8f8978dd2d0432fe8587dec6b2c55?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread) to head [(`4f3e0c2`)](https://app.codecov.io/gh/openthread/openthread/commit/4f3e0c2419a6bfa952092c5bde57990ea4a2078f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/openthread/openthread/pull/11408?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread) | Patch % | Lines |
|---|---|---|
| [src/core/net/mdns.cpp](https://app.codecov.io/gh/openthread/openthread/pull/11408?src=pr&el=tree&filepath=src%2Fcore%2Fnet%2Fmdns.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread#diff-c3JjL2NvcmUvbmV0L21kbnMuY3Bw) | 0.00% | [8 Missing :warning: ](https://app.codecov.io/gh/openthread/openthread/pull/11408?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #11408      +/-   ##
==========================================
- Coverage   73.94%   72.28%   -1.67%     
==========================================
  Files         625      628       +3     
  Lines       95522    97350    +1828     
==========================================
- Hits        70636    70369     -267     
- Misses      24886    26981    +2095     
```

| [Files with missing lines](https://app.codecov.io/gh/openthread/openthread/pull/11408?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread) | Coverage Δ | |
|---|---|---|
| [src/core/net/dns\_types.cpp](https://app.codecov.io/gh/openthread/openthread/pull/11408?src=pr&el=tree&filepath=src%2Fcore%2Fnet%2Fdns_types.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread#diff-c3JjL2NvcmUvbmV0L2Ruc190eXBlcy5jcHA=) | `96.24% <100.00%> (+5.88%)` | :arrow_up: |
| [src/core/net/dns\_types.hpp](https://app.codecov.io/gh/openthread/openthread/pull/11408?src=pr&el=tree&filepath=src%2Fcore%2Fnet%2Fdns_types.hpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread#diff-c3JjL2NvcmUvbmV0L2Ruc190eXBlcy5ocHA=) | `94.73% <ø> (+1.05%)` | :arrow_up: |
| [tests/unit/test\_dns.cpp](https://app.codecov.io/gh/openthread/openthread/pull/11408?src=pr&el=tree&filepath=tests%2Funit%2Ftest_dns.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread#diff-dGVzdHMvdW5pdC90ZXN0X2Rucy5jcHA=) | `100.00% <100.00%> (ø)` | |
| [src/core/net/mdns.cpp](https://app.codecov.io/gh/openthread/openthread/pull/11408?src=pr&el=tree&filepath=src%2Fcore%2Fnet%2Fmdns.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread#diff-c3JjL2NvcmUvbmV0L21kbnMuY3Bw) | `0.97% <0.00%> (-0.01%)` | :arrow_down: |

... and [279 files with indirect coverage changes](https://app.codecov.io/gh/openthread/openthread/pull/11408/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openthread)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE