i2pd-sys 0.0.6

Raw FFI bindings to a minimal C shim over libi2pd (PurpleI2P/i2pd)
Documentation

i2pd-sys

Raw FFI bindings to a minimal C shim over libi2pd (PurpleI2P/i2pd), the reference C++ implementation of the I2P anonymous-networking protocol.

This crate builds a real I2P router in-process — not a client for a separately-running i2pd daemon — and exposes just enough of it (destinations and streams) through a small, stable extern "C" surface for Rust to call into safely at the ABI level.

Status: pre-0.1 (0.0.x), developed alongside and consumed by the tachyon-web workspace. The API may still change without notice.

Is this the crate you want?

This crate is entirely unsafe — raw pointers, manual lifetime management, no ownership types. Unless you need the bindgen-generated FFI surface itself (e.g. to build your own safe wrapper), you almost certainly want tachyon-i2p: an async, safe tokio-based wrapper built directly on this crate.

How it's built

build.rs does all of this offline — no network access, no shelling out to configure/b2/bootstrap.sh:

  1. Compiles a trimmed zlib snapshot (vendor/zlib-src) directly via cc::Build.
  2. Builds a minimal Boost subset (vendor/boost-src, extracted from the upstream git superproject with bcp) via Boost's own CMake support.
  3. Builds AWS-LC (via aws-lc-sys) as the crypto backend, standing in for OpenSSL — nothing OpenSSL-shaped is vendored or linked.
  4. Builds libi2pd (vendor/i2pd-src, a heavily trimmed snapshot) via its own CMake build, as a static library only (WITH_BINARY=OFF — no daemon binary).
  5. Compiles shim/shim.cpp, the flat C ABI declared in shim/shim.h, and links everything.
  6. Runs bindgen against shim/shim.h only — never against libi2pd's own C++ headers, which use std::shared_ptr/STL types with no stable C ABI.

Every dependency is a committed source snapshot, not a submodule or build-time fetch, so the crate builds reproducibly from a crates.io tarball with no network egress. scripts/update-vendor.sh shows how each was produced and refreshed.

Every C/C++ translation unit this crate compiles (zlib, i2pd, and the shim — all three either handle attacker-controlled network/decompression input directly or sit right next to code that does) is built with -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 and -fstack-protector-strong. These are applied as plain compiler flags in build.rs rather than through i2pd's own WITH_HARDENING CMake option, which upstream wires up for GCC only — a silent no-op under Clang.

Trimmed attack surface

The vendored snapshot is cut down to what a library embedding actually reaches. Upstream i2pd is a daemon; most of what it ships exists to serve that daemon's local clients, and none of it is reachable from shim.h. Removed wholesale by scripts/update-vendor.sh:

Removed Why it is safe
libi2pd_client/ — SAM, BOB, I2CP, SOCKS proxy, HTTP proxy, AddressBook, Torrents, I2P/UDP tunnels, ClientContext Upstream links libi2pdclient only into the daemon executable, which is never built here (WITH_BINARY=OFF). libi2pd includes nothing from it. Every file is a listening service this crate does not expose.
i18n/ — 22 translation tables Serve the daemon's web console, which is not built. libi2pd includes nothing from it.
libi2pd/Gost.{cpp,h} and its callers GOST R 34.10/34.11. This crate only ever generates Ed25519 destinations, but Identity.cpp parses identities from arbitrary remote peers, so this hand-rolled curve arithmetic was attacker-reachable for a capability never used.

The first two rows are dead by construction — libi2pd has no #include of either directory. The GOST removal is the one behavioural change: an identity declaring signing key type 9 or 10 is now rejected by the same default: path upstream already uses for RSA, rather than verified. Those types are vanishingly rare on the live network. It also retires 0003-awslc-gost-curve-compat.patch and drops Boost's dynamic_bitset, whose only consumer was libi2pd_client/Torrents.h.

Network participation

This crate runs a real router, so it has a position in the I2P network beyond hosting your own destinations. What it does and does not do:

Listens on NTCP2 (TCP) and SSU2 (UDP), IPv4. These are what make an eepsite reachable.
Never listens on Anything else. SOCKS, HTTP proxy, SAM, BOB, I2CP, I2PControl and the web console are not compiled in at all — see Trimmed attack surface.
Reads config from Nothing. InitI2P never parses i2pd.conf; only upstream's daemon did. Use the setters below.
Floodfill Off unless i2pd_set_floodfill(1).
Transit tunnels On by default.

Transit means carrying other users' tunnels. It is unrelated to your own destinations, which work either way — refusing transit does not make this router less able to host an eepsite.

It defaults to on because a router that relays nothing gives an observer no cover traffic: every byte crossing your link is then yours, which makes correlating your service easier, and the refusal is itself a fingerprint. Turn it off when bandwidth is metered, or when the risk you care about is a memory-safety bug in i2pd rather than traffic analysis. Building without the default transit feature is the strong form — the build-request path is compiled out, so no transit tunnel can exist regardless of runtime settings, and i2pd_accepts_transit() reports what the build does.

To keep transit but bound its cost, call these between i2pd_init() and i2pd_start():

i2pd_set_bandwidth_limit(512);      /* KB/s, whole router. Default 256. */
i2pd_set_share_percent(25);         /* of that limit, transit may use 25%. Default 100. */
i2pd_set_max_transit_tunnels(500);  /* i2pd's own default is 25000. */
i2pd_set_accepts_transit(0);        /* or refuse transit outright at runtime */

A bandwidth limit is always in force, and this matters more than it sounds: i2pd leaves RouterContext::m_BandwidthLimit out of its constructor's initializer list and only ever set it from the daemon's config file, which this crate does not read. Left at the resulting 0, Transports::GetCongestionLevel() returns CONGESTION_LEVEL_FULL unconditionally — the router publishes itself as congested, peers deprioritize it, and it refuses transit anyway. i2pd_init() therefore applies 256 KB/s, below upstream's own 2048 KB/s on a quad-core x86_64: that figure suits a dedicated router, not a library sharing a server's uplink where handing 2 MB/s to strangers arrives with an invoice. Raise it if the machine is meant to carry more.

Requirements

  • A C++17 compiler (cc/c++ on the host, found automatically).
  • CMake (used to build Boost and libi2pd).
  • Rust 1.89+ (edition 2024).

No system boost, openssl, or i2pd packages are required or used — everything is vendored and statically linked.

Cross-compilation

Native builds, including natively inside a musl system such as an Alpine container, need nothing beyond the ambient cc/c++. True cross-compilation (e.g. a glibc host targeting x86_64-unknown-linux-musl) needs a real musl C++ cross toolchain, since no C++ compiler shipped by default on a glibc distro can target musl. build.rs looks for one in order: CC_<target>/CXX_<target> (the cc convention), MUSL_CROSS_TOOLCHAIN pointing at a musl-cross-make-style install containing bin/<target>-g++, then <target>-g++ on $PATH. If none is found the build fails with an explanation rather than building a toolchain — a 20–40 minute first-build surprise.

Usage

use i2pd_sys::*;
use std::ffi::CString;

unsafe {
    let app_name = CString::new("my-app").unwrap();
    i2pd_init(app_name.as_ptr()); // before anything else
    i2pd_start();

    let dest = i2pd_create_transient_destination();
    assert!(!dest.is_null());

    let addr = i2pd_destination_b32_address(dest);
    // ... read the `<52 chars>.b32.i2p` C string, then:
    i2pd_free_string(addr);

    i2pd_destroy_destination(dest);
    i2pd_stop();
    i2pd_terminate();
}

shim/shim.h documents the full API surface — lifecycle, participation, destinations, streams, memory. tests/roundtrip.rs is a complete two-destination, real-network round trip.

Thread safety

i2pd_stream_send/i2pd_stream_receive are safe to call concurrently on the same stream (one sending, one receiving) — each posts work onto i2pd's internal io_service and blocks the calling thread on a condition variable, the same pattern i2pd's own SAM/BOB bridges use. The callback registered via i2pd_accept_stream runs on i2pd's own thread and must return quickly, or it stalls the event loop for every destination sharing that router.

The four lifecycle calls share one mutex held across the underlying libi2pd call, so a repeated or out-of-order call is a no-op rather than a second pass over libi2pd's non-reentrant globals, and a racing caller blocks until the transition it observed has finished. i2pd_terminate stops a still-running router first, so init → start → terminate → init → start works.

Every shim function catches all C++ exceptions internally, including that callback, where an escaping exception would unwind into Boost.Asio's event loop. Failures surface as NULL/0/-1. i2pd_free_buffer takes the length i2pd_generate_keys reported and wipes the buffer first: it only ever frees serialized private keys, which plain free would leave readable in the heap.

Testing

cargo test -p i2pd-sys                                             # no network
cargo test -p i2pd-sys --no-default-features --features fips       # the FIPS backend
cargo test -p i2pd-sys --test roundtrip -- --ignored --nocapture   # live I2P round trip

The round-trip test is #[ignore]d by default: it builds real tunnels over the live I2P network between two in-process destinations, which can take several minutes.

Vendored third-party code

Component License Snapshot
i2pd BSD-3-Clause tag 2.61.0, commit 635b013
Boost (minimal subset via bcp) Boost Software License 1.0 tag boost-1.91.0
zlib zlib License 1.3.2
AWS-LC (via aws-lc-sys, not vendored directly) Apache-2.0 OR ISC

Eight small patches (scripts/patches/) are applied to pristine upstream source automatically by i2pd_snapshot in scripts/update-vendor.sh. Four are AWS-LC compatibility patches. Three (0001, 0002, 0005) are #ifdef OPENSSL_IS_AWSLC-guarded, working around AWS-LC's OpenSSL-1.1.1 API gaps: siphash, raw non-AEAD ChaCha20, and — the largest — a from-scratch ML-KEM implementation against AWS-LC's own KEM API, see Post-quantum (ML-KEM) support. All three apply equally to the fips backend, since AWS-LC-FIPS defines the same OPENSSL_IS_AWSLC marker. The fourth (0004) is unconditional: a missing #include <openssl/hmac.h> that only matters under the fips backend's slightly different header graph.

The other four trim the tree. 0006 and 0007 drop the daemon-only CMake targets and GOST, 0008 drops the config options for services that are no longer built, and 0009 makes transit participation compile-time optional — see Trimmed attack surface and Network participation.

Full attribution and license text: LICENSE-THIRD-PARTY.

Post-quantum (ML-KEM) support

i2pd's own ML-KEM code — what lets a destination publish a MLKEM768_X25519 LeaseSet2 encryption key alongside classical ECIES_X25519/ELGAMAL_2048 ones, for "harvest now, decrypt later" resistance — targets OpenSSL 3.5's provider API (EVP_PKEY_CTX_new_from_name, EVP_PKEY_fromdata, OSSL_PARAM, EVP_PKEY_{en,de}capsulate_init). AWS-LC implements none of it, so upstream's OPENSSL_PQ gate never fires for AWS-LC builds and the code is compiled out by default.

scripts/patches/0005-awslc-mlkem-compat.patch reimplements the same MLKEMKeys methods against AWS-LC's own API: EVP_PKEY_CTX_new_id(EVP_PKEY_KEM) with EVP_PKEY_CTX_kem_set_params for keygen, a plain EVP_PKEY_encapsulate/decapsulate with no separate _init, and EVP_PKEY_kem_new_raw_public_key to rebuild a peer's public key from the raw bytes I2P's wire format carries. It also flips OPENSSL_PQ on for AWS-LC in Crypto.h, which makes Destination.cpp's existing fallback logic prefer ECIES_MLKEM768_X25519_AEAD — so every destination created through this crate publishes hybrid PQ+classical LeaseSet2 keys by default.

The raw-key constructor matters: an earlier version hand-built a DER SubjectPublicKeyInfo for d2i_PUBKEY, which worked under aws-lc but failed under fips with UNSUPPORTED_ALGORITHM, since AWS-LC-FIPS registers no pub_decode for the ML-KEM OID. No caller of Encaps checks its return value, so that failed silently — the shared secret was left untouched and the session key corrupted, surfacing much later as an AEAD tag mismatch pointing nowhere near ML-KEM.

Verified: tests/mlkem_roundtrip.rs runs a real generate → export public key → reconstruct from raw bytes as a peer would → encapsulate → decapsulate → compare shared secrets byte for byte, for all three variants (512/768/1024), against both backends. A destination created this way also publishes the expected three-key LeaseSet2 and is reachable over the live network through a real router's HTTP proxy.

Not verified: wire-level interop of the hybrid Noise ratchet handshake against other live i2pd/Java I2P peers. The patch touches only the KEM primitive, not the ratchet code that consumes it, which is unmodified upstream. Treat it as "the crypto primitive works, on both backends," not as an audit of protocol interoperability, and review it before relying on it for anything security-sensitive — it is the largest and newest of the four and touches post-quantum key material.

FIPS

By default this crate links regular AWS-LC via aws-lc-sys. The fips feature (default-features = false, features = ["fips"]) swaps in the FIPS 140-3-validated AWS-LC-FIPS module via aws-lc-fips-sys, with no manual source changes needed beyond the four AWS-LC patches above.

The two aren't hard-exclusive: if a dependent crate's default pulls in aws-lc while something further up the tree asks for fips, fips silently wins. This crate emits link directives for one backend only and the two use per-version-prefixed symbol names, so an inert copy of the other is harmless. default-features = false only matters to keep aws-lc-sys from compiling at all.

Linking a FIPS-validated crypto module is not a FIPS-certified application. i2pd's protocol code hasn't been evaluated by NIST, and some algorithm choices it still supports — legacy ElGamal destinations, for instance — aren't FIPS-approved at all; those paths work identically either way, they just don't contribute to any compliance claim. GOST is not among them: it is removed from the snapshot entirely. Treat fips as "the underlying crypto library has been through FIPS 140-3 validation," not "this application is FIPS compliant" — check with whoever owns your requirement.

It costs a much larger build (~53 MiB of vendored source, plus the self-test and integrity-check infrastructure the non-FIPS module lacks) and a mandatory runtime self-check: AWS-LC-FIPS links a startup hook into every dependent binary that aborts the process if the runtime library isn't in FIPS mode. That is by design, but a binary that built fine can still abort at startup on a host without it. Pregenerated bindings cover only {x86_64,aarch64}-unknown-linux-{gnu,musl} and {x86_64,aarch64}-apple-darwin; other targets may work via on-the-fly bindgen, unvalidated here.

License

Licensed under either of

at your option, in addition to the vendored third-party licenses listed above (which govern the statically-linked C/C++ code, not this crate's own Rust source) — see LICENSE-THIRD-PARTY for the full text and attribution of each.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.