Skip to content

Commit eed2bd3

Browse files
committed
Merge bitcoin#25355: I2P: add support for transient addresses for outbound connections
59aa54f i2p: log "SAM session" instead of "session" (Vasil Dimov) d7ec30b doc: add release notes about the I2P transient addresses (Vasil Dimov) 47c0d02 doc: document I2P transient addresses usage in doc/i2p.md (Vasil Dimov) 3914e47 test: add a test that -i2pacceptincoming=0 creates a transient session (Vasil Dimov) ae1e97c net: use transient I2P session for outbound if -i2pacceptincoming=0 (Vasil Dimov) a1580a0 net: store an optional I2P session in CNode (Vasil Dimov) 2b781ad i2p: add support for creating transient sessions (Vasil Dimov) Pull request description: Add support for generating a transient, one-time I2P address for ourselves when making I2P outbound connection and discard it once the connection is closed. Background --- In I2P connections, the host that receives the connection knows the I2P address of the connection initiator. This is unlike the Tor network where the recipient does not know who is connecting to them, not even the initiator's Tor address. Persistent vs transient I2P addresses --- Even if an I2P node is not accepting incoming connections, they are known to other nodes by their outgoing I2P address. This creates an opportunity to white-list given nodes or treat them differently based on their I2P address. However, this also creates an opportunity to fingerprint or analyze a given node because it always uses the same I2P address when it connects to other nodes. If this is undesirable, then a node operator can use the newly introduced `-i2ptransientout` to generate a transient (disposable), one-time I2P address for each new outgoing connection. That address is never going to be reused again, not even if reconnecting to the same peer later. ACKs for top commit: mzumsande: ACK 59aa54f (verified via range-diff that just a typo / `unique_ptr` initialisation were fixed) achow101: re-ACK 59aa54f jonatack: utACK 59aa54f reviewed range diff, rebased to master, debug build + relevant tests + review at each commit Tree-SHA512: 2be9b9dd7502b2d44a75e095aaece61700766bff9af0a2846c29ca4e152b0a92bdfa30f61e8e32b6edb1225f74f1a78d19b7bf069f00b8f8173e69705414a93e
2 parents 15692e2 + 59aa54f commit eed2bd3

File tree

9 files changed

+193
-45
lines changed

9 files changed

+193
-45
lines changed

doc/i2p.md

+22-4
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,26 @@ In a typical situation, this suffices:
4747
bitcoind -i2psam=127.0.0.1:7656
4848
```
4949

50-
The first time Bitcoin Core connects to the I2P router, its I2P address (and
51-
corresponding private key) will be automatically generated and saved in a file
52-
named `i2p_private_key` in the Bitcoin Core data directory.
50+
The first time Bitcoin Core connects to the I2P router, if
51+
`-i2pacceptincoming=1`, then it will automatically generate a persistent I2P
52+
address and its corresponding private key. The private key will be saved in a
53+
file named `i2p_private_key` in the Bitcoin Core data directory. The persistent
54+
I2P address is used for accepting incoming connections and for making outgoing
55+
connections if `-i2pacceptincoming=1`. If `-i2pacceptincoming=0` then only
56+
outbound I2P connections are made and a different transient I2P address is used
57+
for each connection to improve privacy.
58+
59+
## Persistent vs transient I2P addresses
60+
61+
In I2P connections, the connection receiver sees the I2P address of the
62+
connection initiator. This is unlike the Tor network where the recipient does
63+
not know who is connecting to them and can't tell if two connections are from
64+
the same peer or not.
65+
66+
If an I2P node is not accepting incoming connections, then Bitcoin Core uses
67+
random, one-time, transient I2P addresses for itself for outbound connections
68+
to make it harder to discriminate, fingerprint or analyze it based on its I2P
69+
address.
5370

5471
## Additional configuration options related to I2P
5572

@@ -85,7 +102,8 @@ one of the networks has issues.
85102

86103
## I2P-related information in Bitcoin Core
87104

88-
There are several ways to see your I2P address in Bitcoin Core:
105+
There are several ways to see your I2P address in Bitcoin Core if accepting
106+
incoming I2P connections (`-i2pacceptincoming`):
89107
- in the "Local addresses" output of CLI `-netinfo`
90108
- in the "localaddresses" output of RPC `getnetworkinfo`
91109
- in the debug log (grep for `AddLocal`; the I2P address ends in `.b32.i2p`)

doc/release-notes-25355.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Notable changes
2+
===============
3+
4+
P2P and network changes
5+
-----------------------
6+
7+
- With I2P connections, a new, transient address is used for each outbound
8+
connection if `-i2pacceptincoming=0`. (#25355)

src/i2p.cpp

+46-17
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
#include <netaddress.h>
1313
#include <netbase.h>
1414
#include <random.h>
15-
#include <util/strencodings.h>
1615
#include <tinyformat.h>
1716
#include <util/readwritefile.h>
1817
#include <util/sock.h>
1918
#include <util/spanparsing.h>
19+
#include <util/strencodings.h>
2020
#include <util/system.h>
2121

2222
#include <chrono>
@@ -115,8 +115,19 @@ namespace sam {
115115
Session::Session(const fs::path& private_key_file,
116116
const CService& control_host,
117117
CThreadInterrupt* interrupt)
118-
: m_private_key_file(private_key_file), m_control_host(control_host), m_interrupt(interrupt),
119-
m_control_sock(std::make_unique<Sock>(INVALID_SOCKET))
118+
: m_private_key_file{private_key_file},
119+
m_control_host{control_host},
120+
m_interrupt{interrupt},
121+
m_control_sock{std::make_unique<Sock>(INVALID_SOCKET)},
122+
m_transient{false}
123+
{
124+
}
125+
126+
Session::Session(const CService& control_host, CThreadInterrupt* interrupt)
127+
: m_control_host{control_host},
128+
m_interrupt{interrupt},
129+
m_control_sock{std::make_unique<Sock>(INVALID_SOCKET)},
130+
m_transient{true}
120131
{
121132
}
122133

@@ -355,29 +366,47 @@ void Session::CreateIfNotCreatedAlready()
355366
return;
356367
}
357368

358-
Log("Creating SAM session with %s", m_control_host.ToString());
369+
const auto session_type = m_transient ? "transient" : "persistent";
370+
const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs
371+
372+
Log("Creating %s SAM session %s with %s", session_type, session_id, m_control_host.ToString());
359373

360374
auto sock = Hello();
361375

362-
const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
363-
if (read_ok) {
364-
m_private_key.assign(data.begin(), data.end());
376+
if (m_transient) {
377+
// The destination (private key) is generated upon session creation and returned
378+
// in the reply in DESTINATION=.
379+
const Reply& reply = SendRequestAndGetReply(
380+
*sock,
381+
strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT", session_id));
382+
383+
m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
365384
} else {
366-
GenerateAndSavePrivateKey(*sock);
367-
}
385+
// Read our persistent destination (private key) from disk or generate
386+
// one and save it to disk. Then use it when creating the session.
387+
const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
388+
if (read_ok) {
389+
m_private_key.assign(data.begin(), data.end());
390+
} else {
391+
GenerateAndSavePrivateKey(*sock);
392+
}
368393

369-
const std::string& session_id = GetRandHash().GetHex().substr(0, 10); // full is an overkill, too verbose in the logs
370-
const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
394+
const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
371395

372-
SendRequestAndGetReply(*sock, strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s",
373-
session_id, private_key_b64));
396+
SendRequestAndGetReply(*sock,
397+
strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s",
398+
session_id,
399+
private_key_b64));
400+
}
374401

375402
m_my_addr = CService(DestBinToAddr(MyDestination()), I2P_SAM31_PORT);
376403
m_session_id = session_id;
377404
m_control_sock = std::move(sock);
378405

379-
LogPrintfCategory(BCLog::I2P, "SAM session created: session id=%s, my address=%s\n",
380-
m_session_id, m_my_addr.ToString());
406+
Log("%s SAM session %s created, my address=%s",
407+
Capitalize(session_type),
408+
m_session_id,
409+
m_my_addr.ToString());
381410
}
382411

383412
std::unique_ptr<Sock> Session::StreamAccept()
@@ -405,9 +434,9 @@ void Session::Disconnect()
405434
{
406435
if (m_control_sock->Get() != INVALID_SOCKET) {
407436
if (m_session_id.empty()) {
408-
Log("Destroying incomplete session");
437+
Log("Destroying incomplete SAM session");
409438
} else {
410-
Log("Destroying session %s", m_session_id);
439+
Log("Destroying SAM session %s", m_session_id);
411440
}
412441
}
413442
m_control_sock = std::make_unique<Sock>(INVALID_SOCKET);

src/i2p.h

+19
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ class Session
7070
const CService& control_host,
7171
CThreadInterrupt* interrupt);
7272

73+
/**
74+
* Construct a transient session which will generate its own I2P private key
75+
* rather than read the one from disk (it will not be saved on disk either and
76+
* will be lost once this object is destroyed). This will not initiate any IO,
77+
* the session will be lazily created later when first used.
78+
* @param[in] control_host Location of the SAM proxy.
79+
* @param[in,out] interrupt If this is signaled then all operations are canceled as soon as
80+
* possible and executing methods throw an exception. Notice: only a pointer to the
81+
* `CThreadInterrupt` object is saved, so it must not be destroyed earlier than this
82+
* `Session` object.
83+
*/
84+
Session(const CService& control_host, CThreadInterrupt* interrupt);
85+
7386
/**
7487
* Destroy the session, closing the internally used sockets. The sockets that have been
7588
* returned by `Accept()` or `Connect()` will not be closed, but they will be closed by
@@ -262,6 +275,12 @@ class Session
262275
* SAM session id.
263276
*/
264277
std::string m_session_id GUARDED_BY(m_mutex);
278+
279+
/**
280+
* Whether this is a transient session (the I2P private key will not be
281+
* read or written to disk).
282+
*/
283+
const bool m_transient;
265284
};
266285

267286
} // namespace sam

src/net.cpp

+36-18
Original file line numberDiff line numberDiff line change
@@ -484,18 +484,27 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
484484
Proxy proxy;
485485
CAddress addr_bind;
486486
assert(!addr_bind.IsValid());
487+
std::unique_ptr<i2p::sam::Session> i2p_transient_session;
487488

488489
if (addrConnect.IsValid()) {
490+
const bool use_proxy{GetProxy(addrConnect.GetNetwork(), proxy)};
489491
bool proxyConnectionFailed = false;
490492

491-
if (addrConnect.GetNetwork() == NET_I2P && m_i2p_sam_session.get() != nullptr) {
493+
if (addrConnect.GetNetwork() == NET_I2P && use_proxy) {
492494
i2p::Connection conn;
493-
if (m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed)) {
494-
connected = true;
495+
496+
if (m_i2p_sam_session) {
497+
connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
498+
} else {
499+
i2p_transient_session = std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
500+
connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed);
501+
}
502+
503+
if (connected) {
495504
sock = std::move(conn.sock);
496505
addr_bind = CAddress{conn.me, NODE_NONE};
497506
}
498-
} else if (GetProxy(addrConnect.GetNetwork(), proxy)) {
507+
} else if (use_proxy) {
499508
sock = CreateSock(proxy.proxy);
500509
if (!sock) {
501510
return nullptr;
@@ -546,7 +555,8 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
546555
addr_bind,
547556
pszDest ? pszDest : "",
548557
conn_type,
549-
/*inbound_onion=*/false);
558+
/*inbound_onion=*/false,
559+
std::move(i2p_transient_session));
550560
pnode->AddRef();
551561

552562
// We're making a new connection, harvest entropy from the time (and our peer count)
@@ -563,6 +573,7 @@ void CNode::CloseSocketDisconnect()
563573
LogPrint(BCLog::NET, "disconnecting peer=%d\n", id);
564574
m_sock.reset();
565575
}
576+
m_i2p_sam_session.reset();
566577
}
567578

568579
void CConnman::AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const {
@@ -2258,7 +2269,7 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions)
22582269
}
22592270

22602271
Proxy i2p_sam;
2261-
if (GetProxy(NET_I2P, i2p_sam)) {
2272+
if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) {
22622273
m_i2p_sam_session = std::make_unique<i2p::sam::Session>(gArgs.GetDataDirNet() / "i2p_private_key",
22632274
i2p_sam.proxy, &interruptNet);
22642275
}
@@ -2332,7 +2343,7 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions)
23322343
// Process messages
23332344
threadMessageHandler = std::thread(&util::TraceThread, "msghand", [this] { ThreadMessageHandler(); });
23342345

2335-
if (connOptions.m_i2p_accept_incoming && m_i2p_sam_session.get() != nullptr) {
2346+
if (m_i2p_sam_session) {
23362347
threadI2PAcceptIncoming =
23372348
std::thread(&util::TraceThread, "i2paccept", [this] { ThreadI2PAcceptIncoming(); });
23382349
}
@@ -2701,20 +2712,27 @@ ServiceFlags CConnman::GetLocalServices() const
27012712

27022713
unsigned int CConnman::GetReceiveFloodSize() const { return nReceiveFloodSize; }
27032714

2704-
CNode::CNode(NodeId idIn, std::shared_ptr<Sock> sock, const CAddress& addrIn,
2705-
uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn,
2706-
const CAddress& addrBindIn, const std::string& addrNameIn,
2707-
ConnectionType conn_type_in, bool inbound_onion)
2715+
CNode::CNode(NodeId idIn,
2716+
std::shared_ptr<Sock> sock,
2717+
const CAddress& addrIn,
2718+
uint64_t nKeyedNetGroupIn,
2719+
uint64_t nLocalHostNonceIn,
2720+
const CAddress& addrBindIn,
2721+
const std::string& addrNameIn,
2722+
ConnectionType conn_type_in,
2723+
bool inbound_onion,
2724+
std::unique_ptr<i2p::sam::Session>&& i2p_sam_session)
27082725
: m_sock{sock},
27092726
m_connected{GetTime<std::chrono::seconds>()},
2710-
addr(addrIn),
2711-
addrBind(addrBindIn),
2727+
addr{addrIn},
2728+
addrBind{addrBindIn},
27122729
m_addr_name{addrNameIn.empty() ? addr.ToStringIPPort() : addrNameIn},
2713-
m_inbound_onion(inbound_onion),
2714-
nKeyedNetGroup(nKeyedNetGroupIn),
2715-
id(idIn),
2716-
nLocalHostNonce(nLocalHostNonceIn),
2717-
m_conn_type(conn_type_in)
2730+
m_inbound_onion{inbound_onion},
2731+
nKeyedNetGroup{nKeyedNetGroupIn},
2732+
id{idIn},
2733+
nLocalHostNonce{nLocalHostNonceIn},
2734+
m_conn_type{conn_type_in},
2735+
m_i2p_sam_session{std::move(i2p_sam_session)}
27182736
{
27192737
if (inbound_onion) assert(conn_type_in == ConnectionType::INBOUND);
27202738

src/net.h

+24-5
Original file line numberDiff line numberDiff line change
@@ -513,10 +513,16 @@ class CNode
513513
* criterium in CConnman::AttemptToEvictConnection. */
514514
std::atomic<std::chrono::microseconds> m_min_ping_time{std::chrono::microseconds::max()};
515515

516-
CNode(NodeId id, std::shared_ptr<Sock> sock, const CAddress& addrIn,
517-
uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn,
518-
const CAddress& addrBindIn, const std::string& addrNameIn,
519-
ConnectionType conn_type_in, bool inbound_onion);
516+
CNode(NodeId id,
517+
std::shared_ptr<Sock> sock,
518+
const CAddress& addrIn,
519+
uint64_t nKeyedNetGroupIn,
520+
uint64_t nLocalHostNonceIn,
521+
const CAddress& addrBindIn,
522+
const std::string& addrNameIn,
523+
ConnectionType conn_type_in,
524+
bool inbound_onion,
525+
std::unique_ptr<i2p::sam::Session>&& i2p_sam_session = nullptr);
520526
CNode(const CNode&) = delete;
521527
CNode& operator=(const CNode&) = delete;
522528

@@ -596,6 +602,18 @@ class CNode
596602

597603
mapMsgTypeSize mapSendBytesPerMsgType GUARDED_BY(cs_vSend);
598604
mapMsgTypeSize mapRecvBytesPerMsgType GUARDED_BY(cs_vRecv);
605+
606+
/**
607+
* If an I2P session is created per connection (for outbound transient I2P
608+
* connections) then it is stored here so that it can be destroyed when the
609+
* socket is closed. I2P sessions involve a data/transport socket (in `m_sock`)
610+
* and a control socket (in `m_i2p_sam_session`). For transient sessions, once
611+
* the data socket is closed, the control socket is not going to be used anymore
612+
* and is just taking up resources. So better close it as soon as `m_sock` is
613+
* closed.
614+
* Otherwise this unique_ptr is empty.
615+
*/
616+
std::unique_ptr<i2p::sam::Session> m_i2p_sam_session GUARDED_BY(m_sock_mutex);
599617
};
600618

601619
/**
@@ -1072,7 +1090,8 @@ class CConnman
10721090

10731091
/**
10741092
* I2P SAM session.
1075-
* Used to accept incoming and make outgoing I2P connections.
1093+
* Used to accept incoming and make outgoing I2P connections from a persistent
1094+
* address.
10761095
*/
10771096
std::unique_ptr<i2p::sam::Session> m_i2p_sam_session;
10781097

src/test/i2p_tests.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv)
3030
i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", CService{}, &interrupt);
3131

3232
{
33-
ASSERT_DEBUG_LOG("Creating SAM session");
33+
ASSERT_DEBUG_LOG("Creating persistent SAM session");
3434
ASSERT_DEBUG_LOG("too many bytes without a terminator");
3535

3636
i2p::Connection conn;

test/functional/p2p_i2p_sessions.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022-2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""
6+
Test whether persistent or transient I2P sessions are being used, based on `-i2pacceptincoming`.
7+
"""
8+
9+
from test_framework.test_framework import BitcoinTestFramework
10+
11+
12+
class I2PSessions(BitcoinTestFramework):
13+
def set_test_params(self):
14+
self.num_nodes = 2
15+
# The test assumes that an I2P SAM proxy is not listening here.
16+
self.extra_args = [
17+
["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=1"],
18+
["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=0"],
19+
]
20+
21+
def run_test(self):
22+
addr = "zsxwyo6qcn3chqzwxnseusqgsnuw3maqnztkiypyfxtya4snkoka.b32.i2p"
23+
24+
self.log.info("Ensure we create a persistent session when -i2pacceptincoming=1")
25+
node0 = self.nodes[0]
26+
with node0.assert_debug_log(expected_msgs=[f"Creating persistent SAM session"]):
27+
node0.addnode(node=addr, command="onetry")
28+
29+
self.log.info("Ensure we create a transient session when -i2pacceptincoming=0")
30+
node1 = self.nodes[1]
31+
with node1.assert_debug_log(expected_msgs=[f"Creating transient SAM session"]):
32+
node1.addnode(node=addr, command="onetry")
33+
34+
35+
if __name__ == '__main__':
36+
I2PSessions().main()

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@
330330
'feature_blocksdir.py',
331331
'wallet_startup.py',
332332
'p2p_i2p_ports.py',
333+
'p2p_i2p_sessions.py',
333334
'feature_config_args.py',
334335
'feature_presegwit_node_upgrade.py',
335336
'feature_settings.py',

0 commit comments

Comments
 (0)