[nat64] handle IPv4 options and discard source route options (#12818)

OpenThread's NAT64 translator assumed a fixed IPv4 header length of 20
bytes, which caused incorrect parsing and translation of IPv4 packets
containing options (IHL > 5).

Specifically, if an IPv4 packet with options was received:
1. The transport header was read from a fixed 20-byte offset, leading
   to corruption of transport layer fields (e.g., UDP ports).
2. Only 20 bytes were removed from the message, leaving the IPv4
   options at the beginning of the translated IPv6 payload.
3. Mandatory security checks for source route options were bypassed.

This commit fixes these issues by:
- Updating Ip4::Header to validate IHL and provide the actual header
  length.
- Using the actual header length for transport header parsing and
  IPv4 header removal in the NAT64 translator.
- Implementing a check to discard packets with LSRR or SSRR options
  as required by RFC 7915.

A new Nexus regression test is added to verify the fix.
This commit is contained in:
Jonathan Hui
2026-04-01 18:24:42 -07:00
committed by GitHub
parent 42b653a18c
commit 26a882dabc
5 changed files with 297 additions and 8 deletions
+56 -4
View File
@@ -34,6 +34,7 @@
#include "ip4_types.hpp"
#include "common/numeric_limits.hpp"
#include "common/offset_range.hpp"
#include "net/ip6_address.hpp"
namespace ot {
@@ -208,33 +209,84 @@ Error Header::ParseFrom(const Message &aMessage)
VerifyOrExit(IsValid());
VerifyOrExit(GetTotalLength() == aMessage.GetLength());
if (GetIhl() > kMinIhl)
{
VerifyOrExit(!HasSourceRouteOption(aMessage));
}
error = kErrorNone;
exit:
return error;
}
bool Header::HasSourceRouteOption(const Message &aMessage) const
{
bool hasSourceRoute = false;
uint16_t headerLen = GetHeaderLength();
OffsetRange range;
VerifyOrExit(headerLen <= aMessage.GetLength());
range.InitFromRange(sizeof(Header), headerLen);
while (!range.IsEmpty())
{
uint8_t optionType;
uint8_t optionLen;
SuccessOrExit(aMessage.Read(range, optionType));
range.AdvanceOffset(sizeof(uint8_t));
if (optionType == kOptionEnd)
{
break;
}
if (optionType == kOptionNop)
{
continue;
}
SuccessOrExit(aMessage.Read(range, optionLen));
VerifyOrExit(optionLen >= 2 && range.Contains(optionLen - 1));
if (optionType == kOptionLsrr || optionType == kOptionSsrr)
{
hasSourceRoute = true;
ExitNow();
}
range.AdvanceOffset(optionLen - 1);
}
exit:
return hasSourceRoute;
}
//---------------------------------------------------------------------------------------------------------------------
// Headers
Error Headers::ParseFrom(const Message &aMessage)
{
Error error = kErrorParse;
Error error = kErrorParse;
uint16_t headerLen;
Clear();
SuccessOrExit(mIp4Header.ParseFrom(aMessage));
headerLen = mIp4Header.GetHeaderLength();
switch (mIp4Header.GetProtocol())
{
case kProtoUdp:
SuccessOrExit(aMessage.Read(sizeof(Header), mHeader.mUdp));
SuccessOrExit(aMessage.Read(headerLen, mHeader.mUdp));
break;
case kProtoTcp:
SuccessOrExit(aMessage.Read(sizeof(Header), mHeader.mTcp));
SuccessOrExit(aMessage.Read(headerLen, mHeader.mTcp));
break;
case kProtoIcmp:
SuccessOrExit(aMessage.Read(sizeof(Header), mHeader.mIcmp));
SuccessOrExit(aMessage.Read(headerLen, mHeader.mIcmp));
break;
default:
break;
+26 -2
View File
@@ -296,7 +296,24 @@ public:
* @retval TRUE If the header appears to be well-formed.
* @retval FALSE If the header does not appear to be well-formed.
*/
bool IsValid(void) const { return IsVersion4(); }
bool IsValid(void) const
{
return IsVersion4() && (GetIhl() >= kMinIhl) && (GetHeaderLength() <= GetTotalLength());
}
/**
* Returns the IPv4 Internet Header Length (IHL) value.
*
* @returns The IPv4 IHL value.
*/
uint8_t GetIhl(void) const { return mVersIhl & kIhlMask; }
/**
* Returns the IPv4 Header Length value.
*
* @returns The IPv4 Header Length value.
*/
uint16_t GetHeaderLength(void) const { return static_cast<uint16_t>(GetIhl()) * 4; }
/**
* Initializes the Version to 4 and sets Traffic Class and Flow fields to zero.
@@ -516,6 +533,7 @@ private:
static constexpr uint8_t kVersion4 = 0x40; // Use with `mVersIhl`
static constexpr uint8_t kVersionMask = 0xf0; // Use with `mVersIhl`
static constexpr uint8_t kIhlMask = 0x0f; // Use with `mVersIhl`
static constexpr uint8_t kMinIhl = 5; ///< Minimum IPv4 Internet Header Length (in 32-bit words).
static constexpr uint8_t kDscpOffset = 2; // Use with `mDscpEcn`
static constexpr uint16_t kDscpMask = 0xfc; // Use with `mDscpEcn`
static constexpr uint8_t kEcnOffset = 0; // Use with `mDscpEcn`
@@ -524,7 +542,13 @@ private:
static constexpr uint16_t kFlagsDf = 0x4000; // Use with `mFlagsFragmentOffset`
static constexpr uint16_t kFlagsMf = 0x2000; // Use with `mFlagsFragmentOffset`
static constexpr uint16_t kFragmentOffsetMask = 0x1fff; // Use with `mFlagsFragmentOffset`
static constexpr uint32_t kVersIhlInit = 0x45; // Version 4, Header length = 5x8 bytes.
static constexpr uint32_t kVersIhlInit = 0x45; // Version 4, Header length = 5x4 bytes.
static constexpr uint8_t kOptionEnd = 0; ///< End of Options List
static constexpr uint8_t kOptionNop = 1; ///< No Operation
static constexpr uint8_t kOptionLsrr = 131; ///< Loose Source and Record Route
static constexpr uint8_t kOptionSsrr = 137; ///< Strict Source and Record Route
bool HasSourceRouteOption(const Message &aMessage) const;
uint8_t mVersIhl;
uint8_t mDscpEcn;
+1 -1
View File
@@ -249,7 +249,7 @@ Error Translator::TranslateIp4ToIp6(Message &aMessage)
dstPortOrId = GetDestinationPortOrIcmp4Id(ip4Headers);
#endif
aMessage.RemoveHeader(sizeof(Ip4::Header));
aMessage.RemoveHeader(ip4Headers.GetIp4Header().GetHeaderLength());
ip6Header.Clear();
ip6Header.InitVersionTrafficClassFlow();
+212
View File
@@ -30,6 +30,8 @@
#include <stdio.h>
#include <string.h>
#include "core/net/ip4_types.hpp"
#include "core/net/nat64_translator.hpp"
#include "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"
@@ -1101,6 +1103,215 @@ void TestNat64Evict(void)
Log("End of TestNat64Evict()");
}
void TestNat64IhlBypass(void)
{
Core nexus;
Node &node = nexus.CreateNode();
Ip6::Prefix prefix;
Ip4::Cidr cidr;
Ip6::Address ip6Addr;
Ip4::Address ip4Addr;
uint16_t srcPort = 5678;
uint16_t dstPort = 1234;
uint16_t payloadLen = 10;
Log("------------------------------------------------------------------------------------------------------");
Log("TestNat64IhlBypass");
nexus.AdvanceTime(0);
node.Form();
nexus.AdvanceTime(50 * Time::kOneSecondInMsec);
VerifyOrQuit(node.Get<Mle::Mle>().IsLeader());
SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelInfo));
SuccessOrQuit(prefix.FromString("fd00:7d03:7d03:7d03::/96"));
SuccessOrQuit(cidr.FromString("192.168.255.0/24"));
node.Get<Nat64::Translator>().SetNat64Prefix(prefix);
SuccessOrQuit(node.Get<Nat64::Translator>().SetIp4Cidr(cidr));
node.Get<Nat64::Translator>().SetEnabled(true);
// Create a mapping by sending a packet from IPv6 to IPv4
SuccessOrQuit(ip6Addr.FromString("fd00::1"));
SuccessOrQuit(ip4Addr.FromString("1.2.3.4"));
{
Log("Translate an IPv6 message to create mapping");
OwnedPtr<Message> message;
message.Reset(node.Get<MessagePool>().Allocate(Message::kTypeIp6));
VerifyOrQuit(message != nullptr);
Ip6::Header ip6Header;
ip6Header.Clear();
ip6Header.InitVersionTrafficClassFlow();
ip6Header.SetSource(ip6Addr);
ip6Header.GetDestination().SynthesizeFromIp4Address(prefix, ip4Addr);
ip6Header.SetNextHeader(Ip6::kProtoUdp);
ip6Header.SetPayloadLength(sizeof(Ip6::Udp::Header) + payloadLen);
SuccessOrQuit(message->Append(ip6Header));
Ip6::Udp::Header udpHeader;
udpHeader.Clear();
udpHeader.SetSourcePort(srcPort);
udpHeader.SetDestinationPort(dstPort);
udpHeader.SetLength(sizeof(Ip6::Udp::Header) + payloadLen);
SuccessOrQuit(message->Append(udpHeader));
for (uint16_t i = 0; i < payloadLen; i++)
{
SuccessOrQuit(message->Append<uint8_t>(0));
}
SuccessOrQuit(node.Get<Nat64::Translator>().TranslateIp6ToIp4(*message));
}
// Now test IPv4 to IPv6 translation with IHL=6 (options)
Log("Test 2 (IHL=6, options)");
{
OwnedPtr<Message> message;
message.Reset(node.Get<MessagePool>().Allocate(Message::kTypeIp4));
VerifyOrQuit(message != nullptr);
Ip4::Header ip4Header;
ip4Header.Clear();
// IHL=6 means header length is 6*4 = 24 bytes.
ip4Header.SetVersionIhl(0x46);
ip4Header.SetTotalLength(24 + sizeof(Ip4::Udp::Header) + payloadLen);
ip4Header.SetProtocol(Ip4::kProtoUdp);
ip4Header.SetTtl(64);
ip4Header.SetSource(ip4Addr);
// Find mapped IPv4 address
Nat64::Translator::AddressMappingIterator iterator;
Nat64::Translator::AddressMapping mapping;
iterator.Init(node.GetInstance());
SuccessOrQuit(iterator.GetNext(mapping));
ip4Header.SetDestination(AsCoreType(&mapping.mIp4));
SuccessOrQuit(message->Append(ip4Header));
// Append 4 bytes of options (NOPs)
uint8_t options[] = {0x01, 0x01, 0x01, 0x00};
SuccessOrQuit(message->Append(options));
Ip4::Udp::Header udpHeader;
udpHeader.Clear();
udpHeader.SetSourcePort(srcPort);
udpHeader.SetDestinationPort(dstPort);
udpHeader.SetLength(sizeof(Ip4::Udp::Header) + payloadLen);
SuccessOrQuit(message->Append(udpHeader));
for (uint16_t i = 0; i < payloadLen; i++)
{
SuccessOrQuit(message->Append<uint8_t>(0));
}
SuccessOrQuit(node.Get<Nat64::Translator>().TranslateIp4ToIp6(*message));
// Check the translated IPv6 packet
Ip6::Headers ip6Headers;
SuccessOrQuit(ip6Headers.ParseFrom(*message));
Log("PayloadLength=%d, SrcPort=%d, DstPort=%d", ip6Headers.GetIp6Header().GetPayloadLength(),
ip6Headers.GetSourcePort(), ip6Headers.GetDestinationPort());
VerifyOrQuit(ip6Headers.GetSourcePort() == srcPort, "Source port corrupted!");
VerifyOrQuit(ip6Headers.GetDestinationPort() == dstPort, "Destination port corrupted!");
}
// Test 3: Source route options should be discarded
Log("Test 3 (IHL=7, LSRR option)");
{
OwnedPtr<Message> message;
message.Reset(node.Get<MessagePool>().Allocate(Message::kTypeIp4));
VerifyOrQuit(message != nullptr);
Ip4::Header ip4Header;
ip4Header.Clear();
// IHL=7 means header length is 7*4 = 28 bytes.
ip4Header.SetVersionIhl(0x47);
ip4Header.SetTotalLength(28 + sizeof(Ip4::Udp::Header) + payloadLen);
ip4Header.SetProtocol(Ip4::kProtoUdp);
ip4Header.SetTtl(64);
ip4Header.SetSource(ip4Addr);
// Find mapped IPv4 address
Nat64::Translator::AddressMappingIterator iterator;
Nat64::Translator::AddressMapping mapping;
iterator.Init(node.GetInstance());
SuccessOrQuit(iterator.GetNext(mapping));
ip4Header.SetDestination(AsCoreType(&mapping.mIp4));
SuccessOrQuit(message->Append(ip4Header));
// Append LSRR option (Type 131, Length 8, Pointer 4, Address 1.1.1.1)
uint8_t options[] = {0x83, 0x08, 0x04, 0x01, 0x01, 0x01, 0x01, 0x00};
SuccessOrQuit(message->Append(options));
Ip4::Udp::Header udpHeader;
udpHeader.Clear();
udpHeader.SetSourcePort(srcPort);
udpHeader.SetDestinationPort(dstPort);
udpHeader.SetLength(sizeof(Ip4::Udp::Header) + payloadLen);
SuccessOrQuit(message->Append(udpHeader));
for (uint16_t i = 0; i < payloadLen; i++)
{
SuccessOrQuit(message->Append<uint8_t>(0));
}
// This should return kErrorDrop
VerifyOrQuit(node.Get<Nat64::Translator>().TranslateIp4ToIp6(*message) == kErrorDrop,
"LSRR option not discarded!");
}
// Test 4: SSRR option should be discarded
Log("Test 4 (IHL=7, SSRR option)");
{
OwnedPtr<Message> message;
message.Reset(node.Get<MessagePool>().Allocate(Message::kTypeIp4));
VerifyOrQuit(message != nullptr);
Ip4::Header ip4Header;
ip4Header.Clear();
// IHL=7 means header length is 7*4 = 28 bytes.
ip4Header.SetVersionIhl(0x47);
ip4Header.SetTotalLength(28 + sizeof(Ip4::Udp::Header) + payloadLen);
ip4Header.SetProtocol(Ip4::kProtoUdp);
ip4Header.SetTtl(64);
ip4Header.SetSource(ip4Addr);
// Find mapped IPv4 address
Nat64::Translator::AddressMappingIterator iterator;
Nat64::Translator::AddressMapping mapping;
iterator.Init(node.GetInstance());
SuccessOrQuit(iterator.GetNext(mapping));
ip4Header.SetDestination(AsCoreType(&mapping.mIp4));
SuccessOrQuit(message->Append(ip4Header));
// Append SSRR option (Type 137, Length 8, Pointer 4, Address 1.1.1.1)
uint8_t options[] = {0x89, 0x08, 0x04, 0x01, 0x01, 0x01, 0x01, 0x00};
SuccessOrQuit(message->Append(options));
Ip4::Udp::Header udpHeader;
udpHeader.Clear();
udpHeader.SetSourcePort(srcPort);
udpHeader.SetDestinationPort(dstPort);
udpHeader.SetLength(sizeof(Ip4::Udp::Header) + payloadLen);
SuccessOrQuit(message->Append(udpHeader));
for (uint16_t i = 0; i < payloadLen; i++)
{
SuccessOrQuit(message->Append<uint8_t>(0));
}
// This should return kErrorDrop
VerifyOrQuit(node.Get<Nat64::Translator>().TranslateIp4ToIp6(*message) == kErrorDrop,
"SSRR option not discarded!");
}
}
} // namespace Nexus
} // namespace ot
@@ -1113,6 +1324,7 @@ int main(void)
ot::Nexus::TestNat64CidrAddressReuse("192.168.103.0/30");
ot::Nexus::TestNat64CidrAddressReuse("192.168.104.0/27");
ot::Nexus::TestNat64Evict();
ot::Nexus::TestNat64IhlBypass();
printf("All tests passed\n");
return 0;
+2 -1
View File
@@ -78,8 +78,9 @@ void TestIp4Header(void)
header.Clear();
header.InitVersionIhl();
header.SetTotalLength(header.GetHeaderLength());
VerifyOrQuit(header.IsValid());
VerifyOrQuit(header.GetTotalLength() == 0);
VerifyOrQuit(header.GetTotalLength() == sizeof(Header));
VerifyOrQuit(header.GetProtocol() == 0);
VerifyOrQuit(header.GetTtl() == 0);
VerifyOrQuit(header.GetSource().mFields.m32 == 0);