Coverage Report

Created: 2026-09-01 13:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/bitcoin/src/i2p.cpp
Line
Count
Source
1
// Copyright (c) 2020-present The Bitcoin Core developers
2
// Distributed under the MIT software license, see the accompanying
3
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5
#include <chainparams.h>
6
#include <common/args.h>
7
#include <compat/compat.h>
8
#include <compat/endian.h>
9
#include <crypto/sha256.h>
10
#include <i2p.h>
11
#include <netaddress.h>
12
#include <netbase.h>
13
#include <random.h>
14
#include <script/parsing.h>
15
#include <sync.h>
16
#include <tinyformat.h>
17
#include <util/fs.h>
18
#include <util/log.h>
19
#include <util/readwritefile.h>
20
#include <util/sock.h>
21
#include <util/strencodings.h>
22
#include <util/threadinterrupt.h>
23
24
#include <chrono>
25
#include <memory>
26
#include <ranges>
27
#include <stdexcept>
28
#include <string>
29
30
using util::Split;
31
32
namespace i2p {
33
34
/**
35
 * Swap Standard Base64 <-> I2P Base64.
36
 * Standard Base64 uses `+` and `/` as last two characters of its alphabet.
37
 * I2P Base64 uses `-` and `~` respectively.
38
 * So it is easy to detect in which one is the input and convert to the other.
39
 * @param[in] from Input to convert.
40
 * @return converted `from`
41
 */
42
static std::string SwapBase64(const std::string& from)
43
524
{
44
524
    std::string to;
45
524
    to.resize(from.size());
46
3.20M
    for (size_t i = 0; i < from.size(); ++i) {
  Branch (46:24): [True: 3.20M, False: 524]
47
3.20M
        switch (from[i]) {
48
685
        case '-':
  Branch (48:9): [True: 685, False: 3.20M]
49
685
            to[i] = '+';
50
685
            break;
51
2.20k
        case '~':
  Branch (51:9): [True: 2.20k, False: 3.20M]
52
2.20k
            to[i] = '/';
53
2.20k
            break;
54
740
        case '+':
  Branch (54:9): [True: 740, False: 3.20M]
55
740
            to[i] = '-';
56
740
            break;
57
2.00k
        case '/':
  Branch (57:9): [True: 2.00k, False: 3.20M]
58
2.00k
            to[i] = '~';
59
2.00k
            break;
60
3.19M
        default:
  Branch (60:9): [True: 3.19M, False: 5.64k]
61
3.19M
            to[i] = from[i];
62
3.19M
            break;
63
3.20M
        }
64
3.20M
    }
65
524
    return to;
66
524
}
67
68
/**
69
 * Decode an I2P-style Base64 string.
70
 * @param[in] i2p_b64 I2P-style Base64 string.
71
 * @return decoded `i2p_b64`
72
 * @throw std::runtime_error if decoding fails
73
 */
74
static Binary DecodeI2PBase64(const std::string& i2p_b64)
75
253
{
76
253
    const std::string& std_b64 = SwapBase64(i2p_b64);
77
253
    auto decoded = DecodeBase64(std_b64);
78
253
    if (!decoded) {
  Branch (78:9): [True: 64, False: 189]
79
64
        throw std::runtime_error(strprintf("Cannot decode Base64: \"%s\"", i2p_b64));
80
64
    }
81
189
    return std::move(*decoded);
82
253
}
83
84
/**
85
 * Derive the .b32.i2p address of an I2P destination (binary).
86
 * @param[in] dest I2P destination.
87
 * @return the address that corresponds to `dest`
88
 * @throw std::runtime_error if conversion fails
89
 */
90
static CNetAddr DestBinToAddr(const Binary& dest)
91
182
{
92
182
    CSHA256 hasher;
93
182
    hasher.Write(dest.data(), dest.size());
94
182
    unsigned char hash[CSHA256::OUTPUT_SIZE];
95
182
    hasher.Finalize(hash);
96
97
182
    CNetAddr addr;
98
182
    const std::string addr_str = EncodeBase32(hash, false) + ".b32.i2p";
99
182
    if (!addr.SetSpecial(addr_str)) {
  Branch (99:9): [True: 0, False: 182]
100
0
        throw std::runtime_error(strprintf("Cannot parse I2P address: \"%s\"", addr_str));
101
0
    }
102
103
182
    return addr;
104
182
}
105
106
/**
107
 * Derive the .b32.i2p address of an I2P destination (I2P-style Base64).
108
 * @param[in] dest I2P destination.
109
 * @return the address that corresponds to `dest`
110
 * @throw std::runtime_error if conversion fails
111
 */
112
static CNetAddr DestB64ToAddr(const std::string& dest)
113
53
{
114
53
    const Binary& decoded = DecodeI2PBase64(dest);
115
53
    return DestBinToAddr(decoded);
116
53
}
117
118
namespace sam {
119
120
Session::Session(const fs::path& private_key_file,
121
                 const Proxy& control_host,
122
                 std::shared_ptr<CThreadInterrupt> interrupt)
123
337
    : m_private_key_file{private_key_file},
124
337
      m_control_host{control_host},
125
337
      m_interrupt{interrupt},
126
337
      m_transient{false}
127
337
{
128
337
}
129
130
Session::Session(const Proxy& control_host, std::shared_ptr<CThreadInterrupt> interrupt)
131
191
    : m_control_host{control_host},
132
191
      m_interrupt{interrupt},
133
191
      m_transient{true}
134
191
{
135
191
}
136
137
Session::~Session()
138
528
{
139
528
    LOCK(m_mutex);
140
528
    Disconnect();
141
528
}
142
143
bool Session::Listen(Connection& conn)
144
337
{
145
337
    try {
146
337
        LOCK(m_mutex);
147
337
        CreateIfNotCreatedAlready();
148
337
        conn.me = m_my_addr;
149
337
        conn.sock = StreamAccept();
150
337
        return true;
151
337
    } catch (const std::runtime_error& e) {
152
270
        LogError("Couldn't listen: %s\n", e.what());
153
270
        CheckControlSock();
154
270
    }
155
270
    return false;
156
337
}
157
158
bool Session::Accept(Connection& conn)
159
67
{
160
67
    AssertLockNotHeld(m_mutex);
161
162
67
    std::string errmsg;
163
67
    bool disconnect{false};
164
165
271
    while (!m_interrupt->interrupted()) {
  Branch (165:12): [True: 267, False: 4]
166
267
        Sock::Event occurred;
167
267
        if (!conn.sock->Wait(MAX_WAIT_FOR_IO, Sock::RecvEvent, &occurred)) {
  Branch (167:13): [True: 1, False: 266]
168
1
            errmsg = "wait on socket failed";
169
1
            break;
170
1
        }
171
172
266
        if (occurred == 0) {
  Branch (172:13): [True: 204, False: 62]
173
            // Timeout, no incoming connections or errors within MAX_WAIT_FOR_IO.
174
204
            continue;
175
204
        }
176
177
62
        std::string peer_dest;
178
62
        try {
179
62
            peer_dest = conn.sock->RecvUntilTerminator('\n', MAX_WAIT_FOR_IO, *m_interrupt, MAX_MSG_SIZE);
180
62
        } catch (const std::runtime_error& e) {
181
9
            errmsg = e.what();
182
9
            break;
183
9
        }
184
185
53
        CNetAddr peer_addr;
186
53
        try {
187
53
            peer_addr = DestB64ToAddr(peer_dest);
188
53
        } catch (const std::runtime_error& e) {
189
            // The I2P router is expected to send the Base64 of the connecting peer,
190
            // but it may happen that something like this is sent instead:
191
            // STREAM STATUS RESULT=I2P_ERROR MESSAGE="Session was closed"
192
            // In that case consider the session damaged and close it right away,
193
            // even if the control socket is alive.
194
36
            if (peer_dest.find("RESULT=I2P_ERROR") != std::string::npos) {
  Branch (194:17): [True: 6, False: 30]
195
6
                errmsg = strprintf("unexpected reply that hints the session is unusable: %s", peer_dest);
196
6
                disconnect = true;
197
30
            } else {
198
30
                errmsg = e.what();
199
30
            }
200
36
            break;
201
36
        }
202
203
17
        conn.peer = CService(peer_addr, I2P_SAM31_PORT);
204
205
17
        return true;
206
53
    }
207
208
50
    if (m_interrupt->interrupted()) {
  Branch (208:9): [True: 4, False: 46]
209
4
        LogDebug(BCLog::I2P, "Accept was interrupted\n");
210
46
    } else {
211
46
        LogDebug(BCLog::I2P, "Error accepting%s: %s\n", disconnect ? " (will close the session)" : "", errmsg);
212
46
    }
213
50
    if (disconnect) {
  Branch (213:9): [True: 6, False: 44]
214
6
        LOCK(m_mutex);
215
6
        Disconnect();
216
44
    } else {
217
44
        CheckControlSock();
218
44
    }
219
50
    return false;
220
67
}
221
222
bool Session::Connect(const CService& to, Connection& conn, bool& proxy_error)
223
2.21k
{
224
    // Refuse connecting to arbitrary ports. We don't specify any destination port to the SAM proxy
225
    // when connecting (SAM 3.1 does not use ports) and it forces/defaults it to I2P_SAM31_PORT.
226
2.21k
    if (to.GetPort() != I2P_SAM31_PORT) {
  Branch (226:9): [True: 441, False: 1.77k]
227
441
        LogDebug(BCLog::I2P, "Error connecting to %s, connection refused due to arbitrary port %s\n", to.ToStringAddrPort(), to.GetPort());
228
441
        proxy_error = false;
229
441
        return false;
230
441
    }
231
232
1.77k
    proxy_error = true;
233
234
1.77k
    std::string session_id;
235
1.77k
    std::unique_ptr<Sock> sock;
236
1.77k
    conn.peer = to;
237
238
1.77k
    try {
239
1.77k
        {
240
1.77k
            LOCK(m_mutex);
241
1.77k
            CreateIfNotCreatedAlready();
242
1.77k
            session_id = m_session_id;
243
1.77k
            conn.me = m_my_addr;
244
1.77k
            sock = Hello();
245
1.77k
        }
246
247
1.77k
        const Reply& lookup_reply =
248
1.77k
            SendRequestAndGetReply(*sock, strprintf("NAMING LOOKUP NAME=%s", to.ToStringAddr()));
249
250
1.77k
        const std::string& dest = lookup_reply.Get("VALUE");
251
252
1.77k
        const Reply& connect_reply = SendRequestAndGetReply(
253
1.77k
            *sock, strprintf("STREAM CONNECT ID=%s DESTINATION=%s SILENT=false", session_id, dest),
254
1.77k
            false);
255
256
1.77k
        const std::string& result = connect_reply.Get("RESULT");
257
258
1.77k
        if (result == "OK") {
  Branch (258:13): [True: 11, False: 1.76k]
259
11
            conn.sock = std::move(sock);
260
11
            return true;
261
11
        }
262
263
1.76k
        if (result == "INVALID_ID") {
  Branch (263:13): [True: 2, False: 1.76k]
264
2
            LOCK(m_mutex);
265
2
            Disconnect();
266
2
            throw std::runtime_error("Invalid session id");
267
2
        }
268
269
1.76k
        if (result == "CANT_REACH_PEER" || result == "TIMEOUT") {
  Branch (269:13): [True: 1.75k, False: 12]
  Branch (269:44): [True: 3, False: 9]
270
4
            proxy_error = false;
271
4
        }
272
273
1.76k
        throw std::runtime_error(strprintf("\"%s\"", connect_reply.full));
274
1.76k
    } catch (const std::runtime_error& e) {
275
1.76k
        LogDebug(BCLog::I2P, "Error connecting to %s: %s\n", to.ToStringAddrPort(), e.what());
276
1.76k
        CheckControlSock();
277
1.76k
        return false;
278
1.76k
    }
279
1.77k
}
280
281
// Private methods
282
283
std::string Session::Reply::Get(const std::string& key) const
284
1.34k
{
285
1.34k
    const auto& pos = keys.find(key);
286
1.34k
    if (pos == keys.end() || !pos->second.has_value()) {
  Branch (286:9): [True: 272, False: 1.07k]
  Branch (286:9): [True: 280, False: 1.06k]
  Branch (286:30): [True: 8, False: 1.06k]
287
280
        throw std::runtime_error(
288
280
            strprintf("Missing %s= in the reply to \"%s\"", key, request));
289
280
    }
290
1.06k
    return pos->second.value();
291
1.34k
}
292
293
Session::Reply Session::SendRequestAndGetReply(const Sock& sock,
294
                                               const std::string& request,
295
                                               bool check_result_ok) const
296
2.02k
{
297
2.02k
    sock.SendComplete(request + "\n", MAX_WAIT_FOR_IO, *m_interrupt);
298
299
2.02k
    Reply reply;
300
301
    // Don't log the full "SESSION CREATE ..." because it contains our private key.
302
2.02k
    reply.request = request.starts_with("SESSION CREATE") ? "SESSION CREATE ..." : request;
  Branch (302:21): [True: 239, False: 1.78k]
303
304
    // It could take a few minutes for the I2P router to reply as it is querying the I2P network
305
    // (when doing name lookup, for example). Notice: `RecvUntilTerminator()` is checking
306
    // `m_interrupt` more often, so we would not be stuck here for long if `m_interrupt` is
307
    // signaled.
308
2.02k
    static constexpr auto recv_timeout = 3min;
309
310
2.02k
    reply.full = sock.RecvUntilTerminator('\n', recv_timeout, *m_interrupt, MAX_MSG_SIZE);
311
312
92.1k
    for (const auto& kv : Split(reply.full, ' ')) {
  Branch (312:25): [True: 92.1k, False: 2.02k]
313
92.1k
        const auto pos{std::ranges::find(kv, '=')};
314
92.1k
        if (pos != kv.end()) {
  Branch (314:13): [True: 27.7k, False: 64.3k]
315
27.7k
            reply.keys.emplace(std::string{kv.begin(), pos}, std::string{pos + 1, kv.end()});
316
64.3k
        } else {
317
64.3k
            reply.keys.emplace(std::string{kv.begin(), kv.end()}, std::nullopt);
318
64.3k
        }
319
92.1k
    }
320
321
2.02k
    if (check_result_ok && reply.Get("RESULT") != "OK") {
  Branch (321:9): [True: 978, False: 1.04k]
  Branch (321:9): [True: 39, False: 1.98k]
  Branch (321:28): [True: 39, False: 939]
322
39
        throw std::runtime_error(
323
39
            strprintf("Reply to \"%s\": had a RESULT not equal to OK.", reply.request));
324
39
    }
325
326
1.98k
    return reply;
327
2.02k
}
328
329
std::unique_ptr<Sock> Session::Hello() const
330
2.27k
{
331
2.27k
    auto sock = m_control_host.Connect();
332
333
2.27k
    if (!sock) {
  Branch (333:9): [True: 933, False: 1.34k]
334
933
        throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToString()));
335
933
    }
336
337
1.34k
    SendRequestAndGetReply(*sock, "HELLO VERSION MIN=3.1 MAX=3.1");
338
339
1.34k
    return sock;
340
2.27k
}
341
342
void Session::CheckControlSock()
343
2.07k
{
344
2.07k
    LOCK(m_mutex);
345
346
2.07k
    std::string errmsg;
347
2.07k
    if (m_control_sock && !m_control_sock->IsConnected(errmsg)) {
  Branch (347:9): [True: 152, False: 1.92k]
  Branch (347:27): [True: 127, False: 25]
348
127
        LogDebug(BCLog::I2P, "Control socket error: %s\n", errmsg);
349
127
        Disconnect();
350
127
    }
351
2.07k
}
352
353
void Session::DestGenerate(const Sock& sock)
354
233
{
355
    // https://i2p.net/en/docs/specs/common-structures/#key-certificates
356
    // "7" or "EdDSA_SHA512_Ed25519" - "Recent Router Identities and Destinations".
357
    // Use "7" because i2pd <2.24.0 does not recognize the textual form.
358
    // If SIGNATURE_TYPE is not specified, then the default one is DSA_SHA1.
359
233
    const Reply& reply = SendRequestAndGetReply(sock, "DEST GENERATE SIGNATURE_TYPE=7", false);
360
361
233
    m_private_key = DecodeI2PBase64(reply.Get("PRIV"));
362
233
}
363
364
void Session::GenerateAndSavePrivateKey(const Sock& sock)
365
233
{
366
233
    DestGenerate(sock);
367
368
    // umask is set to 0077 in common/system.cpp, which is ok.
369
233
    if (!WriteBinaryFile(m_private_key_file,
  Branch (369:9): [True: 0, False: 233]
370
233
                         std::string(m_private_key.begin(), m_private_key.end()))) {
371
0
        throw std::runtime_error(
372
0
            strprintf("Cannot save I2P private key to %s", fs::quoted(fs::PathToString(m_private_key_file))));
373
0
    }
374
233
}
375
376
Binary Session::MyDestination() const
377
177
{
378
    // From https://i2p.net/en/docs/specs/common-structures/#destination:
379
    // "They are 387 bytes plus the certificate length specified at bytes 385-386, which may be
380
    // non-zero"
381
177
    static constexpr size_t DEST_LEN_BASE = 387;
382
177
    static constexpr size_t CERT_LEN_POS = 385;
383
384
177
    uint16_t cert_len;
385
386
177
    if (m_private_key.size() < CERT_LEN_POS + sizeof(cert_len)) {
  Branch (386:9): [True: 7, False: 170]
387
7
        throw std::runtime_error(strprintf("The private key is too short (%d < %d)",
388
7
                                           m_private_key.size(),
389
7
                                           CERT_LEN_POS + sizeof(cert_len)));
390
7
    }
391
392
170
    memcpy(&cert_len, &m_private_key.at(CERT_LEN_POS), sizeof(cert_len));
393
170
    cert_len = be16toh_internal(cert_len);
394
395
170
    const size_t dest_len = DEST_LEN_BASE + cert_len;
396
397
170
    if (dest_len > m_private_key.size()) {
  Branch (397:9): [True: 5, False: 165]
398
5
        throw std::runtime_error(strprintf("Certificate length (%d) designates that the private key should "
399
5
                                           "be %d bytes, but it is only %d bytes",
400
5
                                           cert_len,
401
5
                                           dest_len,
402
5
                                           m_private_key.size()));
403
5
    }
404
405
165
    return Binary{m_private_key.begin(), m_private_key.begin() + dest_len};
406
170
}
407
408
void Session::CreateIfNotCreatedAlready()
409
2.11k
{
410
2.11k
    std::string errmsg;
411
2.11k
    if (m_control_sock && m_control_sock->IsConnected(errmsg)) {
  Branch (411:9): [True: 34, False: 2.07k]
  Branch (411:27): [True: 16, False: 18]
412
16
        return;
413
16
    }
414
415
2.09k
    const auto session_type = m_transient ? "transient" : "persistent";
  Branch (415:31): [True: 1.43k, False: 658]
416
2.09k
    const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs
417
418
2.09k
    LogDebug(BCLog::I2P, "Creating %s I2P SAM session %s with %s\n", session_type, session_id, m_control_host.ToString());
419
420
2.09k
    auto sock = Hello();
421
422
2.09k
    if (m_transient) {
  Branch (422:9): [True: 4, False: 2.09k]
423
        // The destination (private key) is generated upon session creation and returned
424
        // in the reply in DESTINATION=.
425
4
        const Reply& reply = SendRequestAndGetReply(
426
4
            *sock,
427
4
            strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 "
428
4
                      "i2cp.leaseSetEncType=4,0 inbound.quantity=1 outbound.quantity=1",
429
4
                      session_id));
430
431
4
        m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
432
2.09k
    } else {
433
        // Read our persistent destination (private key) from disk or generate
434
        // one and save it to disk. Then use it when creating the session.
435
2.09k
        const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
436
2.09k
        if (read_ok) {
  Branch (436:13): [True: 99, False: 1.99k]
437
99
            m_private_key.assign(data.begin(), data.end());
438
1.99k
        } else {
439
1.99k
            GenerateAndSavePrivateKey(*sock);
440
1.99k
        }
441
442
2.09k
        const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
443
444
2.09k
        SendRequestAndGetReply(*sock,
445
2.09k
                               strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s "
446
2.09k
                                         "i2cp.leaseSetEncType=4,0 inbound.quantity=3 outbound.quantity=3",
447
2.09k
                                         session_id,
448
2.09k
                                         private_key_b64));
449
2.09k
    }
450
451
2.09k
    m_my_addr = CService(DestBinToAddr(MyDestination()), I2P_SAM31_PORT);
452
2.09k
    m_session_id = session_id;
453
2.09k
    m_control_sock = std::move(sock);
454
455
2.09k
    LogInfo("%s I2P SAM session %s created, my address=%s",
456
2.09k
        Capitalize(session_type),
457
2.09k
        m_session_id,
458
2.09k
        m_my_addr.ToStringAddrPort());
459
2.09k
}
460
461
std::unique_ptr<Sock> Session::StreamAccept()
462
108
{
463
108
    auto sock = Hello();
464
465
108
    const Reply& reply = SendRequestAndGetReply(
466
108
        *sock, strprintf("STREAM ACCEPT ID=%s SILENT=false", m_session_id), false);
467
468
108
    const std::string& result = reply.Get("RESULT");
469
470
108
    if (result == "OK") {
  Branch (470:9): [True: 67, False: 41]
471
67
        return sock;
472
67
    }
473
474
41
    if (result == "INVALID_ID") {
  Branch (474:9): [True: 3, False: 38]
475
        // If our session id is invalid, then force session re-creation on next usage.
476
3
        Disconnect();
477
3
    }
478
479
41
    throw std::runtime_error(strprintf("\"%s\"", reply.full));
480
108
}
481
482
void Session::Disconnect()
483
666
{
484
666
    if (m_control_sock) {
  Branch (484:9): [True: 157, False: 509]
485
157
        if (m_session_id.empty()) {
  Branch (485:13): [True: 0, False: 157]
486
0
            LogInfo("Destroying incomplete I2P SAM session");
487
157
        } else {
488
157
            LogInfo("Destroying I2P SAM session %s", m_session_id);
489
157
        }
490
157
        m_control_sock.reset();
491
157
    }
492
666
    m_session_id.clear();
493
666
}
494
} // namespace sam
495
} // namespace i2p