# i2pd-sys
Raw FFI bindings to a minimal C shim over [`libi2pd`](https://github.com/PurpleI2P/i2pd)
(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`](https://github.com/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`](https://crates.io/crates/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](https://github.com/aws/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`](https://github.com/tachyon-web/i2pd-sys/blob/main/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`](https://github.com/tachyon-web/i2pd-sys/blob/main/scripts/update-vendor.sh):
| `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](#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()`:
```c
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](https://cmake.org/) (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`](https://docs.rs/cc) convention), `MUSL_CROSS_TOOLCHAIN`
pointing at a [`musl-cross-make`](https://github.com/richfelker/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
```rust
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`](https://github.com/tachyon-web/i2pd-sys/blob/main/shim/shim.h) documents the full API surface — lifecycle, participation,
destinations, streams, memory. [`tests/roundtrip.rs`](https://github.com/tachyon-web/i2pd-sys/blob/main/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
```sh
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
| [i2pd](https://github.com/PurpleI2P/i2pd) | BSD-3-Clause | tag `2.61.0`, commit [`635b013`](https://github.com/PurpleI2P/i2pd/commit/635b013a612ff47278ef02acf8580a28e10e26c5) |
| [Boost](https://www.boost.org/) (minimal subset via `bcp`) | Boost Software License 1.0 | tag `boost-1.91.0` |
| [zlib](https://zlib.net/) | zlib License | `1.3.2` |
| [AWS-LC](https://github.com/aws/aws-lc) (via `aws-lc-sys`, not vendored directly) | Apache-2.0 OR ISC | — |
Eight small patches ([`scripts/patches/`](https://github.com/tachyon-web/i2pd-sys/tree/main/scripts/patches)) are applied to pristine upstream
source automatically by `i2pd_snapshot` in
[`scripts/update-vendor.sh`](https://github.com/tachyon-web/i2pd-sys/blob/main/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](#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](#trimmed-attack-surface) and
[Network participation](#network-participation).
Full attribution and license text: [`LICENSE-THIRD-PARTY`](https://github.com/tachyon-web/i2pd-sys/blob/main/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](https://github.com/aws/aws-lc) via `aws-lc-sys`. The
`fips` feature (`default-features = false, features = ["fips"]`) swaps in the FIPS 140-3-validated
[AWS-LC-FIPS](https://github.com/aws/aws-lc/tree/fips-2024-09-27) module via
[`aws-lc-fips-sys`](https://crates.io/crates/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
- [Apache License, Version 2.0](https://github.com/tachyon-web/i2pd-sys/blob/main/LICENSE-APACHE)
- [MIT license](https://github.com/tachyon-web/i2pd-sys/blob/main/LICENSE-MIT)
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`](https://github.com/tachyon-web/i2pd-sys/blob/main/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.