| /* | |
| * | |
| * Copyright (c) 2026 Project CHIP Authors | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <lib/dnssd/wire/Parser.h> | |
| #include <string> | |
| #include <vector> | |
| #include <pw_unit_test/framework.h> | |
| #include <lib/core/StringBuilderAdapters.h> | |
| namespace { | |
| using namespace chip; | |
| using namespace chip::Dnssd; | |
| constexpr uint16_t kKeyRecordType = 25; | |
| std::vector<std::string> NameLabels(SerializedQNameIterator it) | |
| { | |
| std::vector<std::string> labels; | |
| while (it.Next()) | |
| { | |
| labels.push_back(it.Value()); | |
| } | |
| return labels; | |
| } | |
| struct ParsedQuery | |
| { | |
| uint16_t type; | |
| QClass klass; | |
| bool unicastAnswer; | |
| std::vector<std::string> name; | |
| }; | |
| struct ParsedResource | |
| { | |
| ResourceType section; | |
| uint16_t type; | |
| QClass klass; | |
| uint64_t ttl; | |
| std::vector<std::string> name; | |
| std::vector<uint8_t> data; | |
| }; | |
| class PacketCapture : public ParserDelegate | |
| { | |
| public: | |
| void OnHeader(ConstHeaderRef & header) override { mMessageId = header.GetMessageId(); } | |
| void OnQuery(const QueryData & data) override | |
| { | |
| mQueries.push_back(ParsedQuery{ static_cast<uint16_t>(data.GetType()), data.GetClass(), data.RequestedUnicastAnswer(), | |
| NameLabels(data.GetName()) }); | |
| } | |
| void OnResource(ResourceType section, const ResourceData & data) override | |
| { | |
| mResources.push_back(ParsedResource{ section, static_cast<uint16_t>(data.GetType()), data.GetClass(), data.GetTtlSeconds(), | |
| NameLabels(data.GetName()), | |
| std::vector<uint8_t>(data.GetData().Start(), data.GetData().End()) }); | |
| } | |
| uint16_t mMessageId = 0; | |
| std::vector<ParsedQuery> mQueries; | |
| std::vector<ParsedResource> mResources; | |
| }; | |
| TEST(TestParser, ParsesQuestion) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x00, 0x00, // flags: query, opcode 0, rcode 0 | |
| 0x00, 0x01, // one question | |
| 0x00, 0x00, // no answers | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x00, // no additional records | |
| 4, 'h', 'o', 's', 't', // QNAME part: host | |
| 5, 'l', 'o', 'c', 'a', 'l', // QNAME part: local | |
| 0, // QNAME ends | |
| 0x00, 0x0C, // QTYPE PTR | |
| 0x00, 0x01, // QCLASS IN | |
| }; | |
| PacketCapture capture; | |
| ASSERT_TRUE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| EXPECT_EQ(capture.mMessageId, 0x1234); | |
| EXPECT_TRUE(capture.mResources.empty()); | |
| ASSERT_EQ(capture.mQueries.size(), 1u); | |
| const ParsedQuery & query = capture.mQueries[0]; | |
| EXPECT_EQ(query.type, static_cast<uint16_t>(QType::PTR)); | |
| EXPECT_EQ(query.klass, QClass::IN); | |
| EXPECT_FALSE(query.unicastAnswer); | |
| EXPECT_EQ(query.name, (std::vector<std::string>{ "host", "local" })); | |
| } | |
| TEST(TestParser, QuestionUnicastFlagIsSplitFromClass) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x00, 0x00, // flags: query, opcode 0, rcode 0 | |
| 0x00, 0x01, // one question | |
| 0x00, 0x00, // no answers | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x00, // no additional records | |
| 4, 'h', 'o', 's', 't', // QNAME part: host | |
| 0, // QNAME ends | |
| 0x00, 0x01, // QTYPE A | |
| 0x80, 0x01, // QCLASS IN with the unicast-response bit set | |
| }; | |
| PacketCapture capture; | |
| ASSERT_TRUE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| ASSERT_EQ(capture.mQueries.size(), 1u); | |
| // The unicast bit is reported separately and must not leak into the class. | |
| EXPECT_TRUE(capture.mQueries[0].unicastAnswer); | |
| EXPECT_EQ(capture.mQueries[0].klass, QClass::IN); | |
| } | |
| TEST(TestParser, ReportsEachSectionSeparately) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x84, 0x00, // flags: response, authoritative, opcode 0, rcode 0 | |
| 0x00, 0x00, // no questions | |
| 0x00, 0x01, // one answer | |
| 0x00, 0x01, // one authority record | |
| 0x00, 0x01, // one additional record | |
| 1, 'a', // QNAME part: a | |
| 0, // QNAME ends | |
| 0x00, 0x10, // TYPE TXT | |
| 0x80, 0x01, // CLASS IN with the mDNS cache-flush bit set | |
| 0x00, 0x00, 0x00, 0x0A, // TTL 10 | |
| 0x00, 0x01, // RDLENGTH | |
| 0xAA, | |
| 1, 'b', // QNAME part: b | |
| 0, // QNAME ends | |
| 0x00, 0x02, // TYPE NS | |
| 0x00, 0x01, // CLASS IN | |
| 0x00, 0x00, 0x00, 0x14, // TTL 20 | |
| 0x00, 0x01, // RDLENGTH | |
| 0xBB, | |
| 1, 'c', // QNAME part: c | |
| 0, // QNAME ends | |
| 0x00, 0x01, // TYPE A | |
| 0x00, 0x01, // CLASS IN | |
| 0x00, 0x00, 0x00, 0x1E, // TTL 30 | |
| 0x00, 0x01, // RDLENGTH | |
| 0xCC, | |
| }; | |
| PacketCapture capture; | |
| ASSERT_TRUE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| ASSERT_EQ(capture.mResources.size(), 3u); | |
| EXPECT_EQ(capture.mResources[0].section, ResourceType::kAnswer); | |
| EXPECT_EQ(capture.mResources[0].name, (std::vector<std::string>{ "a" })); | |
| EXPECT_EQ(capture.mResources[0].ttl, 10u); | |
| EXPECT_EQ(capture.mResources[0].data, (std::vector<uint8_t>{ 0xAA })); | |
| // Unlike the question unicast bit, the cache-flush bit stays part of the class. | |
| EXPECT_EQ(capture.mResources[0].klass, QClass::IN_FLUSH); | |
| EXPECT_EQ(capture.mResources[1].section, ResourceType::kAuthority); | |
| EXPECT_EQ(capture.mResources[1].name, (std::vector<std::string>{ "b" })); | |
| EXPECT_EQ(capture.mResources[1].ttl, 20u); | |
| EXPECT_EQ(capture.mResources[1].data, (std::vector<uint8_t>{ 0xBB })); | |
| EXPECT_EQ(capture.mResources[1].klass, QClass::IN); | |
| EXPECT_EQ(capture.mResources[2].section, ResourceType::kAdditional); | |
| EXPECT_EQ(capture.mResources[2].name, (std::vector<std::string>{ "c" })); | |
| EXPECT_EQ(capture.mResources[2].ttl, 30u); | |
| EXPECT_EQ(capture.mResources[2].data, (std::vector<uint8_t>{ 0xCC })); | |
| } | |
| TEST(TestParser, ResolvesCompressedRecordName) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, | |
| 0x34, // message id | |
| 0x00, | |
| 0x00, // flags: query, opcode 0, rcode 0 | |
| 0x00, | |
| 0x01, // one question | |
| 0x00, | |
| 0x01, // one answer | |
| 0x00, | |
| 0x00, // no authority records | |
| 0x00, | |
| 0x00, // no additional records | |
| // Question name starts at offset 12. | |
| 4, | |
| 'h', | |
| 'o', | |
| 's', | |
| 't', // QNAME part: host | |
| 5, | |
| 'l', | |
| 'o', | |
| 'c', | |
| 'a', | |
| 'l', // QNAME part: local | |
| 0, // QNAME ends | |
| 0x00, | |
| 0x01, // QTYPE A | |
| 0x00, | |
| 0x01, // QCLASS IN | |
| 0xC0, | |
| 0x0C, // QNAME: pointer back to offset 12 | |
| 0x00, | |
| 0x01, // TYPE A | |
| 0x00, | |
| 0x01, // CLASS IN | |
| 0x00, | |
| 0x00, | |
| 0x00, | |
| 0x3C, // TTL 60 | |
| 0x00, | |
| 0x04, // RDLENGTH | |
| 10, | |
| 0, | |
| 0, | |
| 1, | |
| }; | |
| PacketCapture capture; | |
| ASSERT_TRUE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| ASSERT_EQ(capture.mResources.size(), 1u); | |
| EXPECT_EQ(capture.mResources[0].name, (std::vector<std::string>{ "host", "local" })); | |
| EXPECT_EQ(capture.mResources[0].data, (std::vector<uint8_t>{ 10, 0, 0, 1 })); | |
| } | |
| TEST(TestParser, UnknownRecordTypeKeepsDataOpaque) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x00, 0x00, // flags: query, opcode 0, rcode 0 | |
| 0x00, 0x00, // no questions | |
| 0x00, 0x00, // no answers | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x01, // one additional record | |
| 4, 'h', 'o', 's', 't', // QNAME part: host | |
| 5, 'l', 'o', 'c', 'a', 'l', // QNAME part: local | |
| 0, // QNAME ends | |
| 0x00, 0x19, // TYPE 25 (KEY) | |
| 0x00, 0x01, // CLASS IN | |
| 0x00, 0x00, 0x0E, 0x10, // TTL 3600 | |
| 0x00, 0x06, // RDLENGTH | |
| 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, | |
| }; | |
| PacketCapture capture; | |
| ASSERT_TRUE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| EXPECT_EQ(capture.mMessageId, 0x1234); | |
| EXPECT_TRUE(capture.mQueries.empty()); | |
| ASSERT_EQ(capture.mResources.size(), 1u); | |
| const ParsedResource & resource = capture.mResources[0]; | |
| EXPECT_EQ(resource.section, ResourceType::kAdditional); | |
| EXPECT_EQ(resource.type, kKeyRecordType); | |
| EXPECT_EQ(resource.klass, QClass::IN); | |
| EXPECT_EQ(resource.ttl, 3600u); | |
| EXPECT_EQ(resource.data, (std::vector<uint8_t>{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 })); | |
| } | |
| TEST(TestParser, AcceptsHeaderOnlyPacket) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x00, 0x00, // flags: query, opcode 0, rcode 0 | |
| 0x00, 0x00, // no questions | |
| 0x00, 0x00, // no answers | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x00, // no additional records | |
| }; | |
| PacketCapture capture; | |
| EXPECT_TRUE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| EXPECT_EQ(capture.mMessageId, 0x1234); | |
| EXPECT_TRUE(capture.mQueries.empty()); | |
| EXPECT_TRUE(capture.mResources.empty()); | |
| } | |
| TEST(TestParser, RejectsNonZeroOpcode) | |
| { | |
| // RFC 6762 section 18.3 requires mDNS receivers to ignore anything with a non-zero opcode. | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x28, 0x00, // flags: opcode 5 | |
| 0x00, 0x00, // no questions | |
| 0x00, 0x00, // no answers | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x00, // no additional records | |
| }; | |
| PacketCapture capture; | |
| EXPECT_FALSE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| EXPECT_TRUE(capture.mResources.empty()); | |
| } | |
| TEST(TestParser, ParseDnsPacketAcceptsUpdateOpcode) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0xA8, 0x05, // flags: response, opcode 5, rcode 5 | |
| 0x00, 0x00, // no questions | |
| 0x00, 0x00, // no answers | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x00, // no additional records | |
| }; | |
| PacketCapture capture; | |
| ASSERT_TRUE(ParseDnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| EXPECT_EQ(capture.mMessageId, 0x1234); | |
| EXPECT_TRUE(capture.mQueries.empty()); | |
| EXPECT_TRUE(capture.mResources.empty()); | |
| } | |
| TEST(TestParser, RejectsTruncatedHeader) | |
| { | |
| const uint8_t packet[] = { 0x12, 0x34, 0x00, 0x00, 0x00, 0x00 }; | |
| PacketCapture capture; | |
| EXPECT_FALSE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| } | |
| TEST(TestParser, RejectsRecordDataPastEndOfPacket) | |
| { | |
| const uint8_t packet[] = { | |
| 0x12, 0x34, // message id | |
| 0x00, 0x00, // flags: query, opcode 0, rcode 0 | |
| 0x00, 0x00, // no questions | |
| 0x00, 0x01, // one answer | |
| 0x00, 0x00, // no authority records | |
| 0x00, 0x00, // no additional records | |
| 4, 'h', 'o', 's', 't', // QNAME part: host | |
| 0, // QNAME ends | |
| 0x00, 0x19, // TYPE 25 (KEY) | |
| 0x00, 0x01, // CLASS IN | |
| 0x00, 0x00, 0x0E, 0x10, // TTL 3600 | |
| 0x00, 0x20, // RDLENGTH claims 32 bytes ... | |
| 0x01, 0x02, // ... but only 2 are present | |
| }; | |
| PacketCapture capture; | |
| EXPECT_FALSE(ParseMdnsPacket(BytesRange::BufferWithSize(packet, sizeof(packet)), &capture)); | |
| } | |
| } // namespace |