polaris-vpn 0.2.0 → 0.4.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/CHANGELOG.md +8 -1
- package/README.md +2 -0
- package/package.json +6 -2
- package/src/cli.js +123 -3
- package/src/commands/deploy.js +57 -0
- package/src/commands/monitor.js +120 -0
- package/src/commands/peer.js +121 -0
- package/src/commands/server.js +35 -0
- package/src/commands/start.js +64 -27
- package/src/commands/stop.js +1 -1
- package/src/core/deploy-service.js +150 -0
- package/src/core/peer-service.js +238 -0
- package/src/core/tunnel-service.js +53 -5
- package/src/server/api.js +77 -0
- package/src/tunnel/wg.js +59 -0
- package/src/utils/kill-switch.js +69 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.4.0] - 2026-07-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Live Bandwidth Monitor**: Use `polaris monitor` to see real-time up/down speeds, data usage, and peer activity for WireGuard tunnels.
|
|
13
|
+
- **Local REST API**: Use `polaris server start` to run a local API on `127.0.0.1:7070` for external integrations and status queries.
|
|
14
|
+
|
|
15
|
+
## [0.3.1] - 2026-06-28
|
|
9
16
|
|
|
10
17
|
### Added
|
|
11
18
|
- Companion `polaris-server` dynamic HTTP/TLS forwarding proxy on port 8443.
|
package/README.md
CHANGED
|
@@ -46,10 +46,12 @@ polaris stop
|
|
|
46
46
|
| `polaris start --server user@host [--port 1080]` | Start the encrypted SSH tunnel. |
|
|
47
47
|
| `polaris stop` | Stop the active tunnel. |
|
|
48
48
|
| `polaris status` | Show current tunnel status, IP, and uptime. |
|
|
49
|
+
| `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
|
|
49
50
|
| `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
|
|
50
51
|
| `polaris add <alias> --server user@host` | Save a server profile for quick access. |
|
|
51
52
|
| `polaris list` | List all saved server profiles. |
|
|
52
53
|
| `polaris use <alias>` | Set a saved profile as the active default. |
|
|
54
|
+
| `polaris server start` | Start a local REST API on `127.0.0.1:7070`. |
|
|
53
55
|
|
|
54
56
|
> All commands support the `--json` flag for machine-readable output.
|
|
55
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polaris-vpn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,9 +37,13 @@
|
|
|
37
37
|
"commander": "^15.0.0",
|
|
38
38
|
"conf": "^15.1.0",
|
|
39
39
|
"dns2": "^3.0.0",
|
|
40
|
+
"express": "^5.2.1",
|
|
40
41
|
"node-fetch": "^3.3.2",
|
|
41
42
|
"node-notifier": "^10.0.1",
|
|
42
43
|
"ora": "^9.4.1",
|
|
43
|
-
"
|
|
44
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"socks-proxy-agent": "^10.1.0",
|
|
46
|
+
"ssh2": "^1.17.0",
|
|
47
|
+
"wireguard-tools": "^0.1.0"
|
|
44
48
|
}
|
|
45
49
|
}
|
package/src/cli.js
CHANGED
|
@@ -22,10 +22,10 @@ const willPrintJson = () => process.argv.includes('--json');
|
|
|
22
22
|
|
|
23
23
|
program
|
|
24
24
|
.command('start')
|
|
25
|
-
.description('Start the encrypted SSH or
|
|
26
|
-
.option('-s, --server <user@host>', 'SSH/TLS server to connect to')
|
|
25
|
+
.description('Start the encrypted SSH, TLS or WireGuard tunnel')
|
|
26
|
+
.option('-s, --server <user@host>', 'SSH/TLS/WireGuard server to connect to')
|
|
27
27
|
.option('-p, --port <number>', 'Local SOCKS5 port to bind', '1080')
|
|
28
|
-
.option('-m, --mode <type>', 'Tunnel mode: ssh, tls or auto', 'auto')
|
|
28
|
+
.option('-m, --mode <type>', 'Tunnel mode: ssh, tls, wireguard or auto', 'auto')
|
|
29
29
|
.action(async (options, cmd) => {
|
|
30
30
|
if (!cmd.optsWithGlobals().json) printBanner();
|
|
31
31
|
try {
|
|
@@ -168,6 +168,126 @@ dnsCmd
|
|
|
168
168
|
}
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
program
|
|
172
|
+
.command('deploy')
|
|
173
|
+
.description('Provision a fresh Ubuntu server as a WireGuard server')
|
|
174
|
+
.requiredOption('-s, --server <user@host>', 'SSH server to configure')
|
|
175
|
+
.option('-i, --identity <path>', 'SSH private key file path')
|
|
176
|
+
.option('-p, --password <password>', 'SSH password (alternative to identity)')
|
|
177
|
+
.action(async (options, cmd) => {
|
|
178
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
179
|
+
try {
|
|
180
|
+
const run = (await import('./commands/deploy.js')).default;
|
|
181
|
+
await run(cmd.optsWithGlobals());
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (!cmd.optsWithGlobals().json) printError('Command failed', err);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const peerCmd = program.command('peer').description('Manage WireGuard client peers');
|
|
189
|
+
|
|
190
|
+
peerCmd
|
|
191
|
+
.command('add <name>')
|
|
192
|
+
.description('Generate and add a new peer config')
|
|
193
|
+
.action(async (name, options, cmd) => {
|
|
194
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
195
|
+
try {
|
|
196
|
+
const { peerAdd } = await import('./commands/peer.js');
|
|
197
|
+
await peerAdd(name, cmd.optsWithGlobals());
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (!cmd.optsWithGlobals().json) printError('Command failed', err);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
peerCmd
|
|
205
|
+
.command('list')
|
|
206
|
+
.description('List all active peer configurations')
|
|
207
|
+
.action(async (options, cmd) => {
|
|
208
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
209
|
+
try {
|
|
210
|
+
const { peerList } = await import('./commands/peer.js');
|
|
211
|
+
await peerList(cmd.optsWithGlobals());
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (!cmd.optsWithGlobals().json) printError('Command failed', err);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
peerCmd
|
|
219
|
+
.command('remove <name>')
|
|
220
|
+
.description('Revoke a peer from the server')
|
|
221
|
+
.action(async (name, options, cmd) => {
|
|
222
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
223
|
+
try {
|
|
224
|
+
const { peerRemove } = await import('./commands/peer.js');
|
|
225
|
+
await peerRemove(name, cmd.optsWithGlobals());
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (!cmd.optsWithGlobals().json) printError('Command failed', err);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
peerCmd
|
|
233
|
+
.command('qr <name>')
|
|
234
|
+
.description('Show peer configuration QR code')
|
|
235
|
+
.action(async (name, options, cmd) => {
|
|
236
|
+
try {
|
|
237
|
+
const { peerQr } = await import('./commands/peer.js');
|
|
238
|
+
await peerQr(name, cmd.optsWithGlobals());
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (!cmd.optsWithGlobals().json) printError('Command failed', err);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const configCmd = program.command('config').description('Configure polaris settings');
|
|
246
|
+
configCmd
|
|
247
|
+
.command('set <key> <value>')
|
|
248
|
+
.description('Set a configuration setting (e.g. kill-switch true)')
|
|
249
|
+
.action(async (key, value, options, cmd) => {
|
|
250
|
+
const isJson = cmd.optsWithGlobals().json;
|
|
251
|
+
if (!isJson) printBanner();
|
|
252
|
+
try {
|
|
253
|
+
if (key === 'kill-switch') {
|
|
254
|
+
const val = value === 'true';
|
|
255
|
+
const { setKillSwitchConfig } = await import('./utils/kill-switch.js');
|
|
256
|
+
setKillSwitchConfig(val);
|
|
257
|
+
const { printSuccess } = await import('./utils/display.js');
|
|
258
|
+
if (!isJson) {
|
|
259
|
+
printSuccess(`Set config 'kill-switch' to ${val}`);
|
|
260
|
+
} else {
|
|
261
|
+
console.log(JSON.stringify({ success: true, key, value: val }));
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
throw new Error(`Unknown config key '${key}'`);
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (!isJson) printError('Command failed', err);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
program
|
|
273
|
+
.command('server')
|
|
274
|
+
.description('Manage the local REST API server')
|
|
275
|
+
.command('start')
|
|
276
|
+
.description('Start the local REST API server')
|
|
277
|
+
.option('-p, --port <port>', 'Port to listen on', '7070')
|
|
278
|
+
.action(async (options) => {
|
|
279
|
+
const { serverStart } = await import('./commands/server.js');
|
|
280
|
+
await serverStart({ ...options, json: willPrintJson() });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
program
|
|
284
|
+
.command('monitor')
|
|
285
|
+
.description('Live bandwidth monitor for WireGuard/AmneziaWG tunnels')
|
|
286
|
+
.action(async (options, cmd) => {
|
|
287
|
+
const run = (await import('./commands/monitor.js')).default;
|
|
288
|
+
await run(cmd.optsWithGlobals());
|
|
289
|
+
});
|
|
290
|
+
|
|
171
291
|
program.parseAsync(process.argv).catch(err => {
|
|
172
292
|
if (!willPrintJson()) printError('Fatal error', err);
|
|
173
293
|
process.exit(1);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { printSuccess, printError, createSpinner, printInfo } from '../utils/display.js';
|
|
3
|
+
import { deployServer } from '../core/deploy-service.js';
|
|
4
|
+
import startCommand from './start.js';
|
|
5
|
+
|
|
6
|
+
export default async (options) => {
|
|
7
|
+
const isJson = options.json;
|
|
8
|
+
const server = options.server;
|
|
9
|
+
|
|
10
|
+
if (!server) {
|
|
11
|
+
if (isJson) {
|
|
12
|
+
console.log(JSON.stringify({ error: 'Missing --server argument' }));
|
|
13
|
+
} else {
|
|
14
|
+
printError('You must specify a server with --server <user@host>');
|
|
15
|
+
}
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const spinner = isJson ? null : createSpinner('Initiating server deployment...').start();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const res = await deployServer(server, {
|
|
24
|
+
privateKey: options.identity,
|
|
25
|
+
password: options.password,
|
|
26
|
+
onProgress: (msg) => {
|
|
27
|
+
if (spinner) spinner.text = msg;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (spinner) {
|
|
32
|
+
spinner.succeed('Server provisioning completed successfully!');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isJson) {
|
|
36
|
+
console.log(JSON.stringify({ success: true, ...res }));
|
|
37
|
+
} else {
|
|
38
|
+
printSuccess(`WireGuard configured on remote VPS ${server}`);
|
|
39
|
+
printInfo(`Local Client Config: ${res.clientConfPath}`);
|
|
40
|
+
printInfo(`Client Public Key : ${res.clientPublicKey}`);
|
|
41
|
+
printInfo(`Server Public Key : ${res.serverPublicKey}`);
|
|
42
|
+
console.log(chalk.cyan('\nStarting WireGuard tunnel connection locally...\n'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Automatically trigger local connection
|
|
46
|
+
await startCommand({ mode: 'wireguard', json: isJson });
|
|
47
|
+
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (spinner) spinner.fail('Deployment failed');
|
|
50
|
+
if (isJson) {
|
|
51
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
52
|
+
} else {
|
|
53
|
+
printError('Deployment failed', err);
|
|
54
|
+
}
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getActiveTunnel } from '../core/tunnel-service.js';
|
|
3
|
+
import { createTable, printError } from '../utils/display.js';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { CONFIG_DIR } from '../utils/config.js';
|
|
8
|
+
|
|
9
|
+
const getWgStats = () => {
|
|
10
|
+
const confPathAwg = path.join(CONFIG_DIR, 'wg', 'awg0.conf');
|
|
11
|
+
const showCmd = fs.existsSync(confPathAwg) ? 'awg' : 'wg';
|
|
12
|
+
|
|
13
|
+
const dumpRes = spawnSync('sudo', [showCmd, 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
14
|
+
if (dumpRes.status !== 0) return null;
|
|
15
|
+
|
|
16
|
+
const lines = dumpRes.stdout.trim().split('\n');
|
|
17
|
+
if (lines.length <= 1) return null;
|
|
18
|
+
|
|
19
|
+
const peers = [];
|
|
20
|
+
let totalRx = 0;
|
|
21
|
+
let totalTx = 0;
|
|
22
|
+
|
|
23
|
+
for (let i = 1; i < lines.length; i++) {
|
|
24
|
+
const peerInfo = lines[i].split('\t');
|
|
25
|
+
const rx = parseInt(peerInfo[6], 10) || 0;
|
|
26
|
+
const tx = parseInt(peerInfo[7], 10) || 0;
|
|
27
|
+
totalRx += rx;
|
|
28
|
+
totalTx += tx;
|
|
29
|
+
peers.push({
|
|
30
|
+
pubKey: peerInfo[1].substring(0, 8) + '...',
|
|
31
|
+
endpoint: peerInfo[4],
|
|
32
|
+
rx,
|
|
33
|
+
tx
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { peers, totalRx, totalTx };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const formatBytes = (bytes) => {
|
|
41
|
+
if (bytes === 0) return '0 B';
|
|
42
|
+
const k = 1024;
|
|
43
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
44
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
45
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default async (options) => {
|
|
49
|
+
const isJson = options.json;
|
|
50
|
+
const info = getActiveTunnel();
|
|
51
|
+
|
|
52
|
+
if (!info || info.mode !== 'wireguard') {
|
|
53
|
+
if (isJson) {
|
|
54
|
+
console.log(JSON.stringify({ error: 'WireGuard tunnel is not active' }));
|
|
55
|
+
} else {
|
|
56
|
+
printError('Monitor requires an active WireGuard or AmneziaWG tunnel.');
|
|
57
|
+
}
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isJson) {
|
|
63
|
+
console.log(JSON.stringify(getWgStats()));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let prevRx = 0;
|
|
68
|
+
let prevTx = 0;
|
|
69
|
+
let hasInit = false;
|
|
70
|
+
|
|
71
|
+
console.clear();
|
|
72
|
+
console.log(chalk.cyan.bold('\n● Polaris Live Bandwidth Monitor\n'));
|
|
73
|
+
console.log(chalk.dim('Press Ctrl+C to exit.\n'));
|
|
74
|
+
|
|
75
|
+
setInterval(() => {
|
|
76
|
+
const stats = getWgStats();
|
|
77
|
+
if (!stats) return;
|
|
78
|
+
|
|
79
|
+
if (!hasInit) {
|
|
80
|
+
prevRx = stats.totalRx;
|
|
81
|
+
prevTx = stats.totalTx;
|
|
82
|
+
hasInit = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rxSpeed = stats.totalRx - prevRx;
|
|
87
|
+
const txSpeed = stats.totalTx - prevTx;
|
|
88
|
+
|
|
89
|
+
prevRx = stats.totalRx;
|
|
90
|
+
prevTx = stats.totalTx;
|
|
91
|
+
|
|
92
|
+
console.clear();
|
|
93
|
+
console.log(chalk.cyan.bold('\n● Polaris Live Bandwidth Monitor\n'));
|
|
94
|
+
console.log(chalk.dim('Press Ctrl+C to exit.\n'));
|
|
95
|
+
|
|
96
|
+
const table = createTable(['Metric', 'Value']);
|
|
97
|
+
table.push(
|
|
98
|
+
['Download Speed', chalk.green(`${formatBytes(rxSpeed)}/s`)],
|
|
99
|
+
['Upload Speed', chalk.green(`${formatBytes(txSpeed)}/s`)],
|
|
100
|
+
['Total Data Received', formatBytes(stats.totalRx)],
|
|
101
|
+
['Total Data Sent', formatBytes(stats.totalTx)]
|
|
102
|
+
);
|
|
103
|
+
console.log(table.toString());
|
|
104
|
+
|
|
105
|
+
if (stats.peers.length > 0) {
|
|
106
|
+
console.log(chalk.bold('\nActive Peers:'));
|
|
107
|
+
const peerTable = createTable(['Peer', 'Endpoint', 'Received', 'Sent']);
|
|
108
|
+
for (const peer of stats.peers) {
|
|
109
|
+
peerTable.push([
|
|
110
|
+
peer.pubKey,
|
|
111
|
+
peer.endpoint,
|
|
112
|
+
formatBytes(peer.rx),
|
|
113
|
+
formatBytes(peer.tx)
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
console.log(peerTable.toString());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}, 1000);
|
|
120
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import qrcode from 'qrcode-terminal';
|
|
4
|
+
import { printSuccess, printError, createSpinner, printInfo, createTable } from '../utils/display.js';
|
|
5
|
+
import { addPeer, listPeers, removePeer, getLocalPeerConfPath } from '../core/peer-service.js';
|
|
6
|
+
|
|
7
|
+
export const peerAdd = async (name, options) => {
|
|
8
|
+
const isJson = options.json;
|
|
9
|
+
const spinner = isJson ? null : createSpinner(`Creating peer '${name}' on server...`).start();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const res = await addPeer(name);
|
|
13
|
+
if (spinner) spinner.succeed(`Peer '${name}' created successfully!`);
|
|
14
|
+
|
|
15
|
+
if (isJson) {
|
|
16
|
+
console.log(JSON.stringify({ success: true, ...res }));
|
|
17
|
+
} else {
|
|
18
|
+
printSuccess(`Saved local peer profile: ${res.confPath}`);
|
|
19
|
+
printInfo(`IP Assigned: ${res.ip}`);
|
|
20
|
+
printInfo(`Public Key : ${res.publicKey}`);
|
|
21
|
+
console.log(chalk.cyan('\nScan this QR code with the WireGuard app on your mobile device:\n'));
|
|
22
|
+
|
|
23
|
+
const confString = fs.readFileSync(res.confPath, 'utf-8');
|
|
24
|
+
qrcode.generate(confString, { small: true });
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (spinner) spinner.fail('Failed to add peer');
|
|
28
|
+
if (isJson) {
|
|
29
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
30
|
+
} else {
|
|
31
|
+
printError('Failed to add peer', err);
|
|
32
|
+
}
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const peerList = async (options) => {
|
|
38
|
+
const isJson = options.json;
|
|
39
|
+
const spinner = isJson ? null : createSpinner('Fetching peer list...').start();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const peers = await listPeers();
|
|
43
|
+
if (spinner) spinner.stop();
|
|
44
|
+
|
|
45
|
+
if (isJson) {
|
|
46
|
+
console.log(JSON.stringify({ success: true, peers }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (peers.length === 0) {
|
|
51
|
+
printInfo('No peers configured on server.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const table = createTable(['Name', 'Assigned IP', 'Latest Handshake', 'Transfer Received/Sent']);
|
|
56
|
+
|
|
57
|
+
for (const p of peers) {
|
|
58
|
+
table.push([
|
|
59
|
+
chalk.green.bold(p.name),
|
|
60
|
+
p.ip,
|
|
61
|
+
p.handshake,
|
|
62
|
+
`${p.transferRx} / ${p.transferTx}`
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('\n' + table.toString() + '\n');
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (spinner) spinner.stop();
|
|
69
|
+
if (isJson) {
|
|
70
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
71
|
+
} else {
|
|
72
|
+
printError('Failed to list peers', err);
|
|
73
|
+
}
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const peerRemove = async (name, options) => {
|
|
79
|
+
const isJson = options.json;
|
|
80
|
+
const spinner = isJson ? null : createSpinner(`Revoking peer '${name}'...`).start();
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await removePeer(name);
|
|
84
|
+
if (spinner) spinner.succeed(`Peer '${name}' revoked from server.`);
|
|
85
|
+
|
|
86
|
+
if (isJson) {
|
|
87
|
+
console.log(JSON.stringify({ success: true }));
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (spinner) spinner.fail('Failed to remove peer');
|
|
91
|
+
if (isJson) {
|
|
92
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
93
|
+
} else {
|
|
94
|
+
printError('Failed to remove peer', err);
|
|
95
|
+
}
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const peerQr = async (name, options) => {
|
|
101
|
+
const isJson = options.json;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const confPath = getLocalPeerConfPath(name);
|
|
105
|
+
const confString = fs.readFileSync(confPath, 'utf-8');
|
|
106
|
+
|
|
107
|
+
if (isJson) {
|
|
108
|
+
console.log(JSON.stringify({ success: true, config: confString }));
|
|
109
|
+
} else {
|
|
110
|
+
console.log(chalk.cyan(`\nQR code for peer configuration '${name}':\n`));
|
|
111
|
+
qrcode.generate(confString, { small: true });
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (isJson) {
|
|
115
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
116
|
+
} else {
|
|
117
|
+
printError('Failed to retrieve QR code', err);
|
|
118
|
+
}
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { printSuccess, printError, printInfo } from '../utils/display.js';
|
|
2
|
+
import { startApiServer } from '../server/api.js';
|
|
3
|
+
|
|
4
|
+
export const serverStart = async (options) => {
|
|
5
|
+
const isJson = options.json;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
if (!isJson) {
|
|
9
|
+
printInfo('Starting local REST API server...');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const port = parseInt(options.port || '7070', 10);
|
|
13
|
+
await startApiServer(port);
|
|
14
|
+
|
|
15
|
+
if (isJson) {
|
|
16
|
+
console.log(JSON.stringify({ success: true, url: `http://127.0.0.1:${port}` }));
|
|
17
|
+
} else {
|
|
18
|
+
printSuccess(`Local API running at http://127.0.0.1:${port}`);
|
|
19
|
+
console.log(`
|
|
20
|
+
Endpoints:
|
|
21
|
+
GET /status — Get tunnel state
|
|
22
|
+
POST /connect — Start tunnel { "server": "user@host", "mode": "auto" }
|
|
23
|
+
POST /disconnect — Stop active tunnel
|
|
24
|
+
GET /peers — List WireGuard peers (requires active deploy)
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (isJson) {
|
|
29
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
30
|
+
} else {
|
|
31
|
+
printError('Failed to start local API', err);
|
|
32
|
+
}
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
};
|
package/src/commands/start.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import tls from 'tls';
|
|
3
|
-
import
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { printError, printSuccess, createSpinner, printInfo } from '../utils/display.js';
|
|
4
7
|
import { getPublicIp, getProxiedIp } from '../net/ip-check.js';
|
|
5
8
|
import { getProfiles } from '../core/profile-service.js';
|
|
6
9
|
import { getActiveTunnel, startTunnel } from '../core/tunnel-service.js';
|
|
10
|
+
import { CONFIG_DIR } from '../utils/config.js';
|
|
11
|
+
|
|
12
|
+
const WG_CONF = path.join(CONFIG_DIR, 'wg', 'wg0.conf');
|
|
7
13
|
|
|
8
14
|
const detectTlsServer = (host, port = 8443) => {
|
|
9
15
|
return new Promise((resolve) => {
|
|
@@ -28,6 +34,15 @@ const detectTlsServer = (host, port = 8443) => {
|
|
|
28
34
|
});
|
|
29
35
|
};
|
|
30
36
|
|
|
37
|
+
const hasWgQuick = () => {
|
|
38
|
+
try {
|
|
39
|
+
const res = spawnSync('which', ['wg-quick'], { encoding: 'utf-8' });
|
|
40
|
+
return res.status === 0;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
31
46
|
export default async (options) => {
|
|
32
47
|
const isJson = options.json;
|
|
33
48
|
let server = options.server;
|
|
@@ -55,7 +70,7 @@ export default async (options) => {
|
|
|
55
70
|
if (isJson) {
|
|
56
71
|
console.log(JSON.stringify({ error: 'Tunnel already running', pid: info.pid }));
|
|
57
72
|
} else {
|
|
58
|
-
printError(`Tunnel is already running
|
|
73
|
+
printError(`Tunnel is already running connected to ${info.server}. Run "polaris stop" first.`);
|
|
59
74
|
}
|
|
60
75
|
process.exitCode = 1;
|
|
61
76
|
return;
|
|
@@ -74,17 +89,21 @@ export default async (options) => {
|
|
|
74
89
|
let actualMode = requestedMode;
|
|
75
90
|
|
|
76
91
|
if (requestedMode === 'auto') {
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
spinner.start();
|
|
80
|
-
}
|
|
81
|
-
const hasTls = await detectTlsServer(hostPart, 8443);
|
|
82
|
-
if (hasTls) {
|
|
83
|
-
actualMode = 'tls';
|
|
92
|
+
if (fs.existsSync(WG_CONF) && hasWgQuick()) {
|
|
93
|
+
actualMode = 'wireguard';
|
|
84
94
|
} else {
|
|
85
|
-
actualMode = 'ssh';
|
|
86
95
|
if (!isJson) {
|
|
87
|
-
spinner.
|
|
96
|
+
spinner.text = 'Checking server for TLS support...';
|
|
97
|
+
spinner.start();
|
|
98
|
+
}
|
|
99
|
+
const hasTls = await detectTlsServer(hostPart, 8443);
|
|
100
|
+
if (hasTls) {
|
|
101
|
+
actualMode = 'tls';
|
|
102
|
+
} else {
|
|
103
|
+
actualMode = 'ssh';
|
|
104
|
+
if (!isJson) {
|
|
105
|
+
spinner.info('TLS server not detected. Falling back to SSH mode...');
|
|
106
|
+
}
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
109
|
}
|
|
@@ -94,25 +113,43 @@ export default async (options) => {
|
|
|
94
113
|
spinner.start();
|
|
95
114
|
}
|
|
96
115
|
|
|
97
|
-
const res = await startTunnel(server, port, actualMode);
|
|
98
|
-
|
|
99
|
-
if (!isJson) {
|
|
100
|
-
spinner.text = 'Verifying IP through proxy...';
|
|
101
|
-
}
|
|
116
|
+
const res = await startTunnel(server, port, actualMode, isJson);
|
|
102
117
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
if (actualMode === 'wireguard') {
|
|
119
|
+
if (!isJson) {
|
|
120
|
+
spinner.text = 'Waiting for interface to configure...';
|
|
121
|
+
}
|
|
122
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
123
|
+
|
|
124
|
+
if (!isJson) {
|
|
125
|
+
spinner.text = 'Verifying system-wide IP...';
|
|
126
|
+
}
|
|
127
|
+
const newIp = await getPublicIp();
|
|
111
128
|
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
if (!isJson) {
|
|
130
|
+
spinner.stop();
|
|
131
|
+
printSuccess(`WireGuard full tunnel established successfully to ${server}`);
|
|
132
|
+
console.log(`\n ${chalk.dim('Old IP:')} ${chalk.red(oldIp)}`);
|
|
133
|
+
console.log(` ${chalk.dim('New IP:')} ${chalk.green(newIp)}`);
|
|
134
|
+
console.log(` ${chalk.dim('Routing:')} System-wide (All OS traffic)`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(JSON.stringify({ success: true, oldIp, newIp, mode: 'wireguard', pid: res.pid }));
|
|
137
|
+
}
|
|
114
138
|
} else {
|
|
115
|
-
|
|
139
|
+
if (!isJson) {
|
|
140
|
+
spinner.text = 'Verifying IP through proxy...';
|
|
141
|
+
}
|
|
142
|
+
const newIp = await getProxiedIp(port);
|
|
143
|
+
|
|
144
|
+
if (!isJson) {
|
|
145
|
+
spinner.stop();
|
|
146
|
+
printSuccess(`Tunnel established successfully to ${server} (${actualMode.toUpperCase()} mode)`);
|
|
147
|
+
console.log(`\n ${chalk.dim('Old IP:')} ${chalk.red(oldIp)}`);
|
|
148
|
+
console.log(` ${chalk.dim('New IP:')} ${chalk.green(newIp)}`);
|
|
149
|
+
console.log(` ${chalk.dim('Proxy :')} ${chalk.cyan(`socks5://127.0.0.1:${port}`)}`);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(JSON.stringify({ success: true, oldIp, newIp, proxy: `socks5://127.0.0.1:${port}`, pid: res.pid, mode: actualMode }));
|
|
152
|
+
}
|
|
116
153
|
}
|
|
117
154
|
|
|
118
155
|
} catch (err) {
|
package/src/commands/stop.js
CHANGED
|
@@ -5,7 +5,7 @@ export default async (options) => {
|
|
|
5
5
|
const isJson = options.json;
|
|
6
6
|
|
|
7
7
|
try {
|
|
8
|
-
const info = stopActiveTunnel();
|
|
8
|
+
const info = stopActiveTunnel(isJson);
|
|
9
9
|
if (!info) {
|
|
10
10
|
if (isJson) {
|
|
11
11
|
console.log(JSON.stringify({ success: true, message: 'No active tunnel found.' }));
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { generateKeyPair } from '../tunnel/wg.js';
|
|
6
|
+
import { ensureDir, CONFIG_DIR } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
const getDefaultPrivateKey = () => {
|
|
9
|
+
const keys = ['id_rsa', 'id_ed25519', 'id_ecdsa', 'id_dsa'];
|
|
10
|
+
for (const k of keys) {
|
|
11
|
+
const p = path.join(os.homedir(), '.ssh', k);
|
|
12
|
+
if (fs.existsSync(p)) {
|
|
13
|
+
return fs.readFileSync(p);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const sshExec = (client, command) => {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
client.exec(command, (err, stream) => {
|
|
22
|
+
if (err) return reject(err);
|
|
23
|
+
let stdout = '';
|
|
24
|
+
let stderr = '';
|
|
25
|
+
stream.on('close', (code) => {
|
|
26
|
+
resolve({ code, stdout, stderr });
|
|
27
|
+
}).on('data', (data) => {
|
|
28
|
+
stdout += data.toString('utf8');
|
|
29
|
+
}).stderr.on('data', (data) => {
|
|
30
|
+
stderr += data.toString('utf8');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const deployServer = async (serverStr, options = {}) => {
|
|
37
|
+
const parts = serverStr.split('@');
|
|
38
|
+
const username = parts.length > 1 ? parts[0] : 'ubuntu';
|
|
39
|
+
const host = parts.length > 1 ? parts[1] : parts[0];
|
|
40
|
+
|
|
41
|
+
const privateKey = options.privateKey
|
|
42
|
+
? fs.readFileSync(options.privateKey)
|
|
43
|
+
: getDefaultPrivateKey();
|
|
44
|
+
|
|
45
|
+
if (!privateKey && !options.password) {
|
|
46
|
+
throw new Error('No SSH authentication method found. Please configure default SSH keys or specify key path/password.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 1. Generate local keys
|
|
50
|
+
const serverKeys = generateKeyPair();
|
|
51
|
+
const clientKeys = generateKeyPair();
|
|
52
|
+
|
|
53
|
+
const conn = new Client();
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
conn.on('ready', async () => {
|
|
57
|
+
try {
|
|
58
|
+
const onProgress = options.onProgress || (() => {});
|
|
59
|
+
|
|
60
|
+
// Step 1: Install packages
|
|
61
|
+
onProgress('Installing WireGuard and UFW on remote VPS...');
|
|
62
|
+
let res = await sshExec(conn, 'sudo apt-get update -y && sudo apt-get install -y wireguard ufw');
|
|
63
|
+
if (res.code !== 0) throw new Error(`Installation failed: ${res.stderr}`);
|
|
64
|
+
|
|
65
|
+
// Step 2: Enable packet forwarding
|
|
66
|
+
onProgress('Enabling IPv4 forwarding...');
|
|
67
|
+
res = await sshExec(conn, 'sudo sysctl -w net.ipv4.ip_forward=1 && echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf');
|
|
68
|
+
if (res.code !== 0) throw new Error(`IPv4 forwarding setup failed: ${res.stderr}`);
|
|
69
|
+
|
|
70
|
+
// Step 3: Detect default interface
|
|
71
|
+
onProgress('Detecting default interface...');
|
|
72
|
+
res = await sshExec(conn, "ip route show default | awk '/default/ {print $5}'");
|
|
73
|
+
const ethInterface = res.stdout.trim() || 'eth0';
|
|
74
|
+
|
|
75
|
+
// Step 4: Write server config
|
|
76
|
+
onProgress('Configuring WireGuard server wg0...');
|
|
77
|
+
const serverConf = `[Interface]
|
|
78
|
+
PrivateKey = ${serverKeys.privateKey}
|
|
79
|
+
Address = 10.0.0.1/24
|
|
80
|
+
ListenPort = 51820
|
|
81
|
+
PostUp = ufw route allow in on wg0; iptables -t nat -A POSTROUTING -o ${ethInterface} -j MASQUERADE
|
|
82
|
+
PostDown = ufw route delete allow in on wg0; iptables -t nat -D POSTROUTING -o ${ethInterface} -j MASQUERADE
|
|
83
|
+
|
|
84
|
+
[Peer]
|
|
85
|
+
PublicKey = ${clientKeys.publicKey}
|
|
86
|
+
AllowedIPs = 10.0.0.2/32
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
res = await sshExec(conn, `cat << 'EOF' > /tmp/wg0.conf\n${serverConf}\nEOF\nsudo mv /tmp/wg0.conf /etc/wireguard/wg0.conf && sudo chmod 600 /etc/wireguard/wg0.conf`);
|
|
90
|
+
if (res.code !== 0) throw new Error(`Server config write failed: ${res.stderr}`);
|
|
91
|
+
|
|
92
|
+
// Step 5: Start service
|
|
93
|
+
onProgress('Starting WireGuard interface...');
|
|
94
|
+
res = await sshExec(conn, 'sudo systemctl stop wg-quick@wg0 || true && sudo systemctl start wg-quick@wg0 && sudo systemctl enable wg-quick@wg0');
|
|
95
|
+
if (res.code !== 0) throw new Error(`Starting WireGuard failed: ${res.stderr}`);
|
|
96
|
+
|
|
97
|
+
// Step 6: Configure UFW firewall
|
|
98
|
+
onProgress('Configuring UFW firewall...');
|
|
99
|
+
res = await sshExec(conn, 'sudo ufw allow 51820/udp && sudo ufw allow 22/tcp && echo "y" | sudo ufw enable');
|
|
100
|
+
if (res.code !== 0) throw new Error(`Firewall setup failed: ${res.stderr}`);
|
|
101
|
+
|
|
102
|
+
// Step 7: Write client config locally
|
|
103
|
+
onProgress('Saving local client configuration...');
|
|
104
|
+
const clientConf = `[Interface]
|
|
105
|
+
PrivateKey = ${clientKeys.privateKey}
|
|
106
|
+
Address = 10.0.0.2/24
|
|
107
|
+
DNS = 1.1.1.1
|
|
108
|
+
|
|
109
|
+
[Peer]
|
|
110
|
+
PublicKey = ${serverKeys.publicKey}
|
|
111
|
+
Endpoint = ${host}:51820
|
|
112
|
+
AllowedIPs = 0.0.0.0/0
|
|
113
|
+
PersistentKeepalive = 25
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const wgDir = path.join(CONFIG_DIR, 'wg');
|
|
117
|
+
ensureDir(wgDir);
|
|
118
|
+
|
|
119
|
+
// Save client config
|
|
120
|
+
const clientConfPath = path.join(wgDir, 'wg0.conf');
|
|
121
|
+
fs.writeFileSync(clientConfPath, clientConf, 'utf-8');
|
|
122
|
+
|
|
123
|
+
// Also save provisioning info to config
|
|
124
|
+
fs.writeFileSync(path.join(wgDir, 'deploy.json'), JSON.stringify({
|
|
125
|
+
server: serverStr,
|
|
126
|
+
serverPublicKey: serverKeys.publicKey,
|
|
127
|
+
clientPublicKey: clientKeys.publicKey,
|
|
128
|
+
interface: ethInterface,
|
|
129
|
+
timestamp: new Date().toISOString()
|
|
130
|
+
}, null, 2), 'utf-8');
|
|
131
|
+
|
|
132
|
+
conn.end();
|
|
133
|
+
resolve({ clientConfPath, clientPublicKey: clientKeys.publicKey, serverPublicKey: serverKeys.publicKey });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
conn.end();
|
|
136
|
+
reject(err);
|
|
137
|
+
}
|
|
138
|
+
}).on('error', (err) => {
|
|
139
|
+
reject(err);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
conn.connect({
|
|
143
|
+
host,
|
|
144
|
+
port: options.port || 22,
|
|
145
|
+
username,
|
|
146
|
+
privateKey,
|
|
147
|
+
password: options.password
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { generateKeyPair } from '../tunnel/wg.js';
|
|
6
|
+
import { CONFIG_DIR, ensureDir } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
const DEPLOY_JSON = path.join(CONFIG_DIR, 'wg', 'deploy.json');
|
|
9
|
+
|
|
10
|
+
const getDeployInfo = () => {
|
|
11
|
+
if (!fs.existsSync(DEPLOY_JSON)) {
|
|
12
|
+
throw new Error('No deployment found. Please deploy a server first using "polaris deploy".');
|
|
13
|
+
}
|
|
14
|
+
return JSON.parse(fs.readFileSync(DEPLOY_JSON, 'utf-8'));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getDefaultPrivateKey = () => {
|
|
18
|
+
const keys = ['id_rsa', 'id_ed25519', 'id_ecdsa', 'id_dsa'];
|
|
19
|
+
for (const k of keys) {
|
|
20
|
+
const p = path.join(os.homedir(), '.ssh', k);
|
|
21
|
+
if (fs.existsSync(p)) {
|
|
22
|
+
return fs.readFileSync(p);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const sshConnect = (info) => {
|
|
29
|
+
const parts = info.server.split('@');
|
|
30
|
+
const username = parts.length > 1 ? parts[0] : 'ubuntu';
|
|
31
|
+
const host = parts.length > 1 ? parts[1] : parts[0];
|
|
32
|
+
|
|
33
|
+
const privateKey = getDefaultPrivateKey();
|
|
34
|
+
|
|
35
|
+
const conn = new Client();
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
conn.on('ready', () => resolve(conn))
|
|
38
|
+
.on('error', (err) => reject(err));
|
|
39
|
+
conn.connect({
|
|
40
|
+
host,
|
|
41
|
+
port: 22,
|
|
42
|
+
username,
|
|
43
|
+
privateKey
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const sshExec = (client, command) => {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
client.exec(command, (err, stream) => {
|
|
51
|
+
if (err) return reject(err);
|
|
52
|
+
let stdout = '';
|
|
53
|
+
let stderr = '';
|
|
54
|
+
stream.on('close', (code) => {
|
|
55
|
+
resolve({ code, stdout, stderr });
|
|
56
|
+
}).on('data', (data) => {
|
|
57
|
+
stdout += data.toString('utf8');
|
|
58
|
+
}).stderr.on('data', (data) => {
|
|
59
|
+
stderr += data.toString('utf8');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const addPeer = async (name) => {
|
|
66
|
+
const info = getDeployInfo();
|
|
67
|
+
const conn = await sshConnect(info);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// 1. Read remote wg0.conf
|
|
71
|
+
const catRes = await sshExec(conn, 'sudo cat /etc/wireguard/wg0.conf');
|
|
72
|
+
if (catRes.code !== 0) throw new Error(`Failed to read server config: ${catRes.stderr}`);
|
|
73
|
+
const configContent = catRes.stdout;
|
|
74
|
+
|
|
75
|
+
// Check if name already exists
|
|
76
|
+
if (configContent.includes(`# Name: ${name}\n`) || configContent.includes(`# Name: ${name}\r\n`)) {
|
|
77
|
+
throw new Error(`Peer with name '${name}' already exists.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Parse IPs to find next free IP
|
|
81
|
+
const ipMatches = [...configContent.matchAll(/AllowedIPs\s*=\s*10\.0\.0\.(\d+)\/32/g)];
|
|
82
|
+
let nextIpIndex = 2;
|
|
83
|
+
if (ipMatches.length > 0) {
|
|
84
|
+
const indices = ipMatches.map(m => parseInt(m[1], 10));
|
|
85
|
+
nextIpIndex = Math.max(...indices) + 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (nextIpIndex >= 254) {
|
|
89
|
+
throw new Error('IP range exhausted for subnet 10.0.0.0/24');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const peerIp = `10.0.0.${nextIpIndex}`;
|
|
93
|
+
const peerKeys = generateKeyPair();
|
|
94
|
+
|
|
95
|
+
// 2. Append peer to remote wg0.conf
|
|
96
|
+
const peerConfigBlock = `\n[Peer]
|
|
97
|
+
# Name: ${name}
|
|
98
|
+
PublicKey = ${peerKeys.publicKey}
|
|
99
|
+
AllowedIPs = ${peerIp}/32
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const writeCmd = `echo "${peerConfigBlock.replace(/"/g, '\\"')}" | sudo tee -a /etc/wireguard/wg0.conf`;
|
|
103
|
+
let res = await sshExec(conn, writeCmd);
|
|
104
|
+
if (res.code !== 0) throw new Error(`Failed to append remote peer: ${res.stderr}`);
|
|
105
|
+
|
|
106
|
+
// Sync remote wireguard
|
|
107
|
+
await sshExec(conn, 'sudo systemctl restart wg-quick@wg0');
|
|
108
|
+
|
|
109
|
+
// 3. Save peer config locally
|
|
110
|
+
const clientConf = `[Interface]
|
|
111
|
+
PrivateKey = ${peerKeys.privateKey}
|
|
112
|
+
Address = ${peerIp}/24
|
|
113
|
+
DNS = 1.1.1.1
|
|
114
|
+
|
|
115
|
+
[Peer]
|
|
116
|
+
PublicKey = ${info.serverPublicKey}
|
|
117
|
+
Endpoint = ${info.server.split('@')[1]}:51820
|
|
118
|
+
AllowedIPs = 0.0.0.0/0
|
|
119
|
+
PersistentKeepalive = 25
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const peersDir = path.join(CONFIG_DIR, 'wg', 'peers');
|
|
123
|
+
ensureDir(peersDir);
|
|
124
|
+
const peerConfPath = path.join(peersDir, `${name}.conf`);
|
|
125
|
+
fs.writeFileSync(peerConfPath, clientConf, 'utf-8');
|
|
126
|
+
|
|
127
|
+
conn.end();
|
|
128
|
+
return { name, ip: peerIp, publicKey: peerKeys.publicKey, confPath: peerConfPath };
|
|
129
|
+
} catch (err) {
|
|
130
|
+
conn.end();
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const listPeers = async () => {
|
|
136
|
+
const info = getDeployInfo();
|
|
137
|
+
const conn = await sshConnect(info);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// 1. Read remote configuration to parse names
|
|
141
|
+
const catRes = await sshExec(conn, 'sudo cat /etc/wireguard/wg0.conf');
|
|
142
|
+
if (catRes.code !== 0) throw new Error('Failed to read remote config');
|
|
143
|
+
const content = catRes.stdout;
|
|
144
|
+
|
|
145
|
+
// Parse blocks
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
const peersMap = {}; // publicKey -> name
|
|
148
|
+
let currentName = 'Default Client';
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const line = lines[i].trim();
|
|
152
|
+
if (line.startsWith('# Name:')) {
|
|
153
|
+
currentName = line.substring(7).trim();
|
|
154
|
+
}
|
|
155
|
+
if (line.startsWith('PublicKey')) {
|
|
156
|
+
const pk = line.split('=')[1].trim();
|
|
157
|
+
peersMap[pk] = currentName;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 2. Fetch wg status
|
|
162
|
+
const showRes = await sshExec(conn, 'sudo wg show wg0 dump');
|
|
163
|
+
if (showRes.code !== 0) throw new Error('Failed to run wg show');
|
|
164
|
+
|
|
165
|
+
const dumpLines = showRes.stdout.trim().split('\n');
|
|
166
|
+
const list = [];
|
|
167
|
+
|
|
168
|
+
// First line is server key info, subsequent lines are peers
|
|
169
|
+
for (let i = 1; i < dumpLines.length; i++) {
|
|
170
|
+
const parts = dumpLines[i].split('\t');
|
|
171
|
+
if (parts.length < 8) continue;
|
|
172
|
+
const [publicKey, , endpoint, allowedIps, latestHandshake, transferRx, transferTx] = parts;
|
|
173
|
+
|
|
174
|
+
list.push({
|
|
175
|
+
name: peersMap[publicKey] || 'Unknown',
|
|
176
|
+
publicKey,
|
|
177
|
+
endpoint: endpoint === '(none)' ? 'Disconnected' : endpoint,
|
|
178
|
+
ip: allowedIps,
|
|
179
|
+
handshake: parseInt(latestHandshake, 10) === 0 ? 'Never' : `${Math.floor(Date.now() / 1000 - parseInt(latestHandshake, 10))}s ago`,
|
|
180
|
+
transferRx: `${Math.round(parseInt(transferRx, 10) / 1024 / 1024 * 100) / 100} MB`,
|
|
181
|
+
transferTx: `${Math.round(parseInt(transferTx, 10) / 1024 / 1024 * 100) / 100} MB`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
conn.end();
|
|
186
|
+
return list;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
conn.end();
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const removePeer = async (name) => {
|
|
194
|
+
const info = getDeployInfo();
|
|
195
|
+
const conn = await sshConnect(info);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const catRes = await sshExec(conn, 'sudo cat /etc/wireguard/wg0.conf');
|
|
199
|
+
if (catRes.code !== 0) throw new Error('Failed to read remote config');
|
|
200
|
+
const content = catRes.stdout;
|
|
201
|
+
|
|
202
|
+
if (!content.includes(`# Name: ${name}`)) {
|
|
203
|
+
throw new Error(`Peer '${name}' not found on server.`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Strip peer block from remote config
|
|
207
|
+
const blocks = content.split('[Peer]');
|
|
208
|
+
const filteredBlocks = blocks.filter(b => !b.includes(`# Name: ${name}`));
|
|
209
|
+
const newContent = filteredBlocks.join('[Peer]');
|
|
210
|
+
|
|
211
|
+
// Save remote config back
|
|
212
|
+
const writeRes = await sshExec(conn, `cat << 'EOF' > /tmp/wg0.conf\n${newContent}\nEOF\nsudo mv /tmp/wg0.conf /etc/wireguard/wg0.conf && sudo chmod 600 /etc/wireguard/wg0.conf`);
|
|
213
|
+
if (writeRes.code !== 0) throw new Error('Failed to update remote config');
|
|
214
|
+
|
|
215
|
+
// Sync remote wireguard
|
|
216
|
+
await sshExec(conn, 'sudo systemctl restart wg-quick@wg0');
|
|
217
|
+
|
|
218
|
+
// Remove local peer config if it exists
|
|
219
|
+
const peerConfPath = path.join(CONFIG_DIR, 'wg', 'peers', `${name}.conf`);
|
|
220
|
+
if (fs.existsSync(peerConfPath)) {
|
|
221
|
+
fs.unlinkSync(peerConfPath);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
conn.end();
|
|
225
|
+
return true;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
conn.end();
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const getLocalPeerConfPath = (name) => {
|
|
233
|
+
const peerConfPath = path.join(CONFIG_DIR, 'wg', 'peers', `${name}.conf`);
|
|
234
|
+
if (!fs.existsSync(peerConfPath)) {
|
|
235
|
+
throw new Error(`Local config for peer '${name}' not found.`);
|
|
236
|
+
}
|
|
237
|
+
return peerConfPath;
|
|
238
|
+
};
|
|
@@ -1,26 +1,74 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
1
4
|
import { spawnSSH, waitForSocks } from '../tunnel/ssh.js';
|
|
2
5
|
import { startTlsBackground } from '../tunnel/tls.js';
|
|
6
|
+
import { startWgTunnel, stopWgTunnel } from '../tunnel/wg.js';
|
|
7
|
+
import { enableKillSwitch, disableKillSwitch } from '../utils/kill-switch.js';
|
|
3
8
|
import { loadDaemonState, clearDaemonState, killPid, saveDaemonState } from '../utils/daemon.js';
|
|
4
|
-
import { TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, ensureDir } from '../utils/config.js';
|
|
9
|
+
import { TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, CONFIG_DIR, ensureDir } from '../utils/config.js';
|
|
10
|
+
|
|
11
|
+
const WG_CONF = path.join(CONFIG_DIR, 'wg', 'wg0.conf');
|
|
5
12
|
|
|
6
13
|
export const getActiveTunnel = () => {
|
|
7
|
-
|
|
14
|
+
const info = loadDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
|
|
15
|
+
if (!info) return null;
|
|
16
|
+
|
|
17
|
+
// For WireGuard, PID file tracks start process, but we verify interface status
|
|
18
|
+
if (info.mode === 'wireguard') {
|
|
19
|
+
try {
|
|
20
|
+
const showRes = spawnSync('sudo', ['wg', 'show'], { encoding: 'utf-8' });
|
|
21
|
+
if (showRes.status !== 0 || !showRes.stdout.includes('interface:')) {
|
|
22
|
+
clearDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
clearDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return info;
|
|
8
31
|
};
|
|
9
32
|
|
|
10
|
-
export const stopActiveTunnel = () => {
|
|
33
|
+
export const stopActiveTunnel = (isJson = false) => {
|
|
11
34
|
const info = getActiveTunnel();
|
|
12
35
|
if (info) {
|
|
13
|
-
|
|
36
|
+
if (info.mode === 'wireguard') {
|
|
37
|
+
stopWgTunnel(WG_CONF, isJson);
|
|
38
|
+
disableKillSwitch(info.server);
|
|
39
|
+
} else {
|
|
40
|
+
killPid(info.pid);
|
|
41
|
+
}
|
|
14
42
|
}
|
|
15
43
|
clearDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
|
|
16
44
|
return info;
|
|
17
45
|
};
|
|
18
46
|
|
|
19
|
-
export const startTunnel = async (server, port, mode = 'ssh') => {
|
|
47
|
+
export const startTunnel = async (server, port, mode = 'ssh', isJson = false) => {
|
|
20
48
|
const hostPart = server.includes('@') ? server.split('@')[1] : server;
|
|
21
49
|
ensureDir();
|
|
22
50
|
|
|
23
51
|
let pid;
|
|
52
|
+
if (mode === 'wireguard') {
|
|
53
|
+
if (!fs.existsSync(WG_CONF)) {
|
|
54
|
+
throw new Error(`WireGuard client configuration not found at ${WG_CONF}. Please provision the server first with "polaris deploy".`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pid = startWgTunnel(WG_CONF, isJson);
|
|
58
|
+
|
|
59
|
+
// Save state before enableKillSwitch in case sudo prompt needs to block
|
|
60
|
+
saveDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, {
|
|
61
|
+
pid,
|
|
62
|
+
server,
|
|
63
|
+
port: 0,
|
|
64
|
+
mode: 'wireguard',
|
|
65
|
+
startTime: new Date().toISOString()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
enableKillSwitch(server);
|
|
69
|
+
return { pid, server, port: 0, mode: 'wireguard' };
|
|
70
|
+
}
|
|
71
|
+
|
|
24
72
|
if (mode === 'tls') {
|
|
25
73
|
pid = startTlsBackground(port, hostPart, 8443);
|
|
26
74
|
} else {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { getActiveTunnel, startTunnel, stopActiveTunnel } from '../core/tunnel-service.js';
|
|
3
|
+
import { listPeers } from '../core/peer-service.js';
|
|
4
|
+
import { getProfiles } from '../core/profile-service.js';
|
|
5
|
+
|
|
6
|
+
export const startApiServer = (port = 7070) => {
|
|
7
|
+
const app = express();
|
|
8
|
+
app.use(express.json());
|
|
9
|
+
|
|
10
|
+
app.get('/status', (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const info = getActiveTunnel();
|
|
13
|
+
if (!info) {
|
|
14
|
+
return res.json({ status: 'disconnected' });
|
|
15
|
+
}
|
|
16
|
+
res.json({ status: 'connected', ...info });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
res.status(500).json({ error: err.message });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.post('/connect', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { server, mode, proxyPort } = req.body;
|
|
25
|
+
let targetServer = server;
|
|
26
|
+
|
|
27
|
+
if (!targetServer) {
|
|
28
|
+
const { profiles, active } = getProfiles();
|
|
29
|
+
if (active && profiles[active]) {
|
|
30
|
+
targetServer = profiles[active];
|
|
31
|
+
} else {
|
|
32
|
+
return res.status(400).json({ error: 'No server specified and no active profile found.' });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const info = getActiveTunnel();
|
|
37
|
+
if (info) {
|
|
38
|
+
return res.status(400).json({ error: 'Tunnel is already active.', current: info });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pPort = proxyPort || 1080;
|
|
42
|
+
const tMode = mode || 'auto';
|
|
43
|
+
|
|
44
|
+
const result = await startTunnel(targetServer, pPort, tMode, true);
|
|
45
|
+
res.json({ status: 'connected', ...result });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
res.status(500).json({ error: err.message });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.post('/disconnect', (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const info = stopActiveTunnel(true);
|
|
54
|
+
if (!info) {
|
|
55
|
+
return res.json({ status: 'disconnected', message: 'No active tunnel.' });
|
|
56
|
+
}
|
|
57
|
+
res.json({ status: 'disconnected', message: 'Tunnel stopped successfully.' });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
res.status(500).json({ error: err.message });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
app.get('/peers', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const peers = await listPeers();
|
|
66
|
+
res.json({ success: true, peers });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
res.status(500).json({ error: err.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const server = app.listen(port, '127.0.0.1', () => {
|
|
74
|
+
resolve(server);
|
|
75
|
+
}).on('error', reject);
|
|
76
|
+
});
|
|
77
|
+
};
|
package/src/tunnel/wg.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawnSync, spawn } from 'child_process';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
// Pure JS Curve25519/X25519 key generator matching WireGuard format
|
|
5
|
+
export const generateKeyPair = () => {
|
|
6
|
+
const rawPriv = crypto.randomBytes(32);
|
|
7
|
+
// Apply Curve25519 private key clamping:
|
|
8
|
+
// - clear the lowest three bits of the first byte
|
|
9
|
+
// - clear the highest bit of the last byte
|
|
10
|
+
// - set the second highest bit of the last byte
|
|
11
|
+
rawPriv[0] &= 248;
|
|
12
|
+
rawPriv[31] &= 127;
|
|
13
|
+
rawPriv[31] |= 64;
|
|
14
|
+
|
|
15
|
+
const keyDer = Buffer.concat([
|
|
16
|
+
Buffer.from('302e020100300506032b656e04220420', 'hex'),
|
|
17
|
+
rawPriv
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const priv = crypto.createPrivateKey({ key: keyDer, format: 'der', type: 'pkcs8' });
|
|
21
|
+
const pub = crypto.createPublicKey(priv);
|
|
22
|
+
const pubRaw = pub.export({ format: 'der', type: 'spki' }).slice(-32);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
privateKey: rawPriv.toString('base64'),
|
|
26
|
+
publicKey: pubRaw.toString('base64')
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const startWgTunnel = (confPath, isJson = false) => {
|
|
31
|
+
const sudoCheck = spawnSync('sudo', ['-n', 'true']);
|
|
32
|
+
if (sudoCheck.status !== 0 && isJson) {
|
|
33
|
+
throw new Error('Sudo privileges required. Please run with "sudo polaris start ..." for JSON mode.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const res = spawnSync('sudo', ['wg-quick', 'up', confPath], {
|
|
37
|
+
stdio: isJson ? 'ignore' : 'inherit'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (res.status !== 0) {
|
|
41
|
+
throw new Error(`wg-quick up failed with code ${res.status}`);
|
|
42
|
+
}
|
|
43
|
+
return process.pid;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const stopWgTunnel = (confPath, isJson = false) => {
|
|
47
|
+
const sudoCheck = spawnSync('sudo', ['-n', 'true']);
|
|
48
|
+
if (sudoCheck.status !== 0 && isJson) {
|
|
49
|
+
throw new Error('Sudo privileges required. Please run with "sudo polaris stop" for JSON mode.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const res = spawnSync('sudo', ['wg-quick', 'down', confPath], {
|
|
53
|
+
stdio: isJson ? 'ignore' : 'inherit'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (res.status !== 0) {
|
|
57
|
+
throw new Error(`wg-quick down failed with code ${res.status}`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { store } from './config.js';
|
|
5
|
+
|
|
6
|
+
export const isKillSwitchConfigured = () => {
|
|
7
|
+
return store.get('kill-switch', false);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const setKillSwitchConfig = (enabled) => {
|
|
11
|
+
store.set('kill-switch', enabled);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const enableKillSwitch = (serverIp) => {
|
|
15
|
+
if (!isKillSwitchConfigured()) return;
|
|
16
|
+
|
|
17
|
+
const platform = os.platform();
|
|
18
|
+
const hostIp = serverIp.includes('@') ? serverIp.split('@')[1] : serverIp;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (platform === 'linux') {
|
|
22
|
+
// 1. Loopback
|
|
23
|
+
execSync('sudo iptables -C OUTPUT -o lo -j ACCEPT || sudo iptables -A OUTPUT -o lo -j ACCEPT', { stdio: 'ignore' });
|
|
24
|
+
// 2. WireGuard interface (default wg0)
|
|
25
|
+
execSync('sudo iptables -C OUTPUT -o wg0 -j ACCEPT || sudo iptables -A OUTPUT -o wg0 -j ACCEPT', { stdio: 'ignore' });
|
|
26
|
+
// 3. Connect to the VPS server
|
|
27
|
+
execSync(`sudo iptables -C OUTPUT -d ${hostIp} -j ACCEPT || sudo iptables -A OUTPUT -d ${hostIp} -j ACCEPT`, { stdio: 'ignore' });
|
|
28
|
+
// 4. Drop other outbound connections
|
|
29
|
+
execSync('sudo iptables -C OUTPUT -j REJECT || sudo iptables -A OUTPUT -j REJECT', { stdio: 'ignore' });
|
|
30
|
+
} else if (platform === 'darwin') {
|
|
31
|
+
// macOS Packet Filter (PF) setup
|
|
32
|
+
const pfRules = `
|
|
33
|
+
block out all
|
|
34
|
+
pass out on lo0 all
|
|
35
|
+
pass out on utun all
|
|
36
|
+
pass out proto {tcp, udp} to ${hostIp}
|
|
37
|
+
`;
|
|
38
|
+
fs.writeFileSync('/tmp/polaris_pf.conf', pfRules, 'utf-8');
|
|
39
|
+
execSync('sudo pfctl -ef /tmp/polaris_pf.conf', { stdio: 'ignore' });
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('Failed to enable kill switch rules:', err.message);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const disableKillSwitch = (serverIp) => {
|
|
47
|
+
const platform = os.platform();
|
|
48
|
+
const hostIp = serverIp.includes('@') ? serverIp.split('@')[1] : serverIp;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (platform === 'linux') {
|
|
52
|
+
execSync('sudo iptables -D OUTPUT -j REJECT', { stdio: 'ignore' });
|
|
53
|
+
execSync(`sudo iptables -D OUTPUT -d ${hostIp} -j ACCEPT`, { stdio: 'ignore' });
|
|
54
|
+
execSync('sudo iptables -D OUTPUT -o wg0 -j ACCEPT', { stdio: 'ignore' });
|
|
55
|
+
execSync('sudo iptables -D OUTPUT -o lo -j ACCEPT', { stdio: 'ignore' });
|
|
56
|
+
} else if (platform === 'darwin') {
|
|
57
|
+
execSync('sudo pfctl -d', { stdio: 'ignore' });
|
|
58
|
+
// Reload default system rules
|
|
59
|
+
if (fs.existsSync('/etc/pf.conf')) {
|
|
60
|
+
execSync('sudo pfctl -f /etc/pf.conf', { stdio: 'ignore' });
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync('/tmp/polaris_pf.conf')) {
|
|
63
|
+
fs.unlinkSync('/tmp/polaris_pf.conf');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Ignore cleanup errors
|
|
68
|
+
}
|
|
69
|
+
};
|