polaris-vpn 0.3.1 → 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 +5 -14
- package/README.md +2 -0
- package/package.json +2 -1
- package/src/cli.js +19 -0
- package/src/commands/monitor.js +120 -0
- package/src/commands/server.js +35 -0
- package/src/server/api.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,23 +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
9
|
|
|
10
|
-
###
|
|
11
|
-
- Gracefully handled interactive `sudo` password prompts during WireGuard configuration when running in `--json` mode.
|
|
12
|
-
- Improved robust error handling for `wg-quick` start/stop routines.
|
|
10
|
+
### Added
|
|
13
11
|
|
|
14
|
-
|
|
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.
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
- Automated system-wide VPN deployment command (`polaris deploy --server user@host`) for Ubuntu 22.04.
|
|
18
|
-
- Peer command namespace (`polaris peer add/list/remove/qr`) to manage multiple WireGuard client connections.
|
|
19
|
-
- Terminal QR code generation (`polaris peer qr <name>`) using `qrcode-terminal` for mobile client pairing.
|
|
20
|
-
- OS-level Network Kill Switch (`polaris config set kill-switch true`) for Linux (`iptables`) and macOS (`pfctl`) interfaces.
|
|
21
|
-
- Pure JavaScript base64 Curve25519/X25519 key derivation helper using standard Node.js `crypto` (no local binary `wg` CLI command dependency).
|
|
22
|
-
- Integrated smart auto-detection mode fallback sequence (`wireguard` -> `tls` -> `ssh`) for client tunnel startup.
|
|
23
|
-
|
|
24
|
-
## [0.2.0] - 2026-06-28
|
|
15
|
+
## [0.3.1] - 2026-06-28
|
|
25
16
|
|
|
26
17
|
### Added
|
|
27
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,6 +37,7 @@
|
|
|
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",
|
package/src/cli.js
CHANGED
|
@@ -269,6 +269,25 @@ configCmd
|
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
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
|
+
|
|
272
291
|
program.parseAsync(process.argv).catch(err => {
|
|
273
292
|
if (!willPrintJson()) printError('Fatal error', err);
|
|
274
293
|
process.exit(1);
|
|
@@ -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,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
|
+
};
|
|
@@ -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
|
+
};
|