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 thetachyon-webworkspace. 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):
- Compiles a trimmed zlib snapshot (
vendor/zlib-src) directly viacc::Build. - Builds a minimal Boost subset (
vendor/boost-src, extracted from the upstream git superproject withbcp) via Boost's own CMake support. - Builds AWS-LC (via the
aws-lc-sysdependency) as the crypto/TLS backend, standing in for OpenSSL — nothing OpenSSL-shaped is vendored or linked. - 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). - Compiles
shim/shim.cpp, a hand-written C++ file implementing the flat C ABI declared inshim/shim.h, and links everything together. - Runs
bindgenagainstshim/shim.honly — never againstlibi2pd's own C++ headers, which usestd::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:
CC_<target>/CXX_<target>env vars (the standardcccrate convention), e.g.CXX_x86_64_unknown_linux_musl.MUSL_CROSS_TOOLCHAINpointing at amusl-cross-make-style install (expects<dir>/bin/<target-triple>-g++).- 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
[]
= "0.0.4"
use *;
use ;
unsafe
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
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 API —
EVP_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 default — libi2pd'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-sysvendors 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 (bindgencan 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.