i2pd-sys 0.0.4

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.4), 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 100% unsafe — raw pointers, manual lifetime management, no ownership types. Unless you specifically need the bindgen-generated FFI surface itself (e.g. you're building your own safe wrapper), you almost certainly want tachyon-i2p instead: an async, safe tokio-based wrapper built directly on top of this crate.

How it's built

build.rs does the following, entirely 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 the aws-lc-sys dependency) as the crypto/TLS backend, standing in for OpenSSL — nothing OpenSSL-shaped is vendored or linked.
  4. Builds libi2pd/libi2pdclient (vendor/i2pd-src, a trimmed snapshot of upstream i2pd) via its own CMake build, as static libraries only (WITH_BINARY=OFF — no daemon binary).
  5. Compiles shim/shim.cpp, a hand-written C++ file implementing the flat C ABI declared in shim/shim.h, and links everything together.
  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 a build-time fetch, so the crate builds reproducibly and works from a crates.io tarball with no network egress at build time. See scripts/update-vendor.sh for how each snapshot was produced and how to refresh one against a newer upstream release.

Hardening: 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 -D_FORTIFY_SOURCE=2 and -fstack-protector-strong, applied as plain compiler flags in build.rs rather than relying on i2pd's own WITH_HARDENING CMake option (which is wired up for GCC only upstream — a silent no-op under Clang).

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 building natively inside a musl system, e.g. an Alpine container) need nothing beyond the ambient cc/c++. True cross-compilation (e.g. a glibc host targeting x86_64-unknown-linux-musl) requires 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:

  1. CC_<target> / CXX_<target> env vars (the standard cc crate convention), e.g. CXX_x86_64_unknown_linux_musl.
  2. MUSL_CROSS_TOOLCHAIN pointing at a musl-cross-make-style install (expects <dir>/bin/<target-triple>-g++).
  3. A same-named compiler already on $PATH (<target-triple>-g++).

If none is found, the build fails with an explanation rather than attempting to build a toolchain from scratch (a 20–40 minute surprise on a library crate's first build).

Usage

[dependencies]
i2pd-sys = "0.0.4"
use i2pd_sys::*;
use std::ffi::{c_void, CString};

unsafe {
    // 1. Initialize i2pd's config/crypto/router-context globals — exactly once, before
    //    anything else.
    let app_name = CString::new("my-app").unwrap();
    i2pd_init(app_name.as_ptr());
    i2pd_start();

    // 2. Create a destination (an I2P address you can send/receive on).
    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);

    // 3. Register an acceptor, then create streams to other destinations with
    //    i2pd_create_stream / i2pd_stream_send / i2pd_stream_receive.

    // 4. Tear down in reverse order.
    i2pd_destroy_destination(dest);
    i2pd_stop();
    i2pd_terminate();
}

See shim/shim.h for the full, documented API surface (lifecycle, destinations, streams), and tests/roundtrip.rs for a complete two-destination, real-network round trip.

API overview

Group Functions
Lifecycle i2pd_init, i2pd_start, i2pd_stop, i2pd_terminate
Destinations i2pd_create_transient_destination, i2pd_generate_keys, i2pd_create_persistent_destination, i2pd_destroy_destination, i2pd_destination_b32_address, i2pd_destination_ident_hash, i2pd_accept_stream
Streams i2pd_create_stream, i2pd_stream_send, i2pd_stream_receive, i2pd_stream_is_open, i2pd_stream_close, i2pd_destroy_stream
Memory i2pd_free_buffer, i2pd_free_string

Thread safety

i2pd_stream_send/i2pd_stream_receive are safe to call concurrently from different threads on the same stream (one thread sending, another 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 inbound-stream callback registered via i2pd_accept_stream runs on i2pd's own internal thread and must return quickly without blocking, or it stalls the event loop for every destination sharing that router.

Every shim function catches all C++ exceptions internally (try { ... } catch (...) {}), since an exception unwinding across the extern "C" boundary would be undefined behavior — failures surface as NULL/0/-1 return values instead.

Testing

cargo test -p i2pd-sys                              # build + unit tests, no network
cargo test -p i2pd-sys --test roundtrip -- --ignored --nocapture   # live I2P network 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.86.0
zlib zlib License 1.3.1
AWS-LC (via aws-lc-sys, not vendored directly) Apache-2.0 OR ISC

Six small compatibility patches (scripts/patches/) are applied automatically (by i2pd_snapshot in scripts/update-vendor.sh) on top of pristine upstream i2pd source. Five are #ifdef OPENSSL_IS_AWSLC-guarded, working around AWS-LC's OpenSSL-1.1.1 API-compatibility gaps (siphash, GOST curve naming, raw ChaCha20, compressed EC point decoding, and — the newest, largest one — a from-scratch ML-KEM implementation against AWS-LC's own KEM API; see "Post-quantum (ML-KEM) support" below); those five apply equally to the fips backend, since AWS-LC-FIPS defines the same OPENSSL_IS_AWSLC marker. The sixth is unconditional (a missing #include <openssl/hmac.h> that only matters — see the "FIPS" section below — under the fips backend's slightly different header graph). Full attribution and license text: LICENSE-THIRD-PARTY.

Post-quantum (ML-KEM) support

i2pd's own ML-KEM (hybrid post-quantum) code — the thing that lets a destination publish a MLKEM768_X25519 LeaseSet2 encryption key alongside classical ECIES_X25519/ELGAMAL_2048 ones, for "harvest now, decrypt later" resistance — is written against 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 that (confirmed against its own headers — zero matches for any of those symbols), so upstream's OPENSSL_PQ gate never fires for AWS-LC builds and this code is compiled out entirely by default.

scripts/patches/0005-awslc-mlkem-compat.patch reimplements the same MLKEMKeys methods against AWS-LC's own (differently-shaped, non-"provider") ML-KEM APIEVP_PKEY_CTX_new_id(EVP_PKEY_KEM) + EVP_PKEY_CTX_kem_set_params for keygen, a plain EVP_PKEY_encapsulate/decapsulate with no separate _init call, and — since AWS-LC's EVP_PKEY_new_raw_public_key doesn't accept EVP_PKEY_KEM at all — a small hand-built DER SubjectPublicKeyInfo wrapper (mirroring exactly what AWS-LC's own kem_pub_encode produces) fed through d2i_PUBKEY to reconstruct a peer's public key from the raw bytes I2P's wire format carries. Once this patch is applied, OPENSSL_PQ is also flipped on for AWS-LC in Crypto.h, which means every destination created through this crate publishes hybrid PQ+classical LeaseSet2 encryption keys by defaultlibi2pd's own existing (unmodified) fallback logic in Destination.cpp already prefers ECIES_MLKEM768_X25519_AEAD over ECIES_X25519_AEAD/ ELGAMAL_2048 whenever OPENSSL_PQ is set; this patch is what makes that condition true.

What's been verified: the ML-KEM-768 primitive itself round-trips correctly under this patch (generate → export public key → reconstruct from raw bytes as a peer would → encapsulate → decapsulate → compare shared secrets byte-for-byte, on both the AWS-LC and AWS-LC-FIPS backends this crate supports), and a destination created this way publishes the expected three-key LeaseSet2 and is reachable over the live I2P network through a real router's HTTP proxy.

What has not been independently verified: wire-level interop of the ML-KEM hybrid Noise ratchet handshake itself against other live i2pd/Java I2P peers — this patch only touches the underlying KEM primitive (MLKEMKeys), not the ratchet protocol code in ECIESX25519AEADRatchetSession.cpp that consumes it, which is unmodified upstream i2pd. Treat this as "the crypto primitive works," not as an independent audit of the protocol-level interoperability claim. Review this patch specifically — it's larger and newer than the other five, touches post-quantum key material, and hand-builds DER — before relying on it for anything security-sensitive.

FIPS

By default this crate links regular AWS-LC (via aws-lc-sys). Enabling the fips feature instead (default-features = false, features = ["fips"]) swaps in the FIPS 140-3-validated AWS-LC-FIPS module via aws-lc-fips-sys instead, with no manual source changes needed on i2pd's side beyond the five patches above (four shared with the default backend, one FIPS-specific) applied automatically by scripts/update-vendor.sh.

aws-lc/fips aren't a hard-exclusive pair: if a dependent crate's own default pulls in aws-lc while something further up the dependency tree also asks for fips (e.g. a workspace-wide FIPS switch), fips silently takes priority — this crate only ever emits link directives for one backend (see build.rs), and aws-lc-sys/aws-lc-fips-sys link with per-version-prefixed symbol names, so an inert, unused copy of the other backend being compiled in alongside it is harmless. You only need default-features = false if you want to guarantee aws-lc-sys isn't compiled at all (e.g. to keep build times down).

What this does and does not get you: linking a FIPS-validated crypto module is not the same thing as a FIPS-certified application. i2pd's own protocol code hasn't been evaluated by NIST, and some of i2pd's supported algorithm choices (legacy ElGamal destinations, GOST signatures — libi2pd/Gost.cpp) aren't FIPS-approved at all; those code paths still compile and still work identically either way (they only use generic, algorithm-agnostic EC/bignum primitives, not a FIPS "service"), they just don't contribute to any compliance claim. Treat fips as "the underlying crypto library has been through FIPS 140-3 validation," not as "this application is FIPS compliant" — check with whoever owns your compliance requirement before relying on it as one.

Costs of enabling it:

  • A much larger build: aws-lc-fips-sys vendors the full AWS-LC-FIPS source tree (~53 MiB) and compiles considerably more (self-test and integrity-check infrastructure the non-FIPS module doesn't have).
  • A mandatory runtime self-check: AWS-LC-FIPS links a startup hook into every dependent binary that calls FIPS_mode() and aborts the process if the runtime library isn't actually running in FIPS mode. This is by design (it's what "FIPS-validated" requires operationally), but it's a real behavior change worth knowing about before flipping the feature on in production — a binary that built fine can still abort at startup on a host where FIPS mode isn't available.
  • Narrower pregenerated-bindings platform support than the default backend: currently {x86_64,aarch64}-unknown-linux-{gnu,musl} and {x86_64,aarch64}-apple-darwin. Other targets may still work (bindgen can generate bindings on the fly) but aren't validated here.
  • Go and Perl toolchains are used for some AWS-LC-FIPS code generation if present, falling back to committed pre-generated code if not — so, same as the rest of this crate, no new hard build-time tool requirement, just potentially slower/less-optimized codegen without them.

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.