polaris-vpn 0.6.0 → 2.0.0-beta

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,11 +5,34 @@ 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
+ ## [2.0.0-beta] - 2026-07-12
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
+
15
+ ## [1.0.0] - 2026-07-12
16
+
17
+ ### Added
18
+
19
+ - **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.
20
+ - **Terminal QR Codes**: Automatically generate terminal-rendered QR codes using `polaris peer qr <name>` for instantly setting up mobile devices (iOS/Android).
21
+ - **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.
22
+
23
+ ## [0.7.0] - 2026-07-12
24
+
25
+ ### Added
26
+
27
+ - **AmneziaWG Stealth Mode**: Added full support for AmneziaWG obfuscated WireGuard tunnels. Use `--mode amneziawg` with `polaris deploy` and `polaris start` to bypass Deep Packet Inspection (DPI) with randomized junk packet sizes and headers.
28
+ - **Automated Obfuscation Params**: The CLI now automatically generates and manages unique AmneziaWG obfuscation parameters (`Jc`, `Jmin`, `Jmax`, `S1`, `S2`, `H1`-`H4`) without requiring manual user configuration.
29
+
8
30
  ## [0.6.0] - 2026-07-12
9
31
 
10
32
  ### Added
11
33
 
12
34
  - **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.
35
+ - **TUI Dashboard**: Added the `polaris dashboard` command, which launches a rich Terminal UI utilizing `blessed` and `blessed-contrib` to render live Rx/Tx bandwidth graphs, GeoIP server maps, and real-time WireGuard peer data.
13
36
 
14
37
  ## [0.5.0] - 2026-07-12
15
38
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)
4
4
 
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.
5
+ 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
6
 
7
7
  ### Prerequisites
8
8
 
@@ -45,10 +45,17 @@ polaris stop
45
45
 
46
46
  | Command | Description |
47
47
  | --- | --- |
48
- | `polaris start --server user@host [--port 1080]` | Start the encrypted SSH tunnel. |
48
+ | `polaris deploy --server user@host --mode amneziawg` | Provision server with AmneziaWG (Stealth Mode) |
49
+ | `polaris start --server user@host [--port 1080] [--mode <type>]` | Start the encrypted tunnel. |
49
50
  | `polaris stop` | Stop the active tunnel. |
51
+ | `polaris dashboard` | Opens a rich Terminal User Interface (TUI) with real-time stats. |
50
52
  | `polaris status [--full]` | Show current tunnel status. `--full` shows GeoIP, ping latency, and WG stats. |
51
53
  | `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
54
+ | `polaris killswitch on/off` | Enable or disable the system-wide VPN kill switch. |
55
+ | `polaris peer add <name>` | Generate and add a new peer config for WireGuard/AmneziaWG. |
56
+ | `polaris peer list` | List all peers configured on the server. |
57
+ | `polaris peer remove <name>` | Revoke a peer from the server. |
58
+ | `polaris peer qr <name>` | Display the peer config as a QR code for mobile devices. |
52
59
  | `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
53
60
  | `polaris add <alias> --server user@host` | Save a server profile for quick access. |
54
61
  | `polaris list` | List all saved server profiles. |
@@ -56,6 +63,22 @@ polaris stop
56
63
  | `polaris server start` | Start a local REST API on `127.0.0.1:7070`. |
57
64
  | `polaris update` | Update `polaris-vpn` to the latest version via npm. |
58
65
 
66
+ #### `polaris dashboard`
67
+
68
+ Opens a rich Terminal User Interface (TUI) with real-time graphs, server location maps, and live statistics (Rx/Tx, Ping).
69
+
70
+ #### `polaris status [--full]`
71
+
72
+ Displays the connection state, mode, current proxy IP, and active server. Use `--full` to query GeoIP and ping latency.
73
+
74
+ #### `polaris monitor`
75
+
76
+ Live bandwidth monitor that actively graphs the download and upload speeds of your tunnel.
77
+
78
+ #### `polaris check`
79
+
80
+ Checks if your IP matches the proxy, tests for DNS leaks via `ipleak.net`, and verifies IPv6 routing.
81
+
59
82
  > All commands support the `--json` flag for machine-readable output.
60
83
 
61
84
  ## Oracle Cloud Free Tier Setup
@@ -90,10 +113,11 @@ One Oracle VM can serve multiple people:
90
113
 
91
114
  ## Roadmap
92
115
 
93
- - **v0.1**: MVP — SSH SOCKS5 tunnel (Current)
94
- - **v0.2**: TLS tunnel + DNS-over-HTTPS + leak checker
116
+ - **v0.1**: MVP — SSH SOCKS5 tunnel
95
117
  - **v0.5**: WireGuard full tunnel + server provisioning
96
- - **v1.0**: AmneziaWG stealth mode + multi-peer + QR codes + kill switch
118
+ - **v0.6**: Windows NT Service bindings + Blessed TUI Dashboard
119
+ - **v0.7**: AmneziaWG stealth mode (DPI bypass)
120
+ - **v1.0**: Multi-peer management + QR codes + Kill switch (Current)
97
121
 
98
122
  ## Contributing
99
123
 
@@ -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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "0.6.0",
3
+ "version": "2.0.0-beta",
4
4
  "description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,10 +9,15 @@
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": "https://github.com/Divyo/polaris-vpn.git"
20
+ },
16
21
  "engines": {
17
22
  "node": ">=18"
18
23
  },
@@ -32,6 +37,8 @@
32
37
  "oracle-cloud"
33
38
  ],
34
39
  "dependencies": {
40
+ "blessed": "^0.1.81",
41
+ "blessed-contrib": "^4.11.0",
35
42
  "chalk": "^5.6.2",
36
43
  "cli-table3": "^0.6.5",
37
44
  "commander": "^15.0.0",
package/src/cli.js CHANGED
@@ -41,10 +41,10 @@ program
41
41
 
42
42
  program
43
43
  .command('start')
44
- .description('Start the encrypted SSH, TLS or WireGuard tunnel')
44
+ .description('Start the encrypted SSH, TLS, WireGuard or AmneziaWG tunnel')
45
45
  .option('-s, --server <user@host>', 'SSH/TLS/WireGuard server to connect to')
46
46
  .option('-p, --port <number>', 'Local SOCKS5 port to bind', '1080')
47
- .option('-m, --mode <type>', 'Tunnel mode: ssh, tls, wireguard or auto', 'auto')
47
+ .option('-m, --mode <type>', 'Tunnel mode: ssh, tls, wireguard, amneziawg or auto', 'auto')
48
48
  .action(async (options, cmd) => {
49
49
  if (!cmd.optsWithGlobals().json) printBanner();
50
50
  try {
@@ -85,6 +85,19 @@ program
85
85
  }
86
86
  });
87
87
 
88
+ program
89
+ .command('dashboard')
90
+ .description('Open the live TUI dashboard')
91
+ .action(async (options, cmd) => {
92
+ try {
93
+ const run = (await import('./commands/dashboard.js')).default;
94
+ await run(cmd.optsWithGlobals());
95
+ } catch (err) {
96
+ printError('Command failed', err);
97
+ process.exit(1);
98
+ }
99
+ });
100
+
88
101
  program
89
102
  .command('check')
90
103
  .description('Run a 3-point privacy check (IP, DNS leak, IPv6 leak)')
@@ -190,10 +203,11 @@ dnsCmd
190
203
 
191
204
  program
192
205
  .command('deploy')
193
- .description('Provision a fresh Ubuntu server as a WireGuard server')
206
+ .description('Provision a remote server with WireGuard or AmneziaWG')
194
207
  .requiredOption('-s, --server <user@host>', 'SSH server to configure')
195
208
  .option('-i, --identity <path>', 'SSH private key file path')
196
209
  .option('-p, --password <password>', 'SSH password (alternative to identity)')
210
+ .option('-m, --mode <type>', 'Tunnel mode: wireguard or amneziawg', 'wireguard')
197
211
  .action(async (options, cmd) => {
198
212
  if (!cmd.optsWithGlobals().json) printBanner();
199
213
  try {
@@ -205,7 +219,39 @@ program
205
219
  }
206
220
  });
207
221
 
208
- const peerCmd = program.command('peer').description('Manage WireGuard client peers');
222
+ const ksCmd = program.command('killswitch').description('Manage the system VPN kill switch');
223
+
224
+ ksCmd
225
+ .command('on')
226
+ .description('Enable the kill switch')
227
+ .action(async (options, cmd) => {
228
+ if (!cmd.optsWithGlobals().json) printBanner();
229
+ try {
230
+ const { setKillSwitchConfig } = await import('./utils/kill-switch.js');
231
+ setKillSwitchConfig(true);
232
+ printSuccess('Kill switch enabled. All non-VPN traffic will be blocked when the tunnel is active.');
233
+ } catch (err) {
234
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
235
+ process.exit(1);
236
+ }
237
+ });
238
+
239
+ ksCmd
240
+ .command('off')
241
+ .description('Disable the kill switch')
242
+ .action(async (options, cmd) => {
243
+ if (!cmd.optsWithGlobals().json) printBanner();
244
+ try {
245
+ const { setKillSwitchConfig } = await import('./utils/kill-switch.js');
246
+ setKillSwitchConfig(false);
247
+ printSuccess('Kill switch disabled.');
248
+ } catch (err) {
249
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
250
+ process.exit(1);
251
+ }
252
+ });
253
+
254
+ const peerCmd = program.command('peer').description('Manage WireGuard/AmneziaWG client peers');
209
255
 
210
256
  peerCmd
211
257
  .command('add <name>')
@@ -0,0 +1,229 @@
1
+ import blessed from 'blessed';
2
+ import contrib from 'blessed-contrib';
3
+ import { getActiveTunnel } from '../core/tunnel-service.js';
4
+ import { getProxiedIp } from '../net/ip-check.js';
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 {
31
+ text: `${data.city}, ${data.country}`,
32
+ lat: data.lat,
33
+ lon: data.lon,
34
+ isp: data.isp
35
+ };
36
+ }
37
+ } catch (err) {}
38
+ return null;
39
+ };
40
+
41
+ const formatBytes = (bytes) => {
42
+ if (bytes === 0) return '0 B';
43
+ const k = 1024;
44
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
45
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
46
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
47
+ };
48
+
49
+ const getWgStats = () => {
50
+ const dumpRes = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
51
+ if (dumpRes.status !== 0) return null;
52
+ const lines = dumpRes.stdout.trim().split('\n');
53
+ if (lines.length <= 1) return null;
54
+
55
+ let totalRx = 0;
56
+ let totalTx = 0;
57
+ const peers = [];
58
+ for (let i = 1; i < lines.length; i++) {
59
+ const peerInfo = lines[i].split('\t');
60
+ const rx = parseInt(peerInfo[6], 10) || 0;
61
+ const tx = parseInt(peerInfo[7], 10) || 0;
62
+ totalRx += rx;
63
+ totalTx += tx;
64
+ peers.push({
65
+ pubKey: peerInfo[1].substring(0, 8) + '...',
66
+ endpoint: peerInfo[4],
67
+ rx,
68
+ tx
69
+ });
70
+ }
71
+ return { totalRx, totalTx, peers };
72
+ };
73
+
74
+ export default async () => {
75
+ const info = getActiveTunnel();
76
+
77
+ if (!info) {
78
+ console.error('Tunnel is down. Start the tunnel first to use the dashboard.');
79
+ process.exit(1);
80
+ }
81
+
82
+ const screen = blessed.screen({
83
+ smartCSR: true,
84
+ title: 'Polaris VPN Dashboard'
85
+ });
86
+
87
+ const grid = new contrib.grid({ rows: 12, cols: 12, screen: screen });
88
+
89
+ // 1. Header Box (Status, IP, Uptime)
90
+ const headerBox = grid.set(0, 0, 3, 4, blessed.box, {
91
+ label: ' Tunnel Status ',
92
+ content: 'Loading...',
93
+ tags: true,
94
+ style: { fg: 'green', border: { fg: 'cyan' } }
95
+ });
96
+
97
+ // 2. Line Chart (Bandwidth)
98
+ const lineChart = grid.set(0, 4, 6, 8, contrib.line, {
99
+ label: ' Bandwidth (Bytes/s) ',
100
+ showLegend: true,
101
+ legend: { width: 12 },
102
+ style: { baseline: 'gray', line: 'yellow', text: 'green' },
103
+ xLabelPadding: 3,
104
+ xPadding: 5
105
+ });
106
+
107
+ // 3. Map (GeoIP)
108
+ const mapWidget = grid.set(3, 0, 9, 7, contrib.map, {
109
+ label: ' Server Location ',
110
+ style: { shapeColor: 'cyan' }
111
+ });
112
+
113
+ // 4. Peers Table
114
+ const peersTable = grid.set(6, 7, 6, 5, contrib.table, {
115
+ keys: true,
116
+ fg: 'white',
117
+ selectedFg: 'white',
118
+ selectedBg: 'blue',
119
+ interactive: false,
120
+ label: ' Active Peers ',
121
+ width: '100%',
122
+ height: '100%',
123
+ border: { type: 'line', fg: 'cyan' },
124
+ columnSpacing: 2,
125
+ columnWidth: [15, 20, 10, 10]
126
+ });
127
+
128
+ const rxData = { title: 'RX', x: [], y: [], style: { line: 'green' } };
129
+ const txData = { title: 'TX', x: [], y: [], style: { line: 'red' } };
130
+
131
+ // Seed initial chart data
132
+ for (let i = 0; i < 20; i++) {
133
+ const t = new Date(Date.now() - (20 - i) * 1000).toLocaleTimeString().split(' ')[0];
134
+ rxData.x.push(t);
135
+ rxData.y.push(0);
136
+ txData.x.push(t);
137
+ txData.y.push(0);
138
+ }
139
+
140
+ let prevRx = 0;
141
+ let prevTx = 0;
142
+ let hasInitWg = false;
143
+ let currentIp = 'Loading...';
144
+ let geoData = null;
145
+ let latency = '...';
146
+ let geoFetched = false;
147
+
148
+ screen.key(['escape', 'q', 'C-c'], () => {
149
+ return process.exit(0);
150
+ });
151
+
152
+ const updateDashboard = async () => {
153
+ // 1. Fetch IP & GeoIP (runs once)
154
+ if (!geoFetched) {
155
+ geoFetched = true;
156
+ try {
157
+ currentIp = await getProxiedIp(info.port);
158
+ geoData = await getGeoIp(currentIp);
159
+ if (geoData) {
160
+ mapWidget.addMarker({ lat: geoData.lat, lon: geoData.lon, color: 'red', char: 'X' });
161
+ }
162
+ } catch (err) {}
163
+ }
164
+
165
+ // 2. Fetch Ping
166
+ const serverIp = info.server.split('@').pop();
167
+ latency = pingServer(serverIp);
168
+
169
+ // 3. Update Header
170
+ const uptimeMin = Math.floor((Date.now() - new Date(info.startTime).getTime()) / 60000);
171
+ headerBox.setContent(
172
+ `{bold}Status{/bold}: {green-fg}UP{/green-fg}\n` +
173
+ `{bold}Server{/bold}: ${info.server}\n` +
174
+ `{bold}Mode{/bold}: ${(info.mode || 'ssh').toUpperCase()}\n` +
175
+ `{bold}IP{/bold}: ${currentIp}\n` +
176
+ `{bold}Uptime{/bold}: ${uptimeMin} min\n` +
177
+ `{bold}Ping{/bold}: ${latency}\n` +
178
+ `{bold}GeoIP{/bold}: ${geoData ? geoData.text : 'N/A'}`
179
+ );
180
+
181
+ // 4. Update WG Stats & Chart
182
+ if (info.mode === 'wireguard' || info.mode === 'amneziawg' || info.mode === 'auto') {
183
+ const stats = getWgStats();
184
+ if (stats) {
185
+ if (!hasInitWg) {
186
+ prevRx = stats.totalRx;
187
+ prevTx = stats.totalTx;
188
+ hasInitWg = true;
189
+ } else {
190
+ const rxSpeed = stats.totalRx - prevRx;
191
+ const txSpeed = stats.totalTx - prevTx;
192
+ prevRx = stats.totalRx;
193
+ prevTx = stats.totalTx;
194
+
195
+ const t = new Date().toLocaleTimeString().split(' ')[0];
196
+ rxData.x.shift();
197
+ rxData.x.push(t);
198
+ rxData.y.shift();
199
+ rxData.y.push(rxSpeed);
200
+
201
+ txData.x.shift();
202
+ txData.x.push(t);
203
+ txData.y.shift();
204
+ txData.y.push(txSpeed);
205
+
206
+ lineChart.setData([rxData, txData]);
207
+ }
208
+
209
+ const tableData = stats.peers.map(p => [
210
+ p.pubKey,
211
+ p.endpoint || 'N/A',
212
+ formatBytes(p.rx),
213
+ formatBytes(p.tx)
214
+ ]);
215
+
216
+ peersTable.setData({
217
+ headers: ['Peer', 'Endpoint', 'RX', 'TX'],
218
+ data: tableData.length ? tableData : [['No peers', '-', '-', '-']]
219
+ });
220
+ }
221
+ }
222
+
223
+ screen.render();
224
+ };
225
+
226
+ setInterval(updateDashboard, 1000);
227
+ updateDashboard();
228
+ screen.render();
229
+ };
@@ -88,8 +88,21 @@ export default async (options) => {
88
88
  const hostPart = server.includes('@') ? server.split('@')[1] : server;
89
89
  let actualMode = requestedMode;
90
90
 
91
+ const AWG_CONF = path.join(CONFIG_DIR, 'wg', 'awg0.conf');
92
+
93
+ const hasAwgQuick = () => {
94
+ try {
95
+ const res = spawnSync('which', ['awg-quick'], { encoding: 'utf-8' });
96
+ return res.status === 0;
97
+ } catch (e) {
98
+ return false;
99
+ }
100
+ };
101
+
91
102
  if (requestedMode === 'auto') {
92
- if (fs.existsSync(WG_CONF) && hasWgQuick()) {
103
+ if (fs.existsSync(AWG_CONF) && hasAwgQuick()) {
104
+ actualMode = 'amneziawg';
105
+ } else if (fs.existsSync(WG_CONF) && hasWgQuick()) {
93
106
  actualMode = 'wireguard';
94
107
  } else {
95
108
  if (!isJson) {
@@ -115,7 +128,7 @@ export default async (options) => {
115
128
 
116
129
  const res = await startTunnel(server, port, actualMode, isJson);
117
130
 
118
- if (actualMode === 'wireguard') {
131
+ if (actualMode === 'wireguard' || actualMode === 'amneziawg') {
119
132
  if (!isJson) {
120
133
  spinner.text = 'Waiting for interface to configure...';
121
134
  }
@@ -128,12 +141,12 @@ export default async (options) => {
128
141
 
129
142
  if (!isJson) {
130
143
  spinner.stop();
131
- printSuccess(`WireGuard full tunnel established successfully to ${server}`);
144
+ printSuccess(`${actualMode === 'amneziawg' ? 'AmneziaWG (Stealth)' : 'WireGuard'} full tunnel established successfully to ${server}`);
132
145
  console.log(`\n ${chalk.dim('Old IP:')} ${chalk.red(oldIp)}`);
133
146
  console.log(` ${chalk.dim('New IP:')} ${chalk.green(newIp)}`);
134
147
  console.log(` ${chalk.dim('Routing:')} System-wide (All OS traffic)`);
135
148
  } else {
136
- console.log(JSON.stringify({ success: true, oldIp, newIp, mode: 'wireguard', pid: res.pid }));
149
+ console.log(JSON.stringify({ success: true, oldIp, newIp, mode: actualMode, pid: res.pid }));
137
150
  }
138
151
  } else {
139
152
  if (!isJson) {
@@ -2,7 +2,7 @@ import { Client } from 'ssh2';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import { generateKeyPair } from '../tunnel/wg.js';
5
+ import { generateKeyPair, generateAwgParams } from '../tunnel/wg.js';
6
6
  import { ensureDir, CONFIG_DIR } from '../utils/config.js';
7
7
 
8
8
  const getDefaultPrivateKey = () => {
@@ -37,6 +37,7 @@ export const deployServer = async (serverStr, options = {}) => {
37
37
  const parts = serverStr.split('@');
38
38
  const username = parts.length > 1 ? parts[0] : 'ubuntu';
39
39
  const host = parts.length > 1 ? parts[1] : parts[0];
40
+ const isAwg = options.mode === 'amneziawg';
40
41
 
41
42
  const privateKey = options.privateKey
42
43
  ? fs.readFileSync(options.privateKey)
@@ -49,6 +50,7 @@ export const deployServer = async (serverStr, options = {}) => {
49
50
  // 1. Generate local keys
50
51
  const serverKeys = generateKeyPair();
51
52
  const clientKeys = generateKeyPair();
53
+ const awgParams = isAwg ? generateAwgParams() : null;
52
54
 
53
55
  const conn = new Client();
54
56
 
@@ -56,10 +58,17 @@ export const deployServer = async (serverStr, options = {}) => {
56
58
  conn.on('ready', async () => {
57
59
  try {
58
60
  const onProgress = options.onProgress || (() => {});
61
+ const ifaceName = isAwg ? 'awg0' : 'wg0';
62
+ const configDir = isAwg ? '/etc/amnezia/amneziawg' : '/etc/wireguard';
63
+ const quickCmd = isAwg ? 'awg-quick' : 'wg-quick';
59
64
 
60
65
  // Step 1: Install packages
61
- onProgress('Installing WireGuard and UFW on remote VPS...');
62
- let res = await sshExec(conn, 'sudo apt-get update -y && sudo apt-get install -y wireguard ufw');
66
+ onProgress(`Installing ${isAwg ? 'AmneziaWG' : 'WireGuard'}, UFW, Unbound, and Fail2Ban...`);
67
+ let installCmd = 'sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard ufw unbound fail2ban';
68
+ if (isAwg) {
69
+ 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
+ }
71
+ let res = await sshExec(conn, installCmd);
63
72
  if (res.code !== 0) throw new Error(`Installation failed: ${res.stderr}`);
64
73
 
65
74
  // Step 2: Enable packet forwarding
@@ -73,26 +82,59 @@ export const deployServer = async (serverStr, options = {}) => {
73
82
  const ethInterface = res.stdout.trim() || 'eth0';
74
83
 
75
84
  // Step 4: Write server config
76
- onProgress('Configuring WireGuard server wg0...');
85
+ onProgress(`Configuring server ${ifaceName}...`);
86
+
87
+ let obfuscationBlock = '';
88
+ if (isAwg) {
89
+ obfuscationBlock = `Jc = ${awgParams.Jc}
90
+ Jmin = ${awgParams.Jmin}
91
+ Jmax = ${awgParams.Jmax}
92
+ S1 = ${awgParams.S1}
93
+ S2 = ${awgParams.S2}
94
+ H1 = ${awgParams.H1}
95
+ H2 = ${awgParams.H2}
96
+ H3 = ${awgParams.H3}
97
+ H4 = ${awgParams.H4}`;
98
+ }
99
+
77
100
  const serverConf = `[Interface]
78
101
  PrivateKey = ${serverKeys.privateKey}
79
102
  Address = 10.0.0.1/24
80
103
  ListenPort = 51820
81
- PostUp = ufw route allow in on wg0; iptables -t nat -A POSTROUTING -o ${ethInterface} -j MASQUERADE
82
- PostDown = ufw route delete allow in on wg0; iptables -t nat -D POSTROUTING -o ${ethInterface} -j MASQUERADE
104
+ PostUp = ufw route allow in on ${ifaceName}; iptables -t nat -A POSTROUTING -o ${ethInterface} -j MASQUERADE
105
+ PostDown = ufw route delete allow in on ${ifaceName}; iptables -t nat -D POSTROUTING -o ${ethInterface} -j MASQUERADE
106
+ ${obfuscationBlock}
83
107
 
84
108
  [Peer]
85
109
  PublicKey = ${clientKeys.publicKey}
86
110
  AllowedIPs = 10.0.0.2/32
87
111
  `;
88
112
 
89
- res = await sshExec(conn, `cat << 'EOF' > /tmp/wg0.conf\n${serverConf}\nEOF\nsudo mv /tmp/wg0.conf /etc/wireguard/wg0.conf && sudo chmod 600 /etc/wireguard/wg0.conf`);
113
+ res = await sshExec(conn, `sudo mkdir -p ${configDir} && cat << 'EOF' > /tmp/${ifaceName}.conf\n${serverConf}\nEOF\nsudo mv /tmp/${ifaceName}.conf ${configDir}/${ifaceName}.conf && sudo chmod 600 ${configDir}/${ifaceName}.conf`);
90
114
  if (res.code !== 0) throw new Error(`Server config write failed: ${res.stderr}`);
91
115
 
92
116
  // Step 5: Start service
93
- onProgress('Starting WireGuard interface...');
94
- res = await sshExec(conn, 'sudo systemctl stop wg-quick@wg0 || true && sudo systemctl start wg-quick@wg0 && sudo systemctl enable wg-quick@wg0');
95
- if (res.code !== 0) throw new Error(`Starting WireGuard failed: ${res.stderr}`);
117
+ onProgress(`Starting ${ifaceName} interface...`);
118
+ res = await sshExec(conn, `sudo systemctl stop ${quickCmd}@${ifaceName} || true && sudo systemctl start ${quickCmd}@${ifaceName} && sudo systemctl enable ${quickCmd}@${ifaceName}`);
119
+ if (res.code !== 0) throw new Error(`Starting tunnel failed: ${res.stderr}`);
120
+
121
+ // Step 5b: Configure Unbound DNS
122
+ onProgress('Configuring Unbound Zero-Log DNS...');
123
+ const unboundConf = `server:
124
+ interface: 10.0.0.1
125
+ access-control: 10.0.0.0/24 allow
126
+ hide-identity: yes
127
+ hide-version: yes
128
+ use-caps-for-id: yes
129
+ prefetch: yes
130
+ `;
131
+ 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`);
132
+ if (res.code !== 0) throw new Error(`Unbound setup failed: ${res.stderr}`);
133
+
134
+ // Step 5c: Start Fail2Ban
135
+ onProgress('Starting Fail2Ban SSH protection...');
136
+ res = await sshExec(conn, 'sudo systemctl enable fail2ban && sudo systemctl restart fail2ban');
137
+ if (res.code !== 0) throw new Error(`Fail2ban setup failed: ${res.stderr}`);
96
138
 
97
139
  // Step 6: Configure UFW firewall
98
140
  onProgress('Configuring UFW firewall...');
@@ -104,7 +146,8 @@ AllowedIPs = 10.0.0.2/32
104
146
  const clientConf = `[Interface]
105
147
  PrivateKey = ${clientKeys.privateKey}
106
148
  Address = 10.0.0.2/24
107
- DNS = 1.1.1.1
149
+ DNS = 10.0.0.1
150
+ ${obfuscationBlock}
108
151
 
109
152
  [Peer]
110
153
  PublicKey = ${serverKeys.publicKey}
@@ -117,15 +160,17 @@ PersistentKeepalive = 25
117
160
  ensureDir(wgDir);
118
161
 
119
162
  // Save client config
120
- const clientConfPath = path.join(wgDir, 'wg0.conf');
163
+ const clientConfPath = path.join(wgDir, `${ifaceName}.conf`);
121
164
  fs.writeFileSync(clientConfPath, clientConf, 'utf-8');
122
165
 
123
166
  // Also save provisioning info to config
124
167
  fs.writeFileSync(path.join(wgDir, 'deploy.json'), JSON.stringify({
125
168
  server: serverStr,
169
+ mode: options.mode || 'wireguard',
126
170
  serverPublicKey: serverKeys.publicKey,
127
171
  clientPublicKey: clientKeys.publicKey,
128
172
  interface: ethInterface,
173
+ awgParams: isAwg ? awgParams : null,
129
174
  timestamp: new Date().toISOString()
130
175
  }, null, 2), 'utf-8');
131
176
 
@@ -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`);
@@ -9,15 +9,17 @@ import { loadDaemonState, clearDaemonState, killPid, saveDaemonState } from '../
9
9
  import { TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, CONFIG_DIR, ensureDir } from '../utils/config.js';
10
10
 
11
11
  const WG_CONF = path.join(CONFIG_DIR, 'wg', 'wg0.conf');
12
+ const AWG_CONF = path.join(CONFIG_DIR, 'wg', 'awg0.conf');
12
13
 
13
14
  export const getActiveTunnel = () => {
14
15
  const info = loadDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
15
16
  if (!info) return null;
16
17
 
17
- // For WireGuard, PID file tracks start process, but we verify interface status
18
- if (info.mode === 'wireguard') {
18
+ // For WireGuard/AmneziaWG, PID file tracks start process, but we verify interface status
19
+ if (info.mode === 'wireguard' || info.mode === 'amneziawg') {
20
+ const showCmd = info.mode === 'amneziawg' ? 'awg' : 'wg';
19
21
  try {
20
- const showRes = spawnSync('sudo', ['wg', 'show'], { encoding: 'utf-8' });
22
+ const showRes = spawnSync('sudo', [showCmd, 'show'], { encoding: 'utf-8' });
21
23
  if (showRes.status !== 0 || !showRes.stdout.includes('interface:')) {
22
24
  clearDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
23
25
  return null;
@@ -33,8 +35,9 @@ export const getActiveTunnel = () => {
33
35
  export const stopActiveTunnel = (isJson = false) => {
34
36
  const info = getActiveTunnel();
35
37
  if (info) {
36
- if (info.mode === 'wireguard') {
37
- stopWgTunnel(WG_CONF, isJson);
38
+ if (info.mode === 'wireguard' || info.mode === 'amneziawg') {
39
+ const confPath = info.mode === 'amneziawg' ? AWG_CONF : WG_CONF;
40
+ stopWgTunnel(confPath, isJson, info.mode === 'amneziawg');
38
41
  disableKillSwitch(info.server);
39
42
  } else {
40
43
  killPid(info.pid);
@@ -49,24 +52,26 @@ export const startTunnel = async (server, port, mode = 'ssh', isJson = false) =>
49
52
  ensureDir();
50
53
 
51
54
  let pid;
52
- if (mode === 'wireguard') {
53
- if (!fs.existsSync(WG_CONF)) {
54
- throw new Error(`WireGuard client configuration not found at ${WG_CONF}. Please provision the server first with "polaris deploy".`);
55
+ if (mode === 'wireguard' || mode === 'amneziawg') {
56
+ const isAwg = mode === 'amneziawg';
57
+ const confPath = isAwg ? AWG_CONF : WG_CONF;
58
+ if (!fs.existsSync(confPath)) {
59
+ throw new Error(`${isAwg ? 'AmneziaWG' : 'WireGuard'} client configuration not found at ${confPath}. Please provision the server first with "polaris deploy".`);
55
60
  }
56
61
 
57
- pid = startWgTunnel(WG_CONF, isJson);
62
+ pid = startWgTunnel(confPath, isJson, isAwg);
58
63
 
59
64
  // Save state before enableKillSwitch in case sudo prompt needs to block
60
65
  saveDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, {
61
66
  pid,
62
67
  server,
63
68
  port: 0,
64
- mode: 'wireguard',
69
+ mode: mode,
65
70
  startTime: new Date().toISOString()
66
71
  });
67
72
 
68
73
  enableKillSwitch(server);
69
- return { pid, server, port: 0, mode: 'wireguard' };
74
+ return { pid, server, port: 0, mode: mode };
70
75
  }
71
76
 
72
77
  if (mode === 'tls') {
package/src/tunnel/wg.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { spawnSync, spawn } from 'child_process';
2
2
  import crypto from 'crypto';
3
+ import os from 'os';
4
+ import path from 'path';
3
5
 
4
6
  // Pure JS Curve25519/X25519 key generator matching WireGuard format
5
7
  export const generateKeyPair = () => {
@@ -27,11 +29,24 @@ export const generateKeyPair = () => {
27
29
  };
28
30
  };
29
31
 
30
- import os from 'os';
31
- import path from 'path';
32
+ export const generateAwgParams = () => {
33
+ return {
34
+ Jc: crypto.randomInt(1, 100),
35
+ Jmin: crypto.randomInt(1, 50),
36
+ Jmax: crypto.randomInt(50, 1000),
37
+ S1: crypto.randomInt(15, 150),
38
+ S2: crypto.randomInt(15, 150),
39
+ H1: crypto.randomInt(1, 2147483647),
40
+ H2: crypto.randomInt(1, 2147483647),
41
+ H3: crypto.randomInt(1, 2147483647),
42
+ H4: crypto.randomInt(1, 2147483647)
43
+ };
44
+ };
32
45
 
33
- export const startWgTunnel = (confPath, isJson = false) => {
46
+ export const startWgTunnel = (confPath, isJson = false, isAwg = false) => {
34
47
  const isWin = os.platform() === 'win32';
48
+ const quickCmd = isAwg ? 'awg-quick' : 'wg-quick';
49
+ const serviceCmd = isAwg ? 'amneziawg' : 'wireguard';
35
50
 
36
51
  if (!isWin) {
37
52
  const sudoCheck = spawnSync('sudo', ['-n', 'true']);
@@ -41,27 +56,29 @@ export const startWgTunnel = (confPath, isJson = false) => {
41
56
  }
42
57
 
43
58
  if (isWin) {
44
- const res = spawnSync('wireguard', ['/installtunnelservice', confPath], {
59
+ const res = spawnSync(serviceCmd, ['/installtunnelservice', confPath], {
45
60
  stdio: isJson ? 'ignore' : 'inherit'
46
61
  });
47
62
  if (res.status !== 0) {
48
- throw new Error(`wireguard /installtunnelservice failed (ensure Admin privileges) with code ${res.status}`);
63
+ throw new Error(`${serviceCmd} /installtunnelservice failed (ensure Admin privileges) with code ${res.status}`);
49
64
  }
50
65
  return process.pid;
51
66
  }
52
67
 
53
- const res = spawnSync('sudo', ['wg-quick', 'up', confPath], {
68
+ const res = spawnSync('sudo', [quickCmd, 'up', confPath], {
54
69
  stdio: isJson ? 'ignore' : 'inherit'
55
70
  });
56
71
 
57
72
  if (res.status !== 0) {
58
- throw new Error(`wg-quick up failed with code ${res.status}`);
73
+ throw new Error(`${quickCmd} up failed with code ${res.status}`);
59
74
  }
60
75
  return process.pid;
61
76
  };
62
77
 
63
- export const stopWgTunnel = (confPath, isJson = false) => {
78
+ export const stopWgTunnel = (confPath, isJson = false, isAwg = false) => {
64
79
  const isWin = os.platform() === 'win32';
80
+ const quickCmd = isAwg ? 'awg-quick' : 'wg-quick';
81
+ const serviceCmd = isAwg ? 'amneziawg' : 'wireguard';
65
82
 
66
83
  if (!isWin) {
67
84
  const sudoCheck = spawnSync('sudo', ['-n', 'true']);
@@ -72,20 +89,20 @@ export const stopWgTunnel = (confPath, isJson = false) => {
72
89
 
73
90
  if (isWin) {
74
91
  const interfaceName = path.parse(confPath).name;
75
- const res = spawnSync('wireguard', ['/uninstalltunnelservice', interfaceName], {
92
+ const res = spawnSync(serviceCmd, ['/uninstalltunnelservice', interfaceName], {
76
93
  stdio: isJson ? 'ignore' : 'inherit'
77
94
  });
78
95
  if (res.status !== 0) {
79
- throw new Error(`wireguard /uninstalltunnelservice failed with code ${res.status}`);
96
+ throw new Error(`${serviceCmd} /uninstalltunnelservice failed with code ${res.status}`);
80
97
  }
81
98
  return;
82
99
  }
83
100
 
84
- const res = spawnSync('sudo', ['wg-quick', 'down', confPath], {
101
+ const res = spawnSync('sudo', [quickCmd, 'down', confPath], {
85
102
  stdio: isJson ? 'ignore' : 'inherit'
86
103
  });
87
104
 
88
105
  if (res.status !== 0) {
89
- throw new Error(`wg-quick down failed with code ${res.status}`);
106
+ throw new Error(`${quickCmd} down failed with code ${res.status}`);
90
107
  }
91
108
  };