Skip to content

Commit 9bf3829

Browse files
kwvgPastaPastaPasta
authored andcommitted
merge bitcoin#25355: add support for transient addresses for outbound connections
1 parent 4977073 commit 9bf3829

File tree

9 files changed

+174
-41
lines changed

9 files changed

+174
-41
lines changed

doc/i2p.md

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

50-
The first time Dash 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 Dash Core data directory.
50+
The first time Dash 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 Dash 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 Dash 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

@@ -89,7 +106,8 @@ of the networks has issues.
89106

90107
## I2P-related information in Dash Core
91108

92-
There are several ways to see your I2P address in Dash Core:
109+
There are several ways to see your I2P address in Dash Core if accepting
110+
incoming I2P connections (`-i2pacceptincoming`):
93111
- in the debug log (grep for `AddLocal`, the I2P address ends in `.b32.i2p`)
94112
- in the output of the `getnetworkinfo` RPC in the "localaddresses" section
95113
- in the output of `dash-cli -netinfo` peer connections dashboard

doc/release-notes-25355.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
P2P and network changes
2+
-----------------------
3+
4+
- With I2P connections, a new, transient address is used for each outbound
5+
connection if `-i2pacceptincoming=0`.

src/i2p.cpp

Lines changed: 46 additions & 17 deletions
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>
@@ -116,8 +116,19 @@ namespace sam {
116116
Session::Session(const fs::path& private_key_file,
117117
const CService& control_host,
118118
CThreadInterrupt* interrupt)
119-
: m_private_key_file(private_key_file), m_control_host(control_host), m_interrupt(interrupt),
120-
m_control_sock(std::make_unique<Sock>(INVALID_SOCKET))
119+
: m_private_key_file{private_key_file},
120+
m_control_host{control_host},
121+
m_interrupt{interrupt},
122+
m_control_sock{std::make_unique<Sock>(INVALID_SOCKET)},
123+
m_transient{false}
124+
{
125+
}
126+
127+
Session::Session(const CService& control_host, CThreadInterrupt* interrupt)
128+
: m_control_host{control_host},
129+
m_interrupt{interrupt},
130+
m_control_sock{std::make_unique<Sock>(INVALID_SOCKET)},
131+
m_transient{true}
121132
{
122133
}
123134

@@ -356,29 +367,47 @@ void Session::CreateIfNotCreatedAlready()
356367
return;
357368
}
358369

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

361375
auto sock = Hello();
362376

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

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

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

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

380-
LogPrintf("I2P: SAM session created: session id=%s, my address=%s\n", m_session_id,
381-
m_my_addr.ToString());
407+
Log("%s SAM session %s created, my address=%s",
408+
Capitalize(session_type),
409+
m_session_id,
410+
m_my_addr.ToString());
382411
}
383412

384413
std::unique_ptr<Sock> Session::StreamAccept()
@@ -406,9 +435,9 @@ void Session::Disconnect()
406435
{
407436
if (m_control_sock->Get() != INVALID_SOCKET) {
408437
if (m_session_id.empty()) {
409-
Log("Destroying incomplete session");
438+
Log("Destroying incomplete SAM session");
410439
} else {
411-
Log("Destroying session %s", m_session_id);
440+
Log("Destroying SAM session %s", m_session_id);
412441
}
413442
}
414443
m_control_sock->Reset();

src/i2p.h

Lines changed: 19 additions & 0 deletions
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

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -475,18 +475,27 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
475475
proxyType proxy;
476476
CAddress addr_bind;
477477
assert(!addr_bind.IsValid());
478+
std::unique_ptr<i2p::sam::Session> i2p_transient_session;
478479

479480
if (addrConnect.IsValid()) {
481+
const bool use_proxy{GetProxy(addrConnect.GetNetwork(), proxy)};
480482
bool proxyConnectionFailed = false;
481483

482-
if (addrConnect.GetNetwork() == NET_I2P && m_i2p_sam_session.get() != nullptr) {
484+
if (addrConnect.GetNetwork() == NET_I2P && use_proxy) {
483485
i2p::Connection conn;
484-
if (m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed)) {
485-
connected = true;
486+
487+
if (m_i2p_sam_session) {
488+
connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
489+
} else {
490+
i2p_transient_session = std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
491+
connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed);
492+
}
493+
494+
if (connected) {
486495
sock = std::move(conn.sock);
487496
addr_bind = CAddress{conn.me, NODE_NONE};
488497
}
489-
} else if (GetProxy(addrConnect.GetNetwork(), proxy)) {
498+
} else if (use_proxy) {
490499
sock = CreateSock(proxy.proxy);
491500
if (!sock) {
492501
return nullptr;
@@ -528,7 +537,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
528537
if (!addr_bind.IsValid()) {
529538
addr_bind = GetBindAddress(sock->Get());
530539
}
531-
CNode* pnode = new CNode(id, nLocalServices, sock->Release(), addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", conn_type, /* inbound_onion */ false);
540+
CNode* pnode = new CNode(id, nLocalServices, sock->Release(), addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", conn_type, /* inbound_onion */ false, std::move(i2p_transient_session));
532541
pnode->AddRef();
533542
statsClient.inc("peers.connect", 1.0f);
534543

@@ -571,6 +580,8 @@ void CNode::CloseSocketDisconnect(CConnman* connman)
571580

572581
LogPrint(BCLog::NET, "disconnecting peer=%d\n", id);
573582
CloseSocket(hSocket);
583+
m_i2p_sam_session.reset();
584+
574585
statsClient.inc("peers.disconnect", 1.0f);
575586
}
576587

@@ -3342,7 +3353,7 @@ bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_met
33423353
}
33433354

33443355
proxyType i2p_sam;
3345-
if (GetProxy(NET_I2P, i2p_sam)) {
3356+
if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) {
33463357
m_i2p_sam_session = std::make_unique<i2p::sam::Session>(GetDataDir() / "i2p_private_key",
33473358
i2p_sam.proxy, &interruptNet);
33483359
}
@@ -3444,7 +3455,7 @@ bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_met
34443455
// Process messages
34453456
threadMessageHandler = std::thread(&util::TraceThread, "msghand", [this] { ThreadMessageHandler(); });
34463457

3447-
if (connOptions.m_i2p_accept_incoming && m_i2p_sam_session.get() != nullptr) {
3458+
if (m_i2p_sam_session) {
34483459
threadI2PAcceptIncoming =
34493460
std::thread(&util::TraceThread, "i2paccept", [this, &mn_sync] { ThreadI2PAcceptIncoming(mn_sync); });
34503461
}
@@ -4012,17 +4023,18 @@ ServiceFlags CConnman::GetLocalServices() const
40124023

40134024
unsigned int CConnman::GetReceiveFloodSize() const { return nReceiveFloodSize; }
40144025

4015-
CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress& addrBindIn, const std::string& addrNameIn, ConnectionType conn_type_in, bool inbound_onion)
4016-
: nTimeConnected(GetTimeSeconds()),
4017-
addr(addrIn),
4018-
addrBind(addrBindIn),
4026+
CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, 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)
4027+
: nTimeConnected{GetTimeSeconds()},
4028+
addr{addrIn},
4029+
addrBind{addrBindIn},
40194030
m_addr_name{addrNameIn.empty() ? addr.ToStringIPPort() : addrNameIn},
4020-
m_inbound_onion(inbound_onion),
4021-
nKeyedNetGroup(nKeyedNetGroupIn),
4022-
id(idIn),
4023-
nLocalHostNonce(nLocalHostNonceIn),
4024-
m_conn_type(conn_type_in),
4025-
nLocalServices(nLocalServicesIn)
4031+
m_inbound_onion{inbound_onion},
4032+
nKeyedNetGroup{nKeyedNetGroupIn},
4033+
id{idIn},
4034+
nLocalHostNonce{nLocalHostNonceIn},
4035+
m_conn_type{conn_type_in},
4036+
nLocalServices{nLocalServicesIn},
4037+
m_i2p_sam_session{std::move(i2p_sam_session)}
40264038
{
40274039
if (inbound_onion) assert(conn_type_in == ConnectionType::INBOUND);
40284040
hSocket = hSocketIn;

src/net.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ class CNode
622622

623623
bool IsBlockRelayOnly() const;
624624

625-
CNode(NodeId id, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const CAddress &addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress &addrBindIn, const std::string &addrNameIn, ConnectionType conn_type_in, bool inbound_onion);
625+
CNode(NodeId id, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, 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);
626626
~CNode();
627627
CNode(const CNode&) = delete;
628628
CNode& operator=(const CNode&) = delete;
@@ -776,6 +776,18 @@ class CNode
776776

777777
mapMsgCmdSize mapSendBytesPerMsgCmd GUARDED_BY(cs_vSend);
778778
mapMsgCmdSize mapRecvBytesPerMsgCmd GUARDED_BY(cs_vRecv);
779+
780+
/**
781+
* If an I2P session is created per connection (for outbound transient I2P
782+
* connections) then it is stored here so that it can be destroyed when the
783+
* socket is closed. I2P sessions involve a data/transport socket (in `m_sock`)
784+
* and a control socket (in `m_i2p_sam_session`). For transient sessions, once
785+
* the data socket is closed, the control socket is not going to be used anymore
786+
* and is just taking up resources. So better close it as soon as `m_sock` is
787+
* closed.
788+
* Otherwise this unique_ptr is empty.
789+
*/
790+
std::unique_ptr<i2p::sam::Session> m_i2p_sam_session GUARDED_BY(cs_hSocket);
779791
};
780792

781793
/**
@@ -1498,7 +1510,8 @@ friend class CNode;
14981510

14991511
/**
15001512
* I2P SAM session.
1501-
* Used to accept incoming and make outgoing I2P connections.
1513+
* Used to accept incoming and make outgoing I2P connections from a persistent
1514+
* address.
15021515
*/
15031516
std::unique_ptr<i2p::sam::Session> m_i2p_sam_session;
15041517

src/test/i2p_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv)
3131
i2p::sam::Session session(GetDataDir() / "test_i2p_private_key", CService{}, &interrupt);
3232

3333
{
34-
ASSERT_DEBUG_LOG("Creating SAM session");
34+
ASSERT_DEBUG_LOG("Creating persistent SAM session");
3535
ASSERT_DEBUG_LOG("too many bytes without a terminator");
3636

3737
i2p::Connection conn;
Lines changed: 36 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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_settings.py',
335336
'rpc_getdescriptorinfo.py',

0 commit comments

Comments
 (0)