[ip6] resolve extension headers before forward-path protocol checks (#13412)

On the forward path, extension headers are intentionally left
unprocessed, so the forwarded-ICMPv6 type allowlist and the
untrusted-origin TMF port filter matched on the first Next Header
value rather than the final upper-layer protocol. A Destination
Options (or atomic Fragment) header inserted before the upper-layer
header caused both checks to be skipped.

Add a `ResolveUpperLayerProtocol()` helper that walks the extension
header chain (shared with `HasIp6InIpTunnel()`), failing closed on
malformed or overlong chains, and apply the forward-path checks to
the resolved protocol. A continuation (non-first) fragment does not
contain the upper-layer header and resolves to `kProtoFragment`; it
is forwarded as-is, since it cannot complete reassembly anywhere
unless its first fragment also passed through this filter.

Add a nexus test covering the forward-path checks with extension
header chains.

Co-authored-by: aussinfosec <[email protected]>
This commit is contained in:
Griffin Francis
2026-07-28 11:48:31 -07:00
committed by GitHub
co-authored by aussinfosec
parent b035ecbc07
commit 83e79fe9bd
4 changed files with 365 additions and 20 deletions
+58 -20
View File
@@ -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<Message> aMessagePtr, bool aIsReassembled, ui
if (forwardThread)
{
OffsetRange upperOffsetRange;
if (aMessagePtr->IsOriginThreadNetif())
{
VerifyOrExit(Get<Mle::Mle>().IsRouterOrLeader());
@@ -1347,11 +1379,17 @@ Error Ip6::HandleDatagram(OwnedPtr<Message> aMessagePtr, bool aIsReassembled, ui
aMessagePtr->Write<uint8_t>(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<Message> aMessagePtr, bool aIsReassembled, ui
{
UdpHeader udpHeader;
SuccessOrExit(error = aMessagePtr->Read(aMessagePtr->GetOffset(), udpHeader));
SuccessOrExit(error = aMessagePtr->Read(upperOffsetRange, udpHeader));
if (udpHeader.GetDestinationPort() == Tmf::kUdpPort)
{
+1
View File
@@ -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);
+1
View File
@@ -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")
@@ -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 <stdio.h>
#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<MessagePool>().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<Ip6::Ip6>().SendRaw(OwnedPtr<Message>(&aMessage));
}
void TestIp6ForwardExtHeaders(void)
{
Core nexus;
Node &node = nexus.CreateNode();
Error error;
node.Form();
nexus.AdvanceTime(13 * 1000);
VerifyOrQuit(node.Get<Mle::Mle>().IsLeader());
sNode = &node;
sSource = node.Get<Mle::Mle>().GetLinkLocalAddress();
// A link-local destination that is not on this node.
sDestination = node.Get<Mle::Mle>().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;
}