From 73126f70994c2977778e714a4ef313bb6b2fc48c Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 8 Dec 2020 12:20:19 -0800 Subject: [PATCH] [dns] add helper methods to append/parse DNS names (#5922) This commit adds `Dns::Name` type which provides a set of helper methods to encode/decode DNS names. `AppendName()` method encodes and appends a full name (e.g., "test.example.com") to a message. Other helper methods enable appending labels (or groups of labels) and/or constructing a compressed name (using pointer labels). `ParseName()` method parses and skips over a full name in a message. `ReadLabel()` method reads labels one by one and works independently of whether the encoded name is compressed or not. `ReadName()` method read an entire name from message. The new methods are used in `Dns::Client`. This commit also add a unit test `test-dns` covering the behavior of the newly added helper methods. Finally, this commit adds few helper methods in `ResourceRecords` to get the size and check the type of a DNS record. --- Android.mk | 1 + src/core/BUILD.gn | 1 + src/core/CMakeLists.txt | 1 + src/core/Makefile.am | 1 + src/core/common/string.cpp | 16 + src/core/common/string.hpp | 11 + src/core/net/dns_client.cpp | 88 +--- src/core/net/dns_client.hpp | 15 - src/core/net/dns_headers.cpp | 335 +++++++++++++ src/core/net/dns_headers.hpp | 323 +++++++++++-- tests/toranj/openthread-core-toranj-config.h | 8 + tests/unit/CMakeLists.txt | 22 + tests/unit/Makefile.am | 4 + tests/unit/test_dns.cpp | 476 +++++++++++++++++++ tests/unit/test_string.cpp | 22 + 15 files changed, 1196 insertions(+), 128 deletions(-) create mode 100644 src/core/net/dns_headers.cpp create mode 100644 tests/unit/test_dns.cpp diff --git a/Android.mk b/Android.mk index 1945f42b0..6404c0363 100644 --- a/Android.mk +++ b/Android.mk @@ -237,6 +237,7 @@ LOCAL_SRC_FILES := \ src/core/net/dhcp6_client.cpp \ src/core/net/dhcp6_server.cpp \ src/core/net/dns_client.cpp \ + src/core/net/dns_headers.cpp \ src/core/net/icmp6.cpp \ src/core/net/ip6.cpp \ src/core/net/ip6_address.cpp \ diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 85174b390..1e2b85b34 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -466,6 +466,7 @@ openthread_core_files = [ "net/dhcp6_server.hpp", "net/dns_client.cpp", "net/dns_client.hpp", + "net/dns_headers.cpp", "net/dns_headers.hpp", "net/icmp6.cpp", "net/icmp6.hpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 369f7871c..249413008 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -129,6 +129,7 @@ set(COMMON_SOURCES net/dhcp6_client.cpp net/dhcp6_server.cpp net/dns_client.cpp + net/dns_headers.cpp net/icmp6.cpp net/ip6.cpp net/ip6_address.cpp diff --git a/src/core/Makefile.am b/src/core/Makefile.am index 4b7420ea9..0fcaf2e66 100644 --- a/src/core/Makefile.am +++ b/src/core/Makefile.am @@ -206,6 +206,7 @@ SOURCES_COMMON = \ net/dhcp6_client.cpp \ net/dhcp6_server.cpp \ net/dns_client.cpp \ + net/dns_headers.cpp \ net/icmp6.cpp \ net/ip6.cpp \ net/ip6_address.cpp \ diff --git a/src/core/common/string.cpp b/src/core/common/string.cpp index 3ec72d411..cba642bd0 100644 --- a/src/core/common/string.cpp +++ b/src/core/common/string.cpp @@ -49,6 +49,22 @@ uint16_t StringLength(const char *aString, uint16_t aMaxLength) return ret; } +const char *StringFind(const char *aString, char aChar) +{ + const char *ret = nullptr; + + for (; *aString != '\0'; aString++) + { + if (*aString == aChar) + { + ret = aString; + break; + } + } + + return ret; +} + otError StringBase::Write(char *aBuffer, uint16_t aSize, uint16_t &aLength, const char *aFormat, va_list aArgs) { otError error = OT_ERROR_NONE; diff --git a/src/core/common/string.hpp b/src/core/common/string.hpp index 79acbd47c..934b501c1 100644 --- a/src/core/common/string.hpp +++ b/src/core/common/string.hpp @@ -68,6 +68,17 @@ namespace ot { */ uint16_t StringLength(const char *aString, uint16_t aMaxLength); +/** + * This function finds the first occurrence of a given character in a null-terminated string. + * + * @param[in] aString A pointer to the string. + * @param[in] aChar A char to search for in the string. + * + * @returns The pointer to first occurrence of the @p aChar in @p aString, or nullptr if cannot be found. + * + */ +const char *StringFind(const char *aString, char aChar); + /** * This class defines the base class for `String`. * diff --git a/src/core/net/dns_client.cpp b/src/core/net/dns_client.cpp index 0ed41861a..e922b3236 100644 --- a/src/core/net/dns_client.cpp +++ b/src/core/net/dns_client.cpp @@ -105,7 +105,7 @@ otError Client::Query(const QueryInfo &aQuery, ResponseHandler aHandler, void *a VerifyOrExit((message = NewMessage(header)) != nullptr, error = OT_ERROR_NO_BUFS); - SuccessOrExit(error = AppendCompressedHostname(*message, aQuery.GetHostname())); + SuccessOrExit(error = Name::AppendName(aQuery.GetHostname(), *message)); SuccessOrExit(error = question.AppendTo(*message)); queryMetadata.mHostname = aQuery.GetHostname(); @@ -199,46 +199,6 @@ exit: } } -otError Client::AppendCompressedHostname(Message &aMessage, const char *aHostname) -{ - otError error = OT_ERROR_NONE; - uint8_t index = 0; - uint8_t labelPosition = 0; - uint8_t labelSize = 0; - - while (true) - { - // Look for string separator. - if (aHostname[index] == kLabelSeparator || aHostname[index] == kLabelTerminator) - { - VerifyOrExit(labelSize > 0, error = OT_ERROR_INVALID_ARGS); - SuccessOrExit(error = aMessage.Append(labelSize)); - SuccessOrExit(error = aMessage.AppendBytes(&aHostname[labelPosition], labelSize)); - - labelPosition += labelSize + 1; - labelSize = 0; - - if (aHostname[index] == kLabelTerminator) - { - break; - } - } - else - { - labelSize++; - } - - index++; - } - - // Add termination character at the end. - labelSize = kLabelTerminator; - SuccessOrExit(error = aMessage.Append(labelSize)); - -exit: - return error; -} - otError Client::CompareQuestions(Message &aMessageResponse, Message &aMessageQuery, uint16_t &aOffset) { otError error = OT_ERROR_NONE; @@ -268,46 +228,6 @@ exit: return error; } -otError Client::SkipHostname(Message &aMessage, uint16_t &aOffset) -{ - otError error = OT_ERROR_NONE; - uint8_t buf[kBufSize]; - uint16_t index; - uint16_t read = 0; - uint16_t offset = aOffset; - uint16_t length = aMessage.GetLength() - aOffset; - - while (length > 0) - { - VerifyOrExit((read = aMessage.ReadBytes(offset, buf, sizeof(buf))) > 0, error = OT_ERROR_PARSE); - - index = 0; - - while (index < read) - { - if (buf[index] == kLabelTerminator) - { - ExitNow(aOffset = offset + 1); - } - - if ((buf[index] & kCompressionOffsetMask) == kCompressionOffsetMask) - { - ExitNow(aOffset = offset + 2); - } - - index++; - offset++; - } - - length -= read; - } - - ExitNow(error = OT_ERROR_PARSE); - -exit: - return error; -} - Message *Client::FindRelatedQuery(const Header &aResponseHeader, QueryMetadata &aQueryMetadata) { uint16_t messageId; @@ -438,18 +358,18 @@ void Client::HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessag { uint32_t newOffset; - SuccessOrExit(error = SkipHostname(aMessage, offset)); + SuccessOrExit(error = Name::ParseName(aMessage, offset)); SuccessOrExit(error = aMessage.Read(offset, record)); - if (ResourceRecordAaaa::IsAaaa(record)) + if (record.Matches(ResourceRecord::kTypeAaaa)) { // Return the first found IPv6 address. FinalizeDnsTransaction(*message, queryMetadata, &record.GetAddress(), record.GetTtl(), OT_ERROR_NONE); ExitNow(error = OT_ERROR_NONE); } - newOffset = offset + sizeof(ResourceRecord) + record.GetLength(); + newOffset = offset + record.GetSize(); VerifyOrExit(newOffset <= aMessage.GetLength(), error = OT_ERROR_PARSE); offset = static_cast(newOffset); } diff --git a/src/core/net/dns_client.hpp b/src/core/net/dns_client.hpp index 9bb4450d7..4a491fe48 100644 --- a/src/core/net/dns_client.hpp +++ b/src/core/net/dns_client.hpp @@ -154,19 +154,6 @@ private: kMaxRetransmit = OPENTHREAD_CONFIG_DNS_MAX_RETRANSMIT, }; - /** - * Special DNS symbols. - */ - enum - { - kLabelTerminator = 0, - kLabelSeparator = '.', - kCompressionOffsetMask = 0xc0 - }; - - /** - * Operating on message buffers. - */ enum { kBufSize = 16 @@ -194,9 +181,7 @@ private: otError SendMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo); void SendCopy(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo); - otError AppendCompressedHostname(Message &aMessage, const char *aHostname); otError CompareQuestions(Message &aMessageResponse, Message &aMessageQuery, uint16_t &aOffset); - otError SkipHostname(Message &aMessage, uint16_t &aOffset); Message *FindRelatedQuery(const Header &aResponseHeader, QueryMetadata &aQueryMetadata); void FinalizeDnsTransaction(Message & aQuery, diff --git a/src/core/net/dns_headers.cpp b/src/core/net/dns_headers.cpp new file mode 100644 index 000000000..21ff0dbac --- /dev/null +++ b/src/core/net/dns_headers.cpp @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements generating and processing of DNS headers and helper functions/methods. + */ + +#include "dns_headers.hpp" + +#include "common/code_utils.hpp" +#include "common/debug.hpp" +#include "common/string.hpp" + +namespace ot { +namespace Dns { + +using ot::Encoding::BigEndian::HostSwap16; + +otError Name::AppendLabel(const char *aLabel, Message &aMessage) +{ + return AppendLabel(aLabel, static_cast(StringLength(aLabel, kMaxLabelLength + 1)), aMessage); +} + +otError Name::AppendLabel(const char *aLabel, uint8_t aLabelLength, Message &aMessage) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit((0 < aLabelLength) && (aLabelLength <= kMaxLabelLength), error = OT_ERROR_INVALID_ARGS); + + SuccessOrExit(error = aMessage.Append(aLabelLength)); + error = aMessage.AppendBytes(aLabel, aLabelLength); + +exit: + return error; +} + +otError Name::AppendMultipleLabels(const char *aLabels, Message &aMessage) +{ + otError error = OT_ERROR_NONE; + uint16_t index = 0; + uint16_t labelStartIndex = 0; + char ch; + + VerifyOrExit(aLabels != nullptr); + + do + { + VerifyOrExit(index < kMaxLength, error = OT_ERROR_INVALID_ARGS); + + ch = aLabels[index]; + + if ((ch == kNullChar) || (ch == kLabelSeperatorChar)) + { + uint8_t labelLength = static_cast(index - labelStartIndex); + + if (labelLength == 0) + { + // Empty label (e.g., consecutive dots) is invalid, but we + // allow for two cases: (1) where `aLabels` ends with a dot + // (`labelLength` is zero but we are at end of `aLabels` string + // and `ch` is null char. (2) if `aLabels` is just "." (we + // see a dot at index 0, and index 1 is null char). + + error = ((ch == kNullChar) || ((index == 0) && (aLabels[1] == kNullChar))) ? OT_ERROR_NONE + : OT_ERROR_INVALID_ARGS; + ExitNow(); + } + + SuccessOrExit(error = AppendLabel(&aLabels[labelStartIndex], labelLength, aMessage)); + + labelStartIndex = index + 1; + } + + index++; + + } while (ch != kNullChar); + +exit: + return error; +} + +otError Name::AppendTerminator(Message &aMessage) +{ + uint8_t terminator = 0; + + return aMessage.Append(terminator); +} + +otError Name::AppendPointerLabel(uint16_t aOffset, Message &aMessage) +{ + // A pointer label takes the form of a two byte sequence as a + // `uint16_t` value. The first two bits are ones. This allows a + // pointer to be distinguished from a text label, since the text + // label must begin with two zero bits (note that labels are + // restricted to 63 octets or less). The next 14-bits specify + // an offset value relative to start of DNS header. + + uint16_t value; + + OT_ASSERT(aOffset < kPointerLabelTypeUint16); + + value = HostSwap16(aOffset | kPointerLabelTypeUint16); + + return aMessage.Append(value); +} + +otError Name::AppendName(const char *aName, Message &aMessage) +{ + otError error; + + SuccessOrExit(error = AppendMultipleLabels(aName, aMessage)); + error = AppendTerminator(aMessage); + +exit: + return error; +} + +otError Name::ParseName(const Message &aMessage, uint16_t &aOffset) +{ + otError error; + LabelIterator iterator(aMessage, aOffset); + + while (true) + { + error = iterator.GetNextLabel(); + + VerifyOrExit((error == OT_ERROR_NONE) || (error == OT_ERROR_NOT_FOUND)); + + if (iterator.IsEndOffsetSet()) + { + aOffset = iterator.mNameEndOffset; + ExitNow(error = OT_ERROR_NONE); + } + } + +exit: + return error; +} + +otError Name::ReadLabel(const Message &aMessage, + uint16_t & aOffset, + uint16_t aHeaderOffset, + char * aLabelBuffer, + uint8_t & aLabelLength) +{ + otError error; + LabelIterator iterator(aMessage, aOffset, aHeaderOffset); + + SuccessOrExit(error = iterator.GetNextLabel()); + SuccessOrExit(error = iterator.ReadLabel(aLabelBuffer, aLabelLength, /* aAllowDotCharInLabel */ true)); + aOffset = iterator.mNextLabelOffset; + +exit: + return error; +} + +otError Name::ReadName(const Message &aMessage, + uint16_t & aOffset, + uint16_t aHeaderOffset, + char * aNameBuffer, + uint16_t aNameBufferSize) +{ + otError error; + LabelIterator iterator(aMessage, aOffset, aHeaderOffset); + bool firstLabel = true; + uint8_t labelLength; + + while (true) + { + error = iterator.GetNextLabel(); + + switch (error) + { + case OT_ERROR_NONE: + + if (!firstLabel) + { + *aNameBuffer++ = kLabelSeperatorChar; + aNameBufferSize--; + + // No need to check if we have reached end of the name buffer + // here since `iterator.ReadLabel()` would verify it. + } + + labelLength = static_cast(OT_MIN(kMaxLabelLength + 1, aNameBufferSize)); + SuccessOrExit(error = iterator.ReadLabel(aNameBuffer, labelLength, /* aAllowDotCharInLabel */ false)); + aNameBuffer += labelLength; + aNameBufferSize -= labelLength; + firstLabel = false; + break; + + case OT_ERROR_NOT_FOUND: + // We reach the end of name successfully. Always add a terminating dot + // at the end. + *aNameBuffer++ = kLabelSeperatorChar; + aNameBufferSize--; + VerifyOrExit(aNameBufferSize >= sizeof(uint8_t), error = OT_ERROR_NO_BUFS); + *aNameBuffer = kNullChar; + aOffset = iterator.mNameEndOffset; + error = OT_ERROR_NONE; + + // Fall through + + default: + ExitNow(); + } + } + +exit: + return error; +} + +otError Name::LabelIterator::GetNextLabel(void) +{ + otError error; + + while (true) + { + uint8_t labelLength; + uint8_t labelType; + + SuccessOrExit(error = mMessage.Read(mNextLabelOffset, labelLength)); + + labelType = labelLength & kLabelTypeMask; + + if (labelType == kTextLabelType) + { + if (labelLength == 0) + { + // Zero label length indicates end of a name. + + if (!IsEndOffsetSet()) + { + mNameEndOffset = mNextLabelOffset + sizeof(uint8_t); + } + + ExitNow(error = OT_ERROR_NOT_FOUND); + } + + mLabelStartOffset = mNextLabelOffset + sizeof(uint8_t); + mLabelLength = labelLength; + mNextLabelOffset = mLabelStartOffset + labelLength; + ExitNow(); + } + else if (labelType == kPointerLabelType) + { + // A pointer label takes the form of a two byte sequence as a + // `uint16_t` value. The first two bits are ones. The next 14 bits + // specify an offset value from the start of the DNS header. + + uint16_t pointerValue; + + SuccessOrExit(error = mMessage.Read(mNextLabelOffset, pointerValue)); + + if (!IsEndOffsetSet()) + { + mNameEndOffset = mNextLabelOffset + sizeof(uint16_t); + } + + mNextLabelOffset = mHeaderOffset + (HostSwap16(pointerValue) & kPointerLabelOffsetMask); + + // Go back through the `while(true)` loop to get the next label. + } + else + { + ExitNow(error = OT_ERROR_PARSE); + } + } + +exit: + return error; +} + +otError Name::LabelIterator::ReadLabel(char *aLabelBuffer, uint8_t &aLabelLength, bool aAllowDotCharInLabel) const +{ + otError error; + + VerifyOrExit(mLabelLength < aLabelLength, error = OT_ERROR_NO_BUFS); + + SuccessOrExit(error = mMessage.Read(mLabelStartOffset, aLabelBuffer, mLabelLength)); + aLabelBuffer[mLabelLength] = kNullChar; + aLabelLength = mLabelLength; + + if (!aAllowDotCharInLabel) + { + VerifyOrExit(StringFind(aLabelBuffer, kLabelSeperatorChar) == nullptr, error = OT_ERROR_PARSE); + } + +exit: + return error; +} + +void ResourceRecord::Init(uint16_t aType, uint16_t aClass, uint32_t aTtl) +{ + SetType(aType); + SetClass(aClass); + SetTtl(aTtl); + SetLength(0); +} + +void ResourceRecordAaaa::Init(void) +{ + ResourceRecord::Init(kTypeAaaa); + SetLength(sizeof(mAddress)); + mAddress.Clear(); +} + +} // namespace Dns +} // namespace ot diff --git a/src/core/net/dns_headers.hpp b/src/core/net/dns_headers.hpp index 80ab0409a..9d35d1f46 100644 --- a/src/core/net/dns_headers.hpp +++ b/src/core/net/dns_headers.hpp @@ -374,6 +374,242 @@ private: } OT_TOOL_PACKED_END; +/** + * This class implement helper methods for encoding/decoding of DNS Names. + * + */ +class Name +{ +public: + enum : uint8_t + { + kMaxLabelLength = 63, ///< Max number of characters in a label. + kMaxLength = 255, ///< Max number of characters in a name. + }; + + /** + * This static method encodes and appends a single name label to a message. + * + * The @p aLabel is assumed to contain a single name label as a C string (null-terminated). Unlike + * `AppendMultipleLabels()` which parses the label string and treats it as sequence of multiple (dot-separated) + * labels, this method always appends @p aLabel as a single whole label. This allows the label string to even + * contain dot '.' character, which, for example, is useful for "Service Instance Names" where portion + * is a user-friendly name and can contain dot characters. + * + * @param[in] aLabel The label string to append. MUST NOT be nullptr. + * @param[in] aMessage The message to append to. + * + * @retval OT_ERROR_NONE Successfully encoded and appended the name label to @p aMessage. + * @retval OT_ERROR_INVALID_ARGS @p aLabel is not valid (e.g., label length is not within valid range). + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + static otError AppendLabel(const char *aLabel, Message &aMessage); + + /** + * This static method encodes and appends a sequence of name labels to a given message. + * + * The @p aLabels must follow "..", i.e., a sequence of labels separated by dot '.' char. + * E.g., "_http._tcp", "_http._tcp." (same as previous one), "host-1.test". + * + * This method validates that the @p aLabels is a valid name format, i.e., no empty label, and labels are + * `kMaxLabelLength` (63) characters or less. + * + * @note This method NEVER adds a label terminator (empty label) to the message, even in the case where @p aLabels + * ends with a dot character, e.g., "host-1.test." is treated same as "host-1.test". + * + * @param[in] aLabels A name label string. Can be nullptr (then treated as ""). + * @param[in] aMessage The message to which to append the encoded name. + * + * @retval OT_ERROR_NONE Successfully encoded and appended the name label(s) to @p aMessage. + * @retval OT_ERROR_INVALID_ARGS Name label @p aLabels is not valid. + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + static otError AppendMultipleLabels(const char *aLabels, Message &aMessage); + + /** + * This static method appends a name label terminator to a message. + * + * An encoded name is terminated by an empty label (a zero byte). + * + * @param[in] aMessage The message to append to. + * + * @retval OT_ERROR_NONE Successfully encoded and appended the terminator label to @p aMessage. + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + static otError AppendTerminator(Message &aMessage); + + /** + * This static method appends a pointer type name label to a message. + * + * Pointer label is used for name compression. It allows an entire name or a list of labels at the end of an + * encoded name to be replaced with a pointer to a prior occurrence of the same name within the message. + * + * @param[in] aOffset The offset from the start of DNS header to use for pointer value. + * @param[in] aMessage The message to append to. + * + * @retval OT_ERROR_NONE Successfully encoded and appended the pointer label to @p aMessage. + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + static otError AppendPointerLabel(uint16_t aOffset, Message &aMessage); + + /** + * This static method encodes and appends a full name to a message. + * + * The @p aName must follow "..", i.e., a sequence of labels separated by dot '.' char. + * E.g., "example.com", "example.com." (same as previous one), local.", "default.service.arpa", "." or "" (root). + * + * This method validates that the @p aName is a valid name format, i.e. no empty labels, and labels are + * `kMaxLabelLength` (63) characters or less, and the name is `kMaxLength` (255) characters or less. + * + * @param[in] aName A name string. Can be nullptr (then treated as "." or root). + * @param[in] aMessage The message to append to. + * + * @retval OT_ERROR_NONE Successfully encoded and appended the name to @p aMessage. + * @retval OT_ERROR_INVALID_ARGS Name @p aName is not valid. + * @retval OT_ERROR_NO_BUFS Insufficient available buffers to grow the message. + * + */ + static otError AppendName(const char *aName, Message &aMessage); + + /** + * This static method parses and skips over a full name in a message. + * + * @param[in] aMessage The message to parse the name from. + * @param[inout] aOffset On input the offset in @p aMessage pointing to the start of the name field. + * On exit (when parsed successfully), @p aOffset is updated to point to the byte + * after the end of name field. + * + * @retval OT_ERROR_NONE Successfully parsed and skipped over name, @p Offset is updated. + * @retval OT_ERROR_PARSE Name could not be parsed (invalid format). + * + */ + static otError ParseName(const Message &aMessage, uint16_t &aOffset); + + /** + * This static method reads a name label from a message. + * + * This method can be used to read labels one by one in a name. After a successful label read, @p aOffset is + * updated to point to the start of the next label. When we reach the end of the name, OT_ERROR_NOT_FOUND is + * returned. This method handles compressed names which use pointer labels. So as the labels in a name are read, + * the @p aOffset may jump back in the message and at the end the @p aOffset does not necessarily point to the end + * of the original name field. + * + * Unlike `ReadName()` which requires and verifies that the read label to contain no dot '.' character, this method + * allows the read label to include any character. + * + * @param[in] aMessage The message to read the label from. + * @param[inout] aOffset On input, the offset in @p aMessage pointing to the start of the label to read. + * On exit, when successfully read, @p aOffset is updated to point to the start of + * the next label. + * @param[in] aHeaderOffset The offset in @p aMessage to the start of the DNS header. + * @param[out] aLabelBuffer A pointer to a char array to output the read label as a null-terminated C string. + * @param[inout] aLabelLength On input, the maximum number chars in @p aLabelBuffer array. + * On output, when label is successfully read, @aLabelLength is updated to return + * the label's length (number of chars in the label string, excluding the null char). + * + * @retval OT_ERROR_NONE Successfully read the label and updated @p aLabelBuffer, @p aLabelLength, and + * @p aOffset. + * @retval OT_ERROR_NOT_FOUND Reached the end of name and no more label to read. + * @retval OT_ERROR_PARSE Name could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS Label could not fit in @p aLabelLength chars. + * + */ + static otError ReadLabel(const Message &aMessage, + uint16_t & aOffset, + uint16_t aHeaderOffset, + char * aLabelBuffer, + uint8_t & aLabelLength); + + /** + * This static method reads a full name from a message. + * + * On successful read, the read name follows "...", i.e., a sequence of labels separated by + * dot '.' character. The read name will ALWAYS end with a dot. + * + * This method verifies that the read labels in message do not contain any dot character, otherwise it returns + * `OT_ERROR_PARSE`). + * + * @param[in] aMessage The message to read the name from. + * @param[inout] aOffset On input, the offset in @p aMessage pointing to the start of the name field. + * On exit (when parsed successfully), @p aOffset is updated to point to the byte + * after the end of name field. + * @param[in] aHeaderOffset The offset in @p aMessage to the start of the DNS header. + * @param[out] aNameBuffer A pointer to a char array to output the read name as a null-terminated C string. + * @param[inout] aNameBufferSize The maximum number chars in @p aNameBuffer array. + * + * @retval OT_ERROR_NONE Successfully read the name, @p aNameBuffer and @p Offset are updated. + * @retval OT_ERROR_PARSE Name could not be parsed (invalid format). + * @retval OT_ERROR_NO_BUFS Name could not fit in @p aNameBufferSize chars. + * + */ + static otError ReadName(const Message &aMessage, + uint16_t & aOffset, + uint16_t aHeaderOffset, + char * aNameBuffer, + uint16_t aNameBufferSize); + +private: + enum : char + { + kNullChar = '\0', + kLabelSeperatorChar = '.', + }; + + enum : uint8_t + { + // The first 2 bits of the encoded label specifies label type. + // + // - Value 00 indicates normal text label (lower 6-bits indicates the label length). + // - Value 11 indicates pointer label type (lower 14-bits indicates the pointer offset). + // - Values 01,10 are reserved (RFC 6891 recommends to not use) + + kLabelTypeMask = 0xc0, // 0b1100_0000 (first two bits) + kTextLabelType = 0x00, // Text label type (00) + kPointerLabelType = 0xc0, // Pointer label type - compressed name (11) + }; + + enum : uint16_t + { + kPointerLabelTypeUint16 = 0xc000, // Pointer label type as `uint16_t` mask (first 2 bits). + kPointerLabelOffsetMask = 0x3fff, // Mask to get the offset field in a pointer label (lower 14 bits). + }; + + struct LabelIterator + { + enum : uint16_t + { + kUnsetNameEndOffset = 0, // Special value indicating `mNameEndOffset` is not yet set. + }; + + LabelIterator(const Message &aMessage, uint16_t aLabelOffset, uint16_t aHeaderOffset = 0) + : mMessage(aMessage) + , mHeaderOffset(aHeaderOffset) + , mNextLabelOffset(aLabelOffset) + , mNameEndOffset(kUnsetNameEndOffset) + { + } + + bool IsEndOffsetSet(void) const { return (mNameEndOffset != kUnsetNameEndOffset); } + otError GetNextLabel(void); + otError ReadLabel(char *aLabelBuffer, uint8_t &aLabelLength, bool aAllowDotCharInLabel) const; + + const Message &mMessage; // Message to read labels from. + const uint16_t mHeaderOffset; // Offset in `mMessage` to the start of DNS header. + uint16_t mLabelStartOffset; // Offset in `mMessage` to the first char of current label text. + uint8_t mLabelLength; // Length of current label (number of chars). + uint16_t mNextLabelOffset; // Offset in `mMessage` to the start of the next label. + uint16_t mNameEndOffset; // Offset in `mMessage` to the byte after the end of domain name field. + }; + + Name(void) = default; + + static otError AppendLabel(const char *aLabel, uint8_t aLabelLength, Message &aMessage); +}; + /** * This class implements Resource Record body format (RR). * @@ -382,6 +618,54 @@ OT_TOOL_PACKED_BEGIN class ResourceRecord { public: + /** + * Resource Record Types. + * + */ + enum : uint16_t + { + kTypeA = 1, ///< Address record (IPv4). + kTypePtr = 12, ///< PTR record. + kTypeTxt = 16, ///< TXT record. + kTypeSrv = 33, ///< SRV locator record. + kTypeAaaa = 28, ///< IPv6 address record. + kTypeKey = 25, ///< Key record. + kTypeOpt = 41, ///< Option record. + }; + + /** + * Resource Record Class Codes. + * + */ + enum : uint16_t + { + kClassInternet = 1, ///< Class code Internet (IN). + }; + + /** + * This method initializes the resource record. + * + * @param[in] aType The type of the resource record. + * @param[in] aClass The class of the resource record (default is `kClassInternet`). + * @param[in] aTtl The time to live field of the resource record (default is zero). + * + */ + void Init(uint16_t aType, uint16_t aClass = kClassInternet, uint32_t aTtl = 0); + + /** + * This method indicates whether the resources records matches a given type and class code. + * + * @param[in] aType The resource record type to compare with. + * @param[in] aClass The resource record class code to compare with (default is `kClassInternet`). + * + * @returns TRUE if the resources records matches @p aType and @p aClass, FALSE otherwise. + * + */ + bool Matches(uint16_t aType, uint16_t aClass = kClassInternet) + { + return (mType == HostSwap16(aType)) && (mClass == HostSwap16(aClass)); + } + /** * This method returns the type of the resource record. * @@ -444,6 +728,15 @@ public: */ void SetLength(uint16_t aLength) { mLength = HostSwap16(aLength); } + /** + * This method returns the size of (number of bytes) in resource record and its data RDATA section (excluding the + * name field). + * + * @returns Size (number of bytes) of resource record and its data section (excluding the name field) + * + */ + uint32_t GetSize(void) const { return sizeof(ResourceRecord) + GetLength(); } + private: uint16_t mType; // The type of the data in RDATA section. uint16_t mClass; // The class of the data in RDATA section. @@ -460,31 +753,11 @@ OT_TOOL_PACKED_BEGIN class ResourceRecordAaaa : public ResourceRecord { public: - /** - * This static method indicated whether or not a given Resource Record is of AAAA type. - * - * @param[in] aRecord A Resource Record. - * - * @returns TRUE if @p aRecord is of AAAA type, FALSE otherwise. - * - */ - static bool IsAaaa(const ResourceRecord &aRecord) - { - return (aRecord.GetType() == kType) && (aRecord.GetClass() == kClass); - } - /** * This method initializes the AAAA Resource Record. * */ - void Init(void) - { - ResourceRecord::SetType(kType); - ResourceRecord::SetClass(kClass); - ResourceRecord::SetTtl(0); - ResourceRecord::SetLength(kLength); - mAddress.Clear(); - } + void Init(void); /** * This method sets the IPv6 address of the resource record. @@ -503,15 +776,7 @@ public: Ip6::Address &GetAddress(void) { return mAddress; } private: - enum - { - kType = 0x1C, // AAAA Resource Record type. - kClass = 0x01, // The value of the Internet class. - kLength = 16, // Size of the AAAA Resource Record type. - }; - Ip6::Address mAddress; // IPv6 Address of AAAA Resource Record. - } OT_TOOL_PACKED_END; /** diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index 55e481685..9843972d3 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -454,6 +454,14 @@ */ #define OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE 1 +/** + * @def OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE + * + * Define to 1 to enable DNS Client support. + * + */ +#define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE 1 + #if OPENTHREAD_RADIO /** * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index e411ddcff..64f6ec08d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -173,6 +173,27 @@ target_link_libraries(test-cmd-line-parser add_test(NAME test-cmd-line-parser COMMAND test-cmd-line-parser) +add_executable(test-dns + test_dns.cpp +) + +target_include_directories(test-dns + PRIVATE + ${COMMON_INCLUDES} +) + +target_compile_options(test-dns + PRIVATE + ${COMMON_COMPILE_OPTIONS} +) + +target_link_libraries(test-dns + PRIVATE + ${COMMON_LIBS} +) + +add_test(NAME test-dns COMMAND test-dns) + add_executable(test-flash test_flash.cpp ) @@ -644,6 +665,7 @@ set_target_properties( test-child test-child-table test-cmd-line-parser + test-dns test-flash test-heap test-hmac-sha256 diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index cc4e76ef9..c3718697d 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -111,6 +111,7 @@ check_PROGRAMS += \ test-child \ test-child-table \ test-cmd-line-parser \ + test-dns \ test-flash \ test-heap \ test-hmac-sha256 \ @@ -188,6 +189,9 @@ test_child_table_SOURCES = $(COMMON_SOURCES) test_child_table.cpp test_cmd_line_parser_LDADD = $(COMMON_LDADD) test_cmd_line_parser_SOURCES = $(COMMON_SOURCES) test_cmd_line_parser.cpp +test_dns_LDADD = $(COMMON_LDADD) +test_dns_SOURCES = $(COMMON_SOURCES) test_dns.cpp + test_flash_LDADD = $(COMMON_LDADD) test_flash_SOURCES = $(COMMON_SOURCES) test_flash.cpp diff --git a/tests/unit/test_dns.cpp b/tests/unit/test_dns.cpp new file mode 100644 index 000000000..2450149e5 --- /dev/null +++ b/tests/unit/test_dns.cpp @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include + +#include "test_platform.h" +#include "test_util.hpp" + +#include "common/instance.hpp" +#include "net/dns_headers.hpp" + +namespace ot { + +void TestDnsName(void) +{ + enum + { + kMaxSize = 300, + kLabelSize = 64, + kNameSize = 256, + }; + + struct TestName + { + const char * mName; + uint16_t mEncodedLength; + const uint8_t *mEncodedData; + const char ** mLabels; + const char * mExpectedReadName; + }; + + Instance * instance; + MessagePool *messagePool; + Message * message; + uint8_t buffer[kMaxSize]; + uint16_t len; + uint16_t offset; + char label[kLabelSize]; + uint8_t labelLength; + char name[kNameSize]; + + static const uint8_t kEncodedName1[] = {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}; + static const uint8_t kEncodedName2[] = {3, 'f', 'o', 'o', 1, 'a', 2, 'b', 'b', 3, 'e', 'd', 'u', 0}; + static const uint8_t kEncodedName3[] = {10, 'f', 'o', 'u', 'n', 'd', 'a', 't', 'i', 'o', 'n', 0}; + static const uint8_t kEncodedName4[] = {0}; + + static const char *kLabels1[] = {"example", "com", nullptr}; + static const char *kLabels2[] = {"foo", "a", "bb", "edu", nullptr}; + static const char *kLabels3[] = {"foundation", nullptr}; + static const char *kLabels4[] = {nullptr}; + + static const TestName kTestNames[] = { + {"example.com", sizeof(kEncodedName1), kEncodedName1, kLabels1, "example.com."}, + {"example.com.", sizeof(kEncodedName1), kEncodedName1, kLabels1, "example.com."}, + {"foo.a.bb.edu", sizeof(kEncodedName2), kEncodedName2, kLabels2, "foo.a.bb.edu."}, + {"foo.a.bb.edu.", sizeof(kEncodedName2), kEncodedName2, kLabels2, "foo.a.bb.edu."}, + {"foundation", sizeof(kEncodedName3), kEncodedName3, kLabels3, "foundation."}, + {"foundation.", sizeof(kEncodedName3), kEncodedName3, kLabels3, "foundation."}, + {"", sizeof(kEncodedName4), kEncodedName4, kLabels4, "."}, + {".", sizeof(kEncodedName4), kEncodedName4, kLabels4, "."}, + {nullptr, sizeof(kEncodedName4), kEncodedName4, kLabels4, "."}, + }; + + static const char *kInvalidNames[] = { + "foo..bar", + "..", + "a..", + "..b", + + // Long label + "a.an-invalid-very-long-label-string-with-more-than-sixty-four-characters.com", + + // Long name (more than 255 characters) + "HereIsSomeoneHidden.MyHoldFromMeTaken.FromSelfHasMeDriven.MyLeadFromMeTaken." + "HereIsSomeoneHidden.AsLifeSweeterThanLife.TakesMeToGardenOfSoul.MyFortFromMeTaken." + "HereIsSomeoneHidden.LikeSugarInSugarCane.ASweetSugarTrader.MyShopFromMeTaken." + "SorcererAndMagician.NoEyesCanEverSee.AnArtfulConjurer.MySenseFromMeTaken." + "MyEyesWillNeverSee.BeautiesOfTheWholeWorld.BeholdWhoseVisionFine.MySightFromMeTaken" + "PoemByRumiMolana", + }; + + printf("================================================================\n"); + printf("TestDnsName()\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("Append names, check encoded bytes, parse name and read labels:\n"); + + for (const TestName &test : kTestNames) + { + IgnoreError(message->SetLength(0)); + + SuccessOrQuit(Dns::Name::AppendName(test.mName, *message), "Name::AppendName() failed"); + + len = message->GetLength(); + SuccessOrQuit(message->Read(0, buffer, len), "Message::Read() failed"); + + DumpBuffer(test.mName, buffer, len); + + VerifyOrQuit(len == test.mEncodedLength, "Encoded length does not match expected value"); + VerifyOrQuit(memcmp(buffer, test.mEncodedData, len) == 0, "Encoded name data does not match expected data"); + + // Parse and skip over the name + offset = 0; + SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed"); + VerifyOrQuit(offset == len, "Name::ParseName() returned incorrect offset"); + + // Read labels one by one. + offset = 0; + + for (uint8_t index = 0; test.mLabels[index] != nullptr; index++) + { + labelLength = sizeof(label); + SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, 0, label, labelLength), "Name::ReadLabel() failed"); + + printf("Label[%d] = \"%s\"\n", index, label); + + VerifyOrQuit(strcmp(label, test.mLabels[index]) == 0, "Name::ReadLabel() did not get expected label"); + VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length"); + } + + labelLength = sizeof(label); + VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, 0, label, labelLength) == OT_ERROR_NOT_FOUND, + "Name::ReadLabel() failed at end of the name"); + + // Read entire name + offset = 0; + SuccessOrQuit(Dns::Name::ReadName(*message, offset, 0, name, sizeof(name)), "Name::ReadName() failed"); + + printf("Read name =\"%s\"\n", name); + + VerifyOrQuit(strcmp(name, test.mExpectedReadName) == 0, "Name::ReadName() did not get expected name"); + VerifyOrQuit(offset == len, "Name::ReadName() returned incorrect offset"); + + // Read entire name with different name buffer sizes (just right and one byte off the expected size) + offset = 0; + SuccessOrQuit( + Dns::Name::ReadName(*message, offset, 0, name, static_cast(strlen(test.mExpectedReadName) + 1)), + "Name::ReadName() failed with exact name buffer size"); + offset = 0; + VerifyOrQuit(Dns::Name::ReadName(*message, offset, 0, name, + static_cast(strlen(test.mExpectedReadName))) == OT_ERROR_NO_BUFS, + "Name::ReadName() did not fail with too small name buffer size"); + } + + printf("----------------------------------------------------------------\n"); + printf("Invalid names:\n"); + + for (const char *&invalidName : kInvalidNames) + { + IgnoreError(message->SetLength(0)); + + printf("\"%s\"\n", invalidName); + + VerifyOrQuit(Dns::Name::AppendName(invalidName, *message) == OT_ERROR_INVALID_ARGS, + "Name::AppendName() did not fail with an invalid name"); + } + + printf("----------------------------------------------------------------\n"); + printf("Append as multiple labels and terminator instead of full name:\n"); + + for (const TestName &test : kTestNames) + { + IgnoreError(message->SetLength(0)); + + SuccessOrQuit(Dns::Name::AppendMultipleLabels(test.mName, *message), "Name::Append() failed"); + SuccessOrQuit(Dns::Name::AppendTerminator(*message), "Name::AppendTerminator() failed"); + + len = message->GetLength(); + SuccessOrQuit(message->Read(0, buffer, len), "Message::Read() failed"); + + DumpBuffer(test.mName, buffer, len); + + VerifyOrQuit(len == test.mEncodedLength, "Encoded length does not match expected value"); + VerifyOrQuit(memcmp(buffer, test.mEncodedData, len) == 0, "Encoded name data does not match expected data"); + } + + printf("----------------------------------------------------------------\n"); + printf("Append labels one by one:\n"); + + for (const TestName &test : kTestNames) + { + IgnoreError(message->SetLength(0)); + + for (uint8_t index = 0; test.mLabels[index] != nullptr; index++) + { + SuccessOrQuit(Dns::Name::AppendLabel(test.mLabels[index], *message), "Name::AppendLabel() failed"); + } + + SuccessOrQuit(Dns::Name::AppendTerminator(*message), "Name::AppendTerminator() failed"); + + len = message->GetLength(); + SuccessOrQuit(message->Read(0, buffer, len), "Message::Read() failed"); + + DumpBuffer(test.mName, buffer, len); + + VerifyOrQuit(len == test.mEncodedLength, "Encoded length does not match expected value"); + VerifyOrQuit(memcmp(buffer, test.mEncodedData, len) == 0, "Encoded name data does not match expected data"); + } + + message->Free(); + testFreeInstance(instance); +} + +void TestDnsCompressedName(void) +{ + enum + { + kHeaderOffset = 10, + kGuardBlockSize = 20, + kMaxBufferSize = 100, + kLabelSize = 64, + kNameSize = 256, + + kName2EncodedSize = 4 + 2, // encoded "FOO" + pointer label (2 bytes) + kName3EncodedSize = 2, // pointer label (2 bytes) + kName4EncodedSize = 15 + 2, // encoded "Human.Readable" + pointer label (2 bytes). + + }; + + const char kName[] = "F.ISI.ARPA"; + const char kLabel1[] = "FOO"; + const char kInstanceLabel[] = "Human.Readable"; + + static const uint8_t kEncodedName[] = {1, 'F', 3, 'I', 'S', 'I', 4, 'A', 'R', 'P', 'A', 0}; + static const uint8_t kIsiRelativeIndex = 2; // Index in kEncodedName to the start of "ISI.ARPA" portion. + + static const char *kName1Labels[] = {"F", "ISI", "ARPA"}; + static const char *kName2Labels[] = {"FOO", "F", "ISI", "ARPA"}; + static const char *kName3Labels[] = {"ISI", "ARPA"}; + static const char *kName4Labels[] = {"Human.Readable", "F", "ISI", "ARPA"}; + + static const char kExpectedReadName1[] = "F.ISI.ARPA."; + static const char kExpectedReadName2[] = "FOO.F.ISI.ARPA."; + static const char kExpectedReadName3[] = "ISI.ARPA."; + + Instance * instance; + MessagePool *messagePool; + Message * message; + uint16_t offset; + uint16_t name1Offset; + uint16_t name2Offset; + uint16_t name3Offset; + uint16_t name4Offset; + uint8_t buffer[kMaxBufferSize]; + char label[kLabelSize]; + uint8_t labelLength; + char name[kNameSize]; + + printf("================================================================\n"); + printf("TestDnsCompressedName()\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"); + + // Append name1 "F.ISI.ARPA" + + for (uint8_t index = 0; index < kHeaderOffset + kGuardBlockSize; index++) + { + SuccessOrQuit(message->Append(index), "Message::Append() failed"); + } + + name1Offset = message->GetLength(); + SuccessOrQuit(Dns::Name::AppendName(kName, *message), "Name::AppendName() failed"); + + // Append name2 "FOO.F.ISI.ARPA" as a compressed name after some guard/extra bytes + + for (uint8_t index = 0; index < kGuardBlockSize; index++) + { + uint8_t value = 0xff; + SuccessOrQuit(message->Append(value), "Message::Append() failed"); + } + + name2Offset = message->GetLength(); + + SuccessOrQuit(Dns::Name::AppendLabel(kLabel1, *message), "Name::AppendLabel() failed"); + SuccessOrQuit(Dns::Name::AppendPointerLabel(name1Offset - kHeaderOffset, *message), + "Name::AppendPointerLabel() failed"); + + // Append name3 "ISI.ARPA" as a compressed name after some guard/extra bytes + + for (uint8_t index = 0; index < kGuardBlockSize; index++) + { + uint8_t value = 0xaa; + SuccessOrQuit(message->Append(value), "Message::Append() failed"); + } + + name3Offset = message->GetLength(); + SuccessOrQuit(Dns::Name::AppendPointerLabel(name1Offset + kIsiRelativeIndex - kHeaderOffset, *message), + "Name::AppendPointerLabel() failed"); + + name4Offset = message->GetLength(); + SuccessOrQuit(Dns::Name::AppendLabel(kInstanceLabel, *message), "Name::AppendLabel() failed"); + SuccessOrQuit(Dns::Name::AppendPointerLabel(name1Offset - kHeaderOffset, *message), + "Name::AppendPointerLabel() failed"); + + printf("----------------------------------------------------------------\n"); + printf("Read and parse the uncompressed name-1 \"F.ISI.ARPA\"\n"); + + SuccessOrQuit(message->Read(name1Offset, buffer, sizeof(kEncodedName)), "Message::Read() failed"); + + DumpBuffer(kName, buffer, sizeof(kEncodedName)); + VerifyOrQuit(memcmp(buffer, kEncodedName, sizeof(kEncodedName)) == 0, + "Encoded name data does not match expected data"); + + offset = name1Offset; + SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed"); + + VerifyOrQuit(offset == name1Offset + sizeof(kEncodedName), "Name::ParseName() returned incorrect offset"); + + offset = name1Offset; + + for (const char *nameLabel : kName1Labels) + { + labelLength = sizeof(label); + SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength), + "Name::ReadLabel() failed"); + + printf("label: \"%s\"\n", label); + VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label"); + VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length"); + } + + labelLength = sizeof(label); + VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength) == OT_ERROR_NOT_FOUND, + "Name::ReadLabel() failed at end of the name"); + + offset = name1Offset; + SuccessOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)), "Name::ReadName() failed"); + printf("Read name =\"%s\"\n", name); + VerifyOrQuit(strcmp(name, kExpectedReadName1) == 0, "Name::ReadName() did not return expected name"); + VerifyOrQuit(offset == name1Offset + sizeof(kEncodedName), "Name::ReadName() returned incorrect offset"); + + printf("----------------------------------------------------------------\n"); + printf("Read and parse compressed name-2 \"FOO.F.ISI.ARPA\"\n"); + + SuccessOrQuit(message->Read(name2Offset, buffer, kName2EncodedSize), "Message::Read() failed"); + DumpBuffer("name2(compressed)", buffer, kName2EncodedSize); + + offset = name2Offset; + SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed"); + VerifyOrQuit(offset == name2Offset + kName2EncodedSize, "Name::ParseName() returned incorrect offset"); + + offset = name2Offset; + + for (const char *nameLabel : kName2Labels) + { + labelLength = sizeof(label); + SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength), + "Name::ReadLabel() failed"); + + printf("label: \"%s\"\n", label); + VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label"); + VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length"); + } + + labelLength = sizeof(label); + VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength) == OT_ERROR_NOT_FOUND, + "Name::ReadLabel() failed at end of the name"); + + offset = name2Offset; + SuccessOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)), "Name::ReadName() failed"); + printf("Read name =\"%s\"\n", name); + VerifyOrQuit(strcmp(name, kExpectedReadName2) == 0, "Name::ReadName() did not return expected name"); + VerifyOrQuit(offset == name2Offset + kName2EncodedSize, "Name::ReadName() returned incorrect offset"); + + printf("----------------------------------------------------------------\n"); + printf("Read and parse compressed name-3 \"ISI.ARPA\"\n"); + + SuccessOrQuit(message->Read(name3Offset, buffer, kName3EncodedSize), "Message::Read() failed"); + DumpBuffer("name2(compressed)", buffer, kName3EncodedSize); + + offset = name3Offset; + SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed"); + VerifyOrQuit(offset == name3Offset + kName3EncodedSize, "Name::ParseName() returned incorrect offset"); + + offset = name3Offset; + + for (const char *nameLabel : kName3Labels) + { + labelLength = sizeof(label); + SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength), + "Name::ReadLabel() failed"); + + printf("label: \"%s\"\n", label); + VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label"); + VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length"); + } + + labelLength = sizeof(label); + VerifyOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength) == OT_ERROR_NOT_FOUND, + "Name::ReadLabel() failed at end of the name"); + + offset = name3Offset; + SuccessOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)), "Name::ReadName() failed"); + printf("Read name =\"%s\"\n", name); + VerifyOrQuit(strcmp(name, kExpectedReadName3) == 0, "Name::ReadName() did not return expected name"); + VerifyOrQuit(offset == name3Offset + kName3EncodedSize, "Name::ReadName() returned incorrect offset"); + + printf("----------------------------------------------------------------\n"); + printf("Read and parse the uncompressed name-4 \"Human\\.Readable.F.ISI.ARPA\"\n"); + + SuccessOrQuit(message->Read(name4Offset, buffer, kName4EncodedSize), "Message::Read() failed"); + DumpBuffer("name4(compressed)", buffer, kName4EncodedSize); + + offset = name4Offset; + SuccessOrQuit(Dns::Name::ParseName(*message, offset), "Name::ParseName() failed"); + VerifyOrQuit(offset == name4Offset + kName4EncodedSize, "Name::ParseName() returned incorrect offset"); + + offset = name4Offset; + + for (const char *nameLabel : kName4Labels) + { + labelLength = sizeof(label); + SuccessOrQuit(Dns::Name::ReadLabel(*message, offset, kHeaderOffset, label, labelLength), + "Name::ReadLabel() failed"); + + printf("label: \"%s\"\n", label); + VerifyOrQuit(strcmp(label, nameLabel) == 0, "Name::ReadLabel() did not get expected label"); + VerifyOrQuit(labelLength == strlen(label), "Name::ReadLabel() returned incorrect label length"); + } + + // `ReadName()` for name-4 should fails due to first label containing dot char. + offset = name4Offset; + VerifyOrQuit(Dns::Name::ReadName(*message, offset, kHeaderOffset, name, sizeof(name)) == OT_ERROR_PARSE, + "Name::ReadName() did not fail with invalid label"); + + message->Free(); + testFreeInstance(instance); +} + +} // namespace ot + +int main(void) +{ + ot::TestDnsName(); + ot::TestDnsCompressedName(); + + printf("All tests passed\n"); + return 0; +} diff --git a/tests/unit/test_string.cpp b/tests/unit/test_string.cpp index e16801d1e..9f265d356 100644 --- a/tests/unit/test_string.cpp +++ b/tests/unit/test_string.cpp @@ -154,6 +154,27 @@ void TestUtf8(void) printf(" -- PASS\n"); } +void TestStringFind(void) +{ + char emptyString[1] = {'\0'}; + char testString[] = "foo.bar\\."; + + printf("\nTest 6: StringFind() function\n"); + + VerifyOrQuit(StringFind(testString, 'f') == testString, "StringFind() failed"); + VerifyOrQuit(StringFind(testString, 'o') == &testString[1], "StringFind() failed"); + VerifyOrQuit(StringFind(testString, '.') == &testString[3], "StringFind() failed"); + VerifyOrQuit(StringFind(testString, 'r') == &testString[6], "StringFind() failed"); + VerifyOrQuit(StringFind(testString, '\\') == &testString[7], "StringFind() failed"); + VerifyOrQuit(StringFind(testString, 'x') == nullptr, "StringFind() failed"); + VerifyOrQuit(StringFind(testString, ',') == nullptr, "StringFind() failed"); + + VerifyOrQuit(StringFind(emptyString, 'f') == nullptr, "StringFind() failed"); + VerifyOrQuit(StringFind(emptyString, '.') == nullptr, "StringFind() failed"); + + printf(" -- PASS\n"); +} + } // namespace ot int main(void) @@ -161,6 +182,7 @@ int main(void) ot::TestString(); ot::TestStringLength(); ot::TestUtf8(); + ot::TestStringFind(); printf("\nAll tests passed.\n"); return 0; }