diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp index 7a6e0f1b7..18c435c7c 100644 --- a/src/core/net/ip6.cpp +++ b/src/core/net/ip6.cpp @@ -854,37 +854,50 @@ exit: return error; } -bool Ip6::HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const +Error Ip6::ResolveUpperLayerProtocol(const Message &aMessage, uint8_t &aNextHeader, OffsetRange &aOffsetRange) const { - bool hasTunnel = false; - OffsetRange offsetRange; + // Walks the extension header chain starting at the message offset, + // updating `aNextHeader` in place to the final upper-layer + // protocol carried in `aMessage`, with `aOffsetRange` starting at + // the corresponding upper-layer header. + // + // A continuation (non-first) fragment does not contain the + // upper-layer header; in this case `aNextHeader` is left as + // `kProtoFragment`. + // + // Returns `kErrorParse` if the chain is malformed (an extension + // header does not fit within the message) or cannot be resolved + // within `kMaxExtHeaderChain` headers. - offsetRange.InitFromMessageOffsetToEnd(aMessage); + static constexpr uint8_t kMaxExtHeaderChain = 8; - while (!offsetRange.IsEmpty()) + Error error = kErrorNone; + + aOffsetRange.InitFromMessageOffsetToEnd(aMessage); + + for (uint8_t numHeaders = 0;; numHeaders++) { - if (aNextHeader == kProtoIp6) - { - hasTunnel = true; - break; - } - - if (aNextHeader == kProtoHopOpts || aNextHeader == kProtoDstOpts || aNextHeader == kProtoRouting) + 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); + VerifyOrExit(numHeaders < kMaxExtHeaderChain, error = kErrorParse); + SuccessOrExit(error = aMessage.Read(aOffsetRange, extHeader)); + VerifyOrExit(aOffsetRange.Contains(extHeader.GetSize()), error = kErrorParse); + aOffsetRange.AdvanceOffset(extHeader.GetSize()); aNextHeader = extHeader.GetNextHeader(); } else if (aNextHeader == kProtoFragment) { FragmentHeader fragHeader; - SuccessOrExit(aMessage.ReadAndAdvance(offsetRange, fragHeader)); + VerifyOrExit(numHeaders < kMaxExtHeaderChain, error = kErrorParse); + SuccessOrExit(error = aMessage.ReadAndAdvance(aOffsetRange, fragHeader)); + + // A continuation fragment does not contain the upper-layer + // header; leave `aNextHeader` as `kProtoFragment`. + VerifyOrExit(fragHeader.GetOffset() == 0); + aNextHeader = fragHeader.GetNextHeader(); } else @@ -893,6 +906,23 @@ bool Ip6::HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const } } +exit: + return error; +} + +bool Ip6::HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const +{ + // Fails closed: a chain that cannot be resolved is treated the + // same as a tunnel, since it cannot be verified not to contain + // one. This filter is only applied to host-untrusted messages + // (see `SendRaw()`). + + bool hasTunnel = true; + OffsetRange offsetRange; + + SuccessOrExit(ResolveUpperLayerProtocol(aMessage, aNextHeader, offsetRange)); + hasTunnel = (aNextHeader == kProtoIp6); + exit: return hasTunnel; } @@ -1337,6 +1367,8 @@ Error Ip6::HandleDatagram(OwnedPtr aMessagePtr, bool aIsReassembled, ui if (forwardThread) { + OffsetRange upperOffsetRange; + if (aMessagePtr->IsOriginThreadNetif()) { VerifyOrExit(Get().IsRouterOrLeader()); @@ -1347,11 +1379,17 @@ Error Ip6::HandleDatagram(OwnedPtr aMessagePtr, bool aIsReassembled, ui aMessagePtr->Write(Header::kHopLimitFieldOffset, header.GetHopLimit()); + // Resolve any extension headers left unprocessed on the + // forward path so the checks below apply to the final + // upper-layer protocol. + + SuccessOrExit(error = ResolveUpperLayerProtocol(*aMessagePtr, nextHeader, upperOffsetRange)); + if (nextHeader == kProtoIcmp6) { uint8_t icmpType; - SuccessOrExit(error = aMessagePtr->Read(aMessagePtr->GetOffset(), icmpType)); + SuccessOrExit(error = aMessagePtr->Read(upperOffsetRange, icmpType)); VerifyOrExit(DoesArrayContain(kForwardIcmpTypes, icmpType), error = kErrorDrop); } @@ -1363,7 +1401,7 @@ Error Ip6::HandleDatagram(OwnedPtr aMessagePtr, bool aIsReassembled, ui { UdpHeader udpHeader; - SuccessOrExit(error = aMessagePtr->Read(aMessagePtr->GetOffset(), udpHeader)); + SuccessOrExit(error = aMessagePtr->Read(upperOffsetRange, udpHeader)); if (udpHeader.GetDestinationPort() == Tmf::kUdpPort) { diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp index 3cff4c9f4..eac357a27 100644 --- a/src/core/net/ip6.hpp +++ b/src/core/net/ip6.hpp @@ -353,6 +353,7 @@ private: const Header &aHeader, uint8_t &aNextHeader, bool &aReceive); + Error ResolveUpperLayerProtocol(const Message &aMessage, uint8_t &aNextHeader, OffsetRange &aOffsetRange) const; bool HasIp6InIpTunnel(const Message &aMessage, uint8_t aNextHeader) const; Error FragmentDatagram(Message &aMessage, uint8_t aIpProto); Error HandleFragment(Message &aMessage); diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 7fcd9250c..6f0ec821c 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -418,6 +418,7 @@ ot_nexus_test(fed_rx_only_link_establishment "core;nexus") ot_nexus_test(form_join "core;nexus") ot_nexus_test(history_tracker "core;nexus") ot_nexus_test(inform_previous_parent_on_reattach "core;nexus") +ot_nexus_test(ipv6_forward_ext_headers "core;nexus") ot_nexus_test(ipv6_fragmentation "core;nexus") ot_nexus_test(ipv6_recursion "core;nexus") ot_nexus_test(ipv6_source_selection "core;nexus") diff --git a/tests/nexus/test_ipv6_forward_ext_headers.cpp b/tests/nexus/test_ipv6_forward_ext_headers.cpp new file mode 100644 index 000000000..1d024d2ba --- /dev/null +++ b/tests/nexus/test_ipv6_forward_ext_headers.cpp @@ -0,0 +1,305 @@ +/* + * 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. + */ + +/* + * Verifies that the forward-path protocol checks in `Ip6::HandleDatagram()` (the + * forwarded-ICMPv6 type allowlist and the untrusted-origin TMF port filter) are applied + * to the final upper-layer protocol after resolving any IPv6 extension headers, rather + * than to the first Next Header value of the IPv6 header. + * + * Each case constructs a raw IPv6 datagram with a host-untrusted origin and a link-local + * destination that is not on this node, so that `Ip6::SendRaw()` routes it through the + * forward-to-Thread path of `Ip6::HandleDatagram()`, and asserts on the returned error: + * + * case 1 (control): UDP to the TMF port with no extension header is dropped; + * case 2: the same UDP datagram with a Destination Options header preceding the UDP + * header is also dropped; + * case 3: the same UDP datagram with an atomic Fragment header (offset 0) preceding + * the UDP header is also dropped; + * case 4: ICMPv6 Echo Request with a Destination Options header is still matched by + * the forwarded-ICMPv6 type allowlist and forwarded; + * case 5: ICMPv6 Neighbor Solicitation (not in the allowlist) with a Destination + * Options header is dropped; + * case 6: a chain of extension headers longer than the resolution limit is dropped + * (the resolution fails closed); + * case 7: a continuation fragment (offset != 0), which does not contain the + * upper-layer header, is forwarded as-is; + * case 8: UDP to a non-TMF port with no extension header is forwarded; + * case 9: UDP to a non-TMF port with a Destination Options header is forwarded; + * case 10: TCP with no extension header is forwarded. + */ + +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +static constexpr uint16_t kNonTmfPort = 12345; +static constexpr uint16_t kSourcePort = 1234; +static constexpr uint8_t kHopLimit = 64; +static constexpr uint8_t kLongChainLen = 9; // Longer than the resolution limit (8). + +static Node *sNode; +static Ip6::Address sSource; +static Ip6::Address sDestination; + +static Message &NewForwardedMessage(uint8_t aNextHeader, uint16_t aPayloadLength) +{ + // Allocates a new IPv6 message and appends an IPv6 header with a + // link-local destination that is not on this node, so that the + // message takes the forward-to-Thread path in `HandleDatagram()`. + + Message *message = sNode->Get().Allocate(Message::kTypeIp6); + Ip6::Header header; + + VerifyOrQuit(message != nullptr); + + header.Clear(); + header.InitVersionTrafficClassFlow(); + header.SetSource(sSource); + header.SetDestination(sDestination); + header.SetNextHeader(aNextHeader); + header.SetPayloadLength(aPayloadLength); + header.SetHopLimit(kHopLimit); + + SuccessOrQuit(message->Append(header)); + + return *message; +} + +static void AppendDstOptsHeader(Message &aMessage, uint8_t aNextHeader) +{ + // Appends a minimal (8-byte) Destination Options header carrying a + // single PadN option. + + Ip6::ExtensionHeader extHeader; + Ip6::PadOption padOption; + + extHeader.SetNextHeader(aNextHeader); + extHeader.SetLength(0); + padOption.InitForPadSize(6); + + SuccessOrQuit(aMessage.Append(extHeader)); + SuccessOrQuit(aMessage.AppendBytes(&padOption, padOption.GetSize())); +} + +static void AppendFragmentHeader(Message &aMessage, uint8_t aNextHeader, uint16_t aOffset) +{ + Ip6::FragmentHeader fragHeader; + + fragHeader.Init(); + fragHeader.SetNextHeader(aNextHeader); + fragHeader.SetOffset(aOffset); + + SuccessOrQuit(aMessage.Append(fragHeader)); +} + +static void AppendUdpHeader(Message &aMessage, uint16_t aDestPort) +{ + Ip6::UdpHeader udpHeader; + + udpHeader.Clear(); + udpHeader.SetSourcePort(kSourcePort); + udpHeader.SetDestinationPort(aDestPort); + udpHeader.SetLength(sizeof(Ip6::UdpHeader)); + + SuccessOrQuit(aMessage.Append(udpHeader)); +} + +static void AppendIcmp6Header(Message &aMessage, Ip6::Icmp6Header::Type aType) +{ + Ip6::Icmp6Header icmp6Header; + + icmp6Header.Clear(); + icmp6Header.SetType(aType); + + SuccessOrQuit(aMessage.Append(icmp6Header)); +} + +static Error SendFromHost(Message &aMessage) +{ + // Marks the message as host-untrusted origin and passes it to + // `SendRaw()`, which routes it through `HandleDatagram()`. + + aMessage.SetOrigin(Message::kOriginHostUntrusted); + + return sNode->Get().SendRaw(OwnedPtr(&aMessage)); +} + +void TestIp6ForwardExtHeaders(void) +{ + Core nexus; + Node &node = nexus.CreateNode(); + Error error; + + node.Form(); + nexus.AdvanceTime(13 * 1000); + VerifyOrQuit(node.Get().IsLeader()); + + sNode = &node; + sSource = node.Get().GetLinkLocalAddress(); + + // A link-local destination that is not on this node. + sDestination = node.Get().GetLinkLocalAddress(); + sDestination.mFields.m8[15] ^= 0x01; + + Log("--- Case 1 (control): UDP to TMF port, no extension header -> dropped"); + { + Message &message = NewForwardedMessage(Ip6::kProtoUdp, sizeof(Ip6::UdpHeader)); + + AppendUdpHeader(message, Tmf::kUdpPort); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorDrop); + } + + Log("--- Case 2: UDP to TMF port behind Destination Options header -> dropped"); + { + Message &message = + NewForwardedMessage(Ip6::kProtoDstOpts, sizeof(Ip6::ExtensionHeader) + 6 + sizeof(Ip6::UdpHeader)); + + AppendDstOptsHeader(message, Ip6::kProtoUdp); + AppendUdpHeader(message, Tmf::kUdpPort); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorDrop); + } + + Log("--- Case 3: UDP to TMF port behind atomic Fragment header -> dropped"); + { + Message &message = + NewForwardedMessage(Ip6::kProtoFragment, sizeof(Ip6::FragmentHeader) + sizeof(Ip6::UdpHeader)); + + AppendFragmentHeader(message, Ip6::kProtoUdp, /* aOffset */ 0); + AppendUdpHeader(message, Tmf::kUdpPort); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorDrop); + } + + Log("--- Case 4: ICMPv6 Echo Request behind Destination Options header -> forwarded"); + { + Message &message = + NewForwardedMessage(Ip6::kProtoDstOpts, sizeof(Ip6::ExtensionHeader) + 6 + sizeof(Ip6::Icmp6Header)); + + AppendDstOptsHeader(message, Ip6::kProtoIcmp6); + AppendIcmp6Header(message, Ip6::Icmp6Header::kTypeEchoRequest); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorNone); + } + + Log("--- Case 5: ICMPv6 Neighbor Solicit behind Destination Options header -> dropped"); + { + Message &message = + NewForwardedMessage(Ip6::kProtoDstOpts, sizeof(Ip6::ExtensionHeader) + 6 + sizeof(Ip6::Icmp6Header)); + + AppendDstOptsHeader(message, Ip6::kProtoIcmp6); + AppendIcmp6Header(message, Ip6::Icmp6Header::kTypeNeighborSolicit); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorDrop); + } + + Log("--- Case 6: extension header chain longer than resolution limit -> dropped"); + { + uint16_t payloadLength = kLongChainLen * (sizeof(Ip6::ExtensionHeader) + 6) + sizeof(Ip6::UdpHeader); + Message &message = NewForwardedMessage(Ip6::kProtoDstOpts, payloadLength); + + for (uint8_t i = 0; i < kLongChainLen; i++) + { + AppendDstOptsHeader(message, (i + 1 < kLongChainLen) ? Ip6::kProtoDstOpts : Ip6::kProtoUdp); + } + + AppendUdpHeader(message, kNonTmfPort); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error != kErrorNone); + } + + Log("--- Case 7: continuation fragment (no upper-layer header present) -> forwarded"); + { + static const uint8_t kFragmentData[16] = {0}; + + Message &message = + NewForwardedMessage(Ip6::kProtoFragment, sizeof(Ip6::FragmentHeader) + sizeof(kFragmentData)); + + AppendFragmentHeader(message, Ip6::kProtoUdp, /* aOffset */ 64); + SuccessOrQuit(message.AppendBytes(kFragmentData, sizeof(kFragmentData))); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorNone); + } + + Log("--- Case 8: UDP to non-TMF port, no extension header -> forwarded"); + { + Message &message = NewForwardedMessage(Ip6::kProtoUdp, sizeof(Ip6::UdpHeader)); + + AppendUdpHeader(message, kNonTmfPort); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorNone); + } + + Log("--- Case 9: UDP to non-TMF port behind Destination Options header -> forwarded"); + { + Message &message = + NewForwardedMessage(Ip6::kProtoDstOpts, sizeof(Ip6::ExtensionHeader) + 6 + sizeof(Ip6::UdpHeader)); + + AppendDstOptsHeader(message, Ip6::kProtoUdp); + AppendUdpHeader(message, kNonTmfPort); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorNone); + } + + Log("--- Case 10: TCP, no extension header -> forwarded"); + { + static const uint8_t kTcpHeader[20] = {0}; + + Message &message = NewForwardedMessage(Ip6::kProtoTcp, sizeof(kTcpHeader)); + + SuccessOrQuit(message.AppendBytes(kTcpHeader, sizeof(kTcpHeader))); + error = SendFromHost(message); + Log("SendRaw returned: %s", ErrorToString(error)); + VerifyOrQuit(error == kErrorNone); + } +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestIp6ForwardExtHeaders(); + printf("All tests passed\n"); + return 0; +}