wasmnet
Networking proxy for browser WASM — bridges WASI socket APIs to real TCP/UDP via WebSocket.
Browser WASM cannot do raw TCP or UDP. wasmnet runs server-side and provides real network I/O on behalf of browser WASM programs, with policy controls and bandwidth limiting.
Browser WASM ──WebSocket──▶ wasmnet server ──TCP/UDP/TLS──▶ Internet
Features
- TCP proxy — outbound
connectand inboundbind/listen/accept - TLS termination —
connect_tlshandles the TLS handshake server-side (rustls + webpki-roots) - UDP support —
connect_udp/send/send_towith async receive - DNS resolution —
resolvea hostname to IP addresses without opening a socket - Policy engine — allow/deny lists for IPs (CIDR), domains (wildcards), port ranges, connection limits
- Bandwidth limiting — token-bucket rate limiter enforcing
max_bandwidth_mbps - Connection pooling — reuse idle TCP connections with configurable TTL and warm-up
- Binary framing — optional
[1B type][8B id][payload]binary protocol, auto-detected alongside JSON - Embeddable — use as a library with
Server::builder()or upgrade a single TCP stream withhandle_ws_upgrade()
Install
Server (Rust)
Browser Client (npm)
Quick Start
Run the server
# Default policy (blocks private IPs, allows public)
# Custom port
# With a policy file
# No restrictions
# Bandwidth limit of 5 Mbps + connection pooling
Browser client
import from 'wasmnet';
const client = ;
await client.;
// ── TCP ────────────────────────────────────
const id = await client.;
client.;
client.;
// ── TLS ────────────────────────────────────
const tls = await client.;
client.;
client.;
// ── UDP ────────────────────────────────────
const udp = await client.;
client.;
client.;
// ── DNS resolve ────────────────────────────
const ips = await client.;
console.log; // ["93.184.216.34", "2606:2800:220:1:..."]
// ── Inbound TCP (port binding) ─────────────
const listener = await client.;
console.log;
client.;
// ── Cleanup ────────────────────────────────
client.;
client.;
Binary framing mode
Pass { binary: true } to avoid JSON + base64 overhead on data frames:
const client = ;
The server auto-detects the framing per message — JSON text frames and binary frames can even be mixed within the same session.
Library (embed in Rust)
use Server;
// Builder API
let server = builder
.host
.port
.policy_file?
.max_bandwidth_mbps
.pool // idle 60s, 8 per target
.build?;
server.listen.await?;
// Graceful shutdown
let = channel;
let server = builder.no_policy.build?;
server.listen_with_shutdown.await?;
// Embed in an existing server — upgrade a single TCP stream
use ;
use Arc;
let policy = new;
handle_ws_upgrade.await;
CLI Reference
wasmnet-server [OPTIONS]
Options:
-H, --host <HOST> Listen address [default: 0.0.0.0]
-p, --port <PORT> Listen port [default: 9000]
--policy <FILE> Path to policy TOML file
--no-policy Disable all policy checks
--max-bandwidth-mbps <MBPS> Bandwidth limit (overrides policy file)
--pool-idle-secs <SECS> Enable connection pooling with idle timeout
--pool-per-key <N> Max pooled connections per target [default: 8]
Set RUST_LOG=wasmnet=debug for detailed logging.
Protocol
Single WebSocket connection. Messages are JSON text frames or binary frames (auto-detected).
Requests (browser → server)
| Operation | Fields | Description |
|---|---|---|
connect |
id, addr, port |
Outbound TCP connection |
connect_tls |
id, addr, port |
Outbound TCP + TLS (server-side handshake) |
connect_udp |
id, addr, port |
Create a UDP socket connected to target |
bind |
id, addr, port |
Bind a local TCP listener |
listen |
id, backlog? |
Start accepting connections |
send |
id, data (base64) |
Send data on a TCP or UDP socket |
send_to |
id, addr, port, data (base64) |
Send a UDP datagram to a specific address |
resolve |
id, name |
DNS lookup — resolve hostname to IPs |
close |
id |
Close a socket or listener |
Events (server → browser)
| Event | Fields | Description |
|---|---|---|
connected |
id |
TCP or TLS connection established |
data |
id, data (base64) |
Data received on TCP/TLS socket |
data_from |
id, data, addr, port |
UDP datagram received |
udp_bound |
id, port |
UDP socket bound |
listening |
id, port |
TCP listener bound |
accepted |
id, conn_id, remote |
New inbound TCP connection |
resolved |
id, addrs |
DNS result (array of IP strings) |
closed |
id |
Socket closed |
error |
id, msg |
Error occurred |
denied |
id, msg |
Blocked by policy |
Binary frame format
When the client sends a WebSocket binary message, the server switches to binary framing for responses. Frame layout:
[1 byte message type][8 bytes connection ID (big-endian)][payload…]
Data payloads are raw bytes — no base64 encoding. See src/binary.rs for type constants.
Policy
Configure via TOML. The default policy blocks private IP ranges and allows all public addresses.
[]
# Block private/internal ranges
= ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16"]
# Allow everything else
= ["*"]
# Port binding restricted to user range
= "3000-9999"
# Limits
= 50
= 10
= 30
Deny-by-default mode
[]
= ["*"]
= ["api.example.com:443", "*.github.com:443"]
= "3000,8080"
= 5
Features:
- CIDR matching —
10.0.0.0/8,172.16.0.0/12, etc. - Domain patterns — exact (
api.example.com:443) or wildcard (*.github.com:443) - Port ranges —
3000-9999or3000,8080,9090 - Connection limits —
max_connectionsper session - Bandwidth limiting —
max_bandwidth_mbpsenforced via token-bucket rate limiter - Connection timeout —
connection_timeout_secsfor outbound TCP connects
See policy.example.toml for a full example.
Architecture
┌─ Browser WASM ──────────────────────────────────────────────┐
│ WASI socket call → serialize → WebSocket.send() │
└──────────────────────────┬──────────────────────────────────┘
│ WebSocket (JSON or binary frames)
┌──────────────────────────▼──────────────────────────────────┐
│ wasmnet server │
│ │
│ ┌──────────┐ ┌─────────────┐ ┌───────────────────┐ │
│ │ Policy │ │ Rate Limiter│ │ Connection Pool │ │
│ │ Engine │ │ (token │ │ (idle TCP streams │ │
│ │ │ │ bucket) │ │ w/ TTL + warmup) │ │
│ └────┬─────┘ └──────┬──────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Proxy: bidirectional bridge │ │
│ │ WebSocket frames ↔ TCP / TLS / UDP bytes │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
TCP stream TLS stream UDP socket
Source layout
src/
├── main.rs # CLI binary (clap)
├── lib.rs # Server, ServerBuilder, handle_ws_upgrade
├── proxy.rs # WebSocket ↔ TCP/TLS/UDP bidirectional proxy
├── policy.rs # Allow/deny lists, CIDR, domains, rate limits
├── protocol.rs # Request/Event message types (serde JSON)
├── binary.rs # Binary frame codec
├── rate_limit.rs # Token-bucket bandwidth limiter
├── dns.rs # Async DNS resolution
└── pool.rs # Idle TCP connection pool
client/
├── wasmnet-client.js # ES module (JSON + binary framing)
├── wasmnet-client.d.ts # TypeScript declarations
└── README.md