From 9ea89ca20499466e16dcb8e25ef92cdf259268b2 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Mon, 25 Jan 2021 21:16:39 -0800 Subject: [PATCH] [dns-header] helper methods to parse/find/read record(s) in a message (#6097) This commit adds new helper methods in `ResourceRecord` class. `ParseRecords()` parses and skips over a given number of resource records in a message. `FindRecord()` searches in a given message to find the first resource record matching a given record name. And `ReadRecord()` tries to read a matching resource record (of a given type) from a message. If the record type does not match the type, it skips over the record. This commit also adds methods in `PtrRecord`, `SrvRecord`, `SigRecord` and `TxtRecord` to parse and read the data fields such as PTR name, host name, signer name and TXT data. This commit updates the DNS unit test to add a detailed new test constructing and parsing a DNS message (covering the behavior of the newly added and some of the existing methods). --- src/core/net/dns_headers.cpp | 208 ++++++++++++++++++++++ src/core/net/dns_headers.hpp | 280 ++++++++++++++++++++++++++++++ tests/unit/test_dns.cpp | 327 +++++++++++++++++++++++++++++++++++ 3 files changed, 815 insertions(+) diff --git a/src/core/net/dns_headers.cpp b/src/core/net/dns_headers.cpp index 5f6a23b9a..fc21b7a29 100644 --- a/src/core/net/dns_headers.cpp +++ b/src/core/net/dns_headers.cpp @@ -531,6 +531,169 @@ bool Name::LabelIterator::CompareLabel(const LabelIterator &aOtherIterator) cons mLabelLength); } +otError ResourceRecord::ParseRecords(const Message &aMessage, uint16_t &aOffset, uint16_t aNumRecords) +{ + otError error = OT_ERROR_NONE; + + while (aNumRecords > 0) + { + ResourceRecord record; + + SuccessOrExit(error = Name::ParseName(aMessage, aOffset)); + SuccessOrExit(error = record.ReadFrom(aMessage, aOffset)); + aOffset += static_cast(record.GetSize()); + aNumRecords--; + } + +exit: + return error; +} + +otError ResourceRecord::FindRecord(const Message &aMessage, uint16_t &aOffset, uint16_t &aNumRecords, const char *aName) +{ + otError error; + + while (aNumRecords > 0) + { + bool matches = true; + ResourceRecord record; + + error = Name::CompareName(aMessage, aOffset, aName); + + switch (error) + { + case OT_ERROR_NONE: + break; + case OT_ERROR_NOT_FOUND: + matches = false; + break; + default: + ExitNow(); + } + + SuccessOrExit(error = record.ReadFrom(aMessage, aOffset)); + aNumRecords--; + VerifyOrExit(!matches); + aOffset += static_cast(record.GetSize()); + } + + error = OT_ERROR_NOT_FOUND; + +exit: + return error; +} + +otError ResourceRecord::ReadRecord(const Message & aMessage, + uint16_t & aOffset, + uint16_t aType, + ResourceRecord &aRecord, + uint16_t aMinRecordSize) +{ + // This static method tries to read a matching resource record of a + // given type and a minimum record size from a message. The `aType` + // value of `kTypeAny` matches any type. If the record in the + // message does not match, it skips over the record. Please see + // `ReadRecord()` for more details. + + otError error; + ResourceRecord record; + + SuccessOrExit(error = record.ReadFrom(aMessage, aOffset)); + + if (((aType == kTypeAny) || (record.GetType() == aType)) && (record.GetSize() >= aMinRecordSize)) + { + IgnoreError(aMessage.Read(aOffset, &aRecord, aMinRecordSize)); + aOffset += aMinRecordSize; + } + else + { + // Skip over the entire record. + aOffset += static_cast(record.GetSize()); + error = OT_ERROR_NOT_FOUND; + } + +exit: + return error; +} + +otError ResourceRecord::ReadName(const Message &aMessage, + uint16_t & aOffset, + uint16_t aStartOffset, + char * aNameBuffer, + uint16_t aNameBufferSize, + bool aSkipRecord) const +{ + // This protected method parses and reads a name field in a record + // from a message. It is intended only for sub-classes of + // `ResourceRecord`. + // + // On input `aOffset` gives the offset in `aMessage` to the start of + // name field. `aStartOffset` gives the offset to the start of the + // `ResourceRecord`. `aSkipRecord` indicates whether to skip over + // the entire resource record or just the read name. On exit, when + // successfully read, `aOffset` is updated to either point after the + // end of record or after the the name field. + // + // When read successfully, this method returns `OT_ERROR_NONE`. On a + // parse error (invalid format) returns `OT_ERROR_PARSE`. If the + // name does not fit in the given name buffer it returns + // `OT_ERROR_NO_BUFS` + + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = Name::ReadName(aMessage, aOffset, aNameBuffer, aNameBufferSize)); + VerifyOrExit(aOffset <= aStartOffset + GetSize(), error = OT_ERROR_PARSE); + + VerifyOrExit(aSkipRecord); + aOffset = aStartOffset; + error = SkipRecord(aMessage, aOffset); + +exit: + return error; +} + +otError ResourceRecord::SkipRecord(const Message &aMessage, uint16_t &aOffset) const +{ + // This protected method parses and skips over a resource record + // in a message. + // + // On input `aOffset` gives the offset in `aMessage` to the start of + // the `ResourceRecord`. On exit, when successfully parsed, `aOffset` + // is updated to point to byte after the entire record. + + otError error; + + SuccessOrExit(error = CheckRecord(aMessage, aOffset)); + aOffset += static_cast(GetSize()); + +exit: + return error; +} + +otError ResourceRecord::CheckRecord(const Message &aMessage, uint16_t aOffset) const +{ + // This method checks that the entire record (including record data) + // is present in `aMessage` at `aOffset` (pointing to the start of + // the `ResourceRecord` in `aMessage`). + + return (aOffset + GetSize() <= aMessage.GetLength()) ? OT_ERROR_NONE : OT_ERROR_PARSE; +} + +otError ResourceRecord::ReadFrom(const Message &aMessage, uint16_t aOffset) +{ + // This method reads the `ResourceRecord` from `aMessage` at + // `aOffset`. It verifies that the entire record (including record + // data) is present in the message. + + otError error; + + SuccessOrExit(error = aMessage.Read(aOffset, *this)); + error = CheckRecord(aMessage, aOffset); + +exit: + return error; +} + bool AaaaRecord::IsValid(void) const { return GetType() == Dns::ResourceRecord::kTypeAaaa && GetSize() == sizeof(*this); @@ -565,5 +728,50 @@ bool LeaseOption::IsValid(void) const return GetLeaseInterval() <= GetKeyLeaseInterval(); } +otError PtrRecord::ReadPtrName(const Message &aMessage, + uint16_t & aOffset, + char * aLabelBuffer, + uint8_t aLabelBufferSize, + char * aNameBuffer, + uint16_t aNameBufferSize) const +{ + otError error = OT_ERROR_NONE; + uint16_t startOffset = aOffset - sizeof(PtrRecord); // start of `PtrRecord`. + + // Verify that the name is within the record data length. + SuccessOrExit(error = Name::ParseName(aMessage, aOffset)); + VerifyOrExit(aOffset <= startOffset + GetSize(), error = OT_ERROR_PARSE); + + aOffset = startOffset + sizeof(PtrRecord); + SuccessOrExit(error = Name::ReadLabel(aMessage, aOffset, aLabelBuffer, aLabelBufferSize)); + + if (aNameBuffer != nullptr) + { + SuccessOrExit(error = Name::ReadName(aMessage, aOffset, aNameBuffer, aNameBufferSize)); + } + + aOffset = startOffset; + error = SkipRecord(aMessage, aOffset); + +exit: + return error; +} + +otError TxtRecord::ReadTxtData(const Message &aMessage, + uint16_t & aOffset, + uint8_t * aTxtBuffer, + uint16_t & aTxtBufferSize) const +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit(GetLength() <= aTxtBufferSize, error = OT_ERROR_NO_BUFS); + SuccessOrExit(error = aMessage.Read(aOffset, aTxtBuffer, GetLength())); + aTxtBufferSize = GetLength(); + aOffset += GetLength(); + +exit: + return error; +} + } // namespace Dns } // namespace ot diff --git a/src/core/net/dns_headers.hpp b/src/core/net/dns_headers.hpp index 6d36ad582..5daaa1a0a 100644 --- a/src/core/net/dns_headers.hpp +++ b/src/core/net/dns_headers.hpp @@ -233,6 +233,7 @@ public: * This method denotes whether recursive query support is available in the name server. * * @returns True if Recursion Available flag (RA) is set in the header, false otherwise. + * */ bool IsRecursionAvailableFlagSet(void) const { return (mFlags[1] & kRaFlagMask) == kRaFlagMask; } @@ -934,7 +935,108 @@ public: */ uint32_t GetSize(void) const { return sizeof(ResourceRecord) + GetLength(); } + /** + * This static method parses and skips over a given number of resource records in a message from a given offset. + * + * @param[in] aMessage The message from which to parse/read the resource records. `aMessage.GetOffset()` + * MUST point to the start of DNS header. + * @param[inout] aOffset On input the offset in @p aMessage pointing to the start of the first record. + * On exit (when parsed successfully), @p aOffset is updated to point to the byte after + * the last parsed record. + * @param[in] aNumRecords Number of resource records to parse. + * + * @retval OT_ERROR_NONE Parsed records successfully. @p aOffset is updated. + * @retval OT_ERROR_PARSE Could not parse the records from @p aMessage (e.g., ran out of bytes in @p aMessage). + * + */ + static otError ParseRecords(const Message &aMessage, uint16_t &aOffset, uint16_t aNumRecords); + + /** + * This static method searches in a given message to find the first resource record matching a given record name. + * + * @param[in] aMessage The message in which to search for a matching resource record. + * `aMessage.GetOffset()` MUST point to the start of DNS header. + * @param[inout] aOffset On input, the offset in @p aMessage pointing to the start of the first record. + * On exit, if a matching record is found, @p aOffset is updated to point to the byte + * after the record name. + * If a matching record could not be found, @p aOffset is updated to point to the byte + * after the last record that was checked. + * @param[inout] aNumRecords On input, the maximum number of records to check (starting from @p aOffset). + * On exit and if a matching record is found, @p aNumRecords is updated to give the + * number of remaining records after @p aOffset (excluding the matching record). + * @param[in] aName The record name to match against (MUST be a null terminated string). + * + * @retval OT_ERROR_NONE A matching record was found. @p aOffset, @p aNumRecords are updated. + * @retval OT_ERROR_NOT_FOUND A matching record could not be found. @p aOffset and @p aNumRecords are updated. + * @retval OT_ERROR_PARSE Could not parse records from @p aMessage (e.g., ran out of bytes in @p aMessage). + * + */ + static otError FindRecord(const Message &aMessage, uint16_t &aOffset, uint16_t &aNumRecords, const char *aName); + + /** + * This template static method tries to read a resource record of a given type from a message. If the record type + * does not matches the type, it skips over the record. + * + * This method requires the record name to be already parsed/read from the message. On input, @p aOffset should + * point to the start of the `ResourceRecord` fields (type, class, TTL, data length) in @p aMessage. + * + * This method verifies that the record is well-formed in the message. It then reads the record type and compares + * it with `RecordType::kType` and ensures that the record size is at least `sizeof(RecordType)`. If it all matches, + * the record is read into @p aRecord. + * + * On success (i.e., when a matching record is read from the message), the @p aOffset is updated to point to after + * the last byte read from the message and copied into @p aRecord and not necessarily the end of the record. + * Depending on the `RecordType` format, there may still be more data bytes left in the record to be read. For + * example, when reading a SRV record using `SrvRecord` type, @p aOffset would point to after the last field in + * `SrvRecord` which is the start of "target host domain name" field. + * + * @tparam RecordType The resource record type (i.e., a sub-class of `ResourceRecord`). + * + * @param[in] aMessage The message from which to read the record. + * @param[inout] aOffset On input, the offset in @p aMessage pointing to the byte after the record name. + * On exit, if a matching record is read, @p aOffset is updated to point to the last + * read byte in the record. + * If a matching record could not be read, @p aOffset is updated to point to the byte + * after the entire record (skipping over the record). + * @param[out] aRecord A reference to a record to read a matching record into. + * If a matching record is found, `sizeof(RecordType)` bytes from @p aMessage are + * read from @p aMessage and copied into @p aRecord. + * + * @retval OT_ERROR_NONE A matching record was read successfully. @p aOffset, and @p aRecord are updated. + * @retval OT_ERROR_NOT_FOUND A matching record could not be found. @p aOffset is updated. + * @retval OT_ERROR_PARSE Could not parse records from @p aMessage (e.g., ran out of bytes in @p aMessage). + * + */ + template + static otError ReadRecord(const Message &aMessage, uint16_t &aOffset, RecordType &aRecord) + { + return ReadRecord(aMessage, aOffset, RecordType::kType, aRecord, sizeof(RecordType)); + } + +protected: + otError ReadName(const Message &aMessage, + uint16_t & aOffset, + uint16_t aStartOffset, + char * aNameBuffer, + uint16_t aNameBufferSize, + bool aSkipRecord) const; + otError SkipRecord(const Message &aMessage, uint16_t &aOffset) const; + private: + enum : uint8_t + { + kType = kTypeAny, // This is intended for used by `ReadRecord()` only. + }; + + static otError ReadRecord(const Message & aMessage, + uint16_t & aOffset, + uint16_t aType, + ResourceRecord &aRecord, + uint16_t aMinRecordSize); + + otError CheckRecord(const Message &aMessage, uint16_t aOffset) const; + otError ReadFrom(const Message &aMessage, uint16_t aOffset); + uint16_t mType; // The type of the data in RDATA section. uint16_t mClass; // The class of the data in RDATA section. uint32_t mTtl; // Specifies the maximum time that the resource record may be cached. @@ -950,6 +1052,11 @@ OT_TOOL_PACKED_BEGIN class PtrRecord : public ResourceRecord { public: + enum : uint16_t + { + kType = kTypePtr, ///< The PTR record type. + }; + /** * This method initializes the PTR Resource Record by setting its type and class. * @@ -960,6 +1067,70 @@ public: */ void Init(uint16_t aClass = kClassInternet) { ResourceRecord::Init(kTypePtr, aClass); } + /** + * This method parses and reads the PTR name from a message. + * + * This method also verifies that the PTR record is well-formed (e.g., the record data length `GetLength()` matches + * the PTR encoded name). + * + * @param[in] aMessage The message to read from. `aMessage.GetOffset()` MUST point to the start of + * DNS header. + * @param[inout] aOffset On input, the offset in @p aMessage to start of PTR name field. + * On exit when successfully read, @p aOffset is updated to point to the byte + * after the entire PTR record (skipping over the record). + * @param[out] aNameBuffer A pointer to a char array to output the read name as a null-terminated C string + * (MUST NOT be nullptr). + * @param[in] aNameBufferSize The size of @p aNameBuffer. + * + * @retval OT_ERROR_NONE The PTR name was read successfully. @p aOffset and @p aNameBuffer are updated. + * @retval OT_ERROR_PARSE The PTR record in @p aMessage could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS Name could not fit in @p aNameBufferSize chars. + * + */ + otError ReadPtrName(const Message &aMessage, uint16_t &aOffset, char *aNameBuffer, uint16_t aNameBufferSize) const + { + return ResourceRecord::ReadName(aMessage, aOffset, /* aStartOffset */ aOffset - sizeof(PtrRecord), aNameBuffer, + aNameBufferSize, + /* aSkipRecord */ true); + } + + /** + * This method parses and reads the PTR name from a message. + * + * This method also verifies that the PTR record is well-formed (e.g., the record data length `GetLength()` matches + * the PTR encoded name). + * + * Unlike the previous method which reads the entire PTR name into a single char buffer, this method reads the + * first label separately and into a different buffer @p aLabelBuffer and the rest of the name into @p aNameBuffer. + * The @p aNameBuffer can be set to `nullptr` if the caller is only interested in the first label. This method is + * intended for "Service Instance Name" where first label (`` portion) can be a user-friendly string and + * can contain dot character. + * + * @param[in] aMessage The message to read from. `aMessage.GetOffset()` MUST point to the start of + * DNS header. + * @param[inout] aOffset On input, the offset in @p aMessage to the start of PTR name field. + * On exit, when successfully read, @p aOffset is updated to point to the byte + * after the entire PTR record (skipping over the record). + * @param[out] aLabelBuffer A pointer to a char array to output the first label as a null-terminated C + * string (MUST NOT be nullptr). + * @param[in] aLabelBufferSize The size of @p aLabelBuffer. + * @param[out] aNameBuffer A pointer to a char array to output the rest of name (after first label). Can + * be `nullptr` if caller is only interested in the first label. + * @param[in] aNameBufferSize The size of @p aNameBuffer. + * + * @retval OT_ERROR_NONE The PTR name was read successfully. @p aOffset, @aLabelBuffer and @aNameBuffer + * are updated. + * @retval OT_ERROR_PARSE The PTR record in @p aMessage could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS Either label or name could not fit in the related char buffers. + * + */ + otError ReadPtrName(const Message &aMessage, + uint16_t & aOffset, + char * aLabelBuffer, + uint8_t aLabelBufferSize, + char * aNameBuffer, + uint16_t aNameBufferSize) const; + } OT_TOOL_PACKED_END; /** @@ -970,6 +1141,11 @@ OT_TOOL_PACKED_BEGIN class TxtRecord : public ResourceRecord { public: + enum : uint16_t + { + kType = kTypeTxt, ///< The TXT record type. + }; + /** * This method initializes the TXT Resource Record by setting its type and class. * @@ -980,6 +1156,28 @@ public: */ void Init(uint16_t aClass = kClassInternet) { ResourceRecord::Init(kTypeTxt, aClass); } + /** + * This method parses and reads the TXT record data from a message. + * + * @param[in] aMessage The message to read from. + * @param[inout] aOffset On input, the offset in @p aMessage to start of TXT record data. + * On exit when successfully read, @p aOffset is updated to point to the byte + * after the entire TXT record (skipping over the record). + * @param[out] aTxtBuffer A pointer to a byte array to output the read TXT data. + * @param[inout] aTxtBufferSize On input, the size of @p aTxtBuffer (max bytes that can be read). + * On exit, @p aTxtBufferSize gives number of bytes written to @p aTxtBuffer. + * + * @retval OT_ERROR_NONE The TXT data was read successfully. @p aOffset, @p aTxtBuffer and + * @p aTxtBufferSize are updated. + * @retval OT_ERROR_PARSE The TXT record in @p aMessage could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS TXT data could not fit in @p aTxtBufferSize bytes. + * + */ + otError ReadTxtData(const Message &aMessage, + uint16_t & aOffset, + uint8_t * aTxtBuffer, + uint16_t & aTxtBufferSize) const; + } OT_TOOL_PACKED_END; /** @@ -990,6 +1188,11 @@ OT_TOOL_PACKED_BEGIN class AaaaRecord : public ResourceRecord { public: + enum : uint16_t + { + kType = kTypeAaaa, ///< The AAAA record type. + }; + /** * This method initializes the AAAA Resource Record by setting its type, class, and length. * @@ -1038,6 +1241,11 @@ OT_TOOL_PACKED_BEGIN class SrvRecord : public ResourceRecord { public: + enum : uint16_t + { + kType = kTypeSrv, ///< The SRV record type. + }; + /** * This method initializes the SRV Resource Record by settings its type and class. * @@ -1096,6 +1304,36 @@ public: */ void SetPort(uint16_t aPort) { mPort = HostSwap16(aPort); } + /** + * This method parses and reads the SRV target host name from a message. + * + * This method also verifies that the SRV record is well-formed (e.g., the record data length `GetLength()` matches + * the SRV encoded name). + * + * @param[in] aMessage The message to read from. `aMessage.GetOffset()` MUST point to the start of + * DNS header. + * @param[inout] aOffset On input, the offset in @p aMessage to start of target host name field. + * On exit when successfully read, @p aOffset is updated to point to the byte + * after the entire SRV record (skipping over the record). + * @param[out] aNameBuffer A pointer to a char array to output the read name as a null-terminated C string + * (MUST NOT be nullptr). + * @param[in] aNameBufferSize The size of @p aNameBuffer. + * + * @retval OT_ERROR_NONE The host name was read successfully. @p aOffset and @p aNameBuffer are updated. + * @retval OT_ERROR_PARSE The SRV record in @p aMessage could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS Name could not fit in @p aNameBufferSize chars. + * + */ + otError ReadTargetHostName(const Message &aMessage, + uint16_t & aOffset, + char * aNameBuffer, + uint16_t aNameBufferSize) const + { + return ResourceRecord::ReadName(aMessage, aOffset, /* aStartOffset */ aOffset - sizeof(SrvRecord), aNameBuffer, + aNameBufferSize, + /* aSkipRecord */ true); + } + private: uint16_t mPriority; uint16_t mWeight; @@ -1112,6 +1350,11 @@ OT_TOOL_PACKED_BEGIN class KeyRecord : public ResourceRecord { public: + enum : uint16_t + { + kType = kTypeKey, ///< The KEY record type. + }; + /** * This enumeration defines protocol field values (RFC 2535 - section 3.1.3). * @@ -1347,6 +1590,11 @@ OT_TOOL_PACKED_BEGIN class SigRecord : public ResourceRecord, public Clearable { public: + enum : uint16_t + { + kType = kTypeSig, ///< The SIG record type. + }; + /** * This method initializes the SIG Resource Record by setting its type and class. * @@ -1489,6 +1737,33 @@ public: */ const uint8_t *GetRecordData(void) const { return reinterpret_cast(&mTypeCovered); } + /** + * This method parses and reads the SIG signer name from a message. + * + * @param[in] aMessage The message to read from. `aMessage.GetOffset()` MUST point to the start of DNS + * header. + * @param[inout] aOffset On input, the offset in @p aMessage to start of signer name field. + * On exit when successfully read, @p aOffset is updated to point to the byte + * after the name field (i.e., start of signature field). + * @param[out] aNameBuffer A pointer to a char array to output the read name as a null-terminated C string + * (MUST NOT be nullptr). + * @param[in] aNameBufferSize The size of @p aNameBuffer. + * + * @retval OT_ERROR_NONE The name was read successfully. @p aOffset and @p aNameBuffer are updated. + * @retval OT_ERROR_PARSE The SIG record in @p aMessage could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS Name could not fit in @p aNameBufferSize chars. + * + */ + otError ReadSignerName(const Message &aMessage, + uint16_t & aOffset, + char * aNameBuffer, + uint16_t aNameBufferSize) const + { + return ResourceRecord::ReadName(aMessage, aOffset, /* aStartOffset */ aOffset - sizeof(SigRecord), aNameBuffer, + aNameBufferSize, + /* aSkipRecord */ false); + } + private: uint16_t mTypeCovered; // type of the other RRs covered by this SIG. set to zero for SIG(0). uint8_t mAlgorithm; // Algorithm number (see `KeyRecord` enumeration). @@ -1508,6 +1783,11 @@ OT_TOOL_PACKED_BEGIN class OptRecord : public ResourceRecord { public: + enum : uint16_t + { + kType = kTypeOpt, ///< The OPT record type. + }; + /** * This method initializes the OPT Resource Record by setting its type and clearing extended Response Code, version * and all flags. diff --git a/tests/unit/test_dns.cpp b/tests/unit/test_dns.cpp index 88c4f85fb..185a42b48 100644 --- a/tests/unit/test_dns.cpp +++ b/tests/unit/test_dns.cpp @@ -653,12 +653,339 @@ void TestDnsCompressedName(void) testFreeInstance(instance); } +void TestHeaderAndResourceRecords(void) +{ + enum + { + kHeaderOffset = 0, + kQuestionCount = 1, + kAnswerCount = 2, + kAdditionalCount = 5, + kTtl = 7200, + kTxtTtl = 7300, + kSrvPort = 1234, + kSrvPriority = 1, + kSrvWeight = 2, + kMaxSize = 600, + }; + + const char kMessageString[] = "DnsMessage"; + const char kDomainName[] = "example.com."; + const char kServiceLabels[] = "_service._udp"; + const char kServiceName[] = "_service._udp.example.com."; + const char kInstance1Label[] = "inst1"; + const char kInstance2Label[] = "instance2"; + const char kInstance1Name[] = "inst1._service._udp.example.com."; + const char kInstance2Name[] = "instance2._service._udp.example.com."; + const char kHostName[] = "host.example.com."; + const uint8_t kTxtData[] = {9, 'k', 'e', 'y', '=', 'v', 'a', 'l', 'u', 'e', 0}; + const char kHostAddress[] = "fd00::abcd:"; + + 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; + Dns::PtrRecord ptrRecord; + Dns::SrvRecord srvRecord; + Dns::TxtRecord txtRecord; + Dns::AaaaRecord aaaaRecord; + Dns::ResourceRecord record; + Ip6::Address hostAddress; + + char label[Dns::Name::kMaxLabelLength + 1]; + char name[Dns::Name::kMaxLength]; + uint8_t buffer[kMaxSize]; + + printf("================================================================\n"); + printf("TestHeaderAndResourceRecords()\n"); + + instance = static_cast(testInitInstance()); + VerifyOrQuit(instance != nullptr, "Null OpenThread instance"); + + messagePool = &instance->Get(); + VerifyOrQuit((message = messagePool->New(Message::kTypeIp6, 0)) != nullptr, "Message::New failed"); + + printf("----------------------------------------------------------------\n"); + printf("Preparing the message\n"); + + SuccessOrQuit(message->Append(kMessageString), "Message::Append() failed"); + + // Header + + headerOffset = message->GetLength(); + SuccessOrQuit(header.SetRandomMessageId(), "Header::SetRandomMessageId() failed"); + messageId = header.GetMessageId(); + header.SetType(Dns::Header::kTypeResponse); + header.SetQuestionCount(kQuestionCount); + header.SetAnswerCount(kAnswerCount); + header.SetAdditionalRecordCount(kAdditionalCount); + SuccessOrQuit(message->Append(header), "Message::Append() failed"); + message->SetOffset(headerOffset); + + // Question section + + serviceNameOffset = message->GetLength() - headerOffset; + SuccessOrQuit(Dns::Name::AppendMultipleLabels(kServiceLabels, *message), "AppendMultipleLabels() failed"); + SuccessOrQuit(Dns::Name::AppendName(kDomainName, *message), "AppendName() failed"); + SuccessOrQuit(message->Append(Dns::Question(Dns::ResourceRecord::kTypePtr)), "Message::Append() failed"); + + // Answer section + + answerSectionOffset = message->GetLength(); + + for (const char *instanceLabel : kInstanceLabels) + { + SuccessOrQuit(Dns::Name::AppendPointerLabel(serviceNameOffset, *message), "AppendPointerLabel() failed"); + ptrRecord.Init(); + ptrRecord.SetTtl(kTtl); + offset = message->GetLength(); + SuccessOrQuit(message->Append(ptrRecord), "Message::Append() failed"); + SuccessOrQuit(Dns::Name::AppendLabel(instanceLabel, *message), "AppendLabel failed"); + SuccessOrQuit(Dns::Name::AppendPointerLabel(serviceNameOffset, *message), "AppendPointerLabel() failed"); + ptrRecord.SetLength(message->GetLength() - offset - sizeof(Dns::ResourceRecord)); + message->Write(offset, ptrRecord); + } + + // Additional section + + additionalSectionOffset = message->GetLength(); + + for (const char *instanceName : kInstanceNames) + { + uint16_t instanceNameOffset = message->GetLength() - headerOffset; + + // SRV record + SuccessOrQuit(Dns::Name::AppendName(instanceName, *message), "AppendName() failed"); + srvRecord.Init(); + srvRecord.SetTtl(kTtl); + srvRecord.SetPort(kSrvPort); + srvRecord.SetWeight(kSrvWeight); + srvRecord.SetPriority(kSrvPriority); + offset = message->GetLength(); + SuccessOrQuit(message->Append(srvRecord), "Message::Append() failed"); + hostNameOffset = message->GetLength() - headerOffset; + SuccessOrQuit(Dns::Name::AppendName(kHostName, *message), "AppendName() failed"); + srvRecord.SetLength(message->GetLength() - offset - sizeof(Dns::ResourceRecord)); + message->Write(offset, srvRecord); + + // TXT record + SuccessOrQuit(Dns::Name::AppendPointerLabel(instanceNameOffset, *message), "AppendPointerLabel() failed"); + txtRecord.Init(); + txtRecord.SetTtl(kTxtTtl); + txtRecord.SetLength(sizeof(kTxtData)); + SuccessOrQuit(message->Append(txtRecord), "Message::Append() failed"); + SuccessOrQuit(message->Append(kTxtData), "Message::Append() failed"); + } + + SuccessOrQuit(hostAddress.FromString(kHostAddress), "Address::FromString() failed"); + SuccessOrQuit(Dns::Name::AppendPointerLabel(hostNameOffset, *message), "AppendPointerLabel() failed"); + aaaaRecord.Init(); + aaaaRecord.SetTtl(kTtl); + aaaaRecord.SetAddress(hostAddress); + SuccessOrQuit(message->Append(aaaaRecord), "Message::Append()"); + + // Dump the entire message + + VerifyOrQuit(message->GetLength() < kMaxSize, "Message is too long"); + SuccessOrQuit(message->Read(0, buffer, message->GetLength()), "Message::Read() failed"); + DumpBuffer("message", buffer, message->GetLength()); + + printf("----------------------------------------------------------------\n"); + printf("Parse and verify the message\n"); + + offset = 0; + VerifyOrQuit(message->Compare(offset, kMessageString), "Message header does not match"); + offset += sizeof(kMessageString); + + // Header + + VerifyOrQuit(offset == headerOffset, "headerOffset is incorrect"); + SuccessOrQuit(message->Read(offset, header), "Message::Read() failed"); + offset += sizeof(header); + + VerifyOrQuit(header.GetMessageId() == messageId, "Header::GetMessageId() failed"); + VerifyOrQuit(header.GetType() == Dns::Header::kTypeResponse, "Header::GetType() failed"); + VerifyOrQuit(header.GetQuestionCount() == kQuestionCount, "Header::GetQuestionCount() failed"); + VerifyOrQuit(header.GetAnswerCount() == kAnswerCount, "Header::GetAnswerCount() failed"); + VerifyOrQuit(header.GetAdditionalRecordCount() == kAdditionalCount, "Header::GetAdditionalRecordCount() failed"); + + printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n"); + printf("Question Section\n"); + + SuccessOrQuit(Dns::Name::CompareName(*message, offset, kServiceName), "Question name does not match"); + VerifyOrQuit(message->Compare(offset, Dns::Question(Dns::ResourceRecord::kTypePtr)), "Question does not match"); + offset += sizeof(Dns::Question); + + printf("PTR for \"%s\"\n", kServiceName); + + printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n"); + printf("Answer Section\n"); + + VerifyOrQuit(offset == answerSectionOffset, "answer section offset is incorrect"); + + for (const char *instanceName : kInstanceNames) + { + SuccessOrQuit(Dns::Name::CompareName(*message, offset, kServiceName), "ServiceName is incorrect"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, ptrRecord), "ReadRecord() failed"); + VerifyOrQuit(ptrRecord.GetTtl() == kTtl, "Read PTR is incorrect"); + + SuccessOrQuit(ptrRecord.ReadPtrName(*message, offset, name, sizeof(name)), "ReadName() failed"); + VerifyOrQuit(strcmp(name, instanceName) == 0, "Inst1 name is incorrect"); + + printf(" \"%s\" PTR %u %d \"%s\"\n", kServiceName, ptrRecord.GetTtl(), ptrRecord.GetLength(), name); + } + + VerifyOrQuit(offset == additionalSectionOffset, "offset is incorrect after answer section parse"); + + offset = answerSectionOffset; + SuccessOrQuit(Dns::ResourceRecord::ParseRecords(*message, offset, kAnswerCount), "ParseRecords() failed"); + VerifyOrQuit(offset == additionalSectionOffset, "offset is incorrect after answer section parse"); + + printf("Use FindRecord() to find and iterate through all the records:\n"); + + offset = answerSectionOffset; + numRecords = kAnswerCount; + + while (numRecords > 0) + { + uint16_t prevNumRecords = numRecords; + + SuccessOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, kServiceName), "FindRecord failed"); + VerifyOrQuit(numRecords == prevNumRecords - 1, "Incorrect num records"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, ptrRecord), "ReadRecord() failed"); + VerifyOrQuit(ptrRecord.GetTtl() == kTtl, "Read PTR is incorrect"); + SuccessOrQuit(ptrRecord.ReadPtrName(*message, offset, label, sizeof(label), name, sizeof(name)), + "ReadName() failed"); + printf(" \"%s\" PTR %u %d inst:\"%s\" at \"%s\"\n", kServiceName, ptrRecord.GetTtl(), ptrRecord.GetLength(), + label, name); + } + + VerifyOrQuit(offset == additionalSectionOffset, "offset is incorrect after answer section parse"); + VerifyOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, kServiceName) == OT_ERROR_NOT_FOUND, + "FindRecord did not fail with no records"); + + // Use `ReadRecord()` with a non-matching record type. Verify that it correct skips over the record. + + offset = answerSectionOffset; + numRecords = kAnswerCount; + + while (numRecords > 0) + { + SuccessOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, kServiceName), "FindRecord failed"); + VerifyOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, srvRecord) == OT_ERROR_NOT_FOUND, + "ReadRecord() did not fail with non-matching type"); + } + + VerifyOrQuit(offset == additionalSectionOffset, "offset is incorrect after answer section parse"); + + // Use `FindRecord` with a non-matching name. Verify that it correctly skips over all records. + + offset = answerSectionOffset; + numRecords = kAnswerCount; + VerifyOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, kInstance1Name) == OT_ERROR_NOT_FOUND, + "FindRecord did not fail with non-matching name"); + VerifyOrQuit(numRecords == 0, "Incorrect num records"); + VerifyOrQuit(offset == additionalSectionOffset, "offset is incorrect after answer section parse"); + + printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n"); + printf("Additional Section\n"); + + for (const char *instanceName : kInstanceNames) + { + // SRV record + SuccessOrQuit(Dns::Name::CompareName(*message, offset, instanceName), "Instance is incorrect"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, srvRecord), "ReadRecord() failed"); + VerifyOrQuit(srvRecord.GetTtl() == kTtl, "Read SRV is incorrect"); + VerifyOrQuit(srvRecord.GetPort() == kSrvPort, "Read SRV port is incorrect"); + VerifyOrQuit(srvRecord.GetWeight() == kSrvWeight, "Read SRV weight is incorrect"); + VerifyOrQuit(srvRecord.GetPriority() == kSrvPriority, "Read SRV priority is incorrect"); + SuccessOrQuit(srvRecord.ReadTargetHostName(*message, offset, name, sizeof(name)), + "ReadTargetHostName() failed"); + VerifyOrQuit(strcmp(name, kHostName) == 0, "Inst1 name is incorrect"); + printf(" \"%s\" SRV %u %d %d %d %d \"%s\"\n", instanceName, srvRecord.GetTtl(), srvRecord.GetLength(), + srvRecord.GetPort(), srvRecord.GetWeight(), srvRecord.GetPriority(), name); + + // TXT record + SuccessOrQuit(Dns::Name::CompareName(*message, offset, instanceName), "Instance is incorrect"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, txtRecord), "ReadRecord() failed"); + VerifyOrQuit(txtRecord.GetTtl() == kTxtTtl, "Read TXT is incorrect"); + len = sizeof(buffer); + SuccessOrQuit(txtRecord.ReadTxtData(*message, offset, buffer, len), "ReadTxtData() failed"); + VerifyOrQuit(len == sizeof(kTxtData), "TXT data length is not valid"); + VerifyOrQuit(memcmp(buffer, kTxtData, len) == 0, "TXT data is not valid"); + printf(" \"%s\" TXT %u %d \"%s\"\n", instanceName, txtRecord.GetTtl(), txtRecord.GetLength(), + reinterpret_cast(buffer)); + } + + SuccessOrQuit(Dns::Name::CompareName(*message, offset, kHostName), "HostName is incorrect"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, aaaaRecord), "ReadRecord() failed"); + VerifyOrQuit(aaaaRecord.GetTtl() == kTtl, "Read AAAA is incorrect"); + VerifyOrQuit(aaaaRecord.GetAddress() == hostAddress, "Read host address is incorrect"); + printf(" \"%s\" AAAA %u %d \"%s\"\n", kHostName, aaaaRecord.GetTtl(), aaaaRecord.GetLength(), + aaaaRecord.GetAddress().ToString().AsCString()); + + VerifyOrQuit(offset == message->GetLength(), "offset is incorrect after additional section parse"); + + // Use `ParseRecords()` to parse all records + offset = additionalSectionOffset; + SuccessOrQuit(Dns::ResourceRecord::ParseRecords(*message, offset, kAdditionalCount), "ParseRecords() failed"); + VerifyOrQuit(offset == message->GetLength(), "offset is incorrect after additional section parse"); + + printf("Use FindRecord() to search for specific name:\n"); + + for (const char *instanceName : kInstanceNames) + { + offset = additionalSectionOffset; + numRecords = kAdditionalCount; + + SuccessOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, instanceName), "FindRecord failed"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, srvRecord), "ReadRecord() failed"); + SuccessOrQuit(Dns::Name::ParseName(*message, offset), "ParseName() failed"); + printf(" \"%s\" SRV %u %d %d %d %d\n", instanceName, srvRecord.GetTtl(), srvRecord.GetLength(), + srvRecord.GetPort(), srvRecord.GetWeight(), srvRecord.GetPriority()); + + SuccessOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, instanceName), "FindRecord failed"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, txtRecord), "ReadRecord() failed"); + offset += txtRecord.GetLength(); + printf(" \"%s\" TXT %u %d\n", instanceName, txtRecord.GetTtl(), txtRecord.GetLength()); + + VerifyOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, instanceName) == OT_ERROR_NOT_FOUND, + "FindRecord() did not fail with no more records"); + + VerifyOrQuit(offset == message->GetLength(), "offset is incorrect after additional section parse"); + } + + offset = additionalSectionOffset; + numRecords = kAdditionalCount; + SuccessOrQuit(Dns::ResourceRecord::FindRecord(*message, offset, numRecords, kHostName), "FindRecord() failed"); + SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, record), "ReadRecord() failed"); + VerifyOrQuit(record.GetType() == Dns::ResourceRecord::kTypeAaaa, "Read record has incorrect type"); + offset += record.GetLength(); + VerifyOrQuit(offset == message->GetLength(), "offset is incorrect after additional section parse"); + + message->Free(); + testFreeInstance(instance); +} + } // namespace ot int main(void) { ot::TestDnsName(); ot::TestDnsCompressedName(); + ot::TestHeaderAndResourceRecords(); printf("All tests passed\n"); return 0;