blob: 52cac4cf7936bfe36558f9bb42bc4833620eb8c4 [file] [log] [blame]
/*
* Copyright (c) 2016, 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 the Joiner role.
*/
#include "joiner.hpp"
#if OPENTHREAD_CONFIG_JOINER_ENABLE
#include <stdio.h>
#include "common/as_core_type.hpp"
#include "common/code_utils.hpp"
#include "common/debug.hpp"
#include "common/encoding.hpp"
#include "common/instance.hpp"
#include "common/locator_getters.hpp"
#include "common/logging.hpp"
#include "common/string.hpp"
#include "meshcop/meshcop.hpp"
#include "radio/radio.hpp"
#include "thread/thread_netif.hpp"
#include "thread/uri_paths.hpp"
#include "utils/otns.hpp"
namespace ot {
namespace MeshCoP {
Joiner::Joiner(Instance &aInstance)
: InstanceLocator(aInstance)
, mId()
, mDiscerner()
, mState(kStateIdle)
, mCallback(nullptr)
, mContext(nullptr)
, mJoinerRouterIndex(0)
, mFinalizeMessage(nullptr)
, mTimer(aInstance, Joiner::HandleTimer)
, mJoinerEntrust(UriPath::kJoinerEntrust, &Joiner::HandleJoinerEntrust, this)
{
SetIdFromIeeeEui64();
mDiscerner.Clear();
memset(mJoinerRouters, 0, sizeof(mJoinerRouters));
Get<Tmf::Agent>().AddResource(mJoinerEntrust);
}
void Joiner::SetIdFromIeeeEui64(void)
{
Mac::ExtAddress eui64;
Get<Radio>().GetIeeeEui64(eui64);
ComputeJoinerId(eui64, mId);
}
const JoinerDiscerner *Joiner::GetDiscerner(void) const
{
return mDiscerner.IsEmpty() ? nullptr : &mDiscerner;
}
Error Joiner::SetDiscerner(const JoinerDiscerner &aDiscerner)
{
Error error = kErrorNone;
VerifyOrExit(aDiscerner.IsValid(), error = kErrorInvalidArgs);
VerifyOrExit(mState == kStateIdle, error = kErrorInvalidState);
mDiscerner = aDiscerner;
mDiscerner.GenerateJoinerId(mId);
exit:
return error;
}
Error Joiner::ClearDiscerner(void)
{
Error error = kErrorNone;
VerifyOrExit(mState == kStateIdle, error = kErrorInvalidState);
VerifyOrExit(!mDiscerner.IsEmpty());
mDiscerner.Clear();
SetIdFromIeeeEui64();
exit:
return error;
}
void Joiner::SetState(State aState)
{
State oldState = mState;
OT_UNUSED_VARIABLE(oldState);
SuccessOrExit(Get<Notifier>().Update(mState, aState, kEventJoinerStateChanged));
otLogInfoMeshCoP("JoinerState: %s -> %s", StateToString(oldState), StateToString(aState));
exit:
return;
}
Error Joiner::Start(const char * aPskd,
const char * aProvisioningUrl,
const char * aVendorName,
const char * aVendorModel,
const char * aVendorSwVersion,
const char * aVendorData,
otJoinerCallback aCallback,
void * aContext)
{
Error error;
JoinerPskd joinerPskd;
Mac::ExtAddress randomAddress;
SteeringData::HashBitIndexes filterIndexes;
otLogInfoMeshCoP("Joiner starting");
VerifyOrExit(aProvisioningUrl == nullptr || IsValidUtf8String(aProvisioningUrl), error = kErrorInvalidArgs);
VerifyOrExit(aVendorName == nullptr || IsValidUtf8String(aVendorName), error = kErrorInvalidArgs);
VerifyOrExit(aVendorSwVersion == nullptr || IsValidUtf8String(aVendorSwVersion), error = kErrorInvalidArgs);
VerifyOrExit(mState == kStateIdle, error = kErrorBusy);
VerifyOrExit(Get<ThreadNetif>().IsUp() && Get<Mle::Mle>().GetRole() == Mle::kRoleDisabled,
error = kErrorInvalidState);
SuccessOrExit(error = joinerPskd.SetFrom(aPskd));
// Use random-generated extended address.
randomAddress.GenerateRandom();
Get<Mac::Mac>().SetExtAddress(randomAddress);
Get<Mle::MleRouter>().UpdateLinkLocalAddress();
SuccessOrExit(error = Get<Coap::CoapSecure>().Start(kJoinerUdpPort));
Get<Coap::CoapSecure>().SetPsk(joinerPskd);
for (JoinerRouter &router : mJoinerRouters)
{
router.mPriority = 0; // Priority zero means entry is not in-use.
}
SuccessOrExit(error = PrepareJoinerFinalizeMessage(aProvisioningUrl, aVendorName, aVendorModel, aVendorSwVersion,
aVendorData));
if (!mDiscerner.IsEmpty())
{
SteeringData::CalculateHashBitIndexes(mDiscerner, filterIndexes);
}
else
{
SteeringData::CalculateHashBitIndexes(mId, filterIndexes);
}
SuccessOrExit(error = Get<Mle::DiscoverScanner>().Discover(Mac::ChannelMask(0), Get<Mac::Mac>().GetPanId(),
/* aJoiner */ true, /* aEnableFiltering */ true,
&filterIndexes, HandleDiscoverResult, this));
mCallback = aCallback;
mContext = aContext;
SetState(kStateDiscover);
exit:
if (error != kErrorNone)
{
FreeJoinerFinalizeMessage();
}
LogError("start joiner", error);
return error;
}
void Joiner::Stop(void)
{
otLogInfoMeshCoP("Joiner stopped");
// Callback is set to `nullptr` to skip calling it from `Finish()`
mCallback = nullptr;
Finish(kErrorAbort);
}
void Joiner::Finish(Error aError)
{
switch (mState)
{
case kStateIdle:
ExitNow();
case kStateConnect:
case kStateConnected:
case kStateEntrust:
case kStateJoined:
Get<Coap::CoapSecure>().Disconnect();
IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
mTimer.Stop();
OT_FALL_THROUGH;
case kStateDiscover:
Get<Coap::CoapSecure>().Stop();
break;
}
SetState(kStateIdle);
FreeJoinerFinalizeMessage();
if (mCallback)
{
mCallback(aError, mContext);
}
exit:
return;
}
uint8_t Joiner::CalculatePriority(int8_t aRssi, bool aSteeringDataAllowsAny)
{
int16_t priority;
if (aRssi == OT_RADIO_RSSI_INVALID)
{
aRssi = -127;
}
// Limit the RSSI to range (-128, 0), i.e. -128 < aRssi < 0.
if (aRssi <= -128)
{
priority = -127;
}
else if (aRssi >= 0)
{
priority = -1;
}
else
{
priority = aRssi;
}
// Assign higher priority to networks with an exact match of Joiner
// ID in the Steering Data (128 < priority < 256) compared to ones
// that allow all Joiners (0 < priority < 128). Sub-prioritize
// based on signal strength. Priority 0 is reserved for unused
// entry.
priority += aSteeringDataAllowsAny ? 128 : 256;
return static_cast<uint8_t>(priority);
}
void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult, void *aContext)
{
static_cast<Joiner *>(aContext)->HandleDiscoverResult(aResult);
}
void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult)
{
VerifyOrExit(mState == kStateDiscover);
if (aResult != nullptr)
{
SaveDiscoveredJoinerRouter(*aResult);
}
else
{
Get<Mac::Mac>().SetExtAddress(mId);
Get<Mle::MleRouter>().UpdateLinkLocalAddress();
mJoinerRouterIndex = 0;
TryNextJoinerRouter(kErrorNone);
}
exit:
return;
}
void Joiner::SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult &aResult)
{
uint8_t priority;
bool doesAllowAny;
JoinerRouter *end = OT_ARRAY_END(mJoinerRouters);
JoinerRouter *entry;
doesAllowAny = AsCoreType(&aResult.mSteeringData).PermitsAllJoiners();
otLogInfoMeshCoP("Joiner discover network: %s, pan:0x%04x, port:%d, chan:%d, rssi:%d, allow-any:%s",
AsCoreType(&aResult.mExtAddress).ToString().AsCString(), aResult.mPanId, aResult.mJoinerUdpPort,
aResult.mChannel, aResult.mRssi, ToYesNo(doesAllowAny));
priority = CalculatePriority(aResult.mRssi, doesAllowAny);
// We keep the list sorted based on priority. Find the place to
// add the new result.
for (entry = &mJoinerRouters[0]; entry < end; entry++)
{
if (priority > entry->mPriority)
{
break;
}
}
VerifyOrExit(entry < end);
// Shift elements in array to make room for the new one.
memmove(entry + 1, entry,
static_cast<size_t>(reinterpret_cast<uint8_t *>(end - 1) - reinterpret_cast<uint8_t *>(entry)));
entry->mExtAddr = AsCoreType(&aResult.mExtAddress);
entry->mPanId = aResult.mPanId;
entry->mJoinerUdpPort = aResult.mJoinerUdpPort;
entry->mChannel = aResult.mChannel;
entry->mPriority = priority;
exit:
return;
}
void Joiner::TryNextJoinerRouter(Error aPrevError)
{
for (; mJoinerRouterIndex < OT_ARRAY_LENGTH(mJoinerRouters); mJoinerRouterIndex++)
{
JoinerRouter &router = mJoinerRouters[mJoinerRouterIndex];
Error error;
if (router.mPriority == 0)
{
break;
}
error = Connect(router);
VerifyOrExit(error != kErrorNone, mJoinerRouterIndex++);
// Save the error from `Connect` only if there is no previous
// error from earlier attempts. This ensures that if there has
// been a previous Joiner Router connect attempt where
// `Connect()` call itself was successful, the error status
// emitted from `Finish()` call corresponds to the error from
// that attempt.
if (aPrevError == kErrorNone)
{
aPrevError = error;
}
}
if (aPrevError == kErrorNone)
{
aPrevError = kErrorNotFound;
}
Finish(aPrevError);
exit:
return;
}
Error Joiner::Connect(JoinerRouter &aRouter)
{
Error error = kErrorNotFound;
Ip6::SockAddr sockAddr(aRouter.mJoinerUdpPort);
otLogInfoMeshCoP("Joiner connecting to %s, pan:0x%04x, chan:%d", aRouter.mExtAddr.ToString().AsCString(),
aRouter.mPanId, aRouter.mChannel);
Get<Mac::Mac>().SetPanId(aRouter.mPanId);
SuccessOrExit(error = Get<Mac::Mac>().SetPanChannel(aRouter.mChannel));
SuccessOrExit(error = Get<Ip6::Filter>().AddUnsecurePort(kJoinerUdpPort));
sockAddr.GetAddress().SetToLinkLocalAddress(aRouter.mExtAddr);
SuccessOrExit(error = Get<Coap::CoapSecure>().Connect(sockAddr, Joiner::HandleSecureCoapClientConnect, this));
SetState(kStateConnect);
exit:
LogError("start secure joiner connection", error);
return error;
}
void Joiner::HandleSecureCoapClientConnect(bool aConnected, void *aContext)
{
static_cast<Joiner *>(aContext)->HandleSecureCoapClientConnect(aConnected);
}
void Joiner::HandleSecureCoapClientConnect(bool aConnected)
{
VerifyOrExit(mState == kStateConnect);
if (aConnected)
{
SetState(kStateConnected);
SendJoinerFinalize();
mTimer.Start(kReponseTimeout);
}
else
{
TryNextJoinerRouter(kErrorSecurity);
}
exit:
return;
}
Error Joiner::PrepareJoinerFinalizeMessage(const char *aProvisioningUrl,
const char *aVendorName,
const char *aVendorModel,
const char *aVendorSwVersion,
const char *aVendorData)
{
Error error = kErrorNone;
VendorNameTlv vendorNameTlv;
VendorModelTlv vendorModelTlv;
VendorSwVersionTlv vendorSwVersionTlv;
VendorStackVersionTlv vendorStackVersionTlv;
ProvisioningUrlTlv provisioningUrlTlv;
VerifyOrExit((mFinalizeMessage = Get<Coap::CoapSecure>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
mFinalizeMessage->InitAsConfirmablePost();
SuccessOrExit(error = mFinalizeMessage->AppendUriPathOptions(UriPath::kJoinerFinalize));
SuccessOrExit(error = mFinalizeMessage->SetPayloadMarker());
mFinalizeMessage->SetOffset(mFinalizeMessage->GetLength());
SuccessOrExit(error = Tlv::Append<StateTlv>(*mFinalizeMessage, StateTlv::kAccept));
vendorNameTlv.Init();
vendorNameTlv.SetVendorName(aVendorName);
SuccessOrExit(error = vendorNameTlv.AppendTo(*mFinalizeMessage));
vendorModelTlv.Init();
vendorModelTlv.SetVendorModel(aVendorModel);
SuccessOrExit(error = vendorModelTlv.AppendTo(*mFinalizeMessage));
vendorSwVersionTlv.Init();
vendorSwVersionTlv.SetVendorSwVersion(aVendorSwVersion);
SuccessOrExit(error = vendorSwVersionTlv.AppendTo(*mFinalizeMessage));
vendorStackVersionTlv.Init();
vendorStackVersionTlv.SetOui(OPENTHREAD_CONFIG_STACK_VENDOR_OUI);
vendorStackVersionTlv.SetMajor(OPENTHREAD_CONFIG_STACK_VERSION_MAJOR);
vendorStackVersionTlv.SetMinor(OPENTHREAD_CONFIG_STACK_VERSION_MINOR);
vendorStackVersionTlv.SetRevision(OPENTHREAD_CONFIG_STACK_VERSION_REV);
SuccessOrExit(error = vendorStackVersionTlv.AppendTo(*mFinalizeMessage));
if (aVendorData != nullptr)
{
VendorDataTlv vendorDataTlv;
vendorDataTlv.Init();
vendorDataTlv.SetVendorData(aVendorData);
SuccessOrExit(error = vendorDataTlv.AppendTo(*mFinalizeMessage));
}
provisioningUrlTlv.Init();
provisioningUrlTlv.SetProvisioningUrl(aProvisioningUrl);
if (provisioningUrlTlv.GetLength() > 0)
{
SuccessOrExit(error = provisioningUrlTlv.AppendTo(*mFinalizeMessage));
}
exit:
if (error != kErrorNone)
{
FreeJoinerFinalizeMessage();
}
return error;
}
void Joiner::FreeJoinerFinalizeMessage(void)
{
VerifyOrExit(mState == kStateIdle && mFinalizeMessage != nullptr);
mFinalizeMessage->Free();
mFinalizeMessage = nullptr;
exit:
return;
}
void Joiner::SendJoinerFinalize(void)
{
OT_ASSERT(mFinalizeMessage != nullptr);
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
LogCertMessage("[THCI] direction=send | type=JOIN_FIN.req |", *mFinalizeMessage);
#endif
SuccessOrExit(Get<Coap::CoapSecure>().SendMessage(*mFinalizeMessage, Joiner::HandleJoinerFinalizeResponse, this));
mFinalizeMessage = nullptr;
otLogInfoMeshCoP("Joiner sent finalize");
exit:
return;
}
void Joiner::HandleJoinerFinalizeResponse(void * aContext,
otMessage * aMessage,
const otMessageInfo *aMessageInfo,
Error aResult)
{
static_cast<Joiner *>(aContext)->HandleJoinerFinalizeResponse(AsCoapMessagePtr(aMessage), &AsCoreType(aMessageInfo),
aResult);
}
void Joiner::HandleJoinerFinalizeResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult)
{
OT_UNUSED_VARIABLE(aMessageInfo);
uint8_t state;
VerifyOrExit(mState == kStateConnected && aResult == kErrorNone);
OT_ASSERT(aMessage != nullptr);
VerifyOrExit(aMessage->IsAck() && aMessage->GetCode() == Coap::kCodeChanged);
SuccessOrExit(Tlv::Find<StateTlv>(*aMessage, state));
SetState(kStateEntrust);
mTimer.Start(kReponseTimeout);
otLogInfoMeshCoP("Joiner received finalize response %d", state);
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
LogCertMessage("[THCI] direction=recv | type=JOIN_FIN.rsp |", *aMessage);
#endif
exit:
Get<Coap::CoapSecure>().Disconnect();
IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
}
void Joiner::HandleJoinerEntrust(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
static_cast<Joiner *>(aContext)->HandleJoinerEntrust(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
}
void Joiner::HandleJoinerEntrust(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
Error error;
Dataset::Info datasetInfo;
VerifyOrExit(mState == kStateEntrust && aMessage.IsConfirmablePostRequest(), error = kErrorDrop);
otLogInfoMeshCoP("Joiner received entrust");
otLogCertMeshCoP("[THCI] direction=recv | type=JOIN_ENT.ntf");
datasetInfo.Clear();
SuccessOrExit(error = Tlv::Find<NetworkKeyTlv>(aMessage, datasetInfo.UpdateNetworkKey()));
datasetInfo.SetChannel(Get<Mac::Mac>().GetPanChannel());
datasetInfo.SetPanId(Get<Mac::Mac>().GetPanId());
IgnoreError(Get<MeshCoP::ActiveDataset>().Save(datasetInfo));
otLogInfoMeshCoP("Joiner successful!");
SendJoinerEntrustResponse(aMessage, aMessageInfo);
// Delay extended address configuration to allow DTLS wrap up.
mTimer.Start(kConfigExtAddressDelay);
exit:
LogError("process joiner entrust", error);
}
void Joiner::SendJoinerEntrustResponse(const Coap::Message &aRequest, const Ip6::MessageInfo &aRequestInfo)
{
Error error = kErrorNone;
Coap::Message * message;
Ip6::MessageInfo responseInfo(aRequestInfo);
VerifyOrExit((message = Get<Tmf::Agent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
SuccessOrExit(error = message->SetDefaultResponseHeader(aRequest));
message->SetSubType(Message::kSubTypeJoinerEntrust);
responseInfo.GetSockAddr().Clear();
SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, responseInfo));
SetState(kStateJoined);
otLogInfoMeshCoP("Joiner sent entrust response");
otLogCertMeshCoP("[THCI] direction=send | type=JOIN_ENT.rsp");
exit:
FreeMessageOnError(message, error);
}
void Joiner::HandleTimer(Timer &aTimer)
{
aTimer.Get<Joiner>().HandleTimer();
}
void Joiner::HandleTimer(void)
{
Error error = kErrorNone;
switch (mState)
{
case kStateIdle:
case kStateDiscover:
case kStateConnect:
OT_ASSERT(false);
OT_UNREACHABLE_CODE(break);
case kStateConnected:
case kStateEntrust:
error = kErrorResponseTimeout;
break;
case kStateJoined:
Mac::ExtAddress extAddress;
extAddress.GenerateRandom();
Get<Mac::Mac>().SetExtAddress(extAddress);
Get<Mle::MleRouter>().UpdateLinkLocalAddress();
error = kErrorNone;
break;
}
Finish(error);
}
// LCOV_EXCL_START
const char *Joiner::StateToString(State aState)
{
static const char *const kStateStrings[] = {
"Idle", // (0) kStateIdle
"Discover", // (1) kStateDiscover
"Connecting", // (2) kStateConnect
"Connected", // (3) kStateConnected
"Entrust", // (4) kStateEntrust
"Joined", // (5) kStateJoined
};
static_assert(kStateIdle == 0, "kStateIdle value is incorrect");
static_assert(kStateDiscover == 1, "kStateDiscover value is incorrect");
static_assert(kStateConnect == 2, "kStateConnect value is incorrect");
static_assert(kStateConnected == 3, "kStateConnected value is incorrect");
static_assert(kStateEntrust == 4, "kStateEntrust value is incorrect");
static_assert(kStateJoined == 5, "kStateJoined value is incorrect");
return kStateStrings[aState];
}
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
void Joiner::LogCertMessage(const char *aText, const Coap::Message &aMessage) const
{
uint8_t buf[OPENTHREAD_CONFIG_MESSAGE_BUFFER_SIZE];
VerifyOrExit(aMessage.GetLength() <= sizeof(buf));
aMessage.ReadBytes(aMessage.GetOffset(), buf, aMessage.GetLength() - aMessage.GetOffset());
otDumpCertMeshCoP(aText, buf, aMessage.GetLength() - aMessage.GetOffset());
exit:
return;
}
#endif
// LCOV_EXCL_STOP
} // namespace MeshCoP
} // namespace ot
#endif // OPENTHREAD_CONFIG_JOINER_ENABLE