polaris-vpn 0.4.0 → 0.5.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 +7 -0
- package/README.md +2 -1
- package/package.json +2 -1
- package/src/cli.js +21 -1
- package/src/commands/status.js +95 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.5.0] - 2026-07-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Server Health Dashboard**: `polaris status --full` now queries GeoIP data, tests ping latency directly to the server, and retrieves live WireGuard rx/tx data limits.
|
|
13
|
+
- **Auto-Updater**: Introduced `polaris update` and background update notifications via `update-notifier`.
|
|
14
|
+
|
|
8
15
|
## [0.4.0] - 2026-07-12
|
|
9
16
|
|
|
10
17
|
### Added
|
package/README.md
CHANGED
|
@@ -45,13 +45,14 @@ polaris stop
|
|
|
45
45
|
| --- | --- |
|
|
46
46
|
| `polaris start --server user@host [--port 1080]` | Start the encrypted SSH tunnel. |
|
|
47
47
|
| `polaris stop` | Stop the active tunnel. |
|
|
48
|
-
| `polaris status` | Show current tunnel status,
|
|
48
|
+
| `polaris status [--full]` | Show current tunnel status. `--full` shows GeoIP, ping latency, and WG stats. |
|
|
49
49
|
| `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
|
|
50
50
|
| `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
|
|
51
51
|
| `polaris add <alias> --server user@host` | Save a server profile for quick access. |
|
|
52
52
|
| `polaris list` | List all saved server profiles. |
|
|
53
53
|
| `polaris use <alias>` | Set a saved profile as the active default. |
|
|
54
54
|
| `polaris server start` | Start a local REST API on `127.0.0.1:7070`. |
|
|
55
|
+
| `polaris update` | Update `polaris-vpn` to the latest version via npm. |
|
|
55
56
|
|
|
56
57
|
> All commands support the `--json` flag for machine-readable output.
|
|
57
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polaris-vpn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"qrcode-terminal": "^0.12.0",
|
|
45
45
|
"socks-proxy-agent": "^10.1.0",
|
|
46
46
|
"ssh2": "^1.17.0",
|
|
47
|
+
"update-notifier": "^7.3.1",
|
|
47
48
|
"wireguard-tools": "^0.1.0"
|
|
48
49
|
}
|
|
49
50
|
}
|
package/src/cli.js
CHANGED
|
@@ -3,12 +3,17 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import
|
|
6
|
+
import updateNotifier from 'update-notifier';
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import { printBanner, printError, printSuccess } from './utils/display.js';
|
|
9
|
+
import chalk from 'chalk';
|
|
7
10
|
|
|
8
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
12
|
const __dirname = path.dirname(__filename);
|
|
10
13
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
11
14
|
|
|
15
|
+
updateNotifier({ pkg }).notify();
|
|
16
|
+
|
|
12
17
|
const program = new Command();
|
|
13
18
|
|
|
14
19
|
program
|
|
@@ -20,6 +25,20 @@ program
|
|
|
20
25
|
// Helper to check if we should print the banner
|
|
21
26
|
const willPrintJson = () => process.argv.includes('--json');
|
|
22
27
|
|
|
28
|
+
program
|
|
29
|
+
.command('update')
|
|
30
|
+
.description('Update polaris-vpn to the latest version')
|
|
31
|
+
.action(() => {
|
|
32
|
+
console.log(chalk.cyan('Updating polaris-vpn...'));
|
|
33
|
+
const res = spawnSync('npm', ['install', '-g', 'polaris-vpn@latest'], { stdio: 'inherit' });
|
|
34
|
+
if (res.status === 0) {
|
|
35
|
+
printSuccess('Successfully updated to the latest version!');
|
|
36
|
+
} else {
|
|
37
|
+
printError('Failed to update polaris-vpn.');
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
23
42
|
program
|
|
24
43
|
.command('start')
|
|
25
44
|
.description('Start the encrypted SSH, TLS or WireGuard tunnel')
|
|
@@ -54,6 +73,7 @@ program
|
|
|
54
73
|
program
|
|
55
74
|
.command('status')
|
|
56
75
|
.description('Show current tunnel status, IP, and uptime')
|
|
76
|
+
.option('--full', 'Show GeoIP, latency, and full data usage stats')
|
|
57
77
|
.action(async (options, cmd) => {
|
|
58
78
|
if (!cmd.optsWithGlobals().json) printBanner();
|
|
59
79
|
try {
|
package/src/commands/status.js
CHANGED
|
@@ -2,9 +2,64 @@ import { getActiveTunnel } from '../core/tunnel-service.js';
|
|
|
2
2
|
import { getProxiedIp } from '../net/ip-check.js';
|
|
3
3
|
import { createTable, createSpinner, printError } from '../utils/display.js';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
const pingServer = (ip) => {
|
|
10
|
+
const isWin = os.platform() === 'win32';
|
|
11
|
+
const args = isWin ? ['-n', '1', '-w', '2000', ip] : ['-c', '1', '-W', '2', ip];
|
|
12
|
+
const res = spawnSync('ping', args, { encoding: 'utf-8' });
|
|
13
|
+
if (res.status === 0) {
|
|
14
|
+
if (isWin) {
|
|
15
|
+
const match = res.stdout.match(/Average = (\d+)ms/);
|
|
16
|
+
if (match) return `${match[1]} ms`;
|
|
17
|
+
} else {
|
|
18
|
+
const match = res.stdout.match(/time=([\d.]+)\s*ms/);
|
|
19
|
+
if (match) return `${match[1]} ms`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return 'Timeout';
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getGeoIp = async (ip) => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`http://ip-api.com/json/${ip}`);
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (data.status === 'success') {
|
|
30
|
+
return `${data.city}, ${data.country} (${data.isp})`;
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {}
|
|
33
|
+
return 'Unknown';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const formatBytes = (bytes) => {
|
|
37
|
+
if (bytes === 0) return '0 B';
|
|
38
|
+
const k = 1024;
|
|
39
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
40
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
41
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getWgStats = () => {
|
|
45
|
+
const dumpRes = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
46
|
+
if (dumpRes.status !== 0) return null;
|
|
47
|
+
const lines = dumpRes.stdout.trim().split('\n');
|
|
48
|
+
if (lines.length <= 1) return null;
|
|
49
|
+
|
|
50
|
+
let totalRx = 0;
|
|
51
|
+
let totalTx = 0;
|
|
52
|
+
for (let i = 1; i < lines.length; i++) {
|
|
53
|
+
const peerInfo = lines[i].split('\t');
|
|
54
|
+
totalRx += parseInt(peerInfo[6], 10) || 0;
|
|
55
|
+
totalTx += parseInt(peerInfo[7], 10) || 0;
|
|
56
|
+
}
|
|
57
|
+
return { totalRx, totalTx };
|
|
58
|
+
};
|
|
5
59
|
|
|
6
60
|
export default async (options) => {
|
|
7
61
|
const isJson = options.json;
|
|
62
|
+
const isFull = options.full;
|
|
8
63
|
const info = getActiveTunnel();
|
|
9
64
|
|
|
10
65
|
if (!info) {
|
|
@@ -23,9 +78,33 @@ export default async (options) => {
|
|
|
23
78
|
currentIp = await getProxiedIp(info.port);
|
|
24
79
|
if (spinner) spinner.stop();
|
|
25
80
|
|
|
81
|
+
let geo = 'N/A';
|
|
82
|
+
let latency = 'N/A';
|
|
83
|
+
let dataUsage = 'N/A';
|
|
84
|
+
|
|
85
|
+
if (isFull) {
|
|
86
|
+
if (spinner) {
|
|
87
|
+
spinner.text = 'Fetching full health data...';
|
|
88
|
+
spinner.start();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const serverIp = info.server.split('@').pop();
|
|
92
|
+
latency = pingServer(serverIp);
|
|
93
|
+
geo = await getGeoIp(currentIp);
|
|
94
|
+
|
|
95
|
+
if (info.mode === 'wireguard') {
|
|
96
|
+
const stats = getWgStats();
|
|
97
|
+
if (stats) {
|
|
98
|
+
dataUsage = `${formatBytes(stats.totalRx)} / ${formatBytes(stats.totalTx)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (spinner) spinner.stop();
|
|
103
|
+
}
|
|
104
|
+
|
|
26
105
|
if (isJson) {
|
|
27
106
|
const uptimeMs = Date.now() - new Date(info.startTime).getTime();
|
|
28
|
-
|
|
107
|
+
const payload = {
|
|
29
108
|
status: 'up',
|
|
30
109
|
server: info.server,
|
|
31
110
|
port: info.port,
|
|
@@ -33,7 +112,13 @@ export default async (options) => {
|
|
|
33
112
|
mode: info.mode || 'ssh',
|
|
34
113
|
ip: currentIp,
|
|
35
114
|
uptimeMs
|
|
36
|
-
}
|
|
115
|
+
};
|
|
116
|
+
if (isFull) {
|
|
117
|
+
payload.geo = geo;
|
|
118
|
+
payload.latency = latency;
|
|
119
|
+
payload.dataUsage = dataUsage;
|
|
120
|
+
}
|
|
121
|
+
console.log(JSON.stringify(payload));
|
|
37
122
|
} else {
|
|
38
123
|
console.log(chalk.green.bold('\n● Tunnel is UP\n'));
|
|
39
124
|
|
|
@@ -49,6 +134,14 @@ export default async (options) => {
|
|
|
49
134
|
['Uptime', `${uptimeMin} minutes`],
|
|
50
135
|
['PID', info.pid.toString()]
|
|
51
136
|
);
|
|
137
|
+
|
|
138
|
+
if (isFull) {
|
|
139
|
+
table.push(
|
|
140
|
+
['Geo Location', geo],
|
|
141
|
+
['Latency (Ping)', latency],
|
|
142
|
+
['Data (Rx/Tx)', dataUsage]
|
|
143
|
+
);
|
|
144
|
+
}
|
|
52
145
|
|
|
53
146
|
console.log(table.toString());
|
|
54
147
|
console.log('');
|