From 42f98c9dfab03a4423038884df4f74a7954a3bb6 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Thu, 13 Mar 2025 09:29:19 +0100 Subject: [PATCH] i2p: make a time gap between creating transient sessions and using them Connecting to an I2P peer consists of creating a session (the `SESSION CREATE` command) and then connecting to the peer using that session (`STREAM CONNECT ID=session_id ...`). This change is only relevant for transient sessions because when a persistent session is used it is created once and used for all connections. Before this change Bitcoin Core would create the session and use it in quick succession. That is, the `SESSION CREATE` command would be immediately followed by `STREAM CONNECT`. This could ease network activity monitoring by an adversary. To mitigate that, this change creates sessions upfront without an immediate demand for new sessions and later uses them. This creates a time gap between `SESSION CREATE` and `STREAM CONNECT`. Note that there is always some demand for new I2P connections due to disconnects. --- Summary of the changes in the code: * Create the session from the `Session` constructor (send `SESSION CREATE` to the I2P SAM proxy). This constructor was only called when transient sessions were needed and was immediately followed by `Connect()` which would have created the session. So this is a noop change if viewed in isolation. * Every time we try to connect to any peer (not just I2P) create a new I2P session, up to a limit (currently 10). This way a bunch of sessions are created (`SESSION CREATE`) at times that are decoupled from the time when sessions will be used (`STREAM CONNECT`). * Tweak the code in `CConnman::ConnectNode()` to avoid creating session objects while holding `m_unused_i2p_sessions_mutex` because now creating `i2p::sam::Session` involves network activity and could take a few seconds. --- src/i2p.cpp | 7 +++++++ src/net.cpp | 34 ++++++++++++++++++++++++++++++---- src/net.h | 5 +++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/i2p.cpp b/src/i2p.cpp index 0420bc92382..6dd723083ae 100644 --- a/src/i2p.cpp +++ b/src/i2p.cpp @@ -131,6 +131,13 @@ Session::Session(const Proxy& control_host, CThreadInterrupt* interrupt) m_interrupt{interrupt}, m_transient{true} { + try { + LOCK(m_mutex); + CreateIfNotCreatedAlready(); + } catch (const std::runtime_error&) { + // This was just an eager optimistic attempt to create the session. + // If it fails, then it will be reattempted again when `Connect()` is called. + } } Session::~Session() diff --git a/src/net.cpp b/src/net.cpp index 735985a8414..369d498410b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -455,6 +455,8 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo connect_to.push_back(addrConnect); } + AddI2PSessionsIfNeeded(); + // Connect std::unique_ptr sock; Proxy proxy; @@ -474,16 +476,18 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo if (m_i2p_sam_session) { connected = m_i2p_sam_session->Connect(target_addr, conn, proxyConnectionFailed); } else { + // Use an existent transient session, if one was created earlier... { LOCK(m_unused_i2p_sessions_mutex); - if (m_unused_i2p_sessions.empty()) { - i2p_transient_session = - std::make_unique(proxy, &interruptNet); - } else { + if (!m_unused_i2p_sessions.empty()) { i2p_transient_session.swap(m_unused_i2p_sessions.front()); m_unused_i2p_sessions.pop(); } } + // ... or create a new one if m_unused_i2p_sessions was empty. + if (!i2p_transient_session) { + i2p_transient_session = std::make_unique(proxy, &interruptNet); + } connected = i2p_transient_session->Connect(target_addr, conn, proxyConnectionFailed); if (!connected) { LOCK(m_unused_i2p_sessions_mutex); @@ -3237,6 +3241,28 @@ uint16_t CConnman::GetDefaultPort(const std::string& addr) const return a.SetSpecial(addr) ? GetDefaultPort(a.GetNetwork()) : m_params.GetDefaultPort(); } +void CConnman::AddI2PSessionsIfNeeded() +{ + AssertLockNotHeld(m_unused_i2p_sessions_mutex); + + Proxy i2p_proxy; + + if (!g_reachable_nets.Contains(NET_I2P) || // Not using I2P at all. + m_i2p_sam_session || // Using a single persistent session, m_unused_i2p_sessions is not used. + WITH_LOCK(m_unused_i2p_sessions_mutex, return m_unused_i2p_sessions.size() >= MAX_UNUSED_I2P_SESSIONS_SIZE) || // Already have enough sessions. + !GetProxy(NET_I2P, i2p_proxy)) { + return; + } + + // It is preferable to slightly exceed MAX_UNUSED_I2P_SESSIONS_SIZE compared to holding + // m_unused_i2p_sessions_mutex while creating the session which could take a few seconds. + + auto new_session = std::make_unique(i2p_proxy, &interruptNet); + + LOCK(m_unused_i2p_sessions_mutex); + m_unused_i2p_sessions.emplace(std::move(new_session)); +} + bool CConnman::Bind(const CService& addr_, unsigned int flags, NetPermissionFlags permissions) { const CService addr{MaybeFlipIPv6toCJDNS(addr_)}; diff --git a/src/net.h b/src/net.h index e64d9a67f46..52e7ef57c49 100644 --- a/src/net.h +++ b/src/net.h @@ -1408,6 +1408,11 @@ private: uint16_t GetDefaultPort(Network net) const; uint16_t GetDefaultPort(const std::string& addr) const; + /** + * Add entries to `m_unused_i2p_sessions` if needed. + */ + void AddI2PSessionsIfNeeded() EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + // Network usage totals mutable Mutex m_total_bytes_sent_mutex; std::atomic nTotalBytesRecv{0};