polaris-vpn 0.4.0 → 0.6.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 CHANGED
@@ -5,6 +5,19 @@ 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.6.0] - 2026-07-12
9
+
10
+ ### Added
11
+
12
+ - **First-Class Windows Support**: Polaris now natively binds to `wireguard.exe` NT Services on Windows, bypassing the need for `wg-quick` and `sudo`. This makes Polaris fully cross-platform.
13
+
14
+ ## [0.5.0] - 2026-07-12
15
+
16
+ ### Added
17
+
18
+ - **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.
19
+ - **Auto-Updater**: Introduced `polaris update` and background update notifications via `update-notifier`.
20
+
8
21
  ## [0.4.0] - 2026-07-12
9
22
 
10
23
  ### Added
package/README.md CHANGED
@@ -4,10 +4,12 @@
4
4
 
5
5
  Your True North in Digital Privacy. `polaris` is a production-quality, open-source, self-hosted VPN CLI tool. It wraps SSH dynamic port forwarding into a beautiful CLI experience.
6
6
 
7
- ## Prerequisites
7
+ ### Prerequisites
8
8
 
9
- - SSH access to any VPS (Virtual Private Server).
10
- - Node.js >= 18
9
+ - Node.js (v18+)
10
+ - SSH access to a remote Linux VPS (Ubuntu/Debian recommended)
11
+ - **Linux/macOS**: `sudo` privileges for configuring WireGuard and network routes locally.
12
+ - **Windows**: The official [WireGuard for Windows](https://www.wireguard.com/install/) client must be installed, and `polaris` must be run from an Administrator prompt.
11
13
 
12
14
  We highly recommend **Oracle Cloud Free Tier** for your VPS.
13
15
 
@@ -45,13 +47,14 @@ polaris stop
45
47
  | --- | --- |
46
48
  | `polaris start --server user@host [--port 1080]` | Start the encrypted SSH tunnel. |
47
49
  | `polaris stop` | Stop the active tunnel. |
48
- | `polaris status` | Show current tunnel status, IP, and uptime. |
50
+ | `polaris status [--full]` | Show current tunnel status. `--full` shows GeoIP, ping latency, and WG stats. |
49
51
  | `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
50
52
  | `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
51
53
  | `polaris add <alias> --server user@host` | Save a server profile for quick access. |
52
54
  | `polaris list` | List all saved server profiles. |
53
55
  | `polaris use <alias>` | Set a saved profile as the active default. |
54
56
  | `polaris server start` | Start a local REST API on `127.0.0.1:7070`. |
57
+ | `polaris update` | Update `polaris-vpn` to the latest version via npm. |
55
58
 
56
59
  > All commands support the `--json` flag for machine-readable output.
57
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "0.4.0",
3
+ "version": "0.6.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 { printBanner, printError } from './utils/display.js';
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 {
@@ -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
- console.log(JSON.stringify({
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('');
package/src/tunnel/wg.js CHANGED
@@ -27,10 +27,27 @@ export const generateKeyPair = () => {
27
27
  };
28
28
  };
29
29
 
30
+ import os from 'os';
31
+ import path from 'path';
32
+
30
33
  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
+ const isWin = os.platform() === 'win32';
35
+
36
+ if (!isWin) {
37
+ const sudoCheck = spawnSync('sudo', ['-n', 'true']);
38
+ if (sudoCheck.status !== 0 && isJson) {
39
+ throw new Error('Sudo privileges required. Please run with "sudo polaris start ..." for JSON mode.');
40
+ }
41
+ }
42
+
43
+ if (isWin) {
44
+ const res = spawnSync('wireguard', ['/installtunnelservice', confPath], {
45
+ stdio: isJson ? 'ignore' : 'inherit'
46
+ });
47
+ if (res.status !== 0) {
48
+ throw new Error(`wireguard /installtunnelservice failed (ensure Admin privileges) with code ${res.status}`);
49
+ }
50
+ return process.pid;
34
51
  }
35
52
 
36
53
  const res = spawnSync('sudo', ['wg-quick', 'up', confPath], {
@@ -44,9 +61,24 @@ export const startWgTunnel = (confPath, isJson = false) => {
44
61
  };
45
62
 
46
63
  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.');
64
+ const isWin = os.platform() === 'win32';
65
+
66
+ if (!isWin) {
67
+ const sudoCheck = spawnSync('sudo', ['-n', 'true']);
68
+ if (sudoCheck.status !== 0 && isJson) {
69
+ throw new Error('Sudo privileges required. Please run with "sudo polaris stop" for JSON mode.');
70
+ }
71
+ }
72
+
73
+ if (isWin) {
74
+ const interfaceName = path.parse(confPath).name;
75
+ const res = spawnSync('wireguard', ['/uninstalltunnelservice', interfaceName], {
76
+ stdio: isJson ? 'ignore' : 'inherit'
77
+ });
78
+ if (res.status !== 0) {
79
+ throw new Error(`wireguard /uninstalltunnelservice failed with code ${res.status}`);
80
+ }
81
+ return;
50
82
  }
51
83
 
52
84
  const res = spawnSync('sudo', ['wg-quick', 'down', confPath], {