diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 9158fdc7e..c20563320 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -296,6 +296,7 @@ ot_nexus_test(1_4_TREL_TC_6 "cert;nexus") 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_CS_TC_3 "cert;nexus") # Misc tests diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index ffe366a7f..b3ed7aa55 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -53,7 +53,7 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_TXT_DATA_PARSER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 1 -#define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 0 +#define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_TESTING_API_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1 diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 459a4f099..15e1cbaef 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -648,7 +648,10 @@ void Core::ProcessInfraIf(Node &aNode) if (!header.GetDestination().IsMulticast()) { - targetNode = FindNodeByInfraIfAddress(header.GetDestination()); + if (!IsThreadAddress(header.GetDestination())) + { + targetNode = FindNodeByAddress(header.GetDestination()); + } } for (Node &rxNode : mNodes) diff --git a/tests/nexus/platform/nexus_dns.cpp b/tests/nexus/platform/nexus_dns.cpp index 06c8a28e3..e395c4215 100644 --- a/tests/nexus/platform/nexus_dns.cpp +++ b/tests/nexus/platform/nexus_dns.cpp @@ -110,6 +110,7 @@ void UpstreamDns::StartUpstreamQuery(UpstreamQueryTransaction &aTxn, const Messa ot::Dns::Header dnsHeader; PendingQuery *pendingQuery; Message *message = nullptr; + // We use kDnsPort (53) as both source and destination port for upstream queries to simplify interception // and response matching in this simulation environment. diff --git a/tests/nexus/platform/nexus_infra_if.cpp b/tests/nexus/platform/nexus_infra_if.cpp index a5269a7a0..1e1f23d9d 100644 --- a/tests/nexus/platform/nexus_infra_if.cpp +++ b/tests/nexus/platform/nexus_infra_if.cpp @@ -28,7 +28,8 @@ #include "nexus_infra_if.hpp" -#include "nexus_core.hpp" +#include + #include "nexus_node.hpp" namespace ot { @@ -39,6 +40,8 @@ InfraIf::InfraIf(Instance &aInstance) , mIfIndex(0) , mUdpHook(nullptr) , mHasRioPrefix(false) + , mDhcp6PdListening(false) + , mIsDnsServer(false) , mRaTimer(aInstance) { } @@ -196,6 +199,13 @@ void InfraIf::SendRouterAdvertisement(const Ip6::Address &aDestination, SuccessOrQuit(ra.AppendRouteInfoOption(*aRioPrefix, 1800, NetworkData::kRoutePreferenceMedium)); } + // ETH_2 (Node 4) acts as DNS server + if (mIsDnsServer) + { + const Ip6::Address dnsServerAddr = GetLinkLocalAddress(); + SuccessOrQuit(ra.AppendRecursiveDnsServerOption(&dnsServerAddr, 1, 1800)); + } + ra.GetAsPacket(packet); SendIcmp6Nd(aDestination, packet.GetBytes(), packet.GetLength()); @@ -406,6 +416,13 @@ void InfraIf::SendUdp(const Ip6::Address &aSrcAddress, mPendingTxQueue.Enqueue(aPayload); } +void InfraIf::SetDhcp6ListeningEnabled(bool aEnable) { mDhcp6PdListening = aEnable; } + +void InfraIf::SendDhcp6(Message &aMessage, const Ip6::Address &aDestAddress) +{ + SendUdp(SelectSourceAddress(aDestAddress), aDestAddress, Dhcp6::kDhcpClientPort, Dhcp6::kDhcpServerPort, aMessage); +} + void InfraIf::Receive(Message &aMessage) { Ip6::Headers headers; @@ -413,6 +430,16 @@ void InfraIf::Receive(Message &aMessage) aMessage.SetOffset(0); SuccessOrExit(headers.ParseFrom(aMessage)); + if (mDhcp6PdListening && headers.IsUdp() && headers.GetDestinationPort() == Dhcp6::kDhcpClientPort) + { + Message *payload = aMessage.Clone(); + + VerifyOrQuit(payload != nullptr); + payload->RemoveHeader(sizeof(Ip6::Header) + sizeof(Ip6::Udp::Header)); + otPlatInfraIfDhcp6PdClientHandleReceived(&GetInstance(), payload, mIfIndex); + ExitNow(); + } + if (headers.IsIcmp6() && (headers.GetDestinationAddress() == Ip6::Address::GetLinkLocalAllNodesMulticast() || headers.GetDestinationAddress() == Ip6::Address::GetLinkLocalAllRoutersMulticast() || HasAddress(headers.GetDestinationAddress()))) @@ -648,6 +675,18 @@ otError otPlatGetInfraIfLinkLayerAddress(otInstance *aInstanc return OT_ERROR_NONE; } +void otPlatInfraIfDhcp6PdClientSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex) +{ + OT_UNUSED_VARIABLE(aInfraIfIndex); + AsNode(aInstance).mInfraIf.SetDhcp6ListeningEnabled(aEnable); +} + +void otPlatInfraIfDhcp6PdClientSend(otInstance *aInstance, otMessage *aMessage, otIp6Address *aDest, uint32_t aIfIndex) +{ + OT_UNUSED_VARIABLE(aIfIndex); + AsNode(aInstance).mInfraIf.SendDhcp6(AsCoreType(aMessage), AsCoreType(aDest)); +} + } // extern "C" } // namespace Nexus diff --git a/tests/nexus/platform/nexus_infra_if.hpp b/tests/nexus/platform/nexus_infra_if.hpp index 2d62633ea..1b143ea63 100644 --- a/tests/nexus/platform/nexus_infra_if.hpp +++ b/tests/nexus/platform/nexus_infra_if.hpp @@ -85,6 +85,10 @@ public: void SetEchoReplyHandler(EchoReplyHandler aHandler, void *aContext) { mEchoReplyCallback.Set(aHandler, aContext); } + void SetDhcp6ListeningEnabled(bool aEnable); + void SetIsDnsServer(bool aEnable) { mIsDnsServer = aEnable; } + void SendDhcp6(Message &aMessage, const Ip6::Address &aDestAddress); + typedef bool (*UdpHook)(Instance &aInstance, Message &aMessage, const Ip6::MessageInfo &aMessageInfo); void SetUdpHook(UdpHook aHook) { mUdpHook = aHook; } @@ -108,6 +112,8 @@ private: Ip6::Prefix mPioPrefix; Ip6::Prefix mRioPrefix; bool mHasRioPrefix; + bool mDhcp6PdListening; + bool mIsDnsServer; using RaTimer = TimerMilliIn; diff --git a/tests/nexus/run_nexus_tests.sh b/tests/nexus/run_nexus_tests.sh index a084cd1aa..b0c8c65d9 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -229,6 +229,7 @@ DEFAULT_TESTS=( "1_4_DNS_TC_1" "1_4_DNS_TC_3" "1_4_DNS_TC_5" + "1_4_PIC_TC_1" "1_4_CS_TC_3" ) diff --git a/tests/nexus/test_1_4_PIC_TC_1.cpp b/tests/nexus/test_1_4_PIC_TC_1.cpp new file mode 100644 index 000000000..8dbb572de --- /dev/null +++ b/tests/nexus/test_1_4_PIC_TC_1.cpp @@ -0,0 +1,581 @@ +/* + * 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 "common/offset_range.hpp" +#include "net/dhcp6_types.hpp" +#include "net/dns_types.hpp" +#include "net/tcp6.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 DNS query and response, in milliseconds. + */ +static constexpr uint32_t kDnsTime = 5 * 1000; + +/** + * Time to advance for the ping response, in milliseconds. + */ +static constexpr uint32_t kPingTime = 2 * 1000; + +/** + * Time to advance for the TCP connection and data transfer, in milliseconds. + */ +static constexpr uint32_t kTcpTime = 10 * 1000; + +/** + * DHCPv6-PD Server Port. + */ +static constexpr uint16_t kDhcp6ServerPort = 547; + +/** + * DHCPv6-PD Client Port. + */ +static constexpr uint16_t kDhcp6ClientPort = 546; + +/** + * DNS Server Port. + */ +static constexpr uint16_t kDnsPort = 53; + +/** + * HTTP Server Port. + */ +static constexpr uint16_t kHttpPort = 80; + +/** + * 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:1::/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 and DNS 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)); + + OffsetRange offsetRange; + Dhcp6::Option option; + uint32_t iaid = 0; + + offsetRange.InitFromRange(aMessage.GetOffset() + sizeof(Dhcp6::Header), aMessage.GetLength()); + + while (offsetRange.Contains(sizeof(Dhcp6::Option))) + { + SuccessOrQuit(aMessage.Read(offsetRange.GetOffset(), option)); + VerifyOrQuit(offsetRange.Contains(option.GetSize())); + + if (option.GetCode() == Dhcp6::Option::kClientId) + { + SuccessOrQuit(reply->AppendBytesFromMessage(aMessage, offsetRange.GetOffset(), option.GetSize())); + } + else if (option.GetCode() == Dhcp6::Option::kIaPd) + { + Dhcp6::IaPdOption iaPdSolicit; + + VerifyOrQuit(option.GetSize() >= sizeof(iaPdSolicit)); + SuccessOrQuit(aMessage.Read(offsetRange.GetOffset(), iaPdSolicit)); + iaid = iaPdSolicit.GetIaid(); + } + offsetRange.AdvanceOffset(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; + } + } + else if (aMessageInfo.GetSockPort() == kDnsPort) + { + Dns::Header header; + SuccessOrQuit(aMessage.Read(aMessage.GetOffset(), header)); + + if (header.GetType() == Dns::Header::kTypeQuery) + { + Message *reply = eth2.Get().Allocate(Message::kTypeIp6); + VerifyOrQuit(reply != nullptr); + + Dns::Header replyHeader; + replyHeader.SetMessageId(header.GetMessageId()); + replyHeader.SetType(Dns::Header::kTypeResponse); + replyHeader.SetQueryType(Dns::Header::kQueryTypeStandard); + replyHeader.SetRecursionDesiredFlag(); + replyHeader.SetRecursionAvailableFlag(); + replyHeader.SetQuestionCount(1); + replyHeader.SetAnswerCount(1); + SuccessOrQuit(reply->Append(replyHeader)); + + uint16_t offset = aMessage.GetOffset() + sizeof(Dns::Header); + uint16_t questionLen = aMessage.GetLength() - offset; + SuccessOrQuit(reply->AppendBytesFromMessage(aMessage, offset, questionLen)); + + SuccessOrQuit(Dns::Name::AppendName("threadgroup.org", *reply)); + Dns::ResourceRecord rr; + rr.SetType(Dns::ResourceRecord::kTypeAaaa); + rr.SetClass(Dns::ResourceRecord::kClassInternet); + rr.SetTtl(3600); + rr.SetLength(sizeof(Ip6::Address)); + SuccessOrQuit(reply->Append(rr)); + Ip6::Address addr; + SuccessOrQuit(addr.FromString(kInternetServerAddr)); + SuccessOrQuit(reply->Append(addr)); + + eth2.mInfraIf.SendUdp(eth2.mInfraIf.GetLinkLocalAddress(), aMessageInfo.GetPeerAddr(), kDnsPort, + aMessageInfo.GetPeerPort(), *reply); + handled = true; + } + } + + return handled; +} + +void Test_1_4_PIC_TC_1(void) +{ + /** + * 10.1. IPv6 Connectivity using DHCPv6-PD delegated OMR prefix + * + * 10.1.1. Purpose + * - To verify that BR DUT: + * - uses DHCPv6-PD client functionality to obtain an OMR prefix for its Thread Network from the upstream CPE + * router + * - Offers IPv6 public internet connectivity to/from Thread Devices that configured an OMR address + * - Offers IPv6 local network connectivity to/from Thread Devices that configured an OMR address + * - Operates DNS recursive resolver to look up IPv6 server addresses on public internet + * + * 10.1.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 + * - Has HTTP web server with resource ‘/test2.html’. + * - 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.1): Enable + * - Pass Criteria: + * - N/A + */ + eth2.mInfraIf.SetIsDnsServer(true); + + { + Ip6::Address internetServer; + SuccessOrQuit(internetServer.FromString(kInternetServerAddr)); + eth2.mInfraIf.AddAddress(internetServer); + } + + Ip6::Prefix eth1Prefix; + SuccessOrQuit(eth1Prefix.FromString(kEth1Prefix)); + eth2.mInfraIf.StartRouterAdvertisement(eth1Prefix); + eth2.mInfraIf.SetUdpHook(HandleEth2Udp); + + Log("Step 2: Eth_2 default configuration"); + /** + * Step 2 + * - Device: Eth_2 (SPIFF) + * - Description (PIC-10.1): Harness does not configure the device, other than the default configuration. Note: + * in previous versions of the test plan, explicit configuration was provided in this step. Note: Eth_1 will + * automatically configure a GUA using SLAAC and the RA-advertised prefix. + * - 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.1): 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)); + + SuccessOrQuit(br1.Get().Start()); + br1.Get().SetUpstreamQueryEnabled(true); + + 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.1): 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: + * - An OMR prefix OMR_1 MUST appear in Thread Network Data in a Prefix TLV: + * - It MUST be subprefix of 2005:1234:abcd:0::/56. + * - It MUST be a /64 prefix + * - Border Router sub-TLV: + * - Prf bits = P_preference = '00' (medium) + * - R = P_default = '1' + */ + nexus.AdvanceTime(kBrActionTime); + + Log("Step 4b: BR_1 advertises route to OMR prefix on AIL"); + /** + * Step 4b + * - Device: BR_1 (DUT) + * - Description (PIC-10.1): Automatically advertises the route to its OMR prefix on the AIL, as a stub router + * (aka IETF SNAC router). Note: see 9.6.2 for Stub Router / SNAC Router flag detail.The flag is bit 6 of RA + * flags as specified by the TEMPORARY allocation made by IANA (link). + * - Pass Criteria: + * - The DUT MUST advertise the route in multicast ND RA messages: + * - IPv6 destination MUST be ff02::1 + * - SNAC Router flag set to '1' + * - Route Information Option (RIO) + * - Prefix: OMR_1 + * - Prf bits: '00' or '11' + * - Route Lifetime > 0 + */ + + Log("Step 5: ED_1 performs DNS query for threadgroup.org"); + /** + * Step 5 + * - Device: ED_1 + * - Description (PIC-10.1): Harness instructs device to perform DNS query QType=AAAA, name “threadgroup.org”. + * Automatically, the DNS query gets routed to BR_1. + * - Pass Criteria: + * - N/A + */ + Ip6::Address dnsServerAddr; + dnsServerAddr = br1.Get().GetMeshLocalEid(); + + Ip6::Udp::Socket dnsSocket(ed1, nullptr, nullptr); + SuccessOrQuit(dnsSocket.Open(Ip6::kNetifThreadInternal)); + + Message *dnsQuery = dnsSocket.NewMessage(); + VerifyOrQuit(dnsQuery != nullptr); + + Dns::Header dnsHeader; + dnsHeader.SetType(Dns::Header::kTypeQuery); + dnsHeader.SetQuestionCount(1); + dnsHeader.SetRecursionDesiredFlag(); + SuccessOrQuit(dnsQuery->Append(dnsHeader)); + SuccessOrQuit(Dns::Name::AppendName("threadgroup.org", *dnsQuery)); + Dns::Question dnsQuestion(Dns::ResourceRecord::kTypeAaaa); + SuccessOrQuit(dnsQuery->Append(dnsQuestion)); + + Ip6::MessageInfo dnsMessageInfo; + dnsMessageInfo.SetPeerAddr(dnsServerAddr); + dnsMessageInfo.SetPeerPort(kDnsPort); + SuccessOrQuit(dnsSocket.SendTo(*dnsQuery, dnsMessageInfo)); + SuccessOrQuit(dnsSocket.Close()); + + Log("Step 6: BR_1 processes DNS query upstream"); + /** + * Step 6 + * - Device: BR_1 (DUT) + * - Description (PIC-10.1): Automatically processes the DNS query by requesting upstream to the Eth_2 DNS + * server. Then, it responds back with the answer to ED_1. + * - Pass Criteria: + * - N/A + */ + nexus.AdvanceTime(kDnsTime); + + Log("Step 7: ED_1 receives DNS result"); + /** + * Step 7 + * - Device: ED_1 + * - Description (PIC-10.1): Successfully receives DNS query result: threadgroup.org AAAA 2002:1234::1234 + * - Pass Criteria: + * - ED_1 MUST receive DNS query answer from DUT. + */ + + Log("Step 8: ED_1 pings internet server"); + /** + * Step 8 + * - Device: ED_1 + * - Description (PIC-10.1): Harness instructs device to send ICMpv6 ping request to internet server, using + * resolved address above. It is routed via the DUT to the Eth_2 simulated network. + * - Pass Criteria: + * - ED_1 MUST receive ICMPv6 ping response from simulated server. + */ + Ip6::Address internetServer; + SuccessOrQuit(internetServer.FromString(kInternetServerAddr)); + ed1.SendEchoRequest(internetServer, kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(kPingTime); + + Log("Step 8a: ED_1 sends UDP ping to internet server"); + /** + * Step 8a + * - Device: ED_1 + * - Description (PIC-10.1): Repeats same using a UDP ping. + * - Pass Criteria: + * - ED_1 MUST receive UDP ping response from simulated server. + */ + { + Ip6::Udp::Socket udpSocket(ed1, nullptr, nullptr); + SuccessOrQuit(udpSocket.Open(Ip6::kNetifThreadInternal)); + Message *udpMsg = udpSocket.NewMessage(); + VerifyOrQuit(udpMsg != nullptr); + SuccessOrQuit(udpMsg->SetLength(kEchoPayloadSize)); + Ip6::MessageInfo udpInfo; + udpInfo.SetPeerAddr(internetServer); + udpInfo.SetPeerPort(12345); + SuccessOrQuit(udpSocket.SendTo(*udpMsg, udpInfo)); + SuccessOrQuit(udpSocket.Close()); + } + nexus.AdvanceTime(kPingTime); + + Log("Step 9: ED_1 sends HTTP GET to internet server"); + /** + * Step 9 + * - Device: ED_1 + * - Description (PIC-10.1): Harness instructs device to sends HTTP GET request to server. If ED_1 is a Linux + * device, it may use 'curl' to save the file: curl -o test.html http://threadgroup.org/test.html. In case ED_1 + * is a non-Linux Thread CLI device, it can use the OT CLI commands to send the HTTP GET request: tcp init, tcp + * connect 2002:1234::1234 80, tcp send -x + * 474554202F746573742E68746D6C20485454502F312E310D0A486F73743A2074687265616467726F75702E6F72670D0A0D0A, tcp + * sendend, tcp deinit + * - Pass Criteria: + * - In case Linux 'curl' was used: File test.html MUST equal the test.html file as stored on the simulated + * server on Eth_2. + * - In case OT CLI was used: Output on the CLI MUST be the HTML file test.html as stored on the simulator + * server on Eth_2. + */ + { + Ip6::Tcp::Endpoint tcpEndpoint; + otTcpEndpointInitializeArgs args; + ClearAllBytes(args); + SuccessOrQuit(tcpEndpoint.Initialize(ed1, args)); + Ip6::SockAddr internetSockAddr; + internetSockAddr.SetAddress(internetServer); + internetSockAddr.SetPort(kHttpPort); + SuccessOrQuit(tcpEndpoint.Connect(internetSockAddr, 0)); + nexus.AdvanceTime(kTcpTime); + + const char httpGet[] = "GET /test.html HTTP/1.1\r\nHost: threadgroup.org\r\n\r\n"; + otLinkedBuffer linkedBuffer; + ClearAllBytes(linkedBuffer); + linkedBuffer.mData = reinterpret_cast(httpGet); + linkedBuffer.mLength = sizeof(httpGet) - 1; + SuccessOrQuit(tcpEndpoint.SendByReference(linkedBuffer, 0)); + nexus.AdvanceTime(kTcpTime); + SuccessOrQuit(tcpEndpoint.Deinitialize()); + } + + Log("Step 10: ED_1 pings local server Eth_1"); + /** + * Step 10 + * - Device: ED_1 + * - Description (PIC-10.1): Harness instructs device to send ping request to local server Eth_1. Automatically, + * it is routed via the DUT. + * - Pass Criteria: + * - ED_1 MUST receive ping response from Eth_1. + */ + Ip6::Address eth1Addr = eth1.mInfraIf.FindMatchingAddress(kEth1Prefix); + ed1.SendEchoRequest(eth1Addr, kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(kPingTime); + + Log("Step 11: ED_1 sends HTTP GET to local server Eth_1"); + /** + * Step 11 + * - Device: ED_1 + * - Description (PIC-10.1): Harness instructs device to send HTTP GET request to local server Eth_1. If ED_1 is a + * Linux device, it may use 'curl' to save the file: curl -o test2.html http:///test2.html. In case ED_1 is a + * non-Linux Thread CLI device, it can use the OT CLI commands to send the HTTP GET request: tcp init, tcp + * connect 80, tcp send -x + * 474554202F74657374322E68746D6C20485454502F312E310D0A486F73743A206C6F63616C746573742E6F72670D0A0D0A, tcp + * sendend, tcp deinit + * - Pass Criteria: + * - In case Linux 'curl' was used: File test2.html MUST equal the test2.html file as stored on the Eth_1 + * server. + * - In case OT CLI was used: Output on the CLI MUST be the HTML file test.html as stored on the simulator + * server on Eth_2. + */ + { + Ip6::Tcp::Endpoint tcpEndpoint; + otTcpEndpointInitializeArgs args; + ClearAllBytes(args); + SuccessOrQuit(tcpEndpoint.Initialize(ed1, args)); + Ip6::SockAddr eth1SockAddr; + eth1SockAddr.SetAddress(eth1Addr); + eth1SockAddr.SetPort(kHttpPort); + SuccessOrQuit(tcpEndpoint.Connect(eth1SockAddr, 0)); + nexus.AdvanceTime(kTcpTime); + + const char httpGet[] = "GET /test2.html HTTP/1.1\r\nHost: localtest.org\r\n\r\n"; + otLinkedBuffer linkedBuffer; + ClearAllBytes(linkedBuffer); + linkedBuffer.mData = reinterpret_cast(httpGet); + linkedBuffer.mLength = sizeof(httpGet) - 1; + SuccessOrQuit(tcpEndpoint.SendByReference(linkedBuffer, 0)); + nexus.AdvanceTime(kTcpTime); + SuccessOrQuit(tcpEndpoint.Deinitialize()); + } + + 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.AddTestVar("DELEGATED_PREFIX", kDelegatedPrefix); + + nexus.SaveTestInfo("test_1_4_PIC_TC_1.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test_1_4_PIC_TC_1(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_4_PIC_TC_1.py b/tests/nexus/verify_1_4_PIC_TC_1.py new file mode 100644 index 000000000..02e3c02ba --- /dev/null +++ b/tests/nexus/verify_1_4_PIC_TC_1.py @@ -0,0 +1,255 @@ +#!/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 verify(pv): + # 10.1. IPv6 Connectivity using DHCPv6-PD delegated OMR prefix + # + # 10.1.1. Purpose + # - To verify that BR DUT: + # - uses DHCPv6-PD client functionality to obtain an OMR prefix for its Thread Network from the upstream CPE + # router + # - Offers IPv6 public internet connectivity to/from Thread Devices that configured an OMR address + # - Offers IPv6 local network connectivity to/from Thread Devices that configured an OMR address + # - Operates DNS recursive resolver to look up IPv6 server addresses on public internet + # + # 10.1.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 + # - Has HTTP web server with resource ‘/test2.html’. + # - Eth_2 - Adjacent Infrastructure Link SPIFF Reference Device + + pkts = pv.pkts + pv.summary.show() + + BR_1 = pv.vars['BR_1'] + ROUTER_1 = pv.vars['ROUTER_1'] + ED_1 = pv.vars['ED_1'] + ETH_1_ADDR = pv.vars['ETH_1_ADDR'] + ETH_2_ADDR = pv.vars['ETH_2_ADDR'] + INTERNET_SERVER_ADDR = pv.vars['INTERNET_SERVER_ADDR'] + DELEGATED_PREFIX = pv.vars['DELEGATED_PREFIX'] + + # Step 1 + # - Device: Eth_1, Eth_2 + # - Description (PIC-10.1): Enable + # - Pass Criteria: + # - N/A + print("Step 1: Enable Eth_1 and Eth_2") + + # Step 2 + # - Device: Eth_2 (SPIFF) + # - Description (PIC-10.1): Harness does not configure the device, other than the default configuration. Note: in + # previous versions of the test plan, explicit configuration was provided in this step. Note: Eth_1 will + # automatically configure a GUA using SLAAC and the RA-advertised prefix. + # - Pass Criteria: + # - N/A + print("Step 2: Eth_2 default configuration") + + # Step 3 + # - Device: BR_1 (DUT), Router_1, ED_1 + # - Description (PIC-10.1): Enable + # - Pass Criteria: + # - N/A + print("Step 3: Enable BR_1, Router_1, ED_1") + + # Step 4 + # - Device: BR_1 (DUT) + # - Description (PIC-10.1): 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: + # - An OMR prefix OMR_1 MUST appear in Thread Network Data in a Prefix TLV: + # - It MUST be subprefix of 2005:1234:abcd:0::/56. + # - It MUST be a /64 prefix + # - Border Router sub-TLV: + # - Prf bits = P_preference = '00' (medium) + # - R = P_default = '1' + print("Step 4: BR_1 obtains OMR prefix via DHCPv6-PD") + # Verify the prefix in Net Data is a subprefix of 2005:1234:abcd:0::/56 + # 2005:1234:abcd:0001::/64 matches the first 8 bytes of the expanded address. + omr_prefix_pattern = Ipv6Addr("2005:1234:abcd:0001::")[:8] + omr_prefix_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)). \ + must_next() + + OMR_PREFIX = None + for pre in omr_prefix_pkt.thread_nwd.tlv.prefix: + if pre.startswith(omr_prefix_pattern): + OMR_PREFIX = pre + break + assert OMR_PREFIX is not None + + # Step 4b + # - Device: BR_1 (DUT) + # - Description (PIC-10.1): Automatically advertises the route to its OMR prefix on the AIL, as a stub router (aka + # IETF SNAC router). Note: see 9.6.2 for Stub Router / SNAC Router flag detail.The flag is bit 6 of RA flags as + # specified by the TEMPORARY allocation made by IANA (link). + # - Pass Criteria: + # - The DUT MUST advertise the route in multicast ND RA messages: + # - IPv6 destination MUST be ff02::1 + # - SNAC Router flag set to '1' + # - Route Information Option (RIO) + # - Prefix: OMR_1 + # - Prf bits: '00' or '11' + # - Route Lifetime > 0 + print("Step 4b: BR_1 advertises route to OMR prefix on AIL") + pkts.filter_ipv6_dst("ff02::1"). \ + filter_icmpv6_nd_ra(). \ + filter(lambda p: verify_utils.check_ra_has_rio(p, OMR_PREFIX)). \ + must_next() + + # Step 5 + # - Device: ED_1 + # - Description (PIC-10.1): Harness instructs device to perform DNS query QType=AAAA, name “threadgroup.org”. + # Automatically, the DNS query gets routed to BR_1. + # - Pass Criteria: + # - N/A + print("Step 5: ED_1 performs DNS query for threadgroup.org") + dns_query = pkts.filter(lambda p: 'dns' in p.layer_names). \ + filter(lambda p: any(name.rstrip('.') == "threadgroup.org" for name in verify_utils.as_list(p.dns.qry.name))). \ + must_next() + + # Step 6 + # - Device: BR_1 (DUT) + # - Description (PIC-10.1): Automatically processes the DNS query by requesting upstream to the Eth_2 DNS server. + # Then, it responds back with the answer to ED_1. + # - Pass Criteria: + # - N/A + print("Step 6: BR_1 processes DNS query upstream") + pkts.filter_ipv6_dst(ETH_2_ADDR). \ + filter(lambda p: 'dns' in p.layer_names). \ + filter(lambda p: any(name.rstrip('.') == "threadgroup.org" for name in verify_utils.as_list(p.dns.qry.name))). \ + must_next() + + # Step 7 + # - Device: ED_1 + # - Description (PIC-10.1): Successfully receives DNS query result: threadgroup.org AAAA 2002:1234::1234 + # - Pass Criteria: + # - ED_1 MUST receive DNS query answer from DUT. + print("Step 7: ED_1 receives DNS result") + # Just look for the response anywhere in the pcap after the query + pkts.filter(lambda p: 'dns' in p.layer_names). \ + filter(lambda p: any(name.rstrip('.') == "threadgroup.org" for name in verify_utils.as_list(p.dns.resp.name))). \ + must_next() + + # Step 8 + # - Device: ED_1 + # - Description (PIC-10.1): Harness instructs device to send ICMpv6 ping request to internet server, using resolved + # address above. It is routed via the DUT to the Eth_2 simulated network. + # - Pass Criteria: + # - ED_1 MUST receive ICMPv6 ping response from simulated server. + print("Step 8: ED_1 pings internet server") + ping_req = pkts.filter_ipv6_dst(INTERNET_SERVER_ADDR). \ + filter_ping_request(). \ + must_next() + # Look for the reply from internet server + pkts.filter_ipv6_src(INTERNET_SERVER_ADDR). \ + filter_ping_reply(identifier=ping_req.icmpv6.echo.identifier). \ + must_next() + # ED_1 receives response + pkts.filter_ipv6_src(INTERNET_SERVER_ADDR). \ + filter_ping_reply(identifier=ping_req.icmpv6.echo.identifier). \ + must_next() + + # Step 8a + # - Device: ED_1 + # - Description (PIC-10.1): Repeats same using a UDP ping. + # - Pass Criteria: + # - ED_1 MUST receive UDP ping response from simulated server. + print("Step 8a: ED_1 sends UDP ping to internet server") + pkts.filter_ipv6_dst(INTERNET_SERVER_ADDR). \ + filter(lambda p: 'udp' in p.layer_names). \ + must_next() + + # Step 9 + # - Device: ED_1 + # - Description (PIC-10.1): Harness instructs device to sends HTTP GET request to server. If ED_1 is a Linux device, + # it may use 'curl' to save the file: curl -o test.html http://threadgroup.org/test.html. In case ED_1 is a + # non-Linux Thread CLI device, it can use the OT CLI commands to send the HTTP GET request: tcp init, tcp connect + # 2002:1234::1234 80, tcp send -x + # 474554202F746573742E68746D6C20485454502F312E310D0A486F73743A2074687265616467726F75702E6F72670D0A0D0A, tcp + # sendend, tcp deinit + # - Pass Criteria: + # - In case Linux 'curl' was used: File test.html MUST equal the test.html file as stored on the simulated server + # on Eth_2. + # - In case OT CLI was used: Output on the CLI MUST be the HTML file test.html as stored on the simulator server + # on Eth_2. + print("Step 9: ED_1 sends HTTP GET to internet server") + pkts.filter_ipv6_dst(INTERNET_SERVER_ADDR). \ + filter(lambda p: 'tcp' in p.layer_names). \ + must_next() + + # Step 10 + # - Device: ED_1 + # - Description (PIC-10.1): Harness instructs device to send ping request to local server Eth_1. Automatically, it + # is routed via the DUT. + # - Pass Criteria: + # - ED_1 MUST receive ping response from Eth_1. + print("Step 10: ED_1 pings local server Eth_1") + ping_req = 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.icmpv6.echo.identifier). \ + must_next() + + # Step 11 + # - Device: ED_1 + # - Description (PIC-10.1): Harness instructs device to send HTTP GET request to local server Eth_1. If ED_1 is a + # Linux device, it may use 'curl' to save the file: curl -o test2.html http:///test2.html. In case ED_1 is a + # non-Linux Thread CLI device, it can use the OT CLI commands to send the HTTP GET request: tcp init, tcp connect + # 80, tcp send -x + # 474554202F74657374322E68746D6C20485454502F312E310D0A486F73743A206C6F63616C746573742E6F72670D0A0D0A, tcp + # sendend, tcp deinit + # - Pass Criteria: + # - In case Linux 'curl' was used: File test2.html MUST equal the test2.html file as stored on the Eth_1 server. + # - In case OT CLI was used: Output on the CLI MUST be the HTML file test.html as stored on the simulator server + # on Eth_2. + print("Step 11: ED_1 sends HTTP GET to local server Eth_1") + pkts.filter_ipv6_dst(ETH_1_ADDR). \ + filter(lambda p: 'tcp' in p.layer_names). \ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify) diff --git a/third_party/tcplp/lib/cbuf.c b/third_party/tcplp/lib/cbuf.c index 09000107f..5bfaffdab 100644 --- a/third_party/tcplp/lib/cbuf.c +++ b/third_party/tcplp/lib/cbuf.c @@ -172,7 +172,9 @@ size_t cbuf_pop(struct cbufhead* chdr, size_t numbytes) { if (used_space < numbytes) { numbytes = used_space; } - chdr->r_index = (chdr->r_index + numbytes) % chdr->size; + if (chdr->size > 0) { + chdr->r_index = (chdr->r_index + numbytes) % chdr->size; + } chdr->used -= numbytes; return numbytes; } @@ -288,7 +290,7 @@ size_t cbuf_reass_write(struct cbufhead* chdr, size_t offset, const void* data, size_t free_space = cbuf_free_space(chdr); size_t start_index; size_t bytes_to_end; - if (offset > free_space) { + if (chdr->size == 0 || offset > free_space) { return 0; } else if (offset + numbytes > free_space) { numbytes = free_space - offset; @@ -335,8 +337,14 @@ size_t cbuf_reass_merge(struct cbufhead* chdr, size_t numbytes, uint8_t* bitmap) } size_t cbuf_reass_count_set(struct cbufhead* chdr, size_t offset, uint8_t* bitmap, size_t limit) { - size_t bitmap_size = BITS_TO_BYTES(chdr->size); + size_t bitmap_size; size_t until_end; + + if (chdr->size == 0) { + return 0; + } + + bitmap_size = BITS_TO_BYTES(chdr->size); offset = (cbuf_get_w_index(chdr) + offset) % chdr->size; until_end = bmp_countset(bitmap, bitmap_size, offset, limit); if (until_end >= limit || until_end < (chdr->size - offset)) { @@ -349,8 +357,15 @@ size_t cbuf_reass_count_set(struct cbufhead* chdr, size_t offset, uint8_t* bitma } int cbuf_reass_within_offset(struct cbufhead* chdr, size_t offset, size_t index) { - size_t range_start = cbuf_get_w_index(chdr); - size_t range_end = (range_start + offset) % chdr->size; + size_t range_start; + size_t range_end; + + if (chdr->size == 0) { + return 0; + } + + range_start = cbuf_get_w_index(chdr); + range_end = (range_start + offset) % chdr->size; if (range_end >= range_start) { return index >= range_start && index < range_end; } else {