wireshade 1.0.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/.github/workflows/publish.yml +111 -0
- package/Cargo.lock +1238 -0
- package/Cargo.toml +27 -0
- package/LICENSE +21 -0
- package/README.de.md +256 -0
- package/README.es.md +109 -0
- package/README.fr.md +109 -0
- package/README.md +258 -0
- package/README.zh.md +109 -0
- package/build.rs +5 -0
- package/examples/01_quickstart.js +36 -0
- package/examples/02_http_request.js +44 -0
- package/examples/03_https_custom_dns.js +47 -0
- package/examples/04_tcp_socket.js +50 -0
- package/examples/05_internet_routing.js +33 -0
- package/examples/06_simple_server.js +40 -0
- package/examples/07_express_app.js +37 -0
- package/examples/08_local_forwarding.js +30 -0
- package/examples/09_reconnect_config.js +128 -0
- package/examples/10_remote_forwarding.js +63 -0
- package/examples/README.md +54 -0
- package/examples/_legacy_easy_api.js +53 -0
- package/examples/wireguard.conf +8 -0
- package/index.d.ts +0 -0
- package/index.js +29 -0
- package/lib/agent.js +116 -0
- package/lib/client.js +548 -0
- package/lib/config_parser.js +77 -0
- package/lib/server.js +121 -0
- package/package.json +31 -0
- package/parallel_output.txt +0 -0
- package/src/lib.rs +841 -0
- package/wireshade.darwin-arm64.node +0 -0
- package/wireshade.darwin-x64.node +0 -0
- package/wireshade.linux-arm-gnueabihf.node +0 -0
- package/wireshade.linux-arm64-gnu.node +0 -0
- package/wireshade.linux-x64-gnu.node +0 -0
- package/wireshade.win32-x64-msvc.node +0 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
let binding;
|
|
6
|
+
try {
|
|
7
|
+
binding = require('../wireshade.node');
|
|
8
|
+
} catch (e) {
|
|
9
|
+
try {
|
|
10
|
+
binding = require('../wireshade.win32-x64-msvc.node');
|
|
11
|
+
} catch (e2) {
|
|
12
|
+
throw new Error('Could not load native binding: ' + e2.message);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const { WireShade } = binding;
|
|
16
|
+
const { WireShadeAgent } = require('./agent');
|
|
17
|
+
const { WireShadeServer } = require('./server');
|
|
18
|
+
const { readWireGuardConfig } = require('./config_parser');
|
|
19
|
+
const http = require('http');
|
|
20
|
+
const https = require('https');
|
|
21
|
+
const tls = require('tls');
|
|
22
|
+
const net = require('net');
|
|
23
|
+
const dns = require('dns');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Connection states
|
|
27
|
+
*/
|
|
28
|
+
const ConnectionState = {
|
|
29
|
+
DISCONNECTED: 'disconnected',
|
|
30
|
+
CONNECTING: 'connecting',
|
|
31
|
+
CONNECTED: 'connected',
|
|
32
|
+
RECONNECTING: 'reconnecting'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class WireShadeClient extends EventEmitter {
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object|string} configOrPath - Config object OR path to .conf file
|
|
38
|
+
* @param {Object} [options] - Additional options if using config path
|
|
39
|
+
*/
|
|
40
|
+
constructor(configOrPath, options = {}) {
|
|
41
|
+
super();
|
|
42
|
+
|
|
43
|
+
let config = configOrPath;
|
|
44
|
+
if (typeof configOrPath === 'string') {
|
|
45
|
+
config = {
|
|
46
|
+
...options,
|
|
47
|
+
wireguard: readWireGuardConfig(configOrPath)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.config = config;
|
|
52
|
+
this.hosts = config.hosts || {};
|
|
53
|
+
this.agents = { http: null, https: null, tcp: null };
|
|
54
|
+
this.servers = [];
|
|
55
|
+
this.gw = null;
|
|
56
|
+
|
|
57
|
+
// Connection state
|
|
58
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
59
|
+
this.reconnectAttempts = 0;
|
|
60
|
+
this.reconnectTimer = null;
|
|
61
|
+
this.healthCheckTimer = null;
|
|
62
|
+
|
|
63
|
+
// Reconnection config with defaults
|
|
64
|
+
this.reconnectConfig = {
|
|
65
|
+
enabled: config.reconnect?.enabled !== false,
|
|
66
|
+
maxAttempts: config.reconnect?.maxAttempts ?? 10,
|
|
67
|
+
delay: config.reconnect?.delay ?? 1000,
|
|
68
|
+
maxDelay: config.reconnect?.maxDelay ?? 30000,
|
|
69
|
+
backoffMultiplier: config.reconnect?.backoffMultiplier ?? 1.5,
|
|
70
|
+
healthCheckInterval: config.reconnect?.healthCheckInterval ?? 30000
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// Support for property-style callbacks
|
|
78
|
+
if (config.onConnect) this.on('connect', config.onConnect);
|
|
79
|
+
if (config.onDisconnect) this.on('disconnect', config.onDisconnect);
|
|
80
|
+
if (config.onReconnect) this.on('reconnect', config.onReconnect);
|
|
81
|
+
|
|
82
|
+
// Pre-create wrappers (lazy or eager)
|
|
83
|
+
this._httpWrapper = this._wrapModule(http, () => this.getHttpAgent());
|
|
84
|
+
this._httpsWrapper = this._wrapModule(https, () => this.getHttpsAgent());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Access the `http` module wrapper that routes requests through VPN
|
|
89
|
+
*/
|
|
90
|
+
get http() { return this._httpWrapper; }
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Access the `https` module wrapper that routes requests through VPN
|
|
94
|
+
*/
|
|
95
|
+
get https() { return this._httpsWrapper; }
|
|
96
|
+
|
|
97
|
+
set onConnect(cb) { this.on('connect', cb); }
|
|
98
|
+
set onDisconnect(cb) { this.on('disconnect', cb); }
|
|
99
|
+
|
|
100
|
+
log(msg, ...args) {
|
|
101
|
+
if (this.config.logging !== false) {
|
|
102
|
+
console.log(msg, ...args);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Start the VPN connection
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async start() {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
// If already connected, resolve immediately
|
|
113
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
114
|
+
return resolve();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const onConnect = () => {
|
|
118
|
+
cleanup();
|
|
119
|
+
resolve();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const onError = (err) => {
|
|
123
|
+
cleanup();
|
|
124
|
+
reject(err);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
this.removeListener('connect', onConnect);
|
|
129
|
+
this.removeListener('error', onError);
|
|
130
|
+
// Also remove the disconnect listener we might catch during startup?
|
|
131
|
+
// For simplicity, rely on error or connect.
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
this.once('connect', onConnect);
|
|
135
|
+
// We might also want to catch immediate startup errors
|
|
136
|
+
this.once('disconnect', (err) => {
|
|
137
|
+
if (this.state !== ConnectionState.CONNECTED) {
|
|
138
|
+
cleanup();
|
|
139
|
+
reject(err || new Error("Disconnected during startup"));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this._initNative();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Internal: Initialize native binding
|
|
149
|
+
*/
|
|
150
|
+
_initNative() {
|
|
151
|
+
this.state = ConnectionState.CONNECTING;
|
|
152
|
+
this.emit('stateChange', this.state);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// Create new native instance
|
|
156
|
+
this.gw = new WireShade(
|
|
157
|
+
this.config.wireguard.privateKey,
|
|
158
|
+
this.config.wireguard.peerPublicKey,
|
|
159
|
+
this.config.wireguard.presharedKey || "",
|
|
160
|
+
this.config.wireguard.endpoint,
|
|
161
|
+
this.config.wireguard.sourceIp
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Initialize/Update TCP Agent
|
|
165
|
+
this.agents.tcp = new WireShadeAgent(this.gw, {
|
|
166
|
+
keepAlive: true,
|
|
167
|
+
logging: this.logging,
|
|
168
|
+
onConnectionError: (err) => this._handleConnectionError(err)
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Reset cached agents
|
|
172
|
+
this.agents.http = null;
|
|
173
|
+
this.agents.https = null;
|
|
174
|
+
|
|
175
|
+
// Simulate async handshake completion
|
|
176
|
+
// Native currently doesn't expose a "Handshake Complete" event,
|
|
177
|
+
// so we assume success if no error occurs quickly.
|
|
178
|
+
// Future improvement: Expose handshake state from Rust.
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
if (this.state === ConnectionState.CONNECTING) {
|
|
181
|
+
this._onConnected();
|
|
182
|
+
}
|
|
183
|
+
}, 1000); // Reduced to 1s for snappier feel
|
|
184
|
+
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this.log('[WireShadeClient] Connection failed:', err.message);
|
|
187
|
+
this._handleConnectionError(err);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Called when connection is established
|
|
193
|
+
*/
|
|
194
|
+
_onConnected() {
|
|
195
|
+
const wasReconnecting = this.state === ConnectionState.RECONNECTING;
|
|
196
|
+
this.state = ConnectionState.CONNECTED;
|
|
197
|
+
this.reconnectAttempts = 0;
|
|
198
|
+
|
|
199
|
+
this.emit('stateChange', this.state);
|
|
200
|
+
this.emit('connect');
|
|
201
|
+
|
|
202
|
+
if (wasReconnecting) {
|
|
203
|
+
this.log('[WireShadeClient] Reconnected successfully!');
|
|
204
|
+
this.emit('reconnect');
|
|
205
|
+
if (this.config.onReconnect) this.config.onReconnect();
|
|
206
|
+
} else {
|
|
207
|
+
this.log('[WireShadeClient] Connected!');
|
|
208
|
+
if (this.config.onConnect) this.config.onConnect();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Start health check
|
|
212
|
+
this._startHealthCheck();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle connection errors
|
|
217
|
+
*/
|
|
218
|
+
_handleConnectionError(err) {
|
|
219
|
+
this.log('[WireShadeClient] Connection error:', err?.message || err);
|
|
220
|
+
|
|
221
|
+
if (this.state === ConnectionState.DISCONNECTED) {
|
|
222
|
+
return; // Already closed
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
226
|
+
this.emit('stateChange', this.state);
|
|
227
|
+
this.emit('disconnect', err);
|
|
228
|
+
|
|
229
|
+
if (this.config.onDisconnect) this.config.onDisconnect(err);
|
|
230
|
+
|
|
231
|
+
// Attempt reconnection if enabled
|
|
232
|
+
if (this.reconnectConfig.enabled) {
|
|
233
|
+
this._scheduleReconnect();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Schedule a reconnection attempt
|
|
239
|
+
*/
|
|
240
|
+
_scheduleReconnect() {
|
|
241
|
+
if (this.reconnectTimer) {
|
|
242
|
+
clearTimeout(this.reconnectTimer);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check max attempts
|
|
246
|
+
if (this.reconnectConfig.maxAttempts > 0 &&
|
|
247
|
+
this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
|
|
248
|
+
this.log('[WireShadeClient] Max reconnection attempts reached');
|
|
249
|
+
this.emit('reconnectFailed');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Calculate delay with exponential backoff
|
|
254
|
+
const delay = Math.min(
|
|
255
|
+
this.reconnectConfig.delay * Math.pow(this.reconnectConfig.backoffMultiplier, this.reconnectAttempts),
|
|
256
|
+
this.reconnectConfig.maxDelay
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
this.reconnectAttempts++;
|
|
260
|
+
this.state = ConnectionState.RECONNECTING;
|
|
261
|
+
this.emit('stateChange', this.state);
|
|
262
|
+
|
|
263
|
+
this.log(`[WireShadeClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.reconnectConfig.maxAttempts || '∞'})`);
|
|
264
|
+
|
|
265
|
+
this.reconnectTimer = setTimeout(() => {
|
|
266
|
+
this.emit('reconnecting', this.reconnectAttempts);
|
|
267
|
+
this._initNative();
|
|
268
|
+
}, delay);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Start periodic health checks
|
|
273
|
+
*/
|
|
274
|
+
_startHealthCheck() {
|
|
275
|
+
this._stopHealthCheck();
|
|
276
|
+
|
|
277
|
+
if (this.reconnectConfig.healthCheckInterval > 0) {
|
|
278
|
+
this.healthCheckTimer = setInterval(() => {
|
|
279
|
+
this._performHealthCheck();
|
|
280
|
+
}, this.reconnectConfig.healthCheckInterval);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Stop health checks
|
|
286
|
+
*/
|
|
287
|
+
_stopHealthCheck() {
|
|
288
|
+
if (this.healthCheckTimer) {
|
|
289
|
+
clearInterval(this.healthCheckTimer);
|
|
290
|
+
this.healthCheckTimer = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Perform a health check (attempt a simple operation)
|
|
296
|
+
*/
|
|
297
|
+
async _performHealthCheck() {
|
|
298
|
+
// For now, we rely on the WireGuard keepalives
|
|
299
|
+
// Future: Could ping a known VPN host
|
|
300
|
+
this.emit('healthCheck');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Manually trigger reconnection
|
|
305
|
+
*/
|
|
306
|
+
reconnect() {
|
|
307
|
+
this.log('[WireShadeClient] Manual reconnect triggered');
|
|
308
|
+
this.reconnectAttempts = 0;
|
|
309
|
+
this._stopHealthCheck();
|
|
310
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
311
|
+
this._initNative();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getHttpAgent() {
|
|
315
|
+
if (!this.agents.http) {
|
|
316
|
+
this.agents.http = new http.Agent({
|
|
317
|
+
keepAlive: true,
|
|
318
|
+
lookup: this._customLookup.bind(this)
|
|
319
|
+
});
|
|
320
|
+
this.agents.http.createConnection = (options, cb) => {
|
|
321
|
+
return this.agents.tcp.createConnection(options, cb);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return this.agents.http;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getHttpsAgent() {
|
|
328
|
+
if (!this.agents.https) {
|
|
329
|
+
this.agents.https = new https.Agent({
|
|
330
|
+
keepAlive: true,
|
|
331
|
+
lookup: this._customLookup.bind(this),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
this.agents.https.createConnection = (options, cb) => {
|
|
335
|
+
const rawSocket = this.agents.tcp.createConnection(options);
|
|
336
|
+
const tlsOptions = {
|
|
337
|
+
...options,
|
|
338
|
+
socket: rawSocket,
|
|
339
|
+
servername: options.hostname || options.host
|
|
340
|
+
};
|
|
341
|
+
return tls.connect(tlsOptions, cb);
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return this.agents.https;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
addHost(hostname, ip) {
|
|
348
|
+
this.hosts[hostname] = ip;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async forwardLocal(localPort, remoteHost, remotePort) {
|
|
352
|
+
return new Promise((resolve, reject) => {
|
|
353
|
+
const server = net.createServer((clientSocket) => {
|
|
354
|
+
const tunnelSocket = this.agents.tcp.createConnection({
|
|
355
|
+
host: remoteHost,
|
|
356
|
+
port: remotePort
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
clientSocket.pipe(tunnelSocket);
|
|
360
|
+
tunnelSocket.pipe(clientSocket);
|
|
361
|
+
|
|
362
|
+
const cleanup = () => {
|
|
363
|
+
clientSocket.destroy();
|
|
364
|
+
tunnelSocket.destroy();
|
|
365
|
+
};
|
|
366
|
+
clientSocket.on('error', cleanup);
|
|
367
|
+
tunnelSocket.on('error', cleanup);
|
|
368
|
+
clientSocket.on('close', cleanup);
|
|
369
|
+
tunnelSocket.on('close', cleanup);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
server.listen(localPort, () => {
|
|
373
|
+
this.servers.push(server);
|
|
374
|
+
resolve(server);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
server.on('error', reject);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Listen on a VPN port and forward all traffic to a local destination (Reverse Port Forwarding).
|
|
383
|
+
* @param {number} vpnPort - The port to listen on inside the VPN.
|
|
384
|
+
* @param {string} targetHost - The local host to forward to (e.g., 'localhost').
|
|
385
|
+
* @param {number} targetPort - The local port to forward to.
|
|
386
|
+
* @returns {Promise<WireShadeServer>}
|
|
387
|
+
*/
|
|
388
|
+
async forwardRemote(vpnPort, targetHost, targetPort) {
|
|
389
|
+
return this.listen(vpnPort, (vpnSocket) => {
|
|
390
|
+
const localSocket = net.connect(targetPort, targetHost, () => {
|
|
391
|
+
// Pipe data between VPN socket and Local socket
|
|
392
|
+
vpnSocket.pipe(localSocket);
|
|
393
|
+
localSocket.pipe(vpnSocket);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const cleanup = () => {
|
|
397
|
+
vpnSocket.destroy();
|
|
398
|
+
localSocket.destroy();
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
vpnSocket.on('error', cleanup);
|
|
402
|
+
localSocket.on('error', cleanup);
|
|
403
|
+
vpnSocket.on('close', cleanup);
|
|
404
|
+
localSocket.on('close', cleanup);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_customLookup(hostname, options, callback) {
|
|
409
|
+
if (this.hosts[hostname]) {
|
|
410
|
+
return callback(null, this.hosts[hostname], 4);
|
|
411
|
+
}
|
|
412
|
+
dns.lookup(hostname, options, callback);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Internal: Wrap http/https module to inject agent
|
|
417
|
+
*/
|
|
418
|
+
_wrapModule(module, agentGetter) {
|
|
419
|
+
const wrapper = { ...module };
|
|
420
|
+
|
|
421
|
+
wrapper.request = (...args) => {
|
|
422
|
+
// Determine where options object is
|
|
423
|
+
let options = typeof args[0] === 'string' || args[0] instanceof URL
|
|
424
|
+
? args[1]
|
|
425
|
+
: args[0];
|
|
426
|
+
|
|
427
|
+
// Handle case where options is actually callback (if valid usage) or missing
|
|
428
|
+
if (typeof options === 'function' || !options) {
|
|
429
|
+
options = {};
|
|
430
|
+
if (typeof args[0] === 'string' || args[0] instanceof URL) {
|
|
431
|
+
if (typeof args[1] === 'function') {
|
|
432
|
+
return module.request(args[0], { agent: agentGetter() }, args[1]);
|
|
433
|
+
} else if (!args[1]) {
|
|
434
|
+
return module.request(args[0], { agent: agentGetter() });
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
return module.request({ ...args[0], agent: agentGetter() }, args[1]);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// If we are here, options exists and is an object.
|
|
442
|
+
const newOptions = { ...options, agent: agentGetter() };
|
|
443
|
+
|
|
444
|
+
if (typeof args[0] === 'string' || args[0] instanceof URL) {
|
|
445
|
+
return module.request(args[0], newOptions, args[2]);
|
|
446
|
+
} else {
|
|
447
|
+
return module.request(newOptions, args[1]);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
wrapper.get = (...args) => {
|
|
452
|
+
const req = wrapper.request(...args);
|
|
453
|
+
req.end();
|
|
454
|
+
return req;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
return wrapper;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Start a TCP server listener on the VPN interface
|
|
462
|
+
* @param {number} port
|
|
463
|
+
* @param {Function} [onConnection] - (socket) => void
|
|
464
|
+
* @returns {Promise<GhostWireServer>}
|
|
465
|
+
*/
|
|
466
|
+
async listen(port, onConnection) {
|
|
467
|
+
if (!this.gw) throw new Error("WireShade not initialized");
|
|
468
|
+
|
|
469
|
+
const server = new WireShadeServer(this.gw, { logging: this.logging });
|
|
470
|
+
|
|
471
|
+
if (onConnection) {
|
|
472
|
+
server.on('connection', onConnection);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await server.listen(port);
|
|
476
|
+
this.servers.push(server);
|
|
477
|
+
return server;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Perform an HTTP GET request
|
|
482
|
+
* @param {string} url
|
|
483
|
+
* @param {Object} [options]
|
|
484
|
+
* @returns {Promise<string>} Body content
|
|
485
|
+
*/
|
|
486
|
+
async get(url, options = {}) {
|
|
487
|
+
return this.request(url, { ...options, method: 'GET' });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Perform an HTTP request
|
|
492
|
+
* @param {string} url
|
|
493
|
+
* @param {Object} [options]
|
|
494
|
+
* @returns {Promise<string>} Body content
|
|
495
|
+
*/
|
|
496
|
+
request(urlStr, options = {}) {
|
|
497
|
+
return new Promise((resolve, reject) => {
|
|
498
|
+
const isHttps = urlStr.startsWith('https:');
|
|
499
|
+
const agent = isHttps ? this.getHttpsAgent() : this.getHttpAgent();
|
|
500
|
+
const mod = isHttps ? https : http;
|
|
501
|
+
|
|
502
|
+
const req = mod.request(urlStr, { ...options, agent }, (res) => {
|
|
503
|
+
let data = '';
|
|
504
|
+
res.on('data', c => data += c);
|
|
505
|
+
res.on('end', () => resolve(data));
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
req.on('error', reject);
|
|
509
|
+
|
|
510
|
+
if (options.body) {
|
|
511
|
+
req.write(options.body);
|
|
512
|
+
}
|
|
513
|
+
req.end();
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Create a TCP connection through the tunnel
|
|
519
|
+
* @param {Object} options - { host, port }
|
|
520
|
+
* @returns {net.Socket}
|
|
521
|
+
*/
|
|
522
|
+
connect(options, connectionListener) {
|
|
523
|
+
if (!this.agents.tcp) throw new Error("WireShade not initialized");
|
|
524
|
+
return this.agents.tcp.createConnection(options, connectionListener);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
close() {
|
|
528
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
529
|
+
this.reconnectConfig.enabled = false; // Prevent reconnection
|
|
530
|
+
|
|
531
|
+
this._stopHealthCheck();
|
|
532
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
533
|
+
|
|
534
|
+
if (this.agents.http) this.agents.http.destroy();
|
|
535
|
+
if (this.agents.https) this.agents.https.destroy();
|
|
536
|
+
if (this.agents.tcp) this.agents.tcp.destroy();
|
|
537
|
+
this.servers.forEach(s => s.close());
|
|
538
|
+
|
|
539
|
+
this.emit('stateChange', this.state);
|
|
540
|
+
this.emit('close');
|
|
541
|
+
|
|
542
|
+
if (this.config.onDisconnect) {
|
|
543
|
+
this.config.onDisconnect();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
module.exports = { WireShadeClient, ConnectionState };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses a standard WireGuard configuration file content.
|
|
5
|
+
* @param {string} content - The content of the .conf file
|
|
6
|
+
* @returns {Object} Config object suitable for WireShade
|
|
7
|
+
*/
|
|
8
|
+
function parseWireGuardConfig(content) {
|
|
9
|
+
const lines = content.split('\n');
|
|
10
|
+
const config = {
|
|
11
|
+
privateKey: '',
|
|
12
|
+
sourceIp: '',
|
|
13
|
+
peerPublicKey: '',
|
|
14
|
+
presharedKey: '',
|
|
15
|
+
endpoint: ''
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let currentSection = '';
|
|
19
|
+
|
|
20
|
+
for (let line of lines) {
|
|
21
|
+
line = line.trim();
|
|
22
|
+
if (!line || line.startsWith('#')) continue;
|
|
23
|
+
|
|
24
|
+
if (line.startsWith('[') && line.endsWith(']')) {
|
|
25
|
+
currentSection = line.slice(1, -1).toLowerCase();
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [key, ...valueParts] = line.split('=');
|
|
30
|
+
if (!key || valueParts.length === 0) continue;
|
|
31
|
+
|
|
32
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
33
|
+
const value = valueParts.join('=').trim();
|
|
34
|
+
|
|
35
|
+
if (currentSection === 'interface') {
|
|
36
|
+
if (normalizedKey === 'privatekey') {
|
|
37
|
+
config.privateKey = value;
|
|
38
|
+
} else if (normalizedKey === 'address') {
|
|
39
|
+
// Remove subnet mask (e.g., /32) if present
|
|
40
|
+
config.sourceIp = value.split('/')[0].trim();
|
|
41
|
+
}
|
|
42
|
+
} else if (currentSection === 'peer') {
|
|
43
|
+
if (normalizedKey === 'publickey') {
|
|
44
|
+
config.peerPublicKey = value;
|
|
45
|
+
} else if (normalizedKey === 'presharedkey') {
|
|
46
|
+
config.presharedKey = value;
|
|
47
|
+
} else if (normalizedKey === 'endpoint') {
|
|
48
|
+
config.endpoint = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!config.privateKey || !config.peerPublicKey || !config.endpoint) {
|
|
54
|
+
throw new Error('Invalid WireGuard config: Missing required fields (PrivateKey, PublicKey, or Endpoint)');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reads and parses a WireGuard config file.
|
|
62
|
+
* @param {string} filePath - Path to the .conf file
|
|
63
|
+
* @returns {Object} Config object
|
|
64
|
+
*/
|
|
65
|
+
function readWireGuardConfig(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
68
|
+
return parseWireGuardConfig(content);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new Error(`Failed to read config file: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
parseWireGuardConfig,
|
|
76
|
+
readWireGuardConfig
|
|
77
|
+
};
|