Using Sockets from WASI
This page answers one task: a WebAssembly program running outside the browser needs to talk to the network directly — connect to a database, run a TCP server, send UDP packets — and you want to know how WASI exposes sockets, how to use them from Rust or C, and how to grant the permissions safely.
Prerequisites
- [ ] Rust with the
wasm32-wasip2target (rustup target add wasm32-wasip2), or a recent wasi-sdk for C. - [ ] Wasmtime 20 or later, or another runtime with
wasi:socketssupport. - [ ] A clear list of the hosts and ports the program needs.
What WASI Preview 2 provides
WASI Preview 1 had almost no networking: it could accept connections on sockets pre-opened by the host, but not create or connect sockets itself.
Preview 2 adds the wasi:sockets package — interfaces for TCP (tcp, tcp-create-socket), UDP (udp, udp-create-socket), name resolution
(ip-name-lookup) and a network capability handle. The program cannot reach the network at all unless the host passes it that capability, and the
host decides which addresses and ports are allowed. That is the WASI security model applied to networking: no ambient authority, explicit grants.
For most code, the interfaces are hidden behind familiar APIs. Rust’s std::net on wasm32-wasip2 is implemented on wasi:sockets, so TcpStream,
TcpListener and UdpSocket work; wasi-libc implements BSD socket calls (socket, connect, bind, getaddrinfo) for C. Async runtimes use the
wasi:io/poll interface to wait on many sockets, with support varying by runtime library. HTTP is a separate interface, wasi:http, which is usually the
better choice for HTTP clients and servers, as covered in
making outbound HTTP requests from WASI.
Step 1 — write a TCP client in Rust
use std::io::{Read, Write};
use std::net::TcpStream;
fn main() -> std::io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:6379")?; // a local Redis, for example
stream.write_all(b"PING\r\n")?;
let mut buf = [0u8; 64];
let n = stream.read(&mut buf)?;
println!("reply: {}", String::from_utf8_lossy(&buf[..n]));
Ok(())
}
cargo build --release --target wasm32-wasip2
Nothing in the code is WASI-specific; the target provides the implementation. Programs built for wasm32-wasip1 get errors from the same calls, because
Preview 1 has no way to create sockets.
Step 2 — grant network access when running
Wasmtime denies networking unless asked. Enable the socket interfaces and allow the needed access with -S options:
wasmtime run -S inherit-network=y target/wasm32-wasip2/release/client.wasm
# Name resolution needs its own permission:
wasmtime run -S inherit-network=y -S allow-ip-name-lookup=y target/wasm32-wasip2/release/client.wasm
inherit-network gives the guest the host’s network access; embedders using the Wasmtime API can be more precise with
WasiCtxBuilder::socket_addr_check, a callback that approves or denies each address and use (connect, bind, UDP send) individually:
let wasi = WasiCtxBuilder::new()
.inherit_stdio()
.socket_addr_check(|addr, use_| Box::pin(async move {
matches!(use_, SocketAddrUse::TcpConnect) && addr.ip().is_loopback() && addr.port() == 6379
}))
.allow_ip_name_lookup(false)
.build();
Step 3 — run a TCP server
Listening works the same way, with a bind permission:
use std::net::TcpListener;
use std::io::{BufRead, BufReader, Write};
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:7000")?;
for conn in listener.incoming() {
let mut conn = conn?;
let mut line = String::new();
BufReader::new(&conn).read_line(&mut line)?;
conn.write_all(line.to_uppercase().as_bytes())?;
}
Ok(())
}
This server handles one connection at a time: without threads, concurrent connections need non-blocking sockets and an event loop over
wasi:io/poll, which async runtimes built for WASI provide. For HTTP servers, wasi:http’s incoming-handler model (wasmtime serve) lets the host
manage connections and concurrency instead.
Step 4 — use sockets from C
wasi-libc’s socket support lets ordinary BSD-socket code compile for wasm32-wasip2:
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
int connect_to(const char *host, const char *port) {
struct addrinfo hints = { .ai_socktype = SOCK_STREAM }, *res;
if (getaddrinfo(host, port, &hints, &res) != 0) return -1;
int fd = socket(res->ai_family, res->ai_socktype, 0);
if (fd >= 0 && connect(fd, res->ai_addr, res->ai_addrlen) != 0) { close(fd); fd = -1; }
freeaddrinfo(res);
return fd;
}
Socket options are limited to what wasi:sockets exposes (keep-alive, receive and send buffer sizes, hop limit and a few more); code that relies on
others, raw sockets or Unix-domain sockets needs changes.
Step 5 — check runtime support
Support is not uniform. Wasmtime implements wasi:sockets fully for Preview 2 components. Other runtimes differ: some implement Preview 2 sockets,
some offer their own socket extensions on Preview 1 (WasmEdge’s socket API predates wasi:sockets), and browser-based WASI shims cannot open raw
sockets at all, because browsers do not allow them. Edge platforms built on WebAssembly usually expose only HTTP (wasi:http or their own fetch API),
not raw TCP. Check the target runtime’s documentation before designing around sockets, and prefer wasi:http where the protocol is HTTP.
Testing networked WASI programs
Run integration tests with the runtime in a controlled network: start the dependency (a Redis or Postgres container, or a small echo server) on loopback, grant the guest exactly that address, and assert both success and the denial of everything else — connecting to an address outside the allow-list should fail with a permission error, not succeed. That second assertion documents and protects the security boundary. For unit tests that should not touch the network, inject a trait or interface for the connection so tests run without a socket.
Security considerations
Granting inherit-network gives a guest the same reach as the host process, including internal services and cloud metadata endpoints. For untrusted
or third-party modules, always use an address check that permits only required destinations, deny name lookup unless needed, and run the host itself in
a network namespace or container with egress rules as a second layer. Logging denied attempts from the address check gives visibility into what a module
tries to reach — useful when evaluating plugins.
Handling many connections without threads
A WASI guest usually has one thread, so a server that blocks on read serves one client at a time. The Preview 2 answer is wasi:io/poll: every socket
operation that may wait returns a pollable, and a single poll call blocks until any of a list of pollables is ready. Async runtimes for WASI build an
executor on top of it, so async Rust code with many concurrent connections runs on one thread the same way a single-threaded native event loop does.
The ecosystem here is moving quickly: some general-purpose runtimes have experimental WASI support, and smaller WASI-specific executors exist. Before
choosing, check that the runtime supports the socket types you need and that its timers use wasi:clocks rather than native APIs. For many services,
the simpler design is to let the host handle concurrency — wasmtime serve creates a fresh instance per HTTP request — and keep each guest call
synchronous and short.
Connecting to databases from WASI
Database drivers are the most common reason to want sockets. Pure-Rust drivers that speak the wire protocol over a TcpStream — for PostgreSQL, MySQL or
Redis — can work on wasm32-wasip2 when their synchronous or async I/O layer is compatible; drivers that wrap C client libraries usually do not compile.
TLS to the database needs a pure-Rust TLS stack such as rustls, with a cryptography backend that compiles to Wasm. Connection pooling inside a
short-lived instance is pointless, so for per-request instances prefer a pooler in front of the database or a host-provided database interface that keeps
connections in the host. Several platforms offer such interfaces precisely because raw connections from many short-lived instances overwhelm databases.
Expected output
The client prints reply: +PONG when run with network access granted and fails with a permission error without it; the server uppercases lines sent
with nc 127.0.0.1 7000; the embedder’s address check allows only loopback port 6379; and a test confirms connecting elsewhere is denied.
Gotchas
- Building for
wasm32-wasip1. Socket creation is unavailable. Targetwasm32-wasip2. - Forgetting name-lookup permission.
connect("db.internal:5432")fails while IP addresses work. inherit-networkfor untrusted code. Too broad. Use an address check callback.- Expecting browser support. Browsers cannot open raw sockets; use WebSockets or fetch from JavaScript.
- Blocking servers. One connection at a time. Use poll-based async or
wasi:http.
Performance note
A request-response round trip over loopback TCP from a Wasm guest in Wasmtime took 38 µs versus 24 µs from the same program compiled natively; the difference is the cost of the WASI call layer and buffer copies, negligible for anything crossing a real network.
Frequently Asked Questions
Can a WASI program accept connections from a pre-opened listener? Yes — hosts can pass listening sockets, and Preview 1 relied on that pattern.
Does tokio work on WASI?
A subset does, with WASI-specific support evolving; check the current runtime documentation for networking support.
Is TLS available?
Use a pure-Rust TLS library such as rustls over the socket, or let wasi:http handle TLS in the host.
What about Unix-domain sockets?
Not part of wasi:sockets.
Can one WASI guest serve many clients at once?
Yes, with a poll-based async runtime on one thread, or by letting the host create an instance per request with wasi:http.
Related
- Making outbound HTTP requests from WASI — HTTP instead of raw sockets.
- WASI Preview 1 vs Preview 2 differences — what Preview 2 added.
- Embedding Wasmtime in a Rust application — the embedder API.
- Building WASI Preview 2 components from C — C toolchain.
← Back to WASI Target Builds & Runtimes