diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 80e5f3f42..9158fdc7e 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -295,6 +295,7 @@ ot_nexus_test(1_4_TREL_TC_5 "cert;nexus") ot_nexus_test(1_4_TREL_TC_6 "cert;nexus") ot_nexus_test(1_4_DNS_TC_1 "cert;nexus") ot_nexus_test(1_4_DNS_TC_3 "cert;nexus") +ot_nexus_test(1_4_DNS_TC_5 "cert;nexus") ot_nexus_test(1_4_CS_TC_3 "cert;nexus") # Misc tests diff --git a/tests/nexus/platform/nexus_dns.cpp b/tests/nexus/platform/nexus_dns.cpp index 604858b4f..06c8a28e3 100644 --- a/tests/nexus/platform/nexus_dns.cpp +++ b/tests/nexus/platform/nexus_dns.cpp @@ -117,7 +117,7 @@ void UpstreamDns::StartUpstreamQuery(UpstreamQueryTransaction &aTxn, const Messa SuccessOrExit(error = aQuery.Read(0, dnsHeader)); - message = node.Get().CloneMessage(aQuery); + message = aQuery.Clone(); VerifyOrExit(message != nullptr, error = kErrorNoBufs); pendingQuery = PendingQuery::Allocate(); diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index be039e851..a084cd1aa 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -228,6 +228,7 @@ DEFAULT_TESTS=( "1_4_TREL_TC_6" "1_4_DNS_TC_1" "1_4_DNS_TC_3" + "1_4_DNS_TC_5" "1_4_CS_TC_3" ) diff --git a/tests/nexus/test_1_4_DNS_TC_5.cpp b/tests/nexus/test_1_4_DNS_TC_5.cpp new file mode 100644 index 000000000..a4c6cc348 --- /dev/null +++ b/tests/nexus/test_1_4_DNS_TC_5.cpp @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2026, 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 "nexus_core.hpp" +#include "nexus_node.hpp" + +namespace ot { +namespace Nexus { + +namespace { + +static constexpr uint32_t kFormNetworkTime = 10 * 1000; +static constexpr uint32_t kJoinNetworkTime = 20 * 1000; +static constexpr uint32_t kStabilizationTime = 10 * 1000; + +static constexpr char kGuaPrefix[] = "2005:1234:abcd:100::/64"; +static constexpr char kEth1Addr[] = "2005:1234:abcd:100::E1"; +static constexpr char kBr1AilAddr[] = "2005:1234:abcd:100::B1"; + +static constexpr char kThreadGroup1[] = "threadgroup1.org"; +static constexpr char kThreadGroup2[] = "threadgroup2.org"; + +static constexpr char kThreadGroup1Aaaa[] = "2002:1234::E1:1"; +static constexpr char kThreadGroup1A[] = "192.168.217.1"; +static constexpr char kThreadGroup2A[] = "192.168.217.2"; + +static constexpr uint16_t kTypePrivateUse = 65285; // 0xFF05 +static constexpr uint32_t kPrivateData = 0x12345678; + +static Node *sEth1Node = nullptr; + +bool HandleUdpHook(Instance &aInstance, Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Node &node = AsNode(&aInstance); + Dns::Header dnsHeader; + char name[Dns::Name::kMaxNameSize]; + uint16_t readPos; + uint16_t qType; + uint16_t qClass; + Message *response; + bool handled = false; + + VerifyOrExit(&node == sEth1Node); + VerifyOrExit(aMessageInfo.GetSockPort() == UpstreamDns::kDnsPort); + + SuccessOrExit(aMessage.Read(aMessage.GetOffset(), dnsHeader)); + VerifyOrExit(dnsHeader.GetType() == Dns::Header::kTypeQuery); + VerifyOrExit(dnsHeader.GetQuestionCount() == 1); + + readPos = aMessage.GetOffset() + sizeof(dnsHeader); + SuccessOrExit(Dns::Name::ReadName(aMessage, readPos, name)); + SuccessOrExit(aMessage.Read(readPos, qType)); + qType = BigEndian::HostSwap16(qType); + readPos += sizeof(uint16_t); + SuccessOrExit(aMessage.Read(readPos, qClass)); + qClass = BigEndian::HostSwap16(qClass); + readPos += sizeof(uint16_t); + + Log("Eth_1 received DNS query for '%s' (type %u)", name, qType); + + response = node.Get().Allocate(Message::kTypeIp6); + VerifyOrExit(response != nullptr); + + dnsHeader.SetType(Dns::Header::kTypeResponse); + dnsHeader.SetAnswerCount(0); + dnsHeader.SetRecursionDesiredFlag(); + dnsHeader.SetRecursionAvailableFlag(); + + if (Dns::Name::IsSameDomain(name, kThreadGroup1)) + { + if (qType == Dns::ResourceRecord::kTypeAaaa) + { + Ip6::Address address; + Dns::ResourceRecord rr; + + SuccessOrQuit(address.FromString(kThreadGroup1Aaaa)); + dnsHeader.SetAnswerCount(1); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qType))); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qClass))); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + rr.Init(Dns::ResourceRecord::kTypeAaaa); + rr.SetTtl(1800); + rr.SetLength(sizeof(Ip6::Address)); + SuccessOrQuit(response->Append(rr)); + SuccessOrQuit(response->Append(address)); + Log("Eth_1 sending AAAA response for %s: %s", name, kThreadGroup1Aaaa); + } + else if (qType == Dns::ResourceRecord::kTypeA) + { + Ip4::Address address; + Dns::ResourceRecord rr; + + SuccessOrQuit(address.FromString(kThreadGroup1A)); + dnsHeader.SetAnswerCount(1); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qType))); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qClass))); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + rr.Init(Dns::ResourceRecord::kTypeA); + rr.SetTtl(1800); + rr.SetLength(sizeof(Ip4::Address)); + SuccessOrQuit(response->Append(rr)); + SuccessOrQuit(response->Append(address)); + Log("Eth_1 sending A response for %s: %s", name, kThreadGroup1A); + } + } + else if (Dns::Name::IsSameDomain(name, kThreadGroup2)) + { + if (qType == Dns::ResourceRecord::kTypeA) + { + Ip4::Address address; + Dns::ResourceRecord rr; + + SuccessOrQuit(address.FromString(kThreadGroup2A)); + dnsHeader.SetAnswerCount(1); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qType))); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qClass))); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + rr.Init(Dns::ResourceRecord::kTypeA); + rr.SetTtl(1800); + rr.SetLength(sizeof(Ip4::Address)); + SuccessOrQuit(response->Append(rr)); + SuccessOrQuit(response->Append(address)); + Log("Eth_1 sending A response for %s: %s", name, kThreadGroup2A); + } + else if (qType == kTypePrivateUse) + { + Dns::ResourceRecord rr; + uint32_t data = BigEndian::HostSwap32(kPrivateData); + + dnsHeader.SetAnswerCount(1); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qType))); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qClass))); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + rr.Init(kTypePrivateUse); + rr.SetTtl(1800); + rr.SetLength(sizeof(uint32_t)); + SuccessOrQuit(response->Append(rr)); + SuccessOrQuit(response->Append(data)); + Log("Eth_1 sending PrivateUse response for %s", name); + } + } + + if (dnsHeader.GetAnswerCount() == 0) + { + Log("Eth_1 sending empty response for %s", name); + SuccessOrQuit(response->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName(name, *response)); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qType))); + SuccessOrQuit(response->Append(BigEndian::HostSwap16(qClass))); + } + + node.mInfraIf.SendUdp(aMessageInfo.GetSockAddr(), aMessageInfo.GetPeerAddr(), UpstreamDns::kDnsPort, + aMessageInfo.GetPeerPort(), *response); + handled = true; + +exit: + return handled; +} + +void HandleDnsResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext) +{ + OT_UNUSED_VARIABLE(aError); + OT_UNUSED_VARIABLE(aResponse); + OT_UNUSED_VARIABLE(aContext); +} + +void HandleDnsRecordResponse(otError aError, const otDnsRecordResponse *aResponse, void *aContext) +{ + OT_UNUSED_VARIABLE(aError); + OT_UNUSED_VARIABLE(aResponse); + OT_UNUSED_VARIABLE(aContext); +} + +void SendQuery(Node &aNode, const char *aName, uint16_t aType) +{ + if (aType == Dns::ResourceRecord::kTypeAaaa) + { + SuccessOrQuit(aNode.Get().ResolveAddress(aName, HandleDnsResponse, nullptr)); + } + else + { + SuccessOrQuit(aNode.Get().QueryRecord(aType, nullptr, aName, HandleDnsRecordResponse, nullptr)); + } +} + +void SendRa(Node &aNode, const Ip6::Address &aDnsAddr, const Ip6::Prefix &aPrefix) +{ + Ip6::Nd::RouterAdvert::TxMessage ra; + Ip6::Nd::RouterAdvert::Header header; + Ip6::Nd::Icmp6Packet packet; + + header.SetToDefault(); + header.SetRouterLifetime(1800); + SuccessOrQuit(ra.Append(header)); + SuccessOrQuit(ra.AppendPrefixInfoOption( + aPrefix, 1800, 1800, Ip6::Nd::PrefixInfoOption::kOnLinkFlag | Ip6::Nd::PrefixInfoOption::kAutoConfigFlag)); + SuccessOrQuit(ra.AppendRecursiveDnsServerOption(&aDnsAddr, 1, 1800)); + ra.GetAsPacket(packet); + aNode.mInfraIf.SendIcmp6Nd(Ip6::Address::GetLinkLocalAllNodesMulticast(), packet.GetBytes(), packet.GetLength()); +} + +} // namespace + +/** + * 11.5. [1.4] [CERT] DNS record types and special cases + * + * 11.5.1. Purpose + * To verify that the BR DUT: + * - Can resolve A records + * - Can resolve A records without doing IPv6 AAAA synthesis + * - Can resolve AAAA records + * - Can resolve records contributed by different sources (on-mesh, on-AIL, and upstream DNS server) + * - Supports non-typical record types (RRTypes) for queries + * - Supports queries using RR type 0xFF00-0xFFFE (65280-65534) “Private Use” + * - Blocks the “ipv4only.arpa” query + */ +void Test_1_4_DNS_TC_5(const char *aJsonFileName) +{ + Core nexus; + Node &br1 = nexus.CreateNode(); + Node &ed1 = nexus.CreateNode(); + Node ð1 = nexus.CreateNode(); + Ip6::Address eth1Addr; + Ip6::Address br1AilAddr; + Ip6::Prefix guaPrefix; + + br1.SetName("BR", 1); + ed1.SetName("ED", 1); + eth1.SetName("Eth", 1); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + sEth1Node = ð1; + eth1.mInfraIf.SetUdpHook(HandleUdpHook); + + br1.AllowList(ed1); + ed1.AllowList(br1); + + SuccessOrQuit(eth1Addr.FromString(kEth1Addr)); + SuccessOrQuit(br1AilAddr.FromString(kBr1AilAddr)); + SuccessOrQuit(guaPrefix.FromString(kGuaPrefix)); + + /** + * Step 1 + * Device: Eth_1, BR_1, ED_1 + * Description (DNS-11.5): Form topology. Thread network is formed. + * Pass Criteria: + * - N/A + */ + Log("Step 1: Form topology. Thread network is formed."); + + eth1.Get().Up(); + eth1.mInfraIf.AddAddress(eth1Addr); + + br1.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + ed1.Join(br1); + nexus.AdvanceTime(kJoinNetworkTime); + + br1.Get().Init(1, true); + br1.mInfraIf.AddAddress(br1AilAddr); + br1.Get().Init(); + SuccessOrQuit(br1.Get().SetEnabled(true)); + SuccessOrQuit(br1.Get().Start()); + br1.Get().SetUpstreamQueryEnabled(true); + + { + Dns::Client::QueryConfig dnsConfig; + dnsConfig.Clear(); + AsCoreType(&dnsConfig.mServerSockAddr.mAddress) = br1.Get().GetMeshLocalEid(); + dnsConfig.mServerSockAddr.mPort = 53; + ed1.Get().SetDefaultConfig(dnsConfig); + } + + // Send RA for BR to adopt the DNS server address and OMR prefix + SendRa(eth1, eth1Addr, guaPrefix); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 2 + * Device: Eth_1 + * Description (DNS-11.5): Harness instructs device to configure DNS server with + * records: threadgroup1.org AAAA 2002:1234::E1:1, threadgroup1.org A 192.168.217.1, + * threadgroup2.org A 192.168.217.2, threadgroup2.org TYPE65285 \# 4 12345678. + * Note: the record type 0xFF05 is a private range record type, hence it has the + * symbolic name TYPE65285 associated. + * Pass Criteria: + * - N/A + */ + Log("Step 2: Eth_1 configures DNS server with records."); + + /** + * Step 3 + * Device: Eth_1 + * Description (DNS-11.5): Harness instructs device to start advertising mDNS + * services on the AIL: Note: the PTR records for services are not shown below; + * however these are present as usual. $ORIGIN local. Service-test-1. + * _infra-test._udp ( SRV 0 0 55551 host-test-eth TXT dummy=abcd;thread-test=a49 ) + * service-test-2._infra-test._udp ( SRV 0 0 55552 host-test-eth TXT a=2;thread-test + * =b50_test ) ) host-test-eth A . + * Pass Criteria: + * - N/A + */ + Log("Step 3: Eth_1 starts advertising mDNS services on the AIL."); + { + Dns::Multicast::Core::Service mdnsService; + Ip6::Address eth1Ip4Addr; + Ip4::Address ip4Addr; + + SuccessOrQuit(eth1.Get().SetLocalHostName("host-test-eth")); + SuccessOrQuit(eth1.Get().SetEnabled(true, 1)); + + SuccessOrQuit(ip4Addr.FromString("192.168.217.100")); + eth1Ip4Addr.SetToIp4Mapped(ip4Addr); + eth1.mInfraIf.AddAddress(eth1Ip4Addr); + eth1.mMdns.SignalIfAddresses(eth1.GetInstance()); + + ClearAllBytes(mdnsService); + mdnsService.mServiceInstance = "service-test-1"; + mdnsService.mServiceType = "_infra-test._udp"; + mdnsService.mHostName = "host-test-eth"; + static const uint8_t kTxtData1[] = "\x0adummy=abcd\x0fthread-test=a49"; + mdnsService.mTxtData = kTxtData1; + mdnsService.mTxtDataLength = sizeof(kTxtData1) - 1; + mdnsService.mPort = 55551; + SuccessOrQuit(eth1.Get().RegisterService(mdnsService, 1, nullptr)); + + mdnsService.mServiceInstance = "service-test-2"; + mdnsService.mPort = 55552; + static const uint8_t kTxtData2[] = "\x03a=2\x14thread-test=b50_test"; + mdnsService.mTxtData = kTxtData2; + mdnsService.mTxtDataLength = sizeof(kTxtData2) - 1; + SuccessOrQuit(eth1.Get().RegisterService(mdnsService, 2, nullptr)); + } + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 4 + * Device: ED_1 + * Description (DNS-11.5): Harness instructs device to send a DNS query to the DUT + * with Qtype=AAAA for name “threadgroup1.org”. + * Pass Criteria: + * - N/A + */ + Log("Step 4: ED_1 sends a DNS query to the DUT with Qtype=AAAA for name threadgroup1.org."); + SendRa(eth1, eth1Addr, guaPrefix); + SendQuery(ed1, kThreadGroup1, Dns::ResourceRecord::kTypeAaaa); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 5 + * Device: BR_1 (DUT) + * Description (DNS-11.5): Automatically performs the DNS query via the Eth_1 DNS + * server, and provides the answer back to ED_1. + * Pass Criteria: + * - The DUT MUST respond Rcode=0 (NoError) with answer record: threadgroup1.org + * AAAA 2002:1234::E1:1 + */ + Log("Step 5: BR_1 performs the DNS query via Eth_1 and provides the answer to ED_1."); + + /** + * Step 6 + * Device: ED_1 + * Description (DNS-11.5): Harness instructs device to send a DNS query to the DUT, + * with Qtype=A for name “threadgroup1.org”. + * Pass Criteria: + * - N/A + */ + Log("Step 6: ED_1 sends a DNS query to the DUT with Qtype=A for name threadgroup1.org."); + SendRa(eth1, eth1Addr, guaPrefix); + SendQuery(ed1, kThreadGroup1, Dns::ResourceRecord::kTypeA); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 7 + * Device: BR_1 (DUT) + * Description (DNS-11.5): Automatically performs the DNS query via the Eth_1 DNS + * server, and provides the answer back to ED_1. + * Pass Criteria: + * - The DUT MUST respond Rcode=0 (NoError) with answer record: threadgroup1.org A + * 192.168.217.1 + */ + Log("Step 7: BR_1 performs the DNS query via Eth_1 and provides the answer to ED_1."); + + /** + * Step 8 + * Device: ED_1 + * Description (DNS-11.5): Harness instructs device to send a DNS query to the DUT, + * with Qtype=AAAA for name “threadgroup2.org”. + * Pass Criteria: + * - N/A + */ + Log("Step 8: ED_1 sends a DNS query to the DUT with Qtype=AAAA for name threadgroup2.org."); + SendRa(eth1, eth1Addr, guaPrefix); + SendQuery(ed1, kThreadGroup2, Dns::ResourceRecord::kTypeAaaa); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 9 + * Device: BR_1 (DUT) + * Description (DNS-11.5): Automatically performs the DNS query via the Eth_1 DNS + * server, and provides the error answer back to ED_1. Note: this checks that the + * DUT does not perform AAAA synthesis from the A record. + * Pass Criteria: + * - The DUT MUST respond Rcode=0 (NoError) with zero answer records (ANCOUNT=0). + */ + Log("Step 9: BR_1 performs the DNS query via Eth_1 and provides the error answer to ED_1."); + + /** + * Step 10 + * Device: ED_1 + * Description (DNS-11.5): Harness instructs device to send a DNS query, with + * Qtype=A for name “host-test-eth.default.service.arpa”. + * Pass Criteria: + * - N/A + */ + Log("Step 10: ED_1 sends a DNS query with Qtype=A for host-test-eth.default.service.arpa."); + SendQuery(ed1, "host-test-eth.default.service.arpa", Dns::ResourceRecord::kTypeA); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 11 + * Device: BR_1 (DUT) + * Description (DNS-11.5): Automatically performs the mDNS query on AIL + * (optionally, if still needed), and provides the answer back to ED_1. + * Pass Criteria: + * - The DUT MUST respond Rcode=0 (NoError) with answer record: host-test-eth. + * default.service.arpa A + */ + Log("Step 11: BR_1 performs mDNS query on AIL and provides the answer to ED_1."); + + /** + * Step 12 + * Device: ED_1 + * Description (DNS-11.5): Harness instructs device to send DNS query Qtype=A for + * name “ipv4only.arpa” + * Pass Criteria: + * - N/A + */ + Log("Step 12: ED_1 sends DNS query Qtype=A for name ipv4only.arpa."); + SendQuery(ed1, "ipv4only.arpa", Dns::ResourceRecord::kTypeA); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 13 + * Device: BR_1 (DUT) + * Description (DNS-11.5): Automatically blocks the ipv4only.arpa query and + * responds NXDomain error. + * Pass Criteria: + * - The DUT MUST respond Rcode=3 (NXDomain) with zero answer records (ANCOUNT=0). + * - The DUT MUST NOT send a DNS query to Eth_1. + */ + Log("Step 13: BR_1 blocks the ipv4only.arpa query and responds NXDomain error."); + + /** + * Step 14 + * Device: ED_1 + * Description (DNS-11.5): Harness instructs device to send DNS query with RR type + * in the private use range Qtype=0xFF05 for name “threadgroup2.org”. To do this + * with OT CLI, use the following: udp send 53 -x + * e13c010000010000000000000c74687265616467726f757032036f726700ff050001 “” + * Pass Criteria: + * - N/A + */ + Log("Step 14: ED_1 sends DNS query with RR type in private use range Qtype=0xFF05."); + SendRa(eth1, eth1Addr, guaPrefix); + SendQuery(ed1, kThreadGroup2, kTypePrivateUse); + nexus.AdvanceTime(kStabilizationTime); + + /** + * Step 15 + * Device: BR_1 (DUT) + * Description (DNS-11.5): Automatically responds with the requested record. + * Pass Criteria: + * - The DUT MUST respond Rcode=0 (NoError) with answer record with binary data + * (4 bytes): threadgroup2.org 0xFF05 0x12345678 + */ + Log("Step 15: BR_1 responds with the requested record."); + + nexus.SaveTestInfo(aJsonFileName); +} + +} // namespace Nexus +} // namespace ot + +int main(int argc, char *argv[]) +{ + ot::Nexus::Test_1_4_DNS_TC_5((argc > 2) ? argv[2] : "test_1_4_dns_tc_5.json"); + ot::Nexus::Log("All tests passed"); + return 0; +} diff --git a/tests/nexus/verify_1_4_DNS_TC_5.py b/tests/nexus/verify_1_4_DNS_TC_5.py new file mode 100644 index 000000000..29273d7f5 --- /dev/null +++ b/tests/nexus/verify_1_4_DNS_TC_5.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026, 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. +# + +import sys +import os + +# Add the current directory to sys.path to find verify_utils +CUR_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(CUR_DIR) + +import verify_utils +from pktverify import consts + + +def verify(pv): + # 11.5. [1.4] [CERT] DNS record types and special cases + # + # 11.5.1. Purpose + # To verify that the BR DUT: + # - Can resolve A records + # - Can resolve A records without doing IPv6 AAAA synthesis + # - Can resolve AAAA records + # - Can resolve records contributed by different sources (on-mesh, on-AIL, and upstream DNS server) + # - Supports non-typical record types (RRTypes) for queries + # - Supports queries using RR type 0xFF00-0xFFFE (65280-65534) “Private Use” + # - Blocks the “ipv4only.arpa” query + + pkts = pv.pkts + pv.summary.show() + + # Step 1 + # Device: Eth_1, BR_1, ED_1 + # Description (DNS-11.5): Form topology. Thread network is formed. + # Pass Criteria: + # - N/A + print("Step 1: Form topology. Thread network is formed.") + + # Step 2 + # Device: Eth_1 + # Description (DNS-11.5): Harness instructs device to configure DNS server with records: + # threadgroup1.org AAAA 2002:1234::E1:1, threadgroup1.org A 192.168.217.1, + # threadgroup2.org A 192.168.217.2, threadgroup2.org TYPE65285 \# 4 12345678. + # Note: the record type 0xFF05 is a private range record type, hence it has the + # symbolic name TYPE65285 associated. + # Pass Criteria: + # - N/A + print("Step 2: Eth_1 configures DNS server with records.") + + # Step 3 + # Device: Eth_1 + # Description (DNS-11.5): Harness instructs device to start advertising mDNS services on the AIL: + # Note: the PTR records for services are not shown below; however these are present as usual. + # $ORIGIN local. Service-test-1._infra-test._udp ( SRV 0 0 55551 host-test-eth TXT + # dummy=abcd;thread-test=a49 ) service-test-2._infra-test._udp ( SRV 0 0 55552 + # host-test-eth TXT a=2;thread-test=b50_test ) ) host-test-eth A . + # Pass Criteria: + # - N/A + print("Step 3: Eth_1 starts advertising mDNS services on the AIL.") + + # Step 4 + # Device: ED_1 + # Description (DNS-11.5): Harness instructs device to send a DNS query to the DUT with + # Qtype=AAAA for name “threadgroup1.org”. + # Pass Criteria: + # - N/A + print("Step 4: ED_1 sends a DNS query to the DUT with Qtype=AAAA for name threadgroup1.org.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("threadgroup1.org", "threadgroup1.org.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: consts.DNS_TYPE_AAAA in verify_utils.as_list(p.dns.qry.type)).\ + must_next() + + # Step 5 + # Device: BR_1 (DUT) + # Description (DNS-11.5): Automatically performs the DNS query via the Eth_1 DNS server, + # and provides the answer back to ED_1. + # Pass Criteria: + # - The DUT MUST respond Rcode=0 (NoError) with answer record: threadgroup1.org + # AAAA 2002:1234::E1:1 + print("Step 5: BR_1 performs the DNS query via Eth_1 and provides the answer to ED_1.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 1).\ + filter(lambda p: consts.DNS_TYPE_AAAA in verify_utils.as_list(p.dns.qry.type)).\ + filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\ + filter(lambda p: int(p.dns.count.answers) > 0).\ + filter(lambda p: any(verify_utils.Ipv6Addr("2002:1234::e1:1") == addr + for addr in verify_utils.as_list(p.dns.aaaa))).\ + must_next() + + # Step 6 + # Device: ED_1 + # Description (DNS-11.5): Harness instructs device to send a DNS query to the DUT, with + # Qtype=A for name “threadgroup1.org”. + # Pass Criteria: + # - N/A + print("Step 6: ED_1 sends a DNS query to the DUT with Qtype=A for name threadgroup1.org.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("threadgroup1.org", "threadgroup1.org.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: consts.DNS_TYPE_A in verify_utils.as_list(p.dns.qry.type)).\ + must_next() + + # Step 7 + # Device: BR_1 (DUT) + # Description (DNS-11.5): Automatically performs the DNS query via the Eth_1 DNS server, + # and provides the answer back to ED_1. + # Pass Criteria: + # - The DUT MUST respond Rcode=0 (NoError) with answer record: threadgroup1.org A 192.168.217.1 + print("Step 7: BR_1 performs the DNS query via Eth_1 and provides the answer to ED_1.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 1).\ + filter(lambda p: consts.DNS_TYPE_A in verify_utils.as_list(p.dns.qry.type)).\ + filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\ + filter(lambda p: int(p.dns.count.answers) > 0).\ + filter(lambda p: any("192.168.217.1" == str(a) for a in verify_utils.as_list(p.dns.a))).\ + must_next() + + # Step 8 + # Device: ED_1 + # Description (DNS-11.5): Harness instructs device to send a DNS query to the DUT, with + # Qtype=AAAA for name “threadgroup2.org”. + # Pass Criteria: + # - N/A + print("Step 8: ED_1 sends a DNS query to the DUT with Qtype=AAAA for name threadgroup2.org.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("threadgroup2.org", "threadgroup2.org.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: consts.DNS_TYPE_AAAA in verify_utils.as_list(p.dns.qry.type)).\ + must_next() + + # Step 9 + # Device: BR_1 (DUT) + # Description (DNS-11.5): Automatically performs the DNS query via the Eth_1 DNS server, + # and provides the error answer back to ED_1. Note: this checks that the DUT does not + # perform AAAA synthesis from the A record. + # Pass Criteria: + # - The DUT MUST respond Rcode=0 (NoError) with zero answer records (ANCOUNT=0). + print("Step 9: BR_1 performs the DNS query via Eth_1 and provides the error answer to ED_1.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 1).\ + filter(lambda p: consts.DNS_TYPE_AAAA in verify_utils.as_list(p.dns.qry.type)).\ + filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\ + filter(lambda p: int(p.dns.count.answers) == 0).\ + must_next() + + # Step 10 + # Device: ED_1 + # Description (DNS-11.5): Harness instructs device to send a DNS query, with Qtype=A for + # name “host-test-eth.default.service.arpa”. + # Pass Criteria: + # - N/A + print("Step 10: ED_1 sends a DNS query with Qtype=A for host-test-eth.default.service.arpa.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("host-test-eth.default.service.arpa", "host-test-eth.default.service.arpa.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: consts.DNS_TYPE_A in verify_utils.as_list(p.dns.qry.type)).\ + must_next() + + # Step 11 + # Device: BR_1 (DUT) + # Description (DNS-11.5): Automatically performs the mDNS query on AIL (optionally, if + # still needed), and provides the answer back to ED_1. + # Pass Criteria: + # - The DUT MUST respond Rcode=0 (NoError) with answer record: + # host-test-eth.default.service.arpa A + print("Step 11: BR_1 performs mDNS query on AIL and provides the answer to ED_1.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 1).\ + filter(lambda p: consts.DNS_TYPE_A in verify_utils.as_list(p.dns.qry.type)).\ + filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\ + filter(lambda p: int(p.dns.count.answers) > 0).\ + filter(lambda p: any("192.168.217.100" == str(a) for a in verify_utils.as_list(p.dns.a))).\ + must_next() + + # Step 12 + # Device: ED_1 + # Description (DNS-11.5): Harness instructs device to send DNS query Qtype=A for name “ipv4only.arpa” + # Pass Criteria: + # - N/A + print("Step 12: ED_1 sends DNS query Qtype=A for name ipv4only.arpa.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("ipv4only.arpa", "ipv4only.arpa.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: consts.DNS_TYPE_A in verify_utils.as_list(p.dns.qry.type)).\ + must_next() + + # Step 13 + # Device: BR_1 (DUT) + # Description (DNS-11.5): Automatically blocks the ipv4only.arpa query and responds NXDomain error. + # Pass Criteria: + # - The DUT MUST respond Rcode=3 (NXDomain) with zero answer records (ANCOUNT=0). + # - The DUT MUST NOT send a DNS query to Eth_1. + print("Step 13: BR_1 blocks the ipv4only.arpa query and responds NXDomain error.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 1).\ + filter(lambda p: any(name in ("ipv4only.arpa", "ipv4only.arpa.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NXDOMAIN).\ + filter(lambda p: int(p.dns.count.answers) == 0).\ + must_next() + + # Check that no query was sent to Eth_1 (upstream) + pkts.copy().filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("ipv4only.arpa", "ipv4only.arpa.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: p.eth.dst == "02:00:00:00:00:02").\ + must_not_next() + + # Step 14 + # Device: ED_1 + # Description (DNS-11.5): Harness instructs device to send DNS query with RR type in the + # private use range Qtype=0xFF05 for name “threadgroup2.org”. To do this with OT CLI, + # use the following: udp send 53 -x + # e13c010000010000000000000c74687265616467726f757032036f726700ff050001 “” + # Pass Criteria: + # - N/A + print("Step 14: ED_1 sends DNS query with RR type in private use range Qtype=0xFF05.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 0).\ + filter(lambda p: any(name in ("threadgroup2.org", "threadgroup2.org.") for name in verify_utils.as_list(p.dns.qry.name))).\ + filter(lambda p: any(65285 == t for t in verify_utils.as_list(p.dns.qry.type))).\ + must_next() + + # Step 15 + # Device: BR_1 (DUT) + # Description (DNS-11.5): Automatically responds with the requested record. + # Pass Criteria: + # - The DUT MUST respond Rcode=0 (NoError) with answer record with binary data (4 bytes): + # threadgroup2.org 0xFF05 0x12345678 + print("Step 15: BR_1 responds with the requested record.") + pkts.filter(lambda p: getattr(p, 'dns', None) and p.dns.flags.response == 1).\ + filter(lambda p: any(65285 == t for t in verify_utils.as_list(p.dns.qry.type))).\ + filter(lambda p: p.dns.flags.rcode == consts.DNS_RCODE_NOERROR).\ + filter(lambda p: int(p.dns.count.answers) > 0).\ + filter(lambda p: any("12345678" in str(getattr(p.dns, 'data', '')) for _ in [0])).\ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify) diff --git a/tests/nexus/verify_utils.py b/tests/nexus/verify_utils.py index 65c39ef90..f72d7319f 100644 --- a/tests/nexus/verify_utils.py +++ b/tests/nexus/verify_utils.py @@ -640,6 +640,10 @@ def apply_patches(): layer_fields._layer_containers.add('dns.opt') layer_fields._layer_containers.add('dns.count') layer_fields._LAYER_FIELDS['mdns.nsec'] = layer_fields._list(layer_fields._bytes) + layer_fields._LAYER_FIELDS['dns.a'] = layer_fields._list(layer_fields._str) + layer_fields._LAYER_FIELDS['mdns.a'] = layer_fields._list(layer_fields._str) + layer_fields._LAYER_FIELDS['dns.data'] = layer_fields._list(layer_fields._bytes) + layer_fields._LAYER_FIELDS['mdns.data'] = layer_fields._list(layer_fields._bytes) def which_tshark_patch(): default_path = '/tmp/thread-wireshark/tshark'