4 Commits

6 changed files with 71 additions and 18 deletions

View File

@@ -1,5 +1,18 @@
# Changelog
## 2026-03-30 - 1.10.2 - fix(client)
wait for the connection task to shut down cleanly before disconnecting and increase test timeout
- store the spawned client connection task handle and await it during disconnect with a 5 second timeout so the disconnect frame can be sent before closing
- increase the test script timeout from 60 seconds to 90 seconds to reduce flaky test runs
## 2026-03-29 - 1.10.1 - fix(test, docs, scripts)
correct test command verbosity, shorten load test timings, and document forwarding modes
- Fixes the test script by removing the duplicated verbose flag in package.json.
- Reduces load test delays and burst sizes to keep keepalive and connection tests faster and more stable.
- Updates the README to describe forwardingMode options, userspace NAT support, and related configuration examples.
## 2026-03-29 - 1.10.0 - feat(rust-server, rust-client, ts-interfaces)
add configurable packet forwarding with TUN and userspace NAT modes

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartvpn",
"version": "1.10.0",
"version": "1.10.2",
"private": false,
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
"type": "module",
@@ -12,7 +12,7 @@
"scripts": {
"build": "(tsbuild tsfolders) && (tsrust)",
"test:before": "(tsrust)",
"test": "tstest test/ --verbose--verbose --logfile --timeout 60",
"test": "tstest test/ --verbose --logfile --timeout 90",
"buildDocs": "tsdoc"
},
"repository": {

View File

@@ -9,6 +9,7 @@ A high-performance VPN solution with a **TypeScript control plane** and a **Rust
📊 **Adaptive QoS**: per-client rate limiting, priority queues, connection quality tracking
🔄 **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
🌐 **Flexible forwarding**: TUN device (kernel), userspace NAT (no root), or testing mode
## Issue Reporting and Security
@@ -54,6 +55,7 @@ await server.start({
publicKey: '<server-noise-public-key-base64>',
subnet: '10.8.0.0/24',
transportMode: 'both', // WebSocket + QUIC simultaneously
forwardingMode: 'tun', // 'tun' (kernel), 'socket' (userspace NAT), or 'testing'
enableNat: true,
dns: ['1.1.1.1', '8.8.8.8'],
});
@@ -152,6 +154,33 @@ await server.start({
- `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
### 📦 Packet Forwarding Modes
SmartVPN supports three forwarding modes, configurable per-server and per-client:
| 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 |
| **Testing** | `'testing'` | Monitoring only — packets are counted but not forwarded | ❌ No |
```typescript
// Server with userspace NAT (no root required)
await server.start({
// ...
forwardingMode: 'socket',
enableNat: true,
});
// 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.
### 📊 Telemetry & QoS
- **Connection quality**: Smoothed RTT, jitter, min/max RTT, loss ratio, link health (`healthy` / `degraded` / `critical`)
@@ -244,8 +273,8 @@ const unit = VpnInstaller.generateServiceUnit({
| Interface | Purpose |
|-----------|---------|
| `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, clients, proxy protocol) |
| `IVpnClientConfig` | Client configuration (server URL, keys, transport, WG options) |
| `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, forwarding mode, clients, proxy protocol) |
| `IVpnClientConfig` | Client configuration (server URL, keys, transport, forwarding mode, WG options) |
| `IClientEntry` | Server-side client definition (ID, keys, security, priority, tags, expiry) |
| `IClientSecurity` | Per-client ACLs and rate limits (SmartProxy-aligned naming) |
| `IClientRateLimit` | Rate limiting config (bytesPerSec, burstBytes) |
@@ -341,7 +370,7 @@ pnpm install
# Build (TypeScript + Rust cross-compile)
pnpm build
# Run all tests (79 TS + 129 Rust = 208 tests)
# Run all tests (79 TS + 132 Rust = 211 tests)
pnpm test
# Run Rust tests directly
@@ -380,6 +409,7 @@ smartvpn/
│ ├── codec.rs # Binary frame protocol
│ ├── keepalive.rs # Adaptive keepalives
│ ├── ratelimit.rs # Token bucket
│ ├── userspace_nat.rs # Userspace TCP/UDP NAT proxy
│ └── ... # tunnel, network, telemetry, qos, mtu, reconnect
├── test/ # 9 test files (79 tests)
├── dist_ts/ # Compiled TypeScript
@@ -388,7 +418,7 @@ smartvpn/
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license.md) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) 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.

View File

@@ -81,6 +81,7 @@ pub struct VpnClient {
connected_since: Arc<RwLock<Option<std::time::Instant>>>,
quality_rx: Option<watch::Receiver<ConnectionQuality>>,
link_health: Arc<RwLock<LinkHealth>>,
connection_handle: Option<tokio::task::JoinHandle<()>>,
}
impl VpnClient {
@@ -93,6 +94,7 @@ impl VpnClient {
connected_since: Arc::new(RwLock::new(None)),
quality_rx: None,
link_health: Arc::new(RwLock::new(LinkHealth::Degraded)),
connection_handle: None,
}
}
@@ -280,7 +282,7 @@ impl VpnClient {
// Spawn packet forwarding loop
let assigned_ip_clone = assigned_ip.clone();
tokio::spawn(client_loop(
let join_handle = tokio::spawn(client_loop(
sink,
stream,
noise_transport,
@@ -294,6 +296,7 @@ impl VpnClient {
tun_writer,
tun_subnet,
));
self.connection_handle = Some(join_handle);
Ok(assigned_ip_clone)
}
@@ -303,6 +306,13 @@ impl VpnClient {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(()).await;
}
// Wait for the connection task to send the Disconnect frame and close
if let Some(handle) = self.connection_handle.take() {
let _ = tokio::time::timeout(
std::time::Duration::from_secs(5),
handle,
).await;
}
*self.assigned_ip.write().await = None;
*self.connected_since.write().await = None;
*self.state.write().await = ClientState::Disconnected;

View File

@@ -211,8 +211,8 @@ tap.test('throttled connection: handshake succeeds through throttle', async () =
});
tap.test('sustained keepalive under throttle', async () => {
// Wait for at least 2 keepalive cycles (3s interval)
await delay(8000);
// Wait for at least 1 keepalive cycle (3s interval)
await delay(4000);
const client = allClients[0];
const stats = await client.getStatistics();
@@ -262,14 +262,14 @@ tap.test('rate limiting combined with network throttle', async () => {
await server.removeClientRateLimit(targetId);
});
tap.test('burst waves: 3 waves of 3 clients', async () => {
tap.test('burst waves: 2 waves of 2 clients', async () => {
const initialCount = (await server.listClients()).length;
for (let wave = 0; wave < 3; wave++) {
for (let wave = 0; wave < 2; wave++) {
const waveClients: VpnClient[] = [];
// Connect 3 clients
for (let i = 0; i < 3; i++) {
// Connect 2 clients
for (let i = 0; i < 2; i++) {
const c = await createConnectedClient(proxyPort);
waveClients.push(c);
}
@@ -277,7 +277,7 @@ tap.test('burst waves: 3 waves of 3 clients', async () => {
// Verify all connected
await waitFor(async () => {
const all = await server.listClients();
return all.length === initialCount + 3;
return all.length === initialCount + 2;
});
// Disconnect all wave clients
@@ -296,7 +296,7 @@ tap.test('burst waves: 3 waves of 3 clients', async () => {
// Verify total connections accumulated
const stats = await server.getStatistics();
expect(stats.totalConnections).toBeGreaterThanOrEqual(9 + initialCount);
expect(stats.totalConnections).toBeGreaterThanOrEqual(4 + initialCount);
// Original clients still connected
const remaining = await server.listClients();
@@ -315,7 +315,7 @@ tap.test('aggressive throttle: 10 KB/s', async () => {
expect(status.state).toEqual('connected');
// Wait for keepalive exchange (might take longer due to throttle)
await delay(10000);
await delay(4000);
const stats = await client.getStatistics();
expect(stats.keepalivesSent).toBeGreaterThanOrEqual(1);
@@ -332,7 +332,7 @@ tap.test('post-load health: direct connection still works', async () => {
const status = await directClient.getStatus();
expect(status.state).toEqual('connected');
await delay(5000);
await delay(3500);
const stats = await directClient.getStatistics();
expect(stats.keepalivesSent).toBeGreaterThanOrEqual(1);

View File

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