feat(readme): document QoS, telemetry, MTU, and rate limiting capabilities in the README

This commit is contained in:
2026-03-15 18:16:15 +00:00
parent 4fab721d87
commit aec545fe8c
3 changed files with 210 additions and 68 deletions

View File

@@ -1,5 +1,12 @@
# Changelog
## 2026-03-15 - 1.2.0 - feat(readme)
document QoS, telemetry, MTU, and rate limiting capabilities in the README
- Expand the architecture and feature overview to cover adaptive keepalive, telemetry, QoS, rate limiting, and MTU handling
- Update client and server examples to show new APIs such as getConnectionQuality(), getMtuInfo(), setClientRateLimit(), and getClientTelemetry()
- Add TypeScript interface documentation for connection quality, MTU info, enriched client statistics, and per-client telemetry
## 2026-03-15 - 1.1.0 - feat(rust-core)
add adaptive keepalive telemetry, MTU handling, and per-client rate limiting APIs

269
readme.md
View File

@@ -1,6 +1,6 @@
# @push.rocks/smartvpn
A high-performance VPN solution with a **TypeScript control plane** and a **Rust data plane daemon**. Manage VPN connections with clean, typed APIs while all networking heavy lifting — encryption, tunneling, packet forwarding — runs at native speed in Rust.
A high-performance VPN with a **TypeScript control plane** and a **Rust data plane daemon**. Manage VPN connections with clean, fully-typed APIs while all networking heavy lifting — encryption, tunneling, QoS, rate limiting — runs at native speed in Rust.
## Issue Reporting and Security
@@ -9,8 +9,6 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## Install
```bash
npm install @push.rocks/smartvpn
# or
pnpm install @push.rocks/smartvpn
```
@@ -18,17 +16,21 @@ pnpm install @push.rocks/smartvpn
```
TypeScript (control plane) Rust (data plane)
┌──────────────────────────┐ ┌───────────────────────────────┐
│ VpnClient / VpnServer │ │ smartvpn_daemon │
│ └─ VpnBridge │──stdio/──▶ │ ├─ management (JSON IPC) │
│ └─ RustBridge │ socket │ ├─ transport (WebSocket/TLS) │
│ (smartrust) │ │ ├─ crypto (Noise NK + XCha)
└──────────────────────────┘ │ ├─ codec (binary framing) │
│ ├─ keepalive (app-level)
│ ├─ tunnel (TUN device)
│ ├─ network (NAT/IP pool)
─ reconnect (backoff) │
└───────────────────────────────┘
┌──────────────────────────┐ ┌────────────────────────────────────
│ VpnClient / VpnServer │ │ smartvpn_daemon
│ └─ VpnBridge │──stdio/──▶ │ ├─ management (JSON IPC)
│ └─ RustBridge │ socket │ ├─ transport (WebSocket/TLS)
│ (smartrust) │ │ ├─ crypto (Noise NK + XCha20)
└──────────────────────────┘ │ ├─ codec (binary framing)
│ ├─ keepalive (adaptive state FSM)
│ ├─ telemetry (RTT/jitter/loss)
│ ├─ qos (classify + priority Q)
─ ratelimit (token bucket) │
│ ├─ mtu (overhead calc + ICMP) │
│ ├─ tunnel (TUN device) │
│ ├─ network (NAT/IP pool) │
│ └─ reconnect (exp. backoff) │
└────────────────────────────────────┘
```
**Key design decisions:**
@@ -37,8 +39,10 @@ TypeScript (control plane) Rust (data plane)
|----------|--------|-----|
| Transport | WebSocket over HTTPS | Works through Cloudflare and other terminating proxies |
| Encryption | Noise NK + XChaCha20-Poly1305 | Strong forward secrecy, large nonce space (no counter needed) |
| Keepalive | App-level (not WS pings) | Cloudflare drops WS ping frames; app-level pings survive |
| IPC | JSON lines over stdio/Unix socket | `stdio` for dev, `socket` for production (daemon stays alive) |
| Keepalive | Adaptive app-level pings | Cloudflare drops WS pings; interval adapts to link health (1060s) |
| QoS | Packet classification + priority queues | DNS/SSH/ICMP always drain first; bulk flows get deprioritized |
| Rate limiting | Per-client token bucket | Byte-granular, dynamically reconfigurable via IPC |
| IPC | JSON lines over stdio / Unix socket | `stdio` for dev, `socket` for production (daemon stays alive) |
| Binary protocol | `[type:1B][length:4B][payload:NB]` | Minimal overhead, easy to parse at wire speed |
## 🚀 Quick Start
@@ -48,15 +52,12 @@ TypeScript (control plane) Rust (data plane)
```typescript
import { VpnClient } from '@push.rocks/smartvpn';
// Development: spawn the Rust daemon as a child process
const client = new VpnClient({
transport: { transport: 'stdio' },
});
// Start the daemon bridge
await client.start();
// Connect to a VPN server
const { assignedIp } = await client.connect({
serverUrl: 'wss://vpn.example.com/tunnel',
serverPublicKey: 'BASE64_SERVER_PUBLIC_KEY',
@@ -67,15 +68,23 @@ const { assignedIp } = await client.connect({
console.log(`Connected! Assigned IP: ${assignedIp}`);
// Check status
const status = await client.getStatus();
console.log(status); // { state: 'connected', assignedIp: '10.8.0.2', ... }
// Connection quality (adaptive keepalive + telemetry)
const quality = await client.getConnectionQuality();
console.log(quality);
// {
// srttMs: 42.5, jitterMs: 3.2, minRttMs: 38.0, maxRttMs: 67.0,
// lossRatio: 0.0, consecutiveTimeouts: 0,
// linkHealth: 'healthy', currentKeepaliveIntervalSecs: 60
// }
// Get traffic stats
// MTU info
const mtu = await client.getMtuInfo();
console.log(mtu);
// { tunMtu: 1420, effectiveMtu: 1421, linkMtu: 1500, overheadBytes: 79, ... }
// Traffic stats (includes quality snapshot)
const stats = await client.getStatistics();
console.log(stats); // { bytesSent, bytesReceived, packetsSent, ... }
// Disconnect
await client.disconnect();
client.stop();
```
@@ -89,33 +98,44 @@ const server = new VpnServer({
transport: { transport: 'stdio' },
});
// Start the daemon and the VPN server
// Generate a Noise keypair first
await server.start();
// If you don't have keys yet:
const keypair = await server.generateKeypair();
// Start the VPN listener (or pass config to start() directly)
await server.start({
listenAddr: '0.0.0.0:443',
privateKey: 'BASE64_PRIVATE_KEY',
publicKey: 'BASE64_PUBLIC_KEY',
privateKey: keypair.privateKey,
publicKey: keypair.publicKey,
subnet: '10.8.0.0/24',
dns: ['1.1.1.1'],
mtu: 1420,
enableNat: true,
// Optional: default rate limit for all new clients
defaultRateLimitBytesPerSec: 10_000_000, // 10 MB/s
defaultBurstBytes: 20_000_000, // 20 MB burst
});
// Generate a Noise keypair
const keypair = await server.generateKeypair();
console.log(keypair); // { publicKey: '...', privateKey: '...' }
// List connected clients
const clients = await server.listClients();
// [{ clientId, assignedIp, connectedSince, bytesSent, bytesReceived }]
// Disconnect a specific client
await server.disconnectClient('some-client-id');
// Per-client rate limiting (live, no reconnect needed)
await server.setClientRateLimit('client-id', 5_000_000, 10_000_000);
await server.removeClientRateLimit('client-id'); // unlimited
// Get server stats
const stats = await server.getStatistics();
// { bytesSent, bytesReceived, activeClients, totalConnections, ... }
// Per-client telemetry
const telemetry = await server.getClientTelemetry('client-id');
console.log(telemetry);
// {
// clientId, assignedIp, lastKeepaliveAt, keepalivesReceived,
// packetsDropped, bytesDropped, bytesReceived, bytesSent,
// rateLimitBytesPerSec, burstBytes
// }
// Kick a client
await server.disconnectClient('client-id');
// Stop
await server.stopServer();
server.stop();
```
@@ -151,7 +171,9 @@ When using socket transport, `client.stop()` closes the socket but **does not ki
| `connect(config?)` | `Promise<{ assignedIp }>` | Connect to VPN server |
| `disconnect()` | `Promise<void>` | Disconnect from VPN |
| `getStatus()` | `Promise<IVpnStatus>` | Current connection state |
| `getStatistics()` | `Promise<IVpnStatistics>` | Traffic statistics |
| `getStatistics()` | `Promise<IVpnStatistics>` | Traffic stats + connection quality |
| `getConnectionQuality()` | `Promise<IVpnConnectionQuality>` | RTT, jitter, loss, link health |
| `getMtuInfo()` | `Promise<IVpnMtuInfo>` | MTU info and overhead breakdown |
| `stop()` | `void` | Kill/close the daemon bridge |
| `running` | `boolean` | Whether bridge is active |
@@ -163,9 +185,12 @@ When using socket transport, `client.stop()` closes the socket but **does not ki
| `stopServer()` | `Promise<void>` | Stop the VPN server |
| `getStatus()` | `Promise<IVpnStatus>` | Server connection state |
| `getStatistics()` | `Promise<IVpnServerStatistics>` | Server stats (includes client counts) |
| `listClients()` | `Promise<IVpnClientInfo[]>` | Connected clients |
| `listClients()` | `Promise<IVpnClientInfo[]>` | Connected clients with QoS stats |
| `disconnectClient(id)` | `Promise<void>` | Kick a client |
| `generateKeypair()` | `Promise<IVpnKeypair>` | Generate Noise NK keypair |
| `setClientRateLimit(id, rate, burst)` | `Promise<void>` | Set per-client rate limit (bytes/sec) |
| `removeClientRateLimit(id)` | `Promise<void>` | Remove rate limit (unlimited) |
| `getClientTelemetry(id)` | `Promise<IVpnClientTelemetry>` | Per-client telemetry + drop stats |
| `stop()` | `void` | Kill/close the daemon bridge |
### `VpnConfig`
@@ -191,26 +216,23 @@ Generate system service units for the daemon:
```typescript
import { VpnInstaller } from '@push.rocks/smartvpn';
// Auto-detect platform
const platform = VpnInstaller.detectPlatform(); // 'linux' | 'macos' | 'windows' | 'unknown'
// Generate systemd unit (Linux)
// Linux (systemd)
const unit = VpnInstaller.generateSystemdUnit({
binaryPath: '/usr/local/bin/smartvpn_daemon',
socketPath: '/var/run/smartvpn.sock',
mode: 'server',
});
// unit.content = full systemd .service file
// unit.installPath = '/etc/systemd/system/smartvpn-server.service'
// Generate launchd plist (macOS)
// macOS (launchd)
const plist = VpnInstaller.generateLaunchdPlist({
binaryPath: '/usr/local/bin/smartvpn_daemon',
socketPath: '/var/run/smartvpn.sock',
mode: 'client',
});
// Auto-detect and generate
// Auto-detect platform
const serviceUnit = VpnInstaller.generateServiceUnit({
binaryPath: '/usr/local/bin/smartvpn_daemon',
socketPath: '/var/run/smartvpn.sock',
@@ -223,8 +245,6 @@ const serviceUnit = VpnInstaller.generateServiceUnit({
Both `VpnClient` and `VpnServer` extend `EventEmitter`:
```typescript
client.on('status', (status) => { /* IVpnStatus */ });
client.on('error', (err) => { /* { message, code? } */ });
client.on('exit', ({ code, signal }) => { /* daemon exited */ });
client.on('reconnected', () => { /* socket reconnected */ });
@@ -232,13 +252,84 @@ server.on('client-connected', (info) => { /* IVpnClientInfo */ });
server.on('client-disconnected', ({ clientId, reason }) => { /* ... */ });
```
## 📊 QoS System
The Rust daemon includes a full QoS stack that operates on decrypted IP packets:
### Adaptive Keepalive
The keepalive system automatically adjusts its interval based on connection quality:
| Link Health | Keepalive Interval | Triggered When |
|-------------|-------------------|----------------|
| 🟢 Healthy | 60s | Jitter < 30ms, loss < 2%, no timeouts |
| 🟡 Degraded | 30s | Jitter > 50ms, loss > 5%, or 1+ timeout |
| 🔴 Critical | 10s | Loss > 20% or 2+ consecutive timeouts |
State transitions include hysteresis (3 consecutive good checks to upgrade, 2 to recover) to prevent flapping. Dead peer detection fires after 3 consecutive timeouts in Critical state.
### Packet Classification
IP packets are classified into three priority levels by inspecting headers (no deep packet inspection):
| Priority | Traffic |
|----------|---------|
| **High** | ICMP, DNS (port 53), SSH (port 22), small packets (< 128 bytes) |
| **Normal** | Everything else |
| **Low** | Bulk flows exceeding 1 MB within a 60s window |
Priority channels drain with biased `tokio::select!` — high-priority packets always go first.
### Smart Packet Dropping
Under backpressure, packets are dropped intelligently:
1. **Low** queue full → drop silently
2. **Normal** queue full → drop
3. **High** queue full → wait 5ms, then drop as last resort
Drop statistics are tracked per priority level and exposed via telemetry.
### Per-Client Rate Limiting
Token bucket algorithm with byte granularity:
```typescript
// Set: 10 MB/s sustained, 20 MB burst
await server.setClientRateLimit('client-id', 10_000_000, 20_000_000);
// Check drops via telemetry
const t = await server.getClientTelemetry('client-id');
console.log(`Dropped: ${t.packetsDropped} packets, ${t.bytesDropped} bytes`);
// Remove limit
await server.removeClientRateLimit('client-id');
```
Rate limits can be changed live without disconnecting the client.
### Path MTU
Tunnel overhead is calculated precisely:
| Layer | Bytes |
|-------|-------|
| IP header | 20 |
| TCP header (with timestamps) | 32 |
| WebSocket framing | 6 |
| VPN frame header | 5 |
| Noise AEAD tag | 16 |
| **Total overhead** | **79** |
For a standard 1500-byte Ethernet link, effective TUN MTU = **1421 bytes**. The default TUN MTU of 1420 is conservative and correct. Oversized packets get an ICMP "Fragmentation Needed" (Type 3, Code 4) written back into the TUN, so the source TCP adjusts its MSS automatically.
## 🔐 Security Model
The VPN uses a **Noise NK** handshake pattern:
1. **NK** = client does **N**ot authenticate, but **K**nows the server's static public key
2. The client generates an ephemeral keypair, performs `e, es` (Diffie-Hellman with server's static key)
3. Server responds with `e, ee` (Diffie-Hellman with both ephemeral keys)
2. The client generates an ephemeral keypair, performs `e, es` (DH with server's static key)
3. Server responds with `e, ee` (DH with both ephemeral keys)
4. Result: forward-secret transport keys derived from both DH operations
Post-handshake, all IP packets are encrypted with **XChaCha20-Poly1305**:
@@ -261,8 +352,8 @@ Inside the WebSocket tunnel, packets use a simple binary framing:
| `HandshakeInit` | `0x01` | Client → Server handshake |
| `HandshakeResp` | `0x02` | Server → Client handshake |
| `IpPacket` | `0x10` | Encrypted IP packet |
| `Keepalive` | `0x20` | App-level ping |
| `KeepaliveAck` | `0x21` | App-level pong |
| `Keepalive` | `0x20` | App-level ping (8-byte timestamp payload) |
| `KeepaliveAck` | `0x21` | App-level pong (echoes timestamp for RTT) |
| `SessionResume` | `0x30` | Resume a dropped session |
| `SessionResumeOk` | `0x31` | Resume accepted |
| `SessionResumeErr` | `0x32` | Resume rejected |
@@ -270,8 +361,6 @@ Inside the WebSocket tunnel, packets use a simple binary framing:
## 🛠️ Rust Daemon CLI
The Rust binary supports several modes:
```bash
# Development: stdio management (JSON lines on stdin/stdout)
smartvpn_daemon --management --mode client
@@ -290,16 +379,14 @@ smartvpn_daemon --generate-keypair
# Install dependencies
pnpm install
# Build TypeScript + cross-compile Rust
# Build TypeScript + cross-compile Rust (amd64 + arm64)
pnpm build
# Build Rust only (debug)
cd rust && cargo build
# Run Rust tests
# Run all tests (71 Rust + 32 TypeScript)
cd rust && cargo test
# Run TypeScript tests
pnpm test
```
@@ -323,25 +410,27 @@ type TVpnTransportOptions =
// Client config
interface IVpnClientConfig {
serverUrl: string; // e.g. 'wss://vpn.example.com/tunnel'
serverPublicKey: string; // base64-encoded Noise static key
serverUrl: string;
serverPublicKey: string;
dns?: string[];
mtu?: number; // default: 1420
keepaliveIntervalSecs?: number; // default: 30
mtu?: number;
keepaliveIntervalSecs?: number;
}
// Server config
interface IVpnServerConfig {
listenAddr: string; // e.g. '0.0.0.0:443'
privateKey: string; // base64 Noise static private key
publicKey: string; // base64 Noise static public key
subnet: string; // e.g. '10.8.0.0/24'
listenAddr: string;
privateKey: string;
publicKey: string;
subnet: string;
tlsCert?: string;
tlsKey?: string;
dns?: string[];
mtu?: number;
keepaliveIntervalSecs?: number;
enableNat?: boolean;
defaultRateLimitBytesPerSec?: number;
defaultBurstBytes?: number;
}
// Status
@@ -365,6 +454,7 @@ interface IVpnStatistics {
keepalivesSent: number;
keepalivesReceived: number;
uptimeSeconds: number;
quality?: IVpnConnectionQuality;
}
interface IVpnServerStatistics extends IVpnStatistics {
@@ -372,12 +462,57 @@ interface IVpnServerStatistics extends IVpnStatistics {
totalConnections: number;
}
// Connection quality (QoS)
type TVpnLinkHealth = 'healthy' | 'degraded' | 'critical';
interface IVpnConnectionQuality {
srttMs: number;
jitterMs: number;
minRttMs: number;
maxRttMs: number;
lossRatio: number;
consecutiveTimeouts: number;
linkHealth: TVpnLinkHealth;
currentKeepaliveIntervalSecs: number;
}
// MTU info
interface IVpnMtuInfo {
tunMtu: number;
effectiveMtu: number;
linkMtu: number;
overheadBytes: number;
oversizedPacketsDropped: number;
icmpTooBigSent: number;
}
// Client info (with QoS fields)
interface IVpnClientInfo {
clientId: string;
assignedIp: string;
connectedSince: string;
bytesSent: number;
bytesReceived: number;
packetsDropped: number;
bytesDropped: number;
lastKeepaliveAt?: string;
keepalivesReceived: number;
rateLimitBytesPerSec?: number;
burstBytes?: number;
}
// Per-client telemetry
interface IVpnClientTelemetry {
clientId: string;
assignedIp: string;
lastKeepaliveAt?: string;
keepalivesReceived: number;
packetsDropped: number;
bytesDropped: number;
bytesReceived: number;
bytesSent: number;
rateLimitBytesPerSec?: number;
burstBytes?: number;
}
interface IVpnKeypair {

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartvpn',
version: '1.1.0',
version: '1.2.0',
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
}