[nexus] implement test 1_4_DNS_TC_5 for DNS record types (#12836)

This commit implements the Nexus test specification 1_4_DNS_TC_5 for
DNS record types and special cases in OpenThread 1.4.

The test verifies that the Border Router:
- Can resolve A and AAAA records from upstream DNS servers.
- Does not perform IPv6 AAAA synthesis from A records when not
  specifically requested or configured.
- Can resolve mDNS records on the Adjacent Infrastructure Link (AIL).
- Supports non-typical record types (RRTypes) and "Private Use"
  ranges (0xFF00-0xFFFE).
- Correctly blocks and responds with NXDomain for "ipv4only.arpa"
  queries, ensuring they are not forwarded upstream.

Test Implementation:
- Created test_1_4_DNS_TC_5.cpp to simulate the network topology and
  DNS query/response sequences.
- Created verify_1_4_DNS_TC_5.py to perform packet-level verification
  of the DNS interactions and BR behavior.
- Integrated the new test into the Nexus build and test execution
  scripts.
This commit is contained in:
Jonathan Hui
2026-04-08 20:01:15 -05:00
committed by GitHub
parent 3b84b4c5cb
commit 635d2ffaca
6 changed files with 785 additions and 1 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -117,7 +117,7 @@ void UpstreamDns::StartUpstreamQuery(UpstreamQueryTransaction &aTxn, const Messa
SuccessOrExit(error = aQuery.Read(0, dnsHeader));
message = node.Get<Ip6::Udp>().CloneMessage(aQuery);
message = aQuery.Clone<kSameReservedHeader>();
VerifyOrExit(message != nullptr, error = kErrorNoBufs);
pendingQuery = PendingQuery::Allocate();
+1
View File
@@ -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"
)
+525
View File
@@ -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<MessagePool>().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<Dns::Client>().ResolveAddress(aName, HandleDnsResponse, nullptr));
}
else
{
SuccessOrQuit(aNode.Get<Dns::Client>().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 &eth1 = 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 = &eth1;
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<ThreadNetif>().Up();
eth1.mInfraIf.AddAddress(eth1Addr);
br1.Form();
nexus.AdvanceTime(kFormNetworkTime);
ed1.Join(br1);
nexus.AdvanceTime(kJoinNetworkTime);
br1.Get<BorderRouter::InfraIf>().Init(1, true);
br1.mInfraIf.AddAddress(br1AilAddr);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
SuccessOrQuit(br1.Get<Dns::ServiceDiscovery::Server>().Start());
br1.Get<Dns::ServiceDiscovery::Server>().SetUpstreamQueryEnabled(true);
{
Dns::Client::QueryConfig dnsConfig;
dnsConfig.Clear();
AsCoreType(&dnsConfig.mServerSockAddr.mAddress) = br1.Get<Mle::Mle>().GetMeshLocalEid();
dnsConfig.mServerSockAddr.mPort = 53;
ed1.Get<Dns::Client>().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 <IPv4 address of Eth_1>.
* 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<Dns::Multicast::Core>().SetLocalHostName("host-test-eth"));
SuccessOrQuit(eth1.Get<Dns::Multicast::Core>().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<Dns::Multicast::Core>().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<Dns::Multicast::Core>().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 <IPv4 address of Eth_1>
*/
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 <BR_1 ML-EID address> 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;
}
+253
View File
@@ -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 <IPv4 address of Eth_1>.
# 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 <IPv4 address of Eth_1>
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 <BR_1 ML-EID address> 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)
+4
View File
@@ -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'