/* * * Copyright (c) 2020-2021 Project CHIP Authors * * 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. */ #pragma once #include #include #include #include #include #include namespace chip { namespace Transport { inline constexpr uint16_t kMaxSessionID = UINT16_MAX; inline constexpr uint16_t kUnsecuredSessionId = 0; /** * Handles a set of sessions. * * Intended for: * - handle session active time and expiration * - allocate and free space for sessions. */ class SecureSessionTable { public: ~SecureSessionTable() { mEntries.ReleaseAll(); } void Init() { mNextSessionId = chip::Crypto::GetRandU16(); } /** * Allocate a new secure session out of the internal resource pool. * * @param secureSessionType secure session type * @param localSessionId unique identifier for the local node's secure unicast session context * @param localNodeId represents the local Node ID for this node * @param peerNodeId represents peer Node's ID * @param peerCATs represents peer CASE Authenticated Tags * @param peerSessionId represents the encryption key ID assigned by peer node * @param fabricIndex represents fabric index for the session * @param config represents the reliable message protocol configuration * * @note the newly created state will have an 'active' time set based on the current time source. * * @returns CHIP_NO_ERROR if state could be initialized. May fail if maximum session count * has been reached (with CHIP_ERROR_NO_MEMORY). */ CHECK_RETURN_VALUE Optional CreateNewSecureSessionForTest(SecureSession::Type secureSessionType, uint16_t localSessionId, NodeId localNodeId, NodeId peerNodeId, CATValues peerCATs, uint16_t peerSessionId, FabricIndex fabricIndex, const ReliableMessageProtocolConfig & config); /** * Allocate a new secure session out of the internal resource pool with a * non-colliding session ID and increments mNextSessionId to give a clue to * the allocator for the next allocation. The secure session session will * not become active until the call to SecureSession::Activate. * * @returns allocated session, or NullOptional on failure */ CHECK_RETURN_VALUE Optional CreateNewSecureSession(SecureSession::Type secureSessionType, ScopedNodeId sessionEvictionHint); void ReleaseSession(SecureSession * session) { mEntries.ReleaseObject(session); } template Loop ForEachSession(Function && function) { return mEntries.ForEachActiveObject(std::forward(function)); } /** * Get a secure session given its session ID. * * @param localSessionId the identifier of a secure unicast session context within the local node * * @return the session if found, NullOptional if not found */ CHECK_RETURN_VALUE Optional FindSecureSessionByLocalKey(uint16_t localSessionId); // Select SessionHolders which are pointing to a session with the same peer as the given session. Shift them to the given // session. // This is an internal API, using raw pointer to a session is allowed here. void NewerSessionAvailable(SecureSession * session) { VerifyOrDie(session->GetSecureSessionType() == SecureSession::Type::kCASE); mEntries.ForEachActiveObject([&](SecureSession * oldSession) { if (session == oldSession) return Loop::Continue; SessionHandle ref(*oldSession); // This will give all SessionHolders pointing to oldSession a chance to switch to the provided session // // See documentation for SessionDelegate::GetNewSessionHandlingPolicy about how session auto-shifting works, and how // to disable it for a specific SessionHolder in a specific scenario. if (oldSession->GetSecureSessionType() == SecureSession::Type::kCASE && oldSession->GetPeer() == session->GetPeer() && oldSession->GetPeerCATs() == session->GetPeerCATs()) { oldSession->NewerSessionAvailable(SessionHandle(*session)); } return Loop::Continue; }); } private: friend class TestSecureSessionTable; /** * This provides a sortable wrapper for a SecureSession object. A SecureSession * isn't directly sortable since it is not swappable (i.e meet criteria for ValueSwappable). * * However, this wrapper has a stable pointer to a SecureSession while being swappable with * another instance of it. * */ struct SortableSession { public: void swap(SortableSession & other) { SortableSession tmp(other); other.mSession = mSession; mSession = tmp.mSession; } const Transport::SecureSession * operator->() const { return mSession; } auto GetNumMatchingOnFabric() { return mNumMatchingOnFabric; } auto GetNumMatchingOnPeer() { return mNumMatchingOnPeer; } private: SecureSession * mSession; uint16_t mNumMatchingOnFabric; uint16_t mNumMatchingOnPeer; static_assert(CHIP_CONFIG_SECURE_SESSION_POOL_SIZE <= std::numeric_limits::max(), "mNumMatchingOnFabric must be able to count up to CHIP_CONFIG_SECURE_SESSION_POOL_SIZE!"); static_assert(CHIP_CONFIG_SECURE_SESSION_POOL_SIZE <= std::numeric_limits::max(), "mNumMatchingOnPeer must be able to count up to CHIP_CONFIG_SECURE_SESSION_POOL_SIZE!"); friend class SecureSessionTable; }; /** * * Encapsulates all the necessary context for an eviction policy callback * to implement its specific policy. The context is provided to the callee * with the expectation that it'll call Sort() with a comparator function provided * to get the list of sessions sorted in the desired order. * */ class EvictionPolicyContext { public: /* * Called by the policy implementor to sort the list of sessions given a comparator * function. The provided function shall have the following signature: * * bool CompareFunc(const SortableSession &a, const SortableSession &b); * * If a is a better candidate than b, true should be returned. Else, return false. * * NOTE: Sort() can be called multiple times. * */ template void Sort(CompareFunc func) { Sorting::InsertionSort(mSessionList.begin(), mSessionList.size(), func); } const ScopedNodeId & GetSessionEvictionHint() const { return mSessionEvictionHint; } private: EvictionPolicyContext(Span sessionList, ScopedNodeId sessionEvictionHint) { mSessionList = sessionList; mSessionEvictionHint = sessionEvictionHint; } friend class SecureSessionTable; Span mSessionList; ScopedNodeId mSessionEvictionHint; }; /** * * This implements an eviction policy by sorting sessions using the following sorting keys and selecting * the session that is most ahead as the best candidate for eviction: * * - Key1: Sessions on fabrics that have more sessions in the table are placed ahead of sessions on fabrics * with fewer sessions. We conclusively know that if a particular fabric has more sessions in the table * than another, then that fabric is definitely over minimas (assuming a minimally sized session table * conformant to spec minimas). * * Key2: Sessions that match the eviction hint's fabric are placed ahead of those that don't. This ensures that * if Key1 is even (i.e two fabrics are tied in count), that you attempt to select sessions that match * the eviction hint's fabric to ensure we evict sessions within the fabric that a new session might be about * to be created within. This is essential to preventing cross-fabric denial of service possibilities. * * Key3: Sessions with a higher mNumMatchingOnPeer are placed ahead of those with a lower one. This ensures * we pick sessions that have a higher number of duplicated sessions to a peer over those with lower since * evicting a duplicated session will have less of an impact to that peer. * * Key4: Sessions whose target peer's ScopedNodeId matches the eviction hint are placed ahead of those who don't. This * ensures that all things equal, a session that already exists to the peer is refreshed ahead of another to another peer. * * Key5: Sessions that are in defunct state are placed ahead of those in the active state, ahead of any other state. * This ensures that we prioritize evicting defunct sessions (since they have been deemed non-functional anyways) * over active, healthy ones, over those are currently in the process of establishment. * * Key6: Sessions that have a less recent activity time are placed ahead of those with a more recent activity time. This * is the canonical sorting criteria for basic LRU. * */ void DefaultEvictionPolicy(EvictionPolicyContext & evictionContext); /** * * Evicts a session from the session table using the DefaultEvictionPolicy implementation. * */ SecureSession * EvictAndAllocate(uint16_t localSessionId, SecureSession::Type secureSessionType, const ScopedNodeId & sessionEvictionHint); /** * Find an available session ID that is unused in the secure session table. * * The search algorithm iterates over the session ID space in the outer loop * and the session table in the inner loop to locate an available session ID * from the starting mNextSessionId clue. * * The outer-loop considers 64 session IDs in each iteration to give a * runtime complexity of O(CHIP_CONFIG_PEER_CONNECTION_POOL_SIZE^2/64). Speed up could be * achieved with a sorted session table or additional storage. * * @return an unused session ID if any is found, else NullOptional */ CHECK_RETURN_VALUE Optional FindUnusedSessionId(); bool mRunningEvictionLogic = false; ObjectPool mEntries; size_t GetMaxSessionTableSize() const { #if CONFIG_BUILD_FOR_HOST_UNIT_TEST return mMaxSessionTableSize; #else return CHIP_CONFIG_SECURE_SESSION_POOL_SIZE; #endif } #if CONFIG_BUILD_FOR_HOST_UNIT_TEST size_t mMaxSessionTableSize = CHIP_CONFIG_SECURE_SESSION_POOL_SIZE; void SetMaxSessionTableSize(size_t size) { mMaxSessionTableSize = size; } #endif uint16_t mNextSessionId = 0; }; } // namespace Transport } // namespace chip