[nexus] add BBR-TC-03 test case for mDNS discovery of BBR function (#12724)

This commit implements the BBR-TC-03 test case in the Nexus simulation
framework to verify that a Backbone Router (BBR) function can be
discovered using mDNS and that changes are correctly reflected.

Key implementation details include:
- Implementation of BBR-TC-03 in C++ simulating a topology with two
  Border Routers (BR_1 as initial Primary BBR, BR_2 as Secondary)
  and a non-Thread IPv6 Host used for mDNS queries.
- Use of direct method calls instead of OpenThread public APIs where
  appropriate, following Nexus test conventions.
- Configuration of the test environment including fixed Operational
  Datasets to ensure predictable verification.
- Simulation of various network states:
  - Initial Primary/Secondary BBR discovery.
  - BBR function persistence after device reboot.
  - Role transition (Secondary becoming Primary) when the original
    Primary BBR powers down.
  - Secondary BBR discovery when the original Primary BBR rejoins.
- Addition of a Python verification script to validate mDNS packets on
  the simulated infrastructure link, checking for:
  - Correct mDNS query/response exchanges between Host and BBRs.
  - Presence and format of mandatory TXT records (dn, bb, sq, rv, tv,
    sb, nn, xp, omr).
  - Proper state bitmap (sb) transitions reflecting Primary vs.
    Secondary status.
- Inclusion of the full test specification as inline comments in both
  C++ and Python files, adhering to strict formatting requirements.
- Registration of the new test case in tests/nexus/CMakeLists.txt and
  the default test list in tests/nexus/run_nexus_tests.sh.
- Setting log level to 'note' for improved visibility into state
  transitions.
This commit is contained in:
Jonathan Hui
2026-03-20 18:43:51 -05:00
committed by GitHub
parent 7b5871913a
commit 05e7eb7e7a
8 changed files with 956 additions and 0 deletions
+1
View File
@@ -253,6 +253,7 @@ ot_nexus_test(1_2_MATN_TC_23 "cert;nexus")
ot_nexus_test(1_2_MATN_TC_26 "cert;nexus")
ot_nexus_test(1_2_BBR_TC_1 "cert;nexus")
ot_nexus_test(1_2_BBR_TC_2 "cert;nexus")
ot_nexus_test(1_2_BBR_TC_3 "cert;nexus")
# Misc tests
ot_nexus_test(border_admitter "core;nexus")
+21
View File
@@ -270,6 +270,27 @@ void Core::AddTestVar(const char *aName, const char *aValue)
var->mValue.Clear().Append("%s", aValue);
}
void Core::AddOmrPrefixTestVar(const char *aName, Node &aNode)
{
#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
BorderRouter::RoutingManager &routingManager = aNode.Get<BorderRouter::RoutingManager>();
Ip6::Prefix omrPrefix;
BorderRouter::RoutePreference preference;
String<17> omrPrefixString;
if (routingManager.GetFavoredOmrPrefix(omrPrefix, preference) != kErrorNone)
{
SuccessOrQuit(routingManager.GetOmrPrefix(omrPrefix));
}
omrPrefixString.AppendHexBytes(omrPrefix.GetBytes(), 8);
AddTestVar(aName, omrPrefixString.AsCString());
#else
OT_UNUSED_VARIABLE(aName);
OT_UNUSED_VARIABLE(aNode);
#endif
}
Core::~Core(void) { sInUse = false; }
Node &Core::CreateNode(void)
+1
View File
@@ -66,6 +66,7 @@ public:
void SaveTestInfo(const char *aFilename, Node *aLeaderNode = nullptr);
void AddNetworkKey(const NetworkKey &aKey);
void AddTestVar(const char *aName, const char *aValue);
void AddOmrPrefixTestVar(const char *aName, Node &aNode);
void SendAndVerifyEchoRequest(Node &aSender,
const Ip6::Address &aDestination,
uint16_t aPayloadSize = 0,
+2
View File
@@ -98,6 +98,7 @@ DEFAULT_TESTS=(
"1_1_5_7_3"
"1_1_5_8_2"
"1_1_5_8_3"
"1_2_BBR_TC_3"
"1_1_5_8_4"
"1_1_6_1_1_A"
"1_1_6_1_1_B"
@@ -188,6 +189,7 @@ DEFAULT_TESTS=(
"1_2_MATN_TC_26"
"1_2_BBR_TC_1"
"1_2_BBR_TC_2"
"1_2_BBR_TC_3"
)
# Use provided arguments or the default test list
+538
View File
@@ -0,0 +1,538 @@
/*
* 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 <stdio.h>
#include "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"
namespace ot {
namespace Nexus {
/**
* Time to advance for a node to form a network and become leader, in milliseconds.
*/
static constexpr uint32_t kFormNetworkTime = 10 * 1000;
/**
* Time to advance for a node to join as a router, in milliseconds.
*/
static constexpr uint32_t kAttachToRouterTime = 200 * 1000;
/**
* Time to advance for the network to stabilize, in milliseconds.
*/
static constexpr uint32_t kStabilizationTime = 60 * 1000;
/**
* Time to advance for the BBR selection to complete, in milliseconds.
*/
static constexpr uint32_t kBbrSelectionTime = 10 * 1000;
/**
* Time to wait for BR_2 to become Primary BBR and Leader, in milliseconds.
*/
static constexpr uint32_t kWaitBbrTime = 140 * 1000;
/**
* Infrastructure interface index.
*/
static constexpr uint32_t kInfraIfIndex = 1;
void HandleMdnsBrowse(otInstance *, const otPlatDnssdBrowseResult *) {}
void TestBbrTc3(void)
{
/**
* 5.11.3 BBR-TC-03: mDNS discovery of BBR function
*
* 5.11.3.1 Topology
* - BR_1: BR device initially operating as the Primary BBR and Leader.
* - BR_2: BR device initially operating as a Secondary BBR.
* - Host: Test bed BR device operating as a non-Thread IPv6 host. It is used to send out the mDNS queries.
*
* 5.11.3.2 Purpose & Description
* The purpose of this test case is to verify that a BBR Function (both Primary and Secondary) can be discovered
* using mDNS and that any relevant changes are reflected in the mDNS data sent by the BBR. Also, to verify that
* the mandatory mDNS data fields are present and in the correct format. The BBR Sequence Number updating is not
* verified.
*
* Spec Reference | V1.2 Section
* ---------------|-------------
* BBR Discovery | 5.11.3
*/
Core nexus;
Node &br1 = nexus.CreateNode();
Node &br2 = nexus.CreateNode();
Node &host = nexus.CreateNode();
br1.SetName("BR_1");
br2.SetName("BR_2");
host.SetName("HOST");
br1.Form();
{
MeshCoP::Dataset::Info datasetInfo;
String<17> xpanIdString;
SuccessOrQuit(br1.Get<MeshCoP::ActiveDatasetManager>().Read(datasetInfo));
nexus.AddTestVar("NETWORK_NAME", datasetInfo.mNetworkName.m8);
xpanIdString.AppendHexBytes(datasetInfo.mExtendedPanId.m8, sizeof(datasetInfo.mExtendedPanId.m8));
nexus.AddTestVar("XPAN_ID", xpanIdString.AsCString());
}
nexus.AdvanceTime(0);
Instance::SetLogLevel(kLogLevelNote);
Log("---------------------------------------------------------------------------------------");
Log("Step 0: Topology formation - BR_1");
/**
* Step 0
* - Device: BR_1
* - Description: Topology formation - BR_1
* - Pass Criteria:
* - N/A
*/
br1.AllowList(br2);
br2.AllowList(br1);
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br1.Get<BackboneRouter::Local>().SetEnabled(true);
SuccessOrQuit(br1.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
nexus.AdvanceTime(kFormNetworkTime);
VerifyOrQuit(br1.Get<Mle::Mle>().IsLeader());
Log("---------------------------------------------------------------------------------------");
Log("Step 0b: Topology addition - BR_2");
/**
* Step 0b
* - Device: BR_2
* - Description: Topology addition - BR_2
* - Pass Criteria:
* - N/A
*/
br2.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br2.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br2.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br2.Get<BackboneRouter::Local>().SetEnabled(true);
SuccessOrQuit(br2.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
br2.Join(br1, Node::kAsFtd);
nexus.AdvanceTime(kAttachToRouterTime);
VerifyOrQuit(br2.Get<Mle::Mle>().IsRouter());
host.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
SuccessOrQuit(host.Get<BorderRouter::RoutingManager>().SetEnabled(false));
host.Get<Dns::Multicast::Core>().SetAutoEnableMode(false);
SuccessOrQuit(host.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
nexus.AdvanceTime(kBbrSelectionTime);
nexus.AdvanceTime(kStabilizationTime);
VerifyOrQuit(br1.Get<BackboneRouter::Local>().IsPrimary());
VerifyOrQuit(!br2.Get<BackboneRouter::Local>().IsPrimary());
Log("---------------------------------------------------------------------------------------");
Log("Step 1: Harness instructs the device to send an mDNS query (per P2).");
/**
* Step 1
* - Device: Host
* - Description: Harness instructs the device to send an mDNS query (per P2).
* - Pass Criteria:
* - N/A
*/
{
Dns::Multicast::Core::Browser browser;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
VerifyOrQuit(host.Get<Dns::Multicast::Core>().IsEnabled());
ClearAllBytes(browser);
browser.mServiceType = "_meshcop._udp";
browser.mInfraIfIndex = kInfraIfIndex;
browser.mCallback = HandleMdnsBrowse;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StartBrowser(browser));
nexus.AdvanceTime(kStabilizationTime);
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StopBrowser(browser));
}
nexus.AddOmrPrefixTestVar("OMR_PREFIX_STEP_0", br1);
Log("---------------------------------------------------------------------------------------");
Log("Step 2: Automatically responds to the mDNS query.");
/**
* Step 2
* - Device: BR_1
* - Description: Automatically responds to the mDNS query.
* - Pass Criteria:
* - For DUT = BR_1:
* - The DUT MUST either 1. Unicast an mDNS response message, destined to the UDP source port of the query or to
* port 5353, or 2. Multicast an mDNS response message, destined to UDP port 5353, containing the following TXT
* records in specified format:
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - TXT record key: rv, TXT record value: 1, Value format: String
* - TXT record key: tv, TXT record value: 1.2.0 or higher, Value format: String (5 bytes)
* - TXT record key: sb, TXT record value: Verify Bit 3-4: 0b10, Verify Bit 7: 1, Verify Bit 8: 1, Value format:
* Binary (4 bytes)
* - TXT record key: nn, TXT record value: NetwName1, Value format: String
* - TXT record key: xp, TXT record value: <Equal to XPAN ID>, Value format: Binary (8 bytes)
* - TXT record key: omr, TXT record value: <byte 0x40 followed by 8 bytes of the OMR prefix created by BR_1>,
* Value format: Binary (9 bytes)
* - and OPTIONALLY containing vendor-specific data in the following format:
* - TXT record key: v<anyname>, TXT record value: <any vendor data>, Value format: <any data up to 64 bytes>
* - TXT record key: vo, TXT record value: <vendor OID>, Value format: Binary uint24
* - Above, v<anyname> stands for any TXT record key that starts with a lowercase v character. There may be zero,
* or multiple, of such keys present. If such vendor-specific data is included, the vo key MUST be included as
* well.
* - Also, verify that the complete DNS-SD Service Instance Name ends with the string ._meshcop._udp_.local. and
* has >1 characters before this prefix.
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 3: Automatically responds to the mDNS query.");
/**
* Step 3
* - Device: BR_2
* - Description: Automatically responds to the mDNS query.
* - Pass Criteria:
* - For DUT = BR_2:
* - The DUT MUST either 1. Unicast an mDNS response message, destined to the UDP source port of the query or to
* port 5353, or 2. Multicast an mDNS response message, destined to UDP port 5353 containing the following TXT
* records in specified format:
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - TXT record key: sb, TXT record value: Verify Bit 3-4: 0b10, Verify Bit 7: 1, Verify Bit 8: 0, Value format:
* Binary (4 bytes)
* - rv,tv,nn,xp,omr <as in step 2> <as in step 2>
* - Verify DNS-SD Service Instance Name as in step 2.
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 3b: Harness instructs device to disable the BBR function.");
/**
* Step 3b
* - Device: BR_2
* - Description: Only if DUT=BR_1: Harness instructs device to disable the BBR function. Note: see 5.10.13 step
* 34b for details and reason for this.
* - Pass Criteria:
* - N/A
*/
br2.Get<BackboneRouter::Local>().SetEnabled(false);
Log("---------------------------------------------------------------------------------------");
Log("Step 4: The device must be rebooted (reset); wait until it is back online.");
{
MeshCoP::Dataset::Info datasetInfo;
SuccessOrQuit(br1.Get<MeshCoP::ActiveDatasetManager>().Read(datasetInfo));
br1.Reset();
br1.Get<MeshCoP::ActiveDatasetManager>().SaveLocal(datasetInfo);
}
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br1.Get<BackboneRouter::Local>().SetEnabled(true);
br1.Get<ThreadNetif>().Up();
SuccessOrQuit(br1.Get<Mle::Mle>().Start());
nexus.AdvanceTime(kFormNetworkTime);
VerifyOrQuit(br1.Get<Mle::Mle>().IsLeader());
br1.Get<BackboneRouter::Local>().SetEnabled(true);
nexus.AdvanceTime(kBbrSelectionTime);
nexus.AdvanceTime(kStabilizationTime);
VerifyOrQuit(br1.Get<Mle::Mle>().IsLeader());
VerifyOrQuit(br1.Get<BackboneRouter::Local>().IsPrimary());
SuccessOrQuit(br1.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
nexus.AdvanceTime(kStabilizationTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 5: Harness instructs the device to send an mDNS query.");
/**
* Step 5
* - Device: Host
* - Description: Harness instructs the device to send an mDNS query.
* - Pass Criteria:
* - N/A
*/
{
Dns::Multicast::Core::Browser browser;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
ClearAllBytes(browser);
browser.mServiceType = "_meshcop._udp";
browser.mInfraIfIndex = kInfraIfIndex;
browser.mCallback = HandleMdnsBrowse;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StartBrowser(browser));
nexus.AdvanceTime(kStabilizationTime);
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StopBrowser(browser));
}
nexus.AddOmrPrefixTestVar("OMR_PREFIX_STEP_4", br1);
Log("---------------------------------------------------------------------------------------");
Log("Step 6: Automatically responds to the mDNS query.");
/**
* Step 6
* - Device: BR_1
* - Description: Automatically responds to the mDNS query.
* - Pass Criteria:
* - For DUT = BR_1:
* - The DUT MUST unicast or multicast an mDNS response message containing :
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - rv,tv,sb,nn,xp,omr <as in step 2> <as in step 2>
* - Verify DNS-SD Service Instance Name as in step 2.
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 7: Automatically responds to the mDNS query.");
/**
* Step 7
* - Device: BR_2
* - Description: Automatically responds to the mDNS query.
* - Pass Criteria:
* - For DUT = BR_2:
* - The DUT MUST unicast or multicast an mDNS response message containing :
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - sb <as in step 3> Binary (4 bytes)
* - rv,tv,nn,xp,omr <as in step 2> <as in step 2>
* - Verify DNS-SD Service Instance Name as in step 2.
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 7b: Harness instructs device to enable the BBR function again.");
br2.Get<BackboneRouter::Local>().SetEnabled(true);
nexus.AdvanceTime(kStabilizationTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 8: BR_1 powered down; wait 140s for BR_2 to become Primary BBR and Leader.");
br1.Get<Mle::Mle>().Stop();
nexus.AdvanceTime(kWaitBbrTime);
VerifyOrQuit(br2.Get<Mle::Mle>().IsLeader());
VerifyOrQuit(br2.Get<BackboneRouter::Local>().IsPrimary());
Log("---------------------------------------------------------------------------------------");
Log("Step 9: Harness instructs the device to send an mDNS query.");
/**
* Step 9
* - Device: Host
* - Description: Harness instructs the device to send an mDNS query.
* - Pass Criteria:
* - N/A
*/
{
Dns::Multicast::Core::Browser browser;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
ClearAllBytes(browser);
browser.mServiceType = "_meshcop._udp";
browser.mInfraIfIndex = kInfraIfIndex;
browser.mCallback = HandleMdnsBrowse;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StartBrowser(browser));
nexus.AdvanceTime(kStabilizationTime);
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StopBrowser(browser));
}
nexus.AddOmrPrefixTestVar("OMR_PREFIX_STEP_10", br2);
Log("---------------------------------------------------------------------------------------");
Log("Step 9b: Optionally responds to the mDNS query, as a BR with disabled BBR Function.");
/**
* Step 9b
* - Device: BR_1
* - Description: Optionally responds to the mDNS query, as a BR with disabled BBR Function. Note: the power down
* of step 8 is not actual powering down in the TH context; rather the Thread Interface and thereby BBR Function
* are disabled during the test via the THCI but the backbone interface remains active typically.
* - Pass Criteria:
* - Optionally unicasts or multicasts an mDNS response message containing at least :
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sb, TXT record value: Verify Bit 3-4: 0b00 or 0b01, Verify Bit 7: 0, Verify Bit 8: 0, Value
* format: Binary (4 bytes)
* - (other fields not verified)
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 10: Automatically responds to the mDNS query, as a PBBR.");
/**
* Step 10
* - Device: BR_2
* - Description: Automatically responds to the mDNS query, as a PBBR.
* - Pass Criteria:
* - For DUT = BR_2:
* - The DUT MUST unicast or multicast an mDNS response message containing :
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - sb <as in step 2> Binary (4 bytes)
* - rv,tv,nn,xp,omr <as in step 2> <as in step 2>
* - Verify DNS-SD Service Instance Name as in step 2.
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 11: BR_1 powered up; wait 30s for it to join the Thread Network.");
/**
* Step 11
* - Device: BR_1
* - Description: The device must be powered up Afterwards, the harness waits 10 seconds for it to join the Thread
* Network.
* - Pass Criteria:
* - For DUT = BR_1:
* - The DUT MUST join the Partition of BR_2.
*/
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br1.Get<BackboneRouter::Local>().SetEnabled(true);
SuccessOrQuit(br1.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
br1.Get<ThreadNetif>().Up();
SuccessOrQuit(br1.Get<Mle::Mle>().Start());
nexus.AdvanceTime(kAttachToRouterTime);
VerifyOrQuit(br1.Get<Mle::Mle>().IsRouter());
nexus.AdvanceTime(kBbrSelectionTime);
nexus.AdvanceTime(kStabilizationTime);
// Reset mDNS state to ensure they respond to Step 12 query
SuccessOrQuit(br1.Get<Dns::Multicast::Core>().SetEnabled(false, kInfraIfIndex));
SuccessOrQuit(br2.Get<Dns::Multicast::Core>().SetEnabled(false, kInfraIfIndex));
nexus.AdvanceTime(1000);
SuccessOrQuit(br1.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
SuccessOrQuit(br2.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
nexus.AdvanceTime(kStabilizationTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 12: Harness instructs the device to send an mDNS query.");
/**
* Step 12
* - Device: Host
* - Description: Harness instructs the device to send an mDNS query.
* - Pass Criteria:
* - N/A
*/
{
Dns::Multicast::Core::Browser browser;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().SetEnabled(true, kInfraIfIndex));
ClearAllBytes(browser);
browser.mServiceType = "_meshcop._udp";
browser.mInfraIfIndex = kInfraIfIndex;
browser.mCallback = HandleMdnsBrowse;
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StartBrowser(browser));
nexus.AdvanceTime(kStabilizationTime);
SuccessOrQuit(host.Get<Dns::Multicast::Core>().StopBrowser(browser));
}
nexus.AddOmrPrefixTestVar("OMR_PREFIX_STEP_11", br1);
Log("---------------------------------------------------------------------------------------");
Log("Step 13: Automatically Responds to the mDNS query, as a SBBR.");
/**
* Step 13
* - Device: BR_1
* - Description: Automatically Responds to the mDNS query, as a SBBR.
* - Pass Criteria:
* - For DUT = BR_1:
* - The DUT MUST unicast or multicast an mDNS response message containing :
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - sb <as in step 3> Binary (4 bytes)
* - rv,tv,nn,xp,omr <as in step 2> <as in step 2>
* - Verify DNS-SD Service Instance Name as in step 2.
*/
Log("---------------------------------------------------------------------------------------");
Log("Step 14: Automatically responds to the mDNS query, as the PBBR");
/**
* Step 14
* - Device: BR_2
* - Description: Automatically responds to the mDNS query, as the PBBR
* - Pass Criteria:
* - For DUT = BR_2:
* - The DUT MUST unicast or multicast an mDNS response message containing :
* - TXT record key: dn, TXT record value: TDN, Value format: String
* - TXT record key: bb, TXT record value: 61631 (BB_PORT default), Value format: Binary uint16
* - TXT record key: sq, TXT record value: n/a, Value format: Binary uint8
* - sb <as in step 2> Binary (4 bytes)
* - rv,tv,nn,xp,omr <as in step 2> <as in step 2>
* - Verify DNS-SD Service Instance Name as in step 2.
*/
nexus.SaveTestInfo("test_1_2_BBR_TC_3.json");
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::TestBbrTc3();
printf("All tests passed\n");
return 0;
}
+360
View File
@@ -0,0 +1,360 @@
#!/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
import struct
# 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.bytes import Bytes
from pktverify.null_field import nullField
def verify(pv):
# 5.11.3 BBR-TC-03: mDNS discovery of BBR function
#
# 5.11.3.1 Topology
# - BR_1: BR device initially operating as the Primary BBR and Leader.
# - BR_2: BR device initially operating as a Secondary BBR.
# - Host: Test bed BR device operating as a non-Thread IPv6 host. It is used to
# send out the mDNS queries.
#
# 5.11.3.2 Purpose & Description
# The purpose of this test case is to verify that a BBR Function (both Primary
# and Secondary) can be discovered using mDNS and that any relevant changes
# are reflected in the mDNS data sent by the BBR. Also, to verify that the
# mandatory mDNS data fields are present and in the correct format. The BBR
# Sequence Number updating is not verified.
#
# Spec Reference | V1.2 Section
# ---------------|-------------
# BBR Discovery | 5.11.3
pkts = pv.pkts
pv.summary.show()
# Constants
MDNS_IPV6_ADDR = 'ff02::fb'
MDNS_UDP_PORT = 5353
MESHCOP_SERVICE = '_meshcop._udp.local'
NETWORK_NAME = pv.vars['NETWORK_NAME']
XPAN_ID = Bytes(pv.vars['XPAN_ID'])
# In Nexus, Ethernet MAC addresses are 02:00:00:00:00:<node_id>
BR_1_ETH = '02:00:00:00:00:00'
BR_2_ETH = '02:00:00:00:00:01'
HOST_ETH = '02:00:00:00:00:02'
def is_mdns_response(p):
return p.udp.srcport == MDNS_UDP_PORT
def get_txt_entries(p):
entries = {}
txt_list = []
# Try to get all values if it's a list
layer = None
if hasattr(p, 'mdns') and p.mdns:
layer = p.mdns
elif hasattr(p, 'dns') and p.dns:
layer = p.dns
if layer:
txt_list = layer.txt
if not isinstance(txt_list, list):
txt_list = [txt_list]
for txt in txt_list:
if isinstance(txt, nullField.__class__):
continue
# txt is now a Bytes object (hex representation)
# Find the '=' character (0x3d)
try:
eq_idx = txt.index(0x3d)
key = bytes(txt[:eq_idx]).decode()
value = txt[eq_idx + 1:]
entries[key] = value
except ValueError:
entries[bytes(txt).decode()] = None
return entries
def verify_mdns_response(p, expected_sb_bits, expected_omr_var):
# Verify Service Instance Name
names = []
if hasattr(p, 'mdns') and p.mdns:
names = p.mdns.resp.name
elif hasattr(p, 'dns') and p.dns:
names = p.dns.resp.name
if not isinstance(names, list):
names = [names]
found_meshcop = False
for name in names:
if isinstance(name, nullField.__class__):
continue
if name.endswith('.' + MESHCOP_SERVICE):
instance_name = name[:-len(MESHCOP_SERVICE) - 1]
assert len(instance_name) > 0, f"Invalid instance name: {instance_name}"
found_meshcop = True
break
elif name == MESHCOP_SERVICE:
# This is the service name itself (PTR record name)
pass
assert found_meshcop, f"MeshCoP service instance not found in response names: {names}"
txts = get_txt_entries(p)
# Mandatory TXT records
assert 'dn' in txts, f"dn TXT record missing. Available: {list(txts.keys())}"
assert len(txts['dn']) > 0, "dn TXT record is empty"
assert bytes(txts['rv']).decode() == '1', f"rv TXT record expected 1, got {txts['rv']}"
tv = bytes(txts['tv']).decode()
assert len(tv) >= 5, f"tv TXT record expected >= 5 chars, got {len(tv)} ({tv})"
tv_parts = [int(x) for x in tv.split('.')]
assert tv_parts >= [1, 2, 0], f"tv version expected >= 1.2.0, got {tv}"
# sb: Verify exactly 4 bytes
assert len(txts['sb']) == 4, f"sb TXT record expected 4 bytes, got {len(txts['sb'])}"
sb = struct.unpack('>I', txts['sb'])[0]
is_active = (sb >> 7) & 1
is_primary = (sb >> 8) & 1
for bit, expected_val in expected_sb_bits.items():
if bit == 'ifstate':
actual = (sb >> 3) & 3
if isinstance(expected_val, list):
assert actual in expected_val, f"ifstate bitmask expected one of {expected_val}, got {actual} (sb={sb:08x})"
else:
assert actual == expected_val, f"ifstate bitmask expected {expected_val}, got {actual} (sb={sb:08x})"
elif bit == 'active':
actual = (sb >> 7) & 1
if isinstance(expected_val, list):
assert actual in expected_val, f"active bitmask expected one of {expected_val}, got {actual} (sb={sb:08x})"
else:
assert actual == expected_val, f"active bitmask expected {expected_val}, got {actual} (sb={sb:08x})"
elif bit == 'primary':
actual = (sb >> 8) & 1
if isinstance(expected_val, list):
assert actual in expected_val, f"primary bitmask expected one of {expected_val}, got {actual} (sb={sb:08x})"
else:
assert actual == expected_val, f"primary bitmask expected {expected_val}, got {actual} (sb={sb:08x})"
# bb: 61631 (0xF0BF) - Only present if BBR is active
# sq: Binary uint8 - Only present if BBR is active
if is_active:
assert 'bb' in txts, f"bb TXT record missing but BBR is active (sb={sb:08x})"
bb = struct.unpack('>H', txts['bb'])[0]
assert bb == 61631, f"bb TXT record expected 61631, got {bb}"
assert 'sq' in txts, "sq TXT record missing but BBR is active"
assert len(txts['sq']) == 1, f"sq TXT record expected 1 byte, got {len(txts['sq'])}"
else:
# Note: Step 9b says bb is mandatory even if active=0 in that specific case
# but generally it's tied to activity.
pass
# nn: NetwName1
nn = bytes(txts['nn']).decode()
assert nn == NETWORK_NAME, f"nn TXT record expected {NETWORK_NAME}, got {nn}"
# xp: Extended PAN ID
xp = txts['xp']
assert xp == XPAN_ID, f"xp TXT record expected {XPAN_ID}, got {xp}"
# omr: byte 0x40 followed by 8 bytes OMR prefix
# Only present if BBR is active (as it's published in Network Data)
if is_active:
assert 'omr' in txts, f"omr TXT record missing but BBR is active (sb={sb:08x})"
omr = txts['omr']
assert len(omr) == 9, f"omr TXT record expected 9 bytes, got {len(omr)} ({omr})"
assert omr[0] == 0x40, f"omr TXT record first byte expected 0x40, got {omr[0]:02x}"
# All BRs in the same Thread mesh will have the same OMR prefix at a given step.
expected_omr = Bytes(pv.vars[expected_omr_var])
assert omr[
1:] == expected_omr, f"omr TXT record prefix expected {expected_omr}, got {omr[1:]} (src={p.eth.src})"
# Vendor specific data check
for key in txts:
if key.startswith('v') and key not in ['rv', 'tv', 'vo', 'vn']:
assert 'vo' in txts, f"vo TXT record missing but vendor-specific record '{key}' present"
assert len(txts['vo']) == 3, f"vo TXT record expected 3 bytes (Binary uint24), got {len(txts['vo'])}"
if key == 'mn':
# 'mn' is also vendor-specific but often present in Nexus without 'vo'
pass
# Step 1: Host sends an mDNS query (per P2).
print("Step 1: Host sends an mDNS query.")
pkts.filter_eth_src(HOST_ETH).\
filter_ipv6_dst(MDNS_IPV6_ADDR).\
filter(lambda p: p.udp.dstport == MDNS_UDP_PORT).\
must_next()
# Step 2 & 3: BR_1 and BR_2 automatically respond.
print("Step 2: BR_1 automatically responds to the mDNS query.")
p1 = pkts.copy().\
filter_eth_src(BR_1_ETH).\
filter(is_mdns_response).\
must_next()
verify_mdns_response(p1, {
'ifstate': [1, 2],
'active': [0, 1],
'primary': [0, 1]
},
expected_omr_var='OMR_PREFIX_STEP_0')
print("Step 3: BR_2 automatically responds to the mDNS query.")
p2 = pkts.copy().\
filter_eth_src(BR_2_ETH).\
filter(is_mdns_response).\
must_next()
verify_mdns_response(p2, {'ifstate': 2, 'active': 1, 'primary': 0}, expected_omr_var='OMR_PREFIX_STEP_0')
# Step 4: The device must be rebooted (reset); wait until it is back online.
print("Step 4: BR_1 is rebooted and remains Leader.")
# Step 5: Harness instructs the device to send an mDNS query.
print("Step 5: Host sends an mDNS query.")
# Skip all packets until the next query which should be around Step 5
pkts.filter_eth_src(HOST_ETH).\
filter_ipv6_dst(MDNS_IPV6_ADDR).\
filter(lambda p: p.udp.dstport == MDNS_UDP_PORT).\
filter(lambda p: p.number > max(p1.number, p2.number)).\
must_next()
q_number = pkts.last().number
# Step 6: Automatically responds to the mDNS query.
print("Step 6: BR_1 automatically responds to the mDNS query.")
p1 = pkts.copy().\
filter_eth_src(BR_1_ETH).\
filter(is_mdns_response).\
filter(lambda p: p.number > q_number).\
must_next()
verify_mdns_response(p1, {
'ifstate': [1, 2],
'active': [0, 1],
'primary': [0, 1]
},
expected_omr_var='OMR_PREFIX_STEP_4')
print("Step 7: BR_2 automatically responds to the mDNS query.")
p2 = pkts.copy().\
filter_eth_src(BR_2_ETH).\
filter(is_mdns_response).\
filter(lambda p: p.number > q_number).\
must_next()
# BBR is disabled on BR_2 in Step 3b (only if DUT=BR_1)
verify_mdns_response(p2, {'ifstate': 2, 'active': 0, 'primary': 0}, expected_omr_var='OMR_PREFIX_STEP_0')
# Step 8: The device must be powered down; wait for BR_2 to become Primary BBR and Leader.
print("Step 8: BR_1 is powered down; BR_2 becomes Primary BBR.")
# Step 9: Harness instructs the device to send an mDNS query.
print("Step 9: Host sends an mDNS query.")
pkts.filter_eth_src(HOST_ETH).\
filter_ipv6_dst(MDNS_IPV6_ADDR).\
filter(lambda p: p.udp.dstport == MDNS_UDP_PORT).\
filter(lambda p: p.number > max(p1.number, p2.number)).\
must_next()
q_number = pkts.last().number
# Step 9b: Optionally responds to the mDNS query, as a BR with disabled BBR Function.
print("Step 9b: Optionally BR_1 responds as disabled BBR.")
p = pkts.copy().\
filter_eth_src(BR_1_ETH).\
filter(is_mdns_response).\
filter(lambda p: p.number > q_number).\
next()
if p:
# Step 9b Pass Criteria: Verify Bit 3-4: 0b00 or 0b01, Verify Bit 7: 0, Verify Bit 8: 0
# Also contains dn record. bb record is optional if active=0.
txts = get_txt_entries(p)
assert 'dn' in txts, "dn TXT record missing in Step 9b"
if 'bb' in txts:
bb = struct.unpack('>H', txts['bb'])[0]
assert bb == 61631, f"bb TXT record expected 61631, got {bb}"
sb = struct.unpack('>I', txts['sb'])[0]
assert (sb >> 3) & 3 in [0, 1], f"sb ifstate bitmask expected 0 or 1, got {(sb >> 3) & 3} (sb={sb:08x})"
assert (sb >> 7) & 1 == 0, f"sb active bitmask expected 0, got {(sb >> 7) & 1} (sb={sb:08x})"
assert (sb >> 8) & 1 == 0, f"sb primary bitmask expected 0, got {(sb >> 8) & 1} (sb={sb:08x})"
# Step 10: Automatically responds to the mDNS query, as a PBBR (BR_2).
print("Step 10: BR_2 automatically responds to the mDNS query as PBBR.")
# We might see stale SBBR responses due to timing, so we look for the PBBR one.
p2 = pkts.copy().\
filter_eth_src(BR_2_ETH).\
filter(is_mdns_response).\
filter(lambda p: p.number > q_number).\
filter(lambda p: (struct.unpack('>I', get_txt_entries(p)['sb'])[0] >> 8) & 1 == 1).\
must_next()
verify_mdns_response(p2, {'ifstate': 2, 'active': 1, 'primary': 1}, expected_omr_var='OMR_PREFIX_STEP_10')
# Step 11: The device must be powered up; BR_1 joins BR_2.
print("Step 11: BR_1 is powered up and joins BR_2.")
# Step 12: Harness instructs the device to send an mDNS query.
print("Step 12: Host sends an mDNS query.")
pkts.filter_eth_src(HOST_ETH).\
filter_ipv6_dst(MDNS_IPV6_ADDR).\
filter(lambda p: p.udp.dstport == MDNS_UDP_PORT).\
filter(lambda p: p.number > p2.number).\
must_next()
q_number = pkts.last().number
# Step 13 & 14: BR_1 and BR_2 automatically respond.
print("Step 13: BR_1 automatically responds to the mDNS query as SBBR.")
p1 = pkts.copy().\
filter_eth_src(BR_1_ETH).\
filter(is_mdns_response).\
filter(lambda p: p.number > q_number).\
filter(lambda p: (struct.unpack('>I', get_txt_entries(p)['sb'])[0] >> 7) & 1 == 1).\
must_next()
verify_mdns_response(p1, {'ifstate': 2, 'active': 1, 'primary': 0}, expected_omr_var='OMR_PREFIX_STEP_11')
print("Step 14: BR_2 automatically responds to the mDNS query as PBBR.")
p2 = pkts.copy().\
filter_eth_src(BR_2_ETH).\
filter(is_mdns_response).\
filter(lambda p: p.number > q_number).\
filter(lambda p: (struct.unpack('>I', get_txt_entries(p)['sb'])[0] >> 8) & 1 == 1).\
must_next()
verify_mdns_response(p2, {'ifstate': 2, 'active': 1, 'primary': 1}, expected_omr_var='OMR_PREFIX_STEP_10')
if __name__ == '__main__':
verify_utils.run_main(verify)
+12
View File
@@ -480,6 +480,18 @@ def run_main(verify_func):
name = pv.test_info.get_node_name(int(node_id))
pv.add_vars(**{f'{name}_CHANNEL': int(channel)})
# Add OMR prefix variables
omr_prefixes = data.get('omr_prefixes', {})
for node_id, omr_prefix in omr_prefixes.items():
if omr_prefix:
name = pv.test_info.get_node_name(int(node_id))
pv.add_vars(**{f'{name}_OMR_PREFIX': omr_prefix})
# If all valid OMR prefixes are the same, add a generic OMR_PREFIX variable
valid_omr_prefixes = [p for p in omr_prefixes.values() if p]
if len(valid_omr_prefixes) > 0 and all(p == valid_omr_prefixes[0] for p in valid_omr_prefixes):
pv.add_vars(OMR_PREFIX=valid_omr_prefixes[0])
verify_func(pv)
print("Verification PASSED")
except Exception as e:
@@ -672,6 +672,24 @@ _LAYER_FIELDS = {
# DNS
'dns.resp.ttl': _auto,
'dns.flags.response': _auto,
'dns.count.answers': _auto,
'dns.resp.name': _list(_str),
'dns.resp.type': _list(_auto),
'dns.txt': _list(_bytes),
'dns.srv.port': _list(_auto),
'dns.srv.target': _list(_str),
'dns.ptr.domain_name': _list(_str),
# MDNS
'mdns.resp.ttl': _auto,
'mdns.flags.response': _auto,
'mdns.count.answers': _auto,
'mdns.resp.name': _list(_str),
'mdns.resp.type': _list(_auto),
'mdns.txt': _list(_bytes),
'mdns.srv.port': _list(_auto),
'mdns.srv.target': _list(_str),
'mdns.ptr.domain_name': _list(_str),
}
_layer_containers = set()
@@ -733,6 +751,9 @@ def get_layer_field(packet: RawPacket, field_uri: str) -> Any:
continue
layer = layers[layer_depth]
v = layer.get_field(field_uri)
if v is None and layer_name == 'mdns':
# Try dns prefix for mdns layer
v = layer.get_field('dns' + field_uri[4:])
if v is not None:
try:
v = _LAYER_FIELDS[field_uri](v)