[nexus] add DBR-TC-01 test case for single BR reachability (#12730)

This commit implements the 1_3_DBR_TC_1 nexus test case to verify
bi-directional reachability between Thread and infrastructure
devices with a single Border Router.

Key changes:
- Implement test_1_3_DBR_TC_1.cpp for step-by-step execution logic.
- Implement verify_1_3_DBR_TC_1.py for pcap-based verification.
- Enhance Nexus InfraIf platform to support Deinit() for AIL
  disconnection.
- Update InfraIf::Receive() to check initialization status and
  ignore kErrorDrop from SendRaw() to handle legitimate packet
  drops in the stack.
- Register the new test case in CMakeLists.txt and the default
  nexus test run script.

The test verifies:
- Automatic OMR and on-link prefix registration in Network Data.
- Periodic ND Router Advertisement multicast on the infrastructure
  link with correct PIO/RIO options and Extended PAN ID derivation.
- Bi-directional reachability between Thread End Devices (OMR) and
  Infrastructure Hosts (ULA).
- Strict enforcement of non-forwarding rules for link-local and
  Mesh-Local EID traffic between the Thread and infrastructure
  networks.
This commit is contained in:
Jonathan Hui
2026-03-21 20:37:13 -05:00
committed by GitHub
parent d64bdee1bb
commit b70b3ccb6a
8 changed files with 743 additions and 9 deletions
+1
View File
@@ -254,6 +254,7 @@ 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")
ot_nexus_test(1_3_DBR_TC_1 "cert;nexus")
# Misc tests
ot_nexus_test(border_admitter "core;nexus")
+3 -3
View File
@@ -36,11 +36,11 @@ die()
cd "$(dirname "$0")" || die "cd failed"
cd ../.. || die "cd failed"
top_srcdir=$(pwd)
if [ -n "${top_builddir}" ]; then
top_srcdir=$(pwd)
mkdir -p "${top_builddir}"
else
top_srcdir=.
top_builddir=.
fi
@@ -63,7 +63,7 @@ cmake -GNinja -DOT_PLATFORM=nexus -DOT_COMPILE_WARNING_AS_ERROR=ON \
-DOT_MULTIPLE_INSTANCE=ON \
-DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
-DOT_15_4=${fifteenfour} -DOT_TREL=${trel} \
-DOT_PROJECT_CONFIG=../tests/nexus/openthread-core-nexus-config.h \
-DOT_PROJECT_CONFIG="${top_srcdir}/tests/nexus/openthread-core-nexus-config.h" \
"${top_srcdir}" || die
ninja || die
+1 -1
View File
@@ -439,7 +439,7 @@ void InfraIf::Receive(Node &aSrcNode, Message &aMessage)
messagePtr->SetOrigin(Message::kOriginHostUntrusted);
messagePtr->SetLoopbackToHostAllowed(false);
SuccessOrQuit(node.Get<Ip6::Ip6>().SendRaw(messagePtr.PassOwnership()));
IgnoreError(node.Get<Ip6::Ip6>().SendRaw(messagePtr.PassOwnership()));
}
exit:
+5
View File
@@ -45,6 +45,11 @@ public:
InfraIf(void);
void Init(Node &aNode);
void Deinit(void)
{
mIfIndex = 0;
mAddresses.Clear();
}
bool IsInitialized(void) const { return mIfIndex != 0; }
+1
View File
@@ -190,6 +190,7 @@ DEFAULT_TESTS=(
"1_2_BBR_TC_1"
"1_2_BBR_TC_2"
"1_2_BBR_TC_3"
"1_3_DBR_TC_1"
)
# Use provided arguments or the default test list
+329
View File
@@ -0,0 +1,329 @@
/*
* 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 = 13 * 1000;
/**
* Time to advance for a node to join as a child and upgrade to a router, in milliseconds.
*/
static constexpr uint32_t kJoinNetworkTime = 200 * 1000;
/**
* Time to advance for the BR to perform automatic actions (RA, Network Data), in milliseconds.
*/
static constexpr uint32_t kBrActionTime = 20 * 1000;
/**
* Time to advance for the ping response, in milliseconds.
*/
static constexpr uint32_t kPingResponseTime = 5 * 1000;
/**
* Infrastructure interface index.
*/
static constexpr uint32_t kInfraIfIndex = 1;
/**
* Echo Request identifier.
*/
static constexpr uint16_t kEchoIdentifier = 0x1234;
/**
* Echo Request payload size.
*/
static constexpr uint16_t kEchoPayloadSize = 10;
void Test_1_3_DBR_TC_1(void)
{
/**
* 1.1. [1.3] [CERT] Reachability - Single BR / Single Infrastructure Link
*
* 1.1.1. Purpose
* To test the following situation
* 1. Bi-directional reachability between Thread devices and infrastructure devices
* 2. No existing IPv6 infrastructure
* 3. Single BR
* 4. Verify that the BR DUT does not forward IPv6 packets that must not be forwarded (e.g. link-local)
* 5. Verify that the BR DUT sends the right ND RA messages on AIL, and includes the right prefixes in Network Data
*
* 1.1.2. Topology
* - BR 1 (DUT) - Thread Border Router and Leader
* - ED 1 - Test bed device operating as a Thread End Device
* - Eth 1 - Test bed border router device on an Adjacent Infrastructure Link
*
* Spec Reference | V1.1 Section | V1.3.0 Section
* -----------------|--------------|---------------
* Reachability | N/A | 1.3
*/
Core nexus;
Node &br1 = nexus.CreateNode();
Node &ed1 = nexus.CreateNode();
Node &eth1 = nexus.CreateNode();
br1.SetName("BR_1");
ed1.SetName("ED_1");
eth1.SetName("Eth_1");
ed1.mInfraIf.Deinit();
nexus.AdvanceTime(0);
Instance::SetLogLevel(kLogLevelNote);
Log("---------------------------------------------------------------------------------------");
Log("Step 1: Device: Eth 1, ED 1 Description (DBR-1.1): Enable.");
/**
* Step 1
* - Device: Eth 1, ED 1
* - Description (DBR-1.1): Enable.
* - Pass Criteria: N/A
*/
eth1.mInfraIf.Init(eth1);
Log("---------------------------------------------------------------------------------------");
Log("Step 2: Device: BR 1 (DUT) Description (DBR-1.1): Enable.");
/**
* Step 2
* - Device: BR 1 (DUT)
* - Description (DBR-1.1): Enable.
* - Pass Criteria: N/A
*/
br1.Form();
nexus.AdvanceTime(kFormNetworkTime);
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
ed1.Join(br1, Node::kAsFed);
nexus.AdvanceTime(kJoinNetworkTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 3: Device: BR 1 (DUT) Description (DBR-1.1): Automatically registers itself as a Border Router.");
/**
* Step 3
* - Device: BR 1 (DUT)
* - Description (DBR-1.1): Automatically registers itself as a Border Router in the Thread Network Data. The DUT:
* adds an OMR prefix OMR_1 to its Thread Network Data; adds an on-link ULA prefix PRE_1 on the adjacent
* infrastructure link (AIL); adds an external route in the Thread Network Data based on PRE_1.
* - Pass Criteria:
* - Note: pass criteria are identical to DBR-1.3 step 2.
* - The DUT internally registers an OMR Prefix OMR_1 in the Thread Network Data.
* - The DUT MUST send a multicast MLE Data Response with Thread Network Data containing at least two Prefix TLVs:
* - 1. Prefix TLV: OMR prefix OMR_1.
* - MUST include a Border Router sub-TLV. Flags in the Border Router TLV MUST be:
* - P_preference = 11 (Low)
* - P_default=true
* - P_stable=true
* - P_on_mesh=true
* - P_preferred=true
* - P_slaac = true
* - P_dhcp=false
* - P_dp=false
* - 2. Prefix TLV: ULA prefix fc00::/7. (This is used as the shortened version of PRE_1)
* - Includes Has Route sub-TLV
* - OMR 1 MUST be 64 bits long and start with 0xFD.
*/
nexus.AdvanceTime(kBrActionTime);
Ip6::Prefix omrPrefix;
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().GetOmrPrefix(omrPrefix));
nexus.AddTestVar("OMR_PREFIX", omrPrefix.ToString().AsCString());
Ip6::Prefix pre1Prefix;
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(pre1Prefix));
nexus.AddTestVar("PRE_1_PREFIX", pre1Prefix.ToString().AsCString());
MeshCoP::Dataset::Info datasetInfo;
SuccessOrQuit(br1.Get<MeshCoP::ActiveDatasetManager>().Read(datasetInfo));
const MeshCoP::ExtendedPanId &extPanId = datasetInfo.Get<MeshCoP::Dataset::kExtendedPanId>();
nexus.AddTestVar("EXT_PAN_ID_VAR", extPanId.ToString().AsCString());
Log("---------------------------------------------------------------------------------------");
Log("Step 4: Device: BR 1 (DUT) Description (DBR-1.1): Automatically multicasts ND RAs on AIL.");
/**
* Step 4
* - Device: BR 1 (DUT)
* - Description (DBR-1.1): Automatically multicasts ND RAs on Adjacent Infrastructure Link.
* - Pass Criteria:
* - The DUT MUST multicast ND RAs.
* - IPv6 destination MUST be ff02::1
* - M bit and O bit MUST be '0'
* - MUST contain "Router Lifetime" = 0. (indicating it's not a default router)
* - MUST contain a Prefix Information Option (PIO) with a ULA prefix PRE_1.
* - A bit MUST be '1'
* - MUST contain a Route Information Option (RIO) with the OMR prefix OMR 1.
* - PRE_1 MUST be 64 bits long and start with 0xFD and MUST differ from OMR_1.
* - PRE_1 MUST contain the Extended PAN ID as follows:
* - Global ID equals the 40 most significant bits of the Extended PAN ID
* - Subnet ID equals the 16 least significant bits of the Extended PAN ID
*/
// Time already advanced in Step 3 is enough for RAs.
Log("---------------------------------------------------------------------------------------");
Log("Step 5: Device: Eth 1 Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED 1.");
/**
* Step 5
* - Device: Eth 1
* - Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED 1. 1. IPv6 Source: Eth_1
* ULA 2. IPv6 Destination: ED_1 OMR address
* - Pass Criteria:
* - Eth 1 receives an ICMPv6 Echo Reply from ED_1.
* - 1. IPv6 Source: ED_1 OMR address
* - 2. IPv6 Destination: Eth_1 ULA
*/
const Ip6::Address &eth1Ula = eth1.mInfraIf.FindMatchingAddress(pre1Prefix.ToString().AsCString());
const Ip6::Address &ed1Omr = ed1.FindMatchingAddress(omrPrefix.ToString().AsCString());
nexus.AddTestVar("ETH_1_ULA_ADDR", eth1Ula.ToString().AsCString());
nexus.AddTestVar("ED_1_OMR_ADDR", ed1Omr.ToString().AsCString());
eth1.mInfraIf.SendEchoRequest(eth1Ula, ed1Omr, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 6: Device: ED 1 Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1.");
/**
* Step 6
* - Device: ED 1
* - Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1. 1. IPv6 Source: ED_1 OMR
* address 2. IPv6 Destination: Eth_1 ULA
* - Pass Criteria:
* - ED 1 receives an ICMPv6 Echo Reply from Eth 1.
* - 1. IPv6 Source: Eth_1 ULA
* - 2. IPv6 Destination: ED_1 OMR address
*/
ed1.SendEchoRequest(eth1Ula, kEchoIdentifier, kEchoPayloadSize, 64, &ed1Omr);
nexus.AdvanceTime(kPingResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 7: Device: Eth 1 Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED 1.");
/**
* Step 7
* - Device: Eth 1
* - Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED_1. 1. IPv6 Source: Eth_1
* link-local 2. IPv6 Destination: ED 1 OMR address
* - Pass Criteria:
* - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Thread network.
*/
const Ip6::Address &eth1Ll = eth1.mInfraIf.FindMatchingAddress("fe80::/10");
nexus.AddTestVar("ETH_1_LL_ADDR", eth1Ll.ToString().AsCString());
eth1.mInfraIf.SendEchoRequest(eth1Ll, ed1Omr, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 8: Device: ED 1 Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1.");
/**
* Step 8
* - Device: ED 1
* - Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1. 1. IPv6 Source: ED 1
* link-local 2. IPv6 Destination: Eth_1 ULA
* - Pass Criteria:
* - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Adjacent Infrastructure Network.
*/
const Ip6::Address &ed1Ll = ed1.FindMatchingAddress("fe80::/10");
nexus.AddTestVar("ED_1_LL_ADDR", ed1Ll.ToString().AsCString());
ed1.SendEchoRequest(eth1Ula, kEchoIdentifier, kEchoPayloadSize, 64, &ed1Ll);
nexus.AdvanceTime(kPingResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 9: Device: Eth 1 Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED 1.");
/**
* Step 9
* - Device: Eth 1
* - Description (DBR-1.1): Harness instructs device to add a route to the Thread Network's mesh-local prefix via
* the infrastructure link. Harness then instructs device to send ICMPv6 Echo Request to ED 1. 1. IPv6 Source: Eth 1
* ULA 2. IPv6 Destination: ED_1-ML-EID
* - Pass Criteria:
* - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Thread network.
*/
const Ip6::Address &ed1Mleid = ed1.Get<Mle::Mle>().GetMeshLocalEid();
nexus.AddTestVar("ED_1_MLEID_ADDR", ed1Mleid.ToString().AsCString());
eth1.mInfraIf.SendEchoRequest(eth1Ula, ed1Mleid, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingResponseTime);
Log("---------------------------------------------------------------------------------------");
Log("Step 10: Device: ED 1 Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1.");
/**
* Step 10
* - Device: ED 1
* - Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1. 1. IPv6 Source: ED 1
* ML-EID
* 2. IPv6 Destination: Eth 1 ULA
* - Pass Criteria:
* - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Adjacent Infrastructure Network.
*/
ed1.SendEchoRequest(eth1Ula, kEchoIdentifier, kEchoPayloadSize, 64, &ed1Mleid);
nexus.AdvanceTime(kPingResponseTime);
nexus.SaveTestInfo("test_1_3_DBR_TC_1.json");
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::Test_1_3_DBR_TC_1();
printf("All tests passed\n");
return 0;
}
+386
View File
@@ -0,0 +1,386 @@
#!/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
from pktverify.addrs import Ipv6Addr
ULA_PREFIX_START_BYTE = 0xfd
# Step 3 BR constants
BR_PREFERENCE_LOW = 3
BR_FLAG_R_FALSE = 0
BR_FLAG_O_TRUE = 1
BR_FLAG_P_TRUE = 1
BR_FLAG_S_TRUE = 1
BR_FLAG_D_FALSE = 0
BR_FLAG_DP_FALSE = 0
# Step 4 RA constants
ICMPV6_TYPE_ROUTER_ADVERTISEMENT = 134
RA_FLAG_M_FALSE = 0
RA_FLAG_O_FALSE = 0
RA_ROUTER_LIFETIME_ZERO = 0
ICMPV6_OPT_TYPE_PIO = 3
ICMPV6_OPT_TYPE_RIO = 24
PIO_FLAG_A_TRUE = 1
# EXT_PAN_ID mapping offsets and lengths
EXT_PAN_ID_GLOBAL_ID_OFFSET = 1
EXT_PAN_ID_GLOBAL_ID_LEN = 5
EXT_PAN_ID_SUBNET_ID_OFFSET = 6
EXT_PAN_ID_SUBNET_ID_LEN = 2
def verify(pv):
# 1.1. [1.3] [CERT] Reachability - Single BR / Single Infrastructure Link
#
# 1.1.1. Purpose
# To test the following situation
# 1. Bi-directional reachability between Thread devices and infrastructure devices
# 2. No existing IPv6 infrastructure
# 3. Single BR
# 4. Verify that the BR DUT does not forward IPv6 packets that must not be forwarded (e.g. link-local)
# 5. Verify that the BR DUT sends the right ND RA messages on AIL, and includes the right prefixes in Network Data
#
# 1.1.2. Topology
# - BR 1 (DUT) - Thread Border Router and Leader
# - ED 1 - Test bed device operating as a Thread End Device
# - Eth 1 - Test bed border router device on an Adjacent Infrastructure Link
#
# Spec Reference | V1.3.0 Section
# ---------------|---------------
# Reachability | 1.3
pkts = pv.pkts
pv.summary.show()
BR_1 = pv.vars['BR_1']
ED_1 = pv.vars['ED_1']
Eth_1 = pv.vars['Eth_1']
OMR_PREFIX = Ipv6Addr(pv.vars['OMR_PREFIX'].split('/')[0])
OMR_PREFIX_LEN = int(pv.vars['OMR_PREFIX'].split('/')[1])
PRE_1_PREFIX = Ipv6Addr(pv.vars['PRE_1_PREFIX'].split('/')[0])
PRE_1_PREFIX_LEN = int(pv.vars['PRE_1_PREFIX'].split('/')[1])
EXT_PAN_ID = pv.vars['EXT_PAN_ID_VAR']
ETH_1_ULA = Ipv6Addr(pv.vars['ETH_1_ULA_ADDR'])
ED_1_OMR = Ipv6Addr(pv.vars['ED_1_OMR_ADDR'])
ED_1_MLEID = Ipv6Addr(pv.vars['ED_1_MLEID_ADDR'])
def as_list(x):
return x if isinstance(x, list) else [x]
# Step 1
# Device: Eth 1, ED 1
# Description (DBR-1.1): Enable.
# Pass Criteria: N/A
print("Step 1: Enable Eth 1, ED 1.")
# Step 2
# Device: BR 1 (DUT)
# Description (DBR-1.1): Enable.
# Pass Criteria: N/A
print("Step 2: Enable BR 1 (DUT).")
# Step 3
# Device: BR 1 (DUT)
# Description (DBR-1.1): Automatically registers itself as a Border Router in the Thread Network Data.
# The DUT: adds an OMR prefix OMR_1 to its Thread Network Data; adds an on-link ULA prefix PRE_1 on the
# adjacent infrastructure link (AIL); adds an external route in the Thread Network Data based on PRE_1.
# Pass Criteria:
# - Note: pass criteria are identical to DBR-1.3 step 2.
# - The DUT internally registers an OMR Prefix OMR_1 in the Thread Network Data.
# - The DUT MUST send a multicast MLE Data Response with Thread Network Data containing at least two
# Prefix TLVs:
# - 1. Prefix TLV: OMR prefix OMR_1.
# - MUST include a Border Router sub-TLV. Flags in the Border Router TLV MUST be:
# - P_preference = 11 (Low)
# - P_default=true (Note: OpenThread currently sets this to false if no infra default route)
# - P_stable=true
# - P_on_mesh=true
# - P_preferred=true
# - P_slaac = true
# - P_dhcp=false
# - P_dp=false
# - 2. Prefix TLV: ULA prefix fc00::/7. (This is used as the shortened version of PRE_1)
# - Includes Has Route sub-TLV
# - OMR 1 MUST be 64 bits long and start with 0xFD.
print("Step 3: BR 1 (DUT) registers itself as a Border Router.")
def check_step3(p):
try:
prefixes = as_list(p.thread_nwd.tlv.prefix)
types = as_list(p.thread_nwd.tlv.type)
stables = as_list(p.thread_nwd.tlv.stable)
except AttributeError:
return False
# 1. Prefix TLV for OMR_1
try:
omr_idx = prefixes.index(OMR_PREFIX)
except ValueError:
return False
# Check OMR prefix properties: 64 bits long and starts with ULA_PREFIX_START_BYTE
if OMR_PREFIX_LEN != 64 or OMR_PREFIX[0] != ULA_PREFIX_START_BYTE:
return False
# Find the NWD_PREFIX_TLV entry index corresponding to omr_idx
prefix_indices = [i for i, t in enumerate(types) if t == consts.NWD_PREFIX_TLV]
if omr_idx >= len(prefix_indices):
return False
t_idx = prefix_indices[omr_idx]
if stables[t_idx] != 1:
return False
# Flags for OMR prefix
# Note: We expect P_default (r flag) to be 0 for now to match OpenThread behavior
try:
if not (as_list(p.thread_nwd.tlv.border_router.pref)[0] == BR_PREFERENCE_LOW and \
as_list(p.thread_nwd.tlv.border_router.flag.r)[0] == BR_FLAG_R_FALSE and \
as_list(p.thread_nwd.tlv.border_router.flag.o)[0] == BR_FLAG_O_TRUE and \
as_list(p.thread_nwd.tlv.border_router.flag.p)[0] == BR_FLAG_P_TRUE and \
as_list(p.thread_nwd.tlv.border_router.flag.s)[0] == BR_FLAG_S_TRUE and \
as_list(p.thread_nwd.tlv.border_router.flag.d)[0] == BR_FLAG_D_FALSE and \
as_list(p.thread_nwd.tlv.border_router.flag.dp)[0] == BR_FLAG_DP_FALSE):
return False
except AttributeError:
return False
# 2. Prefix TLV for fc00::/7 with Has Route sub-TLV
try:
ula_idx = prefixes.index(Ipv6Addr("fc00::"))
except ValueError:
return False
if not hasattr(p.thread_nwd.tlv, 'has_route'):
return False
return True
pkts.filter_wpan_src64(BR_1).\
filter_mle_cmd(consts.MLE_DATA_RESPONSE).\
filter(check_step3).\
must_next()
# Step 4
# Device: BR 1 (DUT)
# Description (DBR-1.1): Automatically multicasts ND RAs on Adjacent Infrastructure Link.
# Pass Criteria:
# - The DUT MUST multicast ND RAs.
# - IPv6 destination MUST be ff02::1
# - M bit and O bit MUST be '0'
# - MUST contain "Router Lifetime" = 0. (indicating it's not a default router)
# - MUST contain a Prefix Information Option (PIO) with a ULA prefix PRE_1.
# - A bit MUST be '1'
# - L bit SHOULD be '1'
# - Preferred Lifetime MUST be non-zero
# - Valid Lifetime MUST be non-zero
# - MUST contain a Route Information Option (RIO) with the OMR prefix OMR 1.
# - PRE_1 MUST be 64 bits long and start with 0xFD and MUST differ from OMR_1.
# - PRE_1 MUST contain the Extended PAN ID as follows:
# - Global ID equals the 40 most significant bits of the Extended PAN ID
# - Subnet ID equals the 16 least significant bits of the Extended PAN ID
print("Step 4: BR 1 (DUT) multicasts ND RA on AIL.")
def check_step4(p):
if p.icmpv6.type != ICMPV6_TYPE_ROUTER_ADVERTISEMENT:
return False
if p.icmpv6.nd.ra.flag.m != RA_FLAG_M_FALSE or p.icmpv6.nd.ra.flag.o != RA_FLAG_O_FALSE:
return False
if p.icmpv6.nd.ra.router_lifetime != RA_ROUTER_LIFETIME_ZERO:
return False
# Check PIO (type ICMPV6_OPT_TYPE_PIO) and RIO (type ICMPV6_OPT_TYPE_RIO)
opts = as_list(p.icmpv6.opt.type)
if ICMPV6_OPT_TYPE_PIO not in opts or ICMPV6_OPT_TYPE_RIO not in opts:
return False
# Find PRE_1 in PIO
try:
pio_idx = as_list(p.icmpv6.opt.type).index(ICMPV6_OPT_TYPE_PIO)
except ValueError:
return False
pre1 = Ipv6Addr(p.icmpv6.opt.prefix[pio_idx])
if pre1 != PRE_1_PREFIX:
return False
# Check PRE_1 properties: starts with ULA_PREFIX_START_BYTE, differs from OMR_1
if PRE_1_PREFIX_LEN != 64 or pre1[0] != ULA_PREFIX_START_BYTE:
return False
if pre1 == OMR_PREFIX:
return False
# Check PIO A bit
if as_list(p.icmpv6.opt.pio_flag.a)[0] != PIO_FLAG_A_TRUE:
return False
# Check RIO contains OMR_1
if OMR_PREFIX not in [Ipv6Addr(prefix) for prefix in p.icmpv6.opt.prefix]:
return False
# Check EXT_PAN_ID mapping in PRE_1
ext_pan_id_bytes = bytes.fromhex(EXT_PAN_ID)
# Global ID equals the 40 most significant bits of the Extended PAN ID
if pre1[EXT_PAN_ID_GLOBAL_ID_OFFSET:EXT_PAN_ID_GLOBAL_ID_OFFSET + EXT_PAN_ID_GLOBAL_ID_LEN] != \
ext_pan_id_bytes[:EXT_PAN_ID_GLOBAL_ID_LEN]:
return False
# Subnet ID equals the 16 least significant bits of the Extended PAN ID
if pre1[EXT_PAN_ID_SUBNET_ID_OFFSET:EXT_PAN_ID_SUBNET_ID_OFFSET + EXT_PAN_ID_SUBNET_ID_LEN] != \
ext_pan_id_bytes[EXT_PAN_ID_SUBNET_ID_OFFSET:EXT_PAN_ID_SUBNET_ID_OFFSET + EXT_PAN_ID_SUBNET_ID_LEN]:
return False
return True
pkts.filter_eth_src(pv.vars['BR_1_ETH']).\
filter_ipv6_dst("ff02::1").\
filter(check_step4).\
must_next()
# Step 5
# Device: Eth 1
# Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED 1.
# 1. IPv6 Source: Eth_1 ULA 2. IPv6 Destination: ED_1 OMR address
# Pass Criteria:
# - Eth 1 receives an ICMPv6 Echo Reply from ED_1.
# - 1. IPv6 Source: ED_1 OMR address
# - 2. IPv6 Destination: Eth_1 ULA
print("Step 5: Eth 1 pings ED 1 OMR.")
_pkt = pkts.filter_eth_src(pv.vars['Eth_1_ETH']).\
filter_ipv6_src(ETH_1_ULA).\
filter_ipv6_dst(ED_1_OMR).\
filter_ping_request().\
must_next()
pkts.filter(lambda p: p.eth.dst == pv.vars['Eth_1_ETH']).\
filter_ipv6_src(ED_1_OMR).\
filter_ipv6_dst(ETH_1_ULA).\
filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\
must_next()
# Step 6
# Device: ED 1
# Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1.
# 1. IPv6 Source: ED_1 OMR address 2. IPv6 Destination: Eth_1 ULA
# Pass Criteria:
# - ED 1 receives an ICMPv6 Echo Reply from Eth 1.
# - 1. IPv6 Source: Eth_1 ULA
# - 2. IPv6 Destination: ED_1 OMR address
print("Step 6: ED 1 OMR pings Eth 1 ULA.")
_pkt = pkts.filter_ipv6_src(ED_1_OMR).\
filter_ipv6_dst(ETH_1_ULA).\
filter_ping_request().\
must_next()
pkts.filter_ipv6_src(ETH_1_ULA).\
filter_ipv6_dst(ED_1_OMR).\
filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).\
must_next()
# Step 7
# Device: Eth 1
# Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to ED_1.
# 1. IPv6 Source: Eth_1 link-local 2. IPv6 Destination: ED 1 OMR address
# Pass Criteria:
# - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Thread network.
print("Step 7: Eth 1 link-local pings ED 1 OMR (must not forward).")
pkts.filter_eth_src(pv.vars['Eth_1_ETH']).\
filter_ipv6_src(Ipv6Addr(pv.vars['ETH_1_LL_ADDR'])).\
filter_ipv6_dst(ED_1_OMR).\
filter_ping_request().\
must_next()
pkts.filter_wpan_src64(BR_1).\
filter_ipv6_src(Ipv6Addr(pv.vars['ETH_1_LL_ADDR'])).\
filter_ipv6_dst(ED_1_OMR).\
filter_ping_request().\
must_not_next()
# Step 8
# Device: ED 1
# Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1.
# 1. IPv6 Source: ED 1 link-local 2. IPv6 Destination: Eth_1 ULA
# Pass Criteria:
# - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Adjacent Infrastructure Network.
print("Step 8: ED 1 link-local pings Eth 1 ULA (must not forward).")
pkts.filter_eth_src(pv.vars['BR_1_ETH']).\
filter_ipv6_src(Ipv6Addr(pv.vars['ED_1_LL_ADDR'])).\
filter_ipv6_dst(ETH_1_ULA).\
filter_ping_request().\
must_not_next()
# Step 9
# Device: Eth 1
# Description (DBR-1.1): Harness instructs device to add a route to the Thread Network's mesh-local prefix
# via the infrastructure link. Harness then instructs device to send ICMPv6 Echo Request to ED 1.
# 1. IPv6 Source: Eth 1 ULA 2. IPv6 Destination: ED_1-ML-EID
# Pass Criteria:
# - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Thread network.
print("Step 9: Eth 1 pings ED 1 ML-EID (must not forward).")
pkts.filter_eth_src(pv.vars['Eth_1_ETH']).\
filter_ipv6_src(ETH_1_ULA).\
filter_ipv6_dst(ED_1_MLEID).\
filter_ping_request().\
must_next()
pkts.filter_wpan_src64(BR_1).\
filter_ipv6_src(ETH_1_ULA).\
filter_ipv6_dst(ED_1_MLEID).\
filter_ping_request().\
must_not_next()
# Step 10
# Device: ED 1
# Description (DBR-1.1): Harness instructs device to send ICMPv6 Echo Request to Eth 1.
# 1. IPv6 Source: ED 1 ML-EID 2. IPv6 Destination: Eth 1 ULA
# Pass Criteria:
# - BR_1 (DUT) MUST NOT forward the ICMPv6 Echo Request to the Adjacent Infrastructure Network.
print("Step 10: ED 1 ML-EID pings Eth 1 ULA (must not forward).")
pkts.filter_eth_src(pv.vars['BR_1_ETH']).\
filter_ipv6_src(ED_1_MLEID).\
filter_ipv6_dst(ETH_1_ULA).\
filter_ping_request().\
must_not_next()
if __name__ == '__main__':
verify_utils.run_main(verify)
@@ -346,6 +346,7 @@ _LAYER_FIELDS = {
'mle.tlv.timeout': _auto,
'mle.tlv.addr16': _auto,
'mle.tlv.channel': _auto,
'mle.tlv.addr_reg': _list(_ipv6_addr),
'mle.tlv.addr_reg_iid': _list(_auto),
'mle.tlv.addr_reg_ipv6': _list(_ipv6_addr),
'mle.tlv.link_enh_ack_flags': _auto,
@@ -505,6 +506,8 @@ _LAYER_FIELDS = {
'icmpv6.opt.route_info.flag.route_preference': _auto,
'icmpv6.opt.route_info.flag.reserved': _auto,
'icmpv6.opt.prefix': _list(_ipv6_addr),
'icmpv6.opt.pio_flag.a': _list(_auto),
'icmpv6.opt.pio_flag.l': _list(_auto),
'icmpv6.opt.length': _list(_auto),
'icmpv6.opt.reserved': _str,
'icmpv6.nd.ra.router_lifetime': _auto,
@@ -647,6 +650,8 @@ _LAYER_FIELDS = {
'thread_nwd.tlv.service.s_data.mlrtimeout': _auto,
'thread_nwd.tlv.server_16': _list(_auto),
'thread_nwd.tlv.border_router_16': _list(_auto),
'thread_nwd.tlv.has_route.br_16': _list(_auto),
'thread_nwd.tlv.has_route.pref': _list(_auto),
'thread_nwd.tlv.sub_tlvs': _list(_str),
# TODO: support thread_nwd.tlv.prefix.length and thread_nwd.tlv.prefix.domain_id
'thread_nwd.tlv.prefix': _list(_ipv6_addr),
@@ -733,6 +738,7 @@ def get_layer_field(packet: RawPacket, field_uri: str) -> Any:
:return: The specified layer field.
"""
assert isinstance(packet, RawPacket)
orig_field_uri = field_uri
secs = field_uri.split('.')
layer_depth = 0
layer_name = secs[0]
@@ -740,11 +746,17 @@ def get_layer_field(packet: RawPacket, field_uri: str) -> Any:
layer_name = layer_name[:-len('inner')]
field_uri = '.'.join([layer_name] + secs[1:])
layer_depth = 1
orig_field_uri = field_uri
if field_uri == 'mle.tlv.addr_reg':
# Field aliases for tshark
if field_uri in ('icmpv6.opt.pio_flag.a', 'icmpv6.opt.prefix.flag.a'):
field_uri, orig_field_uri = 'icmpv6.opt.prefix.flag.a', 'icmpv6.opt.pio_flag.a'
elif field_uri in ('icmpv6.opt.pio_flag.l', 'icmpv6.opt.prefix.flag.l'):
field_uri, orig_field_uri = 'icmpv6.opt.prefix.flag.l', 'icmpv6.opt.pio_flag.l'
elif field_uri == 'mle.tlv.addr_reg':
field_uri = 'mle.tlv.addr_reg_ipv6'
if is_layer_field(field_uri):
if is_layer_field(field_uri) or field_uri in ('icmpv6.opt.prefix.flag.a', 'icmpv6.opt.prefix.flag.l'):
candidate_layers = _get_candidate_layers(packet, layer_name)
for layers in candidate_layers:
if layer_depth >= len(layers):
@@ -756,11 +768,11 @@ def get_layer_field(packet: RawPacket, field_uri: str) -> Any:
v = layer.get_field('dns' + field_uri[4:])
if v is not None:
try:
v = _LAYER_FIELDS[field_uri](v)
print("[%s = %r] " % (field_uri, v), file=sys.stderr)
v = _LAYER_FIELDS[orig_field_uri](v)
print("[%s = %r] " % (orig_field_uri, v), file=sys.stderr)
return v
except Exception as ex:
raise ValueError('can not parse field %s = %r' % (field_uri,
raise ValueError('can not parse field %s = %r' % (orig_field_uri,
(v.get_default_value(), v.raw_value))) from ex
print("[%s = %s] " % (field_uri, "null"), file=sys.stderr)