From f90d50b220273075d74dda58203e7285002921b7 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Fri, 20 Jun 2025 19:35:44 -0700 Subject: [PATCH] [dhcp6] implement DHCPv6 Prefix Delegation (PD) client (#11584) This commit introduces the `Dhcp6PdClient` class, which implements DHCPv6 Prefix Delegation (PD) client functionality. It integrates with `BorderRouter::RoutingManager` and its `PdPrefixManager` sub-component. The CMake `OT_BORDER_ROUTING_DHCP6_PD_CLIENT` mapped to `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` config enables this feature. Previously, the platform layer was expected to implement client functionality, acquiring and providing the delegated prefix(es) to the OT stack using `otPlatBorderRouter*` callbacks. This approach continues to be supported. The `Dhcp6PdClient` feature adds native support for this functionality in the OpenThread core. The `Dhcp6PdClient` implementation follows RFC 8415, focusing on prefix delegation and all required behaviors. The client follows the standard four-message Solicit/Advertise/Request/Reply exchange to obtain a delegated prefix, followed by a two-message Renew/Reply or Rebind/Reply exchange to extend the lifetime of the delegated prefix. When the prefix is no longer needed, a two-message Release/Reply exchange ends its lease. The current client implementation does not support the optional "Reconfigure Accept" mechanism. A set of `otPlatInfraIfDhcp6PdClient*` platform APIs are also introduced for use by the `Dhcp6PdClient`. These APIs are used to enable or disable listening for DHCPv6 messages and to handle sending and receiving them on the standard client and server UDP ports (546 and 547), effectively acting as a UDP socket. This commit also includes a comprehensive unit test covering various aspects of `Dhcp6PdClient`, including common behaviors and many specific edge cases. --- etc/cmake/options.cmake | 1 + examples/platforms/simulation/infra_if.c | 22 + include/openthread/instance.h | 2 +- include/openthread/platform/border_routing.h | 54 +- include/openthread/platform/infra_if.h | 61 + src/core/BUILD.gn | 2 + src/core/CMakeLists.txt | 1 + src/core/border_router/dhcp6_pd_client.cpp | 1359 ++++++++ src/core/border_router/dhcp6_pd_client.hpp | 275 ++ src/core/border_router/infra_if.cpp | 39 + src/core/border_router/infra_if.hpp | 46 +- src/core/border_router/routing_manager.cpp | 20 +- src/core/common/message.cpp | 5 + src/core/common/message.hpp | 16 + src/core/config/border_routing.h | 28 +- src/core/instance/instance.cpp | 3 + src/core/instance/instance.hpp | 10 +- src/core/net/dhcp6_types.cpp | 41 +- src/core/net/dhcp6_types.hpp | 268 +- tests/nexus/openthread-core-nexus-config.h | 1 + ...openthread-core-toranj-config-simulation.h | 2 + tests/unit/CMakeLists.txt | 1 + tests/unit/test_dhcp6_pd_client.cpp | 2796 +++++++++++++++++ tests/unit/test_platform.cpp | 27 +- 24 files changed, 5030 insertions(+), 50 deletions(-) create mode 100644 src/core/border_router/dhcp6_pd_client.cpp create mode 100644 src/core/border_router/dhcp6_pd_client.hpp create mode 100644 tests/unit/test_dhcp6_pd_client.cpp diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 3d982e6bb..e0ae40d9b 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -180,6 +180,7 @@ ot_option(OT_BORDER_AGENT_MESHCOP_SERVICE OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP ot_option(OT_BORDER_ROUTER OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE "border router") ot_option(OT_BORDER_ROUTING OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE "border routing") ot_option(OT_BORDER_ROUTING_DHCP6_PD OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE "dhcpv6 pd support in border routing") +ot_option(OT_BORDER_ROUTING_DHCP6_PD_CLIENT OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE "dhcp6 pd client") ot_option(OT_BORDER_ROUTING_COUNTERS OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE "border routing counters") ot_option(OT_CHANNEL_MANAGER OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE "channel manager") ot_option(OT_CHANNEL_MANAGER_CSL OPENTHREAD_CONFIG_CHANNEL_MANAGER_CSL_CHANNEL_SELECT_ENABLE "channel manager for csl channel") diff --git a/examples/platforms/simulation/infra_if.c b/examples/platforms/simulation/infra_if.c index ad6d68e42..7fd20fb3b 100644 --- a/examples/platforms/simulation/infra_if.c +++ b/examples/platforms/simulation/infra_if.c @@ -214,6 +214,28 @@ otError otPlatInfraIfDiscoverNat64Prefix(uint32_t aInfraIfIndex) return OT_ERROR_NONE; } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + +void otPlatInfraIfDhcp6PdClientSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aEnable); + OT_UNUSED_VARIABLE(aInfraIfIndex); +} + +void otPlatInfraIfDhcp6PdClientSend(otInstance *aInstance, + otMessage *aMessage, + otIp6Address *aDestAddress, + uint32_t aInfraIfIndex) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aDestAddress); + OT_UNUSED_VARIABLE(aInfraIfIndex); + otMessageFree(aMessage); +} + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + //--------------------------------------------------------------------------------------------------------------------- // platformInfraIf diff --git a/include/openthread/instance.h b/include/openthread/instance.h index c9b97d470..c7cdd16dc 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 (512) +#define OPENTHREAD_API_VERSION (513) /** * @addtogroup api-instance diff --git a/include/openthread/platform/border_routing.h b/include/openthread/platform/border_routing.h index 954e455fc..8e983ef09 100644 --- a/include/openthread/platform/border_routing.h +++ b/include/openthread/platform/border_routing.h @@ -45,39 +45,53 @@ extern "C" { #endif /** - * Handles ICMP6 RA messages received on the Thread interface on the platform. + * Callback from the platform to report DHCPv6 Prefix Delegation (PD) prefixes. * - * The `aMessage` should point to a buffer of a valid ICMPv6 message (without IP headers) with router advertisement as - * the value of type field of the message. + * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` and `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE` to + * be enabled, while `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` should be disabled. * - * When DHCPv6 PD is disabled, the message will be dropped silently. + * When `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` is enabled, the OpenThread stack's native DHCPv6 + * PD client will be used. Otherwise, the platform layer is expected to interact with DHCPv6 servers to acquire + * and provide the delegated prefix(es) using this callback or `otPlatBorderRoutingProcessDhcp6PdPrefix()`. * - * Note: RA messages will not be forwarded into Thread networks, while for many platforms, RA messages is the way of - * distributing a prefix and other infomations to the downstream network. The typical usecase of this function is to - * handle the router advertisement messages sent by the platform as a result of DHCPv6 Prefix Delegation. + * In this function, an ICMPv6 Router Advertisement (received on the platform's Thread interface) is passed to the + * OpenThread stack. This RA message is intended as a mechanism to distribute DHCPv6 PD prefixes to a Thread Border + * Router. Each Prefix Information Option (PIO) in the RA is evaluated as a candidate DHCPv6 PD prefix. * - * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE`. + * This function can be called again to renew/refresh the lifetimes of PD prefixes or to signal their deprecation + * (by setting a zero "preferred lifetime") or removal (by setting a zero "valid lifetime"). + * + * Important note: it is not expected that the RA message will contain all currently valid PD prefixes. The OT stack + * will parse the RA and process all included PIOs as PD prefix candidates. Any previously reported PD prefix (from an + * earlier call to this function or `otPlatBorderRoutingProcessDhcp6PdPrefix()`) that does not appear in the new RA + * remains unchanged (i.e., it will be assumed valid until its previously indicated lifetime expires). + * + * The `aMessage` should point to a buffer containing the ICMPv6 message payload (excluding the IP headers but + * including the ICMPv6 header) with "Router Advertisement" (code 134) as the value of the `Type` field in the ICMPv6 + * header. * * @param[in] aInstance A pointer to an OpenThread instance. - * @param[in] aMessage A pointer to an ICMPv6 RouterAdvertisement message. - * @param[in] aLength The length of ICMPv6 RouterAdvertisement message. + * @param[in] aMessage A pointer to an ICMPv6 Router Advertisement message. + * @param[in] aLength The length of the ICMPv6 Router Advertisement message. */ extern void otPlatBorderRoutingProcessIcmp6Ra(otInstance *aInstance, const uint8_t *aMessage, uint16_t aLength); /** - * Process a prefix received from the DHCPv6 PD Server. The prefix is received on - * the DHCPv6 PD client callback and provided to the Routing Manager via this - * API. + * Callback to report a single DHCPv6 Prefix Delegation (PD) prefix. * - * The prefix lifetime can be updated by calling the function again with updated time values. - * If the preferred lifetime of the prefix is set to 0, the prefix becomes deprecated. - * When this function is called multiple times, the smallest prefix is preferred as this rule allows - * choosing a GUA instead of a ULA. + * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` and `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE` to + * be enabled, while `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` should be disabled. * - * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE`. + * When `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` is enabled, the OpenThread stack's native DHCPv6 + * PD client will be used. Otherwise, the platform layer is expected to interact with DHCPv6 servers to acquire + * and provide the delegated prefix(es) using this callback or `otPlatBorderRoutingProcessIcmp6Ra()`. * - * @param[in] aInstance A pointer to an OpenThread instance. - * @param[in] aPrefixInfo A pointer to the prefix information structure + * This function can be called again to renew/refresh the lifetimes of PD prefixes or to signal their deprecation + * (by setting a zero "preferred lifetime") or removal (by setting a zero "valid lifetime"). This function may be + * called multiple times to provide different PD prefixes. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aPrefixInfo A pointer to the prefix information structure. */ extern void otPlatBorderRoutingProcessDhcp6PdPrefix(otInstance *aInstance, const otBorderRoutingPrefixTableEntry *aPrefixInfo); diff --git a/include/openthread/platform/infra_if.h b/include/openthread/platform/infra_if.h index 60e67f3cd..3f911a7fc 100644 --- a/include/openthread/platform/infra_if.h +++ b/include/openthread/platform/infra_if.h @@ -41,6 +41,7 @@ #include #include #include +#include #ifdef __cplusplus extern "C" { @@ -183,6 +184,66 @@ otError otPlatGetInfraIfLinkLayerAddress(otInstance *aInstanc uint32_t aIfIndex, otPlatInfraIfLinkLayerAddress *aInfraIfLinkLayerAddress); +//--------------------------------------------------------------------------------------------------------------------- +// DHCPv6 Prefix Delegation platform APIs (`OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE`) + +/** + * Enables or disables listening for DHCPv6 Prefix Delegation (PD) messages on client. + * + * This function is only used when `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` is enabled. + * + * When enabled, the platform must open a UDP socket on the specified infrastructure interface, binding to the DHCPv6 + * client port 546 to receive messages from DHCPv6 servers. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aEnable A boolean to enable (`true`) or disable (`false`) listening. + * @param[in] aInfraIfIndex The index of the infrastructure interface to operate on. + * + */ +void otPlatInfraIfDhcp6PdClientSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex); + +/** + * Sends a DHCPv6 message to a unicast or multicast destination address. + * + * This function is only used when `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` is enabled. + * + * The platform is responsible for constructing a UDP datagram with the given DHCPv6 message as its payload. The + * datagram must be sent from the DHCPv6 client port (546) to the server port (547) on the specified infrastructure + * interface. The destination IPv6 address can be a unicast address or the multicast `All_DHCP_Relay_Agents_and_Servers` + * address (`ff02::1:2`). + * + * This function passes the ownership of @p aMessage to the platform layer. Platform MUST then free the message + * when no longer needed. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aMessage An `otMessage` containing the DHCPv6 payload. Ownership is passed to the platform layer. + * @param[in] aDestAddress The IPv6 multicast or unicast destination address. + * @param[in] aInfraIfIndex The index of the infrastructure interface from which to send the message. + */ +void otPlatInfraIfDhcp6PdClientSend(otInstance *aInstance, + otMessage *aMessage, + otIp6Address *aDestAddress, + uint32_t aInfraIfIndex); + +/** + * Callback from the platform to notify the OpenThread stack of a received DHCPv6 message. + * + * This function is provided when `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` is enabled. + * + * The platform calls this function whenever a DHCPv6 message is received on the client port (546) while listening on + * this port is enabled (refer to `otPlatInfraIfDhcp6PdClientSetListeningEnabled()`). + * + * The platform is responsible for allocating the `otMessage` to pass the received UDP payload. Ownership of the + * @p aMessage is passed to the OpenThread stack (which will free it once no longer needed). + + * @param[in] aInstance The OpenThread instance. + * @param[in] aMessage The `otMessage` containing the received DHCPv6 payload. Ownership is passed to OT stack. + * @param[in] aInfraIfIndex The index of the infrastructure interface from which the message was received.. + */ +extern void otPlatInfraIfDhcp6PdClientHandleReceived(otInstance *aInstance, + otMessage *aMessage, + uint32_t aInfraIfIndex); + /** * @} */ diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index cd586c0cb..b3a9900bd 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -374,6 +374,8 @@ openthread_core_files = [ "backbone_router/multicast_listeners_table.hpp", "backbone_router/ndproxy_table.cpp", "backbone_router/ndproxy_table.hpp", + "border_router/dhcp6_pd_client.cpp", + "border_router/dhcp6_pd_client.hpp", "border_router/infra_if.cpp", "border_router/infra_if.hpp", "border_router/routing_manager.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c01731ee8..4ce375c6e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -94,6 +94,7 @@ set(COMMON_SOURCES backbone_router/bbr_manager.cpp backbone_router/multicast_listeners_table.cpp backbone_router/ndproxy_table.cpp + border_router/dhcp6_pd_client.cpp border_router/infra_if.cpp border_router/routing_manager.cpp coap/coap.cpp diff --git a/src/core/border_router/dhcp6_pd_client.cpp b/src/core/border_router/dhcp6_pd_client.cpp new file mode 100644 index 000000000..1c8f22d11 --- /dev/null +++ b/src/core/border_router/dhcp6_pd_client.cpp @@ -0,0 +1,1359 @@ +/* + * Copyright (c) 2025, 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. + */ + +/** + * @file + * This file implements DHCPv6 Prefix Delegation (PD) Client feature. + */ + +#include "dhcp6_pd_client.hpp" + +#if OT_CONFIG_DHCP6_PD_CLIENT_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace BorderRouter { + +using namespace ot::Dhcp6; + +RegisterLogModule("Dhcp6PdClient"); + +Dhcp6PdClient::Dhcp6PdClient(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStateStopped) + , mPdPrefixCommited(false) + , mMaxSolicitTimeout(kMaxSolicitTimeout) + , mTimer(aInstance) +{ + mServerAddress.Clear(); +} + +void Dhcp6PdClient::Start(void) +{ + LogInfo("Starting"); + + switch (mState) + { + case kStateStopped: + Get().SetDhcp6ListeningEnabled(true); + OT_FALL_THROUGH; + + case kStateReleasing: + EnterState(kStateToSolicit); + break; + + case kStateToSolicit: + case kStateSoliciting: + case kStateRequesting: + case kStateToRenew: + case kStateRenewing: + case kStateRebinding: + ExitNow(); + } + +exit: + return; +} + +void Dhcp6PdClient::Stop(void) +{ + LogInfo("Stopping"); + + switch (mState) + { + case kStateStopped: + case kStateReleasing: + break; + case kStateToSolicit: + case kStateSoliciting: + case kStateRequesting: + EnterState(kStateStopped); + break; + case kStateToRenew: + case kStateRenewing: + case kStateRebinding: + EnterState(kStateReleasing); + break; + } +} + +void Dhcp6PdClient::EnterState(State aState) +{ + LogInfo("State: %s -> %s", StateToString(mState), StateToString(aState)); + + mState = aState; + + switch (mState) + { + case kStateStopped: + ClearServerDuid(); + ClearPdPrefix(); + Get().SetDhcp6ListeningEnabled(false); + break; + + case kStateToSolicit: + ClearServerDuid(); + ClearPdPrefix(); + mTimer.Start(Random::NonCrypto::GetUint32InRange(0, kMaxDelayFirstSolicit)); + break; + + case kStateSoliciting: + ClearServerDuid(); + ClearPdPrefix(); + // Per RFC 8415 section 18.2.1, the first timeout for Solicit message + // must be selected to be strictly greater than the initial timeout + // value, therefore `kPositiveJitter` is used. + mRetxTracker.Start(kInitialSolicitTimeout, mMaxSolicitTimeout, kPositiveJitter); + break; + + case kStateRequesting: + mRetxTracker.Start(kInitialRequestTimeout, kMaxRequestTimeout, kFullJitter); + mRetxTracker.SetMaxCount(kMaxRequestRetxCount); + break; + + case kStateToRenew: + mTimer.FireAt(mPdPrefix.DetermineT1Time()); + break; + + case kStateRenewing: + mRetxTracker.Start(kInitialRenewTimeout, kMaxRenewTimeout, kFullJitter); + mRetxTracker.SetRetxEndTime(mPdPrefix.DetermineT2Time()); + break; + + case kStateRebinding: + ClearServerDuid(); + mRetxTracker.Start(kInitialRebindTimeout, kMaxRebindTimeout, kFullJitter); + mRetxTracker.SetRetxEndTime(mPdPrefix.DeterminePreferredTime()); + break; + + case kStateReleasing: + mRetxTracker.Start(kInitialReleaseTimeout, kInfiniteTimeout, kFullJitter); + mRetxTracker.SetMaxCount(kMaxReleaseRetxCount); + break; + } + + SendMessage(); +} + +void Dhcp6PdClient::HandleTimer(void) +{ + switch (mState) + { + case kStateStopped: + break; + + case kStateToSolicit: + EnterState(kStateSoliciting); + break; + + case kStateToRenew: + EnterState(kStateRenewing); + break; + + case kStateSoliciting: + if (mPdPrefix.IsValid()) + { + EnterState(kStateRequesting); + break; + } + + OT_FALL_THROUGH; + + case kStateRequesting: + case kStateRenewing: + case kStateRebinding: + case kStateReleasing: + SendMessage(); + break; + } +} + +void Dhcp6PdClient::SendMessage(void) +{ + static const uint16_t kRequestedOptions[] = {BigEndian::HostSwap16(Option::kSolMaxRt)}; + + OwnedPtr message; + Header header; + MsgType msgType; + Ip6::Address dstAddr; + + switch (mState) + { + case kStateSoliciting: + msgType = kMsgTypeSolicit; + break; + case kStateRequesting: + msgType = kMsgTypeRequest; + break; + case kStateRenewing: + msgType = kMsgTypeRenew; + break; + case kStateRebinding: + msgType = kMsgTypeRebind; + break; + case kStateReleasing: + msgType = kMsgTypeRelease; + break; + case kStateStopped: + case kStateToSolicit: + case kStateToRenew: + ExitNow(); + } + + if (!mRetxTracker.ShouldRetx()) + { + // Message exchanges can optionally define limits: A maximum + // retry count (e.g., for `Request` messages) and/or a + // maximum retransmission duration which defines an end time + // for retries (e.g., for `Renew` or `Rebind` messages). We skip + // transmission and update the state once retransmissions are + // exhausted. + + UpdateStateAfterRetxExhausted(); + ExitNow(); + } + + // In the unlikely event that message preparation fails (e.g., due to + // out-of-buffer condition), it will be retried after a short + // delay of `kRetxDelayOnFailedTx`. + + mTimer.Start(kRetxDelayOnFailedTx); + + message.Reset(Get().Allocate(Message::kTypeOther)); + VerifyOrExit(message != nullptr); + + header.SetMsgType(msgType); + header.SetTransactionId(mRetxTracker.GetTransactionId()); + SuccessOrExit(message->Append(header)); + + SuccessOrExit(ClientIdOption::AppendWithEui64Duid(*message, Get().GetExtAddress())); + + if (!mServerDuid.IsEmpty()) + { + SuccessOrExit(ServerIdOption::AppendWithDuid(*message, mServerDuid.GetArrayBuffer(), mServerDuid.GetLength())); + } + + SuccessOrExit(ElapsedTimeOption::AppendTo(*message, mRetxTracker.DetermineElapsedTime())); + + SuccessOrExit( + Option::AppendOption(*message, Option::kOptionRequest, &kRequestedOptions, sizeof(kRequestedOptions))); + + SuccessOrExit(AppendIaPdOption(*message)); + + LogInfo("Sending %s %s%s", MsgTypeToString(msgType), + mServerAddress.IsUnspecified() ? "(multicast)" : "(unicast) to:", + mServerAddress.IsUnspecified() ? "" : mServerAddress.ToString().AsCString()); + + if (!mServerAddress.IsUnspecified()) + { + dstAddr = mServerAddress; + } + else + { + GetAllRelayAgentsAndServersMulticastAddress(dstAddr); + } + + Get().SendDhcp6(*message.Release(), dstAddr); + + mRetxTracker.ScheduleTimeoutTimer(mTimer); + mRetxTracker.UpdateTimeoutAndCountAfterTx(); + +exit: + return; +} + +void Dhcp6PdClient::GetAllRelayAgentsAndServersMulticastAddress(Ip6::Address &aAddress) const +{ + // `All_DHCP_Relay_Agents_and_Servers` address (`ff02::1:2`). + + aAddress.Clear(); + aAddress.mFields.m16[0] = BigEndian::HostSwap16(0xff02); + aAddress.mFields.m16[6] = BigEndian::HostSwap16(1); + aAddress.mFields.m16[7] = BigEndian::HostSwap16(2); +} + +void Dhcp6PdClient::UpdateStateAfterRetxExhausted(void) +{ + switch (mState) + { + case kStateStopped: + case kStateToSolicit: + case kStateSoliciting: + case kStateToRenew: + break; + + case kStateRequesting: + EnterState(kStateSoliciting); + break; + case kStateRenewing: + EnterState(kStateRebinding); + break; + case kStateRebinding: + EnterState(kStateSoliciting); + break; + case kStateReleasing: + EnterState(kStateStopped); + break; + } +} + +Error Dhcp6PdClient::AppendIaPdOption(Message &aMessage) +{ + Error error; + IaPdOption iaPdOption; + + uint16_t optionOffset; + Ip6::Prefix prefix; + + // RFC 8415 - Section 21.21: In message from client, the T1 and T2 + // field SHOULD be set to 0. Server MUST ignore any values in these + // fields from a client. + + iaPdOption.Init(); + iaPdOption.SetIaid(kIaid); + iaPdOption.SetT1(0); + iaPdOption.SetT2(0); + + optionOffset = aMessage.GetLength(); + SuccessOrExit(error = aMessage.Append(iaPdOption)); + + if (mPdPrefix.IsValid()) + { + SuccessOrExit(error = AppendIaPrefixOption(aMessage, mPdPrefix.mPrefix)); + } + else + { + // RFC 8415 Section 21.22: A client MAY send a non-zero value + // in "prefix length" with unspecified (`::`) for the prefix to + // indicates its preference for the size of the prefix to be + // delegated. + + prefix.Clear(); + prefix.mLength = kDesiredPrefixLength; + + SuccessOrExit(error = AppendIaPrefixOption(aMessage, prefix)); + } + + Option::UpdateOptionLengthInMessage(aMessage, optionOffset); + +exit: + return error; +} + +Error Dhcp6PdClient::AppendIaPrefixOption(Message &aMessage, const Ip6::Prefix &aPrefix) +{ + IaPrefixOption iaPrefixOption; + + // RFC 8415 Section 21.22: In a message sent by client, the + // preferred and valid lifetimes fields SHOULD be set to 0. Server + // MUST ignore any received values in these fields. + + iaPrefixOption.Init(); + iaPrefixOption.SetPreferredLifetime(0); + iaPrefixOption.SetValidLifetime(0); + iaPrefixOption.SetPrefix(aPrefix); + + return aMessage.Append(iaPrefixOption); +} + +void Dhcp6PdClient::HandleReceived(Message &aMessage) +{ + Header header; + OffsetRange serverDuidOffsetRange; + + switch (mState) + { + case kStateSoliciting: + case kStateRequesting: + case kStateRenewing: + case kStateRebinding: + case kStateReleasing: + break; + + case kStateStopped: + case kStateToSolicit: + case kStateToRenew: + ExitNow(); + } + + SuccessOrExit(ParseHeaderAndValidateMessage(aMessage, header)); + + switch (header.GetMsgType()) + { + case kMsgTypeAdvertise: + VerifyOrExit(mState == kStateSoliciting); + break; + + case kMsgTypeReply: + VerifyOrExit(mState != kStateSoliciting); + break; + + default: + ExitNow(); + } + + // Per RFC 8415, Section 16, a client MUST discard any received + // Advertise/Reply messages that meet any of the following + // conditions: + // - The "Transaction ID" does not match the value used in the + // Solicit/Request message. + // - The message does not include a Client ID Option, or its content + // does not match the client's DUID. + // - The message does not include a Server ID option. + + VerifyOrExit(header.GetTransactionId() == mRetxTracker.GetTransactionId()); + SuccessOrExit(ClientIdOption::MatchesEui64Duid(aMessage, Get().GetExtAddress())); + SuccessOrExit(ServerIdOption::ReadDuid(aMessage, serverDuidOffsetRange)); + + // If we have selected a server, ensure the received server DUID + // matches the one saved in `mServerDuid`. However, during the + // initial solicitation attempt, we wait until the first timeout + // expires to collect Solicits from all servers. During this + // period, the favored server/prefix is tracked, so `mServerDuid` + // may be set, but we still process Solicits from other servers. + + if (!mServerDuid.IsEmpty() && (mState != kStateSoliciting)) + { + VerifyOrExit(serverDuidOffsetRange.GetLength() == mServerDuid.GetLength()); + VerifyOrExit(aMessage.CompareBytes(serverDuidOffsetRange, mServerDuid.GetArrayBuffer())); + } + + // The client MUST process any SOL_MAX_RT option in an Advertise + // or Reply message, even if the message contains a Status Code + // option indicating a failure and will be discarded by the client + + ProcessSolMaxRtOption(aMessage); + + switch (header.GetMsgType()) + { + case kMsgTypeAdvertise: + HandleAdvertise(aMessage); + break; + + case kMsgTypeReply: + HandleReply(aMessage); + break; + } + +exit: + aMessage.Free(); +} + +Error Dhcp6PdClient::ParseHeaderAndValidateMessage(Message &aMessage, Header &aHeader) +{ + Error error; + OffsetRange offsetRange; + Option option; + + offsetRange.InitFromMessageOffsetToEnd(aMessage); + + SuccessOrExit(error = aMessage.Read(offsetRange, aHeader)); + offsetRange.AdvanceOffset(sizeof(Header)); + + aMessage.SetOffset(offsetRange.GetOffset()); + + // Validate all top-level options. + + while (!offsetRange.IsEmpty()) + { + SuccessOrExit(error = aMessage.Read(offsetRange, option)); + VerifyOrExit(offsetRange.Contains(option.GetSize()), error = kErrorParse); + offsetRange.AdvanceOffset(option.GetSize()); + } + +exit: + return error; +} + +void Dhcp6PdClient::HandleAdvertise(const Message &aMessage) +{ + PdPrefixArray pdPrefixes; + PdPrefix *favoredPdPrefix; + uint8_t preference; + StatusCodeOption::Status status; + + VerifyOrExit(StatusCodeOption::ReadStatusFrom(aMessage) == StatusCodeOption::kSuccess); + + LogInfo("Received %s", MsgTypeToString(kMsgTypeAdvertise)); + + SuccessOrExit(ProcessIaPd(aMessage, pdPrefixes, status)); + + VerifyOrExit(status != StatusCodeOption::kNoPrefixAvail); + + favoredPdPrefix = SelectFavoredPrefix(pdPrefixes); + VerifyOrExit(favoredPdPrefix != nullptr); + + ProcessPreferenceOption(aMessage, preference); + + // Per RFC 8415 Section 18.2.1, for the first Solicit message, a + // client MUST process all valid Advertise messages for the full + // timeout duration, unless it receives a valid Advertise message + // with a preference value of 255. For subsequent Solicit + // retransmissions, the client terminates the retransmission + // process upon receiving any valid Advertise message and acts on + // it immediately, without waiting for additional Advertise + // messages. + // + // This is implemented by tracking the overall favored prefix and + // its corresponding server DUID across all received Advertise + // messages. If this is the first Solicit and a favored prefix from + // a valid server DUID is already being tracked, we ensure the + // newly received favored prefix is indeed more preferred before + // updating the stored information. + + if (mRetxTracker.IsFirstAttempt() && !mServerDuid.IsEmpty()) + { + VerifyOrExit(favoredPdPrefix->mAdjustedPrefix < mPdPrefix.mAdjustedPrefix); + } + + mPdPrefix = *favoredPdPrefix; + + SaveServerDuidAndAddress(aMessage); + + if (!mRetxTracker.IsFirstAttempt() || (preference == 255)) + { + EnterState(kStateRequesting); + } + +exit: + return; +} + +void Dhcp6PdClient::HandleReply(const Message &aMessage) +{ + PdPrefixArray pdPrefixes; + PdPrefix *favoredPdPrefix; + PdPrefix *matchedPdPrefix; + StatusCodeOption::Status status; + + status = StatusCodeOption::ReadStatusFrom(aMessage); + + LogInfo("Received %s, status:%u", MsgTypeToString(kMsgTypeReply), status); + + if (status == StatusCodeOption::kUnspecFail) + { + ExitNow(); + } + + if (status == StatusCodeOption::kUseMulticast) + { + // Per RFC 8514 Section 18.2.10, if the client receives a Reply + // message with a status code of UseMulticast, the client + // records the receipt of the message and sends subsequent + // messages to the server using multicast. The client re-sends the + // original message using multicast. + + VerifyOrExit(!mServerAddress.IsUnspecified()); + mServerAddress.Clear(); + SendMessage(); + ExitNow(); + } + + if (mState == kStateReleasing) + { + // Per RFC 8415 Section 18.2.10.2: When the client receives a + // valid Reply message in response to a Release message, the + // client considers the Release event completed, regardless of + // the Status Code option returned by the server. + + mState = kStateStopped; + ExitNow(); + } + + SuccessOrExit(ProcessIaPd(aMessage, pdPrefixes, status)); + + if (mState == kStateRequesting) + { + favoredPdPrefix = SelectFavoredPrefix(pdPrefixes); + + if ((status == StatusCodeOption::kNoPrefixAvail) || (favoredPdPrefix == nullptr)) + { + EnterState(kStateToSolicit); + ExitNow(); + } + + SaveServerDuidAndAddress(aMessage); + CommitPdPrefix(*favoredPdPrefix); + + ExitNow(); + } + + if ((mState == kStateRenewing) || (mState == kStateRebinding)) + { + VerifyOrExit(status != StatusCodeOption::kNoPrefixAvail); + + matchedPdPrefix = pdPrefixes.FindMatching(mPdPrefix); + + if (matchedPdPrefix != nullptr) + { + SaveServerDuidAndAddress(aMessage); + CommitPdPrefix(*matchedPdPrefix); + + if (mPdPrefix.mPreferredLifetime >= kMinPreferredLifetime) + { + ExitNow(); + } + } + + // The previously delegated prefix does not appear in IA or + // it is included with a unacceptably short lifetime. + // Check if server provided any other prefixes which we + // can use instead. + + favoredPdPrefix = SelectFavoredPrefix(pdPrefixes); + + if (favoredPdPrefix != nullptr) + { + SaveServerDuidAndAddress(aMessage); + CommitPdPrefix(*favoredPdPrefix); + ExitNow(); + } + + if ((status == StatusCodeOption::kNoBinding) && (mState == kStateRenewing)) + { + EnterState(kStateRebinding); + } + + ExitNow(); + } + +exit: + return; +} + +Dhcp6PdClient::PdPrefix *Dhcp6PdClient::SelectFavoredPrefix(PdPrefixArray &aPdPrefixes) +{ + // Select the favored prefix from the list of delegated prefixes. + // While we request a single prefix in an `IaPdOption` and servers + // typically provide one, if multiple prefixes are present, we + // select the numerically smaller (favored) one. We also validate + // that the delegated prefix meets a minimum lifetime. + // + // We request a /64 prefix length, but the server might assign a + // shorter length. In such cases, we adjust it by adding zero bits + // to create a /64 prefix. + + PdPrefix *favoredPdPrefix = nullptr; + + for (PdPrefix &pdPrefix : aPdPrefixes) + { + if (pdPrefix.mPrefix.GetLength() > kDesiredPrefixLength) + { + continue; + } + + if (pdPrefix.mPreferredLifetime < kMinPreferredLifetime) + { + continue; + } + + // Determine the favored prefix + + if ((favoredPdPrefix == nullptr) || (pdPrefix.mAdjustedPrefix < favoredPdPrefix->mAdjustedPrefix)) + { + favoredPdPrefix = &pdPrefix; + } + } + + return favoredPdPrefix; +} + +void Dhcp6PdClient::SaveServerDuidAndAddress(const Message &aMessage) +{ + // Reads the server DUID and Server Unicast option from the given + // message and saves them. The message is assumed to have already + // been validated to contain a Server ID option. + + OffsetRange serverDuidOffsetRange; + Ip6::Address serverAddress; + + SuccessOrAssert(ServerIdOption::ReadDuid(aMessage, serverDuidOffsetRange)); + mServerDuid.SetLength(serverDuidOffsetRange.GetLength()); + aMessage.ReadBytes(serverDuidOffsetRange, mServerDuid.GetArrayBuffer()); + + ProcessServerUnicastOption(aMessage, serverAddress); + + if (!serverAddress.IsUnspecified()) + { + mServerAddress = serverAddress; + } +} + +void Dhcp6PdClient::ClearServerDuid(void) +{ + mServerDuid.Clear(); + mServerAddress.Clear(); +} + +void Dhcp6PdClient::ClearPdPrefix(void) +{ + if (mPdPrefixCommited) + { + mPdPrefix.mPreferredLifetime = 0; + mPdPrefix.mValidLifetime = 0; + mPdPrefix.mT1 = 0; + mPdPrefix.mT2 = 0; + ReportPdPrefixToRoutingManager(); + } + + mPdPrefix.Clear(); + mPdPrefixCommited = false; +} + +void Dhcp6PdClient::CommitPdPrefix(const PdPrefix &aPdPrefix) +{ + if (!mPdPrefix.Matches(aPdPrefix)) + { + // Clear to ensure we report the prefix's removal if it + // was previously committed. + ClearPdPrefix(); + } + + mPdPrefix = aPdPrefix; + mPdPrefix.AdjustLifetimesT1AndT2(); + mPdPrefixCommited = true; + + EnterState(kStateToRenew); + ReportPdPrefixToRoutingManager(); +} + +void Dhcp6PdClient::ReportPdPrefixToRoutingManager(void) +{ + RoutingManager::Dhcp6PdPrefix prefix; + + LogInfo("Delegated prefix:%s, adj:%s, T1:%lu, T2:%lu, prf:%lu, valid:%lu", mPdPrefix.mPrefix.ToString().AsCString(), + mPdPrefix.mAdjustedPrefix.ToString().AsCString(), ToUlong(mPdPrefix.mT1), ToUlong(mPdPrefix.mT2), + ToUlong(mPdPrefix.mPreferredLifetime), ToUlong(mPdPrefix.mValidLifetime)); + + switch (mState) + { + case kStateToSolicit: + case kStateSoliciting: + case kStateRequesting: + case kStateToRenew: + case kStateRenewing: + case kStateRebinding: + break; + + case kStateStopped: + case kStateReleasing: + ExitNow(); + } + + ClearAllBytes(prefix); + prefix.mPrefix = mPdPrefix.mPrefix; + prefix.mValidLifetime = mPdPrefix.mValidLifetime; + prefix.mPreferredLifetime = mPdPrefix.mPreferredLifetime; + + Get().ProcessDhcp6PdPrefix(prefix); + +exit: + return; +} + +void Dhcp6PdClient::ProcessSolMaxRtOption(const Message &aMessage) +{ + // RFC 8415 - Section 18.2.9: The client MUST process any SOL_MAX_RT + // option present in an Advertise message, even if the message + // contains a Status Code option indicating a failure, and the + // Advertise message will be discarded by the client. A client + // SHOULD only update its SOL_MAX_RT values if all received + // Advertise messages that contained the corresponding option + // specified the same value; otherwise, it should use the default + // value. + + OffsetRange offsetRange; + SolMaxRtOption solMaxRtOption; + uint32_t solMaxRt; + + SuccessOrExit(Option::FindOption(aMessage, Option::kSolMaxRt, offsetRange)); + SuccessOrExit(aMessage.Read(offsetRange, solMaxRtOption)); + + // A DHCP client MUST ignore any SOL_MAX_RT option values that are + // less than 60 `kMinSolMaxRt` or more than 86400 `kMaxSolMaxRt`. + + solMaxRt = solMaxRtOption.GetSolMaxRt(); + + VerifyOrExit(solMaxRt >= SolMaxRtOption::kMinSolMaxRt); + VerifyOrExit(solMaxRt <= SolMaxRtOption::kMaxSolMaxRt); + + solMaxRt *= Time::kOneSecondInMsec; + + // If we are using default, adopt the new received value. If we + // have previously updated the value (from another advertisement) + // and the value does not match the newly received one, revert + // back to the default value. + + if (mMaxSolicitTimeout == kMaxSolicitTimeout) + { + mMaxSolicitTimeout = solMaxRt; + } + else if (solMaxRt != mMaxSolicitTimeout) + { + mMaxSolicitTimeout = kMaxSolicitTimeout; + } + + if (mState == kStateSoliciting) + { + mRetxTracker.SetMaxTimeout(mMaxSolicitTimeout); + } + +exit: + return; +} + +Error Dhcp6PdClient::ProcessIaPd(const Message &aMessage, + PdPrefixArray &aPdPrefixes, + StatusCodeOption::Status &aStatus) const +{ + Error error; + Option::Iterator iterator; + + aStatus = StatusCodeOption::kSuccess; + + // Iterate over all `IaPdOption` to find one matching `kIaid`. + + aPdPrefixes.Free(); + + for (iterator.Init(aMessage, Option::kIaPd); !iterator.IsDone(); iterator.Advance()) + { + IaPdOption iaPdOption; + OffsetRange offsetRange = iterator.GetOptionOffsetRange(); + + if ((aMessage.Read(offsetRange, iaPdOption) != kErrorNone) || ShouldSkipPdOption(iaPdOption)) + { + continue; + } + + offsetRange.AdvanceOffset(sizeof(IaPdOption)); + error = ProcessIaPdPrefixes(aMessage, iaPdOption, offsetRange, aPdPrefixes, aStatus); + ExitNow(); + } + + error = iterator.GetError(); + +exit: + return error; +} + +bool Dhcp6PdClient::ShouldSkipPdOption(const IaPdOption &aIaPdOption) const +{ + bool shouldSkip = true; + + VerifyOrExit(aIaPdOption.GetIaid() == kIaid); + + // RFC 8415 Section 21.21: If T1 is greater than T2 and both T1 and + // T2 are non-zero, we discard the `IaPdOption` and processes the + // remainder of the message as though this option had not been + // included. + + if (aIaPdOption.GetT1() != 0 && aIaPdOption.GetT2() != 0) + { + VerifyOrExit(aIaPdOption.GetT1() <= aIaPdOption.GetT2()); + } + + shouldSkip = false; + +exit: + return shouldSkip; +} + +Error Dhcp6PdClient::ProcessIaPdPrefixes(const Message &aMessage, + const IaPdOption &aIaPdOption, + const OffsetRange &aIaPdOptionsOffseRange, + PdPrefixArray &aPdPrefixes, + StatusCodeOption::Status &aStatus) const +{ + Error error; + Option::Iterator iterator; + + aStatus = StatusCodeOption::ReadStatusFrom(aMessage, aIaPdOptionsOffseRange); + + LogInfo("Processing IA prefix options, status:%u", aStatus); + + for (iterator.Init(aMessage, aIaPdOptionsOffseRange, Option::kIaPrefix); !iterator.IsDone(); iterator.Advance()) + { + IaPrefixOption prefixOption; + Ip6::Prefix prefix; + PdPrefix *newEntry; + bool shouldSkip; + + if (aMessage.Read(iterator.GetOptionOffsetRange(), prefixOption) != kErrorNone) + { + continue; + } + + shouldSkip = ShouldSkipPrefixOption(prefixOption); + prefixOption.GetPrefix(prefix); + + LogInfo(" Prefix:%s, T1:%lu, T2:%lu, prf:%lu, valid:%lu, skip:%s", prefix.ToString().AsCString(), + ToUlong(aIaPdOption.GetT1()), ToUlong(aIaPdOption.GetT2()), + ToUlong(prefixOption.GetPreferredLifetime()), ToUlong(prefixOption.GetValidLifetime()), + ToYesNo(shouldSkip)); + + if (shouldSkip) + { + continue; + } + + newEntry = aPdPrefixes.PushBack(); + VerifyOrExit(newEntry != nullptr, error = kErrorNoBufs); + + newEntry->mPrefix = prefix; + newEntry->mT1 = aIaPdOption.GetT1(); + newEntry->mT2 = aIaPdOption.GetT2(); + newEntry->mPreferredLifetime = prefixOption.GetPreferredLifetime(); + newEntry->mValidLifetime = prefixOption.GetValidLifetime(); + newEntry->mUpdateTime = TimerMilli::GetNow(); + + // Adjust a shorter prefix to the desired /64 length. + + if (prefix.GetLength() <= kDesiredPrefixLength) + { + newEntry->mAdjustedPrefix = prefix; + newEntry->mAdjustedPrefix.Tidy(); + newEntry->mAdjustedPrefix.SetLength(kDesiredPrefixLength); + } + else + { + newEntry->mAdjustedPrefix.Clear(); + } + } + + error = iterator.GetError(); + +exit: + return error; +} + +bool Dhcp6PdClient::ShouldSkipPrefixOption(const IaPrefixOption &aPrefixOption) const +{ + bool shouldSkip = true; + + VerifyOrExit(aPrefixOption.GetPrefixLength() <= Ip6::Prefix::kMaxLength); + + // The client MUST discard any prefixes for which the preferred lifetime + // is greater than the valid lifetime. + + VerifyOrExit(aPrefixOption.GetPreferredLifetime() <= aPrefixOption.GetValidLifetime()); + + shouldSkip = false; + +exit: + return shouldSkip; +} + +void Dhcp6PdClient::ProcessServerUnicastOption(const Message &aMessage, Ip6::Address &aServerAddress) const +{ + // Searches the message for a `ServerUnicastOption`. If found, the + // server address is retrieved from it. Otherwise, `aServerAddress` + // is set to `::` (unspecified address). + + OffsetRange offsetRange; + ServerUnicastOption serverUnicastOption; + + aServerAddress.Clear(); + + SuccessOrExit(Option::FindOption(aMessage, Option::kServerUnicast, offsetRange)); + SuccessOrExit(aMessage.Read(offsetRange, serverUnicastOption)); + + aServerAddress = serverUnicastOption.GetServerAddress(); + + LogInfo("Processed Sever Unicast Option, serverAddr:%s", aServerAddress.ToString().AsCString()); + +exit: + return; +} + +void Dhcp6PdClient::ProcessPreferenceOption(const Message &aMessage, uint8_t &aPreference) const +{ + // Searches for `PreferenceOption` in the message. If it is not + // found, assumes default preference value of zero. + + OffsetRange offsetRange; + PreferenceOption preferenceOption; + + aPreference = kDefaultPreference; + + SuccessOrExit(Option::FindOption(aMessage, Option::kPreference, offsetRange)); + SuccessOrExit(aMessage.Read(offsetRange, preferenceOption)); + + aPreference = preferenceOption.GetPreference(); + + LogInfo("Processed Preference Option, prf:%u", aPreference); + +exit: + return; +} + +#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + +const char *Dhcp6PdClient::StateToString(State aState) +{ + static const char *const kStateStrings[] = { + "Stopped", // (0) kStateStopped + "ToSolicit", // (1) kStateToSolicit + "Soliciting", // (2) kStateSoliciting + "Requesting", // (3) kStateRequesting + "ToRenew", // (4) kStateToRenew + "Renewing", // (5) kStateRenewing + "Rebinding", // (6) kStateRebinding + "Releasing", // (7) kStateReleasing + }; + + struct EnumCheck + { + InitEnumValidatorCounter(); + ValidateNextEnum(kStateStopped); + ValidateNextEnum(kStateToSolicit); + ValidateNextEnum(kStateSoliciting); + ValidateNextEnum(kStateRequesting); + ValidateNextEnum(kStateToRenew); + ValidateNextEnum(kStateRenewing); + ValidateNextEnum(kStateRebinding); + ValidateNextEnum(kStateReleasing); + }; + + return kStateStrings[aState]; +} + +const char *Dhcp6PdClient::MsgTypeToString(MsgType aMsgType) +{ + static const char *const kMsgTypeStrings[]{ + "Solicit", // (1) kMsgTypeSolicit + "Advertise", // (2) kMsgTypeAdvertise + "Request", // (3) kMsgTypeRequest + "Confirm", // (4) kMsgTypeConfirm + "Renew", // (5) kMsgTypeRenew + "Rebind", // (6) kMsgTypeRebind + "Reply", // (7) kMsgTypeReply + "Release", // (8) kMsgTypeRelease + "Decline", // (9) kMsgTypeDecline + "Reconfigure", // (10) kMsgTypeReconfigure + "InformationRequest", // (11) kMsgTypeInformationRequest + }; + + struct EnumCheck + { + InitEnumValidatorCounter(); + SkipNextEnum(); + ValidateNextEnum(kMsgTypeSolicit); + ValidateNextEnum(kMsgTypeAdvertise); + ValidateNextEnum(kMsgTypeRequest); + ValidateNextEnum(kMsgTypeConfirm); + ValidateNextEnum(kMsgTypeRenew); + ValidateNextEnum(kMsgTypeRebind); + ValidateNextEnum(kMsgTypeReply); + ValidateNextEnum(kMsgTypeRelease); + ValidateNextEnum(kMsgTypeDecline); + ValidateNextEnum(kMsgTypeReconfigure); + ValidateNextEnum(kMsgTypeInformationRequest); + }; + + const char *string = "UnknownMsg"; + + VerifyOrExit(aMsgType != 0); + VerifyOrExit(aMsgType <= kMsgTypeInformationRequest); + string = kMsgTypeStrings[aMsgType - kMsgTypeSolicit]; + +exit: + return string; +} + +#endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + +//--------------------------------------------------------------------------------------------------------------------- +// PdPrefix + +TimeMilli Dhcp6PdClient::PdPrefix::DetermineT1Time(void) const { return mUpdateTime + TimeMilli::SecToMsec(mT1); } + +TimeMilli Dhcp6PdClient::PdPrefix::DetermineT2Time(void) const { return mUpdateTime + TimeMilli::SecToMsec(mT2); } + +TimeMilli Dhcp6PdClient::PdPrefix::DeterminePreferredTime(void) const +{ + return mUpdateTime + TimeMilli::SecToMsec(mPreferredLifetime); +} + +void Dhcp6PdClient::PdPrefix::AdjustLifetimesT1AndT2(void) +{ + // We limit the preferred lifetime to `kMaxPreferredLifetime` + // (4 hours). This ensures renewals occur within a reasonable + // timeframe, preventing a delegated prefix from being used for an + // excessively long duration. + // + // The `RoutingManager` publishes the delegated PD prefix as an OMR + // in Network Data for its preferred lifetime (which is ensured + // to be at least `kMinPreferredLifetime` (30 minutes)). There's no + // benefit to maintaining the delegated prefix for much longer. + // Therefore, the valid lifetime is adjusted to be at most + // `kMaxValidMarginAfterPreferredLifetime` (2 minutes) longer than + // the preferred lifetime. + + mPreferredLifetime = Min(mPreferredLifetime, kMaxPreferredLifetime); + mValidLifetime = Min(mValidLifetime, mPreferredLifetime + kMaxValidMarginAfterPreferredLifetime); + + // If T1 or T2 not specified, use 0.5 and 0.8 times of the + // preferred lifetime. + + if (mT1 == 0) + { + mT1 = mPreferredLifetime * kDefaultT1FactorNumerator / kDefaultT1FactorDenominator; + } + + if (mT2 == 0) + { + mT2 = mPreferredLifetime * kDefaultT2FactorNumerator / kDefaultT2FactorDenominator; + } + + if (mPreferredLifetime >= kMinPreferredLifetime) + { + // This block handles the common scenario where the prefix has a + // preferred lifetime of at least `kMinPreferredLifetime` + // (30 minutes). During prefix lease renewal or rebind, the + // server might choose not to extend the lease, returning a + // remaining preferred lifetime shorter than 30 minutes. This + // specific case is handled in the `else if` and `else` blocks + // below. + + // We ensure T1 (renewal time) is at least `kMinT1` (5 minutes) + // to prevent frequent renewals. T1 is clamped between `kMinT1` + // and `mPreferredLifetime - kMinT1MarginBeforePreferredLifetime` + // (15 minutes). This margin ensures sufficient time for lease + // renewal before expiration. Similarly, T2 (rebind time) is + // clamped between T1 and `mPreferredLifetime` minus a margin + // of `kMinT2MarginBeforePreferredLifetime`(6 minutes). + + // Since `mPreferredLifetime` is at least 30 minutes, we know + // that `mPreferredLifetime - 15 min` will be at least 15 + // minutes, guaranteeing it is always greater than `kMinT1` + // (5 minutes). Additionally, `mT1` will be at most + // `mPreferredLifetime - 15 min`, ensuring `mT1` is less than + // `mPreferredLifetime - 6 min`. In both `Clamp` function + // calls, the minimum value is thus guaranteed to be less than + // the maximum value. + + mT1 = Clamp(mT1, kMinT1, mPreferredLifetime - kMinT1MarginBeforePreferredLifetime); + mT2 = Clamp(mT2, mT1, mPreferredLifetime - kMinT2MarginBeforePreferredLifetime); + } + else if (mPreferredLifetime >= kMinT1) + { + // This block handles cases where the server cannot extend the + // lease during renewal or rebind. In such scenarios, the + // server may return the remaining preferred lifetime as T1 and + // T2. This signals to the client that the leases associated + // with the prefix will not be extended, eliminating the need + // for further renewal attempts and preventing unnecessary + // network traffic as the remaining lifetime approaches zero. + + mT1 = Clamp(mT1, kMinT1, mPreferredLifetime); + mT2 = Clamp(mT2, mT1, mPreferredLifetime); + } + else + { + // If the preferred lifetime is very short (less than kMinT1), + // set T1 and T2 directly to the remaining preferred + // lifetime. This indicates that the lease is expiring soon + // and no further renewal or rebind attempts are productive. + + mT1 = mPreferredLifetime; + mT2 = mPreferredLifetime; + } +} + +//--------------------------------------------------------------------------------------------------------------------- +// RetxTracker + +uint32_t Dhcp6PdClient::RetxTracker::AddJitter(uint32_t aValue, JitterMode aJitterMode) +{ + // Applies a random jitter to a given value based on the selected + // jitter mode. For `kPositiveJitter`, the returned value is in + // the range `[aValue, aValue * 1.1]`. For `kFullJitter`, the + // range is `[aValue * 0.9, aValue * 1.1]`. + // + // Follows the RFC 8415 Section 15 rules regarding retransmission + // timeout (RT) calculation, where the timeout includes a + // randomization factor (RAND), which is a random number chosen + // with a uniform distribution between -0.1 and +0.1, calculating + // `RT + RAND * RT`. + // + // For a Solicit message's initial timeout, the randomization is + // required to be positive, for which `kPositiveJitter` is used. + + uint32_t randomizedValue = aValue; + uint32_t jitter = aValue / kJitterDivisor; + + switch (aJitterMode) + { + case kPositiveJitter: + randomizedValue += Random::NonCrypto::GetUint32InRange(0, jitter); + break; + + case kFullJitter: + randomizedValue += Random::NonCrypto::GetUint32InRange(0, 2 * jitter) - jitter; + break; + } + + return randomizedValue; +} + +void Dhcp6PdClient::RetxTracker::Start(uint32_t aInitialTimeout, uint32_t aMaxTimeout, JitterMode aJitterMode) +{ + TxnId oldId = mTransactionId; + + do + { + mTransactionId.GenerateRandom(); + } while (mTransactionId == oldId); + + mStartTime = TimerMilli::GetNow(); + mTimeout = AddJitter(aInitialTimeout, aJitterMode); + mMaxTimeout = aMaxTimeout; + mCount = 0; + mMaxCount = kInfiniteRetxCount; + mHasEndTime = false; + mLongElapsedTime = false; +} + +void Dhcp6PdClient::RetxTracker::ScheduleTimeoutTimer(TimerMilli &aTimer) const +{ + TimeMilli now = TimerMilli::GetNow(); + + aTimer.FireAt(now + mTimeout); + + if (mHasEndTime) + { + aTimer.FireAtIfEarlier(mEndTime); + } + +#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + { + uint32_t delay = aTimer.GetFireTime() - now; + + LogInfo("Scheduled timer for %lu.%03u sec", ToUlong(delay / 1000), static_cast(delay % 1000)); + } +#endif +} + +void Dhcp6PdClient::RetxTracker::UpdateTimeoutAndCountAfterTx(void) +{ + // RFC 8415 Section 15 required retransmission timeout (RT) to be updated + // for each subsequent tx based on the previous value using `RT = + // 2*RTprev + RAND*RTprev` This is interpreted as `RT = RTPrev + + // (RTprev + RAND*RTprev)`: + + mTimeout = mTimeout + AddJitter(mTimeout, kFullJitter); + + if ((mMaxTimeout != kInfiniteTimeout) && (mTimeout > mMaxTimeout)) + { + mTimeout = AddJitter(mMaxTimeout, kFullJitter); + } + + if (mCount != NumericLimits::kMax) + { + mCount++; + } +} + +bool Dhcp6PdClient::RetxTracker::ShouldRetx(void) const +{ + // Determines whether we have reached max retx count or + // retx end time has passed. + + bool shouldRetx = false; + + if (mMaxCount != kInfiniteRetxCount) + { + VerifyOrExit(mCount <= mMaxCount); + } + + if (mHasEndTime) + { + VerifyOrExit(TimerMilli::GetNow() < mEndTime); + } + + shouldRetx = true; + +exit: + return shouldRetx; +} + +uint16_t Dhcp6PdClient::RetxTracker::DetermineElapsedTime(void) +{ + // Determine the elapsed time for `ElapsedTimeOption`. This is + // measured from the time at which the client sent the first + // message in the current message exchange. It is set to 0 in the + // first message in the message exchange. Elapsed time is + // expressed in the unit of hundredths of a second. Value + // `0xffff` is used to represent any elapsed-time values greater + // than the largest time value that can be represented as + // `uint16_t`. + + uint16_t elapsed = 0; + + if (mCount == 0) + { + elapsed = 0; + ExitNow(); + } + + // `mLongElapsedTime` tracks whether the elapsed time has previously + // reached the `0xffff` limit, which occurs after approximately 656 + // seconds. Once this flag is set, we'll consistently return + // `0xffff`, ensuring correct behavior for very long retx runs + // (e.g., during solicit). The `mStartTime` (a `uint32_t` + // `TimeMilli` value) tracks the transmission time of the first + // message in the exchange. While `TimeMilli::GetNow()` can roll + // over after/ more than 49 days, `mLongElapsedTime` is marked + // much earlier (around 656 seconds), so we stop using `mStartTime` + // once the limit is hit. + + if (mLongElapsedTime) + { + elapsed = NumericLimits::kMax; + ExitNow(); + } + + // Calculate the duration in msec, divide by 10 to convert to unit + // of hundredths of a second. Clamp to `uint16`. + + elapsed = ClampToUint16((TimerMilli::GetNow() - mStartTime) / 10); + + if (elapsed == NumericLimits::kMax) + { + mLongElapsedTime = true; + } + +exit: + return elapsed; +} + +} // namespace BorderRouter +} // namespace ot + +#endif // OT_CONFIG_DHCP6_PD_CLIENT_ENABLE diff --git a/src/core/border_router/dhcp6_pd_client.hpp b/src/core/border_router/dhcp6_pd_client.hpp new file mode 100644 index 000000000..520f9d4a6 --- /dev/null +++ b/src/core/border_router/dhcp6_pd_client.hpp @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2025, 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. + */ + +/** + * @file + * This file includes definitions for DHCPv6 Prefix Delegation (PD) Client. + */ + +#ifndef DHCP6_PD_CLIENT_HPP_ +#define DHCP6_PD_CLIENT_HPP_ + +#include "openthread-core-config.h" + +#ifdef OT_CONFIG_DHCP6_PD_CLIENT_ENABLE +#error "OT_CONFIG_DHCP6_PD_CLIENT_ENABLE MUST NOT be defined directly. It is derived from other configs" +#endif + +#define OT_CONFIG_DHCP6_PD_CLIENT_ENABLE \ + (OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && \ + OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE) + +#if OT_CONFIG_DHCP6_PD_CLIENT_ENABLE + +#include "border_router/infra_if.hpp" +#include "common/array.hpp" +#include "common/clearable.hpp" +#include "common/error.hpp" +#include "common/heap_array.hpp" +#include "common/locator.hpp" +#include "common/owned_ptr.hpp" +#include "common/timer.hpp" +#include "net/dhcp6_types.hpp" +#include "net/ip6.hpp" + +namespace ot { +namespace BorderRouter { + +/** + * Represents a DHCPv6 Prefix Delegation (PD) Client. + */ +class Dhcp6PdClient : public InstanceLocator +{ + friend class InfraIf; + +public: + /** + * Represents a delegated prefix. + */ + struct DelegatedPrefix + { + Ip6::Prefix mPrefix; ///< The delegated prefix. + Ip6::Prefix mAdjustedPrefix; ///< The delegated prefix, adjusted to prefix length of 64. + uint32_t mT1; ///< T1 duration in sec (time to renew) + uint32_t mT2; ///< T1 duration in sec (time to rebind). + uint32_t mPreferredLifetime; ///< Preferred lifetime in sec. + uint32_t mValidLifetime; ///< Valid lifetime in sec. + TimeMilli mUpdateTime; ///< The last update time of this prefix. + }; + + /** + * Initializes the `Dhcp6PdClient`. + * + * @param[in] aInstance A OpenThread instance. + */ + explicit Dhcp6PdClient(Instance &aInstance); + + /** + * Starts the client. + * + * Once started, the client locates DHCPv6 servers, selects one, and then requests the assignment of a delegated + * prefix from the server. The client manages the lease renewal (extending the lifetimes of a delegated prefix) + * and its removal. + * + * The favored delegated prefix is reported to `RoutingManager` directly using `ProcessDhcp6PdPrefix()`. Any + * changes to the prefix (e.g., lease renewal, removal, or replacement of the prefix) are also reported using the + * same method. + */ + void Start(void); + + /** + * Stops the client. + * + * The client will release any delegated prefixes. + */ + void Stop(void); + + /** + * Returns the delegated prefix. + * + * @returns A pointer to the delegated prefix, or `nullptr` if none. + */ + const DelegatedPrefix *GetDelegatedPrefix(void) const { return mPdPrefixCommited ? &mPdPrefix : nullptr; } + +private: + // All intervals are in milli-seconds (from RFC 8415 section 7.6) + static constexpr uint32_t kMaxDelayFirstSolicit = Time::kOneSecondInMsec; // SOL_MAX_DELAY + static constexpr uint32_t kInitialSolicitTimeout = Time::kOneSecondInMsec; // SOL_TIMEOUT + static constexpr uint32_t kMaxSolicitTimeout = 3600 * Time::kOneSecondInMsec; // SOL_MAX_RT + static constexpr uint32_t kInitialRequestTimeout = Time::kOneSecondInMsec; // REQ_TIMEOUT + static constexpr uint32_t kMaxRequestTimeout = 30 * Time::kOneSecondInMsec; // REQ_MAX_RT + static constexpr uint16_t kMaxRequestRetxCount = 10; // REQ_MAX_RC + static constexpr uint32_t kInitialRenewTimeout = 10 * Time::kOneSecondInMsec; // REN_TIMEOUT + static constexpr uint32_t kMaxRenewTimeout = 600 * Time::kOneSecondInMsec; // REN_MAX_RT + static constexpr uint32_t kInitialRebindTimeout = 10 * Time::kOneSecondInMsec; // REB_TIMEOUT + static constexpr uint32_t kMaxRebindTimeout = 600 * Time::kOneSecondInMsec; // REB_MAX_RT + static constexpr uint32_t kInitialReleaseTimeout = 1 * Time::kOneSecondInMsec; // REL_TIMEOUT + static constexpr uint16_t kMaxReleaseRetxCount = 4; // REL_MAX_RC + static constexpr uint32_t kInfiniteTimeout = 0; + static constexpr uint16_t kInfiniteRetxCount = 0; + + static constexpr uint32_t kRetxDelayOnFailedTx = 330; // in msec + + static constexpr uint32_t kIaid = 0; + static constexpr uint8_t kDesiredPrefixLength = 64; + static constexpr uint8_t kDefaultPreference = 0; + static constexpr uint32_t kJitterDivisor = 10; + + // The constants below are in seconds + static constexpr uint32_t kMinPreferredLifetime = 30 * Time::kOneMinuteInSec; + static constexpr uint32_t kMaxPreferredLifetime = 4 * Time::kOneHourInSec; + static constexpr uint32_t kMaxValidMarginAfterPreferredLifetime = 2 * Time::kOneMinuteInSec; + static constexpr uint32_t kMinT1 = 5 * Time::kOneMinuteInSec; + static constexpr uint32_t kMinT1MarginBeforePreferredLifetime = 15 * Time::kOneMinuteInSec; + static constexpr uint32_t kMinT2MarginBeforePreferredLifetime = 6 * Time::kOneMinuteInSec; + + // Default T1 and T2 as 0.5 and 0.8 times the preferred lifetime + // if they are zero (represented as 5/10 and 8/10). + static constexpr uint32_t kDefaultT1FactorNumerator = 5; + static constexpr uint32_t kDefaultT1FactorDenominator = 10; + static constexpr uint32_t kDefaultT2FactorNumerator = 8; + static constexpr uint32_t kDefaultT2FactorDenominator = 10; + + enum State : uint8_t + { + kStateStopped, + kStateToSolicit, + kStateSoliciting, + kStateRequesting, + kStateToRenew, + kStateRenewing, + kStateRebinding, + kStateReleasing, + }; + + enum JitterMode : uint8_t + { + kPositiveJitter, + kFullJitter, + }; + + typedef Array ServerDuid; + typedef Dhcp6::TransactionId TxnId; + + struct PdPrefix : public DelegatedPrefix, public Clearable + { + PdPrefix(void) { Clear(); } + bool IsValid(void) const { return mPrefix.GetLength() != 0; } + TimeMilli DetermineT1Time(void) const; + TimeMilli DetermineT2Time(void) const; + TimeMilli DeterminePreferredTime(void) const; + void AdjustLifetimesT1AndT2(void); + bool Matches(const PdPrefix &aOther) const { return mPrefix == aOther.mPrefix; } + }; + + typedef Heap::Array PdPrefixArray; + + class RetxTracker + { + // Implements the retx behavior for DHCPv6 message exchanges + // as per RFC 8415 Section 15. + public: + void Start(uint32_t aInitialTimeout, uint32_t aMaxTimeout, JitterMode aJitterMode); + void SetMaxTimeout(uint32_t aMaxTimeout) { mMaxTimeout = aMaxTimeout; } + void SetMaxCount(uint16_t aMaxCount) { mMaxCount = aMaxCount; } + void SetRetxEndTime(TimeMilli aEndTime) { mHasEndTime = true, mEndTime = aEndTime; } + const TxnId &GetTransactionId(void) { return mTransactionId; } + bool IsFirstAttempt(void) const { return (mCount <= 1); } + void ScheduleTimeoutTimer(TimerMilli &aTimer) const; + void UpdateTimeoutAndCountAfterTx(void); + bool ShouldRetx(void) const; + uint16_t DetermineElapsedTime(void); + + private: + static constexpr uint32_t kJitterDivisor = 10; + + static uint32_t AddJitter(uint32_t aValue, JitterMode aJitterMode); + + TimeMilli mStartTime; + TimeMilli mEndTime; + uint32_t mTimeout; + uint32_t mMaxTimeout; + uint16_t mCount; + uint16_t mMaxCount; + TxnId mTransactionId; + bool mHasEndTime; + bool mLongElapsedTime; + }; + + void EnterState(State aState); + void HandleTimer(void); + void SendMessage(void); + void GetAllRelayAgentsAndServersMulticastAddress(Ip6::Address &aAddress) const; + void UpdateStateAfterRetxExhausted(void); + Error AppendIaPdOption(Message &aMessage); + Error AppendIaPrefixOption(Message &aMessage, const Ip6::Prefix &aPrefix); + void HandleReceived(Message &aMessage); + Error ParseHeaderAndValidateMessage(Message &aMessage, Dhcp6::Header &aHeader); + void HandleAdvertise(const Message &aMessage); + void HandleReply(const Message &aMessage); + PdPrefix *SelectFavoredPrefix(PdPrefixArray &aPdPrefixes); + void ProcessSolMaxRtOption(const Message &aMessaget); + Error ProcessIaPd(const Message &aMessage, + PdPrefixArray &aPdPrefixes, + Dhcp6::StatusCodeOption::Status &aStatus) const; + bool ShouldSkipPdOption(const Dhcp6::IaPdOption &aIsPdOption) const; + Error ProcessIaPdPrefixes(const Message &aMessage, + const Dhcp6::IaPdOption &aIaPdOption, + const OffsetRange &aIaPdOptionsOffseRange, + PdPrefixArray &aPdPrefixes, + Dhcp6::StatusCodeOption::Status &aStatus) const; + bool ShouldSkipPrefixOption(const Dhcp6::IaPrefixOption &aPrefixOption) const; + void ProcessServerUnicastOption(const Message &aMessage, Ip6::Address &aServerAddress) const; + void ProcessPreferenceOption(const Message &aMessage, uint8_t &aPreference) const; + void SaveServerDuidAndAddress(const Message &aMessage); + void ClearServerDuid(void); + void ClearPdPrefix(void); + void CommitPdPrefix(const PdPrefix &aPdPrefix); + void ReportPdPrefixToRoutingManager(void); + + static const char *StateToString(State aState); + static const char *MsgTypeToString(Dhcp6::MsgType aMsgType); + + using DelayTimer = TimerMilliIn; + + State mState; + bool mPdPrefixCommited; + RetxTracker mRetxTracker; + uint32_t mMaxSolicitTimeout; + PdPrefix mPdPrefix; + ServerDuid mServerDuid; + Ip6::Address mServerAddress; + DelayTimer mTimer; +}; + +} // namespace BorderRouter +} // namespace ot + +#endif // OT_CONFIG_DHCP6_PD_CLIENT_ENABLE + +#endif // DHCP6_PD_CLIENT_HPP_ diff --git a/src/core/border_router/infra_if.cpp b/src/core/border_router/infra_if.cpp index ce4492793..7ad4fb331 100644 --- a/src/core/border_router/infra_if.cpp +++ b/src/core/border_router/infra_if.cpp @@ -172,6 +172,36 @@ exit: return error; } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + +void InfraIf::SetDhcp6ListeningEnabled(bool aEnable) +{ + otPlatInfraIfDhcp6PdClientSetListeningEnabled(&GetInstance(), aEnable, mIfIndex); +} + +void InfraIf::SendDhcp6(Message &aMessage, Ip6::Address &aDestAddress) +{ + otPlatInfraIfDhcp6PdClientSend(&GetInstance(), &aMessage, &aDestAddress, mIfIndex); +} + +void InfraIf::HandleDhcp6Received(Message &aMessage, uint32_t aInfraIfIndex) +{ + Error error = kErrorNone; + + VerifyOrExit(mInitialized && mIsRunning, error = kErrorInvalidState); + VerifyOrExit(aInfraIfIndex == mIfIndex, error = kErrorDrop); + + Get().HandleReceived(aMessage); + +exit: + if (error != kErrorNone) + { + LogDebg("Dropped DHCPv6 message: %s", ErrorToString(error)); + } +} + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + InfraIf::InfoString InfraIf::ToString(void) const { InfoString string; @@ -206,6 +236,15 @@ extern "C" void otPlatInfraIfDiscoverNat64PrefixDone(otInstance *aInstanc AsCoreType(aInstance).Get().DiscoverNat64PrefixDone(aInfraIfIndex, AsCoreType(aIp6Prefix)); } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE +extern "C" void otPlatInfraIfDhcp6PdClientHandleReceived(otInstance *aInstance, + otMessage *aMessage, + uint32_t aInfraIfIndex) +{ + AsCoreType(aInstance).Get().HandleDhcp6Received(AsCoreType(aMessage), aInfraIfIndex); +} +#endif + } // namespace BorderRouter } // namespace ot diff --git a/src/core/border_router/infra_if.hpp b/src/core/border_router/infra_if.hpp index 9bae2f9ec..9405eb311 100644 --- a/src/core/border_router/infra_if.hpp +++ b/src/core/border_router/infra_if.hpp @@ -43,6 +43,7 @@ #include "common/data.hpp" #include "common/error.hpp" #include "common/locator.hpp" +#include "common/message.hpp" #include "common/string.hpp" #include "net/ip6.hpp" @@ -114,6 +115,18 @@ public: */ void SetIfIndex(uint32_t aIfIndex) { mIfIndex = aIfIndex; } + /** + * Handles infrastructure interface state changes. + * + * @param[in] aIfIndex The infrastructure interface index. + * @param[in] aIsRunning A boolean that indicates whether the infrastructure interface is running. + * + * @retval kErrorNone Successfully updated the infra interface status. + * @retval kErrorInvalidState The `InfraIf` is not initialized. + * @retval kErrorInvalidArgs The @p IfIndex does not match the interface index of `InfraIf`. + */ + Error HandleStateChanged(uint32_t aIfIndex, bool aIsRunning); + /** * Gets the infrastructure interface link-layer address. * @@ -176,17 +189,34 @@ public: */ void DiscoverNat64PrefixDone(uint32_t aIfIndex, const Ip6::Prefix &aPrefix); +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + /** - * Handles infrastructure interface state changes. + * Enables or disables listening for DHCPv6 Prefix Delegation (PD) messages on client (on DHCPv6 client port 546). * - * @param[in] aIfIndex The infrastructure interface index. - * @param[in] aIsRunning A boolean that indicates whether the infrastructure interface is running. - * - * @retval kErrorNone Successfully updated the infra interface status. - * @retval kErrorInvalidState The `InfraIf` is not initialized. - * @retval kErrorInvalidArgs The @p IfIndex does not match the interface index of `InfraIf`. + * @param[in] aEnable A boolean to enable (`true`) or disable (`false`) listening. */ - Error HandleStateChanged(uint32_t aIfIndex, bool aIsRunning); + void SetDhcp6ListeningEnabled(bool aEnable); + + /** + * Sends a DHCPv6 message to a unicast or multicast destination address. + * + * The message must be sent from the DHCPv6 client UDP port 546 to the server port 547. + * + * @param[in] aMessage The `Message` containing the DHCPv6 payload. Ownership is transferred. + * @param[in] aDestAddress The IPv6 destination address. + */ + void SendDhcp6(Message &aMessage, Ip6::Address &aDestAddress); + + /** + * Handle a received DHCPv6 message (on port 546). + * + * @param[in] aMessage The received `Message` containing DHCPv6 payload. Ownership is transferred. + * @param[in] aInfraIfIndex The index of the infrastructure interface from which the message is received. + */ + void HandleDhcp6Received(Message &aMessage, uint32_t aInfraIfIndex); + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE /** * Converts the `InfraIf` to a human-readable string. diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp index 64a4ae42b..e066fdd66 100644 --- a/src/core/border_router/routing_manager.cpp +++ b/src/core/border_router/routing_manager.cpp @@ -4407,9 +4407,22 @@ void RoutingManager::PdPrefixManager::SetState(State aState) LogInfo("PdPrefixManager: %s -> %s", StateToString(mState), StateToString(aState)); mState = aState; - if (mState != kDhcp6PdStateRunning) + switch (mState) { + case kDhcp6PdStateRunning: +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + Get().Start(); +#endif + break; + + case kDhcp6PdStateDisabled: + case kDhcp6PdStateStopped: + case kDhcp6PdStateIdle: WithdrawPrefix(); +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + Get().Stop(); +#endif + break; } mStateCallback.InvokeIfSet(MapEnum(mState)); @@ -4649,6 +4662,8 @@ const char *RoutingManager::PdPrefixManager::StateToString(State aState) return kStateStrings[aState]; } +#if !OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + extern "C" void otPlatBorderRoutingProcessIcmp6Ra(otInstance *aInstance, const uint8_t *aMessage, uint16_t aLength) { InfraIf::Icmp6Packet raMessage; @@ -4665,6 +4680,9 @@ extern "C" void otPlatBorderRoutingProcessDhcp6PdPrefix(otInstance AsCoreType(aInstance).Get().ProcessDhcp6PdPrefix(*aPrefixInfo); } + +#endif // !OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE } // namespace BorderRouter diff --git a/src/core/common/message.cpp b/src/core/common/message.cpp index 54a28d8c4..6a627257d 100644 --- a/src/core/common/message.cpp +++ b/src/core/common/message.cpp @@ -679,6 +679,11 @@ exit: return (bytesToCompare == 0); } +bool Message::CompareBytes(const OffsetRange &aOffsetRange, const void *aBuf, ByteMatcher aMatcher) const +{ + return CompareBytes(aOffsetRange.GetOffset(), aBuf, aOffsetRange.GetLength(), aMatcher); +} + bool Message::CompareBytes(uint16_t aOffset, const Message &aOtherMessage, uint16_t aOtherOffset, diff --git a/src/core/common/message.hpp b/src/core/common/message.hpp index 177fe2ee9..f8cde13a8 100644 --- a/src/core/common/message.hpp +++ b/src/core/common/message.hpp @@ -918,6 +918,22 @@ public: */ bool CompareBytes(uint16_t aOffset, const void *aBuf, uint16_t aLength, ByteMatcher aMatcher = nullptr) const; + /** + * Compares the bytes in the message from a given offset range with a given byte array. + * + * If there are fewer bytes available in the message than @p aOffsetRange, the comparison is treated as failure + * (returns FALSE). + * + * @param[in] aOffsetRange The offset range in the message to read from for the comparison. + * @param[in] aBuf A pointer to a data buffer to compare with the bytes from message. + * @param[in] aMatcher A `ByteMatcher` function pointer to match the bytes. If `nullptr` then bytes are directly + * compared. + * + * @returns TRUE if there are enough bytes available in @p aMessage and they match the bytes from @p aBuf, + * FALSE otherwise. + */ + bool CompareBytes(const OffsetRange &aOffsetRange, const void *aBuf, ByteMatcher aMatcher = nullptr) const; + /** * Compares the bytes in the message at a given offset with bytes read from another message. * diff --git a/src/core/config/border_routing.h b/src/core/config/border_routing.h index a8fb56dc4..70d68678d 100644 --- a/src/core/config/border_routing.h +++ b/src/core/config/border_routing.h @@ -187,15 +187,37 @@ /** * @def OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE * - * Specifies whether to support handling platform generated ND messages. + * Define to 1 to enable the Border Routing Manager's DHCPv6 Prefix Delegation feature. * - * The desired use case is the prefix will be allocated by other software on the interface, and they will advertise the - * assigned prefix to the thread interface via router advertisement messages. + * When enabled, the Routing Manager will track and use delegated DHCPv6 prefix(es) for use as an OMR + * (Off-Mesh-Routable) prefix within the Thread network. + * + * The configuration `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE` (which enables the OpenThread native + * DHCPv6 PD client) controls how the PD prefixes are acquired: + * + * - If the built-in OpenThread DHCPv6 PD client is disabled, the platform will be responsible for acquiring the PD + * prefix. The platform layer is expected to interact with DHCPv6 servers and provide the delegated PD prefix(es) + * through platform APIs such as `otPlatBorderRoutingProcessIcmp6Ra()` or `otPlatBorderRoutingProcessDhcp6PdPrefix()` + * (refer to `include/platform/border_routing.h` for a more detailed description of these APIs). + * + * - If the built-in OpenThread DHCPv6 PD client is enabled, the functionality is implemented and provided by the + * OpenThread core itself, which acts as a DHCPv6 client and interacts with DHCPv6 servers directly. */ #ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + * + * Define to 1 to enable the Border Router's built-in OpenThread DHCPv6 Prefix Delegation (PD) client feature. + * + * See `OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE` for how this is used. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE +#define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 0 +#endif + /** * @def OPENTHREAD_CONFIG_BORDER_ROUTING_TESTING_API_ENABLE * diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index 65384bba4..133a872da 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -263,6 +263,9 @@ Instance::Instance(void) #endif #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE , mRoutingManager(*this) +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + , mDhcp6PdClient(*this) +#endif #endif #if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE , mNat64Translator(*this) diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index 1976f9c1e..25b8126be 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -73,6 +73,7 @@ #include "backbone_router/bbr_leader.hpp" #include "backbone_router/bbr_local.hpp" #include "backbone_router/bbr_manager.hpp" +#include "border_router/dhcp6_pd_client.hpp" #include "border_router/routing_manager.hpp" #include "coap/coap_secure.hpp" #include "common/code_utils.hpp" @@ -702,6 +703,9 @@ private: #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE BorderRouter::RoutingManager mRoutingManager; +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + BorderRouter::Dhcp6PdClient mDhcp6PdClient; +#endif #endif #if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE @@ -1069,8 +1073,10 @@ template <> inline Utils::Otns &Instance::Get(void) { return mOtns; } #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE template <> inline BorderRouter::RoutingManager &Instance::Get(void) { return mRoutingManager; } - -template <> inline BorderRouter::InfraIf &Instance::Get(void) { return mRoutingManager.mInfraIf; } +template <> inline BorderRouter::InfraIf &Instance::Get(void) { return mRoutingManager.mInfraIf; } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE +template <> inline BorderRouter::Dhcp6PdClient &Instance::Get(void) { return mDhcp6PdClient; } +#endif #endif #if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE diff --git a/src/core/net/dhcp6_types.cpp b/src/core/net/dhcp6_types.cpp index 25ee93746..911db2964 100644 --- a/src/core/net/dhcp6_types.cpp +++ b/src/core/net/dhcp6_types.cpp @@ -33,8 +33,6 @@ #include "dhcp6_types.hpp" -#if OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE || OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE - namespace ot { namespace Dhcp6 { @@ -229,13 +227,31 @@ exit: return error; } +Error IdOption::Append(Option::Code aCode, Message &aMessage, const void *aDuid, uint16_t aDuidLength) +{ + return Option::AppendOption(aMessage, aCode, aDuid, aDuidLength); +} + Error IdOption::AppendEui64(Option::Code aCode, Message &aMessage, const Mac::ExtAddress &aExtAddress) { Eui64Duid eui64Duid; eui64Duid.Init(aExtAddress); - return Option::AppendOption(aMessage, aCode, &eui64Duid, sizeof(eui64Duid)); + return Append(aCode, aMessage, &eui64Duid, sizeof(eui64Duid)); +} + +//--------------------------------------------------------------------------------------------------------------------- +// ElapsedTimeOption + +Error ElapsedTimeOption::AppendTo(Message &aMessage, uint16_t aElapsedTime) +{ + ElapsedTimeOption elapsedTimeOption; + + elapsedTimeOption.Init(); + elapsedTimeOption.SetElapsedTime(aElapsedTime); + + return aMessage.Append(elapsedTimeOption); } //--------------------------------------------------------------------------------------------------------------------- @@ -281,7 +297,22 @@ Error RapidCommitOption::AppendTo(Message &aMessage) return Option::AppendOption(aMessage, Option::kRapidCommit, /* aData */ nullptr, 0); } +//--------------------------------------------------------------------------------------------------------------------- +// IaPrefixOption + +void IaPrefixOption::GetPrefix(Ip6::Prefix &aPrefix) const +{ + uint8_t length = Min(GetPrefixLength(), Ip6::Prefix::kMaxLength); + + aPrefix.Set(mPrefix.mFields.m8, length); + aPrefix.Tidy(); +} + +void IaPrefixOption::SetPrefix(const Ip6::Prefix &aPrefix) +{ + mPrefix = AsCoreType(&aPrefix.mPrefix); + mPrefixLength = aPrefix.mLength; +} + } // namespace Dhcp6 } // namespace ot - -#endif // #if OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE || OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE diff --git a/src/core/net/dhcp6_types.hpp b/src/core/net/dhcp6_types.hpp index 838ab015a..a583ff554 100644 --- a/src/core/net/dhcp6_types.hpp +++ b/src/core/net/dhcp6_types.hpp @@ -36,8 +36,6 @@ #include "openthread-core-config.h" -#if OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE || OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE - #include "common/clearable.hpp" #include "common/debug.hpp" #include "common/equatable.hpp" @@ -157,7 +155,7 @@ public: kIaNa = 3, ///< Identity Association for Non-temporary Addresses Option. kIaTa = 4, ///< Identity Association for Temporary Addresses Option. kIaAddress = 5, ///< Identity Association Address Option. - kRequestOption = 6, ///< Option Request Option. + kOptionRequest = 6, ///< Option Request Option. kPreference = 7, ///< Preference Option. kElapsedTime = 8, ///< Elapsed Time Option. kRelayMessage = 9, ///< Relay Message Option. @@ -176,6 +174,7 @@ public: kLeaseQuery = 44, ///< Lease Query Option. kClientData = 45, ///< Client Data Option. kClientLastTransactionTime = 46, ///< Client Last Transaction Time Option. + kSolMaxRt = 82, ///< SOL_MAX_RT Option (Max Solicit timeout value). }; /** @@ -439,6 +438,7 @@ protected: static Error Read(Option::Code aCode, const Message &aMessage, OffsetRange &aDuidOffsetRange); static Error ReadEui64(Option::Code aCode, const Message &aMessage, Mac::ExtAddress &aExtAddress); static Error MatchesEui64(Option::Code aCode, const Message &aMessage, const Mac::ExtAddress &aExtAddress); + static Error Append(Option::Code aCode, Message &aMessage, const void *aDuid, uint16_t aDuidLength); static Error AppendEui64(Option::Code aCode, Message &aMessage, const Mac::ExtAddress &aExtAddress); }; @@ -539,6 +539,21 @@ public: return IdOption::ReadEui64(Option::kServerId, aMessage, aExtAddress); } + /** + * Appends a Server Identifier option to a DHCPv6 message with a given raw DUID. + * + * @param[in,out] aMessage The message to which to append the Server ID option. + * @param[in] aDuid A pointer to a buffer containing the DUID bytes. + * @param[in] aDuidLength The DUID length in bytes. + * + * @retval kErrorNone Successfully appended the Server ID option. + * @retval kErrorNoBufs Insufficient available buffers to grow the message. + */ + static Error AppendWithDuid(Message &aMessage, const void *aDuid, uint16_t aDuidLength) + { + return IdOption::Append(Option::kServerId, aMessage, aDuid, aDuidLength); + } + /** * Appends a Server Identifier option to a DHCPv6 message. * @@ -692,6 +707,36 @@ private: uint32_t mValidLifetime; } OT_TOOL_PACKED_END; +/** + * Represents a Preference DHCPv6 Option. + */ +OT_TOOL_PACKED_BEGIN +class PreferenceOption : public Option +{ +public: + /** + * Initializes the DHCPv6 Option. + */ + void Init(void) { SetCode(kPreference), SetLength(sizeof(*this) - sizeof(Option)); } + + /** + * Returns the preference value. + * + * @returns The preference value. Higher value is preferred. + */ + uint16_t GetPreference(void) const { return mPreference; } + + /** + * Sets the preference. + * + * @param[in] aPreference The preference value. + */ + void SetPreference(uint16_t aPreference) { mPreference = aPreference; } + +private: + uint8_t mPreference; +} OT_TOOL_PACKED_END; + /** * Represents an Elapsed Time DHCPv6 Option. */ @@ -705,19 +750,30 @@ public: void Init(void) { SetCode(kElapsedTime), SetLength(sizeof(*this) - sizeof(Option)); } /** - * Returns the elapsed time since solicit starts. + * Returns the elapsed time. * - * @returns The elapsed time since solicit starts. + * @returns The elapsed time (in unit of hundredths of a second). */ uint16_t GetElapsedTime(void) const { return BigEndian::HostSwap16(mElapsedTime); } /** - * Sets the elapsed time since solicit starts. + * Sets the elapsed time. * - * @param[in] aElapsedTime The elapsed time since solicit starts. + * @param[in] aElapsedTime The elapsed time (in unit of hundredths of a second). */ void SetElapsedTime(uint16_t aElapsedTime) { mElapsedTime = BigEndian::HostSwap16(aElapsedTime); } + /** + * Append an Elapsed Time Option to a message. + * + * @param[in,out] aMessage The message to append to. + * @param[in] aElapsedTime The elapsed time (in unit of hundredths of a second). + * + * @retval kErrorNone Successfully appended the option. + * @retval kErrorNoBufs Insufficient available buffers to grow the message. + */ + static Error AppendTo(Message &aMessage, uint16_t aElapsedTime); + private: uint16_t mElapsedTime; } OT_TOOL_PACKED_END; @@ -829,6 +885,202 @@ public: static Error AppendTo(Message &aMessage); }; +/** + * Represents an Identity Association for Prefix Delegation Option. + */ +OT_TOOL_PACKED_BEGIN +class IaPdOption : public Option +{ +public: + /** + * Initializes the DHCPv6 Option. + */ + void Init(void) { SetCode(kIaPd), SetLength(sizeof(*this) - sizeof(Option)); } + + /** + * Returns IAID. + * + * @returns The IAID. + */ + uint32_t GetIaid(void) const { return BigEndian::HostSwap32(mIaid); } + + /** + * Sets the IAID. + * + * @param[in] aIaid The IAID. + */ + void SetIaid(uint32_t aIaid) { mIaid = BigEndian::HostSwap32(aIaid); } + + /** + * Returns T1. + * + * @returns The value of T1. + */ + uint32_t GetT1(void) const { return BigEndian::HostSwap32(mT1); } + + /** + * Sets the value of T1. + * + * @param[in] aT1 The value of T1. + */ + void SetT1(uint32_t aT1) { mT1 = BigEndian::HostSwap32(aT1); } + + /** + * Returns T2. + * + * @returns The value of T2. + */ + uint32_t GetT2(void) const { return BigEndian::HostSwap32(mT2); } + + /** + * Sets the value of T2. + * + * @param[in] aT2 The value of T2. + */ + void SetT2(uint32_t aT2) { mT2 = BigEndian::HostSwap32(aT2); } + +private: + uint32_t mIaid; + uint32_t mT1; + uint32_t mT2; + // Followed by sub-options +} OT_TOOL_PACKED_END; + +/** + * Represents an Identity Association Prefix Option. + */ +OT_TOOL_PACKED_BEGIN +class IaPrefixOption : public Option +{ +public: + /** + * Initializes the DHCPv6 Option. + */ + void Init(void) { SetCode(kIaPrefix), SetLength(sizeof(*this) - sizeof(Option)); } + + /** + * Returns the preferred lifetime of the IPv6 address. + * + * @returns The preferred lifetime of the IPv6 address. + */ + uint32_t GetPreferredLifetime(void) const { return BigEndian::HostSwap32(mPreferredLifetime); } + + /** + * Sets the preferred lifetime of the IPv6 address. + * + * @param[in] aPreferredLifetime The preferred lifetime of the IPv6 address. + */ + void SetPreferredLifetime(uint32_t aPreferredLifetime) + { + mPreferredLifetime = BigEndian::HostSwap32(aPreferredLifetime); + } + + /** + * Returns the valid lifetime of the IPv6 address. + * + * @returns The valid lifetime of the IPv6 address. + */ + uint32_t GetValidLifetime(void) const { return BigEndian::HostSwap32(mValidLifetime); } + + /** + * Sets the valid lifetime of the IPv6 address. + * + * @param[in] aValidLifetime The valid lifetime of the IPv6 address. + */ + void SetValidLifetime(uint32_t aValidLifetime) { mValidLifetime = BigEndian::HostSwap32(aValidLifetime); } + + /** + * Returns the prefix length in bits. + * + * @returns The prefix length in bits. + */ + uint8_t GetPrefixLength(void) const { return mPrefixLength; } + + /** + * Reads the prefix and its length from the option. + * + * @param[out] aPrefix A reference to an `Ip6::Prefix` to return the prefix and its length. + */ + void GetPrefix(Ip6::Prefix &aPrefix) const; + + /** + * Sets the prefix and its length in the option. + * + * @param[in] aPrefix An IPv6 prefix. + */ + void SetPrefix(const Ip6::Prefix &aPrefix); + +private: + uint32_t mPreferredLifetime; + uint32_t mValidLifetime; + uint8_t mPrefixLength; + Ip6::Address mPrefix; + // Can be followed by sub-options. +} OT_TOOL_PACKED_END; + +/** + * Represents a Server Unicast Option. + */ +OT_TOOL_PACKED_BEGIN +class ServerUnicastOption : public Option +{ +public: + /** + * Initializes the DHCPv6 Option. + */ + void Init(void) { SetCode(kServerUnicast), SetLength(sizeof(*this) - sizeof(Option)); } + + /** + * Returns the server IPv6 address. + * + * @returns the server IPv6 address. + */ + const Ip6::Address &GetServerAddress(void) const { return mServerAddress; } + + /** + * Sets the server IPv6 address. + * + * @param[in] aServerAddress The server IPv6 address. + */ + void SetServerAddress(const Ip6::Address &aServerAddress) { mServerAddress = aServerAddress; } + +private: + Ip6::Address mServerAddress; +} OT_TOOL_PACKED_END; + +/** + * Represents an SOL_MAX_RT Option (Max Solicit timeout value). + */ +OT_TOOL_PACKED_BEGIN +class SolMaxRtOption : public Option +{ +public: + static constexpr uint32_t kMinSolMaxRt = 60; ///< Minimum SOL_MAX_RT value. + static constexpr uint32_t kMaxSolMaxRt = 86400; ///< Maximum SOL_MAX_RT value. + + /** + * Initializes the DHCPv6 Option. + */ + void Init(void) { SetCode(kSolMaxRt), SetLength(sizeof(*this) - sizeof(Option)); } + + /** + * Returns the SOL_MAX_RT value. + * + * @returns The SOL_MAX_RT value (in seconds). + */ + uint16_t GetSolMaxRt(void) const { return BigEndian::HostSwap32(mSolMaxRt); } + + /** + * Sets the SOL_MAX_RT. + * + * @param[in] aSolMaxRt The SOL_MAX_RT value (in seconds). + */ + void SetSolMaxRt(uint32_t aSolMaxRt) { mSolMaxRt = BigEndian::HostSwap32(aSolMaxRt); } + +private: + uint32_t mSolMaxRt; +} OT_TOOL_PACKED_END; + /** * @} */ @@ -836,6 +1088,4 @@ public: } // namespace Dhcp6 } // namespace ot -#endif // #if OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE || OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE - #endif // DHCP6_TYPES_HPP_ diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index a491018e0..ae25f2278 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -48,6 +48,7 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 0 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE 1 diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index 9fd554e63..85412ff3d 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -49,6 +49,8 @@ #define OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 1 + #define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 1 #define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ALLOW_RUN_TIME_SELECTION 1 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index e59a16377..71c3ea41d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -208,6 +208,7 @@ ot_unit_test(cmd_line_parser) ot_unit_test(crc) ot_unit_test(data) ot_unit_test(dataset) +ot_unit_test(dhcp6_pd_client) ot_unit_test(dns) ot_unit_test(dns_client) ot_unit_test(dnssd_discovery_proxy) diff --git a/tests/unit/test_dhcp6_pd_client.cpp b/tests/unit/test_dhcp6_pd_client.cpp new file mode 100644 index 000000000..8cc76444a --- /dev/null +++ b/tests/unit/test_dhcp6_pd_client.cpp @@ -0,0 +1,2796 @@ +/* + * Copyright (c) 2025, 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. + */ + +#include + +#include "test_platform.h" +#include "test_util.hpp" + +#include +#include +#include +#include + +#include "border_router/dhcp6_pd_client.hpp" +#include "common/arg_macros.hpp" +#include "common/array.hpp" +#include "common/num_utils.hpp" +#include "instance/instance.hpp" + +namespace ot { + +#if OT_CONFIG_DHCP6_PD_CLIENT_ENABLE + +// Logs a message and adds current time (sNow) as "::." +#define Log(...) \ + printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 3600000), (sNow / 60000) % 60, \ + (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__)) + +static constexpr uint32_t kInfraIfIndex = 1; + +static Instance *sInstance; + +static uint32_t sNow = 0; +static uint32_t sAlarmTime; +static bool sAlarmOn = false; + +//---------------------------------------------------------------------------------------------------------------------- +// Function prototypes + +void ProcessTasklets(void); +void AdvanceTime(uint32_t aDuration); +void AdvanceNowTo(uint32_t aNewNow); + +extern "C" { + +#if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED +void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...) +{ + OT_UNUSED_VARIABLE(aLogLevel); + OT_UNUSED_VARIABLE(aLogRegion); + + va_list args; + + printf(" "); + va_start(args, aFormat); + vprintf(aFormat, args); + va_end(args); + printf("\n"); +} +#endif + +//---------------------------------------------------------------------------------------------------------------------- +// otPlatAlarams + +void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; } + +void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt) +{ + sAlarmOn = true; + sAlarmTime = aT0 + aDt; +} + +uint32_t otPlatAlarmMilliGetNow(void) { return sNow; } + +//---------------------------------------------------------------------------------------------------------------------- +// Heap + +Array sHeapAllocatedPtrs; + +#if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE + +void *otPlatCAlloc(size_t aNum, size_t aSize) +{ + void *ptr = calloc(aNum, aSize); + + SuccessOrQuit(sHeapAllocatedPtrs.PushBack(ptr)); + + return ptr; +} + +void otPlatFree(void *aPtr) +{ + if (aPtr != nullptr) + { + void **entry = sHeapAllocatedPtrs.Find(aPtr); + + VerifyOrQuit(entry != nullptr, "A heap allocated item is freed twice"); + sHeapAllocatedPtrs.Remove(*entry); + } + + free(aPtr); +} + +#endif + +} // extern "C" + +//--------------------------------------------------------------------------------------------------------------------- +// Dhcp6Msg + +struct Dhcp6Msg : public Clearable +{ + static constexpr uint16_t kMaxIaPds = 2; + static constexpr uint16_t kMaxIaPrefixes = 3; + static constexpr uint16_t kMaxReqOptions = 3; + static constexpr uint16_t kInfoStringSize = 100; + + typedef String InfoString; + + struct IaPrefix : public Clearable + { + Ip6::Prefix mPrefix; + uint32_t mPreferredLifetime; + uint32_t mValidLifetime; + }; + + using IaPrefixArray = Array; + + struct IaPd : Clearable + { + bool Matches(uint32_t aIaid) const { return mIaid == aIaid; } + + uint32_t mIaid; + uint32_t mT1; + uint32_t mT2; + bool mHasStatus; + uint16_t mStatusCode; + IaPrefixArray mIaPrefixes; + }; + + using IaPdArray = Array; + + struct Duid + { + InfoString ToString(void) const + { + InfoString string; + + string.AppendHexBytes(mShared.mBytes, mLength); + return string; + } + + union + { + uint8_t mBytes[Dhcp6::Duid::kMaxSize]; + Dhcp6::Eui64Duid mEui64; + } mShared; + uint16_t mLength; + }; + + using ReqOptionArray = Array; + + void LogMsg(const char *aAction) const; + void ParseFrom(const Message &aMessage); + void PrepareMessage(Message &aMessage); + + uint8_t mMsgType; + Dhcp6::TransactionId mTransactionId; + bool mHasStatus : 1; + bool mHasElapsedTime : 1; + bool mHasClientId : 1; + bool mHasServerId : 1; + bool mHasOptionRequest : 1; + bool mHasPreference : 1; + bool mHasServerUnicast : 1; + bool mHasSolMaxRt : 1; + uint16_t mStatusCode; + uint16_t mElapsedTime; + Duid mClientDuid; + Duid mServerDuid; + ReqOptionArray mRequestedOptions; + uint8_t mPreference; + Ip6::Address mServerAddress; + uint16_t mSolMaxRt; + IaPdArray mIaPds; +}; + +static const char *Dhcp6MsgTypeToString(uint8_t aMsgType) +{ + const char *str = "Unknown"; + + switch (aMsgType) + { + case Dhcp6::kMsgTypeSolicit: + str = "Solicit"; + break; + case Dhcp6::kMsgTypeAdvertise: + str = "Advertise"; + break; + case Dhcp6::kMsgTypeRequest: + str = "Request"; + break; + case Dhcp6::kMsgTypeRenew: + str = "Renew"; + break; + case Dhcp6::kMsgTypeRebind: + str = "Rebind"; + break; + case Dhcp6::kMsgTypeReply: + str = "Reply"; + break; + case Dhcp6::kMsgTypeRelease: + str = "Release"; + break; + case Dhcp6::kMsgTypeReconfigure: + str = "Reconfigure"; + break; + } + + return str; +} + +void Dhcp6Msg::LogMsg(const char *aAction) const +{ + const uint8_t *txnId = reinterpret_cast(&mTransactionId); + + Log("%s %s message", aAction, Dhcp6MsgTypeToString(mMsgType)); + + Log(" %-13s : %02x%02x%02x", "TransactionId", txnId[0], txnId[1], txnId[2]); + + if (mHasStatus) + { + Log(" %-13s : %u", "StatusCode", mStatusCode); + } + + if (mHasElapsedTime) + { + Log(" %-13s : %u", "ElapsedTime", mElapsedTime); + } + + if (mHasClientId) + { + Log(" %-13s : %s", "ClientId", mClientDuid.ToString().AsCString()); + } + + if (mHasServerId) + { + Log(" %-13s : %s", "ServerId", mServerDuid.ToString().AsCString()); + } + + if (mHasOptionRequest) + { + InfoString string; + + string.Append("[ "); + for (uint16_t optionCode : mRequestedOptions) + { + string.Append("%u ", optionCode); + } + string.Append("]"); + Log(" %-13s : %s", "ReqOptions", string.AsCString()); + } + + if (mHasPreference) + { + Log(" %-13s : %u", "Preference", mPreference); + } + + if (mHasServerUnicast) + { + Log(" %-13s : %s", "ServerAddr", mServerAddress.ToString().AsCString()); + } + + if (mHasSolMaxRt) + { + Log(" %-13s : %lu", "SolMaxRt", ToUlong(mSolMaxRt)); + } + + for (const IaPd &iaPd : mIaPds) + { + Log(" %-13s : Iaid:%lu, T1:%lu, T2:%lu", "IaPd", ToUlong(iaPd.mIaid), ToUlong(iaPd.mT1), ToUlong(iaPd.mT2)); + + if (iaPd.mHasStatus) + { + Log(" %-11s : %u", "StatusCode", iaPd.mStatusCode); + } + + for (const IaPrefix &iaPrefix : iaPd.mIaPrefixes) + { + Log(" %-11s : %s, preferred:%lu, valid:%lu", "Prefix", iaPrefix.mPrefix.ToString().AsCString(), + ToUlong(iaPrefix.mPreferredLifetime), ToUlong(iaPrefix.mValidLifetime)); + } + } +} + +void Dhcp6Msg::ParseFrom(const Message &aMessage) +{ + OffsetRange offsetRange; + OffsetRange optionOffsetRange; + OffsetRange subOptionOffsetRange; + Dhcp6::Header header; + IaPd *iaPd; + IaPrefix *iaPrefix; + union + { + Dhcp6::Option option; + Dhcp6::StatusCodeOption statusOption; + Dhcp6::ElapsedTimeOption elapsedTimeOption; + Dhcp6::PreferenceOption preferenceOption; + Dhcp6::ServerUnicastOption serverUnicastOption; + Dhcp6::SolMaxRtOption solMaxRtOption; + Dhcp6::IaPdOption iaPdOption; + Dhcp6::IaPrefixOption iaPrefixOption; + }; + + Clear(); + offsetRange.InitFromMessageFullLength(aMessage); + + SuccessOrQuit(aMessage.Read(offsetRange, header)); + mMsgType = header.GetMsgType(); + mTransactionId = header.GetTransactionId(); + offsetRange.AdvanceOffset(sizeof(header)); + + while (!offsetRange.IsEmpty()) + { + SuccessOrQuit(aMessage.Read(offsetRange, option)); + VerifyOrQuit(offsetRange.Contains(option.GetSize())); + + optionOffsetRange = offsetRange; + optionOffsetRange.ShrinkLength(static_cast(option.GetSize())); + offsetRange.AdvanceOffset(option.GetSize()); + + switch (option.GetCode()) + { + case Dhcp6::Option::kStatusCode: + VerifyOrQuit(!mHasStatus); + mHasStatus = true; + SuccessOrQuit(aMessage.Read(optionOffsetRange, statusOption)); + mStatusCode = statusOption.GetStatusCode(); + break; + + case Dhcp6::Option::kElapsedTime: + VerifyOrQuit(!mHasElapsedTime); + mHasElapsedTime = true; + SuccessOrQuit(aMessage.Read(optionOffsetRange, elapsedTimeOption)); + mElapsedTime = elapsedTimeOption.GetElapsedTime(); + break; + + case Dhcp6::Option::kClientId: + VerifyOrQuit(!mHasClientId); + mHasClientId = true; + optionOffsetRange.AdvanceOffset(sizeof(Dhcp6::Option)); + VerifyOrQuit(optionOffsetRange.GetLength() >= Dhcp6::Duid::kMinSize); + VerifyOrQuit(optionOffsetRange.GetLength() <= Dhcp6::Duid::kMaxSize); + mClientDuid.mLength = optionOffsetRange.GetLength(); + aMessage.ReadBytes(optionOffsetRange, mClientDuid.mShared.mBytes); + break; + + case Dhcp6::Option::kServerId: + VerifyOrQuit(!mHasServerId); + mHasServerId = true; + optionOffsetRange.AdvanceOffset(sizeof(Dhcp6::Option)); + VerifyOrQuit(optionOffsetRange.GetLength() >= Dhcp6::Duid::kMinSize); + VerifyOrQuit(optionOffsetRange.GetLength() <= Dhcp6::Duid::kMaxSize); + mServerDuid.mLength = optionOffsetRange.GetLength(); + aMessage.ReadBytes(optionOffsetRange, mServerDuid.mShared.mBytes); + break; + + case Dhcp6::Option::kOptionRequest: + VerifyOrQuit(!mHasOptionRequest); + mHasOptionRequest = true; + optionOffsetRange.AdvanceOffset(sizeof(Dhcp6::Option)); + VerifyOrQuit(optionOffsetRange.GetLength() > 0); + + while (!optionOffsetRange.IsEmpty()) + { + uint16_t reqOption; + + SuccessOrQuit(aMessage.Read(optionOffsetRange, reqOption)); + reqOption = BigEndian::HostSwap16(reqOption); + SuccessOrQuit(mRequestedOptions.PushBack(reqOption)); + optionOffsetRange.AdvanceOffset(sizeof(uint16_t)); + } + + break; + + case Dhcp6::Option::kPreference: + VerifyOrQuit(!mHasPreference); + mHasPreference = true; + SuccessOrQuit(aMessage.Read(optionOffsetRange, preferenceOption)); + mPreference = preferenceOption.GetPreference(); + break; + + case Dhcp6::Option::kServerUnicast: + VerifyOrQuit(!mHasServerUnicast); + mHasServerUnicast = true; + SuccessOrQuit(aMessage.Read(optionOffsetRange, serverUnicastOption)); + mServerAddress = serverUnicastOption.GetServerAddress(); + break; + + case Dhcp6::Option::kSolMaxRt: + VerifyOrQuit(!mHasSolMaxRt); + mHasSolMaxRt = true; + SuccessOrQuit(aMessage.Read(optionOffsetRange, solMaxRtOption)); + mSolMaxRt = solMaxRtOption.GetSolMaxRt(); + break; + + case Dhcp6::Option::kIaPd: + SuccessOrQuit(aMessage.Read(optionOffsetRange, iaPdOption)); + optionOffsetRange.AdvanceOffset(sizeof(iaPdOption)); + VerifyOrQuit(!mIaPds.ContainsMatching(iaPdOption.GetIaid())); + + iaPd = mIaPds.PushBack(); + VerifyOrQuit(iaPd != nullptr); + iaPd->Clear(); + iaPd->mIaid = iaPdOption.GetIaid(); + iaPd->mT1 = iaPdOption.GetT1(); + iaPd->mT2 = iaPdOption.GetT2(); + + while (!optionOffsetRange.IsEmpty()) + { + SuccessOrQuit(aMessage.Read(optionOffsetRange, option)); + VerifyOrQuit(optionOffsetRange.Contains(option.GetSize())); + + subOptionOffsetRange = optionOffsetRange; + subOptionOffsetRange.ShrinkLength(static_cast(option.GetSize())); + + optionOffsetRange.AdvanceOffset(option.GetSize()); + + switch (option.GetCode()) + { + case Dhcp6::Option::kStatusCode: + VerifyOrQuit(!iaPd->mHasStatus); + iaPd->mHasStatus = true; + SuccessOrQuit(aMessage.Read(subOptionOffsetRange, statusOption)); + iaPd->mStatusCode = statusOption.GetStatusCode(); + break; + + case Dhcp6::Option::kIaPrefix: + SuccessOrQuit(aMessage.Read(subOptionOffsetRange, iaPrefixOption)); + iaPrefix = iaPd->mIaPrefixes.PushBack(); + VerifyOrQuit(iaPrefix != nullptr); + iaPrefix->Clear(); + iaPrefixOption.GetPrefix(iaPrefix->mPrefix); + iaPrefix->mPreferredLifetime = iaPrefixOption.GetPreferredLifetime(); + iaPrefix->mValidLifetime = iaPrefixOption.GetValidLifetime(); + break; + + default: + // Unexpected sub-option + VerifyOrQuit(false); + break; + } + } + break; + + default: + // Unexpected option + VerifyOrQuit(false); + break; + } + } +} + +void Dhcp6Msg::PrepareMessage(Message &aMessage) +{ + Dhcp6::Header header; + union + { + Dhcp6::Option option; + Dhcp6::StatusCodeOption statusOption; + Dhcp6::ElapsedTimeOption elapsedTimeOption; + Dhcp6::PreferenceOption preferenceOption; + Dhcp6::ServerUnicastOption serverUnicastOption; + Dhcp6::SolMaxRtOption solMaxRtOption; + Dhcp6::IaPdOption iaPdOption; + Dhcp6::IaPrefixOption iaPrefixOption; + }; + + header.SetMsgType(static_cast(mMsgType)); + header.SetTransactionId(mTransactionId); + SuccessOrQuit(aMessage.Append(header)); + + if (mHasStatus) + { + statusOption.Init(); + statusOption.SetStatusCode(static_cast(mStatusCode)); + SuccessOrQuit(aMessage.Append(statusOption)); + } + + if (mHasElapsedTime) + { + elapsedTimeOption.Init(); + elapsedTimeOption.SetElapsedTime(mElapsedTime); + SuccessOrQuit(aMessage.Append(elapsedTimeOption)); + } + + if (mHasClientId) + { + SuccessOrQuit(Dhcp6::Option::AppendOption(aMessage, Dhcp6::Option::kClientId, mClientDuid.mShared.mBytes, + mClientDuid.mLength)); + } + + if (mHasServerId) + { + SuccessOrQuit(Dhcp6::Option::AppendOption(aMessage, Dhcp6::Option::kServerId, mServerDuid.mShared.mBytes, + mServerDuid.mLength)); + } + + if (mHasOptionRequest) + { + option.SetCode(Dhcp6::Option::kOptionRequest); + option.SetLength(mRequestedOptions.GetLength() * sizeof(uint16_t)); + SuccessOrQuit(aMessage.Append(option)); + + for (uint16_t reqOption : mRequestedOptions) + { + uint16_t swapedOption = BigEndian::HostSwap16(reqOption); + + SuccessOrQuit(aMessage.Append(swapedOption)); + } + } + + if (mHasPreference) + { + preferenceOption.Init(); + preferenceOption.SetPreference(mPreference); + SuccessOrQuit(aMessage.Append(preferenceOption)); + } + + if (mHasServerUnicast) + { + serverUnicastOption.Init(); + serverUnicastOption.SetServerAddress(mServerAddress); + SuccessOrQuit(aMessage.Append(serverUnicastOption)); + } + + if (mHasSolMaxRt) + { + solMaxRtOption.Init(); + solMaxRtOption.SetSolMaxRt(mSolMaxRt); + SuccessOrQuit(aMessage.Append(solMaxRtOption)); + } + + for (const IaPd &iaPd : mIaPds) + { + uint16_t length; + + length = sizeof(Dhcp6::IaPdOption) - sizeof(Dhcp6::Option); + + if (mHasStatus) + { + length += sizeof(Dhcp6::StatusCodeOption); + } + + length += sizeof(Dhcp6::IaPrefixOption) * iaPd.mIaPrefixes.GetLength(); + + iaPdOption.Init(); + iaPdOption.SetLength(length); + iaPdOption.SetIaid(iaPd.mIaid); + iaPdOption.SetT1(iaPd.mT1); + iaPdOption.SetT2(iaPd.mT2); + SuccessOrQuit(aMessage.Append(iaPdOption)); + + if (mHasStatus) + { + statusOption.Init(); + statusOption.SetStatusCode(static_cast(iaPd.mStatusCode)); + SuccessOrQuit(aMessage.Append(statusOption)); + } + + for (const IaPrefix &iaPrefix : iaPd.mIaPrefixes) + { + iaPrefixOption.Init(); + iaPrefixOption.SetPreferredLifetime(iaPrefix.mPreferredLifetime); + iaPrefixOption.SetValidLifetime(iaPrefix.mValidLifetime); + iaPrefixOption.SetPrefix(iaPrefix.mPrefix); + SuccessOrQuit(aMessage.Append(iaPrefixOption)); + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// Dhcp6RxMsg + +static const uint32_t kExpectedIaid = 0; +static const uint8_t kExpectedPrefixLength = 64; + +static const uint16_t kMaxDhcp6RxMsgs = 32; + +class Dhcp6RxMsg : public Dhcp6Msg +{ +public: + void ValidateAsSolicit(void) const; + + void ValidateAsRequest(const Ip6::Prefix &aPrefix, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr = nullptr) const; + + void ValidateAsRenew(const Ip6::Prefix &aPrefix, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr = nullptr) const; + + void ValidateAsRebind(const Ip6::Prefix &aPrefix) const; + + void ValidateAsRelease(const Ip6::Prefix &aPrefix, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr = nullptr) const; + + uint32_t mRxTime; + Ip6::Address mDstAddr; + +private: + void Validate(Dhcp6::MsgType aMsgType, + const Ip6::Prefix &aPrefix, + const Mac::ExtAddress *aServerMacAddr = nullptr, + const Ip6::Address *aServerIp6Addr = nullptr) const; +}; + +Ip6::Prefix PrefixFromString(const char *aString, uint8_t aPrefixLength) +{ + Ip6::Prefix prefix; + + SuccessOrQuit(AsCoreType(&prefix.mPrefix).FromString(aString)); + prefix.mLength = aPrefixLength; + + return prefix; +} + +Ip6::Address AddressFromString(const char *aString) +{ + Ip6::Address address; + + SuccessOrQuit(address.FromString(aString)); + + return address; +} + +void Dhcp6RxMsg::ValidateAsSolicit(void) const +{ + Ip6::Prefix prefix; + + VerifyOrQuit(mMsgType == Dhcp6::kMsgTypeSolicit); + VerifyOrQuit(mDstAddr == AddressFromString("ff02::1:2")); + + prefix.Clear(); + prefix.SetLength(kExpectedPrefixLength); + + Validate(Dhcp6::kMsgTypeSolicit, prefix); +} + +void Dhcp6RxMsg::ValidateAsRequest(const Ip6::Prefix &aPrefix, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr) const +{ + Validate(Dhcp6::kMsgTypeRequest, aPrefix, &aServerMacAddr, aServerIp6Addr); +} + +void Dhcp6RxMsg::ValidateAsRenew(const Ip6::Prefix &aPrefix, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr) const +{ + Validate(Dhcp6::kMsgTypeRenew, aPrefix, &aServerMacAddr, aServerIp6Addr); +} + +void Dhcp6RxMsg::ValidateAsRebind(const Ip6::Prefix &aPrefix) const { Validate(Dhcp6::kMsgTypeRebind, aPrefix); } + +void Dhcp6RxMsg::ValidateAsRelease(const Ip6::Prefix &aPrefix, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr) const +{ + Validate(Dhcp6::kMsgTypeRelease, aPrefix, &aServerMacAddr, aServerIp6Addr); +} + +void Dhcp6RxMsg::Validate(Dhcp6::MsgType aMsgType, + const Ip6::Prefix &aPrefix, + const Mac::ExtAddress *aServerMacAddr, + const Ip6::Address *aServerIp6Addr) const +{ + VerifyOrQuit(mMsgType == aMsgType); + + if (aServerIp6Addr != nullptr) + { + VerifyOrQuit(mDstAddr == *aServerIp6Addr); + } + else + { + VerifyOrQuit(mDstAddr == AddressFromString("ff02::1:2")); + } + + VerifyOrQuit(!mHasStatus); + VerifyOrQuit(!mHasPreference); + VerifyOrQuit(!mHasServerUnicast); + VerifyOrQuit(!mHasSolMaxRt); + + VerifyOrQuit(mHasElapsedTime); + + VerifyOrQuit(mHasOptionRequest); + VerifyOrQuit(mRequestedOptions.GetLength() == 1); + VerifyOrQuit(mRequestedOptions[0] == Dhcp6::Option::kSolMaxRt); + + VerifyOrQuit(mHasClientId); + VerifyOrQuit(mClientDuid.mShared.mEui64.IsValid()); + VerifyOrQuit(mClientDuid.mShared.mEui64.GetLinkLayerAddress() == sInstance->Get().GetExtAddress()); + + if (aServerMacAddr != nullptr) + { + VerifyOrQuit(mHasServerId); + VerifyOrQuit(mServerDuid.mShared.mEui64.IsValid()); + VerifyOrQuit(mServerDuid.mShared.mEui64.GetLinkLayerAddress() == *aServerMacAddr); + } + else + { + VerifyOrQuit(!mHasServerId); + } + + VerifyOrQuit(mIaPds.GetLength() == 1); + VerifyOrQuit(mIaPds[0].mIaid == kExpectedIaid); + VerifyOrQuit(mIaPds[0].mT1 == 0); + VerifyOrQuit(mIaPds[0].mT2 == 0); + VerifyOrQuit(!mIaPds[0].mHasStatus); + VerifyOrQuit(mIaPds[0].mIaPrefixes.GetLength() == 1); + VerifyOrQuit(mIaPds[0].mIaPrefixes[0].mPrefix == aPrefix); + VerifyOrQuit(mIaPds[0].mIaPrefixes[0].mPreferredLifetime == 0); + VerifyOrQuit(mIaPds[0].mIaPrefixes[0].mValidLifetime == 0); +} + +//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct Dhcp6TxMsg : public Dhcp6Msg +{ + struct PrefixInfo + { + uint16_t mIaid; + uint32_t mT1; + uint32_t mT2; + uint32_t mPreferredLifetime; + uint32_t mValidLifetime; + Ip6::Prefix mPrefix; + }; + + void PrepareAdvertise(const Dhcp6RxMsg &aClientMsg, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr = nullptr); + void PrepareReply(const Dhcp6RxMsg &aClientMsg, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr = nullptr); + void AddIaPrefix(const PrefixInfo &aInfo); + void Send(void); +}; + +void Dhcp6TxMsg::PrepareAdvertise(const Dhcp6RxMsg &aClientMsg, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr) +{ + Clear(); + mMsgType = Dhcp6::kMsgTypeAdvertise; + mTransactionId = aClientMsg.mTransactionId; + mHasClientId = true; + mHasServerId = true; + mClientDuid = aClientMsg.mClientDuid; + mServerDuid.mShared.mEui64.Init(aServerMacAddr); + mServerDuid.mLength = sizeof(Dhcp6::Eui64Duid); + + if (aServerIp6Addr != nullptr) + { + mHasServerUnicast = true; + mServerAddress = *aServerIp6Addr; + } +} + +void Dhcp6TxMsg::PrepareReply(const Dhcp6RxMsg &aClientMsg, + const Mac::ExtAddress &aServerMacAddr, + const Ip6::Address *aServerIp6Addr) +{ + Clear(); + mMsgType = Dhcp6::kMsgTypeReply; + mTransactionId = aClientMsg.mTransactionId; + mHasClientId = true; + mHasServerId = true; + mClientDuid = aClientMsg.mClientDuid; + mServerDuid.mShared.mEui64.Init(aServerMacAddr); + mServerDuid.mLength = sizeof(Dhcp6::Eui64Duid); + + if (aServerIp6Addr != nullptr) + { + mHasServerUnicast = true; + mServerAddress = *aServerIp6Addr; + } +} + +void Dhcp6TxMsg::AddIaPrefix(const PrefixInfo &aInfo) +{ + IaPd *iaPd = nullptr; + IaPrefix *iaPrefix; + + for (IaPd &ia : mIaPds) + { + if (ia.mIaid == aInfo.mIaid) + { + iaPd = &ia; + break; + } + } + + if (iaPd == nullptr) + { + iaPd = mIaPds.PushBack(); + VerifyOrQuit(iaPd != nullptr); + } + + iaPd->mIaid = aInfo.mIaid; + iaPd->mT1 = aInfo.mT1; + iaPd->mT2 = aInfo.mT2; + + iaPrefix = iaPd->mIaPrefixes.PushBack(); + VerifyOrQuit(iaPrefix != nullptr); + iaPrefix->mPrefix = aInfo.mPrefix; + iaPrefix->mPreferredLifetime = aInfo.mPreferredLifetime; + iaPrefix->mValidLifetime = aInfo.mValidLifetime; +} + +void Dhcp6TxMsg::Send(void) +{ + Message *message; + + message = sInstance->Get().Allocate(Message::kTypeOther); + VerifyOrQuit(message != nullptr); + PrepareMessage(*message); + + LogMsg("Sending"); + + otPlatInfraIfDhcp6PdClientHandleReceived(sInstance, message, kInfraIfIndex); +} + +//---------------------------------------------------------------------------------------------------------------------- +// otPlatInfraIf + +typedef BorderRouter::Dhcp6PdClient::DelegatedPrefix DelegatedPrefix; + +static bool sDhcp6ListeningEnabled = false; +static Array sDhcp6RxMsgs; + +extern "C" { + +void otPlatInfraIfDhcp6PdClientSetListeningEnabled(otInstance *aInstance, bool aEnable, uint32_t aInfraIfIndex) +{ + Log("otPlatInfraIfDhcp6PdClientSetListeningEnabled(aEnable:%u)", aEnable); + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aInfraIfIndex == kInfraIfIndex); + sDhcp6ListeningEnabled = aEnable; +} + +void otPlatInfraIfDhcp6PdClientSend(otInstance *aInstance, + otMessage *aMessage, + otIp6Address *aDestAddress, + uint32_t aInfraIfIndex) +{ + Message *message = AsCoreTypePtr(aMessage); + Dhcp6RxMsg *rxMsg; + + VerifyOrQuit(aInstance == sInstance); + VerifyOrQuit(aInfraIfIndex == kInfraIfIndex); + VerifyOrQuit(message != nullptr); + VerifyOrQuit(aDestAddress != nullptr); + + Log("otPlatInfraIfDhcp6PdClientSend(%s)", AsCoreType(aDestAddress).ToString().AsCString()); + + rxMsg = sDhcp6RxMsgs.PushBack(); + VerifyOrQuit(rxMsg != nullptr); + + rxMsg->ParseFrom(*message); + rxMsg->LogMsg("Received"); + + rxMsg->mRxTime = sNow; + rxMsg->mDstAddr = AsCoreType(aDestAddress); + + message->Free(); +} + +} // extern "C" + +//--------------------------------------------------------------------------------------------------------------------- + +void ProcessTasklets(void) +{ + do + { + otTaskletsProcess(sInstance); + } while (otTaskletsArePending(sInstance)); +} + +void AdvanceTime(uint32_t aDuration) +{ + uint32_t time = sNow + aDuration; + + Log("AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000); + + while (TimeMilli(sAlarmTime) <= TimeMilli(time)) + { + ProcessTasklets(); + sNow = sAlarmTime; + otPlatAlarmMilliFired(sInstance); + } + + ProcessTasklets(); + sNow = time; +} + +void AdvanceNowTo(uint32_t aNewNow) +{ + VerifyOrQuit(aNewNow >= sNow); + AdvanceTime(aNewNow - sNow); +} + +void InitTest(void) +{ + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Initialize OT instance. + + sNow = 0; + sAlarmOn = false; + sInstance = static_cast(testInitInstance()); + + sDhcp6ListeningEnabled = false; + sDhcp6RxMsgs.Clear(); + + SuccessOrQuit(otBorderRoutingInit(sInstance, kInfraIfIndex, /* aInfraIfIsRunning */ true)); + AdvanceTime(100); +} + +void FinalizeTest(void) { testFreeInstance(sInstance); } + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdPrefixDelegation(bool aShortPrefix, bool aAddServerUnicastOption) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Ip6::Prefix adjustedPrefix; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + Ip6::Address serverAddr; + const Ip6::Address *serverIp6Addr; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdPrefixDelegation(aShortPrefix:%u, aAddServerUnicastOption:%u)", aShortPrefix, + aAddServerUnicastOption); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + if (aShortPrefix) + { + prefix = PrefixFromString("2001:1111::", 48); + adjustedPrefix = PrefixFromString("2001:1111::", 64); + } + else + { + prefix = PrefixFromString("2001:2222::", 64); + adjustedPrefix = prefix; + } + + // If `aAddServerUnicastOption` is enabled, we add `ServerUnicastOption` + // to Advertise and Reply messages. This prompts the client to use a + // specific server unicast address instead of the all-servers multicast + // address. The client's use of this address is then validated when it + // sends Request or Renew messages throughout all the test steps. + + serverAddr = AddressFromString("fe80::1"); + serverIp6Addr = (aAddServerUnicastOption ? &serverAddr : 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first two Solicit messages"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(2200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr, serverIp6Addr); + + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + + txMsg.Send(); + + AdvanceTime(1); + + VerifyOrQuit(sInstance->Get().GetDelegatedPrefix() == nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is received"); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr, serverIp6Addr); + + for (uint16_t iter = 0; iter < 3; iter++) + { + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Reply message to %s", iter == 0 ? "Request" : "Renew"); + + sDhcp6RxMsgs.Clear(); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr, serverIp6Addr); + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate the delegated prefix on the client"); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == prefixInfo.mT1); + VerifyOrQuit(delegatedPrefix->mT2 == prefixInfo.mT2); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == prefixInfo.mPreferredLifetime); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that no messages is received until renew time T1"); + + AdvanceTime(prefixInfo.mT1 * 1000 - 1); + VerifyOrQuit(sDhcp6RxMsgs.IsEmpty()); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that at T1 time Renew message is received"); + + AdvanceTime(5); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + + sDhcp6RxMsgs[0].ValidateAsRenew(prefix, serverMacAddr, serverIp6Addr); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Do not send a Reply to the Renew. Wait till T2 time and check the Renew message retries"); + + AdvanceTime((prefixInfo.mT2 - prefixInfo.mT1) * 1000 - 6); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() > 1); + + for (uint16_t index = 2; index < sDhcp6RxMsgs.GetLength(); index++) + { + sDhcp6RxMsgs[index].ValidateAsRenew(prefix, serverMacAddr, serverIp6Addr); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime > 0); + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the delegated prefix on the client remains unchanged"); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == prefixInfo.mT1); + VerifyOrQuit(delegatedPrefix->mT2 == prefixInfo.mT2); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == prefixInfo.mPreferredLifetime); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that Rebind message is received at T2 time"); + + sDhcp6RxMsgs.Clear(); + AdvanceTime(10); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRebind(prefix); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Reply to Rebind"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr, serverIp6Addr); + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the delegated prefix on the client is renewed"); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == prefixInfo.mT1); + VerifyOrQuit(delegatedPrefix->mT2 == prefixInfo.mT2); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == prefixInfo.mPreferredLifetime); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that no message is received until renew time T1"); + + AdvanceTime(prefixInfo.mT1 * 1000 - 1); + VerifyOrQuit(sDhcp6RxMsgs.IsEmpty()); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Renew message is received"); + + AdvanceTime(5); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + + sDhcp6RxMsgs[0].ValidateAsRenew(prefix, serverMacAddr, serverIp6Addr); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait till T2 time and check that Renew message is retried"); + + AdvanceTime((prefixInfo.mT2 - prefixInfo.mT1) * 1000 - 6); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() > 1); + + for (uint16_t index = 2; index < sDhcp6RxMsgs.GetLength(); index++) + { + sDhcp6RxMsgs[index].ValidateAsRenew(prefix, serverMacAddr, serverIp6Addr); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime > 0); + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that Rebind message is received after T2 time"); + + sDhcp6RxMsgs.Clear(); + AdvanceTime(5); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRebind(prefix); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait till preferred time and check that Rebind message is now retried"); + + AdvanceTime((prefixInfo.mPreferredLifetime - prefixInfo.mT2) * 1000 - 6); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() > 1); + + for (uint16_t index = 2; index < sDhcp6RxMsgs.GetLength(); index++) + { + sDhcp6RxMsgs[index].ValidateAsRebind(prefix); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime > 0); + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the delegated prefix is still present on the client right before its expire time"); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == prefixInfo.mT1); + VerifyOrQuit(delegatedPrefix->mT2 == prefixInfo.mT2); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == prefixInfo.mPreferredLifetime); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the delegated prefix is removed after its expire time"); + + sDhcp6RxMsgs.Clear(); + AdvanceTime(5); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix == nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that Solicit messages are sent again"); + + AdvanceTime(10 * 1000); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() > 1); + + sDhcp6RxMsgs[0].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + + for (uint16_t index = 2; index < sDhcp6RxMsgs.GetLength(); index++) + { + sDhcp6RxMsgs[index].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime > 0); + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement, check Request message and respond with Reply"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr, serverIp6Addr); + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr, serverIp6Addr); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr, serverIp6Addr); + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate the delegated prefix on the client"); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == prefixInfo.mT1); + VerifyOrQuit(delegatedPrefix->mT2 == prefixInfo.mT2); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == prefixInfo.mPreferredLifetime); + + AdvanceTime(5 * 1000); + VerifyOrQuit(sDhcp6RxMsgs.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client, and validate that Release message is received"); + + sInstance->Get().Stop(); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRelease(prefix, serverMacAddr, serverIp6Addr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Reply to Release message and check that no more messages is received"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr, serverIp6Addr); + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(20 * 1000); + + VerifyOrQuit(sDhcp6RxMsgs.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdPrefixDelegation(aShortPrefix:%u, aAddServerUnicastOption:%u)", aShortPrefix, + aAddServerUnicastOption); + + FinalizeTest(); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdSolicitRetries(void) +{ + static uint32_t kMaxTimeout = 3600 * 1000; + + uint16_t heapAllocations; + uint32_t firstRxTime; + uint32_t timeout; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdSolicitRetries"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and check initial delay for first Solicit"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + + sInstance->Get().Start(); + + VerifyOrQuit(sDhcp6ListeningEnabled); + + // Initial random delay of [0, 1000] msec to send first solicit + AdvanceTime(1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + firstRxTime = sDhcp6RxMsgs[0].mRxTime; + + VerifyOrQuit(sInstance->Get().GetDelegatedPrefix() == nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait for more than 30 minutes and collect all Solicit messages"); + + AdvanceTime(20000 * 1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() >= 14); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate the retx timing of Solicit messages"); + + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[1].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[1].mElapsedTime == (sDhcp6RxMsgs[1].mRxTime - firstRxTime) / 10); + timeout = sDhcp6RxMsgs[1].mRxTime - firstRxTime; + + // The initial time should be randomly picked from [1, 1.1] sec. + + VerifyOrQuit(timeout >= 1000); + VerifyOrQuit(timeout <= 1100); + + for (uint16_t index = 2; index < sDhcp6RxMsgs.GetLength(); index++) + { + uint32_t newTimeout; + uint32_t minTimeout; + uint32_t maxTimeout; + uint32_t elapsedTime; + + sDhcp6RxMsgs[index].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + + newTimeout = sDhcp6RxMsgs[index].mRxTime - sDhcp6RxMsgs[index - 1].mRxTime; + + minTimeout = 2 * timeout - timeout / 10; + maxTimeout = 2 * timeout + timeout / 10; + + if (maxTimeout > kMaxTimeout) + { + minTimeout = Min(minTimeout, kMaxTimeout - kMaxTimeout / 10); + maxTimeout = kMaxTimeout + kMaxTimeout / 10; + } + + Log("Solicit %2u -> timeout:%lu, min:%lu, max:%lu", index, ToUlong(newTimeout), ToUlong(minTimeout), + ToUlong(maxTimeout)); + + elapsedTime = (sDhcp6RxMsgs[index].mRxTime - firstRxTime) / 10; + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime == ClampToUint16(elapsedTime)); + + VerifyOrQuit(newTimeout >= minTimeout); + VerifyOrQuit(newTimeout <= maxTimeout); + + timeout = newTimeout; + } + + VerifyOrQuit(sInstance->Get().GetDelegatedPrefix() == nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client and make sure there are no more transmissions"); + + sDhcp6RxMsgs.Clear(); + sInstance->Get().Stop(); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + + AdvanceTime(200 * 1000); + VerifyOrQuit(sDhcp6RxMsgs.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start again and check the Solicit message tx"); + + sInstance->Get().Start(); + + VerifyOrQuit(sDhcp6ListeningEnabled); + + // Initial random delay of [0, 1000] msec to send first solicit + AdvanceTime(1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client"); + + sInstance->Get().Stop(); + AdvanceTime(15 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdSolicitRetries"); + + FinalizeTest(); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdRequestRetries(void) +{ + static constexpr uint32_t kInitialRequestTimeout = 1000; + static constexpr uint32_t kMaxRequestTimeout = 30 * 1000; + static constexpr uint16_t kMaxRequestRetxCount = 10; + + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Mac::ExtAddress serverMacAddr; + uint32_t firstRxTime; + uint32_t timeout; + uint32_t newTimeout; + uint32_t minTimeout; + uint32_t maxTimeout; + uint32_t elapsedTime; + uint16_t index; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdRequestRetries"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:aa::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first two Solicit messages"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(2200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is received"); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + firstRxTime = sDhcp6RxMsgs[0].mRxTime; + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait for 5 minutes and collect all messages"); + + AdvanceTime(300 * 1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() > kMaxRequestRetxCount); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate the retx timing of Request messages"); + + sDhcp6RxMsgs[1].ValidateAsRequest(prefix, serverMacAddr); + VerifyOrQuit(sDhcp6RxMsgs[1].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[1].mElapsedTime == (sDhcp6RxMsgs[1].mRxTime - firstRxTime) / 10); + timeout = sDhcp6RxMsgs[1].mRxTime - firstRxTime; + + // Validate the initial timeout + + minTimeout = kInitialRequestTimeout - kInitialRequestTimeout / 10; + maxTimeout = kInitialRequestTimeout + kInitialRequestTimeout / 10; + + Log("Request %2u -> timeout:%lu, min:%lu, max:%lu", 1, ToUlong(timeout), ToUlong(minTimeout), ToUlong(maxTimeout)); + + VerifyOrQuit(timeout >= minTimeout); + VerifyOrQuit(timeout <= maxTimeout); + + for (index = 2; index <= kMaxRequestRetxCount; index++) + { + sDhcp6RxMsgs[index].ValidateAsRequest(prefix, serverMacAddr); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + + newTimeout = sDhcp6RxMsgs[index].mRxTime - sDhcp6RxMsgs[index - 1].mRxTime; + + minTimeout = 2 * timeout - timeout / 10; + maxTimeout = 2 * timeout + timeout / 10; + + if (maxTimeout > kMaxRequestTimeout) + { + minTimeout = Min(minTimeout, kMaxRequestTimeout - kMaxRequestTimeout / 10); + maxTimeout = kMaxRequestTimeout + kMaxRequestTimeout / 10; + } + + Log("Request %2u -> timeout:%lu, min:%lu, max:%lu", index, ToUlong(newTimeout), ToUlong(minTimeout), + ToUlong(maxTimeout)); + + elapsedTime = (sDhcp6RxMsgs[index].mRxTime - firstRxTime) / 10; + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime == ClampToUint16(elapsedTime)); + + VerifyOrQuit(newTimeout >= minTimeout); + VerifyOrQuit(newTimeout <= maxTimeout); + + timeout = newTimeout; + } + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that after the Request Retries are finished, client restarts sending Solicit"); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() > kMaxRequestRetxCount + 1); + + index = kMaxRequestRetxCount + 1; + + sDhcp6RxMsgs[index].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[index].mTransactionId != sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[index].mElapsedTime == 0); + + // Check the timeout of the last Request message + timeout = sDhcp6RxMsgs[index].mRxTime - sDhcp6RxMsgs[index - 1].mRxTime; + VerifyOrQuit(timeout >= minTimeout); + VerifyOrQuit(timeout <= maxTimeout); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client"); + + sInstance->Get().Stop(); + AdvanceTime(15 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdRequestRetries"); + + FinalizeTest(); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdSelectBetweenMultipleServers(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix1; + Ip6::Prefix prefix2; + Ip6::Prefix prefix3; + Mac::ExtAddress serverMacAddr1; + Mac::ExtAddress serverMacAddr2; + Mac::ExtAddress serverMacAddr3; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdSelectBetweenMultipleServers()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr1.GenerateRandom(); + serverMacAddr2.GenerateRandom(); + serverMacAddr3.GenerateRandom(); + + prefix1 = PrefixFromString("2001:ff::", 64); + prefix2 = PrefixFromString("2001:0::", 48); + prefix3 = PrefixFromString("2001:dad0::", 40); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first Solicit message"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send multiple Advertisements from different servers providing different prefixes"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr1); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix1; + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr2); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix2; + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr3); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix3; + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the client does wait for the full timeout (on first Solicit) "); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + + // First timeout is at least 1000 msec + AdvanceNowTo(sDhcp6RxMsgs[0].mRxTime + 1000 - 1); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is received and its for the preferred prefix from server2"); + + AdvanceTime(200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[1].ValidateAsRequest(prefix2, serverMacAddr2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client"); + + sInstance->Get().Stop(); + + AdvanceTime(5 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdSelectBetweenMultipleServers()"); + + FinalizeTest(); +} + +void TestDhcp6PdServerWithMaxPreferrence(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Mac::ExtAddress serverMacAddr; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerWithMaxPreferrence()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:7::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first Solicit message"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + + // On the first Solicit, the client must wait for the full timeout + // unless preference is in Advertisement is set to max (255). + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with Preference Option string preference to max to the first Solicit"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.mHasPreference = true; + txMsg.mPreference = 255; + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the client does not wait any longer and sends Request immediately"); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[1].ValidateAsRequest(prefix, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client"); + + sInstance->Get().Stop(); + + AdvanceTime(5 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerWithMaxPreferrence()"); + + FinalizeTest(); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdServerOfferingMultiplePrefixes(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix1; + Ip6::Prefix prefix2; + Ip6::Prefix prefix3; + Mac::ExtAddress serverMacAddr; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerOfferingMultiplePrefixes()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix1 = PrefixFromString("2001:5:baba:beef::", 64); + prefix2 = PrefixFromString("2001:4::", 48); + prefix3 = PrefixFromString("2001:ef::", 40); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first Solicit message"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(1000); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisements from server offering three prefixes"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix1; + txMsg.AddIaPrefix(prefixInfo); + prefixInfo.mPrefix = prefix2; + txMsg.AddIaPrefix(prefixInfo); + prefixInfo.mPrefix = prefix3; + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the client does wait for the full timeout (on first Solicit) "); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + AdvanceNowTo(sDhcp6RxMsgs[0].mRxTime + 1000 - 1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is received and its for the preferred prefix"); + + AdvanceTime(200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[1].ValidateAsRequest(prefix2, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client"); + + sInstance->Get().Stop(); + + AdvanceTime(5 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerOfferingMultiplePrefixes()"); + + FinalizeTest(); +} + +void TestDhcp6PdInvalidOrUnusablePrefix(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Mac::ExtAddress serverMacAddr; + Dhcp6RxMsg modifiedSolicitRxMsg; + Mac::ExtAddress modifiedClientMacAddr; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdInvalidOrUnusablePrefix"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:9::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first two Solicit messages"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(2200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with wrong TransactionId and ensure it is not accepted"); + + modifiedSolicitRxMsg = sDhcp6RxMsgs[0]; + + do + { + modifiedSolicitRxMsg.mTransactionId.GenerateRandom(); + } while (modifiedSolicitRxMsg.mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + + txMsg.PrepareAdvertise(modifiedSolicitRxMsg, serverMacAddr); + prefixInfo.mIaid = modifiedSolicitRxMsg.mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with wrong Client ID and ensure it is not accepted"); + + modifiedSolicitRxMsg = sDhcp6RxMsgs[0]; + + do + { + modifiedClientMacAddr.GenerateRandom(); + } while (modifiedClientMacAddr == sDhcp6RxMsgs[0].mClientDuid.mShared.mEui64.GetLinkLayerAddress()); + + modifiedSolicitRxMsg.mClientDuid.mShared.mEui64.Init(modifiedClientMacAddr); + + txMsg.PrepareAdvertise(modifiedSolicitRxMsg, serverMacAddr); + prefixInfo.mIaid = modifiedSolicitRxMsg.mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with no Client ID and ensure it is not accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.mHasClientId = false; + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with no Server ID and ensure it is not accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.mHasServerId = false; + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with a wrong `Iaid` and ensure it is not accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid + 1; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with a long prefix and ensure it is not accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = PrefixFromString("2001:dead:beef:cafe::", 65); + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement T1 longer than T2 and ensure it is not accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 3000; + prefixInfo.mT2 = 2000; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement preferred lifetime longer than valid lifetime and ensure it is not accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 4000; + prefixInfo.mValidLifetime = 3600; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement with preferred lifetime below the min required threshold of 30 min"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 900; + prefixInfo.mT2 = 1000; + prefixInfo.mPreferredLifetime = 1799; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Finally send Advertisement with everything valid and ensure it is accepted"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 1800; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.Send(); + AdvanceTime(1); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 3); + + sDhcp6RxMsgs[2].ValidateAsRequest(prefix, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client"); + + sInstance->Get().Stop(); + AdvanceTime(15 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdInvalidOrUnusablePrefix"); + + FinalizeTest(); +} + +void TestDhcp6PdLifetimeT1AndT2Adjustments(void) +{ + struct TestCase + { + uint32_t mT1; + uint32_t mT2; + uint32_t mPreferredLifetime; + uint32_t mExpectedT1; + uint32_t mExpectedT2; + uint32_t mExpectedPreferredLifetime; + }; + + // Validate client will adjust the T1 and T2 and lifetime to reasonable range. + + static const TestCase kTestCases[] = { + // T1 and T2 are zero. Client must pick 0.5 and 0.8 times lifetime for T1 and T2. + {0, 0, 1800, 900, 1440, 1800}, + + // Only T1 is zero. + {0, 1300, 1800, 900, 1300, 1800}, + + // Only T2 is zero. + {800, 0, 1800, 800, 1440, 1800}, + + // T1 is zero, but default T1 (half of preferred lifetime) will be larger than given T2. + {0, 800, 1800, 900, 900, 1800}, + + // T1 and T2 are given but way too short. Client enforces min of 300s (5 min). + {1, 5, 1800, 300, 300, 1800}, + + // T1 and T2 zero with preferred lifetime of 7200 (2 hours). + {0, 0, 7200, 3600, 5760, 7200}, + + // T1 and T2 longer than lifetime. + {2000, 2500, 1800, 900, 1440, 1800}, + + // Given T1 and T2 (shorter than 0.5 and 0.8) with preferred lifetime of 7200 (2 hours). + {1000, 1200, 7200, 1000, 1200, 7200}, + + // Given T1 and T2 are too close to the preferred lifetime of 7200 (2 hours). + {7100, 7150, 7200, 6300, 6840, 7200}, + + // Very long preferred lifetime. Client limit it to 4 hours (14400) + {0, 0, 14500, 7200, 11520, 14400}, + + // Very long preferred lifetime. Client limit it to 4 hours (14400) + {2000, 2500, 14500, 2000, 2500, 14400}, + + // Infinite lifetime and T1 and T2. Client limit to 4 hours. + {0xffffffff, 0xffffffff, 0xffffffff, 13500, 14040, 14400}}; + + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Ip6::Prefix adjustedPrefix; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdLifetimeT1AndT2Adjustments()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:13::", 48); + adjustedPrefix = PrefixFromString("2001:13::", 64); + + for (const TestCase &testCase : kTestCases) + { + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Test case: {T1:%lu T2:%lu prf:%lu } --> Expected {T1:%lu T2:%lu prf:%lu}", ToUlong(testCase.mT1), + ToUlong(testCase.mT2), ToUlong(testCase.mPreferredLifetime), ToUlong(testCase.mExpectedT1), + ToUlong(testCase.mExpectedT2), ToUlong(testCase.mExpectedPreferredLifetime)); + + sDhcp6RxMsgs.Clear(); + sInstance->Get().Start(); + + AdvanceTime(2200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = testCase.mT1; + prefixInfo.mT2 = testCase.mT2; + prefixInfo.mPreferredLifetime = testCase.mPreferredLifetime; + prefixInfo.mValidLifetime = testCase.mPreferredLifetime; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr); + + sDhcp6RxMsgs.Clear(); + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + Log("Delegated Prefix -> {T1:%lu T2:%lu prf:%lu}", ToUlong(delegatedPrefix->mT1), ToUlong(delegatedPrefix->mT2), + ToUlong(delegatedPrefix->mPreferredLifetime)); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == testCase.mExpectedT1); + VerifyOrQuit(delegatedPrefix->mT2 == testCase.mExpectedT2); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == testCase.mExpectedPreferredLifetime); + + sInstance->Get().Stop(); + AdvanceTime(100); + } + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdLifetimeT1AndT2Adjustments"); + + FinalizeTest(); +} + +void TestDhcp6PdServerVoidingLeaseDuringRenew(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Ip6::Prefix adjustedPrefix; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerVoidingLeaseDuringRenew()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:cafe:5555::", 48); + adjustedPrefix = PrefixFromString("2001:cafe:5555::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start client, interact with it to successfully delegate a prefix"); + + sInstance->Get().Start(); + + AdvanceTime(2200); + + // Listen for Solicit and send Advertise + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 1800; + prefixInfo.mValidLifetime = 1800; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + // Listen for Request and send Reply + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr); + + sDhcp6RxMsgs.Clear(); + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == 900); + VerifyOrQuit(delegatedPrefix->mT2 == 1440); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == 1800); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait till T1 time for client to renew"); + + AdvanceTime(900 * 1000 + 10); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRenew(prefix, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send a Reply invalidating the previously delegated prefix"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 0; + prefixInfo.mValidLifetime = 0; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix == nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + sInstance->Get().Stop(); + AdvanceTime(5 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerVoidingLeaseDuringRenew"); + + FinalizeTest(); +} + +void TestDhcp6PdServerNotExtendingLeaseDuringRenew(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Ip6::Prefix adjustedPrefix; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerNotExtendingLeaseDuringRenew()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:4567::", 48); + adjustedPrefix = PrefixFromString("2001:4567::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start client, interact with it to successfully delegate a prefix"); + + sInstance->Get().Start(); + + AdvanceTime(2200); + + // Listen for Solicit and send Advertise + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 1800; + prefixInfo.mValidLifetime = 1800; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + // Listen for Request and send Reply + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr); + + sDhcp6RxMsgs.Clear(); + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == 900); + VerifyOrQuit(delegatedPrefix->mT2 == 1440); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == 1800); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait till T1 time for client to renew"); + + AdvanceTime(900 * 1000 + 10); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRenew(prefix, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send a Reply including the previously delegated prefix but with short lifetime"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 100; + prefixInfo.mValidLifetime = 100; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == adjustedPrefix); + VerifyOrQuit(delegatedPrefix->mT1 == 100); + VerifyOrQuit(delegatedPrefix->mT2 == 100); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == 100); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait for the short lifetime to expire and validate that the delegated prefix is removed"); + + AdvanceTime(100 * 1000 + 1); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix == nullptr); + + AdvanceTime(5 * 1000); + + VerifyOrQuit(!sDhcp6RxMsgs.IsEmpty()); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + sInstance->Get().Stop(); + AdvanceTime(5 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerNotExtendingLeaseDuringRenew"); + + FinalizeTest(); +} + +void TestDhcp6PdServerReplacingPrefix(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix1; + Ip6::Prefix prefix2; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerReplacingPrefix()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix1 = PrefixFromString("2001:b2d4:1111::", 64); + prefix1 = PrefixFromString("2001:b2d4:2222::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start client, interact with it to successfully delegate a prefix"); + + sInstance->Get().Start(); + + AdvanceTime(2200); + + // Listen for Solicit and send Advertise + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 1800; + prefixInfo.mValidLifetime = 1800; + prefixInfo.mPrefix = prefix1; + txMsg.AddIaPrefix(prefixInfo); + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + // Listen for Request and send Reply + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix1, serverMacAddr); + + sDhcp6RxMsgs.Clear(); + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.AddIaPrefix(prefixInfo); + txMsg.Send(); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix1); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == prefix1); + VerifyOrQuit(delegatedPrefix->mT1 == 900); + VerifyOrQuit(delegatedPrefix->mT2 == 1440); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == 1800); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Wait till T1 time for client to renew"); + + AdvanceTime(900 * 1000 + 10); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRenew(prefix1, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send a Reply including a new prefix with old one with short lifetime"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 120; + prefixInfo.mValidLifetime = 120; + prefixInfo.mPrefix = prefix1; + txMsg.AddIaPrefix(prefixInfo); + + prefixInfo.mPreferredLifetime = 1800; + prefixInfo.mValidLifetime = 1800; + prefixInfo.mPrefix = prefix2; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + AdvanceTime(1); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix2); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == prefix2); + VerifyOrQuit(delegatedPrefix->mT1 == 900); + VerifyOrQuit(delegatedPrefix->mT2 == 1440); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == 1800); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + sInstance->Get().Stop(); + AdvanceTime(5 * 1000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerReplacingPrefix"); + + FinalizeTest(); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdServerStatusCodeUseMulticast(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Ip6::Prefix adjustedPrefix; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + Ip6::Address serverIp6Addr; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerStatusCodeUseMulticast()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:f57c::", 48); + adjustedPrefix = PrefixFromString("2001:f57c::", 64); + + serverIp6Addr = AddressFromString("fe80::2"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first two Solicit messages"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(2200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisement"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr, &serverIp6Addr); + + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 2000; + prefixInfo.mT2 = 3200; + prefixInfo.mPreferredLifetime = 3600; + prefixInfo.mValidLifetime = 4000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + + txMsg.Send(); + + AdvanceTime(1); + + VerifyOrQuit(sInstance->Get().GetDelegatedPrefix() == nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is received using unicast address of server"); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr, &serverIp6Addr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Reply message with status code UseMulticast"); + + sDhcp6RxMsgs.Clear(); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + txMsg.mHasStatus = true; + txMsg.mStatusCode = Dhcp6::StatusCodeOption::kUseMulticast; + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is sent again now as a multicast"); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr, nullptr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerStatusCodeUseMulticast"); + + FinalizeTest(); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestDhcp6PdServerReplyWithNoBindingToRelease(void) +{ + uint16_t heapAllocations; + Dhcp6TxMsg txMsg; + Dhcp6TxMsg::PrefixInfo prefixInfo; + Ip6::Prefix prefix; + Mac::ExtAddress serverMacAddr; + const DelegatedPrefix *delegatedPrefix; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestDhcp6PdServerReplyWithNoBindingToRelease()"); + + InitTest(); + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + + serverMacAddr.GenerateRandom(); + + prefix = PrefixFromString("2001:8765::", 64); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Start the client and wait for the first two Solicit messages"); + + VerifyOrQuit(!sDhcp6ListeningEnabled); + sInstance->Get().Start(); + VerifyOrQuit(sDhcp6ListeningEnabled); + + AdvanceTime(2200); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[0].ValidateAsSolicit(); + sDhcp6RxMsgs[1].ValidateAsSolicit(); + VerifyOrQuit(sDhcp6RxMsgs[0].mTransactionId == sDhcp6RxMsgs[1].mTransactionId); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Advertisements from server offering three prefixes"); + + txMsg.PrepareAdvertise(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + prefixInfo.mT1 = 0; + prefixInfo.mT2 = 0; + prefixInfo.mPreferredLifetime = 5000; + prefixInfo.mValidLifetime = 5000; + prefixInfo.mPrefix = prefix; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate Request message is received"); + + AdvanceTime(1); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRequest(prefix, serverMacAddr); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send Reply message"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + txMsg.AddIaPrefix(prefixInfo); + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + delegatedPrefix = sInstance->Get().GetDelegatedPrefix(); + VerifyOrQuit(delegatedPrefix != nullptr); + + VerifyOrQuit(delegatedPrefix->mPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mAdjustedPrefix == prefix); + VerifyOrQuit(delegatedPrefix->mPreferredLifetime == prefixInfo.mPreferredLifetime); + + sDhcp6RxMsgs.Clear(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Stop the client to release the prefix"); + + sInstance->Get().Stop(); + + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 1); + sDhcp6RxMsgs[0].ValidateAsRelease(prefix, serverMacAddr); + VerifyOrQuit(sDhcp6RxMsgs[0].mElapsedTime == 0); + + AdvanceTime(1200); + VerifyOrQuit(sDhcp6RxMsgs.GetLength() == 2); + sDhcp6RxMsgs[1].ValidateAsRelease(prefix, serverMacAddr); + VerifyOrQuit(sDhcp6RxMsgs[1].mTransactionId == sDhcp6RxMsgs[0].mTransactionId); + VerifyOrQuit(sDhcp6RxMsgs[1].mElapsedTime > 0); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Send a Reply to the Release with status code NoBinding, validate that it is accepted"); + + txMsg.PrepareReply(sDhcp6RxMsgs[0], serverMacAddr); + prefixInfo.mIaid = sDhcp6RxMsgs[0].mIaPds[0].mIaid; + txMsg.AddIaPrefix(prefixInfo); + + txMsg.mIaPds[0].mHasStatus = true; + txMsg.mIaPds[0].mStatusCode = Dhcp6::StatusCodeOption::kNoBinding; + + sDhcp6RxMsgs.Clear(); + txMsg.Send(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + Log("Validate that the client accepts the Reply and there are no more retries of Release"); + + AdvanceTime(30 * 1000); + VerifyOrQuit(sDhcp6RxMsgs.IsEmpty()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestDhcp6PdServerReplyWithNoBindingToRelease()"); + + FinalizeTest(); +} + +#endif // OT_CONFIG_DHCP6_PD_CLIENT_ENABLE + +} // namespace ot + +int main(void) +{ +#if OT_CONFIG_DHCP6_PD_CLIENT_ENABLE + ot::TestDhcp6PdPrefixDelegation(/* aShortPrefix */ false, /* aAddServerUnicastOption */ false); + ot::TestDhcp6PdPrefixDelegation(/* aShortPrefix */ false, /* aAddServerUnicastOption */ true); + ot::TestDhcp6PdPrefixDelegation(/* aShortPrefix */ true, /* aAddServerUnicastOption */ false); + ot::TestDhcp6PdPrefixDelegation(/* aShortPrefix */ true, /* aAddServerUnicastOption */ true); + ot::TestDhcp6PdSolicitRetries(); + ot::TestDhcp6PdRequestRetries(); + ot::TestDhcp6PdSelectBetweenMultipleServers(); + ot::TestDhcp6PdServerWithMaxPreferrence(); + ot::TestDhcp6PdServerOfferingMultiplePrefixes(); + ot::TestDhcp6PdInvalidOrUnusablePrefix(); + ot::TestDhcp6PdLifetimeT1AndT2Adjustments(); + ot::TestDhcp6PdServerVoidingLeaseDuringRenew(); + ot::TestDhcp6PdServerNotExtendingLeaseDuringRenew(); + ot::TestDhcp6PdServerReplacingPrefix(); + ot::TestDhcp6PdServerStatusCodeUseMulticast(); + ot::TestDhcp6PdServerReplyWithNoBindingToRelease(); + + printf("All tests passed\n"); +#else + printf("DHCP6_PD_CLIENT_ENABLE feature is not enabled\n"); +#endif + + return 0; +} diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp index ebe2e821c..4fb5a1f38 100644 --- a/tests/unit/test_platform.cpp +++ b/tests/unit/test_platform.cpp @@ -479,6 +479,7 @@ OT_TOOL_WEAK otLinkMetrics otPlatRadioGetEnhAckProbingMetrics(otInstance *, cons #endif #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE + OT_TOOL_WEAK bool otPlatInfraIfHasAddress(uint32_t, const otIp6Address *) { return false; } OT_TOOL_WEAK otError otPlatInfraIfSendIcmp6Nd(uint32_t, const otIp6Address *, const uint8_t *, uint16_t) @@ -487,7 +488,31 @@ OT_TOOL_WEAK otError otPlatInfraIfSendIcmp6Nd(uint32_t, const otIp6Address *, co } OT_TOOL_WEAK otError otPlatInfraIfDiscoverNat64Prefix(uint32_t) { return OT_ERROR_FAILED; } -#endif + +#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE + +OT_TOOL_WEAK void otPlatInfraIfDhcp6PdClientSetListeningEnabled(otInstance *aInstance, + bool aEnable, + uint32_t aInfraIfIndex) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aEnable); + OT_UNUSED_VARIABLE(aInfraIfIndex); +} + +OT_TOOL_WEAK void otPlatInfraIfDhcp6PdClientSend(otInstance *aInstance, + otMessage *aMessage, + otIp6Address *aDestAddress, + uint32_t aInfraIfIndex) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aDestAddress); + OT_UNUSED_VARIABLE(aInfraIfIndex); + otMessageFree(aMessage); +} + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE