/* * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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(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 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 * onConnection, Callback::Callback * 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 * onConnection, Callback::Callback * 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 * 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 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 * onConnection, Callback::Callback * onFailure, Callback::Callback * onSetupFailure, TransportPayloadCapability transportPayloadCapability = TransportPayloadCapability::kMRPPayload); void EnqueueConnectionCallbacks(Callback::Callback * onConnection, Callback::Callback * onFailure, Callback::Callback * 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 & 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