mirror of
https://github.com/espressif/openthread.git
synced 2026-09-02 23:30:07 +00:00
[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:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user