From 76dab3b9634451707988672415f29cca77d1fdff Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Wed, 6 May 2026 17:40:25 -0700 Subject: [PATCH] [ip6] enforce strictly in-order IPv6 fragment reassembly (#13067) This commit enforces strictly in-order IPv6 fragment reassembly in the core stack to improve reassembly robustness and correctness. Previously, the reassembly engine did not track contiguous bytes received. An out-of-order or gapped fragment containing the M=0 flag could incorrectly trigger reassembly completion, potentially leading to the forwarding or processing of incomplete packets. To resolve this, we now: 1. Enforce that reassembly must start with a fragment offset of 0. 2. Verify that any subsequent fragment aligns perfectly with the offset where the contiguous payload data currently ends (`offset == message->GetOffset()`). 3. Safely advance `message->GetOffset()` as each fragment is successfully appended to keep track of the contiguous reassembled byte range. 4. Added a robust Nexus test case verifying that gapped reassembly is properly dropped and blocked. This strictly in-order validation approach is consistent with the preexisting 6LoWPAN fragment reassembly in MeshForwarder. --- src/core/net/ip6.cpp | 7 ++ tests/nexus/test_ipv6_fragmentation.cpp | 116 ++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp index 75abe1858..0c88143d2 100644 --- a/src/core/net/ip6.cpp +++ b/src/core/net/ip6.cpp @@ -666,6 +666,7 @@ Error Ip6::HandleFragment(Message &aMessage) if (message == nullptr) { LogDebg("start reassembly"); + VerifyOrExit(offset == 0, error = kErrorDrop); VerifyOrExit((message = NewMessage()) != nullptr, error = kErrorNoBufs); mReassemblyList.Enqueue(*message); @@ -678,6 +679,10 @@ Error Ip6::HandleFragment(Message &aMessage) Get().RegisterReceiver(TimeTicker::kIp6FragmentReassembler); } + else + { + VerifyOrExit(offset == message->GetOffset(), error = kErrorDrop); + } // increase message buffer if necessary if (message->GetLength() < offset + payloadFragment + aMessage.GetOffset()) @@ -690,6 +695,8 @@ Error Ip6::HandleFragment(Message &aMessage) /* aWriteOffset */ aMessage.GetOffset() + offset, aMessage, /* aReadOffset */ aMessage.GetOffset() + sizeof(fragmentHeader), /* aLength */ payloadFragment); + message->SetOffset(offset + payloadFragment); + // check if it is the last frame if (!fragmentHeader.IsMoreFlagSet()) { diff --git a/tests/nexus/test_ipv6_fragmentation.cpp b/tests/nexus/test_ipv6_fragmentation.cpp index 60ab51626..382fb077b 100644 --- a/tests/nexus/test_ipv6_fragmentation.cpp +++ b/tests/nexus/test_ipv6_fragmentation.cpp @@ -43,6 +43,97 @@ static constexpr uint32_t kAttachToRouterTime = 200 * 1000; /** Time to wait for ICMPv6 Echo response, in milliseconds. */ static constexpr uint32_t kEchoTimeout = 10 * 1000; +static bool gForwardedPacketReceived = false; +static uint16_t gForwardedPacketLen = 0; + +static void MyCustomIp6ReceiveCallback(otMessage *aMessage, void *aContext) +{ + OT_UNUSED_VARIABLE(aContext); + Message &message = *AsCoreTypePtr(aMessage); + Ip6::Header header; + if (header.ParseFrom(message) == kErrorNone) + { + if (header.GetNextHeader() == Ip6::kProtoUdp && message.GetLength() == 640) + { + gForwardedPacketReceived = true; + gForwardedPacketLen = message.GetLength(); + Log("MyCustomIp6ReceiveCallback: Received forwarded packet of length %u", gForwardedPacketLen); + } + } +} + +static void SendGapFragment1(Node &aRouter, const Ip6::Address &aLeaderEid, uint32_t aIdentification) +{ + Message *message = aRouter.Get().Allocate(Message::kTypeIp6); + VerifyOrQuit(message != nullptr); + + // 1. Outer IPv6 Header + Ip6::Header outerHeader; + outerHeader.InitVersionTrafficClassFlow(); + outerHeader.SetPayloadLength(48); // 8 bytes (Fragment Header) + 40 bytes (Inner IPv6 Header) + outerHeader.SetNextHeader(Ip6::kProtoFragment); + outerHeader.SetHopLimit(64); + outerHeader.SetSource(aRouter.Get().GetMeshLocalEid()); + outerHeader.SetDestination(aLeaderEid); + SuccessOrQuit(message->Append(outerHeader)); + + // 2. Fragment Header + Ip6::FragmentHeader fragmentHeader; + fragmentHeader.Init(); + fragmentHeader.SetNextHeader(Ip6::kProtoIp6); + fragmentHeader.SetOffset(0); + fragmentHeader.SetMoreFlag(); + fragmentHeader.SetIdentification(aIdentification); + SuccessOrQuit(message->Append(fragmentHeader)); + + // 3. Inner IPv6 Header (Payload of Fragment 1) + Ip6::Header innerHeader; + innerHeader.InitVersionTrafficClassFlow(); + innerHeader.SetPayloadLength(600); // 600 bytes inner payload to stay under MTU limit + innerHeader.SetNextHeader(Ip6::kProtoUdp); + innerHeader.SetHopLimit(64); + innerHeader.SetSource(aLeaderEid); + innerHeader.SetDestination(aRouter.Get().GetMeshLocalEid()); + SuccessOrQuit(message->Append(innerHeader)); + + // Set message priority/etc + SuccessOrQuit(message->SetPriority(Message::kPriorityNormal)); + + // Send it + SuccessOrQuit(aRouter.Get().SendRaw(OwnedPtr(message))); +} + +static void SendGapFragment2(Node &aRouter, const Ip6::Address &aLeaderEid, uint32_t aIdentification) +{ + Message *message = aRouter.Get().Allocate(Message::kTypeIp6); + VerifyOrQuit(message != nullptr); + + // 1. Outer IPv6 Header + Ip6::Header outerHeader; + outerHeader.InitVersionTrafficClassFlow(); + outerHeader.SetPayloadLength(8); // 8 bytes (Fragment Header) + outerHeader.SetNextHeader(Ip6::kProtoFragment); + outerHeader.SetHopLimit(64); + outerHeader.SetSource(aRouter.Get().GetMeshLocalEid()); + outerHeader.SetDestination(aLeaderEid); + SuccessOrQuit(message->Append(outerHeader)); + + // 2. Fragment Header + Ip6::FragmentHeader fragmentHeader; + fragmentHeader.Init(); + fragmentHeader.SetNextHeader(Ip6::kProtoIp6); + fragmentHeader.SetOffset(80); // 80 * 8 = 640 bytes (under MTU limit) + fragmentHeader.ClearMoreFlag(); // M = 0 + fragmentHeader.SetIdentification(aIdentification); + SuccessOrQuit(message->Append(fragmentHeader)); + + // Set message priority/etc + SuccessOrQuit(message->SetPriority(Message::kPriorityNormal)); + + // Send it + SuccessOrQuit(aRouter.Get().SendRaw(OwnedPtr(message))); +} + void TestIPv6Fragmentation(void) { /** @@ -89,6 +180,31 @@ void TestIPv6Fragmentation(void) // This exceeds the 1280 bytes MTU and triggers IPv6 fragmentation. nexus.SendAndVerifyEchoRequest(router, leader.Get().GetMeshLocalEid(), 1831, 64, kEchoTimeout); + Log("Step 4: Fragment Reassembly Gap Check Test"); + gForwardedPacketReceived = false; + gForwardedPacketLen = 0; + + // Register the custom receive callback on Router to intercept the forwarded packet. + router.Get().SetReceiveCallback(MyCustomIp6ReceiveCallback, nullptr); + + { + uint32_t identification = 0x12345678; + Log("Sending Fragment 1 (offset=0, M=1)"); + SendGapFragment1(router, leader.Get().GetMeshLocalEid(), identification); + nexus.AdvanceTime(100); // Short wait + + Log("Sending Fragment 2 (offset=640, M=0)"); + SendGapFragment2(router, leader.Get().GetMeshLocalEid(), identification); + nexus.AdvanceTime(1000); // Wait for reassembly and forwarding + } + + // Check if the packet was received (Expect no packet received in patched version!) + VerifyOrQuit(!gForwardedPacketReceived, "Reassembly gap check failed: forwarded packet was received!"); + Log("Success: No forwarded packet was received. Reassembly gap is properly handled!"); + + // Restore default receive callback on Router + router.Get().SetReceiveCallback(Node::HandleIp6Receive, &router); + nexus.SaveTestInfo("test_ipv6_fragmentation.json"); }