wireguard-proxy
v0.1.4
Published
A Node.js/Bun library for proxying TCP, UDP, and HTTP traffic through WireGuard tunnels — entirely in userspace, no root or TUN device required.
Downloads
307
Readme
wireguard-proxy
A Node.js/Bun library for proxying TCP, UDP, and HTTP traffic through WireGuard tunnels — entirely in userspace, no root or TUN device required.
Features
- Userspace WireGuard — No system-level VPN setup, no root privileges
- Multiple concurrent tunnels — Route traffic through different exits simultaneously
- TCP streams —
ReadableStream/WritableStreaminterface with backpressure - UDP sockets — Send/receive datagrams with remote address metadata
- HTTP fetch —
proxy.fetch()works like the standard Fetch API, routed through a tunnel - Undici integration — Custom dispatcher for Node.js HTTP clients
- Cross-runtime — Works with both Node.js and Bun via Node-API
- Explicit routing — Select a tunnel per-request, or set a default
How It Works
JS/TS API → Native Addon (Rust) → boringtun (WireGuard) → smoltcp (TCP/IP) → Encrypted UDP → PeerThe native layer uses boringtun for the WireGuard protocol and smoltcp as a userspace TCP/IP stack. All packet processing happens inside the application — no kernel networking changes.
Installation
bun install wireguard-proxy
# or
npm install wireguard-proxyYou also need the native addon built for your platform. To build from source:
# Requires Rust toolchain
cd native && cargo build --release
cp target/release/libwireguard_proxy_native.so wireguard-proxy-native.node # Linux
cp target/release/libwireguard_proxy_native.dylib wireguard-proxy-native.node # macOSQuick Start
import { createWireGuardProxy } from "wireguard-proxy"
const proxy = await createWireGuardProxy({ defaultTunnel: "main" })
await proxy.addTunnel({
name: "main",
config: `
[Interface]
PrivateKey = <your-private-key>
Address = 10.0.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <peer-public-key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25
`,
})
// Fetch a URL through the tunnel
const res = await proxy.fetch("https://api.ipify.org?format=json")
console.log(await res.text()) // Your tunnel exit IP
await proxy.close()API
createWireGuardProxy(options?)
Creates a new proxy instance.
const proxy = await createWireGuardProxy({
defaultTunnel: "main", // Default tunnel for requests without explicit selection
dns: "system", // "system" | "tunnel" | "custom"
resolve: async (host) => ["1.2.3.4"], // Custom resolver (when dns: "custom")
})proxy.addTunnel(options)
Add and connect a WireGuard tunnel. Accepts INI config text or a structured object.
const { id, name } = await proxy.addTunnel({
name: "us-east",
config: configText, // string or WireGuardConfig object
})proxy.removeTunnel(idOrName)
Disconnect and remove a tunnel.
await proxy.removeTunnel("us-east")proxy.tcpConnect(options)
Open a TCP connection through a tunnel. Returns Web Streams.
const sock = await proxy.tcpConnect({
host: "example.com",
port: 443,
tunnel: "us-east", // Optional if defaultTunnel is set
})
// Write
const writer = sock.writable.getWriter()
await writer.write(new TextEncoder().encode("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
writer.releaseLock()
// Read
const reader = sock.readable.getReader()
const { value } = await reader.read()
console.log(new TextDecoder().decode(value))
reader.releaseLock()
await sock.close()proxy.createUdpSocket(options?)
Create a UDP socket bound to a tunnel.
const udp = await proxy.createUdpSocket({ tunnel: "us-east" })
await udp.send(dnsQuery, { host: "1.1.1.1", port: 53 })
const { data, remote } = await udp.receive()
console.log(`Response from ${remote.host}:${remote.port}`)
await udp.close()proxy.fetch(input, init?)
HTTP fetch routed through a tunnel. Same interface as the standard fetch().
const res = await proxy.fetch("https://example.com", {
tunnel: "us-east",
method: "POST",
body: JSON.stringify({ key: "value" }),
headers: { "Content-Type": "application/json" },
})proxy.getUndiciDispatcher(tunnel?)
Get a custom Undici dispatcher for use with Node.js HTTP clients.
proxy.tunnelStats(idOrName)
Get tunnel statistics.
const stats = await proxy.tunnelStats("us-east")
// { connected: true, lastHandshakeAt: 1710..., txBytes: 1024, rxBytes: 2048, tcpOpen: 1, udpOpen: 0 }proxy.close()
Shut down all tunnels and clean up resources.
Config Format
wireguard-proxy accepts standard WireGuard INI configs:
[Interface]
PrivateKey = <base64>
Address = 10.0.0.2/32
DNS = 1.1.1.1
MTU = 1420
[Peer]
PublicKey = <base64>
PresharedKey = <base64> # Optional
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25Or structured objects:
await proxy.addTunnel({
name: "main",
config: {
privateKey: "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=",
address: ["10.0.0.2/32"],
dns: ["1.1.1.1"],
mtu: 1420,
peers: [{
publicKey: "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=",
endpoint: "203.0.113.10:51820",
allowedIPs: ["0.0.0.0/0", "::/0"],
persistentKeepalive: 25,
}],
},
})You can also parse configs programmatically:
import { parseWireGuardConfig } from "wireguard-proxy"
const config = parseWireGuardConfig(iniText)
console.log(config.privateKey, config.peers[0].endpoint)Multi-Tunnel Routing
const proxy = await createWireGuardProxy()
await proxy.addTunnel({ name: "ams", config: amsConfig })
await proxy.addTunnel({ name: "tyo", config: tyoConfig })
// Route specific requests through specific tunnels
const [amsRes, tyoRes] = await Promise.all([
proxy.fetch("https://api.ipify.org", { tunnel: "ams" }),
proxy.fetch("https://api.ipify.org", { tunnel: "tyo" }),
])
console.log("Amsterdam:", await amsRes.text())
console.log("Tokyo:", await tyoRes.text())Error Handling
All errors extend WireGuardError with structured context:
import { WireGuardConfigError, WireGuardTcpError } from "wireguard-proxy"
try {
await proxy.tcpConnect({ host: "example.com", port: 80 })
} catch (err) {
if (err instanceof WireGuardTcpError) {
console.log(err.tunnelId, err.operation, err.remoteAddress, err.retryable)
}
}Error types: WireGuardConfigError, WireGuardHandshakeError, WireGuardTimeoutError, WireGuardDnsError, WireGuardTcpError, WireGuardUdpError, WireGuardHttpError.
Project Structure
src/ TypeScript public API
├── index.ts Exports
├── types.ts Type definitions
├── errors.ts Error classes
├── proxy.ts WireGuardProxy implementation
├── config/ Config parsing, validation, normalization
├── http/ Fetch wrapper and Undici dispatcher
└── native/ Native addon bindings
native/src/ Rust native engine
├── lib.rs Node-API exports
├── engine.rs Tunnel registry
├── tunnel.rs WireGuard session + packet loop
├── virtual_net.rs smoltcp ↔ boringtun bridge
├── tcp.rs TCP flow state machine
├── udp.rs UDP flow state machine
├── config.rs Rust-side config deserialization
├── dns.rs DNS policy
└── runtime.rs Tokio runtime
examples/ Usage examples
test/ TestsBuilding from Source
# Prerequisites: Node.js/Bun, Rust toolchain
# Install dependencies
bun install
# Build native addon
cd native && cargo build --release
cp target/release/libwireguard_proxy_native.so wireguard-proxy-native.node
# Build TypeScript
bun run build:ts
# Run tests
bun testLimitations (v1)
- One peer per tunnel (no multi-peer routing)
- IPv4 only for tunnel traffic
- HTTP/1.1 only
- No inbound connections or server mode
- HTTPS over tunnel in Bun requires Node.js runtime
- DNS resolution defaults to system resolver; tunnel DNS is opt-in
License
MIT — see LICENSE for details.
