aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/i2p.md26
-rw-r--r--doc/release-notes-25355.md8
-rw-r--r--src/i2p.cpp63
-rw-r--r--src/i2p.h19
-rw-r--r--src/net.cpp54
-rw-r--r--src/net.h29
-rw-r--r--src/test/i2p_tests.cpp2
-rwxr-xr-xtest/functional/p2p_i2p_sessions.py36
-rwxr-xr-xtest/functional/test_runner.py1
9 files changed, 193 insertions, 45 deletions
diff --git a/doc/i2p.md b/doc/i2p.md
index 39f65c4e5f..1599c2fe0f 100644
--- a/doc/i2p.md
+++ b/doc/i2p.md
@@ -47,9 +47,26 @@ In a typical situation, this suffices:
bitcoind -i2psam=127.0.0.1:7656
```
-The first time Bitcoin Core connects to the I2P router, its I2P address (and
-corresponding private key) will be automatically generated and saved in a file
-named `i2p_private_key` in the Bitcoin Core data directory.
+The first time Bitcoin Core connects to the I2P router, if
+`-i2pacceptincoming=1`, then it will automatically generate a persistent I2P
+address and its corresponding private key. The private key will be saved in a
+file named `i2p_private_key` in the Bitcoin Core data directory. The persistent
+I2P address is used for accepting incoming connections and for making outgoing
+connections if `-i2pacceptincoming=1`. If `-i2pacceptincoming=0` then only
+outbound I2P connections are made and a different transient I2P address is used
+for each connection to improve privacy.
+
+## Persistent vs transient I2P addresses
+
+In I2P connections, the connection receiver sees the I2P address of the
+connection initiator. This is unlike the Tor network where the recipient does
+not know who is connecting to them and can't tell if two connections are from
+the same peer or not.
+
+If an I2P node is not accepting incoming connections, then Bitcoin Core uses
+random, one-time, transient I2P addresses for itself for outbound connections
+to make it harder to discriminate, fingerprint or analyze it based on its I2P
+address.
## Additional configuration options related to I2P
@@ -85,7 +102,8 @@ one of the networks has issues.
## I2P-related information in Bitcoin Core
-There are several ways to see your I2P address in Bitcoin Core:
+There are several ways to see your I2P address in Bitcoin Core if accepting
+incoming I2P connections (`-i2pacceptincoming`):
- in the "Local addresses" output of CLI `-netinfo`
- in the "localaddresses" output of RPC `getnetworkinfo`
- in the debug log (grep for `AddLocal`; the I2P address ends in `.b32.i2p`)
diff --git a/doc/release-notes-25355.md b/doc/release-notes-25355.md
new file mode 100644
index 0000000000..34dd2c3687
--- /dev/null
+++ b/doc/release-notes-25355.md
@@ -0,0 +1,8 @@
+Notable changes
+===============
+
+P2P and network changes
+-----------------------
+
+- With I2P connections, a new, transient address is used for each outbound
+ connection if `-i2pacceptincoming=0`. (#25355)
diff --git a/src/i2p.cpp b/src/i2p.cpp
index c45bcc15d2..f7d480988b 100644
--- a/src/i2p.cpp
+++ b/src/i2p.cpp
@@ -12,11 +12,11 @@
#include <netaddress.h>
#include <netbase.h>
#include <random.h>
-#include <util/strencodings.h>
#include <tinyformat.h>
#include <util/readwritefile.h>
#include <util/sock.h>
#include <util/spanparsing.h>
+#include <util/strencodings.h>
#include <util/system.h>
#include <chrono>
@@ -115,8 +115,19 @@ namespace sam {
Session::Session(const fs::path& private_key_file,
const CService& control_host,
CThreadInterrupt* interrupt)
- : m_private_key_file(private_key_file), m_control_host(control_host), m_interrupt(interrupt),
- m_control_sock(std::make_unique<Sock>(INVALID_SOCKET))
+ : m_private_key_file{private_key_file},
+ m_control_host{control_host},
+ m_interrupt{interrupt},
+ m_control_sock{std::make_unique<Sock>(INVALID_SOCKET)},
+ m_transient{false}
+{
+}
+
+Session::Session(const CService& control_host, CThreadInterrupt* interrupt)
+ : m_control_host{control_host},
+ m_interrupt{interrupt},
+ m_control_sock{std::make_unique<Sock>(INVALID_SOCKET)},
+ m_transient{true}
{
}
@@ -355,29 +366,47 @@ void Session::CreateIfNotCreatedAlready()
return;
}
- Log("Creating SAM session with %s", m_control_host.ToString());
+ const auto session_type = m_transient ? "transient" : "persistent";
+ const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs
+
+ Log("Creating %s SAM session %s with %s", session_type, session_id, m_control_host.ToString());
auto sock = Hello();
- const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
- if (read_ok) {
- m_private_key.assign(data.begin(), data.end());
+ if (m_transient) {
+ // The destination (private key) is generated upon session creation and returned
+ // in the reply in DESTINATION=.
+ const Reply& reply = SendRequestAndGetReply(
+ *sock,
+ strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT", session_id));
+
+ m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
} else {
- GenerateAndSavePrivateKey(*sock);
- }
+ // Read our persistent destination (private key) from disk or generate
+ // one and save it to disk. Then use it when creating the session.
+ const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
+ if (read_ok) {
+ m_private_key.assign(data.begin(), data.end());
+ } else {
+ GenerateAndSavePrivateKey(*sock);
+ }
- const std::string& session_id = GetRandHash().GetHex().substr(0, 10); // full is an overkill, too verbose in the logs
- const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
+ const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
- SendRequestAndGetReply(*sock, strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s",
- session_id, private_key_b64));
+ SendRequestAndGetReply(*sock,
+ strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s",
+ session_id,
+ private_key_b64));
+ }
m_my_addr = CService(DestBinToAddr(MyDestination()), I2P_SAM31_PORT);
m_session_id = session_id;
m_control_sock = std::move(sock);
- LogPrintfCategory(BCLog::I2P, "SAM session created: session id=%s, my address=%s\n",
- m_session_id, m_my_addr.ToString());
+ Log("%s SAM session %s created, my address=%s",
+ Capitalize(session_type),
+ m_session_id,
+ m_my_addr.ToString());
}
std::unique_ptr<Sock> Session::StreamAccept()
@@ -405,9 +434,9 @@ void Session::Disconnect()
{
if (m_control_sock->Get() != INVALID_SOCKET) {
if (m_session_id.empty()) {
- Log("Destroying incomplete session");
+ Log("Destroying incomplete SAM session");
} else {
- Log("Destroying session %s", m_session_id);
+ Log("Destroying SAM session %s", m_session_id);
}
}
m_control_sock = std::make_unique<Sock>(INVALID_SOCKET);
diff --git a/src/i2p.h b/src/i2p.h
index eb0a10103d..ebbcb437da 100644
--- a/src/i2p.h
+++ b/src/i2p.h
@@ -71,6 +71,19 @@ public:
CThreadInterrupt* interrupt);
/**
+ * Construct a transient session which will generate its own I2P private key
+ * rather than read the one from disk (it will not be saved on disk either and
+ * will be lost once this object is destroyed). This will not initiate any IO,
+ * the session will be lazily created later when first used.
+ * @param[in] control_host Location of the SAM proxy.
+ * @param[in,out] interrupt If this is signaled then all operations are canceled as soon as
+ * possible and executing methods throw an exception. Notice: only a pointer to the
+ * `CThreadInterrupt` object is saved, so it must not be destroyed earlier than this
+ * `Session` object.
+ */
+ Session(const CService& control_host, CThreadInterrupt* interrupt);
+
+ /**
* Destroy the session, closing the internally used sockets. The sockets that have been
* returned by `Accept()` or `Connect()` will not be closed, but they will be closed by
* the SAM proxy because the session is destroyed. So they will return an error next time
@@ -262,6 +275,12 @@ private:
* SAM session id.
*/
std::string m_session_id GUARDED_BY(m_mutex);
+
+ /**
+ * Whether this is a transient session (the I2P private key will not be
+ * read or written to disk).
+ */
+ const bool m_transient;
};
} // namespace sam
diff --git a/src/net.cpp b/src/net.cpp
index 865ce2ea97..c2e45898e8 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -484,18 +484,27 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
Proxy proxy;
CAddress addr_bind;
assert(!addr_bind.IsValid());
+ std::unique_ptr<i2p::sam::Session> i2p_transient_session;
if (addrConnect.IsValid()) {
+ const bool use_proxy{GetProxy(addrConnect.GetNetwork(), proxy)};
bool proxyConnectionFailed = false;
- if (addrConnect.GetNetwork() == NET_I2P && m_i2p_sam_session.get() != nullptr) {
+ if (addrConnect.GetNetwork() == NET_I2P && use_proxy) {
i2p::Connection conn;
- if (m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed)) {
- connected = true;
+
+ if (m_i2p_sam_session) {
+ connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
+ } else {
+ i2p_transient_session = std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
+ connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed);
+ }
+
+ if (connected) {
sock = std::move(conn.sock);
addr_bind = CAddress{conn.me, NODE_NONE};
}
- } else if (GetProxy(addrConnect.GetNetwork(), proxy)) {
+ } else if (use_proxy) {
sock = CreateSock(proxy.proxy);
if (!sock) {
return nullptr;
@@ -546,7 +555,8 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
addr_bind,
pszDest ? pszDest : "",
conn_type,
- /*inbound_onion=*/false);
+ /*inbound_onion=*/false,
+ std::move(i2p_transient_session));
pnode->AddRef();
// We're making a new connection, harvest entropy from the time (and our peer count)
@@ -563,6 +573,7 @@ void CNode::CloseSocketDisconnect()
LogPrint(BCLog::NET, "disconnecting peer=%d\n", id);
m_sock.reset();
}
+ m_i2p_sam_session.reset();
}
void CConnman::AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const {
@@ -2258,7 +2269,7 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions)
}
Proxy i2p_sam;
- if (GetProxy(NET_I2P, i2p_sam)) {
+ if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) {
m_i2p_sam_session = std::make_unique<i2p::sam::Session>(gArgs.GetDataDirNet() / "i2p_private_key",
i2p_sam.proxy, &interruptNet);
}
@@ -2332,7 +2343,7 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions)
// Process messages
threadMessageHandler = std::thread(&util::TraceThread, "msghand", [this] { ThreadMessageHandler(); });
- if (connOptions.m_i2p_accept_incoming && m_i2p_sam_session.get() != nullptr) {
+ if (m_i2p_sam_session) {
threadI2PAcceptIncoming =
std::thread(&util::TraceThread, "i2paccept", [this] { ThreadI2PAcceptIncoming(); });
}
@@ -2701,20 +2712,27 @@ ServiceFlags CConnman::GetLocalServices() const
unsigned int CConnman::GetReceiveFloodSize() const { return nReceiveFloodSize; }
-CNode::CNode(NodeId idIn, std::shared_ptr<Sock> sock, const CAddress& addrIn,
- uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn,
- const CAddress& addrBindIn, const std::string& addrNameIn,
- ConnectionType conn_type_in, bool inbound_onion)
+CNode::CNode(NodeId idIn,
+ std::shared_ptr<Sock> sock,
+ const CAddress& addrIn,
+ uint64_t nKeyedNetGroupIn,
+ uint64_t nLocalHostNonceIn,
+ const CAddress& addrBindIn,
+ const std::string& addrNameIn,
+ ConnectionType conn_type_in,
+ bool inbound_onion,
+ std::unique_ptr<i2p::sam::Session>&& i2p_sam_session)
: m_sock{sock},
m_connected{GetTime<std::chrono::seconds>()},
- addr(addrIn),
- addrBind(addrBindIn),
+ addr{addrIn},
+ addrBind{addrBindIn},
m_addr_name{addrNameIn.empty() ? addr.ToStringIPPort() : addrNameIn},
- m_inbound_onion(inbound_onion),
- nKeyedNetGroup(nKeyedNetGroupIn),
- id(idIn),
- nLocalHostNonce(nLocalHostNonceIn),
- m_conn_type(conn_type_in)
+ m_inbound_onion{inbound_onion},
+ nKeyedNetGroup{nKeyedNetGroupIn},
+ id{idIn},
+ nLocalHostNonce{nLocalHostNonceIn},
+ m_conn_type{conn_type_in},
+ m_i2p_sam_session{std::move(i2p_sam_session)}
{
if (inbound_onion) assert(conn_type_in == ConnectionType::INBOUND);
diff --git a/src/net.h b/src/net.h
index 2036e9078c..7156a2259f 100644
--- a/src/net.h
+++ b/src/net.h
@@ -513,10 +513,16 @@ public:
* criterium in CConnman::AttemptToEvictConnection. */
std::atomic<std::chrono::microseconds> m_min_ping_time{std::chrono::microseconds::max()};
- CNode(NodeId id, std::shared_ptr<Sock> sock, const CAddress& addrIn,
- uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn,
- const CAddress& addrBindIn, const std::string& addrNameIn,
- ConnectionType conn_type_in, bool inbound_onion);
+ CNode(NodeId id,
+ std::shared_ptr<Sock> sock,
+ const CAddress& addrIn,
+ uint64_t nKeyedNetGroupIn,
+ uint64_t nLocalHostNonceIn,
+ const CAddress& addrBindIn,
+ const std::string& addrNameIn,
+ ConnectionType conn_type_in,
+ bool inbound_onion,
+ std::unique_ptr<i2p::sam::Session>&& i2p_sam_session = nullptr);
CNode(const CNode&) = delete;
CNode& operator=(const CNode&) = delete;
@@ -596,6 +602,18 @@ private:
mapMsgTypeSize mapSendBytesPerMsgType GUARDED_BY(cs_vSend);
mapMsgTypeSize mapRecvBytesPerMsgType GUARDED_BY(cs_vRecv);
+
+ /**
+ * If an I2P session is created per connection (for outbound transient I2P
+ * connections) then it is stored here so that it can be destroyed when the
+ * socket is closed. I2P sessions involve a data/transport socket (in `m_sock`)
+ * and a control socket (in `m_i2p_sam_session`). For transient sessions, once
+ * the data socket is closed, the control socket is not going to be used anymore
+ * and is just taking up resources. So better close it as soon as `m_sock` is
+ * closed.
+ * Otherwise this unique_ptr is empty.
+ */
+ std::unique_ptr<i2p::sam::Session> m_i2p_sam_session GUARDED_BY(m_sock_mutex);
};
/**
@@ -1072,7 +1090,8 @@ private:
/**
* I2P SAM session.
- * Used to accept incoming and make outgoing I2P connections.
+ * Used to accept incoming and make outgoing I2P connections from a persistent
+ * address.
*/
std::unique_ptr<i2p::sam::Session> m_i2p_sam_session;
diff --git a/src/test/i2p_tests.cpp b/src/test/i2p_tests.cpp
index bd9ba4b8f7..80684ac205 100644
--- a/src/test/i2p_tests.cpp
+++ b/src/test/i2p_tests.cpp
@@ -30,7 +30,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv)
i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", CService{}, &interrupt);
{
- ASSERT_DEBUG_LOG("Creating SAM session");
+ ASSERT_DEBUG_LOG("Creating persistent SAM session");
ASSERT_DEBUG_LOG("too many bytes without a terminator");
i2p::Connection conn;
diff --git a/test/functional/p2p_i2p_sessions.py b/test/functional/p2p_i2p_sessions.py
new file mode 100755
index 0000000000..4e52522b81
--- /dev/null
+++ b/test/functional/p2p_i2p_sessions.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# Copyright (c) 2022-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""
+Test whether persistent or transient I2P sessions are being used, based on `-i2pacceptincoming`.
+"""
+
+from test_framework.test_framework import BitcoinTestFramework
+
+
+class I2PSessions(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 2
+ # The test assumes that an I2P SAM proxy is not listening here.
+ self.extra_args = [
+ ["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=1"],
+ ["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=0"],
+ ]
+
+ def run_test(self):
+ addr = "zsxwyo6qcn3chqzwxnseusqgsnuw3maqnztkiypyfxtya4snkoka.b32.i2p"
+
+ self.log.info("Ensure we create a persistent session when -i2pacceptincoming=1")
+ node0 = self.nodes[0]
+ with node0.assert_debug_log(expected_msgs=[f"Creating persistent SAM session"]):
+ node0.addnode(node=addr, command="onetry")
+
+ self.log.info("Ensure we create a transient session when -i2pacceptincoming=0")
+ node1 = self.nodes[1]
+ with node1.assert_debug_log(expected_msgs=[f"Creating transient SAM session"]):
+ node1.addnode(node=addr, command="onetry")
+
+
+if __name__ == '__main__':
+ I2PSessions().main()
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 37d0c4f87e..edfab57cd0 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -330,6 +330,7 @@ BASE_SCRIPTS = [
'feature_blocksdir.py',
'wallet_startup.py',
'p2p_i2p_ports.py',
+ 'p2p_i2p_sessions.py',
'feature_config_args.py',
'feature_presegwit_node_upgrade.py',
'feature_settings.py',