blob: d5af559aec0a2a7485508deff7f01abd85be3d83 [file] [log] [blame]
/*
*
* Copyright (c) 2020-2021 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file
* This file contains definitions for Device class. The objects of this
* class will be used by Controller applications to interact with CHIP
* devices. The class provides mechanism to construct, send and receive
* messages to and from the corresponding CHIP devices.
*/
#pragma once
#include <app/AppConfig.h>
#include <app/CASEClient.h>
#include <app/CASEClientPool.h>
#include <app/DeviceProxy.h>
#include <app/util/basic-types.h>
#include <credentials/GroupDataProvider.h>
#include <lib/address_resolve/AddressResolve.h>
#include <lib/core/GroupedCallbackList.h>
#include <messaging/ExchangeContext.h>
#include <messaging/ExchangeDelegate.h>
#include <messaging/ExchangeMgr.h>
#include <messaging/Flags.h>
#include <messaging/ReliableMessageProtocolConfig.h>
#include <platform/CHIPDeviceConfig.h>
#include <protocols/secure_channel/CASESession.h>
#include <system/SystemClock.h>
#include <system/SystemLayer.h>
#include <transport/SessionManager.h>
#include <transport/TransportMgr.h>
#include <transport/raw/MessageHeader.h>
#include <transport/raw/UDP.h>
namespace chip {
class OperationalSessionSetup;
/**
* @brief Delegate provided when creating OperationalSessionSetup.
*
* Once OperationalSessionSetup establishes a connection (or errors out) and has notified all
* registered application callbacks via OnDeviceConnected/OnDeviceConnectionFailure, this delegate
* is used to deallocate the OperationalSessionSetup.
*/
class OperationalSessionReleaseDelegate
{
public:
virtual ~OperationalSessionReleaseDelegate() = default;
virtual void ReleaseSession(OperationalSessionSetup * sessionSetup) = 0;
};
/**
* @brief Minimal implementation of DeviceProxy that encapsulates a SessionHolder to track a CASE session.
*
* Deprecated - Avoid using this object.
*
* OperationalDeviceProxy is a minimal implementation of DeviceProxy. It is meant to provide a transition
* for existing consumers of OperationalDeviceProxy that were delivered a reference to that object in
* their respective OnDeviceConnected callback, but were incorrectly holding onto that object past
* the function call. OperationalDeviceProxy can be held on for as long as is desired, while still
* minimizing the code changes needed to transition to a more final solution by virtue of
* implementing DeviceProxy.
*/
class OperationalDeviceProxy : public DeviceProxy
{
public:
OperationalDeviceProxy(Messaging::ExchangeManager * exchangeMgr, const SessionHandle & sessionHandle) :
mExchangeMgr(exchangeMgr), mSecureSession(sessionHandle), mPeerScopedNodeId(sessionHandle->GetPeer())
{}
OperationalDeviceProxy() {}
void Disconnect() override
{
if (IsSecureConnected())
{
GetSecureSession().Value()->AsSecureSession()->MarkAsDefunct();
}
mSecureSession.Release();
mExchangeMgr = nullptr;
mPeerScopedNodeId = ScopedNodeId();
}
Messaging::ExchangeManager * GetExchangeManager() const override { return mExchangeMgr; }
chip::Optional<SessionHandle> GetSecureSession() const override { return mSecureSession.Get(); }
NodeId GetDeviceId() const override { return mPeerScopedNodeId.GetNodeId(); }
ScopedNodeId GetPeerScopedNodeId() const { return mPeerScopedNodeId; }
bool ConnectionReady() const { return (mExchangeMgr != nullptr && IsSecureConnected()); }
private:
bool IsSecureConnected() const override { return static_cast<bool>(mSecureSession); }
Messaging::ExchangeManager * mExchangeMgr = nullptr;
SessionHolder mSecureSession;
ScopedNodeId mPeerScopedNodeId;
};
/**
* @brief Callback prototype when secure session is established.
*
* Callback implementations are not supposed to store the exchangeMgr or the sessionHandle. Older
* application code does incorrectly hold onto this information so do not follow those incorrect
* implementations as an example.
*/
typedef void (*OnDeviceConnected)(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle);
/**
* Callback prototype when secure session establishment fails.
*/
typedef void (*OnDeviceConnectionFailure)(void * context, const ScopedNodeId & peerId, CHIP_ERROR error);
/**
* Callback prototype when secure session establishement has failed and will be
* retried. retryTimeout indicates how much time will pass before we know
* whether the retry has timed out waiting for a response to our Sigma1 message.
*/
typedef void (*OnDeviceConnectionRetry)(void * context, const ScopedNodeId & peerId, CHIP_ERROR error,
System::Clock::Seconds16 retryTimeout);
/**
* Object used to either establish a connection to peer or performing address lookup to a peer.
*
* OperationalSessionSetup is capable of either:
* 1. Establishing a CASE session connection to a peer. Upon success or failure, the OnDeviceConnected or
* OnDeviceConnectionFailure callback will be called to notify the caller the results of trying to
* estblish a CASE session. Some additional details about the steps to establish a connection are:
* - Discover the device using DNSSD (find out what IP address to use and what
* communication parameters are appropriate for it)
* - Establish a secure channel to it via CASE
* - Expose to consumers the secure session for talking to the device via OnDeviceConnected
* callback.
* 2. Performing an address lookup for given a scoped nodeid. On success, it will call into
* SessionManager to update the addresses for all matching sessions in the session table.
*
* OperationalSessionSetup has a very limited lifetime. Once it has completed its purpose outlined above,
* it will use `releaseDelegate` to release itself.
*
* It is possible to determine which of the two purposes the OperationalSessionSetup is for by calling
* IsForAddressUpdate().
*/
class DLL_EXPORT OperationalSessionSetup : public SessionEstablishmentDelegate, public AddressResolve::NodeListener
{
public:
struct ConnectionFailureInfo
{
const ScopedNodeId peerId;
CHIP_ERROR error;
SessionEstablishmentStage sessionStage;
// When the response was BUSY, error will be CHIP_ERROR_BUSY and
// requestedBusyDelay will be set, if handling of BUSY responses is
// enabled.
#if CHIP_CONFIG_ENABLE_BUSY_HANDLING_FOR_OPERATIONAL_SESSION_SETUP
Optional<System::Clock::Milliseconds16> requestedBusyDelay;
#endif // CHIP_CONFIG_ENABLE_BUSY_HANDLING_FOR_OPERATIONAL_SESSION_SETUP
ConnectionFailureInfo(const ScopedNodeId & peer, CHIP_ERROR err, SessionEstablishmentStage stage) :
peerId(peer), error(err), sessionStage(stage)
{}
};
using OnSetupFailure = void (*)(void * context, const ConnectionFailureInfo & failureInfo);
~OperationalSessionSetup() override;
OperationalSessionSetup(const CASEClientInitParams & params, CASEClientPoolDelegate * clientPool, ScopedNodeId peerId,
OperationalSessionReleaseDelegate * releaseDelegate)
{
mInitParams = params;
if (params.Validate() != CHIP_NO_ERROR || clientPool == nullptr || releaseDelegate == nullptr)
{
mState = State::Uninitialized;
return;
}
mClientPool = clientPool;
mPeerId = peerId;
mReleaseDelegate = releaseDelegate;
mState = State::NeedsAddress;
mAddressLookupHandle.SetListener(this);
}
/*
* This function can be called to establish a secure session with the device.
*
* The device is expected to have been commissioned, A CASE session
* setup will be triggered.
*
* If session setup succeeds, the callback function `onConnection` will be called.
* If session setup fails, `onFailure` will be called.
*
* If the session already exists, `onConnection` will be called immediately,
* before the Connect call returns.
*
* `onFailure` may be called before the Connect call returns, for error
* cases that are detected synchronously (e.g. inability to start an address
* lookup).
*
* `transportPayloadCapability` is set to kLargePayload when the session needs to be established
* over a transport that allows large payloads to be transferred, e.g., TCP.
*/
void Connect(Callback::Callback<OnDeviceConnected> * onConnection, Callback::Callback<OnDeviceConnectionFailure> * onFailure,
TransportPayloadCapability transportPayloadCapability = TransportPayloadCapability::kMRPPayload);
/*
* This function can be called to establish a secure session with the device.
*
* The device is expected to have been commissioned, A CASE session
* setup will be triggered.
*
* If session setup succeeds, the callback function `onConnection` will be called.
* If session setup fails, `onSetupFailure` will be called.
*
* If the session already exists, `onConnection` will be called immediately,
* before the Connect call returns.
*
* `onSetupFailure` may be called before the Connect call returns, for error cases that are detected synchronously
* (e.g. inability to start an address lookup).
*
* `transportPayloadCapability` is set to kLargePayload when the session needs to be established
* over a transport that allows large payloads to be transferred, e.g., TCP.
*/
void Connect(Callback::Callback<OnDeviceConnected> * onConnection, Callback::Callback<OnSetupFailure> * onSetupFailure,
TransportPayloadCapability transportPayloadCapability = TransportPayloadCapability::kMRPPayload);
bool IsForAddressUpdate() const { return mPerformingAddressUpdate; }
//////////// SessionEstablishmentDelegate Implementation ///////////////
void OnSessionEstablished(const SessionHandle & session) override;
void OnSessionEstablishmentError(CHIP_ERROR error, SessionEstablishmentStage stage) override;
void OnResponderBusy(System::Clock::Milliseconds16 requestedDelay) override;
ScopedNodeId GetPeerId() const { return mPeerId; }
static Transport::PeerAddress ToPeerAddress(const Dnssd::ResolvedNodeData & nodeData)
{
Inet::InterfaceId interfaceId = Inet::InterfaceId::Null();
// TODO - Revisit usage of InterfaceID only for addresses that are IPv6 LLA
// Only use the DNS-SD resolution's InterfaceID for addresses that are IPv6 LLA.
// For all other addresses, we should rely on the device's routing table to route messages sent.
// Forcing messages down an InterfaceId might fail. For example, in bridged networks like Thread,
// mDNS advertisements are not usually received on the same interface the peer is reachable on.
if (nodeData.resolutionData.ipAddress[0].IsIPv6LinkLocal())
{
interfaceId = nodeData.resolutionData.interfaceId;
}
return Transport::PeerAddress::UDP(nodeData.resolutionData.ipAddress[0], nodeData.resolutionData.port, interfaceId);
}
/**
* @brief Get the fabricIndex
*/
FabricIndex GetFabricIndex() const { return mPeerId.GetFabricIndex(); }
void PerformAddressUpdate();
// AddressResolve::NodeListener - notifications when dnssd finds a node IP address
void OnNodeAddressResolved(const PeerId & peerId, const AddressResolve::ResolveResult & result) override;
void OnNodeAddressResolutionFailed(const PeerId & peerId, CHIP_ERROR reason) override;
#if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES
// Update our remaining attempt count to be at least the given value.
void UpdateAttemptCount(uint8_t attemptCount);
// Add a retry handler for this session setup.
void AddRetryHandler(Callback::Callback<OnDeviceConnectionRetry> * onRetry);
#endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES
private:
enum class State : uint8_t
{
Uninitialized, // Error state: OperationalSessionSetup is useless
NeedsAddress, // No address known, lookup not started yet.
ResolvingAddress, // Address lookup in progress.
HasAddress, // Have an address, CASE handshake not started yet.
Connecting, // CASE handshake in progress.
SecureConnected, // CASE session established.
WaitingForRetry, // No address known, but a retry is pending. Added at
// end to make logs easier to understand.
};
CASEClientInitParams mInitParams;
CASEClientPoolDelegate * mClientPool = nullptr;
// mCASEClient is only non-null if we are in State::Connecting or just
// allocated it as part of an attempt to enter State::Connecting.
CASEClient * mCASEClient = nullptr;
ScopedNodeId mPeerId;
Transport::PeerAddress mDeviceAddress = Transport::PeerAddress::UDP(Inet::IPAddress::Any);
SessionHolder mSecureSession;
typedef Callback::GroupedCallbackList<OnDeviceConnected, OnDeviceConnectionFailure, OnSetupFailure> SuccessFailureCallbackList;
SuccessFailureCallbackList mCallbacks;
OperationalSessionReleaseDelegate * mReleaseDelegate;
/// This is used when a node address is required.
chip::AddressResolve::NodeLookupHandle mAddressLookupHandle;
State mState = State::Uninitialized;
bool mPerformingAddressUpdate = false;
#if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES || CHIP_CONFIG_ENABLE_BUSY_HANDLING_FOR_OPERATIONAL_SESSION_SETUP
System::Clock::Milliseconds16 mRequestedBusyDelay = System::Clock::kZero;
#endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES || CHIP_CONFIG_ENABLE_BUSY_HANDLING_FOR_OPERATIONAL_SESSION_SETUP
TransportPayloadCapability mTransportPayloadCapability = TransportPayloadCapability::kMRPPayload;
#if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES
// When we TryNextResult on the resolver, it will synchronously call back
// into our OnNodeAddressResolved when it succeeds. We need to track
// whether the OnNodeAddressResolved is coming from handling a session
// establishment error or whether it's happening because we didn't even
// manage to start a session establishment at all. Use this member to keep
// track of that.
bool mTryingNextResultDueToSessionEstablishmentError = false;
uint8_t mRemainingAttempts = 0;
uint8_t mAttemptsDone = 0;
uint8_t mResolveAttemptsAllowed = 0;
Callback::CallbackDeque mConnectionRetry;
#endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES
void MoveToState(State aTargetState);
CHIP_ERROR EstablishConnection(const AddressResolve::ResolveResult & result);
/*
* This checks to see if an existing CASE session exists to the peer within the SessionManager
* and if one exists, to load that into mSecureSession.
*
* Returns true if a valid session was found, false otherwise.
*
*/
bool AttachToExistingSecureSession();
void CleanupCASEClient();
void Connect(Callback::Callback<OnDeviceConnected> * onConnection, Callback::Callback<OnDeviceConnectionFailure> * onFailure,
Callback::Callback<OnSetupFailure> * onSetupFailure,
TransportPayloadCapability transportPayloadCapability = TransportPayloadCapability::kMRPPayload);
void EnqueueConnectionCallbacks(Callback::Callback<OnDeviceConnected> * onConnection,
Callback::Callback<OnDeviceConnectionFailure> * onFailure,
Callback::Callback<OnSetupFailure> * onSetupFailure);
enum class ReleaseBehavior
{
Release,
DoNotRelease
};
/*
* This dequeues all failure and success callbacks and appropriately invokes either set depending
* on the value of error.
*
* If error == CHIP_NO_ERROR, only success callbacks are invoked. Otherwise, only failure callbacks are invoked.
*
* The state offers additional context regarding the failure, indicating the specific state in which
* the error occurs. It is only relayed through failure callbacks when the error is not equal to CHIP_NO_ERROR.
*
* If releaseBehavior is Release, this uses mReleaseDelegate to release
* ourselves (aka `this`). As a result any caller should return right away
* without touching `this`.
*
* Setting releaseBehavior to DoNotRelease is meant for use from the destructor
*/
void DequeueConnectionCallbacks(CHIP_ERROR error, SessionEstablishmentStage stage,
ReleaseBehavior releaseBehavior = ReleaseBehavior::Release);
void DequeueConnectionCallbacks(CHIP_ERROR error, ReleaseBehavior releaseBehavior = ReleaseBehavior::Release)
{
this->DequeueConnectionCallbacks(error, SessionEstablishmentStage::kNotInKeyExchange, releaseBehavior);
}
/**
* Helper for DequeueConnectionCallbacks that handles the actual callback
* notifications. This happens after the object has been released, if it's
* being released.
*/
static void NotifyConnectionCallbacks(SuccessFailureCallbackList & ready, CHIP_ERROR error, SessionEstablishmentStage stage,
const ScopedNodeId & peerId, Messaging::ExchangeManager * exchangeMgr,
const Optional<SessionHandle> & optionalSessionHandle,
// requestedBusyDelay will be 0 if not
// CHIP_CONFIG_ENABLE_BUSY_HANDLING_FOR_OPERATIONAL_SESSION_SETUP,
// and only has a meaningful value
// when the error is CHIP_ERROR_BUSY.
System::Clock::Milliseconds16 requestedBusyDelay);
/**
* Triggers a DNSSD lookup to find a usable peer address.
*/
CHIP_ERROR LookupPeerAddress();
/**
* This function will set new IP address, port and MRP retransmission intervals of the device.
*/
void UpdateDeviceData(const AddressResolve::ResolveResult & result);
#if CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES
/**
* Schedule a setup reattempt, if possible. The outparam indicates how long
* it will be before the reattempt happens.
*/
CHIP_ERROR ScheduleSessionSetupReattempt(System::Clock::Seconds16 & timerDelay);
/**
* Cancel a scheduled setup reattempt, if we can (i.e. if we still have
* access to the SystemLayer).
*/
void CancelSessionSetupReattempt();
/**
* Helper for our backoff retry timer.
*/
static void TrySetupAgain(System::Layer * systemLayer, void * state);
/**
* Helper to notify our retry callbacks that a setup error occurred and we
* will retry.
*/
void NotifyRetryHandlers(CHIP_ERROR error, const ReliableMessageProtocolConfig & remoteMrpConfig,
System::Clock::Seconds16 retryDelay);
/**
* A version of NotifyRetryHandlers that passes in a retry timeout estimate
* directly.
*/
void NotifyRetryHandlers(CHIP_ERROR error, System::Clock::Seconds16 timeoutEstimate);
#endif // CHIP_DEVICE_CONFIG_ENABLE_AUTOMATIC_CASE_RETRIES
};
} // namespace chip