polaris-vpn 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)
4
4
 
5
+ <div align="center">
6
+ <img src="./docs/demo.gif" alt="Polaris VPN Terminal Demo" width="800">
7
+ </div>
8
+
5
9
  Your True North in Digital Privacy. `polaris` is a production-quality, open-source, self-hosted VPN CLI tool. It supports SSH dynamic port forwarding, WireGuard, and AmneziaWG (Stealth) tunnels.
6
10
 
7
11
  ### Prerequisites
@@ -0,0 +1,68 @@
1
+ # Marketing Drafts for Polaris VPN
2
+
3
+ Use these templates to post to various communities to drive your npm downloads.
4
+
5
+ ---
6
+
7
+ ## 1. Hacker News (Show HN)
8
+ **Title:** Show HN: I built a free, self-hosted stealth VPN CLI for Oracle Cloud
9
+ **Link / Text:**
10
+
11
+ I got tired of paying for commercial VPNs that harvest data, so I built Polaris. It’s an open-source CLI tool that automatically provisions a WireGuard or AmneziaWG (Stealth Mode) VPN on any Linux VPS in about 30 seconds.
12
+
13
+ I optimized it specifically for Oracle Cloud's "Always Free" ARM tier (2 OCPUs, 12GB RAM, 10TB bandwidth), so you can effectively get a high-performance personal VPN completely for free, forever.
14
+
15
+ Features:
16
+ - Native WireGuard & Stealth Mode (AmneziaWG for DPI bypass)
17
+ - Zero-Log Self-Hosted DNS (Unbound is automatically configured on the server)
18
+ - SOCKS5 / SSH tunnel fallback modes
19
+ - Built-in live bandwidth monitoring & terminal TUI dashboard
20
+ - System-wide Kill Switch
21
+ - Terminal QR codes for mobile setup
22
+
23
+ Would love for you to try it out and tear the code apart.
24
+
25
+ Repo: https://github.com/Divyo/polaris-vpn
26
+ `npm install -g polaris-vpn`
27
+
28
+ ---
29
+
30
+ ## 2. Reddit (r/selfhosted, r/privacy, r/homelab)
31
+ **Title:** I built a CLI tool to automate deploying a free, zero-log stealth VPN on Oracle Cloud in 30 seconds
32
+
33
+ **Body:**
34
+ Hey r/selfhosted!
35
+
36
+ I’ve been working on a project called **Polaris VPN**. The goal was to make spinning up a secure, self-hosted VPN as easy as typing a single command, without having to manually mess with `iptables` routing, DNS leaks, or WireGuard configs.
37
+
38
+ It works perfectly with the Oracle Cloud Free Tier. If you have an empty Ubuntu VM sitting there, you can run:
39
+
40
+ `polaris deploy --server ubuntu@<your-ip>`
41
+
42
+ And it will automatically:
43
+ 1. Install WireGuard (or AmneziaWG for stealth/DPI bypass)
44
+ 2. Setup UFW and Fail2Ban for security
45
+ 3. Install and configure **Unbound** so your DNS queries never hit Cloudflare or Google (Zero-Log DNS)
46
+ 4. Configure your local machine to route all traffic through it.
47
+
48
+ It also has a cool terminal dashboard (`polaris dashboard`) to monitor your bandwidth and peers, and can generate QR codes in the terminal (`polaris peer qr mobile`) to instantly connect your phone.
49
+
50
+ It's completely open-source (Apache 2.0).
51
+ Repo: https://github.com/Divyo/polaris-vpn
52
+
53
+ If anyone has an Oracle Cloud account or a spare VPS, I'd love to hear your feedback!
54
+
55
+ ---
56
+
57
+ ## 3. Dev.to / Medium Tutorial
58
+ **Title:** How to host your own Stealth VPN for free (and never pay for a VPN again)
59
+
60
+ **(Outline for your blog post):**
61
+ 1. **The Problem:** Commercial VPNs are honeypots. They log your data, get blocked by Netflix/DPI, and cost $5-10/month.
62
+ 2. **The Solution:** Oracle Cloud Always Free tier + Polaris VPN.
63
+ 3. **Step 1:** Sign up for Oracle Cloud and launch an Ubuntu ARM VM.
64
+ 4. **Step 2:** Run `npm install -g polaris-vpn`
65
+ 5. **Step 3:** Run `polaris deploy --server ubuntu@your-ip`
66
+ 6. **Step 4:** Run `polaris start --server ubuntu@your-ip`
67
+ 7. **Explain the Tech:** Talk about AmneziaWG obfuscation, Unbound local DNS for privacy, and how it bypasses DPI (Deep Packet Inspection) which blocks normal WireGuard.
68
+ 8. **Call to Action:** Ask them to star the repo and try it out!
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "polaris": "src/cli.js",
8
- "polaris-server": "src/server/server.js"
8
+ "polaris-server": "src/commands/server.js"
9
9
  },
10
10
  "files": [
11
11
  "src",
@@ -16,13 +16,23 @@
16
16
  ],
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/Divyo/polaris-vpn.git"
19
+ "url": "git+https://github.com/Divyo/polaris-vpn.git"
20
+ },
21
+ "homepage": "https://github.com/Divyo/polaris-vpn#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/Divyo/polaris-vpn/issues"
24
+ },
25
+ "author": "Divyo",
26
+ "funding": {
27
+ "type": "github",
28
+ "url": "https://github.com/sponsors/Divyo"
20
29
  },
21
30
  "engines": {
22
31
  "node": ">=18"
23
32
  },
24
33
  "scripts": {
25
- "start": "node ./src/cli.js"
34
+ "start": "node ./src/cli.js",
35
+ "test": "node --test"
26
36
  },
27
37
  "license": "Apache-2.0",
28
38
  "keywords": [
@@ -34,7 +44,16 @@
34
44
  "cli",
35
45
  "wireguard",
36
46
  "self-hosted",
37
- "oracle-cloud"
47
+ "oracle-cloud",
48
+ "free-vpn",
49
+ "ssh-tunnel",
50
+ "amneziawg",
51
+ "stealth-vpn",
52
+ "dpi-bypass",
53
+ "zero-trust",
54
+ "kill-switch",
55
+ "peer-to-peer",
56
+ "terminal"
38
57
  ],
39
58
  "dependencies": {
40
59
  "blessed": "^0.1.81",
@@ -51,7 +70,6 @@
51
70
  "qrcode-terminal": "^0.12.0",
52
71
  "socks-proxy-agent": "^10.1.0",
53
72
  "ssh2": "^1.17.0",
54
- "update-notifier": "^7.3.1",
55
- "wireguard-tools": "^0.1.0"
73
+ "update-notifier": "^7.3.1"
56
74
  }
57
75
  }
package/src/cli.js CHANGED
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import updateNotifier from 'update-notifier';
7
7
  import { spawnSync } from 'child_process';
8
8
  import { printBanner, printError, printSuccess } from './utils/display.js';
9
+ import { handleError } from './utils/error-handler.js';
9
10
  import chalk from 'chalk';
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
@@ -51,8 +52,7 @@ program
51
52
  const run = (await import('./commands/start.js')).default;
52
53
  await run(cmd.optsWithGlobals());
53
54
  } catch (err) {
54
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
55
- process.exit(1);
55
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
56
56
  }
57
57
  });
58
58
 
@@ -65,8 +65,7 @@ program
65
65
  const run = (await import('./commands/stop.js')).default;
66
66
  await run(cmd.optsWithGlobals());
67
67
  } catch (err) {
68
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
69
- process.exit(1);
68
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
70
69
  }
71
70
  });
72
71
 
@@ -80,8 +79,7 @@ program
80
79
  const run = (await import('./commands/status.js')).default;
81
80
  await run(cmd.optsWithGlobals());
82
81
  } catch (err) {
83
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
84
- process.exit(1);
82
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
85
83
  }
86
84
  });
87
85
 
@@ -107,8 +105,7 @@ program
107
105
  const run = (await import('./commands/check.js')).default;
108
106
  await run(cmd.optsWithGlobals());
109
107
  } catch (err) {
110
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
111
- process.exit(1);
108
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
112
109
  }
113
110
  });
114
111
 
@@ -122,8 +119,7 @@ program
122
119
  const { addServer } = await import('./commands/servers.js');
123
120
  await addServer(alias, cmd.optsWithGlobals());
124
121
  } catch (err) {
125
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
126
- process.exit(1);
122
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
127
123
  }
128
124
  });
129
125
 
@@ -136,8 +132,7 @@ program
136
132
  const { listServers } = await import('./commands/servers.js');
137
133
  await listServers(cmd.optsWithGlobals());
138
134
  } catch (err) {
139
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
140
- process.exit(1);
135
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
141
136
  }
142
137
  });
143
138
 
@@ -150,8 +145,7 @@ program
150
145
  const { useServer } = await import('./commands/servers.js');
151
146
  await useServer(alias, cmd.optsWithGlobals());
152
147
  } catch (err) {
153
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
154
- process.exit(1);
148
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
155
149
  }
156
150
  });
157
151
 
@@ -168,8 +162,7 @@ dnsCmd
168
162
  const { dnsStart } = await import('./commands/dns.js');
169
163
  await dnsStart(cmd.optsWithGlobals());
170
164
  } catch (err) {
171
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
172
- process.exit(1);
165
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
173
166
  }
174
167
  });
175
168
 
@@ -182,8 +175,7 @@ dnsCmd
182
175
  const { dnsStop } = await import('./commands/dns.js');
183
176
  await dnsStop(cmd.optsWithGlobals());
184
177
  } catch (err) {
185
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
186
- process.exit(1);
178
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
187
179
  }
188
180
  });
189
181
 
@@ -196,8 +188,7 @@ dnsCmd
196
188
  const { dnsStatus } = await import('./commands/dns.js');
197
189
  await dnsStatus(cmd.optsWithGlobals());
198
190
  } catch (err) {
199
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
200
- process.exit(1);
191
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
201
192
  }
202
193
  });
203
194
 
@@ -214,8 +205,7 @@ program
214
205
  const run = (await import('./commands/deploy.js')).default;
215
206
  await run(cmd.optsWithGlobals());
216
207
  } catch (err) {
217
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
218
- process.exit(1);
208
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
219
209
  }
220
210
  });
221
211
 
@@ -231,8 +221,7 @@ ksCmd
231
221
  setKillSwitchConfig(true);
232
222
  printSuccess('Kill switch enabled. All non-VPN traffic will be blocked when the tunnel is active.');
233
223
  } catch (err) {
234
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
235
- process.exit(1);
224
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
236
225
  }
237
226
  });
238
227
 
@@ -246,8 +235,7 @@ ksCmd
246
235
  setKillSwitchConfig(false);
247
236
  printSuccess('Kill switch disabled.');
248
237
  } catch (err) {
249
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
250
- process.exit(1);
238
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
251
239
  }
252
240
  });
253
241
 
@@ -262,8 +250,7 @@ peerCmd
262
250
  const { peerAdd } = await import('./commands/peer.js');
263
251
  await peerAdd(name, cmd.optsWithGlobals());
264
252
  } catch (err) {
265
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
266
- process.exit(1);
253
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
267
254
  }
268
255
  });
269
256
 
@@ -276,8 +263,7 @@ peerCmd
276
263
  const { peerList } = await import('./commands/peer.js');
277
264
  await peerList(cmd.optsWithGlobals());
278
265
  } catch (err) {
279
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
280
- process.exit(1);
266
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
281
267
  }
282
268
  });
283
269
 
@@ -290,8 +276,7 @@ peerCmd
290
276
  const { peerRemove } = await import('./commands/peer.js');
291
277
  await peerRemove(name, cmd.optsWithGlobals());
292
278
  } catch (err) {
293
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
294
- process.exit(1);
279
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
295
280
  }
296
281
  });
297
282
 
@@ -303,8 +288,7 @@ peerCmd
303
288
  const { peerQr } = await import('./commands/peer.js');
304
289
  await peerQr(name, cmd.optsWithGlobals());
305
290
  } catch (err) {
306
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
307
- process.exit(1);
291
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
308
292
  }
309
293
  });
310
294
 
@@ -1,4 +1,5 @@
1
- import { printSuccess, printError, printInfo } from '../utils/display.js';
1
+ import { printSuccess, printInfo } from '../utils/display.js';
2
+ import { handleError } from '../utils/error-handler.js';
2
3
  import { startApiServer } from '../server/api.js';
3
4
 
4
5
  export const serverStart = async (options) => {
@@ -25,11 +26,6 @@ Endpoints:
25
26
  `);
26
27
  }
27
28
  } 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;
29
+ handleError('Failed to start local API', err, isJson);
34
30
  }
35
31
  };
@@ -3,7 +3,8 @@ import tls from 'tls';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { spawnSync } from 'child_process';
6
- import { printError, printSuccess, createSpinner, printInfo } from '../utils/display.js';
6
+ import { printSuccess, createSpinner, printInfo } from '../utils/display.js';
7
+ import { handleError } from '../utils/error-handler.js';
7
8
  import { getPublicIp, getProxiedIp } from '../net/ip-check.js';
8
9
  import { getProfiles } from '../core/profile-service.js';
9
10
  import { getActiveTunnel, startTunnel } from '../core/tunnel-service.js';
@@ -54,12 +55,7 @@ export default async (options) => {
54
55
  if (active && profiles[active]) {
55
56
  server = profiles[active];
56
57
  } else {
57
- if (isJson) {
58
- console.log(JSON.stringify({ error: 'No server specified' }));
59
- } else {
60
- printError('No server specified. Use --server <user@host> or set an active profile with "polaris use".');
61
- }
62
- process.exitCode = 1;
58
+ handleError('No server specified. Use --server <user@host> or set an active profile with "polaris use".', null, isJson);
63
59
  return;
64
60
  }
65
61
  }
@@ -67,12 +63,7 @@ export default async (options) => {
67
63
  // Check if already running
68
64
  const info = getActiveTunnel();
69
65
  if (info) {
70
- if (isJson) {
71
- console.log(JSON.stringify({ error: 'Tunnel already running', pid: info.pid }));
72
- } else {
73
- printError(`Tunnel is already running connected to ${info.server}. Run "polaris stop" first.`);
74
- }
75
- process.exitCode = 1;
66
+ handleError(`Tunnel is already running connected to ${info.server}. Run "polaris stop" first.`, null, isJson);
76
67
  return;
77
68
  }
78
69
 
@@ -167,11 +158,6 @@ export default async (options) => {
167
158
 
168
159
  } catch (err) {
169
160
  if (!isJson && spinner) spinner.stop();
170
- if (isJson) {
171
- console.log(JSON.stringify({ error: err.message }));
172
- } else {
173
- printError('Failed to start tunnel', err);
174
- }
175
- process.exitCode = 1;
161
+ handleError('Failed to start tunnel', err, isJson);
176
162
  }
177
163
  };
@@ -39,9 +39,15 @@ export const deployServer = async (serverStr, options = {}) => {
39
39
  const host = parts.length > 1 ? parts[1] : parts[0];
40
40
  const isAwg = options.mode === 'amneziawg';
41
41
 
42
- const privateKey = options.privateKey
43
- ? fs.readFileSync(options.privateKey)
44
- : getDefaultPrivateKey();
42
+ let privateKey = null;
43
+ if (options.privateKey) {
44
+ if (!fs.existsSync(options.privateKey)) {
45
+ throw new Error(`SSH private key file not found: ${options.privateKey}`);
46
+ }
47
+ privateKey = fs.readFileSync(options.privateKey);
48
+ } else {
49
+ privateKey = getDefaultPrivateKey();
50
+ }
45
51
 
46
52
  if (!privateKey && !options.password) {
47
53
  throw new Error('No SSH authentication method found. Please configure default SSH keys or specify key path/password.');
@@ -189,7 +195,8 @@ PersistentKeepalive = 25
189
195
  port: options.port || 22,
190
196
  username,
191
197
  privateKey,
192
- password: options.password
198
+ password: options.password,
199
+ readyTimeout: 10000 // 10 second timeout for SSH connection
193
200
  });
194
201
  });
195
202
  };
@@ -0,0 +1,19 @@
1
+ import { printError } from './display.js';
2
+
3
+ export const handleError = (msg, err, isJson = false) => {
4
+ const isDebug = process.argv.includes('--debug');
5
+
6
+ if (isJson) {
7
+ const errorOutput = { error: msg, details: err ? err.message : undefined };
8
+ if (isDebug && err && err.stack) {
9
+ errorOutput.stack = err.stack;
10
+ }
11
+ console.log(JSON.stringify(errorOutput));
12
+ } else {
13
+ printError(msg, err);
14
+ if (isDebug && err && err.stack) {
15
+ console.error('\n' + err.stack);
16
+ }
17
+ }
18
+ process.exitCode = 1;
19
+ };