feat(vpn transport): add QUIC transport support with auto fallback to WebSocket
This commit is contained in:
155
readme.md
155
readme.md
@@ -2,6 +2,12 @@
|
||||
|
||||
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.
|
||||
|
||||
🔒 **Noise NK** handshake + **XChaCha20-Poly1305** encryption
|
||||
🚀 **Dual transport**: WebSocket (Cloudflare-friendly) and raw **QUIC** (with datagram support)
|
||||
📊 **Adaptive QoS**: packet classification, priority queues, per-client rate limiting
|
||||
🔄 **Auto-transport**: tries QUIC first, falls back to WebSocket seamlessly
|
||||
📡 **Real-time telemetry**: RTT, jitter, loss, link health — all exposed via typed APIs
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://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/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
@@ -17,11 +23,13 @@ pnpm install @push.rocks/smartvpn
|
||||
```
|
||||
TypeScript (control plane) Rust (data plane)
|
||||
┌──────────────────────────┐ ┌────────────────────────────────────┐
|
||||
│ VpnClient / VpnServer │ │ smartvpn_daemon │
|
||||
│ VpnClient / VpnServer │ │ smartvpn_daemon │
|
||||
│ └─ VpnBridge │──stdio/──▶ │ ├─ management (JSON IPC) │
|
||||
│ └─ RustBridge │ socket │ ├─ transport (WebSocket/TLS) │
|
||||
│ (smartrust) │ │ ├─ crypto (Noise NK + XCha20) │
|
||||
└──────────────────────────┘ │ ├─ codec (binary framing) │
|
||||
│ └─ RustBridge │ socket │ ├─ transport_trait (abstraction) │
|
||||
│ (smartrust) │ │ │ ├─ transport (WebSocket/TLS) │
|
||||
└──────────────────────────┘ │ │ └─ quic_transport (QUIC/UDP) │
|
||||
│ ├─ crypto (Noise NK + XCha20) │
|
||||
│ ├─ codec (binary framing) │
|
||||
│ ├─ keepalive (adaptive state FSM) │
|
||||
│ ├─ telemetry (RTT/jitter/loss) │
|
||||
│ ├─ qos (classify + priority Q) │
|
||||
@@ -37,8 +45,10 @@ TypeScript (control plane) Rust (data plane)
|
||||
|
||||
| Decision | Choice | Why |
|
||||
|----------|--------|-----|
|
||||
| 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) |
|
||||
| Transport | WebSocket + QUIC (dual) | WS works through Cloudflare; QUIC gives lower latency + unreliable datagrams |
|
||||
| Auto-transport | QUIC first, WS fallback | Best performance when QUIC is available, graceful degradation when it's not |
|
||||
| Encryption | Noise NK + XChaCha20-Poly1305 | Strong forward secrecy, large nonce space (no counter sync needed) |
|
||||
| QUIC auth | Certificate hash pinning | WireGuard-style trust model — no CA needed, just pin the server cert hash |
|
||||
| Keepalive | Adaptive app-level pings | Cloudflare drops WS pings; interval adapts to link health (10–60s) |
|
||||
| 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 |
|
||||
@@ -89,6 +99,39 @@ await client.disconnect();
|
||||
client.stop();
|
||||
```
|
||||
|
||||
### VPN Client with QUIC
|
||||
|
||||
```typescript
|
||||
import { VpnClient } from '@push.rocks/smartvpn';
|
||||
|
||||
// Explicit QUIC — serverUrl is host:port, pinned by cert hash
|
||||
const quicClient = new VpnClient({
|
||||
transport: { transport: 'stdio' },
|
||||
});
|
||||
|
||||
await quicClient.start();
|
||||
|
||||
const { assignedIp } = await quicClient.connect({
|
||||
serverUrl: 'vpn.example.com:443',
|
||||
serverPublicKey: 'BASE64_SERVER_PUBLIC_KEY',
|
||||
transport: 'quic',
|
||||
serverCertHash: 'BASE64_SHA256_CERT_HASH', // printed by server on startup
|
||||
});
|
||||
|
||||
// Or use auto-transport: tries QUIC first (3s timeout), falls back to WS
|
||||
const autoClient = new VpnClient({
|
||||
transport: { transport: 'stdio' },
|
||||
});
|
||||
|
||||
await autoClient.start();
|
||||
|
||||
await autoClient.connect({
|
||||
serverUrl: 'wss://vpn.example.com/tunnel', // WS URL — host:port extracted for QUIC attempt
|
||||
serverPublicKey: 'BASE64_SERVER_PUBLIC_KEY',
|
||||
transport: 'auto', // default — QUIC first, then WS
|
||||
});
|
||||
```
|
||||
|
||||
### VPN Server
|
||||
|
||||
```typescript
|
||||
@@ -100,10 +143,9 @@ const server = new VpnServer({
|
||||
|
||||
// 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)
|
||||
// Start the VPN listener
|
||||
await server.start({
|
||||
listenAddr: '0.0.0.0:443',
|
||||
privateKey: keypair.privateKey,
|
||||
@@ -112,6 +154,12 @@ await server.start({
|
||||
dns: ['1.1.1.1'],
|
||||
mtu: 1420,
|
||||
enableNat: true,
|
||||
// Transport mode: 'websocket', 'quic', or 'both' (default)
|
||||
transportMode: 'both',
|
||||
// Optional: separate QUIC listen address
|
||||
quicListenAddr: '0.0.0.0:4433',
|
||||
// Optional: QUIC idle timeout
|
||||
quicIdleTimeoutSecs: 30,
|
||||
// Optional: default rate limit for all new clients
|
||||
defaultRateLimitBytesPerSec: 10_000_000, // 10 MB/s
|
||||
defaultBurstBytes: 20_000_000, // 20 MB burst
|
||||
@@ -247,11 +295,66 @@ Both `VpnClient` and `VpnServer` extend `EventEmitter`:
|
||||
```typescript
|
||||
client.on('exit', ({ code, signal }) => { /* daemon exited */ });
|
||||
client.on('reconnected', () => { /* socket reconnected */ });
|
||||
client.on('status', (status) => { /* IVpnStatus update */ });
|
||||
client.on('error', (error) => { /* error from daemon */ });
|
||||
|
||||
server.on('client-connected', (info) => { /* IVpnClientInfo */ });
|
||||
server.on('client-disconnected', ({ clientId, reason }) => { /* ... */ });
|
||||
server.on('started', () => { /* server listener started */ });
|
||||
server.on('stopped', () => { /* server listener stopped */ });
|
||||
```
|
||||
|
||||
## 🌐 Transport Modes
|
||||
|
||||
smartvpn supports two transport protocols through a unified transport abstraction layer. Both use the same encryption, framing, and QoS pipeline — the transport is swappable without changing any application logic.
|
||||
|
||||
### WebSocket (default)
|
||||
|
||||
- Works through Cloudflare, reverse proxies, and HTTP load balancers
|
||||
- Reliable delivery only (no datagram support)
|
||||
- URL format: `wss://host/path` or `ws://host:port/path`
|
||||
|
||||
### QUIC
|
||||
|
||||
- Lower latency, built-in multiplexing, 0-RTT connection establishment
|
||||
- Supports **unreliable datagrams** for IP packets (with automatic fallback to reliable if oversized)
|
||||
- Certificate hash pinning — no CA chain needed, WireGuard-style trust
|
||||
- URL format: `host:port`
|
||||
- ALPN protocol: `smartvpn`
|
||||
|
||||
### Auto-Transport (Recommended)
|
||||
|
||||
The default `transport: 'auto'` mode gives you the best of both worlds:
|
||||
|
||||
1. Extract `host:port` from the WebSocket URL
|
||||
2. Attempt QUIC connection (3-second timeout)
|
||||
3. If QUIC fails or times out → fall back to WebSocket
|
||||
4. Completely transparent to the application
|
||||
|
||||
```typescript
|
||||
await client.connect({
|
||||
serverUrl: 'wss://vpn.example.com/tunnel',
|
||||
serverPublicKey: '...',
|
||||
transport: 'auto', // default — QUIC first, WS fallback
|
||||
});
|
||||
```
|
||||
|
||||
### Server Dual-Mode
|
||||
|
||||
The server can listen on both transports simultaneously:
|
||||
|
||||
```typescript
|
||||
await server.start({
|
||||
listenAddr: '0.0.0.0:443', // WebSocket listener
|
||||
quicListenAddr: '0.0.0.0:4433', // QUIC listener (optional, defaults to listenAddr)
|
||||
transportMode: 'both', // 'websocket' | 'quic' | 'both' (default)
|
||||
quicIdleTimeoutSecs: 30, // QUIC connection idle timeout
|
||||
// ... other config
|
||||
});
|
||||
```
|
||||
|
||||
When using `'both'` mode, the server logs the QUIC certificate hash on startup — share this with clients for cert pinning.
|
||||
|
||||
## 📊 QoS System
|
||||
|
||||
The Rust daemon includes a full QoS stack that operates on decrypted IP packets:
|
||||
@@ -337,9 +440,26 @@ Post-handshake, all IP packets are encrypted with **XChaCha20-Poly1305**:
|
||||
- 16-byte authentication tags
|
||||
- Wire format: `[nonce:24B][ciphertext:var][tag:16B]`
|
||||
|
||||
### QUIC Certificate Pinning
|
||||
|
||||
When using QUIC transport, the server generates a self-signed TLS certificate (or uses a configured PEM). Instead of relying on a CA chain, clients pin the server's certificate by its **SHA-256 hash** (base64-encoded) — a WireGuard-inspired trust model:
|
||||
|
||||
```typescript
|
||||
// Server logs the cert hash on startup:
|
||||
// "QUIC cert hash: <BASE64_HASH>"
|
||||
|
||||
// Client pins it:
|
||||
await client.connect({
|
||||
serverUrl: 'vpn.example.com:443',
|
||||
transport: 'quic',
|
||||
serverCertHash: '<BASE64_HASH>',
|
||||
serverPublicKey: '...',
|
||||
});
|
||||
```
|
||||
|
||||
## 📦 Binary Protocol
|
||||
|
||||
Inside the WebSocket tunnel, packets use a simple binary framing:
|
||||
Inside the tunnel (both WebSocket and QUIC reliable channels), packets use a simple binary framing:
|
||||
|
||||
```
|
||||
┌──────────┬──────────┬────────────────────┐
|
||||
@@ -359,6 +479,8 @@ Inside the WebSocket tunnel, packets use a simple binary framing:
|
||||
| `SessionResumeErr` | `0x32` | Resume rejected |
|
||||
| `Disconnect` | `0x3F` | Graceful disconnect |
|
||||
|
||||
When QUIC datagrams are available, IP packets can optionally be sent via the unreliable datagram channel for lower latency. Packets that exceed the max datagram size automatically fall back to the reliable stream.
|
||||
|
||||
## 🛠️ Rust Daemon CLI
|
||||
|
||||
```bash
|
||||
@@ -385,12 +507,12 @@ pnpm build
|
||||
# Build Rust only (debug)
|
||||
cd rust && cargo build
|
||||
|
||||
# Run all tests (71 Rust + 32 TypeScript)
|
||||
# Run all tests (77 Rust + 59 TypeScript)
|
||||
cd rust && cargo test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## TypeScript Interfaces
|
||||
## 📘 TypeScript Interfaces
|
||||
|
||||
<details>
|
||||
<summary>Click to expand full type definitions</summary>
|
||||
@@ -410,8 +532,10 @@ type TVpnTransportOptions =
|
||||
|
||||
// Client config
|
||||
interface IVpnClientConfig {
|
||||
serverUrl: string;
|
||||
serverPublicKey: string;
|
||||
serverUrl: string; // WS: 'wss://host/path' | QUIC: 'host:port'
|
||||
serverPublicKey: string; // Base64-encoded Noise static key
|
||||
transport?: 'auto' | 'websocket' | 'quic'; // Default: 'auto'
|
||||
serverCertHash?: string; // SHA-256 cert hash (base64) for QUIC pinning
|
||||
dns?: string[];
|
||||
mtu?: number;
|
||||
keepaliveIntervalSecs?: number;
|
||||
@@ -429,6 +553,9 @@ interface IVpnServerConfig {
|
||||
mtu?: number;
|
||||
keepaliveIntervalSecs?: number;
|
||||
enableNat?: boolean;
|
||||
transportMode?: 'websocket' | 'quic' | 'both'; // Default: 'both'
|
||||
quicListenAddr?: string; // Separate QUIC bind address
|
||||
quicIdleTimeoutSecs?: number; // QUIC idle timeout (default: 30)
|
||||
defaultRateLimitBytesPerSec?: number;
|
||||
defaultBurstBytes?: number;
|
||||
}
|
||||
@@ -537,7 +664,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user