# libp2p-wasi-sockets
[](https://github.com/cargopete/libp2p-wasi-sockets/actions/workflows/ci.yml)
[](https://crates.io/crates/libp2p-wasi-sockets)
[](https://docs.rs/libp2p-wasi-sockets)
[](LICENSE-MIT)
A [WASI 0.2](https://github.com/WebAssembly/WASI/blob/v0.2.1/README.md) sockets transport for [rust-libp2p](https://github.com/libp2p/rust-libp2p).
Implements `libp2p_core::Transport` over `wasi:sockets/tcp`, enabling rust-libp2p applications to run as **Wasm Components** on any WASI 0.2 host — Wasmtime, Spin, jco, Wasmer — without any modification to the rest of the libp2p stack.
---
## Why does this exist?
Every major rust-libp2p sub-crate (`libp2p-core`, `libp2p-swarm`, `libp2p-noise`, `libp2p-yamux`, `libp2p-gossipsub`, …) **compiles cleanly for `wasm32-wasip2`**. The only missing piece was a transport: `libp2p-tcp` is gated out for all wasm targets, and no `wasi:sockets`-based alternative existed.
This crate closes that gap.
### What you can do with it
- Run libp2p-based protocols as **sandboxed Wasm Components** in Wasmtime or Spin.
- Drop P2P functionality into plugin systems that already embed a WASI 0.2 runtime.
- Leverage the WASI **network capability model** — the host explicitly grants (or denies) network access per component, giving stronger isolation than containers.
### What is deliberately out of scope
- **UDP / QUIC** — `wasi:sockets/udp` doesn't expose the socket controls `quinn` needs.
- **DNS multiaddrs** — `/dns4`, `/dns6`, `/dnsaddr` planned for v0.2.
- **Browser** — already covered by `libp2p-websocket-websys` / `libp2p-webtransport-websys`.
- **NAT traversal** — AutoNAT, circuit-relay, DCUtR are not supported by the `wasi-sockets` API.
---
## Status
**v0.1.0** — all milestones complete; full Noise XX + Yamux interop verified against native rust-libp2p.
| M0 | Scaffold, Cargo.toml, CI, multiaddr utils, unit tests | ✅ |
| M1 | `WasiTcpStream` AsyncRead/AsyncWrite bridge + echo integration test | ✅ |
| M2 | `WasiTcpTransport` listen\_on + dial integration test | ✅ |
| M3 | Multi-listener, AddressExpired / ListenerClosed lifecycle events | ✅ |
| M4 | Noise XX + Yamux upgrade over `WasiTcpTransport` | ✅ |
| M5 | Interop test: WASM component ↔ native rust-libp2p (tokio) | ✅ |
| M6 | Docs polish, examples, 0.1.0 crates.io release | ✅ |
---
## Quick start
```toml
# Cargo.toml — do NOT enable the `tcp` feature on the umbrella `libp2p` crate
[dependencies]
libp2p-wasi-sockets = "0.1"
libp2p-swarm = "0.47"
libp2p-noise = "0.46"
libp2p-yamux = "0.47"
libp2p-ping = "0.47"
libp2p-identity = { version = "0.2", features = ["ed25519", "rand"] }
wstd = "0.6"
```
```rust
use libp2p_swarm::{SwarmBuilder, SwarmEvent};
use libp2p_wasi_sockets::WasiTcpTransport;
use std::time::Duration;
#[wstd::main]
async fn main() {
let keypair = libp2p_identity::Keypair::generate_ed25519();
let mut swarm = SwarmBuilder::with_existing_identity(keypair)
.with_other_transport(|_| WasiTcpTransport::default()).unwrap()
.with_behaviour(|_| libp2p_ping::Behaviour::default()).unwrap()
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60)))
.build();
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()).unwrap();
loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => eprintln!("listening on {address}"),
SwarmEvent::Behaviour(event) => eprintln!("{event:?}"),
_ => {}
}
}
}
```
Build and run:
```bash
rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2
# -S inherit-network grants the component access to the host network.
# Without it, all dials return Error::AccessDenied.
wasmtime run -S inherit-network ./target/wasm32-wasip2/release/my_app.wasm
```
---
## Supported multiaddrs
| `/ip4/<addr>/tcp/<port>` | ✅ |
| `/ip6/<addr>/tcp/<port>` | ✅ |
| `…/p2p/<peer-id>` suffix | ✅ stripped, not dialled |
| `/ip4/0.0.0.0/tcp/0` (ephemeral port) | ✅ |
| `/ip6/::/tcp/0` (ephemeral port) | ✅ |
| `/dns4`, `/dns6`, `/dnsaddr` | ❌ planned for v0.2 |
| `/quic-v1`, `/ws`, `/wss`, `/webrtc` | ❌ out of scope |
---
## Architecture
```
┌─────────────────────────────────────────┐
│ libp2p Swarm (user code) │
│ Behaviour: ping / gossipsub / kad / … │
└──────────────────┬──────────────────────┘
│
┌──────────────────┴──────────────────────┐
│ Yamux (muxer) · Noise (security) │
└──────────────────┬──────────────────────┘
│
┌──────────────────┴──────────────────────┐
│ libp2p-wasi-sockets (this crate) │
│ │
│ WasiTcpTransport impl Transport │
│ WasiTcpStream impl AsyncRead │
│ AsyncWrite │
└──────────────────┬──────────────────────┘
│
┌──────────────────┴──────────────────────┐
│ wstd 0.6 (wasi:sockets/tcp wrapper) │
└──────────────────┬──────────────────────┘
│
┌──────────────────┴──────────────────────┐
│ WASI 0.2 host (Wasmtime / Spin / …) │
└─────────────────────────────────────────┘
```
### Async bridge
`wstd`'s `TcpStream::read` / `write` are `async fn`s; `futures::io::AsyncRead` / `AsyncWrite` are poll-based. The bridge works by boxing each in-flight wstd operation as a `Pin<Box<dyn Future>>` and re-polling it on subsequent `poll_*` calls. On `wasm32-wasip2` there are no threads, so the `Send` bound is satisfied via `unsafe impl` — safe because WASI resource handles are plain integers.
---
## Pitfalls
### Do not pull in `libp2p-tcp`
The umbrella `libp2p` crate gates `libp2p-tcp` off for all wasm targets, but if you pull it in manually the build will fail. Depend on sub-crates directly and verify:
```bash
```
### Wasmtime network permissions
Wasmtime denies all network access by default:
```bash
# Grant full network access (development)
wasmtime run -S inherit-network my_app.wasm
# Grant only specific addresses (production)
wasmtime run --wasi tcp=127.0.0.1:4001 my_app.wasm
```
---
## Requirements
- Rust **1.83+**
- Target: `wasm32-wasip2`
- WASI 0.2 host: Wasmtime ≥ 44, Spin ≥ 3.5, or any WASI 0.2.1-compatible runtime
---
## License
Licensed under either of:
- [Apache License, Version 2.0](LICENSE-APACHE)
- [MIT license](LICENSE-MIT)
at your option. This matches the license of rust-libp2p.