diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index dd9a92524..929ded788 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -298,6 +298,7 @@ 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_PIC_TC_4 "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 d5e2cad75..e4ef5be52 100755 --- a/tests/nexus/run_nexus_tests.sh +++ b/tests/nexus/run_nexus_tests.sh @@ -231,6 +231,7 @@ DEFAULT_TESTS=( "1_4_DNS_TC_5" "1_4_PIC_TC_1" "1_4_PIC_TC_3" + "1_4_PIC_TC_4" "1_4_CS_TC_3" ) diff --git a/tests/nexus/test_1_4_PIC_TC_4.cpp b/tests/nexus/test_1_4_PIC_TC_4.cpp new file mode 100644 index 000000000..117d81e76 --- /dev/null +++ b/tests/nexus/test_1_4_PIC_TC_4.cpp @@ -0,0 +1,484 @@ +/* + * 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 "core/net/ip4_types.hpp" +#include "core/net/nat64_translator.hpp" +#include "net/checksum.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 { + +static constexpr uint32_t kFormNetworkTime = 13 * 1000; +static constexpr uint32_t kJoinNetworkTime = 200 * 1000; +static constexpr uint32_t kBrActionTime = 30 * 1000; +static constexpr uint32_t kDnsTime = 5 * 1000; +static constexpr uint32_t kPingTime = 2 * 1000; +static constexpr uint32_t kTcpTime = 10 * 1000; +static constexpr uint16_t kDnsPort = 53; +static constexpr uint16_t kHttpPort = 80; +static constexpr uint16_t kEchoIdentifier = 0x1234; +static constexpr uint16_t kEchoPayloadSize = 10; +static constexpr uint32_t kInfraIfIndex = 1; + +static const char kInternetServerIp4Addr[] = "38.106.217.165"; +static const char kLocalServerIp4Addr[] = "192.168.1.4"; +static const char kEth2Prefix[] = "2001:db8:1::/64"; +static const char kNat64Cidr[] = "192.168.100.0/24"; + +static bool HandleEth2Udp(Instance &aInstance, Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Node ð2 = Node::From(&aInstance); + bool handled = false; + + 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("threadv4.org", *reply)); + + Dns::ResourceRecord rr; + rr.SetType(Dns::ResourceRecord::kTypeA); + rr.SetClass(Dns::ResourceRecord::kClassInternet); + rr.SetTtl(3600); + rr.SetLength(sizeof(Ip4::Address)); + SuccessOrQuit(reply->Append(rr)); + + Ip4::Address addr; + SuccessOrQuit(addr.FromString(kInternetServerIp4Addr)); + SuccessOrQuit(reply->Append(addr)); + + eth2.mInfraIf.SendUdp(eth2.mInfraIf.GetLinkLocalAddress(), aMessageInfo.GetPeerAddr(), kDnsPort, + aMessageInfo.GetPeerPort(), *reply); + handled = true; + } + } + + return handled; +} + +static void HandleIp4Receive(otMessage *aMessage, void *aContext) +{ + Node *br1 = static_cast(aContext); + Message *message = static_cast(aMessage); + Ip4::Headers ip4Headers; + + VerifyOrQuit(ip4Headers.ParseFrom(*message) == kErrorNone); + + if (ip4Headers.IsIcmp4()) + { + Log("Received ICMPv4 packet"); + Ip4::Icmp::Header icmpHeader; + SuccessOrQuit(message->Read(sizeof(Ip4::Header), icmpHeader)); + + if (icmpHeader.GetType() == Ip4::Icmp::Header::kTypeEchoRequest) + { + Log("Received ICMPv4 Echo Request, sending reply"); + uint16_t payloadLen = message->GetLength() - sizeof(Ip4::Header) - sizeof(Ip4::Icmp::Header); + + OwnedPtr replyMessage; + replyMessage.Reset(br1->Get().Allocate(Message::kTypeIp4)); + VerifyOrQuit(replyMessage != nullptr); + + Ip4::Header ip4Header; + ip4Header.Clear(); + ip4Header.SetVersionIhl(0x45); + ip4Header.SetTotalLength(sizeof(Ip4::Header) + sizeof(Ip4::Icmp::Header) + payloadLen); + ip4Header.SetProtocol(Ip4::kProtoIcmp); + ip4Header.SetTtl(64); + ip4Header.SetSource(ip4Headers.GetDestinationAddress()); + ip4Header.SetDestination(ip4Headers.GetSourceAddress()); + SuccessOrQuit(replyMessage->Append(ip4Header)); + + Ip4::Icmp::Header replyIcmpHeader; + replyIcmpHeader.Clear(); + replyIcmpHeader.SetType(Ip4::Icmp::Header::kTypeEchoReply); + replyIcmpHeader.SetRestOfHeader(icmpHeader.GetRestOfHeader()); + SuccessOrQuit(replyMessage->Append(replyIcmpHeader)); + + // Append the rest of the ICMP request (Id, Sequence, and Payload) + SuccessOrQuit(replyMessage->AppendBytesFromMessage( + *message, sizeof(Ip4::Header) + sizeof(Ip4::Icmp::Header), payloadLen)); + + replyMessage->SetOffset(sizeof(Ip4::Header)); + Checksum::UpdateMessageChecksum(*replyMessage, ip4Header.GetSource(), ip4Header.GetDestination(), + ip4Header.GetProtocol()); + replyMessage->SetOffset(0); + + Checksum::UpdateIp4HeaderChecksum(ip4Header); + replyMessage->Write(0, ip4Header); + + SuccessOrQuit(br1->Get().SendMessage(replyMessage.PassOwnership())); + } + } +} + +void Test_1_4_PIC_TC_4(void) +{ + /** + * 10.4 IPv4 Connectivity using BR built-in NAT64 + * 10.4.1. Purpose + * - To verify that BR DUT: + * - Offers IPv4 internet connectivity to/from Thread Devices using its built-in NAT64 translator, when the + * infrastructure network does not provide a NAT64 translator. + * - Offers IPv4 local network connectivity to/from Thread Devices using its built-in NAT64 translator + * - Operates DNS recursive resolver to look up IPv4 server addresses + * - Withdraws its own NAT64 prefix when another Border Router advertises a preferred NAT64 prefix. TBD - to + * be added. + * - Note: the present test requires a /96 NAT64 prefix, which is stricter than what the specification requires. + * RFC 6052 (Section 2.2) allows other lengths as well; however the present test case is not yet written to + * support all these different options. + * 10.4.2. Topology + * - BR_1 (DUT) - Border Router + * - Router_1 - Thread Router Reference Device + * - ED_1 - Thread Reference Device, End Device (e.g. FED/REED) role + * - Eth_1 - Adjacent Infrastructure Link Linux Reference Device on AIL + * - Has HTTP web server with resource ‘/test2.html’. + * - Eth_2 - Adjacent Infrastructure Link SPIFF Reference Device on AIL + * - Has HTTP web server on simulated-internet with resource ‘/testv4.html’ + */ + + Core nexus; + + Node &br1 = nexus.CreateNode(); + Node &r1 = nexus.CreateNode(); + Node &ed1 = nexus.CreateNode(); + Node ð2 = nexus.CreateNode(); + + br1.SetName("BR_1"); + r1.SetName("ROUTER_1"); + ed1.SetName("ED_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.4): Enable Harness configures Eth_1 with IPv4 address 192.168.1.4. + * - Pass Criteria: N/A + */ + eth2.mInfraIf.SetIsDnsServer(true); + eth2.mInfraIf.SetUdpHook(HandleEth2Udp); + + Ip6::Prefix eth2Prefix; + SuccessOrQuit(eth2Prefix.FromString(kEth2Prefix)); + eth2.mInfraIf.StartRouterAdvertisement(eth2Prefix); + + Log("Step 2: Eth_2 default configuration"); + /** + * Step 2 + * - Device: Eth_2 + * - Description (PIC-10.4): Harness instructs device to: Enable DHCP IPv4 server. + * - 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.4): 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(); + 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 configures IPv4 address and NAT64 prefix"); + /** + * Step 4 + * - Device: BR_1 (DUT) + * - Description (PIC-10.4): Automatically configures an IPv4 address on the AIL; and identifies the IPv4 gateway + * on Eth_2. Automatically configures a NAT64 prefix in the Thread Network Data. Note: the check for length 96 + * is not due to it being required by, but because the current test script is only designed to handle this + * case. Other cases are TBD. See note in Section 10.4.1 above. + * - Pass Criteria: + * - The DUT MUST add one NAT64 prefix in the Thread Network Data, which is a route entry published as follows: + * - Prefix TLV + * - Prefix Length field MUST be 96. + */ + Ip4::Cidr nat64Cidr; + SuccessOrQuit(nat64Cidr.FromString(kNat64Cidr)); + + br1.Get().SetNat64ReceiveIp4Callback(HandleIp4Receive, &br1); + SuccessOrQuit(br1.Get().SetIp4Cidr(nat64Cidr)); + br1.Get().SetNat64PrefixManagerEnabled(true); + + nexus.AdvanceTime(kBrActionTime); + + Ip6::Prefix nat64Prefix; + SuccessOrQuit(br1.Get().GetNat64Prefix(nat64Prefix)); + + br1.Get().SetNat64Prefix(nat64Prefix); + br1.Get().SetEnabled(true); + + Log("Step 5: ED_1 performs DNS query for threadv4.org"); + /** + * Step 5 + * - Device: ED_1 + * - Description (PIC-10.4): Harness instructs device to perform DNS query QType=ANY, name “threadv4.org”. + * Automatically, the DNS query gets routed to the DUT. + * - 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("threadv4.org", *dnsQuery)); + Dns::Question dnsQuestion(Dns::ResourceRecord::kTypeAny); + 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.4): 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.4): Successfully receives DNS query result. + * - Pass Criteria: + * - ED_1 MUST receive DNS query answer from DUT: + * - threadv4.org A 38.106.217.165 + */ + + Log("Step 8: ED_1 pings internet server"); + /** + * Step 8 + * - Device: ED_1 + * - Description (PIC-10.4): Harness instructs device to send ICMPv6 ping request to internet server, using + * synthesized IPv6 address: :266a:d9a5 Request is routed via the DUT to the Eth_2 simulated + * network. The DUT translates the above IPv6 address into the IPv4 destination address 38.106.217.165. + * - Pass Criteria: + * - ED_1 MUST receive ICMPv6 ping response from simulated server. + */ + Ip4::Address internetServerIp4; + SuccessOrQuit(internetServerIp4.FromString(kInternetServerIp4Addr)); + Ip6::Address internetServerIp6; + internetServerIp6.SynthesizeFromIp4Address(nat64Prefix, internetServerIp4); + + ed1.SendEchoRequest(internetServerIp6, kEchoIdentifier, kEchoPayloadSize); + nexus.AdvanceTime(kPingTime); + + Log("Step 8a: ED_1 sends UDP ping to internet server"); + /** + * Step 8a + * - Device: ED_1 + * - Description (PIC-10.4): 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(internetServerIp6); + 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.4): Harness instructs device to send HTTP GET request to server. If ED_1 is a Linux + * device, it may use 'curl' to save the file: curl -o testv4.html http://threadv4.org/testv4.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 :266a:d9a5 80 tcp send -x 474554202F74... tcp sendend tcp deinit + * - Pass Criteria: + * - In case Linux 'curl' was used: File testv4.html MUST equal the testv4.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 testv4.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(internetServerIp6); + internetSockAddr.SetPort(kHttpPort); + SuccessOrQuit(tcpEndpoint.Connect(internetSockAddr, 0)); + nexus.AdvanceTime(kTcpTime); + + const char httpGet[] = "GET /testv4.html HTTP/1.1\r\nHost: threadv4.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.4): Harness instructs device to send ping request to local IPv4 server Eth_1 which has + * IPv4 destination address 192.168.1.4; using synthesized IPv6 address: :c0a8:0104 Automatically, + * it is routed via BR_1 DUT to Eth_1. + * - Pass Criteria: + * - ED_1 MUST receive ping response from Eth_1. + */ + Ip4::Address localServerIp4; + SuccessOrQuit(localServerIp4.FromString(kLocalServerIp4Addr)); + Ip6::Address localServerIp6; + localServerIp6.SynthesizeFromIp4Address(nat64Prefix, localServerIp4); + + ed1.SendEchoRequest(localServerIp6, 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.4): Harness instructs device to send HTTP GET request to local server Eth_1. In case ED_1 + * is a Linux device, it can use 'curl' to save the file locally: 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 :c0a8:0104 80 tcp send -x 474554202F... 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 test2.html as stored on the server on + * Eth_1. + */ + { + Ip6::Tcp::Endpoint tcpEndpoint; + otTcpEndpointInitializeArgs args; + ClearAllBytes(args); + SuccessOrQuit(tcpEndpoint.Initialize(ed1, args)); + Ip6::SockAddr localSockAddr; + localSockAddr.SetAddress(localServerIp6); + localSockAddr.SetPort(kHttpPort); + SuccessOrQuit(tcpEndpoint.Connect(localSockAddr, 0)); + nexus.AdvanceTime(kTcpTime); + + const char httpGet[] = "GET /test2.html HTTP/1.1\r\nHost: localv4.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_2_ADDR", eth2.mInfraIf.GetLinkLocalAddress().ToString().AsCString()); + nexus.AddTestVar("INTERNET_SERVER_ADDR", internetServerIp6.ToString().AsCString()); + nexus.AddTestVar("LOCAL_SERVER_ADDR", localServerIp6.ToString().AsCString()); + nexus.AddTestVar("NAT64_PREFIX", nat64Prefix.ToString().AsCString()); + + nexus.SaveTestInfo("test_1_4_PIC_TC_4.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::Test_1_4_PIC_TC_4(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/nexus/verify_1_4_PIC_TC_4.py b/tests/nexus/verify_1_4_PIC_TC_4.py new file mode 100644 index 000000000..7085d9993 --- /dev/null +++ b/tests/nexus/verify_1_4_PIC_TC_4.py @@ -0,0 +1,219 @@ +#!/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.4 IPv4 Connectivity using BR built-in NAT64 + # 10.4.1. Purpose + # - To verify that BR DUT: + # - Offers IPv4 internet connectivity to/from Thread Devices using its built-in NAT64 translator, when the + # infrastructure network does not provide a NAT64 translator. + # - Offers IPv4 local network connectivity to/from Thread Devices using its built-in NAT64 translator + # - Operates DNS recursive resolver to look up IPv4 server addresses + # - Withdraws its own NAT64 prefix when another Border Router advertises a preferred NAT64 prefix. TBD - to + # be added. + # - Note: the present test requires a /96 NAT64 prefix, which is stricter than what the specification requires. + # RFC 6052 (Section 2.2) allows other lengths as well; however the present test case is not yet written to + # support all these different options. + # 10.4.2. Topology + # - BR_1 (DUT) - Border Router + # - Router_1 - Thread Router Reference Device + # - ED_1 - Thread Reference Device, End Device (e.g. FED/REED) role + # - Eth_1 - Adjacent Infrastructure Link Linux Reference Device on AIL + # - Has HTTP web server with resource ‘/test2.html’. + # - Eth_2 - Adjacent Infrastructure Link SPIFF Reference Device on AIL + # - Has HTTP web server on simulated-internet with resource ‘/testv4.html’ + + 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_2_ADDR = pv.vars['ETH_2_ADDR'] + INTERNET_SERVER_ADDR = pv.vars['INTERNET_SERVER_ADDR'] + LOCAL_SERVER_ADDR = pv.vars['LOCAL_SERVER_ADDR'] + NAT64_PREFIX = pv.vars['NAT64_PREFIX'] + + # Step 1 + # - Device: Eth_1, Eth_2 + # - Description (PIC-10.4): Enable Harness configures Eth_1 with IPv4 address 192.168.1.4. + # - Pass Criteria: N/A + print("Step 1: Enable Eth_1 and Eth_2") + + # Step 2 + # - Device: Eth_2 + # - Description (PIC-10.4): Harness instructs device to: Enable DHCP IPv4 server. + # - Pass Criteria: N/A + print("Step 2: Eth_2 default configuration") + + # Step 3 + # - Device: BR_1 (DUT), Router_1, ED_1 + # - Description (PIC-10.4): Enable + # - Pass Criteria: N/A + print("Step 3: Enable BR_1, Router_1, ED_1") + + # Step 4 + # - Device: BR_1 (DUT) + # - Description (PIC-10.4): Automatically configures an IPv4 address on the AIL; and identifies the IPv4 gateway + # on Eth_2. Automatically configures a NAT64 prefix in the Thread Network Data. Note: the check for length 96 + # is not due to it being required by, but because the current test script is only designed to handle this + # case. Other cases are TBD. See note in Section 10.4.1 above. + # - Pass Criteria: + # - The DUT MUST add one NAT64 prefix in the Thread Network Data, which is a route entry published as follows: + # - Prefix TLV + # - Prefix Length field MUST be 96. + print("Step 4: BR_1 configures IPv4 address and NAT64 prefix") + nat64_prefix_pattern = Ipv6Addr(NAT64_PREFIX.split('/')[0])[:12] + pkts.filter_wpan_src64(BR_1). \ + filter_mle_cmd(consts.MLE_DATA_RESPONSE). \ + filter(lambda p: any(pre.startswith(nat64_prefix_pattern) for pre in p.thread_nwd.tlv.prefix)). \ + must_next() + + # Step 5 + # - Device: ED_1 + # - Description (PIC-10.4): Harness instructs device to perform DNS query QType=ANY, name “threadv4.org”. + # Automatically, the DNS query gets routed to the DUT. + # - Pass Criteria: N/A + print("Step 5: ED_1 performs DNS query for threadv4.org") + dns_query = pkts.filter(lambda p: 'dns' in p.layer_names). \ + filter(lambda p: any(name.rstrip('.') == "threadv4.org" for name in verify_utils.as_list(p.dns.qry.name))). \ + filter(lambda p: any(t == consts.DNS_TYPE_ANY for t in verify_utils.as_list(p.dns.qry.type))). \ + must_next() + + # Step 6 + # - Device: BR_1 (DUT) + # - Description (PIC-10.4): 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('.') == "threadv4.org" for name in verify_utils.as_list(p.dns.qry.name))). \ + filter(lambda p: any(t == consts.DNS_TYPE_ANY for t in verify_utils.as_list(p.dns.qry.type))). \ + must_next() + + # Step 7 + # - Device: ED_1 + # - Description (PIC-10.4): Successfully receives DNS query result. + # - Pass Criteria: + # - ED_1 MUST receive DNS query answer from DUT: + # - threadv4.org A 38.106.217.165 + print("Step 7: ED_1 receives DNS result") + # Verify the response is received + pkts.filter(lambda p: 'dns' in p.layer_names). \ + filter(lambda p: any(name.rstrip('.') == "threadv4.org" for name in verify_utils.as_list(p.dns.resp.name))). \ + must_next() + + # Step 8 + # - Device: ED_1 + # - Description (PIC-10.4): Harness instructs device to send ICMPv6 ping request to internet server, using + # synthesized IPv6 address: :266a:d9a5 Request is routed via the DUT to the Eth_2 simulated + # network. The DUT translates the above IPv6 address into the IPv4 destination address 38.106.217.165. + # - 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() + # 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.4): 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.4): Harness instructs device to send HTTP GET request to server. If ED_1 is a Linux + # device, it may use 'curl' to save the file: curl -o testv4.html http://threadv4.org/testv4.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 :266a:d9a5 80 tcp send -x 474554202F74... tcp sendend tcp deinit + # - Pass Criteria: + # - In case Linux 'curl' was used: File testv4.html MUST equal the testv4.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 testv4.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.4): Harness instructs device to send ping request to local IPv4 server Eth_1 which has + # IPv4 destination address 192.168.1.4; using synthesized IPv6 address: :c0a8:0104 Automatically, + # it is routed via BR_1 DUT to Eth_1. + # - 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(LOCAL_SERVER_ADDR). \ + filter_ping_request(). \ + must_next() + # ED_1 receives response + pkts.filter_ipv6_src(LOCAL_SERVER_ADDR). \ + filter_ping_reply(identifier=ping_req.icmpv6.echo.identifier). \ + must_next() + + # Step 11 + # - Device: ED_1 + # - Description (PIC-10.4): Harness instructs device to send HTTP GET request to local server Eth_1. In case ED_1 + # is a Linux device, it can use 'curl' to save the file locally: 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 :c0a8:0104 80 tcp send -x 474554202F... 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 test2.html as stored on the server on Eth_1. + print("Step 11: ED_1 sends HTTP GET to local server Eth_1") + pkts.filter_ipv6_dst(LOCAL_SERVER_ADDR). \ + filter(lambda p: 'tcp' in p.layer_names). \ + must_next() + + +if __name__ == '__main__': + verify_utils.run_main(verify) diff --git a/tests/scripts/thread-cert/pktverify/consts.py b/tests/scripts/thread-cert/pktverify/consts.py index a23f4c9ad..66863eb9c 100644 --- a/tests/scripts/thread-cert/pktverify/consts.py +++ b/tests/scripts/thread-cert/pktverify/consts.py @@ -422,6 +422,7 @@ DNS_TYPE_PTR = 12 DNS_TYPE_TXT = 16 DNS_TYPE_AAAA = 28 DNS_TYPE_SRV = 33 +DNS_TYPE_ANY = 255 DNS_RCODE_NOERROR = 0 DNS_RCODE_NXDOMAIN = 3