Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70e838c8ff | |||
| dbcfdb1fb6 | |||
| c97beed6e0 | |||
| c3cc237db5 |
13
changelog.md
13
changelog.md
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-03-29 - 1.10.0 - feat(rust-server, rust-client, ts-interfaces)
|
||||||
add configurable packet forwarding with TUN and userspace NAT modes
|
add configurable packet forwarding with TUN and userspace NAT modes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartvpn",
|
"name": "@push.rocks/smartvpn",
|
||||||
"version": "1.10.0",
|
"version": "1.10.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
|
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "(tsbuild tsfolders) && (tsrust)",
|
"build": "(tsbuild tsfolders) && (tsrust)",
|
||||||
"test:before": "(tsrust)",
|
"test:before": "(tsrust)",
|
||||||
"test": "tstest test/ --verbose--verbose --logfile --timeout 60",
|
"test": "tstest test/ --verbose --logfile --timeout 90",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
38
readme.md
38
readme.md
@@ -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
|
📊 **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
|
🔄 **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
|
📡 **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
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ await server.start({
|
|||||||
publicKey: '<server-noise-public-key-base64>',
|
publicKey: '<server-noise-public-key-base64>',
|
||||||
subnet: '10.8.0.0/24',
|
subnet: '10.8.0.0/24',
|
||||||
transportMode: 'both', // WebSocket + QUIC simultaneously
|
transportMode: 'both', // WebSocket + QUIC simultaneously
|
||||||
|
forwardingMode: 'tun', // 'tun' (kernel), 'socket' (userspace NAT), or 'testing'
|
||||||
enableNat: true,
|
enableNat: true,
|
||||||
dns: ['1.1.1.1', '8.8.8.8'],
|
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
|
- `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
|
- **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
|
### 📊 Telemetry & QoS
|
||||||
|
|
||||||
- **Connection quality**: Smoothed RTT, jitter, min/max RTT, loss ratio, link health (`healthy` / `degraded` / `critical`)
|
- **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 |
|
| Interface | Purpose |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, clients, proxy protocol) |
|
| `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, forwarding mode, clients, proxy protocol) |
|
||||||
| `IVpnClientConfig` | Client configuration (server URL, keys, transport, WG options) |
|
| `IVpnClientConfig` | Client configuration (server URL, keys, transport, forwarding mode, WG options) |
|
||||||
| `IClientEntry` | Server-side client definition (ID, keys, security, priority, tags, expiry) |
|
| `IClientEntry` | Server-side client definition (ID, keys, security, priority, tags, expiry) |
|
||||||
| `IClientSecurity` | Per-client ACLs and rate limits (SmartProxy-aligned naming) |
|
| `IClientSecurity` | Per-client ACLs and rate limits (SmartProxy-aligned naming) |
|
||||||
| `IClientRateLimit` | Rate limiting config (bytesPerSec, burstBytes) |
|
| `IClientRateLimit` | Rate limiting config (bytesPerSec, burstBytes) |
|
||||||
@@ -341,7 +370,7 @@ pnpm install
|
|||||||
# Build (TypeScript + Rust cross-compile)
|
# Build (TypeScript + Rust cross-compile)
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
# Run all tests (79 TS + 129 Rust = 208 tests)
|
# Run all tests (79 TS + 132 Rust = 211 tests)
|
||||||
pnpm test
|
pnpm test
|
||||||
|
|
||||||
# Run Rust tests directly
|
# Run Rust tests directly
|
||||||
@@ -380,6 +409,7 @@ smartvpn/
|
|||||||
│ ├── codec.rs # Binary frame protocol
|
│ ├── codec.rs # Binary frame protocol
|
||||||
│ ├── keepalive.rs # Adaptive keepalives
|
│ ├── keepalive.rs # Adaptive keepalives
|
||||||
│ ├── ratelimit.rs # Token bucket
|
│ ├── ratelimit.rs # Token bucket
|
||||||
|
│ ├── userspace_nat.rs # Userspace TCP/UDP NAT proxy
|
||||||
│ └── ... # tunnel, network, telemetry, qos, mtu, reconnect
|
│ └── ... # tunnel, network, telemetry, qos, mtu, reconnect
|
||||||
├── test/ # 9 test files (79 tests)
|
├── test/ # 9 test files (79 tests)
|
||||||
├── dist_ts/ # Compiled TypeScript
|
├── dist_ts/ # Compiled TypeScript
|
||||||
@@ -388,7 +418,7 @@ smartvpn/
|
|||||||
|
|
||||||
## License and Legal Information
|
## 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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ pub struct VpnClient {
|
|||||||
connected_since: Arc<RwLock<Option<std::time::Instant>>>,
|
connected_since: Arc<RwLock<Option<std::time::Instant>>>,
|
||||||
quality_rx: Option<watch::Receiver<ConnectionQuality>>,
|
quality_rx: Option<watch::Receiver<ConnectionQuality>>,
|
||||||
link_health: Arc<RwLock<LinkHealth>>,
|
link_health: Arc<RwLock<LinkHealth>>,
|
||||||
|
connection_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VpnClient {
|
impl VpnClient {
|
||||||
@@ -93,6 +94,7 @@ impl VpnClient {
|
|||||||
connected_since: Arc::new(RwLock::new(None)),
|
connected_since: Arc::new(RwLock::new(None)),
|
||||||
quality_rx: None,
|
quality_rx: None,
|
||||||
link_health: Arc::new(RwLock::new(LinkHealth::Degraded)),
|
link_health: Arc::new(RwLock::new(LinkHealth::Degraded)),
|
||||||
|
connection_handle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +282,7 @@ impl VpnClient {
|
|||||||
|
|
||||||
// Spawn packet forwarding loop
|
// Spawn packet forwarding loop
|
||||||
let assigned_ip_clone = assigned_ip.clone();
|
let assigned_ip_clone = assigned_ip.clone();
|
||||||
tokio::spawn(client_loop(
|
let join_handle = tokio::spawn(client_loop(
|
||||||
sink,
|
sink,
|
||||||
stream,
|
stream,
|
||||||
noise_transport,
|
noise_transport,
|
||||||
@@ -294,6 +296,7 @@ impl VpnClient {
|
|||||||
tun_writer,
|
tun_writer,
|
||||||
tun_subnet,
|
tun_subnet,
|
||||||
));
|
));
|
||||||
|
self.connection_handle = Some(join_handle);
|
||||||
|
|
||||||
Ok(assigned_ip_clone)
|
Ok(assigned_ip_clone)
|
||||||
}
|
}
|
||||||
@@ -303,6 +306,13 @@ impl VpnClient {
|
|||||||
if let Some(tx) = self.shutdown_tx.take() {
|
if let Some(tx) = self.shutdown_tx.take() {
|
||||||
let _ = tx.send(()).await;
|
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.assigned_ip.write().await = None;
|
||||||
*self.connected_since.write().await = None;
|
*self.connected_since.write().await = None;
|
||||||
*self.state.write().await = ClientState::Disconnected;
|
*self.state.write().await = ClientState::Disconnected;
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ tap.test('throttled connection: handshake succeeds through throttle', async () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('sustained keepalive under throttle', async () => {
|
tap.test('sustained keepalive under throttle', async () => {
|
||||||
// Wait for at least 2 keepalive cycles (3s interval)
|
// Wait for at least 1 keepalive cycle (3s interval)
|
||||||
await delay(8000);
|
await delay(4000);
|
||||||
|
|
||||||
const client = allClients[0];
|
const client = allClients[0];
|
||||||
const stats = await client.getStatistics();
|
const stats = await client.getStatistics();
|
||||||
@@ -262,14 +262,14 @@ tap.test('rate limiting combined with network throttle', async () => {
|
|||||||
await server.removeClientRateLimit(targetId);
|
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;
|
const initialCount = (await server.listClients()).length;
|
||||||
|
|
||||||
for (let wave = 0; wave < 3; wave++) {
|
for (let wave = 0; wave < 2; wave++) {
|
||||||
const waveClients: VpnClient[] = [];
|
const waveClients: VpnClient[] = [];
|
||||||
|
|
||||||
// Connect 3 clients
|
// Connect 2 clients
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
const c = await createConnectedClient(proxyPort);
|
const c = await createConnectedClient(proxyPort);
|
||||||
waveClients.push(c);
|
waveClients.push(c);
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ tap.test('burst waves: 3 waves of 3 clients', async () => {
|
|||||||
// Verify all connected
|
// Verify all connected
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const all = await server.listClients();
|
const all = await server.listClients();
|
||||||
return all.length === initialCount + 3;
|
return all.length === initialCount + 2;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disconnect all wave clients
|
// Disconnect all wave clients
|
||||||
@@ -296,7 +296,7 @@ tap.test('burst waves: 3 waves of 3 clients', async () => {
|
|||||||
|
|
||||||
// Verify total connections accumulated
|
// Verify total connections accumulated
|
||||||
const stats = await server.getStatistics();
|
const stats = await server.getStatistics();
|
||||||
expect(stats.totalConnections).toBeGreaterThanOrEqual(9 + initialCount);
|
expect(stats.totalConnections).toBeGreaterThanOrEqual(4 + initialCount);
|
||||||
|
|
||||||
// Original clients still connected
|
// Original clients still connected
|
||||||
const remaining = await server.listClients();
|
const remaining = await server.listClients();
|
||||||
@@ -315,7 +315,7 @@ tap.test('aggressive throttle: 10 KB/s', async () => {
|
|||||||
expect(status.state).toEqual('connected');
|
expect(status.state).toEqual('connected');
|
||||||
|
|
||||||
// Wait for keepalive exchange (might take longer due to throttle)
|
// Wait for keepalive exchange (might take longer due to throttle)
|
||||||
await delay(10000);
|
await delay(4000);
|
||||||
|
|
||||||
const stats = await client.getStatistics();
|
const stats = await client.getStatistics();
|
||||||
expect(stats.keepalivesSent).toBeGreaterThanOrEqual(1);
|
expect(stats.keepalivesSent).toBeGreaterThanOrEqual(1);
|
||||||
@@ -332,7 +332,7 @@ tap.test('post-load health: direct connection still works', async () => {
|
|||||||
const status = await directClient.getStatus();
|
const status = await directClient.getStatus();
|
||||||
expect(status.state).toEqual('connected');
|
expect(status.state).toEqual('connected');
|
||||||
|
|
||||||
await delay(5000);
|
await delay(3500);
|
||||||
|
|
||||||
const stats = await directClient.getStatistics();
|
const stats = await directClient.getStatistics();
|
||||||
expect(stats.keepalivesSent).toBeGreaterThanOrEqual(1);
|
expect(stats.keepalivesSent).toBeGreaterThanOrEqual(1);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartvpn',
|
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'
|
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user