2026-09-13 12:10:14 +00:00
2026-09-13 12:10:14 +00:00
2026-02-27 10:18:23 +00:00
2026-09-13 12:10:14 +00:00
2026-02-27 10:18:23 +00:00
2026-09-13 12:10:14 +00:00

@push.rocks/smartvpn

A high-performance VPN solution with a TypeScript control plane and a Rust data plane daemon. Enterprise-ready client authentication, triple transport support (WebSocket + QUIC + WireGuard), and a typed hub API for managing clients from code.

  • 🔐 Noise IK mutual authentication — per-client X25519 keypairs, server-side registry
  • 🚀 Triple transport: WebSocket (Cloudflare-friendly), raw QUIC (datagrams), and WireGuard (standard protocol)
  • 🛡️ ACL engine — deny-overrides-allow IP filtering, aligned with SmartProxy conventions
  • 🔀 PROXY protocol v2 — real client IPs behind reverse proxies (HAProxy, SmartProxy, Cloudflare Spectrum)
  • 📊 Per-transport metrics: active clients and total connections broken down by websocket, QUIC, and WireGuard
  • 🔄 Hub API: one createClient() call generates keys, assigns IP, returns both SmartVPN + WireGuard configs
  • 📡 Real-time telemetry: RTT, jitter, loss ratio, link health — all via typed APIs
  • 🌐 Unified forwarding pipeline: all transports share the same engine — TUN (kernel), userspace NAT (no root), L2 bridge, hybrid, or testing mode
  • 🏠 Bridge mode: VPN clients get IPs from your LAN subnet — seamlessly bridge remote clients onto a physical network
  • 🔀 Hybrid mode: per-client routing — some clients bridge to the LAN, others use userspace NAT, all on the same server
  • 🏷️ VLAN support: assign individual clients to 802.1Q VLANs on the bridge
  • 🎯 Destination routing policy: force-target, block, or allow traffic per destination with nftables integration
  • Authenticated WireGuard state: peers appear as "connected" after their first authenticated, source-authorized tunnel packet, and auto-disconnect on idle timeout

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install 📦

pnpm install @push.rocks/smartvpn

The package ships with pre-compiled Rust binaries for linux/amd64 and linux/arm64. No Rust toolchain is required at runtime. Set SMARTVPN_RUST_BINARY if you want the TypeScript bridge to use a custom daemon binary.

Architecture 🏗️

┌──────────────────────────────┐     JSON-lines IPC     ┌───────────────────────────────┐
│    TypeScript Control Plane  │ ◄─────────────────────► │     Rust Data Plane Daemon    │
│                              │   stdio  or  Unix sock  │                               │
│  VpnServer / VpnClient      │                          │  Noise IK handshake           │
│  Typed IPC commands          │                          │  Noise transport encryption   │
│  Config validation           │                          │  WS + QUIC + WireGuard        │
│  Hub: client management      │                          │  TUN device, IP pool, NAT     │
│  WireGuard .conf generation  │                          │  Rate limiting, ACLs, QoS     │
│  nftables destination policy │                          │  Destination routing, nftables│
└──────────────────────────────┘                          └───────────────────────────────┘

Split-plane design — TypeScript handles orchestration, config, and DX; Rust handles the hot path with async I/O, framed packet codecs, and the Noise transport state after authentication.

IPC Transport Modes

The bridge between TypeScript and Rust supports two transport modes:

Mode Use Case How It Works
stdio Development, testing Spawns the Rust daemon as a child process, communicates over stdin/stdout
socket Production Connects to an already-running daemon via Unix domain socket, with optional auto-reconnect
// Development: spawn the daemon
const server = new VpnServer({ transport: { transport: 'stdio' } });

// Production: connect to running daemon
const server = new VpnServer({
  transport: {
    transport: 'socket',
    socketPath: '/var/run/smartvpn.sock',
    autoReconnect: true,
    reconnectBaseDelayMs: 100,
    reconnectMaxDelayMs: 5000,
    maxReconnectAttempts: 10,
  },
});

Quick Start 🚀

1. Start a VPN Server (Hub)

import { VpnServer } from '@push.rocks/smartvpn';

const server = new VpnServer({ transport: { transport: 'stdio' } });
await server.start({
  listenAddr: '0.0.0.0:443',
  privateKey: '<server-noise-private-key-base64>',
  publicKey: '<server-noise-public-key-base64>',
  subnet: '10.8.0.0/24',
  transportMode: 'all',       // WebSocket + QUIC + WireGuard simultaneously (default)
  forwardingMode: 'tun',      // 'tun' | 'socket' | 'bridge' | 'hybrid' | 'testing'
  wgPrivateKey: '<server-wg-private-key-base64>', // required for WireGuard transport
  enableNat: true,
  dns: ['1.1.1.1', '8.8.8.8'],
});

2. Create a Client (One Call = Everything)

const bundle = await server.createClient({
  clientId: 'alice-laptop',
  serverDefinedClientTags: ['engineering'],  // trusted tags for access control
  security: {
    destinationAllowList: ['10.0.0.0/8'],        // can only reach internal network
    destinationBlockList: ['10.0.0.99'],           // except this host
    rateLimit: { bytesPerSec: 10_000_000, burstBytes: 20_000_000 },
  },
});

// bundle.smartvpnConfig  → typed IVpnClientConfig, ready to use
// bundle.wireguardConfig → standard WireGuard .conf string
// bundle.secrets         → { noisePrivateKey, wgPrivateKey } — shown ONCE

3. Connect a Client

import { VpnClient } from '@push.rocks/smartvpn';

const client = new VpnClient({ transport: { transport: 'stdio' } });
await client.start();

const { assignedIp } = await client.connect(bundle.smartvpnConfig);
console.log(`Connected! VPN IP: ${assignedIp}`);

Features

Caller-owned client network namespace

A Linux stdio client accepts networkNamespaceFd in IVpnClientOptions. Pass an already open network namespace descriptor; the caller retains ownership and keeps it valid through each start(). The child validates its inherited duplicate, enters the namespace before creating runtime threads or sockets, then closes the duplicate. Invalid descriptors and failed entry cannot produce a ready client. Stopping the client waits for process termination before the caller releases its namespace owner. A running child remains in its namespace if the caller closes the original descriptor, but a later start still requires a valid descriptor.

The caller must provide the hub endpoint's underlay route and any required DNS reachability inside that namespace before connecting. SmartVPN creates neither the namespace nor that underlay. Managed TUN setup still validates the authenticated assignment and rejects routes that capture the actual hub endpoint. Namespace placement does not grant workload access or replace packet policy.

The option is process configuration, separate from VPN connection configuration. It is rejected for socket transport and server bridges. The native equivalent is --management --mode client --network-namespace-fd 3, with an inherited descriptor at FD 3. Server facade firewall setup runs in its parent process, so server namespace placement requires a separately coordinated policy owner.

The Linux amd64 musl candidate passed offline Linux 6.18.35 qualification with all native threads in the target namespace, child descriptor closure, retained caller ownership and denied-entry rejection. The shared serve.zone/testing scenario also passed real WS/Noise traffic through inherited namespace entry, managed TUN creation, allow/deny, reconnect, revocation, route-conflict rollback and child-termination cleanup. Privileged ARM64 and Pallet integration remain separate qualification requirements.

Confirmed bridge shutdown

Always await client.stop(), server.stop() or bridge.stop() before replacing an owned worker. These use Smartrust's confirmed termination API; a timeout is not treated as successful shutdown. In stdio mode they wait for child exit and stdio closure. In socket mode they confirm the local IPC connection has closed, not that the externally supervised daemon process has exited.

server.stop() drains an in-flight start and firewall health check before cleaning up its policy resources. Cleanup errors reject and retain ownership for an explicit retry. If a socket-connected facade installed TUN policy, it first requires the managed server's stop acknowledgement before closing IPC and removing that policy. A socket connection without owned policy only closes IPC; use stopServer() explicitly to request remote listener shutdown.

await client.stop();
await server.stop();

This bridge lifecycle guarantee is separate from per-client packet/session revocation inside the running Rust daemon; see the managed-network qualification boundaries below.

The Rust server now waits for configured listeners and initial WireGuard peers before acknowledging startup. A listener startup failure shuts down the owned runtime. stopServer() cancels and drains server-owned packet tasks, including pre-authentication connections, WireGuard return relays, and NAT socket readers; QUIC also closes its connections and waits for protocol shutdown. Per-client transport and queued forwarding teardown are described below. Transactional rollback of standalone kernel network setup remains outstanding; the isolated managed-client qualification below does not establish that guarantee for other modes. When a raw WireGuard declaration uses a registered client's key, its allowedIps must be exactly that client's assigned /32. Raw peers without a matching client registry entry retain their separately configured prefix lists.

Client runtime shutdown

Always await client.disconnect(): it cancels and joins the native/WireGuard packet runtime before returning, including the native keepalive monitor. A blocked write cannot turn a timeout into a successful disconnect. Failed connection attempts clear their startup state; unexpected packet-task panics remain errors. Packet-loop exit clears connection and MTU metadata; native clients also retire their monitor before reconnect can reuse the client. Dropping the Rust client requests cancellation but cannot provide an awaited cleanup guarantee. Linux native managed-client TUN cleanup is qualified below; standalone native/WireGuard host-route cleanup is not.

🔐 Enterprise Authentication (Noise IK)

Every client authenticates with a Noise IK handshake (Noise_IK_25519_ChaChaPoly_BLAKE2s). The server verifies the client's static public key against its registry — unauthorized clients are rejected before any data flows.

  • Per-client X25519 keypair generated server-side
  • Client registry with enable/disable, expiry, tags
  • Key rotation with rotateClientKey() — generates new keys, returns fresh config bundle, disconnects old session

🌐 Triple Transport

Transport Protocol Best For
WebSocket TLS over TCP Firewall-friendly, Cloudflare compatible
QUIC UDP (via quinn) Low latency, datagram support for IP packets
WireGuard UDP (via boringtun) Standard WG clients (iOS, Android, wg-quick)

The server runs with transportMode: 'all' by default: WebSocket and QUIC are enabled, and WireGuard joins the same server when wgPrivateKey is configured. All server transports share the same forwarding pipeline (ForwardingEngine), IP pool, client registry, and statistics, so WireGuard peers can use the same userspace NAT, bridge/hybrid routing, and monitoring model as WS/QUIC clients. Native SmartVPN clients auto-negotiate with transport: 'auto' (tries QUIC first, falls back to WS).

📊 Per-Transport Metrics

Server statistics include per-transport breakdowns so you can see exactly how many clients use each protocol:

const stats = await server.getStatistics();

// Aggregate
console.log(stats.activeClients);       // total connected clients
console.log(stats.totalConnections);    // total connections since start

// Per-transport active clients
console.log(stats.activeClientsWebsocket);   // currently connected via WS
console.log(stats.activeClientsQuic);        // currently connected via QUIC
console.log(stats.activeClientsWireguard);   // currently connected via WireGuard

// Per-transport total connections
console.log(stats.totalConnectionsWebsocket);
console.log(stats.totalConnectionsQuic);
console.log(stats.totalConnectionsWireguard);

WireGuard connection state follows authenticated tunnel traffic — a configured peer has no live return route until its first authenticated, source-authorized tunnel packet. Handshake or keepalive traffic alone does not admit a packet session. Peers disconnect after 180 seconds of inactivity, registry expiry, or when boringtun reports ConnectionExpired. Disconnect retains the configured peer but resets its cryptographic state, so subsequent traffic requires a fresh handshake. Raw peer connection IDs use wg- followed by the full public key.

🛡️ ACL Engine (SmartProxy-Aligned)

Security policies per client, using the same ipAllowList / ipBlockList naming convention as @push.rocks/smartproxy:

security: {
  ipAllowList: ['192.168.1.0/24'],          // source IPs allowed to connect
  ipBlockList: ['192.168.1.100'],            // deny overrides allow
  destinationAllowList: ['10.0.0.0/8'],      // VPN destinations permitted
  destinationBlockList: ['10.0.0.99'],        // deny overrides allow
  maxConnections: 5,
  rateLimit: { bytesPerSec: 1_000_000, burstBytes: 2_000_000 },
}

Supports exact IPs, CIDR, wildcards (192.168.1.*), and ranges (1.1.1.1-1.1.1.100).

🔀 PROXY Protocol v2

When the VPN server sits behind a reverse proxy, enable PROXY protocol v2 to receive the real client IP instead of the proxy's address. This makes ipAllowList / ipBlockList ACLs work correctly through load balancers.

await server.start({
  // ... other config ...
  proxyProtocol: true,                            // parse PP v2 headers on WS connections
  connectionIpBlockList: ['198.51.100.0/24'],     // server-wide block list (pre-handshake)
});

Two-phase ACL with real IPs:

Phase When What Happens
Pre-handshake After TCP accept Server-level connectionIpBlockList rejects known-bad IPs — zero crypto cost
Post-handshake After Noise IK identifies client Per-client ipAllowList / ipBlockList checked against real source IP
  • Parses the PP v2 binary header from raw TCP before WebSocket upgrade
  • 5-second timeout protects against stalling attacks
  • LOCAL command (proxy health checks) handled gracefully
  • IPv4 and IPv6 addresses supported
  • remoteAddr field on IVpnClientInfo exposes the real client IP for monitoring
  • Security: must be false (default) when accepting direct connections — only enable behind a trusted proxy

🎯 Destination Routing Policy

Control where decrypted VPN client traffic goes — force it to a specific target, block it, or allow it through. Evaluated per-packet before per-client ACLs.

await server.start({
  // ...
  forwardingMode: 'socket', // userspace NAT mode
  destinationPolicy: {
    default: 'forceTarget',                    // redirect all traffic to a target
    target: '127.0.0.1',                       // target IP for 'forceTarget' mode
    allowList: ['10.0.0.0/8'],                 // these destinations pass through directly
    blockList: ['10.0.0.99'],                  // always blocked (deny overrides allow)
  },
});

Policy modes:

Mode Behavior
'forceTarget' Rewrites destination IP to target — funnels all traffic through a single endpoint
'block' Drops all traffic not explicitly in allowList
'allow' Passes all traffic through (default, backward compatible)

In TUN mode, destination policies are enforced via nftables rules (using @push.rocks/smartnftables). A 60-second health check automatically re-applies rules if they're removed externally.

In socket mode, the policy is evaluated in the userspace NAT engine before per-client ACLs.

Per-client override — individual clients can have their own destination policy that overrides the server-level default:

await server.createClient({
  clientId: 'restricted-client',
  security: {
    destinationPolicy: {
      default: 'block',                         // block everything by default
      allowList: ['10.0.0.0/8'],                 // except internal network
    },
    // ... other security settings
  },
});

🔗 Socket Forward Proxy Protocol

When using forwardingMode: 'socket' (userspace NAT), you can prepend PROXY protocol v2 headers on outbound TCP connections. This conveys the VPN client's tunnel IP as the source address to downstream services (e.g., SmartProxy):

await server.start({
  // ...
  forwardingMode: 'socket',
  socketForwardProxyProtocol: true,  // downstream sees VPN client IP, not 127.0.0.1
});

📦 Packet Forwarding Modes

SmartVPN supports five forwarding modes, configurable per-server:

Mode Flag Description Root Required
TUN 'tun' Kernel TUN device — real packet forwarding with system routing Yes
Userspace NAT 'socket' Userspace TCP/UDP proxy via connect(2) — no TUN, no root needed No
Bridge 'bridge' L2 bridge — VPN clients get IPs from a physical LAN subnet Yes
Hybrid 'hybrid' Per-client routing: some clients use socket NAT, others use bridge — both engines run simultaneously Yes
Testing 'testing' Monitoring only — packets are counted but not forwarded No
// Server with userspace NAT (no root required)
await server.start({
  // ...
  forwardingMode: 'socket',
  enableNat: true,
});

// Server with bridge mode — VPN clients appear on the LAN
await server.start({
  // ...
  forwardingMode: 'bridge',
  bridgeLanSubnet: '192.168.1.0/24',      // LAN subnet to bridge into
  bridgePhysicalInterface: 'eth0',         // auto-detected if omitted
  bridgeIpRangeStart: 200,                 // clients get .200.250 (defaults)
  bridgeIpRangeEnd: 250,
});

// Server with hybrid mode — per-client routing
await server.start({
  // ...
  forwardingMode: 'hybrid',
  bridgePhysicalInterface: 'eth0',         // for bridge clients
});

// Client with TUN device
const { assignedIp } = await client.connect({
  // ...
  forwardingMode: 'tun',
});

The userspace NAT mode extracts destination IP/port from IP packets, opens a real socket to the destination, and relays data — supporting both TCP streams and UDP datagrams without requiring CAP_NET_ADMIN or root privileges.

The bridge mode assigns VPN clients IPs from a real LAN subnet instead of a virtual VPN subnet. Clients appear as if they're directly on the physical network — perfect for remote access to home labs, office networks, or IoT devices.

The hybrid mode runs both engines simultaneously. Each authenticated session's useHostIp setting determines whether its packets go through the bridge or socket NAT; routing does not look up identity from the packet source.

Inner packet MTU

mtu means inner IP bytes, not the outer network link MTU. It defaults to 1420 and must be an integer from 576 through 65472, leaving room for the supported Noise and WireGuard framing. Larger previous values now reject before startup effects. The server uses this same ceiling for its TUN/TAP, userspace stack and native assignment; it does not subtract an estimated encapsulation overhead.

Native clients now send their receive ceiling inside the authenticated Noise IK handshake. The server retains min(server MTU, client MTU) on that session, advertises it with the actual assignment subnet, and applies it to ingress, return encryption and the session's userspace TCP stack. Client configuration defaults to 1420; the client uses the negotiated ceiling unchanged for its TUN and both packet directions. Assignment fields must be valid before device setup; there is no guessed /24, gateway or MTU fallback. This changes the native wire contract: both ends must be upgraded together. Standard WireGuard framing is unchanged.

Eligible oversized outbound client IPv4 packets receive feedback through the client TUN using its assigned address and subnet. An incomplete or failed device write is an error, never counted as successful feedback or retried as a second packet. Oversized inbound packets are discarded before TUN writes.

WireGuard clients use the public flat wgPrivateKey, serverPublicKey, wgAddress, wgAddressPrefix, wgEndpoint and nonempty wgAllowedIps fields. The old Rust-only nested peer input is removed, with no translation fallback. Addresses, prefixes, keys, endpoint and MTU validate before TUN setup. The configured prefix supplies the device netmask; AllowedIPs route networks are canonicalized. forwardingMode: 'testing' creates no TUN or routes; the existing WireGuard default remains 'tun' (native defaults to 'testing'). Endpoint hostnames resolve in Rust. AllowedIPs authorizes decrypted source addresses and outbound destinations, after validating IP headers. The local configured MTU applies unchanged in both directions; standard WireGuard has no native SmartVPN MTU negotiation.

client.getMtuInfo() now reports the live native or WireGuard runtime, or null when inactive. tunMtu is null in testing mode because no interface exists; unmeasured linkMtu and overheadBytes are also null. Counters belong to the current connection and icmpTooBigSent counts only complete TUN writes. These nullable results replace the previous hard-coded values.

Authenticated native and integrated WireGuard packets are size-checked after source/ACL/rate admission and before forwarding. Return packets are checked before encryption. Oversized packets are dropped even when no safe error can be sent. Eligible IPv4 DF packets receive a bounded fragmentation-needed response from the owned tunnel gateway or bridge host address, through their exact session. Invalid headers, ICMP errors, noninitial fragments, multicast and known broadcasts do not generate errors. Feedback is limited to 10 packets/second with a burst of 10 per server; internal accounting distinguishes dropped packets from feedback queued.

This is an inner admission ceiling, not IP fragmentation/reassembly or measured path-MTU discovery. Raw WG IPv6 remains kernel/testing-only and oversized IPv6 is dropped without fabricating an IPv6 router address. Standalone host-network cleanup and privileged WireGuard TUN qualification remain outstanding; the unprivileged packet fixtures do not establish host readiness for those modes.

🏠 Per-Client Bridge & VLAN Settings

When using bridge or hybrid mode, each client can be individually configured for LAN bridging, static IPs, DHCP, and 802.1Q VLAN assignment:

// Client that bridges to the LAN with a static IP
await server.createClient({
  clientId: 'office-printer',
  useHostIp: true,           // bridge to LAN instead of VPN subnet
  staticIp: '192.168.1.210', // fixed LAN IP
});

// Client that gets a LAN IP via DHCP
await server.createClient({
  clientId: 'roaming-laptop',
  useHostIp: true,
  useDhcp: true,             // obtain IP from LAN DHCP server
});

// Client on a specific VLAN
await server.createClient({
  clientId: 'iot-sensor',
  useHostIp: true,
  forceVlan: true,
  vlanId: 100,               // 802.1Q VLAN ID (1-4094)
});

// Regular NAT client (default, no bridge)
await server.createClient({
  clientId: 'remote-worker',
  // useHostIp defaults to false → uses socket NAT
});
Field Type Description
useHostIp boolean true = bridge to LAN (host IP), false = VPN subnet via NAT (default)
useDhcp boolean When useHostIp is true, obtain IP via DHCP relay instead of static/auto-assign
staticIp string Fixed LAN IP when useHostIp is true and useDhcp is false
forceVlan boolean Assign this client to a specific 802.1Q VLAN on the bridge
vlanId number VLAN ID (1-4094), required when forceVlan is true

VLAN support uses Linux bridge VLAN filtering — each client's TAP port gets tagged with the specified VLAN ID, isolating traffic at Layer 2.

📊 Telemetry & QoS

  • Connection quality: Smoothed RTT, jitter, min/max RTT, loss ratio, link health (healthy / degraded / critical)
  • Adaptive keepalives: Interval adjusts based on link health (60s → 30s → 10s)
  • Per-client rate limiting: Token bucket with configurable bytes/sec and burst
  • Dead-peer detection: 180s inactivity timeout (all transports)
  • MTU management: Enforced inner packet ceilings and live counters; unmeasured outer-path values remain null
  • Per-transport stats: Active client and total connection counts broken down by websocket, QUIC, and WireGuard

🏷️ Client Tags (Trusted vs Informational)

SmartVPN separates server-managed tags from client-reported tags:

Field Set By Trust Level Use For
serverDefinedClientTags Server admin (via createClient / updateClient) Trusted Access control, routing, billing
clientDefinedClientTags Client (reported after connection) ⚠️ Informational Diagnostics, client self-identification
tags (deprecated) Legacy alias for serverDefinedClientTags
// Server-side: trusted tags
await server.createClient({
  clientId: 'alice-laptop',
  serverDefinedClientTags: ['engineering', 'office-berlin'],
});

// Client-side: informational tags (reported to server)
await client.connect({
  // ...
  clientDefinedClientTags: ['macOS', 'v2.1.0'],
});

🔄 Hub Client Management

The server acts as a hub — one API to manage all clients:

createClient() accepts IClientCreateOptions with a required clientId; updateClient() accepts TClientUpdateOptions. Both reject unknown fields, server-owned identity/key/address fields, malformed security, invalid IPv4 ACL patterns, out-of-range integers and invalid VLAN combinations. Rejected settings do not change the registry or consume a new address. forceTarget requires an explicit IPv4 target; malformed policies are also rejected at server startup.

Omitting an update field preserves it; explicit null clears an optional setting. Responses omit absent optional fields (including nested security/policy fields), matching the TypeScript interfaces; null is a clearing input, not an output value. A supplied security object replaces the whole security object, not individual nested fields. Clear forceVlan together with vlanId; an enabled VLAN requires an ID from 1 to 4094. Use serverDefinedClientTags; the shipped deprecated tags input remains accepted, but supplying both names is rejected as ambiguous. Preloaded entries also reject both populated tag fields; accepted old tags are consumed into the canonical field and are not retained in registry output. Input validation is separate from transport ownership and queued forwarding revocation; the runtime ownership rules below apply after successful admission.

Native WebSocket/QUIC handshakes also recheck the exact registry incarnation and revision after sending the Noise response, before local admission. A successful update, disable/re-enable or remove/recreate invalidates an in-flight handshake; a rejected update does not. Network response writes do not hold the registry lock.

Native and integrated WireGuard transports share one live session owner per registered client. Authenticated reconnect cancels and joins the previous transport generation before admitting its replacement. Disconnect, successful settings changes, disable, expiry, removal and key rotation retire the old transport and its WireGuard return relay. Queued WireGuard returns carry their original session identity and cannot target a replacement. Failed key-rotation reconciliation does not restore old credentials; inspect the registry and retry the operation to obtain fresh credentials.

Registry assignments are reserved before listeners start and reused unchanged across native/WireGuard connections. Disconnect retains that reservation; record removal releases it only after transport teardown. Explicit assignments, including raw WireGuard reservations, precede dynamic allocation. A failed create cannot release an address belonging to a concurrent registry revision or recreated record. Decrypted registered-client packets must have a valid IPv4 header and exactly the assigned source address. Connection ACLs use the outer transport endpoint; destination ACLs use the inner packet destination. Runtime rate-limit adjustments apply to the current session only; use registry security settings for reconnects.

Socket and bridge forwarding queues retain authenticated session ownership until their packet work is destroyed. Socket mode gives each session its own smoltcp stack; policy and PROXY identity come from that captured session, never a mutable registry lookup by packet source. Its TCP/UDP flows and socket readers are joined on revocation, and replies return only to their original owner. Flow incarnations also fence late responses when a tuple is reused. UDP flows sharing a destination are selected by received source endpoint, and real UDP sockets accept replies only from their configured target.

Return queues are also generation-owned: cancellation purges queued payloads without waiting for a consumer, including WireGuard's shared return queue. Dequeued packets retain their destination lifetime, and internal relay packets retain their source lifetime too, until I/O finishes or the payload is destroyed. An interrupted native ciphertext write retires that destination tunnel rather than continuing a partially written frame or an advanced Noise nonce sequence. WireGuard returns require a current encryption session. While rekeying without one, application packets are dropped and counted; only a handshake is requested, so plaintext cannot escape owner cancellation through BoringTun's private queue. Queues are bounded (256 per session, 1024 for shared WG returns); full or closed queues reject admission. Managed relay uses these same two-owner lifetimes.

Socket NAT is bounded to 256 active engines and 1,024 flow incarnations per server dispatcher. The per-client default is 128 TCP/UDP flows combined; a registered client's security.maxConnections overrides that local limit without bypassing the global cap. Late task/message references retain capacity until destroyed. Protocol and dispatch queues are bounded. This IPv4 userspace build does not reassemble fragments and rejects them before creating flows.

These guarantees cover daemon-owned work, not traffic already handed to an external network or kernel. Managed relay is described below. Transactional cleanup and privileged qualification of standalone kernel modes remain outstanding; packet fixtures do not prove production network readiness.

// Create (generates keys, assigns IP, returns config bundle)
const bundle = await server.createClient({ clientId: 'bob-phone' });

// Read
const entry = await server.getClient('bob-phone');
const all   = await server.listRegisteredClients();

// Update (ACLs, tags, description, rate limits...)
await server.updateClient('bob-phone', {
  security: { destinationAllowList: ['0.0.0.0/0'] },
  serverDefinedClientTags: ['mobile', 'field-ops'],
});

// Enable / Disable
await server.disableClient('bob-phone'); // disconnects + blocks reconnection
await server.enableClient('bob-phone');

// Key rotation
const newBundle = await server.rotateClientKey('bob-phone');

// Export config (without secrets)
const wgConf = await server.exportClientConfig('bob-phone', 'wireguard');

// Remove
await server.removeClient('bob-phone');

Caller-owned managed networks

forwardingMode: 'managed' is an exclusive IPv4 node relay. The hub creates no TUN, NAT, bridge or host routes. Start it with a fixed subnet and required managedAuthorityId, then call reconcileManagedNetwork(snapshot). Cloudly (or another trusted controller) owns durable node IDs, keys, control addresses, workload prefixes and policy; SmartVPN owns volatile enforcement, not persistence. Protect the management IPC endpoint: the authority ID is a binding, not a credential.

import type { IManagedNetworkSnapshot } from '@push.rocks/smartvpn';

const snapshot: IManagedNetworkSnapshot = {
  schemaVersion: 1,
  authorityId: 'cloudly',
  revision: 1,
  nodes: [{
    nodeId: 'worker-a',
    publicKey: '<caller-owned-noise-public-key-base64>',
    controlAddress: '10.88.0.2',
    controlPolicyDomain: 'control',
    enabled: true,
    workloadPrefixes: [{ cidr: '10.90.0.0/24', policyDomain: 'task.vc' }],
  }],
  grants: [], // Absent grants deny, including within the same domain.
};
const applied = await server.reconcileManagedNetwork(snapshot);
const status = await server.getManagedNetworkStatus();
const projection = await server.getManagedNodeProjection('worker-a');

Snapshots are complete replacements. Canonical array order makes exact replay idempotent; stale revisions and conflicting equal revisions reject. Revisions are positive JSON-safe integers scoped to lifetimeId. Every daemon restart starts empty and requires the caller to reconcile its current durable state. Node keys must be unique canonical/contributory X25519 keys, including across Noise and WireGuard. Control addresses are exact usable hosts in the fixed subnet; workload prefixes cannot overlap that subnet or any other owned prefix.

Grants are explicit (sourceDomain, destinationDomain) pairs. They neither imply same-domain permission nor reverse/reply permission. Different workload prefixes on one node may belong to different domains. Disabled, expired, missing, offline or denied destinations drop without host fallback. Traffic that never crosses the hub needs local enforcement by the node executor; local workload prefixes are never routed back to the hub merely because a grant involves them.

Invalid preparation preserves active authority. A valid change quarantines and cancels only affected node generations, joins their packet work, prepares WG crypto and commits the whole snapshot atomically before acknowledging. Unchanged nodes keep their sessions. A post-drain failure leaves state: 'failed', its pendingRevision and bounded lastError; old affected admission stays closed. Only the exact pending snapshot can retry. An IPC timeout or disconnected caller does not cancel the process-owned operation. Inspect status and replay that exact target; never assume a timed-out request did not commit. Separate Unix IPC connections can inspect status while apply drains; stdio remains sequential.

Standalone create/update/remove/enable/disable/rotate/disconnect/rate-limit and raw WG-peer mutations reject in managed mode, including direct WG-loop commands. Standalone startup clients, raw peers, DNS, NAT, bridge and destination-routing configuration reject before effects. Managed WireGuard requires an explicit serverEndpoint and starts without peers until reconciliation.

Native clients opt in with managedNetwork: { authorityId: 'cloudly', nodeId: 'worker-a' }. Missing, unexpected or differently bound managed assignments reject before host effects; a managed client cannot downgrade to a standalone subnet assignment. Native encrypted assignments carry schema, node/authority/lifetime/revision, local ownership and remote outgoing routes. Client getStatus().managedNetwork reports only the live validated assignment and clears on retirement. Native Linux TUN setup uses a fresh nonpersistent /32 device with strict exclusive route adds, not shell commands or route-conflict suppression. Preflight rejects malformed, overlapping, default, local-prefix and active-hub-endpoint capture. The netlink owner is polled inline; failure/cancellation drops the device and its owned routes. No DNS or default-route mutation is requested; native forwardingMode: 'testing' has no host effects. Managed TUN requires Linux.

The Linux amd64 musl build passed the isolated serve.zone/testing VM scenario: real WS/Noise transport and TUN workload traffic between nodes connected only to the hub, directed allow/deny, reconnect, natural revocation, partial route-conflict rollback, empty-authority restart, and child termination. Cleanup removed owned namespaces and preserved DNS and IPv4/IPv6 default routes. This qualification does not cover WSS/private CAs, IPv6 workload traffic, privileged ARM64 networking, standalone kernel modes, or Pallet/Cloudly production integration.

WireGuard has no assignment channel. getManagedNodeProjection(nodeId) returns the applied control /32, per-prefix domains, outgoing destinations, incoming sources, MTU and optional public WG configuration, never a private key. Client WG AllowedIPs is the union of remote outgoing/incoming prefixes plus the hub's MTU-feedback gateway /32; the hub independently enforces direction. Server WG AllowedIPs contains the node's owned source prefixes. Authenticated WG keepalive confirmation admits receive-only managed nodes without an inner probe. Projection ready means applied/admissible configuration, not connectivity, node installation or deployment readiness; disabled, absent and quarantined nodes return no config. The controller must apply WG projections through its owned node runtime.

Bounds are 1,024 nodes, 128 workload prefixes per node, 16,384 owned prefixes, 16,384 grants, 128-byte IDs, 1,024 routes per node per direction, and 262,144 total projected routes/grants. Managed relay enforces both live negotiated MTUs and returns eligible IPv4 fragmentation-needed feedback to the exact source owner. The complete third-party notice index covers the locked Cargo graph and statically linked compiler/runtime components. Keep its accompanying notice files with separately redistributed executables.

📝 WireGuard Config Generation

Generate standard .conf files for any WireGuard client:

import { WgConfigGenerator } from '@push.rocks/smartvpn';

const conf = WgConfigGenerator.generateClientConfig({
  privateKey: '<client-wg-private-key>',
  address: '10.8.0.2/24',
  dns: ['1.1.1.1'],
  peer: {
    publicKey: '<server-wg-public-key>',
    endpoint: 'vpn.example.com:51820',
    allowedIps: ['0.0.0.0/0'],
    persistentKeepalive: 25,
  },
});
// → standard WireGuard .conf compatible with wg-quick, iOS, Android

Server configs too:

const serverConf = WgConfigGenerator.generateServerConfig({
  privateKey: '<server-wg-private-key>',
  address: '10.8.0.1/24',
  listenPort: 51820,
  enableNat: true,
  natInterface: 'eth0',
  peers: [
    { publicKey: '<client-wg-public-key>', allowedIps: ['10.8.0.2/32'] },
  ],
});

🖥️ System Service Installation

Generate systemd (Linux) or launchd (macOS) service units:

import { VpnInstaller } from '@push.rocks/smartvpn';

const unit = VpnInstaller.generateServiceUnit({
  binaryPath: '/usr/local/bin/smartvpn_daemon',
  socketPath: '/var/run/smartvpn.sock',
  mode: 'server',
});
// unit.platform    → 'linux' | 'macos'
// unit.content     → systemd unit file or launchd plist
// unit.installPath → /etc/systemd/system/smartvpn-server.service

You can also call generateSystemdUnit() or generateLaunchdPlist() directly for platform-specific options like custom descriptions.

📢 Runtime Events

VpnServer and VpnClient extend EventEmitter. The high-level wrappers currently forward bridge lifecycle events:

server.on('exit', ({ code, signal }) => { /* daemon process exited */ });
server.on('reconnected', () => { /* socket transport reconnected */ });
client.on('exit', ({ code, signal }) => { /* daemon process exited */ });
Event Emitted By Payload
exit Both { code, signal } — daemon process exited
reconnected Both void — socket transport reconnected
stderr Both string — daemon diagnostic line, forwarded without automatic logging

For connection state and telemetry, use getStatus(), getStatistics(), listClients(), and getClientTelemetry().

API Reference 📖

Classes

Class Description
VpnServer Manages the Rust daemon in server mode. Hub methods for client CRUD, telemetry, rate limits, WireGuard peer management.
VpnClient Manages the Rust daemon in client mode. Connect, disconnect, status, telemetry.
VpnBridge<T> Low-level typed IPC bridge (stdio or Unix socket). Handles spawn, connect, reconnect, and typed command dispatch.
VpnConfig Static config validation and JSON file I/O. Validates keys, addresses, CIDRs, MTU, etc.
VpnInstaller Generates systemd/launchd service files for daemon deployment.
WgConfigGenerator Generates standard WireGuard .conf files (client and server).

Key Interfaces

Interface Purpose
IVpnServerConfig Server configuration (listen addr, keys, subnet, transport mode, forwarding mode incl. bridge/hybrid, clients, proxy protocol, destination policy)
IVpnClientConfig Client configuration (server URL, keys, transport, forwarding mode, WG options, client-defined tags)
IClientEntry Server-side client definition (ID, keys, security, priority, server/client tags, expiry, bridge/VLAN settings)
IClientSecurity Per-client ACLs, rate limits, and destination policy override (SmartProxy-aligned naming)
IClientRateLimit Rate limiting config (bytesPerSec, burstBytes)
IClientConfigBundle Full config bundle returned by createClient() — includes SmartVPN config, WireGuard .conf, and secrets
IVpnClientInfo Connected client info (IP, stats, authenticated key, remote addr, transport type)
IVpnServerStatistics Server stats with per-transport breakdowns (activeClientsWebsocket/Quic/Wireguard, totalConnections*)
IVpnConnectionQuality RTT, jitter, loss ratio, link health
IVpnMtuInfo TUN MTU, effective MTU, overhead bytes, oversized packet stats
IVpnKeypair Base64-encoded public/private key pair
IDestinationPolicy Destination routing policy (forceTarget / block / allow with allow/block lists)
IVpnEventMap Exported event payload shapes for lifecycle and daemon event integrations

Server IPC Commands

Command Description
start / stop Start/stop the VPN listener
reconcileManagedNetwork Validate, drain and atomically apply a complete managed snapshot
getManagedNetworkStatus / getManagedNodeProjection Authority/lifetime/revision status and caller-applied node configuration
createClient Generate keys, assign IP, return config bundle
removeClient / getClient / listRegisteredClients Client registry CRUD
updateClient / enableClient / disableClient Modify client state
rotateClientKey Fresh keypairs + new config bundle
exportClientConfig Re-export as SmartVPN config or WireGuard .conf
listClients / disconnectClient Manage live connections
setClientRateLimit / removeClientRateLimit Runtime rate limit adjustments
getStatus / getStatistics / getClientTelemetry Monitoring (stats include per-transport breakdowns)
generateKeypair / generateWgKeypair / generateClientKeypair Key generation
addWgPeer / removeWgPeer / listWgPeers WireGuard peer management

Client IPC Commands

Command Description
connect / disconnect Manage the tunnel
getStatus / getStatistics Connection state and traffic stats
getConnectionQuality RTT, jitter, loss, link health
getMtuInfo MTU and overhead details

Transport Modes 🔀

Server Configuration

// All transports simultaneously (default) — WS + QUIC + WireGuard
{ transportMode: 'all', listenAddr: '0.0.0.0:443', wgPrivateKey: '...', wgListenPort: 51820 }

// WS + QUIC only
{ transportMode: 'both', listenAddr: '0.0.0.0:443', quicListenAddr: '0.0.0.0:4433' }

// WebSocket only
{ transportMode: 'websocket', listenAddr: '0.0.0.0:443' }

// QUIC only
{ transportMode: 'quic', listenAddr: '0.0.0.0:443' }

// WireGuard only
{ transportMode: 'wireguard', wgPrivateKey: '...', wgListenPort: 51820, wgPeers: [...] }

All transport modes share the same forwardingMode — WireGuard peers can use 'socket' (userspace NAT) just like WS/QUIC clients.

Client Configuration

// Auto (tries QUIC first, falls back to WS)
{ transport: 'auto', serverUrl: 'wss://vpn.example.com' }

// Explicit QUIC with certificate pinning
{ transport: 'quic', serverUrl: '1.2.3.4:4433', serverCertHash: '<sha256-base64>' }

// WireGuard clients use the standard .conf returned by createClient()
// or generated via WgConfigGenerator.

Cryptography 🔑

Layer Algorithm Purpose
Handshake Noise IK (X25519 + ChaChaPoly + BLAKE2s) Mutual authentication + key exchange
Transport Noise transport state (ChaChaPoly) All post-handshake data encryption
Utility XChaCha20-Poly1305 helper Nonce-safe symmetric encryption helper in the Rust crypto module
WireGuard X25519 + ChaCha20-Poly1305 (via boringtun) Standard WireGuard crypto

Binary Protocol 📡

All frames use [type:1B][length:4B][payload:NB] with a 64KB max payload:

Type Hex Direction Description
HandshakeInit 0x01 Client → Server Noise IK first message
HandshakeResp 0x02 Server → Client Noise IK response
IpPacket 0x10 Bidirectional Encrypted tunnel data
Keepalive 0x20 Client → Server App-level keepalive (not WS ping)
KeepaliveAck 0x21 Server → Client Keepalive response with RTT payload
SessionResume 0x30 Client → Server Session resume attempt
SessionResumeOk 0x31 Server → Client Session resume accepted
SessionResumeErr 0x32 Server → Client Session resume rejected
Disconnect 0x3F Bidirectional Graceful disconnect

Development 🛠️

Native binary selection and build prerequisites

The package builds smartvpn_daemon_linux_amd64_musl and smartvpn_daemon_linux_arm64_musl with the pinned Rust 1.95.0 toolchain and locked Cargo dependencies. Both targets require genuine musl C toolchains for the native ring and mimalloc dependencies. Put x86_64-linux-musl-gcc and aarch64-linux-musl-gcc on PATH before building or testing; GNU libc headers are not a substitute. The existing build and test commands remain unchanged.

In stdio mode the bridge selects exactly the bundled executable for Linux x64 or arm64. SMARTVPN_RUST_BINARY selects an explicit development executable on any supported native build platform. Missing, non-executable, and empty explicit selections fail: there is no fallback to old GNU artifacts, the current working directory's Rust builds, platform packages, or a daemon found on PATH. Other platforms must provide that explicit executable. Socket transport connects to an independently managed daemon and does not require a local binary.

For an unprivileged build host, musl's upstream musl-gcc wrappers can provide the C toolchains using existing native and cross GCC compilers. Build musl 1.2.5 in separate disposable prefixes for each architecture, with --disable-shared and an explicitly prefix-local --syslibdir. Use the two musl security patches from Rust 1.95.0's build recipe. Expose the generated wrappers under the target-specific names above; never install a replacement libc or dynamic linker into the host's system directories.

# Install dependencies
pnpm install

# Build (TypeScript + Rust cross-compile)
pnpm build

# Run all tests
pnpm test

# Run Rust tests directly
cd rust && cargo test

# Run a specific TS test
tstest test/test.flowcontrol.node.ts --verbose

Project Structure

smartvpn/
├── ts/                         # TypeScript control plane
│   ├── index.ts                # All exports
│   ├── smartvpn.interfaces.ts  # Interfaces, types, IPC command maps
│   ├── smartvpn.plugins.ts     # Dependency imports
│   ├── smartvpn.paths.ts       # Binary path resolution
│   ├── smartvpn.classes.vpnserver.ts
│   ├── smartvpn.classes.vpnclient.ts
│   ├── smartvpn.classes.vpnbridge.ts
│   ├── smartvpn.classes.vpnconfig.ts
│   ├── smartvpn.classes.vpninstaller.ts
│   └── smartvpn.classes.wgconfig.ts
├── rust/                       # Rust data plane daemon
│   └── src/
│       ├── main.rs             # CLI entry point
│       ├── server.rs           # VPN server + hub methods
│       ├── client.rs           # VPN client
│       ├── crypto.rs           # Noise IK + XChaCha20
│       ├── client_registry.rs  # Client database
│       ├── acl.rs              # ACL engine
│       ├── proxy_protocol.rs   # PROXY protocol v2 parser
│       ├── management.rs       # JSON-lines IPC
│       ├── transport.rs        # WebSocket transport
│       ├── transport_trait.rs  # Transport abstraction (Sink/Stream)
│       ├── quic_transport.rs   # QUIC transport
│       ├── wireguard.rs        # WireGuard (boringtun)
│       ├── bridge.rs           # Linux bridge/TAP integration
│       ├── codec.rs            # Binary frame protocol
│       ├── keepalive.rs        # Adaptive keepalives
│       ├── ratelimit.rs        # Token bucket
│       ├── userspace_nat.rs    # Userspace TCP/UDP NAT proxy
│       ├── tunnel.rs           # TUN device management
│       ├── network.rs          # IP pool + networking
│       ├── telemetry.rs        # RTT/jitter/loss tracking
│       ├── qos.rs              # Priority queues + smart dropping
│       ├── mtu.rs              # MTU + ICMP too-big
│       └── reconnect.rs        # Exponential backoff + session tokens
├── test/                       # Test files
├── dist_ts/                    # Compiled TypeScript
└── dist_rust/                  # Static musl binaries (Linux amd64 + arm64)

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
A high-performance VPN solution with a TypeScript control plane and a Rust data plane daemon. Enterprise-ready client authentication, triple transport support (WebSocket + QUIC + WireGuard), and a typed hub API for managing clients from code.
Readme
1.2 MiB
Languages
Rust 48.9%
HTML 40.8%
TypeScript 10%
Python 0.3%