@push.rocks/smartvpn 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_rust/smartvpn_daemon_linux_amd64 +0 -0
- package/dist_rust/smartvpn_daemon_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartvpn.classes.vpnconfig.js +22 -1
- package/dist_ts/smartvpn.classes.vpnserver.d.ts +42 -1
- package/dist_ts/smartvpn.classes.vpnserver.js +65 -1
- package/dist_ts/smartvpn.interfaces.d.ts +152 -1
- package/package.json +1 -1
- package/readme.md +269 -768
- package/readme.plan.md +253 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartvpn.classes.vpnconfig.ts +21 -0
- package/ts/smartvpn.classes.vpnserver.ts +77 -0
- package/ts/smartvpn.interfaces.ts +109 -1
package/readme.plan.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# PROXY Protocol v2 Support for SmartVPN WebSocket Transport
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
SmartVPN's WebSocket transport is designed to sit behind reverse proxies (Cloudflare, HAProxy, SmartProxy). The recently added ACL engine has `ipAllowList`/`ipBlockList` per client, but without PROXY protocol support the server only sees the proxy's IP — not the real client's. This makes source-IP ACLs useless behind a proxy.
|
|
6
|
+
|
|
7
|
+
PROXY protocol v2 solves this by letting the proxy prepend a binary header with the real client IP/port before the WebSocket upgrade.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Design
|
|
12
|
+
|
|
13
|
+
### Two-Phase ACL with Real Client IP
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
TCP accept → Read PP v2 header → Extract real IP
|
|
17
|
+
│
|
|
18
|
+
├─ Phase 1 (pre-handshake): Check server-level connectionIpBlockList → reject early
|
|
19
|
+
│
|
|
20
|
+
├─ WebSocket upgrade → Noise IK handshake → Client identity known
|
|
21
|
+
│
|
|
22
|
+
└─ Phase 2 (post-handshake): Check per-client ipAllowList/ipBlockList → reject if denied
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **Phase 1**: Server-wide block list (`connectionIpBlockList` on `IVpnServerConfig`). Rejects before any crypto work. Protects server resources.
|
|
26
|
+
- **Phase 2**: Per-client ACL from `IClientSecurity.ipAllowList`/`ipBlockList`. Applied after the Noise IK handshake identifies the client.
|
|
27
|
+
|
|
28
|
+
### No New Dependencies
|
|
29
|
+
|
|
30
|
+
PROXY protocol v2 is a fixed-format binary header (16-byte signature + variable address block). Manual parsing (~80 lines) follows the same pattern as `codec.rs`. No crate needed.
|
|
31
|
+
|
|
32
|
+
### Scope: WebSocket Only
|
|
33
|
+
|
|
34
|
+
- **WebSocket**: Needs PP v2 (sits behind reverse proxies)
|
|
35
|
+
- **QUIC**: Direct UDP, just use `conn.remote_address()`
|
|
36
|
+
- **WireGuard**: Direct UDP, uses boringtun peer tracking
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Implementation
|
|
41
|
+
|
|
42
|
+
### Phase 1: New Rust module `proxy_protocol.rs`
|
|
43
|
+
|
|
44
|
+
**New file: `rust/src/proxy_protocol.rs`**
|
|
45
|
+
|
|
46
|
+
PP v2 binary format:
|
|
47
|
+
```
|
|
48
|
+
Bytes 0-11: Signature \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A
|
|
49
|
+
Byte 12: Version (high nibble = 0x2) | Command (low nibble: 0x0=LOCAL, 0x1=PROXY)
|
|
50
|
+
Byte 13: Address family | Protocol (0x11 = IPv4/TCP, 0x21 = IPv6/TCP)
|
|
51
|
+
Bytes 14-15: Address data length (big-endian u16)
|
|
52
|
+
Bytes 16+: IPv4: 4 src_ip + 4 dst_ip + 2 src_port + 2 dst_port (12 bytes)
|
|
53
|
+
IPv6: 16 src_ip + 16 dst_ip + 2 src_port + 2 dst_port (36 bytes)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```rust
|
|
57
|
+
pub struct ProxyHeader {
|
|
58
|
+
pub src_addr: SocketAddr,
|
|
59
|
+
pub dst_addr: SocketAddr,
|
|
60
|
+
pub is_local: bool, // LOCAL command = health check probe
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Read and parse a PROXY protocol v2 header from a TCP stream.
|
|
64
|
+
/// Reads exactly the header bytes — the stream is clean for WS upgrade after.
|
|
65
|
+
pub async fn read_proxy_header(stream: &mut TcpStream) -> Result<ProxyHeader>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- 5-second timeout on header read (constant `PROXY_HEADER_TIMEOUT`)
|
|
69
|
+
- Validates 12-byte signature, version nibble, command type
|
|
70
|
+
- Parses IPv4 and IPv6 address blocks
|
|
71
|
+
- LOCAL command returns `is_local: true` (caller closes connection gracefully)
|
|
72
|
+
- Unit tests: valid IPv4/IPv6 headers, LOCAL command, invalid signature, truncated data
|
|
73
|
+
|
|
74
|
+
**Modify: `rust/src/lib.rs`** — add `pub mod proxy_protocol;`
|
|
75
|
+
|
|
76
|
+
### Phase 2: Server config + client info fields
|
|
77
|
+
|
|
78
|
+
**File: `rust/src/server.rs` — `ServerConfig`**
|
|
79
|
+
|
|
80
|
+
Add:
|
|
81
|
+
```rust
|
|
82
|
+
/// Enable PROXY protocol v2 parsing on WebSocket connections.
|
|
83
|
+
/// SECURITY: Must be false when accepting direct client connections.
|
|
84
|
+
pub proxy_protocol: Option<bool>,
|
|
85
|
+
/// Server-level IP block list — applied at TCP accept time, before Noise handshake.
|
|
86
|
+
pub connection_ip_block_list: Option<Vec<String>>,
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**File: `rust/src/server.rs` — `ClientInfo`**
|
|
90
|
+
|
|
91
|
+
Add:
|
|
92
|
+
```rust
|
|
93
|
+
/// Real client IP:port (from PROXY protocol header or direct TCP connection).
|
|
94
|
+
pub remote_addr: Option<String>,
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Phase 3: ACL helper
|
|
98
|
+
|
|
99
|
+
**File: `rust/src/acl.rs`**
|
|
100
|
+
|
|
101
|
+
Add a public function for the server-level pre-handshake check:
|
|
102
|
+
```rust
|
|
103
|
+
/// Check whether a connection source IP is in a block list.
|
|
104
|
+
pub fn is_connection_blocked(ip: Ipv4Addr, block_list: &[String]) -> bool {
|
|
105
|
+
ip_matches_any(ip, block_list)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
(Keeps `ip_matches_any` private; exposes only the specific check needed.)
|
|
110
|
+
|
|
111
|
+
### Phase 4: WebSocket listener integration
|
|
112
|
+
|
|
113
|
+
**File: `rust/src/server.rs` — `run_ws_listener()`**
|
|
114
|
+
|
|
115
|
+
Between `listener.accept()` and `transport::accept_connection()`:
|
|
116
|
+
|
|
117
|
+
```rust
|
|
118
|
+
// Determine real client address
|
|
119
|
+
let remote_addr = if state.config.proxy_protocol.unwrap_or(false) {
|
|
120
|
+
match proxy_protocol::read_proxy_header(&mut tcp_stream).await {
|
|
121
|
+
Ok(header) if header.is_local => {
|
|
122
|
+
// Health check probe — close gracefully
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
Ok(header) => {
|
|
126
|
+
info!("PP v2: real client {} -> {}", header.src_addr, header.dst_addr);
|
|
127
|
+
Some(header.src_addr)
|
|
128
|
+
}
|
|
129
|
+
Err(e) => {
|
|
130
|
+
warn!("PP v2 parse failed from {}: {}", tcp_addr, e);
|
|
131
|
+
return; // Drop connection
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
Some(tcp_addr) // Direct connection — use TCP SocketAddr
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Pre-handshake server-level block list check
|
|
139
|
+
if let (Some(ref block_list), Some(ref addr)) = (&state.config.connection_ip_block_list, &remote_addr) {
|
|
140
|
+
if let std::net::IpAddr::V4(v4) = addr.ip() {
|
|
141
|
+
if acl::is_connection_blocked(v4, block_list) {
|
|
142
|
+
warn!("Connection blocked by server IP block list: {}", addr);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Then proceed with WS upgrade + handle_client_connection as before
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Key correctness note: `read_proxy_header` reads *exactly* the PP header bytes via `read_exact`. The `TcpStream` is then in a clean state for the WS HTTP upgrade. No buffered wrapper needed.
|
|
152
|
+
|
|
153
|
+
### Phase 5: Update `handle_client_connection` signature
|
|
154
|
+
|
|
155
|
+
**File: `rust/src/server.rs`**
|
|
156
|
+
|
|
157
|
+
Change signature:
|
|
158
|
+
```rust
|
|
159
|
+
async fn handle_client_connection(
|
|
160
|
+
state: Arc<ServerState>,
|
|
161
|
+
mut sink: Box<dyn TransportSink>,
|
|
162
|
+
mut stream: Box<dyn TransportStream>,
|
|
163
|
+
remote_addr: Option<std::net::SocketAddr>, // NEW
|
|
164
|
+
) -> Result<()>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
After Noise IK handshake + registry lookup (where `client_security` is available), add connection-level per-client ACL:
|
|
168
|
+
|
|
169
|
+
```rust
|
|
170
|
+
if let (Some(ref sec), Some(addr)) = (&client_security, &remote_addr) {
|
|
171
|
+
if let std::net::IpAddr::V4(v4) = addr.ip() {
|
|
172
|
+
if acl::is_connection_blocked(v4, sec.ip_block_list.as_deref().unwrap_or(&[])) {
|
|
173
|
+
anyhow::bail!("Client {} connection denied: source IP {} blocked", registered_client_id, addr);
|
|
174
|
+
}
|
|
175
|
+
if let Some(ref allow) = sec.ip_allow_list {
|
|
176
|
+
if !allow.is_empty() && !acl::is_ip_allowed(v4, allow) {
|
|
177
|
+
anyhow::bail!("Client {} connection denied: source IP {} not in allow list", registered_client_id, addr);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Populate `remote_addr` when building `ClientInfo`:
|
|
185
|
+
```rust
|
|
186
|
+
remote_addr: remote_addr.map(|a| a.to_string()),
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Phase 6: QUIC listener — pass remote addr through
|
|
190
|
+
|
|
191
|
+
**File: `rust/src/server.rs` — `run_quic_listener()`**
|
|
192
|
+
|
|
193
|
+
QUIC doesn't use PROXY protocol. Just pass `conn.remote_address()` through:
|
|
194
|
+
```rust
|
|
195
|
+
let remote = conn.remote_address();
|
|
196
|
+
// ...
|
|
197
|
+
handle_client_connection(state, Box::new(sink), Box::new(stream), Some(remote)).await
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Phase 7: TypeScript interface updates
|
|
201
|
+
|
|
202
|
+
**File: `ts/smartvpn.interfaces.ts`**
|
|
203
|
+
|
|
204
|
+
Add to `IVpnServerConfig`:
|
|
205
|
+
```typescript
|
|
206
|
+
/** Enable PROXY protocol v2 on incoming WebSocket connections.
|
|
207
|
+
* Required when behind a reverse proxy that sends PP v2 headers. */
|
|
208
|
+
proxyProtocol?: boolean;
|
|
209
|
+
/** Server-level IP block list — applied at TCP accept time, before Noise handshake. */
|
|
210
|
+
connectionIpBlockList?: string[];
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Add to `IVpnClientInfo`:
|
|
214
|
+
```typescript
|
|
215
|
+
/** Real client IP:port (from PROXY protocol or direct TCP). */
|
|
216
|
+
remoteAddr?: string;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Phase 8: Tests
|
|
220
|
+
|
|
221
|
+
**Rust unit tests in `proxy_protocol.rs`:**
|
|
222
|
+
- `parse_valid_ipv4_header` — construct a valid PP v2 header with known IPs, verify parsed correctly
|
|
223
|
+
- `parse_valid_ipv6_header` — same for IPv6
|
|
224
|
+
- `parse_local_command` — health check probe returns `is_local: true`
|
|
225
|
+
- `reject_invalid_signature` — random bytes rejected
|
|
226
|
+
- `reject_truncated_header` — short reads fail gracefully
|
|
227
|
+
- `reject_v1_header` — PROXY v1 text format rejected (we only support v2)
|
|
228
|
+
|
|
229
|
+
**Rust unit tests in `acl.rs`:**
|
|
230
|
+
- `is_connection_blocked` with various IP patterns
|
|
231
|
+
|
|
232
|
+
**TypeScript tests:**
|
|
233
|
+
- Config validation accepts `proxyProtocol: true` + `connectionIpBlockList`
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Key Files to Modify
|
|
238
|
+
|
|
239
|
+
| File | Changes |
|
|
240
|
+
|------|---------|
|
|
241
|
+
| `rust/src/proxy_protocol.rs` | **NEW** — PP v2 parser + tests |
|
|
242
|
+
| `rust/src/lib.rs` | Add `pub mod proxy_protocol;` |
|
|
243
|
+
| `rust/src/server.rs` | `ServerConfig` + `ClientInfo` fields, `run_ws_listener` PP integration, `handle_client_connection` signature + connection ACL, `run_quic_listener` pass-through |
|
|
244
|
+
| `rust/src/acl.rs` | Add `is_connection_blocked` public function |
|
|
245
|
+
| `ts/smartvpn.interfaces.ts` | `proxyProtocol`, `connectionIpBlockList`, `remoteAddr` |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Verification
|
|
250
|
+
|
|
251
|
+
1. `cargo test` — all existing 121 tests + new PP parser tests pass
|
|
252
|
+
2. `pnpm test` — all 79 TS tests pass (no PP in test setup, just config validation)
|
|
253
|
+
3. Manual: `socat` or test harness to send a PP v2 header before WS upgrade, verify server logs real IP
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -51,6 +51,15 @@ export class VpnConfig {
|
|
|
51
51
|
if (!config.serverPublicKey) {
|
|
52
52
|
throw new Error('VpnConfig: serverPublicKey is required');
|
|
53
53
|
}
|
|
54
|
+
// Noise IK requires client keypair
|
|
55
|
+
if (!config.clientPrivateKey) {
|
|
56
|
+
throw new Error('VpnConfig: clientPrivateKey is required for Noise IK authentication');
|
|
57
|
+
}
|
|
58
|
+
VpnConfig.validateBase64Key(config.clientPrivateKey, 'clientPrivateKey');
|
|
59
|
+
if (!config.clientPublicKey) {
|
|
60
|
+
throw new Error('VpnConfig: clientPublicKey is required for Noise IK authentication');
|
|
61
|
+
}
|
|
62
|
+
VpnConfig.validateBase64Key(config.clientPublicKey, 'clientPublicKey');
|
|
54
63
|
}
|
|
55
64
|
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
|
56
65
|
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
|
@@ -116,6 +125,18 @@ export class VpnConfig {
|
|
|
116
125
|
if (!VpnConfig.isValidSubnet(config.subnet)) {
|
|
117
126
|
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
|
|
118
127
|
}
|
|
128
|
+
// Validate client entries if provided
|
|
129
|
+
if (config.clients) {
|
|
130
|
+
for (const client of config.clients) {
|
|
131
|
+
if (!client.clientId) {
|
|
132
|
+
throw new Error('VpnConfig: client entry must have a clientId');
|
|
133
|
+
}
|
|
134
|
+
if (!client.publicKey) {
|
|
135
|
+
throw new Error(`VpnConfig: client '${client.clientId}' must have a publicKey`);
|
|
136
|
+
}
|
|
137
|
+
VpnConfig.validateBase64Key(client.publicKey, `client '${client.clientId}' publicKey`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
119
140
|
}
|
|
120
141
|
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
|
121
142
|
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
IVpnClientTelemetry,
|
|
11
11
|
IWgPeerConfig,
|
|
12
12
|
IWgPeerInfo,
|
|
13
|
+
IClientEntry,
|
|
14
|
+
IClientConfigBundle,
|
|
13
15
|
TVpnServerCommands,
|
|
14
16
|
} from './smartvpn.interfaces.js';
|
|
15
17
|
|
|
@@ -152,6 +154,81 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
152
154
|
return result.peers;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// ── Client Registry (Hub) Methods ─────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a new client. Generates keypairs, assigns IP, returns full config bundle.
|
|
161
|
+
* The secrets (private keys) are only returned at creation time.
|
|
162
|
+
*/
|
|
163
|
+
public async createClient(opts: Partial<IClientEntry>): Promise<IClientConfigBundle> {
|
|
164
|
+
return this.bridge.sendCommand('createClient', { client: opts });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Remove a registered client (also disconnects if connected).
|
|
169
|
+
*/
|
|
170
|
+
public async removeClient(clientId: string): Promise<void> {
|
|
171
|
+
await this.bridge.sendCommand('removeClient', { clientId });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a registered client by ID.
|
|
176
|
+
*/
|
|
177
|
+
public async getClient(clientId: string): Promise<IClientEntry> {
|
|
178
|
+
return this.bridge.sendCommand('getClient', { clientId });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* List all registered clients.
|
|
183
|
+
*/
|
|
184
|
+
public async listRegisteredClients(): Promise<IClientEntry[]> {
|
|
185
|
+
const result = await this.bridge.sendCommand('listRegisteredClients', {} as Record<string, never>);
|
|
186
|
+
return result.clients;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update a registered client's fields (ACLs, tags, description, etc.).
|
|
191
|
+
*/
|
|
192
|
+
public async updateClient(clientId: string, update: Partial<IClientEntry>): Promise<void> {
|
|
193
|
+
await this.bridge.sendCommand('updateClient', { clientId, update });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Enable a previously disabled client.
|
|
198
|
+
*/
|
|
199
|
+
public async enableClient(clientId: string): Promise<void> {
|
|
200
|
+
await this.bridge.sendCommand('enableClient', { clientId });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Disable a client (also disconnects if connected).
|
|
205
|
+
*/
|
|
206
|
+
public async disableClient(clientId: string): Promise<void> {
|
|
207
|
+
await this.bridge.sendCommand('disableClient', { clientId });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Rotate a client's keys. Returns a new config bundle with fresh keypairs.
|
|
212
|
+
*/
|
|
213
|
+
public async rotateClientKey(clientId: string): Promise<IClientConfigBundle> {
|
|
214
|
+
return this.bridge.sendCommand('rotateClientKey', { clientId });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Export a client config (without secrets) in the specified format.
|
|
219
|
+
*/
|
|
220
|
+
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
|
|
221
|
+
const result = await this.bridge.sendCommand('exportClientConfig', { clientId, format });
|
|
222
|
+
return result.config;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generate a standalone Noise IK keypair (not tied to a client).
|
|
227
|
+
*/
|
|
228
|
+
public async generateClientKeypair(): Promise<IVpnKeypair> {
|
|
229
|
+
return this.bridge.sendCommand('generateClientKeypair', {} as Record<string, never>);
|
|
230
|
+
}
|
|
231
|
+
|
|
155
232
|
/**
|
|
156
233
|
* Stop the daemon bridge.
|
|
157
234
|
*/
|
|
@@ -24,8 +24,12 @@ export type TVpnTransportOptions = IVpnTransportStdio | IVpnTransportSocket;
|
|
|
24
24
|
export interface IVpnClientConfig {
|
|
25
25
|
/** Server WebSocket URL, e.g. wss://vpn.example.com/tunnel */
|
|
26
26
|
serverUrl: string;
|
|
27
|
-
/** Server's static public key (base64) for Noise
|
|
27
|
+
/** Server's static public key (base64) for Noise IK handshake */
|
|
28
28
|
serverPublicKey: string;
|
|
29
|
+
/** Client's Noise IK private key (base64) — required for SmartVPN native transport */
|
|
30
|
+
clientPrivateKey: string;
|
|
31
|
+
/** Client's Noise IK public key (base64) — for reference/display */
|
|
32
|
+
clientPublicKey: string;
|
|
29
33
|
/** Optional DNS servers to use while connected */
|
|
30
34
|
dns?: string[];
|
|
31
35
|
/** Optional MTU for the TUN device */
|
|
@@ -96,6 +100,15 @@ export interface IVpnServerConfig {
|
|
|
96
100
|
wgListenPort?: number;
|
|
97
101
|
/** WireGuard: configured peers */
|
|
98
102
|
wgPeers?: IWgPeerConfig[];
|
|
103
|
+
/** Pre-registered clients for Noise IK authentication */
|
|
104
|
+
clients?: IClientEntry[];
|
|
105
|
+
/** Enable PROXY protocol v2 on incoming WebSocket connections.
|
|
106
|
+
* Required when behind a reverse proxy that sends PP v2 headers (HAProxy, SmartProxy).
|
|
107
|
+
* SECURITY: Must be false when accepting direct client connections. */
|
|
108
|
+
proxyProtocol?: boolean;
|
|
109
|
+
/** Server-level IP block list — applied at TCP accept, before Noise handshake.
|
|
110
|
+
* Supports exact IPs, CIDR, wildcards, ranges. */
|
|
111
|
+
connectionIpBlockList?: string[];
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
export interface IVpnServerOptions {
|
|
@@ -146,6 +159,12 @@ export interface IVpnClientInfo {
|
|
|
146
159
|
keepalivesReceived: number;
|
|
147
160
|
rateLimitBytesPerSec?: number;
|
|
148
161
|
burstBytes?: number;
|
|
162
|
+
/** Client's authenticated Noise IK public key (base64) */
|
|
163
|
+
authenticatedKey: string;
|
|
164
|
+
/** Registered client ID from the client registry */
|
|
165
|
+
registeredClientId: string;
|
|
166
|
+
/** Real client IP:port (from PROXY protocol or direct TCP connection) */
|
|
167
|
+
remoteAddr?: string;
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
export interface IVpnServerStatistics extends IVpnStatistics {
|
|
@@ -205,6 +224,84 @@ export interface IVpnClientTelemetry {
|
|
|
205
224
|
burstBytes?: number;
|
|
206
225
|
}
|
|
207
226
|
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Client Registry (Hub) types — aligned with SmartProxy IRouteSecurity pattern
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
/** Per-client rate limiting. */
|
|
232
|
+
export interface IClientRateLimit {
|
|
233
|
+
/** Max throughput in bytes/sec */
|
|
234
|
+
bytesPerSec: number;
|
|
235
|
+
/** Burst allowance in bytes */
|
|
236
|
+
burstBytes: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Per-client security settings.
|
|
241
|
+
* Mirrors SmartProxy's IRouteSecurity: ipAllowList/ipBlockList naming + deny-overrides-allow.
|
|
242
|
+
* Adds VPN-specific destination filtering.
|
|
243
|
+
*/
|
|
244
|
+
export interface IClientSecurity {
|
|
245
|
+
/** Source IPs/CIDRs the client may connect FROM (empty = any).
|
|
246
|
+
* Supports: exact IP, CIDR, wildcard (192.168.1.*), ranges (1.1.1.1-1.1.1.5). */
|
|
247
|
+
ipAllowList?: string[];
|
|
248
|
+
/** Source IPs blocked — overrides ipAllowList (deny wins). */
|
|
249
|
+
ipBlockList?: string[];
|
|
250
|
+
/** Destination IPs/CIDRs the client may reach through the VPN (empty = all). */
|
|
251
|
+
destinationAllowList?: string[];
|
|
252
|
+
/** Destination IPs blocked — overrides destinationAllowList (deny wins). */
|
|
253
|
+
destinationBlockList?: string[];
|
|
254
|
+
/** Max concurrent connections from this client. */
|
|
255
|
+
maxConnections?: number;
|
|
256
|
+
/** Per-client rate limiting. */
|
|
257
|
+
rateLimit?: IClientRateLimit;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Server-side client definition — the central config object for the Hub.
|
|
262
|
+
* Naming and structure aligned with SmartProxy's IRouteConfig / IRouteSecurity.
|
|
263
|
+
*/
|
|
264
|
+
export interface IClientEntry {
|
|
265
|
+
/** Human-readable client ID (e.g. "alice-laptop") */
|
|
266
|
+
clientId: string;
|
|
267
|
+
/** Client's Noise IK public key (base64) — for SmartVPN native transport */
|
|
268
|
+
publicKey: string;
|
|
269
|
+
/** Client's WireGuard public key (base64) — for WireGuard transport */
|
|
270
|
+
wgPublicKey?: string;
|
|
271
|
+
/** Security settings (ACLs, rate limits) */
|
|
272
|
+
security?: IClientSecurity;
|
|
273
|
+
/** Traffic priority (lower = higher priority, default: 100) */
|
|
274
|
+
priority?: number;
|
|
275
|
+
/** Whether this client is enabled (default: true) */
|
|
276
|
+
enabled?: boolean;
|
|
277
|
+
/** Tags for grouping (e.g. ["engineering", "office"]) */
|
|
278
|
+
tags?: string[];
|
|
279
|
+
/** Optional description */
|
|
280
|
+
description?: string;
|
|
281
|
+
/** Optional expiry (ISO 8601 timestamp, omit = never expires) */
|
|
282
|
+
expiresAt?: string;
|
|
283
|
+
/** Assigned VPN IP address (set by server) */
|
|
284
|
+
assignedIp?: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Complete client config bundle — returned by createClient() and rotateClientKey().
|
|
289
|
+
* Contains everything the client needs to connect.
|
|
290
|
+
*/
|
|
291
|
+
export interface IClientConfigBundle {
|
|
292
|
+
/** The server-side client entry */
|
|
293
|
+
entry: IClientEntry;
|
|
294
|
+
/** Ready-to-use SmartVPN client config (typed object) */
|
|
295
|
+
smartvpnConfig: IVpnClientConfig;
|
|
296
|
+
/** Ready-to-use WireGuard .conf file content (string) */
|
|
297
|
+
wireguardConfig: string;
|
|
298
|
+
/** Client's private keys (ONLY returned at creation time, not stored server-side) */
|
|
299
|
+
secrets: {
|
|
300
|
+
noisePrivateKey: string;
|
|
301
|
+
wgPrivateKey: string;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
208
305
|
// ============================================================================
|
|
209
306
|
// WireGuard-specific types
|
|
210
307
|
// ============================================================================
|
|
@@ -262,6 +359,17 @@ export type TVpnServerCommands = {
|
|
|
262
359
|
addWgPeer: { params: { peer: IWgPeerConfig }; result: void };
|
|
263
360
|
removeWgPeer: { params: { publicKey: string }; result: void };
|
|
264
361
|
listWgPeers: { params: Record<string, never>; result: { peers: IWgPeerInfo[] } };
|
|
362
|
+
// Client Registry (Hub) commands
|
|
363
|
+
createClient: { params: { client: Partial<IClientEntry> }; result: IClientConfigBundle };
|
|
364
|
+
removeClient: { params: { clientId: string }; result: void };
|
|
365
|
+
getClient: { params: { clientId: string }; result: IClientEntry };
|
|
366
|
+
listRegisteredClients: { params: Record<string, never>; result: { clients: IClientEntry[] } };
|
|
367
|
+
updateClient: { params: { clientId: string; update: Partial<IClientEntry> }; result: void };
|
|
368
|
+
enableClient: { params: { clientId: string }; result: void };
|
|
369
|
+
disableClient: { params: { clientId: string }; result: void };
|
|
370
|
+
rotateClientKey: { params: { clientId: string }; result: IClientConfigBundle };
|
|
371
|
+
exportClientConfig: { params: { clientId: string; format: 'smartvpn' | 'wireguard' }; result: { config: string } };
|
|
372
|
+
generateClientKeypair: { params: Record<string, never>; result: IVpnKeypair };
|
|
265
373
|
};
|
|
266
374
|
|
|
267
375
|
// ============================================================================
|