[nat64] use host ID tracking for IPv4 address allocation (#11909)

This change modifies the NAT64 translator to dynamically allocate IPv4
addresses by tracking a range of host IDs within the configured CIDR.
This approach replaces the pre-allocated `mIp4AddressPool`, making it
more memory-efficient by avoiding the storage of an entire address
array.

The translator now maintains `mMinHostId` and `mMaxHostId` derived
from the configured CIDR. When allocating an IPv4 address for a new
mapping:

- If `PORT_TRANSLATION_ENABLE` is enabled, addresses are assigned
  sequentially by cycling through the host ID range. Mappings can
  share an IPv4 address as they are distinguished by translated port
  numbers.

- If `PORT_TRANSLATION_ENABLE` is disabled, a 1-to-1 address mapping
  is used. The translator cycles through host IDs to find an unused
  IPv4 address. If all addresses are allocated, it attempts to free
  expired mappings before failing.

A new test case, `TestNat64CidrAddressReuse`, is added to validate the
address allocation and reuse logic. The test ensures that all
available addresses from a CIDR are used, new requests fail when the
pool is exhausted, and addresses are correctly reused after mappings
expire. It is run against multiple CIDR sizes (`/32`, `/31`, `/30`,
and `/27`) to verify behavior across various configurations.
This commit is contained in:
Abtin Keshavarzian
2025-09-12 21:05:31 -07:00
committed by GitHub
parent d5935a34f3
commit eba5bdc434
6 changed files with 290 additions and 102 deletions
+17
View File
@@ -546,6 +546,23 @@ public:
*/
Type *GetTail(void) { return AsNonConst(AsConst(this)->GetTail()); }
/**
* Counts and returns the number of entries in the linked list.
*
* @returns The number of entries in the linked list.
*/
uint32_t CountAllEntries(void) const
{
uint32_t count = 0;
for (const Type *entry = mHead; entry != nullptr; entry = entry->GetNext())
{
count++;
}
return count;
}
// The following methods are intended to support range-based `for`
// loop iteration over the linked-list entries and should not be
// used directly.
+7
View File
@@ -238,6 +238,13 @@ public:
*/
InfoString ToString(void) const;
/**
* Gets the CIDR length (in bits).
*
* @returns The CIDR length.
*/
uint8_t GetLength(void) const { return mLength; }
/**
* Gets the prefix as a pointer to a byte array.
*
+73 -77
View File
@@ -67,6 +67,9 @@ Translator::Translator(Instance &aInstance)
: InstanceLocator(aInstance)
, mState(kStateDisabled)
, mMappingPool(aInstance)
, mMinHostId(0)
, mMaxHostId(0)
, mNextHostId(0)
, mTimer(aInstance)
{
Random::NonCrypto::Fill(mNextMappingId);
@@ -355,20 +358,6 @@ void Translator::Mapping::Free(void)
{
LogInfo("Mapping removed: %s", ToString().AsCString());
#if OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE
if (Get<Translator>().mIp4Cidr.mLength > kAddressMappingCidrLimit)
{
// If `CONFIG_NAT64_PORT_TRANSLATION_ENABLE` is enabled
// IPv4 addresses are allocated from the pool only when the
// pool size is above a minimum value. Otherwise use just the
// first address from the pool.
}
else
#endif
{
IgnoreError(Get<Translator>().mIp4AddressPool.PushBack(mIp4Address));
}
Get<Translator>().mMappingPool.Free(*this);
}
@@ -401,45 +390,70 @@ uint16_t Translator::AllocateSourcePort(uint16_t aSrcPort)
}
#endif
void Translator::GetNextIp4Address(Ip4::Address &aIp4Address)
{
aIp4Address.SynthesizeFromCidrAndHost(mIp4Cidr, mNextHostId);
mNextHostId++;
if (mNextHostId > mMaxHostId)
{
mNextHostId = mMinHostId;
}
}
#if !OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE
Error Translator::AllocateIp4Address(Ip4::Address &aIp4Address)
{
// Allocate a currently unused IPv4 address from the CIDR range.
// If all addresses are allocated, try to free expired mappings.
Error error = kErrorNone;
uint32_t numberOfHosts;
numberOfHosts = mMaxHostId - mMinHostId + 1;
if (mActiveMappings.CountAllEntries() >= numberOfHosts)
{
mActiveMappings.RemoveAndFreeAllMatching(TimerMilli::GetNow());
VerifyOrExit(mActiveMappings.CountAllEntries() < numberOfHosts, error = kErrorFailed);
}
do
{
GetNextIp4Address(aIp4Address);
} while (mActiveMappings.ContainsMatching(aIp4Address));
exit:
return error;
}
#endif
Translator::Mapping *Translator::AllocateMapping(const Ip6::Headers &aIp6Headers)
{
Mapping *mapping = nullptr;
Ip4::Address ip4Addr;
// When `PORT_TRANSLATION_ENABLE` is enabled, the translator uses
// IPv4 addresses from the provided CIDR range sequentially. In
// this case, mappings can share an IPv4 address because they are
// distinguished by their translated port numbers.
//
// Otherwise, a 1-to-1 address mapping is used, where each IPv6
// address is mapped to a unique IPv4 address from the available
// range.
#if OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE
// When port translation (`NAT64_PORT_TRANSLATION`) is enabled
// the NAT64 translator can work in 2 ways, either with a single
// IPv4 address or a larger pool of addresses. There is also the
// corner case where the address pool is generated from a big
// CIDR length and the number of available IPv4 addresses is not
// big enough to apply a 1 to 1 translation from IPv6 to IPv4
// address. When operating in the first case, there is no need to
// manage the address pool and all active mappings will use 1
// single address (or the limited number alternatively). If a
// larger pool is available each active mapping will use a
// separate IPv4 address.
if (mIp4Cidr.mLength > kAddressMappingCidrLimit)
{
// TODO: add logic to cycle between available IPv4 addresses
ip4Addr = *mIp4AddressPool.At(0);
}
else
GetNextIp4Address(ip4Addr);
#else
SuccessOrExit(AllocateIp4Address(ip4Addr));
#endif
{
if (mIp4AddressPool.IsEmpty())
{
mActiveMappings.RemoveAndFreeAllMatching(TimerMilli::GetNow());
}
VerifyOrExit(!mIp4AddressPool.IsEmpty());
ip4Addr = *mIp4AddressPool.PopBack();
}
mapping = mMappingPool.Allocate();
// We should get a valid item, there is enough space in the
// mapping pool. Otherwise return null and fail the translation.
VerifyOrExit(mapping != nullptr);
mActiveMappings.Push(*mapping);
@@ -572,50 +586,33 @@ exit:
Error Translator::SetIp4Cidr(const Ip4::Cidr &aCidr)
{
Error error = kErrorNone;
Error error = kErrorNone;
uint8_t len = aCidr.GetLength();
uint32_t numberOfHosts;
uint32_t hostIdBegin;
VerifyOrExit(aCidr.mLength > 0 && aCidr.mLength <= 32, error = kErrorInvalidArgs);
VerifyOrExit(IsValueInRange<uint8_t>(len, 1, BitSizeOf(Ip4::Address)), error = kErrorInvalidArgs);
VerifyOrExit(mIp4Cidr != aCidr);
// Avoid using the 0s and 1s in the host id of an address, but
// what if the user provides us with /32 or /31 addresses?
// Determine the usable host ID range based on the CIDR prefix
// length. For prefixes /1 through /30, the all-zeros and
// all-ones host IDs are excluded. For /31 and /32 prefixes,
// all host IDs are used.
if (aCidr.mLength == 32)
mMinHostId = 0;
mMaxHostId = (1 << (BitSizeOf(Ip4::Address) - len)) - 1;
if (len < BitSizeOf(Ip4::Address) - 1)
{
hostIdBegin = 0;
numberOfHosts = 1;
}
else if (aCidr.mLength == 31)
{
hostIdBegin = 0;
numberOfHosts = 2;
}
else
{
hostIdBegin = 1;
numberOfHosts = static_cast<uint32_t>((1 << (Ip4::Address::kSize * 8 - aCidr.mLength)) - 2);
mMinHostId++;
mMaxHostId--;
}
numberOfHosts = OT_MIN(numberOfHosts, kPoolSize);
mNextHostId = mMinHostId;
mActiveMappings.Free();
mIp4AddressPool.Clear();
for (uint32_t i = 0; i < numberOfHosts; i++)
{
Ip4::Address addr;
addr.SynthesizeFromCidrAndHost(aCidr, i + hostIdBegin);
IgnoreError(mIp4AddressPool.PushBack(addr));
}
LogInfo("IPv4 CIDR for NAT64: %s (actual address pool: %s - %s, %lu addresses)", aCidr.ToString().AsCString(),
mIp4AddressPool.Front()->ToString().AsCString(), mIp4AddressPool.Back()->ToString().AsCString(),
ToUlong(numberOfHosts));
LogInfo("IPv4 CIDR for NAT64: %s (%lu addresses)", aCidr.ToString().AsCString(),
ToUlong(mMaxHostId - mMinHostId + 1));
mIp4Cidr = aCidr;
@@ -635,7 +632,6 @@ void Translator::ClearIp4Cidr(void)
mIp4Cidr.Clear();
mActiveMappings.Free();
mIp4AddressPool.Clear();
UpdateState();
+16 -25
View File
@@ -36,7 +36,6 @@
#include "openthread-core-config.h"
#include "common/array.hpp"
#include "common/locator.hpp"
#include "common/owning_list.hpp"
#include "common/pool.hpp"
@@ -284,20 +283,6 @@ private:
static constexpr uint32_t kPoolSize = OPENTHREAD_CONFIG_NAT64_MAX_MAPPINGS;
#if OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE
// Under `PORT_TRANSLATION_ENABLE`, the translator can operate in
// two modes: 1-to-1 address mapping where each IPv6 address gets
// a unique IPv4 address from a pool, or having the mappings
// share one (or a few) IPv4 addresses and are distinguished by
// the translated port numbers.
//
// This constant defines the maximum allowed CIDR prefix length
// for the IPv4 address pool to be considered large enough to use
// 1-to-1 address mapping. If the configured prefix length is
// greater than this value (e.g., /29, /30), the address pool is
// too small, and the translator will fall back to using port
// translation.
static constexpr uint8_t kAddressMappingCidrLimit = 28;
static constexpr uint16_t kMinTranslationPort = 49152;
static constexpr uint16_t kMaxTranslationPort = 65535;
#endif
@@ -323,6 +308,8 @@ private:
bool Matches(const TimeMilli aNow) const { return mExpiry < aNow; }
#if OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE
bool Matches(const uint16_t aPort) const { return mTranslatedPortOrId == aPort; }
#else
bool Matches(const Ip4::Address &aIp4Address) const { return mIp4Address == aIp4Address; }
#endif
Mapping *mNext;
@@ -343,6 +330,8 @@ private:
void UpdateState(void);
Error TranslateIcmp4(Message &aMessage, uint16_t aOriginalId);
Error TranslateIcmp6(Message &aMessage, uint16_t aTranslatedId);
void GetNextIp4Address(Ip4::Address &aIp4Address);
Error AllocateIp4Address(Ip4::Address &aIp4Address);
Mapping *AllocateMapping(const Ip6::Headers &aIp6Headers);
void HandleTimer(void);
#if OPENTHREAD_CONFIG_NAT64_PORT_TRANSLATION_ENABLE
@@ -354,16 +343,18 @@ private:
using TranslatorTimer = TimerMilliIn<Translator, &Translator::HandleTimer>;
State mState;
uint64_t mNextMappingId;
Array<Ip4::Address, kPoolSize> mIp4AddressPool;
Pool<Mapping, kPoolSize> mMappingPool;
OwningList<Mapping> mActiveMappings;
Ip6::Prefix mNat64Prefix;
Ip4::Cidr mIp4Cidr;
TranslatorTimer mTimer;
ProtocolCounters mCounters;
ErrorCounters mErrorCounters;
State mState;
uint64_t mNextMappingId;
Pool<Mapping, kPoolSize> mMappingPool;
OwningList<Mapping> mActiveMappings;
Ip6::Prefix mNat64Prefix;
Ip4::Cidr mIp4Cidr;
uint32_t mMinHostId;
uint32_t mMaxHostId;
uint32_t mNextHostId;
TranslatorTimer mTimer;
ProtocolCounters mCounters;
ErrorCounters mErrorCounters;
};
#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+173
View File
@@ -591,6 +591,175 @@ void TestNat64Mapping(void)
VerifyOrQuit(iterator.GetNext(mapping) == kErrorNotFound);
}
void TestNat64CidrAddressReuse(const char *aCidr)
{
static constexpr uint32_t kExpireTimeout = 120 * Time::kOneSecondInMsec;
static constexpr uint16_t kSrcPort = 55387;
static constexpr uint16_t kDstPort = 55388;
static constexpr uint16_t kPayloadLength = 32;
Core nexus;
Node &node = nexus.CreateNode();
Ip6::Prefix prefix;
Ip4::Cidr cidr;
uint16_t numIp4Addrs;
Nat64::Translator::AddressMappingIterator iterator;
Nat64::Translator::AddressMapping mapping;
OwnedPtr<Message> message;
Ip6::Address ip6Addr;
Ip4::Address ip4Addr;
Ip4::Headers ip4Headers;
uint16_t count;
Log("------------------------------------------------------------------------------------------------------");
Log("TestNat64CidrAddressReuse(%s)", aCidr);
nexus.AdvanceTime(0);
node.Form();
nexus.AdvanceTime(50 * Time::kOneSecondInMsec);
VerifyOrQuit(node.Get<Mle::Mle>().IsLeader());
node.Get<Instance>().SetLogLevel(kLogLevelInfo);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Enable NAT64 translator");
SuccessOrQuit(prefix.FromString("fd01::/96"));
SuccessOrQuit(cidr.FromString(aCidr));
node.Get<Nat64::Translator>().SetNat64Prefix(prefix);
SuccessOrQuit(node.Get<Nat64::Translator>().SetIp4Cidr(cidr));
node.Get<Nat64::Translator>().SetEnabled(true);
VerifyOrQuit(node.Get<Nat64::Translator>().GetState() == Nat64::kStateActive);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Determine number of available IPv4 addresses");
switch (cidr.GetLength())
{
case 32:
numIp4Addrs = 1;
break;
case 31:
numIp4Addrs = 2;
break;
default:
numIp4Addrs = (1 << (32 - cidr.GetLength())) - 2;
break;
}
Log("Number of available IPv4 addresses: %u", numIp4Addrs);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Translate IPv6 messages to use all IPv4 addresses");
for (uint16_t i = 0; i < numIp4Addrs; i++)
{
SuccessOrQuit(ip6Addr.FromString("fd02::0"));
SuccessOrQuit(ip4Addr.FromString("200.100.1.1"));
ip6Addr.mFields.m8[15] = i;
message.Reset(PrepareMessage(node, ip6Addr, ip4Addr, kSrcPort, kDstPort, kPayloadLength));
SuccessOrQuit(node.Get<Nat64::Translator>().TranslateIp6ToIp4(*message));
SuccessOrQuit(ip4Headers.ParseFrom(*message));
VerifyOrQuit(ip4Headers.GetDestinationAddress() == ip4Addr);
VerifyOrQuit(ip4Headers.IsUdp());
VerifyOrQuit(ip4Headers.GetSourcePort() == kSrcPort);
VerifyOrQuit(ip4Headers.GetDestinationPort() == kDstPort);
VerifyOrQuit(ip4Headers.GetUdpHeader().GetLength() == kPayloadLength);
nexus.AdvanceTime(1000);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Check the Address Mappings");
iterator.Init(node.GetInstance());
for (count = 0; iterator.GetNext(mapping) == kErrorNone; count++)
{
LogAddressMapping(mapping);
VerifyOrQuit(mapping.mRemainingTimeMs > 0);
}
VerifyOrQuit(count == numIp4Addrs);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Validate that next translation fails since all IPv4 addresses are in use");
SuccessOrQuit(ip6Addr.FromString("fd02::100"));
message.Reset(PrepareMessage(node, ip6Addr, ip4Addr, kSrcPort, kDstPort, kPayloadLength));
VerifyOrQuit(node.Get<Nat64::Translator>().TranslateIp6ToIp4(*message) == kErrorDrop);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Wait for mapping entries to expire");
nexus.AdvanceTime(kExpireTimeout);
iterator.Init(node.GetInstance());
for (count = 0; iterator.GetNext(mapping) == kErrorNone; count++)
{
LogAddressMapping(mapping);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Translate IPv6 messages again and check that IPv4 addresses are now reused");
for (uint16_t i = 0; i < numIp4Addrs; i++)
{
SuccessOrQuit(ip6Addr.FromString("fd02::200:0"));
SuccessOrQuit(ip4Addr.FromString("200.100.3.3"));
ip6Addr.mFields.m8[15] = i + 100;
message.Reset(PrepareMessage(node, ip6Addr, ip4Addr, kSrcPort, kDstPort, kPayloadLength));
SuccessOrQuit(node.Get<Nat64::Translator>().TranslateIp6ToIp4(*message));
SuccessOrQuit(ip4Headers.ParseFrom(*message));
VerifyOrQuit(ip4Headers.GetDestinationAddress() == ip4Addr);
VerifyOrQuit(ip4Headers.IsUdp());
VerifyOrQuit(ip4Headers.GetSourcePort() == kSrcPort);
VerifyOrQuit(ip4Headers.GetDestinationPort() == kDstPort);
VerifyOrQuit(ip4Headers.GetUdpHeader().GetLength() == kPayloadLength);
nexus.AdvanceTime(1000);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Check the Address Mappings");
iterator.Init(node.GetInstance());
for (count = 0; iterator.GetNext(mapping) == kErrorNone; count++)
{
LogAddressMapping(mapping);
VerifyOrQuit(mapping.mRemainingTimeMs > 0);
}
VerifyOrQuit(count == numIp4Addrs);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Log("Validate that next translation fails since all IPv4 addresses are in use");
SuccessOrQuit(ip6Addr.FromString("fd02::200:100"));
message.Reset(PrepareMessage(node, ip6Addr, ip4Addr, kSrcPort, kDstPort, kPayloadLength));
VerifyOrQuit(node.Get<Nat64::Translator>().TranslateIp6ToIp4(*message) == kErrorDrop);
Log("End of TestNat64CidrAddressReuse(%s)", aCidr);
}
} // namespace Nexus
} // namespace ot
@@ -598,6 +767,10 @@ int main(void)
{
ot::Nexus::TestNat64StateChanges();
ot::Nexus::TestNat64Mapping();
ot::Nexus::TestNat64CidrAddressReuse("192.168.101.133/32");
ot::Nexus::TestNat64CidrAddressReuse("192.168.102.178/31");
ot::Nexus::TestNat64CidrAddressReuse("192.168.103.0/30");
ot::Nexus::TestNat64CidrAddressReuse("192.168.104.0/27");
printf("All tests passed\n");
return 0;
+4
View File
@@ -93,6 +93,7 @@ void VerifyLinkedListContent(const LinkedList<Entry> *aList, ...)
Entry *argPrev = nullptr;
const Entry *prev;
uint16_t unusedId = 100;
uint16_t count = 0;
va_start(args, aList);
@@ -117,6 +118,7 @@ void VerifyLinkedListContent(const LinkedList<Entry> *aList, ...)
VerifyOrQuit(!argEntry->WasFreed());
argPrev = argEntry;
count++;
}
argEntry = va_arg(args, Entry *);
@@ -129,6 +131,8 @@ void VerifyLinkedListContent(const LinkedList<Entry> *aList, ...)
VerifyOrQuit(aList->FindMatching("none") == nullptr, "succeeded for a missing entry");
VerifyOrQuit(aList->FindMatching(unusedId) == nullptr, "succeeded for a missing entry");
VerifyOrQuit(aList->CountAllEntries() == count);
}
void TestLinkedList(void)