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