Skip to content

Commit 2e73358

Browse files
authored
[dns] introduce DecompressRecordData() and use it in mDNS (openthread#11408)
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.
1 parent 7536e3c commit 2e73358

6 files changed

Lines changed: 264 additions & 38 deletions

File tree

include/openthread/instance.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ extern "C" {
5252
*
5353
* @note This number versions both OpenThread platform and user APIs.
5454
*/
55-
#define OPENTHREAD_API_VERSION (497)
55+
#define OPENTHREAD_API_VERSION (498)
5656

5757
/**
5858
* @addtogroup api-instance

include/openthread/mdns.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -836,10 +836,16 @@ otError otMdnsStopIp4AddressResolver(otInstance *aInstance, const otMdnsAddressR
836836
* MUST NOT include the domain name. The reason for a separate first label is to allow it to include a dot `.`
837837
* character (as allowed for service instance labels).
838838
*
839-
* Discovered results are reported through the `mCallback` function in @p aQuerier, providing the raw record
840-
* data bytes. A removed record data is indicated with a TTL value of zero. The callback may be invoked immediately
841-
* with cached information (if available) and potentially before this function returns. When cached results are used,
842-
* the reported TTL value will reflect the original TTL from the last received response.
839+
* Discovered results are reported through the `mCallback` function in @p aQuerier, providing the record data bytes
840+
* (RDATA). For NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC record types, the RDATA format
841+
* contains one or more DNS names (which may use DNS name compression). For the above list, the reported record data
842+
* bytes via @p mCallback will be decompressed to contain the full DNS name(s). For all other record types, the record
843+
* data bytes are provided exactly as they appear in the received mDNS response. This aligns the implementation with
844+
* RFC 6762 (section 18.14) regarding the use of name compression.
845+
*
846+
* A removed record data is indicated with a TTL value of zero. The callback may be invoked immediately with cached
847+
* information (if available) and potentially before this function returns. When cached results are used, the reported
848+
* TTL value will reflect the original TTL from the last received response.
843849
*
844850
* Multiple querier instances can be started for the same name, provided they use different callback functions.
845851
*

src/core/net/dns_types.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,101 @@ Error ResourceRecord::ReadFrom(const Message &aMessage, uint16_t aOffset)
10361036
return error;
10371037
}
10381038

1039+
Error ResourceRecord::DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr<Message> &aDataMsg)
1040+
{
1041+
// Reads the `ResourceRecord` header to identify the record type
1042+
// and uses a predefined recipe to parse the record data.
1043+
1044+
struct DataRecipe
1045+
{
1046+
int Compare(uint16_t aRecordType) const { return (aRecordType - mRecordType); }
1047+
1048+
constexpr static bool AreInOrder(const DataRecipe &aFirst, const DataRecipe &aSecond)
1049+
{
1050+
return (aFirst.mRecordType < aSecond.mRecordType);
1051+
}
1052+
1053+
uint16_t mRecordType; // The record type.
1054+
uint8_t mNumPrefixBytes; // Number of bytes in RDATA before the first name.
1055+
uint8_t mNumNames; // Number of DNS names embedded in the RDATA.
1056+
uint16_t mMinNumSuffixBytes; // Minimum number of expected bytes in RDATA after the last name.
1057+
};
1058+
1059+
static constexpr DataRecipe kRecipes[] = {
1060+
{kTypeNs, 0, 1, 0},
1061+
{kTypeCname, 0, 1, 0},
1062+
{kTypeSoa, 0, 2, 5 * sizeof(uint32_t)}, // mname, rname, followed by five 32-bit values.
1063+
{kTypePtr, 0, 1, 0},
1064+
{kTypeMx, sizeof(uint16_t), 1, 0}, // `preference` 16-bit field, exchange name [RFC 1035]
1065+
{kTypeRp, 0, 2, 0}, /// `mbox-dname` `txt-dname` [RFC 1183]
1066+
{kTypeAfsdb, sizeof(uint16_t), 1, 0}, // `sub-type` 16-bit field, host name [RFC 1183]
1067+
{kTypeRt, sizeof(uint16_t), 1, 0}, // `preference` 16-bit field, host name [RFC 1183]
1068+
{kTypePx, sizeof(uint16_t), 2, 0}, // `preference` 16-bit field, two names [RFC 2163]
1069+
{kTypeSrv, sizeof(SrvRecord) - sizeof(ResourceRecord), 1, 0},
1070+
{kTypeKx, sizeof(uint16_t), 1, 0}, // `preference` 16-bit field, name [RFC 2230]
1071+
{kTypeDname, 0, 1, 0},
1072+
{kTypeNsec, 0, 1, NsecRecord::TypeBitMap::kMinSize},
1073+
};
1074+
1075+
static_assert(BinarySearch::IsSorted(kRecipes), "kRecipes is not sorted");
1076+
1077+
Error error;
1078+
ResourceRecord record;
1079+
const DataRecipe *recipe;
1080+
uint16_t startOffset;
1081+
uint16_t remainingLength;
1082+
1083+
SuccessOrExit(error = record.ReadFrom(aMessage, aOffset));
1084+
aOffset += sizeof(ResourceRecord);
1085+
1086+
recipe = BinarySearch::Find(record.GetType(), kRecipes);
1087+
1088+
if (recipe == nullptr)
1089+
{
1090+
aDataMsg.Free();
1091+
error = kErrorNone;
1092+
ExitNow();
1093+
}
1094+
1095+
aDataMsg.Reset(aMessage.Get<MessagePool>().Allocate(Message::kTypeOther));
1096+
VerifyOrExit(!aDataMsg.IsNull(), error = kErrorNoBufs);
1097+
1098+
startOffset = aOffset;
1099+
1100+
// Check and copy the prefix bytes in the record data.
1101+
1102+
VerifyOrExit(record.GetLength() >= recipe->mNumPrefixBytes, error = kErrorParse);
1103+
SuccessOrExit(error = aDataMsg->AppendBytesFromMessage(aMessage, aOffset, recipe->mNumPrefixBytes));
1104+
aOffset += recipe->mNumPrefixBytes;
1105+
1106+
// Read and decompress embedded DNS names in the record data.
1107+
1108+
for (uint8_t numNames = 0; numNames < recipe->mNumNames; numNames++)
1109+
{
1110+
Name name(aMessage, aOffset);
1111+
1112+
// ParseName() updates `aOffset` to point to the byte after
1113+
// the end of name field.
1114+
1115+
SuccessOrExit(error = Name::ParseName(aMessage, aOffset));
1116+
SuccessOrExit(error = name.AppendTo(*aDataMsg));
1117+
}
1118+
1119+
// Determine the remaining length after the names in the record
1120+
// data. Ensure we have at least `mMinNumSuffixBytes` and copy
1121+
// them into `aDataMsg`.
1122+
1123+
VerifyOrExit(aOffset - startOffset <= record.GetLength(), error = kErrorParse);
1124+
remainingLength = record.GetLength() - (aOffset - startOffset);
1125+
1126+
VerifyOrExit(remainingLength >= recipe->mMinNumSuffixBytes, error = kErrorParse);
1127+
1128+
SuccessOrExit(error = aDataMsg->AppendBytesFromMessage(aMessage, aOffset, remainingLength));
1129+
1130+
exit:
1131+
return error;
1132+
}
1133+
10391134
void TxtEntry::Iterator::Init(const uint8_t *aTxtData, uint16_t aTxtDataLength)
10401135
{
10411136
SetTxtData(aTxtData);

src/core/net/dns_types.hpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "common/encoding.hpp"
4646
#include "common/equatable.hpp"
4747
#include "common/message.hpp"
48+
#include "common/owned_ptr.hpp"
4849
#include "crypto/ecdsa.hpp"
4950
#include "net/ip4_types.hpp"
5051
#include "net/ip6_address.hpp"
@@ -1268,14 +1269,20 @@ class ResourceRecord
12681269
static constexpr uint16_t kTypeZero = 0; ///< Zero as special indicator for the SIG RR (SIG(0) from RFC 2931).
12691270
static constexpr uint16_t kTypeA = 1; ///< Address record (IPv4).
12701271
static constexpr uint16_t kTypeNs = 2; ///< NS record (an authoritative name server).
1271-
static constexpr uint16_t kTypeSoa = 6; ///< Start of (zone of) authority.
12721272
static constexpr uint16_t kTypeCname = 5; ///< CNAME record.
1273+
static constexpr uint16_t kTypeSoa = 6; ///< SOA record (start of (zone of) authority).
12731274
static constexpr uint16_t kTypePtr = 12; ///< PTR record.
1275+
static constexpr uint16_t kTypeMx = 15; ///< MAX record (mail exchange).
12741276
static constexpr uint16_t kTypeTxt = 16; ///< TXT record.
1277+
static constexpr uint16_t kTypeRp = 17; ///< RP record (Responsible Person).
1278+
static constexpr uint16_t kTypeAfsdb = 18; ///< AFSDB record (AFS Data Base location).
1279+
static constexpr uint16_t kTypeRt = 21; ///< RT record (Route Through).
12751280
static constexpr uint16_t kTypeSig = 24; ///< SIG record.
12761281
static constexpr uint16_t kTypeKey = 25; ///< KEY record.
1282+
static constexpr uint16_t kTypePx = 26; ///< PX record (X.400 mail mapping information).
12771283
static constexpr uint16_t kTypeAaaa = 28; ///< IPv6 address record.
12781284
static constexpr uint16_t kTypeSrv = 33; ///< SRV locator record.
1285+
static constexpr uint16_t kTypeKx = 36; ///< KX record (Key Exchanger).
12791286
static constexpr uint16_t kTypeDname = 39; ///< DNAME record.
12801287
static constexpr uint16_t kTypeOpt = 41; ///< Option record.
12811288
static constexpr uint16_t kTypeNsec = 47; ///< NSEC record.
@@ -1506,6 +1513,38 @@ class ResourceRecord
15061513
return ReadRecord(aMessage, aOffset, RecordType::kType, aRecord, sizeof(RecordType));
15071514
}
15081515

1516+
/**
1517+
* Parses and decompresses record data for specific record types where the record data format can contain one or
1518+
* more compressed DNS names.
1519+
*
1520+
* The following record types are handled: NS, CNAME, SOA, PTR, MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC.
1521+
*
1522+
* If the record type is not in the list above, this method returns `kErrorNone`, with @p aDataMsg remaining
1523+
* as `nullptr`.
1524+
*
1525+
* If the record type is in the above list (requires decompression) and is processed successfully, a new message
1526+
* is allocated where the decompressed record data is placed. The allocated message is returned via @p aDataMsg
1527+
* (its ownership is passed to the caller as indicated by the use of `OwnedPtr`). The allocated message contains
1528+
* the decompressed record data. Importantly it does not include the `ResourceRecord` header.
1529+
*
1530+
* When decompressing the record data, this method ensures any embedded DNS names are well-formed and parsable.
1531+
* It also verifies the record data meets the expected data format (e.g., minimum length based on the record type).
1532+
* Any additional bytes beyond the expected record data format are also copied to `aDataMsg`. No deep semantic
1533+
* validation of the record data content is performed.
1534+
*
1535+
* @param[in] aMessage The message to read from. `aMessage.GetOffset()` MUST point to the start of DNS header
1536+
* (this is used to handle compressed names).
1537+
* @param[in] aOffset The offset in @p aMessage pointing to the byte after the record name and the start of
1538+
* `ResourceRecord` fields.
1539+
* @param[out] aDataMsg A reference to an `OwnedPtr<Message>` to output the allocated message containing the
1540+
* decompressed record data. On input, it should be set to `nullptr`.
1541+
*
1542+
* @retval kErrorNone The record type did not require decompression, or decompression was successful.
1543+
* @retval kErrorNoBufs Failed to allocate a buffer to return the decompressed data.
1544+
* @retval kErrorParse The record data format is invalid.
1545+
*/
1546+
static Error DecompressRecordData(const Message &aMessage, uint16_t aOffset, OwnedPtr<Message> &aDataMsg);
1547+
15091548
protected:
15101549
Error ReadName(const Message &aMessage,
15111550
uint16_t &aOffset,

src/core/net/mdns.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7025,15 +7025,30 @@ void Core::RecordCache::ProcessResponseRecord(const Message &aMessage,
70257025
{
70267026
// Name and record type in `aMessage` are already matched.
70277027

7028-
// Adds a new record data to `mNewEntries` list. This called as
7029-
// the records in a received response are processed one by one.
7030-
// Once all records are processed `CommitNewResponseEntries()` is
7031-
// called to update the list.
7028+
// First, checks if the record data needs to be decompressed
7029+
// (the record data format can contain one or more compressed DNS
7030+
// names). This check applies to records: NS, CNAME, SOA, PTR,
7031+
// MX, RP, AFSDB, RT, PX, SRV, KX, DNAME, and NSEC.
7032+
//
7033+
// Then, adds the new record data to the `mNewEntries` list. This
7034+
// step occurs as the records in a received response are
7035+
// processed one by one. Once all records are processed,
7036+
// `CommitNewResponseEntries()` is called to update the list.
70327037

7033-
Heap::Data data;
7034-
NewRecordEntry *entry;
7038+
OwnedPtr<Message> dataMsg;
7039+
Heap::Data data;
7040+
NewRecordEntry *entry;
70357041

7036-
SuccessOrAssert(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength()));
7042+
SuccessOrExit(ResourceRecord::DecompressRecordData(aMessage, aRecordOffset, dataMsg));
7043+
7044+
if (dataMsg != nullptr)
7045+
{
7046+
SuccessOrAssert(data.SetFrom(*dataMsg));
7047+
}
7048+
else
7049+
{
7050+
SuccessOrAssert(data.SetFrom(aMessage, aRecordOffset + sizeof(ResourceRecord), aRecord.GetLength()));
7051+
}
70377052

70387053
// Check for duplicates in the same response. If there
70397054
// are exact duplicates, we remember the last one in the
@@ -7053,6 +7068,9 @@ void Core::RecordCache::ProcessResponseRecord(const Message &aMessage,
70537068

70547069
mNewEntries.Push(*entry);
70557070
}
7071+
7072+
exit:
7073+
return;
70567074
}
70577075

70587076
void Core::RecordCache::CommitNewResponseEntries(void)

0 commit comments

Comments
 (0)