i2pd-sys 0.0.5

Raw FFI bindings to a minimal C shim over libi2pd (PurpleI2P/i2pd).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
/* Implements shim.h -- see there for the boundary contract. i2pd throws, and unwinding across
 * extern "C" is undefined behavior, so every body goes through guard/guard_void. */
#include "shim.h"

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>

#include "api.h"
#include "Destination.h"
#include "Streaming.h"
#include "Identity.h"
#include "PostQuantum.h"
#include "RouterContext.h"
#include "Tunnel.h"

struct I2pdDestination {
    std::shared_ptr<i2p::client::ClientDestination> ptr;
};

struct I2pdStream {
    std::shared_ptr<i2p::stream::Stream> ptr;
};

namespace {

/* `fallback` is a non-deduced context so `nullptr` converts to the lambda's pointer type. */
template <typename F>
std::invoke_result_t<F> guard(F &&f, std::invoke_result_t<F> fallback) noexcept {
    try {
        return static_cast<F &&>(f)();
    } catch (...) {
        return fallback;
    }
}

template <typename F>
void guard_void(F &&f) noexcept {
    try {
        static_cast<F &&>(f)();
    } catch (...) {
    }
}

/* volatile so the write survives dead-store elimination on a buffer about to be freed. */
void secure_zero(void *p, size_t len) noexcept {
    if (!p || !len)
        return;
    volatile unsigned char *v = static_cast<volatile unsigned char *>(p);
    while (len--)
        *v++ = 0;
}

bool ct_equal(const uint8_t *a, const uint8_t *b, size_t len) noexcept {
    unsigned char diff = 0;
    for (size_t i = 0; i < len; i++)
        diff |= static_cast<unsigned char>(a[i] ^ b[i]);
    return diff == 0;
}

/* 'O' = 256 KB/s, applied at init because nothing else will: m_BandwidthLimit is missing from
 * RouterContext's init list and only the daemon ever called SetBandwidth. At the resulting 0,
 * GetCongestionLevel returns CONGESTION_LEVEL_FULL unconditionally. Below the daemon's own 'P'
 * (2048 KB/s), which assumes a dedicated router rather than a library on someone's server. */
constexpr char kDefaultBandwidthClass = i2p::data::CAPS_FLAG_HIGH_BANDWIDTH;

/* send/receive return `long`; a larger count would wrap negative and read as the -1 error. */
constexpr size_t kMaxIoLen = static_cast<size_t>(std::numeric_limits<long>::max());

/* A mutex rather than atomic flags, held across the libi2pd call: InitI2P/TerminateI2P are not
 * re-entrant, and flags flipped before the call would let a racing i2pd_start() run against
 * half-constructed globals. These run a handful of times per process. */
std::mutex g_lifecycle_mutex;
bool g_initialized = false;
bool g_started = false;

} // namespace

extern "C" {

void i2pd_init(const char *app_name) {
    if (!app_name)
        return;
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (g_initialized)
        return;
    // Assigned, not latched: a failed init leaves the flag clear so the caller can retry.
    g_initialized = guard(
        [&] {
            // program_options wants writable `char*[]`, and argv[argc] must be NULL.
            std::vector<char> name(app_name, app_name + std::strlen(app_name) + 1);
            char *argv[] = {name.data(), nullptr};
            i2p::api::InitI2P(1, argv, app_name);
            // After InitI2P: SetBandwidth updates the RouterInfo that context.Init() creates.
            i2p::context.SetBandwidth(kDefaultBandwidthClass);
            return true;
        },
        false);
}

void i2pd_start(void) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized || g_started)
        return;
    g_started = guard([] { i2p::api::StartI2P(); return true; }, false);
}

void i2pd_stop(void) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_started)
        return;
    g_started = false;
    guard_void([] { i2p::api::StopI2P(); });
}

/* Under the lifecycle lock: these write router globals i2pd_start() reads, so a setter racing a
 * start would be lost or torn. Applying after start is unsupported (see shim.h). */

int i2pd_accepts_transit(void) {
#ifdef I2PD_SYS_NO_TRANSIT
    return 0;
#else
    return 1;
#endif
}

void i2pd_set_accepts_transit(int enabled) {
#ifdef I2PD_SYS_NO_TRANSIT
    // The build-request path is compiled out; advertising availability would only attract
    // requests this router then silently drops.
    (void)enabled;
#else
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized || g_started)
        return;
    guard_void([&] { i2p::context.SetAcceptsTunnels(enabled != 0); });
#endif
}

void i2pd_set_bandwidth_limit(int kbps) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized || g_started)
        return;
    guard_void([&] {
        if (kbps > 0)
            i2p::context.SetBandwidth(kbps);
        else
            i2p::context.SetBandwidth(kDefaultBandwidthClass);
    });
}

void i2pd_set_share_percent(int percent) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized || g_started)
        return;
    const int clamped = percent < 0 ? 0 : (percent > 100 ? 100 : percent);
    guard_void([&] { i2p::context.SetShareRatio(clamped); });
}

void i2pd_set_max_transit_tunnels(int max_tunnels) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized || g_started || max_tunnels <= 0)
        return;
    guard_void([&] {
        i2p::tunnel::tunnels.SetMaxNumTransitTunnels(static_cast<uint32_t>(max_tunnels));
    });
}

void i2pd_set_floodfill(int enabled) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized || g_started)
        return;
    guard_void([&] { i2p::context.SetFloodfill(enabled != 0); });
}

void i2pd_terminate(void) {
    std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
    if (!g_initialized)
        return;
    // Skipping stop must not be unrecoverable: leaving g_started set would silently no-op every
    // later i2pd_start(), on top of tearing crypto globals out from under live transports.
    if (g_started) {
        g_started = false;
        guard_void([] { i2p::api::StopI2P(); });
    }
    g_initialized = false;
    guard_void([] { i2p::api::TerminateI2P(); });
}

I2pdDestination *i2pd_create_transient_destination(void) {
    return guard(
        []() -> I2pdDestination * {
            auto dest = i2p::api::CreateLocalDestination(/*isPublic=*/true);
            return dest ? new I2pdDestination{std::move(dest)} : nullptr;
        },
        nullptr);
}

int i2pd_generate_keys(int sig_type, int crypto_type, unsigned char **out_buf, size_t *out_len) {
    (void)crypto_type; // see shim.h: kept for ABI stability, deliberately ignored
    if (!out_buf || !out_len)
        return 0;
    return guard(
        [&] {
            auto keys = i2p::data::PrivateKeys::CreateRandomKeys(
                static_cast<i2p::data::SigningKeyType>(sig_type),
                i2p::data::CRYPTO_KEY_TYPE_ELGAMAL,
                /*isDestination=*/true);
            size_t len = keys.GetFullLen();
            if (!len)
                return 0;
            auto *buf = static_cast<unsigned char *>(std::malloc(len));
            if (!buf)
                return 0;
            if (!guard([&] { keys.ToBuffer(buf, len); return true; }, false)) {
                // ToBuffer may have written part of the private key before throwing.
                secure_zero(buf, len);
                std::free(buf);
                return 0;
            }
            *out_buf = buf;
            *out_len = len;
            return 1;
        },
        0);
}

I2pdDestination *i2pd_create_persistent_destination(const unsigned char *keys_buf, size_t keys_len, int is_public, const char *encryption_types_csv) {
    if (!keys_buf || !keys_len)
        return nullptr;
    return guard(
        [&]() -> I2pdDestination * {
            i2p::data::PrivateKeys keys;
            if (keys.FromBuffer(keys_buf, keys_len) == 0)
                return nullptr;
            std::shared_ptr<i2p::client::ClientDestination> dest;
            if (encryption_types_csv && *encryption_types_csv) {
                i2p::util::Mapping params;
                params.Insert(i2p::client::I2CP_PARAM_LEASESET_ENCRYPTION_TYPE, encryption_types_csv);
                dest = i2p::api::CreateLocalDestination(keys, is_public != 0, &params);
            } else {
                dest = i2p::api::CreateLocalDestination(keys, is_public != 0);
            }
            return dest ? new I2pdDestination{std::move(dest)} : nullptr;
        },
        nullptr);
}

void i2pd_free_buffer(unsigned char *ptr, size_t len) {
    // Only ever a serialized PrivateKeys, so wiping is the default -- hence the length parameter.
    secure_zero(ptr, len);
    std::free(ptr);
}

int i2pd_test_mlkem_roundtrip(int mlkem_variant) {
    return guard(
        [&] {
            i2p::data::CryptoKeyType cryptoType;
            switch (mlkem_variant) {
                case 0: cryptoType = i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM512_X25519_AEAD; break;
                case 1: cryptoType = i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM768_X25519_AEAD; break;
                case 2: cryptoType = i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM1024_X25519_AEAD; break;
                default: return 0;
            }

            auto responder = i2p::crypto::CreateMLKEMKeys(cryptoType);
            auto initiator = i2p::crypto::CreateMLKEMKeys(cryptoType);
            if (!responder || !initiator)
                return 0;
            responder->GenerateKeys();

            std::vector<uint8_t> pub(responder->GetKeyLen());
            responder->GetPublicKey(pub.data());
            initiator->SetPublicKey(pub.data());

            std::vector<uint8_t> ciphertext(initiator->GetCTLen());
            uint8_t sharedInitiator[32] = {0}, sharedResponder[32] = {0};
            initiator->Encaps(ciphertext.data(), sharedInitiator);
            responder->Decaps(ciphertext.data(), sharedResponder);

            int ok = ct_equal(sharedInitiator, sharedResponder, sizeof(sharedInitiator)) ? 1 : 0;
            secure_zero(sharedInitiator, sizeof(sharedInitiator));
            secure_zero(sharedResponder, sizeof(sharedResponder));
            return ok;
        },
        0);
}

void i2pd_destroy_destination(I2pdDestination *dest) {
    if (!dest)
        return;
    guard_void([&] {
        if (dest->ptr) {
            dest->ptr->StopAcceptingStreams();
            i2p::api::DestroyLocalDestination(dest->ptr);
        }
    });
    delete dest;
}

char *i2pd_destination_b32_address(I2pdDestination *dest) {
    if (!dest || !dest->ptr)
        return nullptr;
    return guard(
        [&]() -> char * {
            const std::string addr = dest->ptr->GetIdentHash().ToBase32() + ".b32.i2p";
            char *out = static_cast<char *>(std::malloc(addr.size() + 1));
            if (!out)
                return nullptr;
            std::memcpy(out, addr.c_str(), addr.size() + 1);
            return out;
        },
        nullptr);
}

int i2pd_destination_ident_hash(I2pdDestination *dest, unsigned char *out) {
    static_assert(sizeof(i2p::data::IdentHash) == I2PD_IDENT_HASH_LEN,
                  "libi2pd's IdentHash is no longer 32 bytes -- shim.h's contract must change");
    if (!dest || !dest->ptr || !out)
        return 0;
    return guard(
        [&] {
            std::memcpy(out, dest->ptr->GetIdentHash().data(), I2PD_IDENT_HASH_LEN);
            return 1;
        },
        0);
}

void i2pd_free_string(char *ptr) {
    std::free(ptr);
}

void i2pd_accept_stream(I2pdDestination *dest, I2pdAcceptCallback cb, void *ctx) {
    if (!dest || !dest->ptr || !cb)
        return;
    guard_void([&] {
        i2p::api::AcceptStream(dest->ptr, [cb, ctx](std::shared_ptr<i2p::stream::Stream> stream) {
            guard_void([&] {
                if (!stream)
                    return;
                auto handle = std::make_unique<I2pdStream>(I2pdStream{std::move(stream)});
                cb(ctx, handle.get());
                (void)handle.release();
            });
        });
    });
}

I2pdStream *i2pd_create_stream(I2pdDestination *dest, const unsigned char *remote_ident_hash, int timeout_seconds) {
    if (!dest || !dest->ptr || !remote_ident_hash)
        return nullptr;
    return guard(
        [&]() -> I2pdStream * {
            const i2p::data::IdentHash hash(remote_ident_hash);
            const auto deadline = std::chrono::steady_clock::now() +
                                  std::chrono::seconds(timeout_seconds > 0 ? timeout_seconds : 0);
            for (;;) {
                auto stream = i2p::api::CreateStream(dest->ptr, hash);
                if (stream)
                    return new I2pdStream{std::move(stream)};
                const auto now = std::chrono::steady_clock::now();
                if (now >= deadline)
                    return nullptr;
                std::this_thread::sleep_for(std::min<std::chrono::steady_clock::duration>(
                    deadline - now, std::chrono::seconds(1)));
            }
        },
        nullptr);
}

long i2pd_stream_send(I2pdStream *stream, const unsigned char *buf, size_t len) {
    if (!stream || !stream->ptr || (!buf && len))
        return -1;
    if (len > kMaxIoLen)
        len = kMaxIoLen;
    return guard([&] { return static_cast<long>(stream->ptr->Send(buf, len)); }, -1L);
}

long i2pd_stream_receive(I2pdStream *stream, unsigned char *buf, size_t len, int timeout_seconds) {
    if (!stream || !stream->ptr || (!buf && len))
        return -1;
    if (len > kMaxIoLen)
        len = kMaxIoLen;
    return guard([&] { return static_cast<long>(stream->ptr->Receive(buf, len, timeout_seconds)); }, -1L);
}

int i2pd_stream_is_open(I2pdStream *stream) {
    if (!stream || !stream->ptr)
        return 0;
    return guard([&] { return stream->ptr->IsOpen() ? 1 : 0; }, 0);
}

void i2pd_stream_close(I2pdStream *stream) {
    if (!stream || !stream->ptr)
        return;
    guard_void([&] { stream->ptr->Close(); });
}

void i2pd_destroy_stream(I2pdStream *stream) {
    if (!stream)
        return;
    guard_void([&] {
        if (stream->ptr)
            stream->ptr->Close();
    });
    delete stream;
}

} // extern "C"