feat(vpn transport): add QUIC transport support with auto fallback to WebSocket

This commit is contained in:
2026-03-19 21:53:30 +00:00
parent e14c357ba0
commit e81dd377d8
16 changed files with 2952 additions and 1888 deletions

155
readme.md
View File

@@ -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 (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 |
@@ -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.