/* * 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 */ AllowLinkBetween(br1, r1); AllowLinkBetween(r1, ed1); 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, Node::kAsFed); 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; }