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.5), developed alongside and consumed by thetachyon-webworkspace. 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:
- 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
aws-lc-sys) as the crypto backend, standing in for OpenSSL — nothing OpenSSL-shaped is vendored or linked. - 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). - Compiles
shim/shim.cpp, the flat C ABI declared inshim/shim.h, and links everything. - 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 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():
; /* KB/s, whole router. Default 256. */
; /* of that limit, transit may use 25%. Default 100. */
; /* i2pd's own default is 25000. */
; /* 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
[]
= "0.0.5"
use *;
use CString;
unsafe
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
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.