[nexus] add test for IPv6 default route advertisement (PIC-TC-3) (#12860)

This commit adds a Nexus test case 1_4_PIC_TC_3 to verify the IPv6
default route advertisement behavior of a Border Router (BR) in a
Thread 1.4 network.

The test verifies that:
- The BR correctly advertises a default route (::/0) in Thread Network
  Data when it discovers a default route on the infrastructure link.
- The BR maintains the default route advertisement even if the
  infrastructure default route is withdrawn, provided a non-ULA prefix
  remains active on the infrastructure link.
- The default route advertisement is correctly restored or updated when
  the infrastructure default route is re-enabled.

The test implementation includes:
- test_1_4_PIC_TC_3.cpp: C++ test logic using the Nexus simulation
  framework, simulating BR, Router, End Device, and Infrastructure
  nodes (Eth_1, Eth_2). It uses a custom ICMPv6 receive callback to
  simulate "no route to host" conditions.
- verify_1_4_PIC_TC_3.py: Python verification script that analyzes
  the captured packets to ensure MLE Data Responses and ICMPv6 traffic
  match the expected behavior for each test step.

Integration:
- Updated tests/nexus/CMakeLists.txt and tests/nexus/run_nexus_tests.sh
  to include the new test in the automated test suite.
This commit is contained in:
Jonathan Hui
2026-04-09 17:18:28 -05:00
committed by GitHub
parent 05ad9803d8
commit dbbadb4021
5 changed files with 857 additions and 0 deletions
+1
View File
@@ -297,6 +297,7 @@ ot_nexus_test(1_4_DNS_TC_1 "cert;nexus")
ot_nexus_test(1_4_DNS_TC_3 "cert;nexus")
ot_nexus_test(1_4_DNS_TC_5 "cert;nexus")
ot_nexus_test(1_4_PIC_TC_1 "cert;nexus")
ot_nexus_test(1_4_PIC_TC_3 "cert;nexus")
ot_nexus_test(1_4_CS_TC_3 "cert;nexus")
# Misc tests
+1
View File
@@ -230,6 +230,7 @@ DEFAULT_TESTS=(
"1_4_DNS_TC_3"
"1_4_DNS_TC_5"
"1_4_PIC_TC_1"
"1_4_PIC_TC_3"
"1_4_CS_TC_3"
)
+489
View File
@@ -0,0 +1,489 @@
/*
* 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 <string.h>
#include "net/dhcp6_types.hpp"
#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 (DHCPv6-PD, RA), in milliseconds.
*/
static constexpr uint32_t kBrActionTime = 30 * 1000;
/**
* Time to advance for the ping response, in milliseconds.
*/
static constexpr uint32_t kPingTime = 2 * 1000;
/**
* DHCPv6-PD Server Port.
*/
static constexpr uint16_t kDhcp6ServerPort = 547;
/**
* DHCPv6-PD Client Port.
*/
static constexpr uint16_t kDhcp6ClientPort = 546;
/**
* Echo Request identifier.
*/
static constexpr uint16_t kEchoIdentifier = 0x1234;
/**
* Echo Request payload size.
*/
static constexpr uint16_t kEchoPayloadSize = 10;
/**
* Internet Server Address.
*/
static const char kInternetServerAddr[] = "2002:1234::1234";
/**
* Delegated Prefix.
*/
static const char kDelegatedPrefix[] = "2005:1234:abcd:0::/64";
/**
* Eth_1 GUA address prefix.
*/
static const char kEth1Prefix[] = "2001:db8:1::/64";
/**
* Infrastructure interface index.
*/
static constexpr uint32_t kInfraIfIndex = 1;
/**
* DHCPv6 Server Hook to simulate DHCPv6-PD server.
*/
bool HandleEth2Udp(Instance &aInstance, Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
Node &eth2 = Node::From(&aInstance);
bool handled = false;
if (aMessageInfo.GetSockPort() == kDhcp6ServerPort)
{
Dhcp6::Header header;
SuccessOrQuit(aMessage.Read(aMessage.GetOffset(), header));
if (header.GetMsgType() == Dhcp6::kMsgTypeSolicit || header.GetMsgType() == Dhcp6::kMsgTypeRequest)
{
Message *reply = eth2.Get<MessagePool>().Allocate(Message::kTypeIp6);
VerifyOrQuit(reply != nullptr);
Dhcp6::Header replyHeader;
replyHeader.Clear();
replyHeader.SetMsgType((header.GetMsgType() == Dhcp6::kMsgTypeSolicit) ? Dhcp6::kMsgTypeAdvertise
: Dhcp6::kMsgTypeReply);
replyHeader.SetTransactionId(header.GetTransactionId());
SuccessOrQuit(reply->Append(replyHeader));
uint16_t offset = aMessage.GetOffset() + sizeof(Dhcp6::Header);
uint16_t length = aMessage.GetLength() - offset;
Dhcp6::Option option;
uint32_t iaid = 0;
while (length >= sizeof(Dhcp6::Option))
{
SuccessOrQuit(aMessage.Read(offset, option));
VerifyOrQuit(length >= option.GetSize());
if (option.GetCode() == Dhcp6::Option::kClientId)
{
SuccessOrQuit(reply->AppendBytesFromMessage(aMessage, offset, option.GetSize()));
}
else if (option.GetCode() == Dhcp6::Option::kIaPd)
{
Dhcp6::IaPdOption iaPdSolicit;
VerifyOrQuit(option.GetSize() >= sizeof(iaPdSolicit));
SuccessOrQuit(aMessage.Read(offset, iaPdSolicit));
iaid = iaPdSolicit.GetIaid();
}
offset += option.GetSize();
length -= option.GetSize();
}
// Append Server ID (ETH_2 address)
SuccessOrQuit(Dhcp6::ServerIdOption::AppendWithEui64Duid(*reply, eth2.Get<Mac::Mac>().GetExtAddress()));
if (header.GetMsgType() == Dhcp6::kMsgTypeSolicit)
{
// Append Preference
Dhcp6::PreferenceOption preference;
preference.Init();
preference.SetPreference(255);
SuccessOrQuit(reply->Append(preference));
}
Dhcp6::IaPdOption iaPd;
iaPd.Init();
iaPd.SetIaid(iaid);
iaPd.SetT1(1000);
iaPd.SetT2(2000);
SuccessOrQuit(reply->Append(iaPd));
Dhcp6::IaPrefixOption prefixOption;
prefixOption.Init();
Ip6::Prefix prefix;
SuccessOrQuit(prefix.FromString(kDelegatedPrefix));
prefixOption.SetPrefix(prefix);
prefixOption.SetPreferredLifetime(3600);
prefixOption.SetValidLifetime(7200);
SuccessOrQuit(reply->Append(prefixOption));
Dhcp6::Option::UpdateOptionLengthInMessage(*reply,
reply->GetLength() - sizeof(iaPd) - sizeof(prefixOption));
eth2.mInfraIf.SendUdp(eth2.mInfraIf.GetLinkLocalAddress(), aMessageInfo.GetPeerAddr(), kDhcp6ServerPort,
kDhcp6ClientPort, *reply);
handled = true;
}
}
return handled;
}
static bool sDropInternetPings = false;
void Br1ReceiveCallback(otMessage *aMessage, void *aContext)
{
if (sDropInternetPings)
{
Ip6::Header header;
Ip6::Address internetServer;
SuccessOrQuit(AsCoreType(aMessage).Read(0, header));
SuccessOrQuit(internetServer.FromString(kInternetServerAddr));
if (header.GetDestination() == internetServer)
{
// Drop it to simulate "no route to host"
return;
}
}
Node::HandleIp6Receive(aMessage, aContext);
}
void Test_1_4_PIC_TC_3(void)
{
/**
* 10.3. IPv6 default route advertisement
*
* 10.3.1. Purpose
* - To verify that the BR DUT:
* - Correctly advertises a default route on the Thread Network, using the zero-length route ::/0 in a Prefix TLV.
* - Does not withdraws the advertised default route when the infrastructure network disables the default route,
* as long as there's still a non-ULA prefix active on the AIL.
* - Still advertises the default route, when the infrastructure network re-enables the default route.
* - TBD: a future test update needs to validate that when only a ULA prefix is set at the AIL, the default route
* gets withdrawn and replaced by fc00::/7 route.
*
* 10.3.2. Topology
* - BR_1 (DUT) - Border Router
* - Router_1 - Thread Router Reference Device, attached to BR_1
* - ED_1 - Thread Reference Device, End Device (e.g. FED/REED) role, attached to Router_1
* - Eth_1 - Adjacent Infrastructure Link Linux Reference Device
* - Eth_2 - Adjacent Infrastructure Link SPIFF Reference Device
*/
Core nexus;
Node &br1 = nexus.CreateNode();
Node &r1 = nexus.CreateNode();
Node &ed1 = nexus.CreateNode();
Node &eth1 = nexus.CreateNode();
Node &eth2 = nexus.CreateNode();
br1.SetName("BR_1");
r1.SetName("ROUTER_1");
ed1.SetName("ED_1");
eth1.SetName("ETH_1");
eth2.SetName("ETH_2");
nexus.AdvanceTime(0);
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote));
Log("Step 1: Enable Eth_1 and Eth_2");
/**
* Step 1
* - Device: Eth_1, Eth_2
* - Description (PIC-10.3): Enable
* - Pass Criteria:
* - N/A
*/
eth1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
eth2.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
eth1.Get<BorderRouter::RoutingManager>().Init();
eth2.Get<BorderRouter::RoutingManager>().Init();
SuccessOrQuit(eth1.Get<BorderRouter::RoutingManager>().SetEnabled(false));
SuccessOrQuit(eth2.Get<BorderRouter::RoutingManager>().SetEnabled(false));
{
Ip6::Address internetServer;
SuccessOrQuit(internetServer.FromString(kInternetServerAddr));
eth2.mInfraIf.AddAddress(internetServer);
}
Ip6::Prefix pioPrefix;
SuccessOrQuit(pioPrefix.FromString(kEth1Prefix));
eth2.mInfraIf.StartRouterAdvertisement(pioPrefix);
eth2.mInfraIf.SetUdpHook(HandleEth2Udp);
Log("Step 2: Eth_2 default configuration");
/**
* Step 2
* - Device: Eth_2
* - Description (PIC-10.3): Harness instructs device to: (only default configuration is applied)
* - Pass Criteria:
* - N/A
*/
nexus.AdvanceTime(kPingTime);
Log("Step 3: Enable BR_1, Router_1, ED_1");
/**
* Step 3
* - Device: BR_1 (DUT), Router_1, ED_1
* - Description (PIC-10.3): Enable
* - Pass Criteria:
* - N/A
*/
br1.AllowList(r1);
r1.AllowList(br1);
r1.AllowList(ed1);
ed1.AllowList(r1);
br1.Form();
nexus.AdvanceTime(kFormNetworkTime);
br1.Get<BorderRouter::InfraIf>().Init(kInfraIfIndex, true);
br1.Get<BorderRouter::RoutingManager>().Init();
br1.Get<BorderRouter::RoutingManager>().SetDhcp6PdEnabled(true);
SuccessOrQuit(br1.Get<BorderRouter::RoutingManager>().SetEnabled(true));
br1.Get<Ip6::Ip6>().SetReceiveCallback(Br1ReceiveCallback, &br1);
r1.Join(br1);
nexus.AdvanceTime(kJoinNetworkTime);
ed1.Join(r1);
nexus.AdvanceTime(kJoinNetworkTime);
Log("Step 4: BR_1 obtains OMR prefix via DHCPv6-PD");
/**
* Step 4
* - Device: BR_1 (DUT)
* - Description (PIC-10.3): Automatically uses DHCPv6-PD client function to obtain a delegated prefix from the
* DHCPv6 server. It configures this prefix as the OMR prefix.
* - Pass Criteria:
* - N/A
*/
nexus.AdvanceTime(kBrActionTime);
Log("Step 5: BR_1 automatically advertises a default route in its Thread Network Data");
/**
* Step 5
* - Device: BR_1 (DUT)
* - Description (PIC-10.3): Automatically advertises a default route in its Thread Network Data.
* - Pass Criteria:
* - Thread Network Data MUST contain: Prefix TLV with an OMR prefix OMR_1 that starts with
* 2005:1234:abcd:0::/56.
* - Border Router sub-TLV MUST indicate the RLOC16 of BR_1, in a P_border_router_16 field.
* - Flag R (P_default) MUST be 1
* - Flag O (P_on_mesh) MUST be 1
* - Thread Network Data MUST contain: Prefix TLV with the zero-length prefix ::/0
* - Has Route sub-TLV Flag NP MUST be 0
*/
nexus.AdvanceTime(kBrActionTime);
Log("Step 6: ED_1 pings internet address");
/**
* Step 6
* - Device: ED_1
* - Description (PIC-10.3): Harness instructs device to send ping to destination address 2002:1234::1234 with
* its (default) OMR source address, to validate that default route works.
* - Pass Criteria:
* - Ping response received by ED_1.
*/
Ip6::Address internetServer;
SuccessOrQuit(internetServer.FromString(kInternetServerAddr));
ed1.SendEchoRequest(internetServer, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingTime);
Log("Step 7: Eth_2 stops advertising a default route");
/**
* Step 7
* - Device: Eth_2
* - Description (PIC-10.3): Harnes instructs device to reconfigure ND RA daemon to stop advertising a default
* route: “Router Lifetime” field set to 0 sec (indicating it is not a default router for IPv6).
* - Pass Criteria:
* - N/A
*/
eth2.mInfraIf.StopRouterAdvertisement();
nexus.AdvanceTime(kPingTime);
for (int i = 0; i < 3; i++)
{
Ip6::Nd::RouterAdvert::TxMessage ra;
Ip6::Nd::RouterAdvert::Header header;
Ip6::Nd::Icmp6Packet packet;
header.SetToDefault();
header.SetRouterLifetime(0);
SuccessOrQuit(ra.Append(header));
SuccessOrQuit(ra.AppendPrefixInfoOption(pioPrefix, 1800, 1800,
Ip6::Nd::PrefixInfoOption::kOnLinkFlag |
Ip6::Nd::PrefixInfoOption::kAutoConfigFlag));
ra.GetAsPacket(packet);
eth2.mInfraIf.SendIcmp6Nd(Ip6::Address::GetLinkLocalAllNodesMulticast(), packet.GetBytes(), packet.GetLength());
nexus.AdvanceTime(1000);
}
nexus.AdvanceTime(200 * 1000);
Log("Step 8: BR_1 does not stop advertising a default route");
/**
* Step 8
* - Device: BR_1 (DUT)
* - Description (PIC-10.3): Does not stop advertising a default route in its Thread Network Data.
* - Pass Criteria:
* - Thread Network Data MUST contain: Prefix TLV with same OMR_1.
* - Border Router sub-TLV MUST indicate the RLOC16 of BR_1, in a P_border_router_16 field.
* - Flag R (P_default) MUST be 1
* - Flag O (P_on_mesh) MUST be 1
* - Thread Network Data MUST contain: Prefix TLV with the zero-length prefix ::/0
* - Has Route sub-TLV Flag NP MUST be 0
*/
nexus.AdvanceTime(kBrActionTime);
Log("Step 9: ED_1 pings Eth_1");
/**
* Step 9
* - Device: ED_1
* - Description (PIC-10.3): Harness instructs device to send ping to destination Eth_1 with its (default) OMR
* source address, to validate that route to AIL still works.
* - Pass Criteria:
* - Ping response received by ED_1.
*/
Ip6::Address eth1Addr = eth1.mInfraIf.FindMatchingAddress(kEth1Prefix);
ed1.SendEchoRequest(eth1Addr, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingTime);
Log("Step 10: ED_1 pings internet address (should fail)");
/**
* Step 10
* - Device: ED_1
* - Description (PIC-10.3): Harness instructs device to send ping to destination address 2002:1234::1234 with
* its (default) OMR source address, to validate that default route does not work at the moment.
* - Pass Criteria:
* - Ping request MUST NOT be sent by DUT onto the AIL.
* - Ping response MUST NOT be received by ED_1.
*/
sDropInternetPings = true;
ed1.SendEchoRequest(internetServer, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(10 * 1000);
sDropInternetPings = false;
Log("Step 11: Eth_2 starts advertising a default route again");
/**
* Step 11
* - Device: Eth_2
* - Description (PIC-10.3): Harness instructs device to reconfigure ND RA daemon to start advertising a default
* route again: “Router Lifetime” field set to 875 sec (indicating it is a default router for IPv6).
* - Pass Criteria:
* - N/A
*/
eth2.mInfraIf.StartRouterAdvertisement(pioPrefix);
nexus.AdvanceTime(kBrActionTime);
Log("Step 12: BR_1 automatically advertises a default route (same as step 5)");
/**
* Step 12
* - Device: BR_1 (DUT)
* - Description (PIC-10.3): (As in step 5)
* - Pass Criteria:
* - (As in step 5)
*/
nexus.AdvanceTime(kBrActionTime);
Log("Step 13: ED_1 pings internet address (same as step 6)");
/**
* Step 13
* - Device: ED_1
* - Description (PIC-10.3): (As in step 6)
* - Pass Criteria:
* - (As in step 6)
*/
ed1.SendEchoRequest(internetServer, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingTime);
Log("Step 14: ED_1 pings Eth_1 (same as step 9)");
/**
* Step 14
* - Device: ED_1
* - Description (PIC-10.3): (As in step 9)
* - Pass Criteria:
* - (As in step 9)
*/
ed1.SendEchoRequest(eth1Addr, kEchoIdentifier, kEchoPayloadSize);
nexus.AdvanceTime(kPingTime);
nexus.AddTestVar("ETH_1_ADDR", eth1Addr.ToString().AsCString());
nexus.AddTestVar("ETH_2_ADDR", eth2.mInfraIf.GetLinkLocalAddress().ToString().AsCString());
nexus.AddTestVar("INTERNET_SERVER_ADDR", kInternetServerAddr);
nexus.SaveTestInfo("test_1_4_PIC_TC_3.json");
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::Test_1_4_PIC_TC_3();
printf("All tests passed\n");
return 0;
}
+365
View File
@@ -0,0 +1,365 @@
#!/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
def check_nwd_has_route(packet, target_prefix, rloc16=None):
try:
types = verify_utils.as_list(packet.thread_nwd.tlv.type)
prefixes = verify_utils.as_list(packet.thread_nwd.tlv.prefix)
except (AttributeError, IndexError):
return False
prefix_idx = 0
hr_idx = 0
is_target = False
for t in types:
if t == consts.NWD_PREFIX_TLV:
is_target = (Ipv6Addr(prefixes[prefix_idx]) == Ipv6Addr(target_prefix))
prefix_idx += 1
elif t in (consts.NWD_COMMISSIONING_DATA_TLV, consts.NWD_SERVICE_TLV):
is_target = False
elif t == consts.NWD_HAS_ROUTER_TLV:
if is_target:
if rloc16 is not None:
try:
br_16s = verify_utils.as_list(packet.thread_nwd.tlv.has_route.br_16)
if int(br_16s[hr_idx]) != rloc16:
hr_idx += 1
continue
except (AttributeError, IndexError):
hr_idx += 1
continue
# Check for NP flag if available in pktverify
# Thread 1.4: Has Route sub-TLV (Type 0)
# Field order: RLOC16 (16), Flags (8)
# Flags: Prf (2), P (1), NP (1), R (1), O (1), Reserved (2)
try:
nps = verify_utils.as_list(packet.thread_nwd.tlv.has_route.np)
if int(nps[hr_idx]) != 0:
hr_idx += 1
continue
except (AttributeError, IndexError):
# If field NP is not available, we skip the check
pass
return True
hr_idx += 1
return False
def check_nwd_prefix_flags_with_rloc(packet, target_prefix, rloc16, **expected_flags):
try:
types = verify_utils.as_list(packet.thread_nwd.tlv.type)
prefixes = verify_utils.as_list(packet.thread_nwd.tlv.prefix)
except (AttributeError, IndexError):
return False
prefix_idx = 0
br_idx = 0
is_target = False
for i, t in enumerate(types):
if t == consts.NWD_PREFIX_TLV:
is_target = (Ipv6Addr(prefixes[prefix_idx]) == Ipv6Addr(target_prefix))
prefix_idx += 1
elif t in (consts.NWD_COMMISSIONING_DATA_TLV, consts.NWD_SERVICE_TLV):
is_target = False
elif t == consts.NWD_BORDER_ROUTER_TLV:
if is_target:
try:
br_16s = verify_utils.as_list(packet.thread_nwd.tlv.border_router_16)
if int(br_16s[br_idx]) != rloc16:
br_idx += 1
continue
# Now check flags using verify_utils.check_nwd_prefix_flags logic
# Since we can't easily call it with our filtered indices, we'll do it here
field_map = {
'r': packet.thread_nwd.tlv.border_router.flag.r,
'o': packet.thread_nwd.tlv.border_router.flag.o,
}
match = True
for key, expected_val in expected_flags.items():
if key in field_map:
actual_val = int(verify_utils.as_list(field_map[key])[br_idx])
if actual_val != expected_val:
match = False
break
if match:
return True
except (AttributeError, IndexError):
pass
br_idx += 1
return False
def verify(pv):
# 10.3. IPv6 default route advertisement
#
# 10.3.1. Purpose
# - To verify that the BR DUT:
# - Correctly advertises a default route on the Thread Network, using the zero-length route ::/0 in a Prefix TLV.
# - Does not withdraws the advertised default route when the infrastructure network disables the default route,
# as long as there's still a non-ULA prefix active on the AIL.
# - Still advertises the default route, when the infrastructure network re-enables the default route.
# - TBD: a future test update needs to validate that when only a ULA prefix is set at the AIL, the default route
# gets withdrawn and replaced by fc00::/7 route.
#
# 10.3.2. Topology
# - BR_1 (DUT) - Border Router
# - Router_1 - Thread Router Reference Device, attached to BR_1
# - ED_1 - Thread Reference Device, End Device (e.g. FED/REED) role, attached to Router_1
# - Eth_1 - Adjacent Infrastructure Link Linux Reference Device
# - Eth_2 - Adjacent Infrastructure Link SPIFF Reference Device
pkts = pv.pkts
pv.summary.show()
BR_1 = pv.vars['BR_1']
ED_1 = pv.vars['ED_1']
ETH_1_ADDR = pv.vars['ETH_1_ADDR']
INTERNET_SERVER_ADDR = pv.vars['INTERNET_SERVER_ADDR']
# Step 1
# - Device: Eth_1, Eth_2
# - Description (PIC-10.3): Enable
# - Pass Criteria:
# - N/A
print("Step 1: Enable Eth_1 and Eth_2")
# Step 2
# - Device: Eth_2
# - Description (PIC-10.3): Harness instructs device to: (only default configuration is applied)
# - Pass Criteria:
# - N/A
print("Step 2: Eth_2 default configuration")
# Step 3
# - Device: BR_1 (DUT), Router_1, ED_1
# - Description (PIC-10.3): Enable
# - Pass Criteria:
# - N/A
print("Step 3: Enable BR_1, Router_1, ED_1")
# Step 4
# - Device: BR_1 (DUT)
# - Description (PIC-10.3): Automatically uses DHCPv6-PD client function to obtain a delegated prefix from the
# DHCPv6 server. It configures this prefix as the OMR prefix.
# - Pass Criteria:
# - N/A
print("Step 4: BR_1 obtains OMR prefix via DHCPv6-PD")
# Verify DHCPv6-PD Reply from Eth_2 to BR_1
pkts.filter_ipv6_src(pv.vars['ETH_2_ADDR']).\
filter(lambda p: 'dhcpv6' in p.layer_names).\
must_next()
# Step 5
# - Device: BR_1 (DUT)
# - Description (PIC-10.3): Automatically advertises a default route in its Thread Network Data.
# - Pass Criteria:
# - Thread Network Data MUST contain: Prefix TLV with an OMR prefix OMR_1 that starts with 2005:1234:abcd:0::/56.
# - Border Router sub-TLV MUST indicate the RLOC16 of BR_1, in a P_border_router_16 field.
# - Flag R (P_default) MUST be 1
# - Flag O (P_on_mesh) MUST be 1
# - Thread Network Data MUST contain: Prefix TLV with the zero-length prefix ::/0
# - Has Route sub-TLV Flag NP MUST be 0
print("Step 5: BR_1 automatically advertises a default route in its Thread Network Data")
omr_prefix_pattern = Ipv6Addr("2005:1234:abcd:0::")[:7]
BR_1_RLOC16 = pv.vars['BR_1_RLOC16']
nwd_pkt = pkts.filter_wpan_src64(BR_1).\
filter_mle_cmd(consts.MLE_DATA_RESPONSE).\
filter(lambda p: any(pre.startswith(omr_prefix_pattern) for pre in p.thread_nwd.tlv.prefix)).\
filter(lambda p: any(pre == Ipv6Addr("::") for pre in p.thread_nwd.tlv.prefix)).\
must_next()
OMR_PREFIX = None
for pre in nwd_pkt.thread_nwd.tlv.prefix:
if pre.startswith(omr_prefix_pattern):
OMR_PREFIX = pre
break
assert OMR_PREFIX is not None
# Verify OMR prefix flags and RLOC16
assert check_nwd_prefix_flags_with_rloc(nwd_pkt, OMR_PREFIX, rloc16=BR_1_RLOC16, r=1, o=1)
# Verify ::/0 prefix in Has Route TLV and RLOC16
assert check_nwd_has_route(nwd_pkt, "::", rloc16=BR_1_RLOC16)
# Step 6
# - Device: ED_1
# - Description (PIC-10.3): Harness instructs device to send ping to destination address 2002:1234::1234 with its
# (default) OMR source address, to validate that default route works.
# - Pass Criteria:
# - Ping response received by ED_1.
print("Step 6: ED_1 pings internet address")
ping_req = pkts.filter_ipv6_dst(INTERNET_SERVER_ADDR).\
filter_ping_request().\
must_next()
pkts.filter_ipv6_src(INTERNET_SERVER_ADDR).\
filter_ping_reply(identifier=ping_req.icmpv6.echo.identifier).\
must_next()
# Step 7
# - Device: Eth_2
# - Description (PIC-10.3): Harnes instructs device to reconfigure ND RA daemon to stop advertising a default
# route: “Router Lifetime” field set to 0 sec (indicating it is not a default router for IPv6).
# - Pass Criteria:
# - N/A
print("Step 7: Eth_2 stops advertising a default route")
ra_step7 = pkts.filter_icmpv6_nd_ra().\
filter(lambda p: p.icmpv6.nd.ra.router_lifetime == 0).\
must_next()
# Step 8
# - Device: BR_1 (DUT)
# - Description (PIC-10.3): Does not stop advertising a default route in its Thread Network Data.
# - Pass Criteria:
# - Thread Network Data MUST contain: Prefix TLV with same OMR_1.
# - Border Router sub-TLV MUST indicate the RLOC16 of BR_1, in a P_border_router_16 field.
# - Flag R (P_default) MUST be 1
# - Flag O (P_on_mesh) MUST be 1
# - Thread Network Data MUST contain: Prefix TLV with the zero-length prefix ::/0
# - Has Route sub-TLV Flag NP MUST be 0
print("Step 8: BR_1 does not stop advertising a default route")
# We check that BR_1 still advertises the default route in its next MLE Data Response.
nwd_pkt_8 = pkts.filter_wpan_src64(BR_1).\
filter_mle_cmd(consts.MLE_DATA_RESPONSE).\
filter(lambda p: any(pre == Ipv6Addr("::") for pre in p.thread_nwd.tlv.prefix)).\
must_next()
# Verify OMR prefix flags and RLOC16
assert check_nwd_prefix_flags_with_rloc(nwd_pkt_8, OMR_PREFIX, rloc16=BR_1_RLOC16, r=1, o=1)
# Verify ::/0 prefix in Has Route TLV and RLOC16
assert check_nwd_has_route(nwd_pkt_8, "::", rloc16=BR_1_RLOC16)
# Step 9
# - Device: ED_1
# - Description (PIC-10.3): Harness instructs device to send ping to destination Eth_1 with its (default) OMR
# source address, to validate that route to AIL still works.
# - Pass Criteria:
# - Ping response received by ED_1.
print("Step 9: ED_1 pings Eth_1")
ping_req_9 = pkts.filter_ipv6_dst(ETH_1_ADDR).\
filter_ping_request().\
must_next()
pkts.filter_ipv6_src(ETH_1_ADDR).\
filter_ping_reply(identifier=ping_req_9.icmpv6.echo.identifier).\
must_next()
# Step 10
# - Device: ED_1
# - Description (PIC-10.3): Harness instructs device to send ping to destination address 2002:1234::1234 with its
# (default) OMR source address, to validate that default route does not work at the moment.
# - Pass Criteria:
# - Ping request MUST NOT be sent by DUT onto the AIL.
# - Ping response MUST NOT be received by ED_1.
print("Step 10: ED_1 pings internet address (should fail)")
_step10_start_idx = (pkts.index[0], 0)
# Find Step 11 RA first to define range for Step 10
ra_step11 = pkts.filter_icmpv6_nd_ra().\
filter(lambda p: p.icmpv6.nd.ra.router_lifetime > 0).\
must_next()
# No ping request on AIL for internet server in the range of Step 10.
# Packets on AIL do NOT have 'wpan' layer.
pkts.range(_step10_start_idx, (ra_step11.number, 0)).\
filter_ipv6_dst(INTERNET_SERVER_ADDR).\
filter_ping_request().\
filter(lambda p: 'wpan' not in p.layer_names).\
must_not_next()
# Step 11
# - Device: Eth_2
# - Description (PIC-10.3): Harness instructs device to reconfigure ND RA daemon to start advertising a default
# route again: “Router Lifetime” field set to 875 sec (indicating it is a default router for IPv6).
# - Pass Criteria:
# - N/A
print("Step 11: Eth_2 starts advertising a default route again")
# ra_step11 already found
# Step 12
# - Device: BR_1 (DUT)
# - Description (PIC-10.3): (As in step 5)
# - Pass Criteria:
# - (As in step 5)
print("Step 12: BR_1 automatically advertises a default route (same as step 5)")
# Since the default route was never withdrawn (verified in Step 8), we check that
# it continues to be advertised (i.e., no MLE Data Response from BR_1 withdraws it).
pkts.filter_wpan_src64(BR_1).\
filter_mle_cmd(consts.MLE_DATA_RESPONSE).\
filter(lambda p: any(pre == Ipv6Addr("::") for pre in p.thread_nwd.tlv.prefix) is False).\
must_not_next()
# Step 13
# - Device: ED_1
# - Description (PIC-10.3): (As in step 6)
# - Pass Criteria:
# - (As in step 6)
print("Step 13: ED_1 pings internet address (same as step 6)")
ping_req_13 = pkts.filter_ipv6_dst(INTERNET_SERVER_ADDR).\
filter_ping_request().\
must_next()
pkts.filter_ipv6_src(INTERNET_SERVER_ADDR).\
filter_ping_reply(identifier=ping_req_13.icmpv6.echo.identifier).\
must_next()
# Step 14
# - Device: ED_1
# - Description (PIC-10.3): (As in step 9)
# - Pass Criteria:
# - (As in step 9)
print("Step 14: ED_1 pings Eth_1 (same as step 9)")
ping_req_14 = pkts.filter_ipv6_dst(ETH_1_ADDR).\
filter_ping_request().\
must_next()
pkts.filter_ipv6_src(ETH_1_ADDR).\
filter_ping_reply(identifier=ping_req_14.icmpv6.echo.identifier).\
must_next()
if __name__ == '__main__':
verify_utils.run_main(verify)
@@ -657,6 +657,7 @@ _LAYER_FIELDS = {
'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.has_route.np': _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),