diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp index f62db2a3a..1339e66cf 100644 --- a/src/core/net/ip6.cpp +++ b/src/core/net/ip6.cpp @@ -847,6 +847,51 @@ exit: return error; } +bool Ip6::HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const +{ + bool hasTunnel = false; + OffsetRange offsetRange; + + offsetRange.InitFromMessageOffsetToEnd(aMessage); + + while (!offsetRange.IsEmpty()) + { + if (aNextHeader == kProtoIp6) + { + hasTunnel = true; + break; + } + + if (aNextHeader == kProtoHopOpts || aNextHeader == kProtoDstOpts || aNextHeader == kProtoRouting) + { + ExtensionHeader extHeader; + uint16_t size; + + SuccessOrExit(aMessage.Read(offsetRange, extHeader)); + size = extHeader.GetSize(); + VerifyOrExit(offsetRange.Contains(size)); + offsetRange.AdvanceOffset(size); + aNextHeader = extHeader.GetNextHeader(); + } + else if (aNextHeader == kProtoFragment) + { + FragmentHeader fragHeader; + + SuccessOrExit(aMessage.Read(offsetRange, fragHeader)); + VerifyOrExit(offsetRange.Contains(sizeof(FragmentHeader))); + offsetRange.AdvanceOffset(sizeof(FragmentHeader)); + aNextHeader = fragHeader.GetNextHeader(); + } + else + { + break; + } + } + +exit: + return hasTunnel; +} + Error Ip6::TakeOrCopyMessagePtr(OwnedPtr &aTargetPtr, OwnedPtr &aMessagePtr, MessageOwnership aMessageOwnership) @@ -1044,6 +1089,14 @@ Error Ip6::SendRaw(OwnedPtr aMessagePtr) SuccessOrExit(error = header.ParseFrom(*aMessagePtr)); VerifyOrExit(!header.GetSource().IsMulticast(), error = kErrorInvalidSourceAddress); + aMessagePtr->SetOffset(sizeof(header)); + + if (aMessagePtr->IsOriginHostUntrusted() && HasIp6InIpTunnel(*aMessagePtr, header.GetNextHeader())) + { + LogInfo("Dropping host-untrusted IP-in-IP packet"); + ExitNow(error = kErrorDrop); + } + #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE // The filtering rules don't apply to packets from DUA. if (!Get().IsDomainUnicast(header.GetSource())) diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp index ed4caf5b3..cc90e41f6 100644 --- a/src/core/net/ip6.hpp +++ b/src/core/net/ip6.hpp @@ -349,6 +349,7 @@ private: const Header &aHeader, uint8_t &aNextHeader, bool &aReceive); + bool HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const; Error FragmentDatagram(Message &aMessage, uint8_t aIpProto); Error HandleFragment(Message &aMessage); #if OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index bdf169d4c..0d330ed30 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -436,6 +436,7 @@ ot_nexus_test(srp_scale "core;nexus") ot_nexus_test(srp_server_anycast_mode "core;nexus") ot_nexus_test(srp_server_reboot_port "core;nexus") ot_nexus_test(srp_ttl "core;nexus") +ot_nexus_test(tmf_origin "core;nexus") ot_nexus_test(zero_len_external_route "core;nexus") # Trel diff --git a/tests/nexus/test_tmf_origin.cpp b/tests/nexus/test_tmf_origin.cpp new file mode 100644 index 000000000..18f8002f9 --- /dev/null +++ b/tests/nexus/test_tmf_origin.cpp @@ -0,0 +1,178 @@ +/* + * 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 "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +void TestTmfOrigin(void) +{ + Core nexus; + Node &node = nexus.CreateNode(); + + node.Form(); + nexus.AdvanceTime(10 * 1000); + VerifyOrQuit(node.Get().IsLeader()); + + Ip6::Address mlEid = node.Get().GetMeshLocalEid(); + Ip6::Address linkLocal = node.Get().GetLinkLocalAddress(); + + // Allocate and construct the message: + // Outer IPv6 Header + Inner IPv6 Header + Inner UDP Header + Message *message = node.Get().Allocate(Message::kTypeIp6); + VerifyOrQuit(message != nullptr); + + // 1. Inner UDP Header + Ip6::Udp::Header udpHeader; + udpHeader.Clear(); + udpHeader.SetSourcePort(1234); + udpHeader.SetDestinationPort(Tmf::kUdpPort); + udpHeader.SetLength(sizeof(Ip6::Udp::Header)); + + // 2. Inner IPv6 Header + Ip6::Header innerHeader; + innerHeader.Clear(); + innerHeader.InitVersionTrafficClassFlow(); + innerHeader.SetSource(Ip6::Address::GetLinkLocalAllNodesMulticast()); // dummy LL source + innerHeader.SetDestination(linkLocal); + innerHeader.SetNextHeader(Ip6::kProtoUdp); + innerHeader.SetPayloadLength(sizeof(Ip6::Udp::Header)); + + // 3. Outer IPv6 Header (IP-in-IP) + Ip6::Header outerHeader; + outerHeader.Clear(); + outerHeader.InitVersionTrafficClassFlow(); + outerHeader.SetSource(linkLocal); + outerHeader.SetDestination(mlEid); + outerHeader.SetNextHeader(Ip6::kProtoIp6); + outerHeader.SetPayloadLength(sizeof(Ip6::Header) + sizeof(Ip6::Udp::Header)); + + // Append all headers to the message + SuccessOrQuit(message->Append(outerHeader)); + SuccessOrQuit(message->Append(innerHeader)); + SuccessOrQuit(message->Append(udpHeader)); + + // Mark the message origin as Host Untrusted + message->SetOrigin(Message::kOriginHostUntrusted); + + // Call SendRaw, which will trigger HandleDatagram + Error error = node.Get().SendRaw(OwnedPtr(message)); + + Log("SendRaw returned error: %s", ErrorToString(error)); + + // Verify that SendRaw returned kErrorDrop + VerifyOrQuit(error == kErrorDrop); + + Log("Verified that untrusted IP-in-IP packet was dropped successfully."); +} + +void TestTmfOriginBypassed(void) +{ + Core nexus; + Node &node = nexus.CreateNode(); + + node.Form(); + nexus.AdvanceTime(10 * 1000); + VerifyOrQuit(node.Get().IsLeader()); + + Ip6::Address mlEid = node.Get().GetMeshLocalEid(); + Ip6::Address linkLocal = node.Get().GetLinkLocalAddress(); + + // Allocate and construct the message: + // Outer IPv6 Header + Destination Options Header + Inner IPv6 Header + Inner UDP Header + Message *message = node.Get().Allocate(Message::kTypeIp6); + VerifyOrQuit(message != nullptr); + + // 1. Inner UDP Header + Ip6::Udp::Header udpHeader; + udpHeader.Clear(); + udpHeader.SetSourcePort(1234); + udpHeader.SetDestinationPort(Tmf::kUdpPort); + udpHeader.SetLength(sizeof(Ip6::Udp::Header)); + + // 2. Inner IPv6 Header + Ip6::Header innerHeader; + innerHeader.Clear(); + innerHeader.InitVersionTrafficClassFlow(); + innerHeader.SetSource(Ip6::Address::GetLinkLocalAllNodesMulticast()); // dummy LL source + innerHeader.SetDestination(linkLocal); + innerHeader.SetNextHeader(Ip6::kProtoUdp); + innerHeader.SetPayloadLength(sizeof(Ip6::Udp::Header)); + + // 3. Destination Options Header (containing 6 bytes of padding to make it 8 bytes total) + Ip6::ExtensionHeader dstHeader; + dstHeader.SetNextHeader(Ip6::kProtoIp6); + dstHeader.SetLength(0); + + Ip6::PadOption padOption; + padOption.InitForPadSize(6); + + // 4. Outer IPv6 Header (IP-in-IP) + Ip6::Header outerHeader; + outerHeader.Clear(); + outerHeader.InitVersionTrafficClassFlow(); + outerHeader.SetSource(linkLocal); + outerHeader.SetDestination(mlEid); + outerHeader.SetNextHeader(Ip6::kProtoDstOpts); + outerHeader.SetPayloadLength(dstHeader.GetSize() + sizeof(Ip6::Header) + sizeof(Ip6::Udp::Header)); + + // Append all headers to the message + SuccessOrQuit(message->Append(outerHeader)); + SuccessOrQuit(message->Append(dstHeader)); + SuccessOrQuit(message->AppendBytes(&padOption, padOption.GetSize())); + SuccessOrQuit(message->Append(innerHeader)); + SuccessOrQuit(message->Append(udpHeader)); + + // Mark the message origin as Host Untrusted + message->SetOrigin(Message::kOriginHostUntrusted); + + // Call SendRaw, which will trigger HandleDatagram + Error error = node.Get().SendRaw(OwnedPtr(message)); + + Log("SendRaw bypassed returned error: %s", ErrorToString(error)); + + // Verify that SendRaw returned kErrorDrop due to the traversal detection + VerifyOrQuit(error == kErrorDrop); + + Log("Verified that untrusted bypassed IP-in-IP packet was dropped successfully."); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestTmfOrigin(); + ot::Nexus::TestTmfOriginBypassed(); + printf("All tests passed\n"); + return 0; +}