From 5edb9fbb0be399db40760fd0115f830d364bc8e1 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sat, 29 Aug 2026 07:17:56 +0200 Subject: [PATCH] [posix] install route for the NAT64 prefix on the Thread interface (#13553) The posix platform installs an IPv4 route for the NAT64 CIDR into the Thread network interface so that translated traffic finds its way back to the translator. The IPv6 side has no equivalent: the NAT64 prefix is published as an external route in the network data, but routes published by the device itself are deliberately not installed in the kernel. As a result the border router host cannot use the NAT64 service it provides itself. Traffic to NAT64-synthesized addresses leaks out the default route, or fails with ENETUNREACH when there is no IPv6 default route. This affects the OpenThread features whose sockets are bound to the unspecified network interface and therefore routed by the host: the DNS client (default `OPENTHREAD_CONFIG_DNS_CLIENT_BIND_UDP_TO_THREAD_NETIF` of 0) and the SNTP client, as well as applications running on the host. It makes the built-in NAT64 diagnostics fail in a way that suggests NAT64 itself is broken: `dns resolve4 ` times out while `ping` to the same synthesized address succeeds, because the ping sender runs inside the OpenThread stack where network data routing reaches the translator directly (see discussion #9782). Traffic originated by Thread devices is translated in the stack as well and was never affected. Install a route for the translator's NAT64 prefix into the Thread network interface while the translator is active, mirroring the existing NAT64 IPv4 CIDR route handling, and track which routes are installed so that repeated state change notifications and host netif link transitions do not add or delete a route twice. `Nat64::Translator::SetNat64Prefix()` now signals `kEventNat64TranslatorStateChanged` (like `SetIp4Cidr()` does), so a prefix change while the translator is already active is reported to the platform. A new public API `otNat64GetIp6Prefix()` is added to retrieve the IPv6 prefix configured in the NAT64 translator, mirroring `otNat64GetCidr()`. Also fix the guards of `AddIp4Route()`/`DeleteIp4Route()` and of the `isIp4` local in `processTransmit()`, which required `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` while their users only require `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE`, breaking the build of a NAT64 translator without border routing. --- include/openthread/instance.h | 2 +- include/openthread/nat64.h | 13 +++ src/core/api/nat64_api.cpp | 7 ++ src/core/net/nat64_translator.cpp | 5 + src/posix/platform/netif.cpp | 162 +++++++++++++++++++++++------- 5 files changed, 152 insertions(+), 37 deletions(-) diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 01ba4c0f0..d94d20587 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (619) +#define OPENTHREAD_API_VERSION (620) /** * @addtogroup api-instance diff --git a/include/openthread/nat64.h b/include/openthread/nat64.h index aa52efe79..2fa95d26b 100644 --- a/include/openthread/nat64.h +++ b/include/openthread/nat64.h @@ -410,6 +410,19 @@ void otNat64SetReceiveIp4Callback(otInstance *aInstance, otNat64ReceiveIp4Callba */ otError otNat64GetCidr(otInstance *aInstance, otIp4Cidr *aCidr); +/** + * Gets the IPv6 prefix configured in the NAT64 translator. + * + * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[out] aPrefix A pointer to an `otIp6Prefix` to output the prefix. + * + * @retval OT_ERROR_NONE The prefix was successfully filled. + * @retval OT_ERROR_NOT_FOUND The translator is not configured with an IPv6 prefix. + */ +otError otNat64GetIp6Prefix(otInstance *aInstance, otIp6Prefix *aPrefix); + /** * Test if two IPv4 addresses are the same. * diff --git a/src/core/api/nat64_api.cpp b/src/core/api/nat64_api.cpp index 380799546..f2e87fd26 100644 --- a/src/core/api/nat64_api.cpp +++ b/src/core/api/nat64_api.cpp @@ -100,6 +100,13 @@ otError otNat64GetCidr(otInstance *aInstance, otIp4Cidr *aCidr) return AsCoreType(aInstance).Get().GetIp4Cidr(AsCoreType(aCidr)); } +otError otNat64GetIp6Prefix(otInstance *aInstance, otIp6Prefix *aPrefix) +{ + AssertPointerIsNotNull(aPrefix); + + return AsCoreType(aInstance).Get().GetNat64Prefix(AsCoreType(aPrefix)); +} + otNat64State otNat64GetTranslatorState(otInstance *aInstance) { return MapEnum(AsCoreType(aInstance).Get().GetState()); diff --git a/src/core/net/nat64_translator.cpp b/src/core/net/nat64_translator.cpp index ccb3d718b..1641366f0 100644 --- a/src/core/net/nat64_translator.cpp +++ b/src/core/net/nat64_translator.cpp @@ -760,6 +760,11 @@ void Translator::SetNat64Prefix(const Ip6::Prefix &aNat64Prefix) mNat64Prefix = aNat64Prefix; UpdateState(); + // `UpdateState()` only signals when the state itself changes. Signal here + // as well (similar to `SetIp4Cidr()`), so that a prefix change while the + // translator is already active gets reported too. + Get().Signal(kEventNat64TranslatorStateChanged); + exit: return; } diff --git a/src/posix/platform/netif.cpp b/src/posix/platform/netif.cpp index 829dc697a..894c34fc8 100644 --- a/src/posix/platform/netif.cpp +++ b/src/posix/platform/netif.cpp @@ -903,7 +903,8 @@ exit: } #endif // __linux__ -#if OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE || OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE +#if OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE || OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE || \ + OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE static otError AddRoute(const otIp6Prefix &aPrefix, uint32_t aPriority) { return AddRoute(aPrefix.mPrefix.mFields.m8, aPrefix.mLength, aPriority); @@ -913,7 +914,7 @@ static otError DeleteRoute(const otIp6Prefix &aPrefix) { return DeleteRoute(aPrefix.mPrefix.mFields.m8, aPrefix.mLength); } -#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE || OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE +#endif #if OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE static bool HasAddedOmrRoute(const otIp6Prefix &aOmrPrefix) @@ -1091,7 +1092,7 @@ exit: } #endif // OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE -#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE +#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE static otError AddIp4Route(const otIp4Cidr &aIp4Cidr, uint32_t aPriority) { return AddRoute(aIp4Cidr.mAddress.mFields.m8, aIp4Cidr.mLength, aPriority); @@ -1118,7 +1119,10 @@ static void processAddressChange(const otIp6AddressInfo *aAddressInfo, bool aIsA #if defined(__linux__) && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE -static otIp4Cidr sActiveNat64Cidr; +static otIp4Cidr sActiveNat64Cidr; +static otIp6Prefix sActiveNat64Ip6Prefix; +static bool sIsNat64Ip4RouteAdded = false; +static bool sIsNat64Ip6RouteAdded = false; static constexpr uint32_t kNat64RoutePriority = 100; // Priority for route to NAT64 CIDR, 100 means a high priority. @@ -1127,6 +1131,7 @@ static bool isSameIp4Cidr(const otIp4Cidr &aCidr1, const otIp4Cidr &aCidr2) bool res = true; VerifyOrExit(aCidr1.mLength == aCidr2.mLength, res = false); + VerifyOrExit(aCidr1.mLength > 0); // The higher (32 - length) bits must be the same, host bits are ignored. VerifyOrExit(((ntohl(aCidr1.mAddress.mFields.m32) ^ ntohl(aCidr2.mAddress.mFields.m32)) >> (32 - aCidr1.mLength)) == @@ -1137,50 +1142,127 @@ exit: return res; } +static void addNat64Routes(void) +{ + otError error; + + if (!sIsNat64Ip4RouteAdded && sActiveNat64Cidr.mLength > 0) + { + if ((error = AddIp4Route(sActiveNat64Cidr, kNat64RoutePriority)) != OT_ERROR_NONE) + { + LogWarn("failed to add route for NAT64 CIDR: %s", otThreadErrorToString(error)); + } + else + { + sIsNat64Ip4RouteAdded = true; + LogInfo("Added route for NAT64 CIDR"); + } + } + + // Route the NAT64 prefix into the Thread network interface, so that traffic + // originated by the host to NAT64-synthesized addresses reaches the translator. + if (!sIsNat64Ip6RouteAdded && sActiveNat64Ip6Prefix.mLength > 0) + { + if ((error = AddRoute(sActiveNat64Ip6Prefix, kNat64RoutePriority)) != OT_ERROR_NONE) + { + LogWarn("failed to add route for NAT64 prefix: %s", otThreadErrorToString(error)); + } + else + { + sIsNat64Ip6RouteAdded = true; + LogInfo("Added route for NAT64 prefix"); + } + } +} + +static void deleteNat64Ip4Route(void) +{ + otError error; + + VerifyOrExit(sIsNat64Ip4RouteAdded); + + if ((error = DeleteIp4Route(sActiveNat64Cidr)) != OT_ERROR_NONE) + { + LogWarn("failed to delete route for NAT64 CIDR: %s", otThreadErrorToString(error)); + } + else + { + LogInfo("Deleted route for NAT64 CIDR"); + } + + sIsNat64Ip4RouteAdded = false; + +exit: + return; +} + +static void deleteNat64Ip6Route(void) +{ + otError error; + + VerifyOrExit(sIsNat64Ip6RouteAdded); + + if ((error = DeleteRoute(sActiveNat64Ip6Prefix)) != OT_ERROR_NONE) + { + LogWarn("failed to delete route for NAT64 prefix: %s", otThreadErrorToString(error)); + } + else + { + LogInfo("Deleted route for NAT64 prefix"); + } + + sIsNat64Ip6RouteAdded = false; + +exit: + return; +} + static void processNat64StateChange(void) { - otIp4Cidr translatorCidr; - otError error = OT_ERROR_NONE; + otIp4Cidr translatorCidr; + otIp6Prefix translatorIp6Prefix; - // Skip if NAT64 translator has not been configured with a CIDR. - SuccessOrExit(otNat64GetCidr(gInstance, &translatorCidr)); + if (otNat64GetCidr(gInstance, &translatorCidr) != OT_ERROR_NONE) + { + memset(&translatorCidr, 0, sizeof(translatorCidr)); + } if (!isSameIp4Cidr(translatorCidr, sActiveNat64Cidr)) // Someone sets a new CIDR for NAT64. { char cidrString[OT_IP4_CIDR_STRING_SIZE]; - if (sActiveNat64Cidr.mLength != 0) - { - if ((error = DeleteIp4Route(sActiveNat64Cidr)) != OT_ERROR_NONE) - { - LogWarn("failed to delete route for NAT64: %s", otThreadErrorToString(error)); - } - } + deleteNat64Ip4Route(); // Delete the route of the previous CIDR, if any. sActiveNat64Cidr = translatorCidr; - otIp4CidrToString(&translatorCidr, cidrString, sizeof(cidrString)); + otIp4CidrToString(&sActiveNat64Cidr, cidrString, sizeof(cidrString)); LogInfo("NAT64 CIDR updated to %s.", cidrString); } + if (otNat64GetIp6Prefix(gInstance, &translatorIp6Prefix) != OT_ERROR_NONE) + { + memset(&translatorIp6Prefix, 0, sizeof(translatorIp6Prefix)); + } + + if (!otIp6ArePrefixesEqual(&translatorIp6Prefix, &sActiveNat64Ip6Prefix)) // The NAT64 prefix changed. + { + char prefixString[OT_IP6_PREFIX_STRING_SIZE]; + + deleteNat64Ip6Route(); // Delete the route of the previous prefix, if any. + sActiveNat64Ip6Prefix = translatorIp6Prefix; + + otIp6PrefixToString(&sActiveNat64Ip6Prefix, prefixString, sizeof(prefixString)); + LogInfo("NAT64 prefix updated to %s.", prefixString); + } + if (otNat64GetTranslatorState(gInstance) == OT_NAT64_STATE_ACTIVE) { - if ((error = AddIp4Route(sActiveNat64Cidr, kNat64RoutePriority)) != OT_ERROR_NONE) - { - LogWarn("failed to add route for NAT64: %s", otThreadErrorToString(error)); - } - LogInfo("Adding route for NAT64"); + addNat64Routes(); } - else if (sActiveNat64Cidr.mLength > 0) // Translator is not active. + else // Translator is not active. { - if ((error = DeleteIp4Route(sActiveNat64Cidr)) != OT_ERROR_NONE) - { - LogWarn("failed to delete route for NAT64: %s", otThreadErrorToString(error)); - } - LogInfo("Deleting route for NAT64"); + deleteNat64Ip4Route(); + deleteNat64Ip6Route(); } - -exit: - return; } #endif // defined(__linux__) && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE @@ -1373,7 +1455,7 @@ static void processTransmit(otInstance *aInstance) char packet[kMaxIp6Size]; otError error = OT_ERROR_NONE; size_t offset = 0; -#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE +#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE bool isIp4 = false; #endif @@ -1606,15 +1688,23 @@ static void processNetifLinkEvent(otInstance *aInstance, struct nlmsghdr *aNetli LogInfo("Succeeded to sync netif state with host"); } -#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE - if (isUp && otNat64GetTranslatorState(gInstance) == OT_NAT64_STATE_ACTIVE) +#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE + if (isUp) { - // Recover NAT64 route. - if ((error = AddIp4Route(sActiveNat64Cidr, kNat64RoutePriority)) != OT_ERROR_NONE) + if (otNat64GetTranslatorState(gInstance) == OT_NAT64_STATE_ACTIVE) { - LogWarn("failed to add route for NAT64: %s", otThreadErrorToString(error)); + // Recover the NAT64 routes. This is a no-op for routes that are + // already installed. + addNat64Routes(); } } + else + { + // The kernel removes the routes of an interface that goes down, so + // track them as no longer installed without issuing netlink requests. + sIsNat64Ip4RouteAdded = false; + sIsNat64Ip6RouteAdded = false; + } #endif exit: