polaris-vpn 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ 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
+ ## [1.0.0] - 2026-07-15
9
+
10
+ ### Added
11
+
12
+ - **Zero-Log Self-Hosted DNS (Unbound)**: `polaris deploy` now automatically installs and configures Unbound directly on the VPS, binding it to the tunnel interface (`10.0.0.1:53`). Client configs use this by default, ensuring your DNS queries never hit public resolvers (like Cloudflare) and never leave the encrypted tunnel.
13
+ - **Server Hardening (Fail2Ban)**: Deployment now automatically secures the VPS SSH port against automated botnet brute-force attacks by installing and enabling `fail2ban`.
14
+ - **Multi-Peer Management**: `polaris peer add <name>` allows you to provision additional client configurations dynamically on your existing WireGuard or AmneziaWG server. Use `polaris peer list` and `polaris peer remove <name>` to manage your peers.
15
+ - **Terminal QR Codes**: Automatically generate terminal-rendered QR codes using `polaris peer qr <name>` for instantly setting up mobile devices (iOS/Android).
16
+ - **System Kill Switch**: Run `polaris killswitch on` to drop all non-VPN outbound traffic (using `iptables` or `pf`) when the tunnel is active, ensuring zero IP leaks.
17
+
8
18
  ## [0.7.0] - 2026-07-12
9
19
 
10
20
  ### Added
package/README.md CHANGED
@@ -2,7 +2,12 @@
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.
10
+
6
11
  ### Prerequisites
7
12
 
8
13
  - Node.js (v18+)
@@ -50,6 +55,11 @@ polaris stop
50
55
  | `polaris dashboard` | Opens a rich Terminal User Interface (TUI) with real-time stats. |
51
56
  | `polaris status [--full]` | Show current tunnel status. `--full` shows GeoIP, ping latency, and WG stats. |
52
57
  | `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
58
+ | `polaris killswitch on/off` | Enable or disable the system-wide VPN kill switch. |
59
+ | `polaris peer add <name>` | Generate and add a new peer config for WireGuard/AmneziaWG. |
60
+ | `polaris peer list` | List all peers configured on the server. |
61
+ | `polaris peer remove <name>` | Revoke a peer from the server. |
62
+ | `polaris peer qr <name>` | Display the peer config as a QR code for mobile devices. |
53
63
  | `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
54
64
  | `polaris add <alias> --server user@host` | Save a server profile for quick access. |
55
65
  | `polaris list` | List all saved server profiles. |
@@ -58,15 +68,19 @@ polaris stop
58
68
  | `polaris update` | Update `polaris-vpn` to the latest version via npm. |
59
69
 
60
70
  #### `polaris dashboard`
71
+
61
72
  Opens a rich Terminal User Interface (TUI) with real-time graphs, server location maps, and live statistics (Rx/Tx, Ping).
62
73
 
63
74
  #### `polaris status [--full]`
75
+
64
76
  Displays the connection state, mode, current proxy IP, and active server. Use `--full` to query GeoIP and ping latency.
65
77
 
66
78
  #### `polaris monitor`
79
+
67
80
  Live bandwidth monitor that actively graphs the download and upload speeds of your tunnel.
68
81
 
69
82
  #### `polaris check`
83
+
70
84
  Checks if your IP matches the proxy, tests for DNS leaks via `ipleak.net`, and verifies IPv6 routing.
71
85
 
72
86
  > All commands support the `--json` flag for machine-readable output.
@@ -106,8 +120,8 @@ One Oracle VM can serve multiple people:
106
120
  - **v0.1**: MVP — SSH SOCKS5 tunnel
107
121
  - **v0.5**: WireGuard full tunnel + server provisioning
108
122
  - **v0.6**: Windows NT Service bindings + Blessed TUI Dashboard
109
- - **v0.7**: AmneziaWG stealth mode (DPI bypass) (Current)
110
- - **v1.0**: Multi-peer management + QR codes + Kill switch
123
+ - **v0.7**: AmneziaWG stealth mode (DPI bypass)
124
+ - **v1.0**: Multi-peer management + QR codes + Kill switch (Current)
111
125
 
112
126
  ## Contributing
113
127
 
@@ -0,0 +1,56 @@
1
+ # Oracle Cloud "Always Free" VPS Guide
2
+
3
+ Oracle Cloud offers the most generous free tier in the cloud computing market, making it the perfect companion for a self-hosted Polaris VPN.
4
+
5
+ This guide will walk you through exactly how to set up an Oracle ARM instance, configure the firewall, and prepare it for Polaris.
6
+
7
+ ## 1. Sign Up for Oracle Cloud
8
+
9
+ 1. Go to [Oracle Cloud Free Tier](https://www.oracle.com/cloud/free/) and sign up.
10
+ 2. You will need to provide a credit/debit card for verification. **You will not be charged.** Oracle requires this to prevent spam.
11
+ 3. Select your **Home Region** carefully. You cannot change this later without deleting your account. Pick the region geographically closest to you for the lowest latency.
12
+ - *Example: If you are in South Asia, pick Mumbai or Hyderabad. If you are in Southeast Asia, pick Singapore.*
13
+
14
+ ## 2. Create the Compute Instance
15
+
16
+ 1. From the Oracle Cloud Dashboard, click **Create a VM instance**.
17
+ 2. **Name**: `polaris-vpn` (or whatever you prefer).
18
+ 3. **Placement**: Leave as default (AD-1).
19
+ 4. **Image and Shape**:
20
+ - Click **Edit**.
21
+ - **Image**: Select **Ubuntu** (Recommend version 22.04 or newer).
22
+ - **Shape**: Select **Ampere (ARM)** (`VM.Standard.A1.Flex`).
23
+ - Configure the shape to use **2 OCPUs** and **12 GB RAM**. (You are allowed up to 4 OCPUs and 24 GB RAM for free, but 2 OCPUs is more than enough for a VPN).
24
+ 5. **Networking**:
25
+ - Leave the default VCN settings.
26
+ - Ensure **Assign a public IPv4 address** is checked.
27
+ 6. **Add SSH keys**:
28
+ - Select **Generate a key pair for me**.
29
+ - Click **Save private key**. Keep this `.key` file safe! You will need it to connect.
30
+ 7. Click **Create** at the bottom of the page. Your instance will take about 2-3 minutes to provision.
31
+
32
+ ## 3. Configure the Virtual Cloud Network (VCN) Firewall
33
+
34
+ By default, Oracle Cloud blocks all incoming traffic except SSH (Port 22). WireGuard (and AmneziaWG) requires UDP port 51820 to be open.
35
+
36
+ 1. On your instance details page, look for the **Primary VNIC** section and click on the **Subnet** link (e.g., `subnet-xxxx`).
37
+ 2. Under **Security Lists**, click the Default Security List.
38
+ 3. Click **Add Ingress Rules**.
39
+ 4. Configure the rule as follows:
40
+ - **Source Type**: CIDR
41
+ - **Source CIDR**: `0.0.0.0/0`
42
+ - **IP Protocol**: UDP
43
+ - **Destination Port Range**: `51820`
44
+ - **Description**: Allow WireGuard/AmneziaWG
45
+ 5. Click **Add Ingress Rules**.
46
+
47
+ *(Note: If you plan to use the SSH SOCKS5 proxy instead of WireGuard, you don't need to open port 51820, as SSH uses port 22 which is already open).*
48
+
49
+ ## 4. Get Ready for Polaris
50
+
51
+ 1. Go back to your **Instance Details** page.
52
+ 2. Note down the **Public IP Address** (e.g., `123.45.67.89`).
53
+ 3. Note the default username for Ubuntu images is always `ubuntu`.
54
+ 4. Locate the private key you downloaded in Step 2.6 (e.g., `ssh-key-2026-07-12.key`).
55
+
56
+ You are now ready to deploy Polaris! Head back to the [Full Setup Guide](./SETUP.md) and run the `polaris deploy` command with your new Oracle IP and SSH key.
package/docs/SETUP.md ADDED
@@ -0,0 +1,93 @@
1
+ # Full Step-by-Step Setup Guide
2
+
3
+ Welcome to Polaris VPN! This guide will take you from scratch to having a fully functional, DPI-resistant self-hosted VPN.
4
+
5
+ ## 1. Prepare a Virtual Private Server (VPS)
6
+
7
+ Polaris needs a Linux server (Ubuntu/Debian recommended) to host the VPN.
8
+ If you don't already have one, you can easily get a powerful one for **free forever**.
9
+
10
+ 👉 **[Click here for the Oracle Cloud Free Tier Guide](./ORACLE_CLOUD.md)**
11
+
12
+ *Once you have your server's Public IP address and SSH Private Key, continue to Step 2.*
13
+
14
+ ## 2. Install Polaris VPN
15
+
16
+ On your local machine (where you want to connect *from*), you need Node.js installed.
17
+
18
+ Install the Polaris CLI globally:
19
+
20
+ ```bash
21
+ npm install -g polaris-vpn
22
+ ```
23
+
24
+ ## 3. Provision the Server
25
+
26
+ You will now use Polaris to automatically install and configure the VPN on your new VPS.
27
+
28
+ Run the `deploy` command. We highly recommend using the `amneziawg` mode to ensure your VPN traffic cannot be blocked by Deep Packet Inspection (DPI):
29
+
30
+ ```bash
31
+ polaris deploy --server ubuntu@<YOUR_SERVER_IP> --mode amneziawg
32
+ ```
33
+
34
+ *Note: If you receive an SSH authentication error, you need to provide your SSH private key. By default, Polaris looks for `~/.ssh/id_rsa`. If your key is saved elsewhere (like the one downloaded from Oracle), you can use it like this:*
35
+
36
+ ```bash
37
+ eval "$(ssh-agent -s)"
38
+ ssh-add /path/to/your/oracle-ssh-key.key
39
+ polaris deploy --server ubuntu@<YOUR_SERVER_IP> --mode amneziawg
40
+ ```
41
+
42
+ This process takes about 2-3 minutes. Polaris will install AmneziaWG, configure the stealth parameters, set up the kernel modules, and generate your local client configuration.
43
+
44
+ ## 4. Start the Tunnel
45
+
46
+ Once deployment is successful, you can connect to your new VPN!
47
+
48
+ Run:
49
+
50
+ ```bash
51
+ polaris start
52
+ ```
53
+
54
+ You should see a success message. All your internet traffic is now encrypted and routed through your VPS!
55
+
56
+ To verify everything is working and your IP has changed, run:
57
+
58
+ ```bash
59
+ polaris check
60
+ ```
61
+
62
+ ## 5. Enable the Kill Switch (Optional)
63
+
64
+ To ensure your real IP is never leaked if the VPN connection drops, enable the system kill switch:
65
+
66
+ ```bash
67
+ polaris killswitch on
68
+ ```
69
+
70
+ ## 6. Connect Mobile Devices
71
+
72
+ Want to use the VPN on your phone? Polaris can generate terminal QR codes!
73
+ First, generate a new peer configuration on the server:
74
+
75
+ ```bash
76
+ polaris peer add my-iphone
77
+ ```
78
+
79
+ Then, display the QR code on your terminal:
80
+
81
+ ```bash
82
+ polaris peer qr my-iphone
83
+ ```
84
+
85
+ Download the **AmneziaWG** app on iOS or Android, tap the `+` button, select **Create from QR code**, and scan your screen!
86
+
87
+ ## 7. Stop the Tunnel
88
+
89
+ When you are done, simply run:
90
+
91
+ ```bash
92
+ polaris stop
93
+ ```
@@ -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,23 +1,38 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "0.7.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",
12
+ "docs",
12
13
  "README.md",
13
14
  "LICENSE",
14
15
  "CHANGELOG.md"
15
16
  ],
17
+ "repository": {
18
+ "type": "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"
29
+ },
16
30
  "engines": {
17
31
  "node": ">=18"
18
32
  },
19
33
  "scripts": {
20
- "start": "node ./src/cli.js"
34
+ "start": "node ./src/cli.js",
35
+ "test": "node --test"
21
36
  },
22
37
  "license": "Apache-2.0",
23
38
  "keywords": [
@@ -29,7 +44,16 @@
29
44
  "cli",
30
45
  "wireguard",
31
46
  "self-hosted",
32
- "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"
33
57
  ],
34
58
  "dependencies": {
35
59
  "blessed": "^0.1.81",
@@ -46,7 +70,6 @@
46
70
  "qrcode-terminal": "^0.12.0",
47
71
  "socks-proxy-agent": "^10.1.0",
48
72
  "ssh2": "^1.17.0",
49
- "update-notifier": "^7.3.1",
50
- "wireguard-tools": "^0.1.0"
73
+ "update-notifier": "^7.3.1"
51
74
  }
52
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,12 +205,41 @@ 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);
209
+ }
210
+ });
211
+
212
+ const ksCmd = program.command('killswitch').description('Manage the system VPN kill switch');
213
+
214
+ ksCmd
215
+ .command('on')
216
+ .description('Enable the kill switch')
217
+ .action(async (options, cmd) => {
218
+ if (!cmd.optsWithGlobals().json) printBanner();
219
+ try {
220
+ const { setKillSwitchConfig } = await import('./utils/kill-switch.js');
221
+ setKillSwitchConfig(true);
222
+ printSuccess('Kill switch enabled. All non-VPN traffic will be blocked when the tunnel is active.');
223
+ } catch (err) {
224
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
219
225
  }
220
226
  });
221
227
 
222
- const peerCmd = program.command('peer').description('Manage WireGuard client peers');
228
+ ksCmd
229
+ .command('off')
230
+ .description('Disable the kill switch')
231
+ .action(async (options, cmd) => {
232
+ if (!cmd.optsWithGlobals().json) printBanner();
233
+ try {
234
+ const { setKillSwitchConfig } = await import('./utils/kill-switch.js');
235
+ setKillSwitchConfig(false);
236
+ printSuccess('Kill switch disabled.');
237
+ } catch (err) {
238
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
239
+ }
240
+ });
241
+
242
+ const peerCmd = program.command('peer').description('Manage WireGuard/AmneziaWG client peers');
223
243
 
224
244
  peerCmd
225
245
  .command('add <name>')
@@ -230,8 +250,7 @@ peerCmd
230
250
  const { peerAdd } = await import('./commands/peer.js');
231
251
  await peerAdd(name, cmd.optsWithGlobals());
232
252
  } catch (err) {
233
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
234
- process.exit(1);
253
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
235
254
  }
236
255
  });
237
256
 
@@ -244,8 +263,7 @@ peerCmd
244
263
  const { peerList } = await import('./commands/peer.js');
245
264
  await peerList(cmd.optsWithGlobals());
246
265
  } catch (err) {
247
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
248
- process.exit(1);
266
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
249
267
  }
250
268
  });
251
269
 
@@ -258,8 +276,7 @@ peerCmd
258
276
  const { peerRemove } = await import('./commands/peer.js');
259
277
  await peerRemove(name, cmd.optsWithGlobals());
260
278
  } catch (err) {
261
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
262
- process.exit(1);
279
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
263
280
  }
264
281
  });
265
282
 
@@ -271,8 +288,7 @@ peerCmd
271
288
  const { peerQr } = await import('./commands/peer.js');
272
289
  await peerQr(name, cmd.optsWithGlobals());
273
290
  } catch (err) {
274
- if (!cmd.optsWithGlobals().json) printError('Command failed', err);
275
- process.exit(1);
291
+ handleError('Command failed', err, cmd.optsWithGlobals().json);
276
292
  }
277
293
  });
278
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.');
@@ -63,10 +69,10 @@ export const deployServer = async (serverStr, options = {}) => {
63
69
  const quickCmd = isAwg ? 'awg-quick' : 'wg-quick';
64
70
 
65
71
  // Step 1: Install packages
66
- onProgress(`Installing ${isAwg ? 'AmneziaWG' : 'WireGuard'} and UFW on remote VPS...`);
67
- let installCmd = 'sudo apt-get update -y && sudo apt-get install -y wireguard ufw';
72
+ onProgress(`Installing ${isAwg ? 'AmneziaWG' : 'WireGuard'}, UFW, Unbound, and Fail2Ban...`);
73
+ let installCmd = 'sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard ufw unbound fail2ban';
68
74
  if (isAwg) {
69
- installCmd = 'sudo apt-get update -y && sudo apt-get install -y software-properties-common && sudo add-apt-repository -y ppa:amnezia/ppa && sudo apt-get update -y && sudo apt-get install -y amneziawg-dkms amneziawg-tools ufw';
75
+ installCmd = 'sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common && sudo add-apt-repository -y ppa:amnezia/ppa && sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y amneziawg-dkms amneziawg-tools ufw unbound fail2ban';
70
76
  }
71
77
  let res = await sshExec(conn, installCmd);
72
78
  if (res.code !== 0) throw new Error(`Installation failed: ${res.stderr}`);
@@ -118,6 +124,24 @@ AllowedIPs = 10.0.0.2/32
118
124
  res = await sshExec(conn, `sudo systemctl stop ${quickCmd}@${ifaceName} || true && sudo systemctl start ${quickCmd}@${ifaceName} && sudo systemctl enable ${quickCmd}@${ifaceName}`);
119
125
  if (res.code !== 0) throw new Error(`Starting tunnel failed: ${res.stderr}`);
120
126
 
127
+ // Step 5b: Configure Unbound DNS
128
+ onProgress('Configuring Unbound Zero-Log DNS...');
129
+ const unboundConf = `server:
130
+ interface: 10.0.0.1
131
+ access-control: 10.0.0.0/24 allow
132
+ hide-identity: yes
133
+ hide-version: yes
134
+ use-caps-for-id: yes
135
+ prefetch: yes
136
+ `;
137
+ res = await sshExec(conn, `cat << 'EOF' > /tmp/polaris-dns.conf\n${unboundConf}\nEOF\nsudo mv /tmp/polaris-dns.conf /etc/unbound/unbound.conf.d/polaris-dns.conf && sudo systemctl restart unbound && sudo systemctl enable unbound`);
138
+ if (res.code !== 0) throw new Error(`Unbound setup failed: ${res.stderr}`);
139
+
140
+ // Step 5c: Start Fail2Ban
141
+ onProgress('Starting Fail2Ban SSH protection...');
142
+ res = await sshExec(conn, 'sudo systemctl enable fail2ban && sudo systemctl restart fail2ban');
143
+ if (res.code !== 0) throw new Error(`Fail2ban setup failed: ${res.stderr}`);
144
+
121
145
  // Step 6: Configure UFW firewall
122
146
  onProgress('Configuring UFW firewall...');
123
147
  res = await sshExec(conn, 'sudo ufw allow 51820/udp && sudo ufw allow 22/tcp && echo "y" | sudo ufw enable');
@@ -128,7 +152,7 @@ AllowedIPs = 10.0.0.2/32
128
152
  const clientConf = `[Interface]
129
153
  PrivateKey = ${clientKeys.privateKey}
130
154
  Address = 10.0.0.2/24
131
- DNS = 1.1.1.1
155
+ DNS = 10.0.0.1
132
156
  ${obfuscationBlock}
133
157
 
134
158
  [Peer]
@@ -152,6 +176,7 @@ PersistentKeepalive = 25
152
176
  serverPublicKey: serverKeys.publicKey,
153
177
  clientPublicKey: clientKeys.publicKey,
154
178
  interface: ethInterface,
179
+ awgParams: isAwg ? awgParams : null,
155
180
  timestamp: new Date().toISOString()
156
181
  }, null, 2), 'utf-8');
157
182
 
@@ -170,7 +195,8 @@ PersistentKeepalive = 25
170
195
  port: options.port || 22,
171
196
  username,
172
197
  privateKey,
173
- password: options.password
198
+ password: options.password,
199
+ readyTimeout: 10000 // 10 second timeout for SSH connection
174
200
  });
175
201
  });
176
202
  };
@@ -65,10 +65,13 @@ const sshExec = (client, command) => {
65
65
  export const addPeer = async (name) => {
66
66
  const info = getDeployInfo();
67
67
  const conn = await sshConnect(info);
68
+ const isAwg = info.mode === 'amneziawg';
69
+ const confFile = isAwg ? '/etc/amnezia/amneziawg/awg0.conf' : '/etc/wireguard/wg0.conf';
70
+ const quickCmd = isAwg ? 'awg-quick@awg0' : 'wg-quick@wg0';
68
71
 
69
72
  try {
70
- // 1. Read remote wg0.conf
71
- const catRes = await sshExec(conn, 'sudo cat /etc/wireguard/wg0.conf');
73
+ // 1. Read remote config
74
+ const catRes = await sshExec(conn, `sudo cat ${confFile}`);
72
75
  if (catRes.code !== 0) throw new Error(`Failed to read server config: ${catRes.stderr}`);
73
76
  const configContent = catRes.stdout;
74
77
 
@@ -99,18 +102,32 @@ PublicKey = ${peerKeys.publicKey}
99
102
  AllowedIPs = ${peerIp}/32
100
103
  `;
101
104
 
102
- const writeCmd = `echo "${peerConfigBlock.replace(/"/g, '\\"')}" | sudo tee -a /etc/wireguard/wg0.conf`;
105
+ const writeCmd = `echo "${peerConfigBlock.replace(/"/g, '\\"')}" | sudo tee -a ${confFile}`;
103
106
  let res = await sshExec(conn, writeCmd);
104
107
  if (res.code !== 0) throw new Error(`Failed to append remote peer: ${res.stderr}`);
105
108
 
106
- // Sync remote wireguard
107
- await sshExec(conn, 'sudo systemctl restart wg-quick@wg0');
109
+ // Sync remote tunnel
110
+ await sshExec(conn, `sudo systemctl restart ${quickCmd}`);
111
+
112
+ let obfuscationBlock = '';
113
+ if (isAwg && info.awgParams) {
114
+ obfuscationBlock = `Jc = ${info.awgParams.Jc}
115
+ Jmin = ${info.awgParams.Jmin}
116
+ Jmax = ${info.awgParams.Jmax}
117
+ S1 = ${info.awgParams.S1}
118
+ S2 = ${info.awgParams.S2}
119
+ H1 = ${info.awgParams.H1}
120
+ H2 = ${info.awgParams.H2}
121
+ H3 = ${info.awgParams.H3}
122
+ H4 = ${info.awgParams.H4}`;
123
+ }
108
124
 
109
125
  // 3. Save peer config locally
110
126
  const clientConf = `[Interface]
111
127
  PrivateKey = ${peerKeys.privateKey}
112
128
  Address = ${peerIp}/24
113
- DNS = 1.1.1.1
129
+ DNS = 10.0.0.1
130
+ ${obfuscationBlock}
114
131
 
115
132
  [Peer]
116
133
  PublicKey = ${info.serverPublicKey}
@@ -135,10 +152,13 @@ PersistentKeepalive = 25
135
152
  export const listPeers = async () => {
136
153
  const info = getDeployInfo();
137
154
  const conn = await sshConnect(info);
155
+ const isAwg = info.mode === 'amneziawg';
156
+ const confFile = isAwg ? '/etc/amnezia/amneziawg/awg0.conf' : '/etc/wireguard/wg0.conf';
157
+ const showCmd = isAwg ? 'awg show awg0 dump' : 'wg show wg0 dump';
138
158
 
139
159
  try {
140
160
  // 1. Read remote configuration to parse names
141
- const catRes = await sshExec(conn, 'sudo cat /etc/wireguard/wg0.conf');
161
+ const catRes = await sshExec(conn, `sudo cat ${confFile}`);
142
162
  if (catRes.code !== 0) throw new Error('Failed to read remote config');
143
163
  const content = catRes.stdout;
144
164
 
@@ -158,9 +178,9 @@ export const listPeers = async () => {
158
178
  }
159
179
  }
160
180
 
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');
181
+ // 2. Fetch tunnel status
182
+ const showRes = await sshExec(conn, `sudo ${showCmd}`);
183
+ if (showRes.code !== 0) throw new Error('Failed to run show command');
164
184
 
165
185
  const dumpLines = showRes.stdout.trim().split('\n');
166
186
  const list = [];
@@ -193,9 +213,12 @@ export const listPeers = async () => {
193
213
  export const removePeer = async (name) => {
194
214
  const info = getDeployInfo();
195
215
  const conn = await sshConnect(info);
216
+ const isAwg = info.mode === 'amneziawg';
217
+ const confFile = isAwg ? '/etc/amnezia/amneziawg/awg0.conf' : '/etc/wireguard/wg0.conf';
218
+ const quickCmd = isAwg ? 'awg-quick@awg0' : 'wg-quick@wg0';
196
219
 
197
220
  try {
198
- const catRes = await sshExec(conn, 'sudo cat /etc/wireguard/wg0.conf');
221
+ const catRes = await sshExec(conn, `sudo cat ${confFile}`);
199
222
  if (catRes.code !== 0) throw new Error('Failed to read remote config');
200
223
  const content = catRes.stdout;
201
224
 
@@ -209,11 +232,11 @@ export const removePeer = async (name) => {
209
232
  const newContent = filteredBlocks.join('[Peer]');
210
233
 
211
234
  // 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`);
235
+ const writeRes = await sshExec(conn, `cat << 'EOF' > /tmp/temp.conf\n${newContent}\nEOF\nsudo mv /tmp/temp.conf ${confFile} && sudo chmod 600 ${confFile}`);
213
236
  if (writeRes.code !== 0) throw new Error('Failed to update remote config');
214
237
 
215
- // Sync remote wireguard
216
- await sshExec(conn, 'sudo systemctl restart wg-quick@wg0');
238
+ // Sync remote tunnel
239
+ await sshExec(conn, `sudo systemctl restart ${quickCmd}`);
217
240
 
218
241
  // Remove local peer config if it exists
219
242
  const peerConfPath = path.join(CONFIG_DIR, 'wg', 'peers', `${name}.conf`);
@@ -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
+ };