/* * 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); AllowLinkBetween(br1, ed1); 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, Node::kAsFed); 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.InitAsIp4Mapped(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; }