polaris-vpn 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,11 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0] - 2026-07-12
9
+
10
+ ### Added
11
+
12
+ - **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.
13
+ - **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.
14
+
8
15
  ## [0.6.0] - 2026-07-12
9
16
 
10
17
  ### Added
11
18
 
12
19
  - **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.
20
+ - **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
21
 
14
22
  ## [0.5.0] - 2026-07-12
15
23
 
package/README.md CHANGED
@@ -2,8 +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.
6
-
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.
7
6
  ### Prerequisites
8
7
 
9
8
  - Node.js (v18+)
@@ -45,8 +44,10 @@ polaris stop
45
44
 
46
45
  | Command | Description |
47
46
  | --- | --- |
48
- | `polaris start --server user@host [--port 1080]` | Start the encrypted SSH tunnel. |
47
+ | `polaris deploy --server user@host --mode amneziawg` | Provision server with AmneziaWG (Stealth Mode) |
48
+ | `polaris start --server user@host [--port 1080] [--mode <type>]` | Start the encrypted tunnel. |
49
49
  | `polaris stop` | Stop the active tunnel. |
50
+ | `polaris dashboard` | Opens a rich Terminal User Interface (TUI) with real-time stats. |
50
51
  | `polaris status [--full]` | Show current tunnel status. `--full` shows GeoIP, ping latency, and WG stats. |
51
52
  | `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
52
53
  | `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
@@ -56,6 +57,18 @@ polaris stop
56
57
  | `polaris server start` | Start a local REST API on `127.0.0.1:7070`. |
57
58
  | `polaris update` | Update `polaris-vpn` to the latest version via npm. |
58
59
 
60
+ #### `polaris dashboard`
61
+ Opens a rich Terminal User Interface (TUI) with real-time graphs, server location maps, and live statistics (Rx/Tx, Ping).
62
+
63
+ #### `polaris status [--full]`
64
+ Displays the connection state, mode, current proxy IP, and active server. Use `--full` to query GeoIP and ping latency.
65
+
66
+ #### `polaris monitor`
67
+ Live bandwidth monitor that actively graphs the download and upload speeds of your tunnel.
68
+
69
+ #### `polaris check`
70
+ Checks if your IP matches the proxy, tests for DNS leaks via `ipleak.net`, and verifies IPv6 routing.
71
+
59
72
  > All commands support the `--json` flag for machine-readable output.
60
73
 
61
74
  ## Oracle Cloud Free Tier Setup
@@ -90,10 +103,11 @@ One Oracle VM can serve multiple people:
90
103
 
91
104
  ## Roadmap
92
105
 
93
- - **v0.1**: MVP — SSH SOCKS5 tunnel (Current)
94
- - **v0.2**: TLS tunnel + DNS-over-HTTPS + leak checker
106
+ - **v0.1**: MVP — SSH SOCKS5 tunnel
95
107
  - **v0.5**: WireGuard full tunnel + server provisioning
96
- - **v1.0**: AmneziaWG stealth mode + multi-peer + QR codes + kill switch
108
+ - **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
97
111
 
98
112
  ## Contributing
99
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,8 @@
32
32
  "oracle-cloud"
33
33
  ],
34
34
  "dependencies": {
35
+ "blessed": "^0.1.81",
36
+ "blessed-contrib": "^4.11.0",
35
37
  "chalk": "^5.6.2",
36
38
  "cli-table3": "^0.6.5",
37
39
  "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 {
@@ -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'} and UFW on remote VPS...`);
67
+ let installCmd = 'sudo apt-get update -y && sudo apt-get install -y wireguard ufw';
68
+ 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';
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,41 @@ 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}`);
96
120
 
97
121
  // Step 6: Configure UFW firewall
98
122
  onProgress('Configuring UFW firewall...');
@@ -105,6 +129,7 @@ AllowedIPs = 10.0.0.2/32
105
129
  PrivateKey = ${clientKeys.privateKey}
106
130
  Address = 10.0.0.2/24
107
131
  DNS = 1.1.1.1
132
+ ${obfuscationBlock}
108
133
 
109
134
  [Peer]
110
135
  PublicKey = ${serverKeys.publicKey}
@@ -117,12 +142,13 @@ PersistentKeepalive = 25
117
142
  ensureDir(wgDir);
118
143
 
119
144
  // Save client config
120
- const clientConfPath = path.join(wgDir, 'wg0.conf');
145
+ const clientConfPath = path.join(wgDir, `${ifaceName}.conf`);
121
146
  fs.writeFileSync(clientConfPath, clientConf, 'utf-8');
122
147
 
123
148
  // Also save provisioning info to config
124
149
  fs.writeFileSync(path.join(wgDir, 'deploy.json'), JSON.stringify({
125
150
  server: serverStr,
151
+ mode: options.mode || 'wireguard',
126
152
  serverPublicKey: serverKeys.publicKey,
127
153
  clientPublicKey: clientKeys.publicKey,
128
154
  interface: ethInterface,
@@ -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
  };