diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index c20563320..dd9a92524 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -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 diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index b0c8c65d9..d5e2cad75 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -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" ) diff --git a/tests/nexus/test_1_4_PIC_TC_3.cpp b/tests/nexus/test_1_4_PIC_TC_3.cpp new file mode 100644 index 000000000..26f0bd0c6 --- /dev/null +++ b/tests/nexus/test_1_4_PIC_TC_3.cpp @@ -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 +#include + +#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 ð2 = 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().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().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 ð1 = nexus.CreateNode(); + Node ð2 = 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().Init(kInfraIfIndex, true); + eth2.Get().Init(kInfraIfIndex, true); + eth1.Get().Init(); + eth2.Get().Init(); + SuccessOrQuit(eth1.Get().SetEnabled(false)); + SuccessOrQuit(eth2.Get().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().Init(kInfraIfIndex, true); + br1.Get().Init(); + br1.Get().SetDhcp6PdEnabled(true); + SuccessOrQuit(br1.Get().SetEnabled(true)); + + br1.Get().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; +} diff --git a/tests/nexus/verify_1_4_PIC_TC_3.py b/tests/nexus/verify_1_4_PIC_TC_3.py new file mode 100644 index 000000000..52c1a8199 --- /dev/null +++ b/tests/nexus/verify_1_4_PIC_TC_3.py @@ -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) diff --git a/tests/scripts/thread-cert/pktverify/layer_fields.py b/tests/scripts/thread-cert/pktverify/layer_fields.py index 58b91c690..5e2881aef 100644 --- a/tests/scripts/thread-cert/pktverify/layer_fields.py +++ b/tests/scripts/thread-cert/pktverify/layer_fields.py @@ -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),