polaris-vpn 0.5.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 +14 -0
- package/README.md +25 -9
- package/package.json +3 -1
- package/src/cli.js +17 -3
- package/src/commands/dashboard.js +229 -0
- package/src/commands/start.js +17 -4
- package/src/core/deploy-service.js +37 -11
- package/src/core/tunnel-service.js +16 -11
- package/src/tunnel/wg.js +61 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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
|
+
|
|
15
|
+
## [0.6.0] - 2026-07-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
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.
|
|
21
|
+
|
|
8
22
|
## [0.5.0] - 2026-07-12
|
|
9
23
|
|
|
10
24
|
### Added
|
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Your True North in Digital Privacy. `polaris` is a production-quality, open-source, self-hosted VPN CLI tool. It
|
|
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
|
+
### Prerequisites
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
8
|
+
- Node.js (v18+)
|
|
9
|
+
- SSH access to a remote Linux VPS (Ubuntu/Debian recommended)
|
|
10
|
+
- **Linux/macOS**: `sudo` privileges for configuring WireGuard and network routes locally.
|
|
11
|
+
- **Windows**: The official [WireGuard for Windows](https://www.wireguard.com/install/) client must be installed, and `polaris` must be run from an Administrator prompt.
|
|
11
12
|
|
|
12
13
|
We highly recommend **Oracle Cloud Free Tier** for your VPS.
|
|
13
14
|
|
|
@@ -43,8 +44,10 @@ polaris stop
|
|
|
43
44
|
|
|
44
45
|
| Command | Description |
|
|
45
46
|
| --- | --- |
|
|
46
|
-
| `polaris
|
|
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. |
|
|
47
49
|
| `polaris stop` | Stop the active tunnel. |
|
|
50
|
+
| `polaris dashboard` | Opens a rich Terminal User Interface (TUI) with real-time stats. |
|
|
48
51
|
| `polaris status [--full]` | Show current tunnel status. `--full` shows GeoIP, ping latency, and WG stats. |
|
|
49
52
|
| `polaris monitor` | Live bandwidth monitor for WireGuard tunnels. |
|
|
50
53
|
| `polaris check` | Run a 3-point privacy check (IP, DNS leak, IPv6 leak). |
|
|
@@ -54,6 +57,18 @@ polaris stop
|
|
|
54
57
|
| `polaris server start` | Start a local REST API on `127.0.0.1:7070`. |
|
|
55
58
|
| `polaris update` | Update `polaris-vpn` to the latest version via npm. |
|
|
56
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
|
+
|
|
57
72
|
> All commands support the `--json` flag for machine-readable output.
|
|
58
73
|
|
|
59
74
|
## Oracle Cloud Free Tier Setup
|
|
@@ -88,10 +103,11 @@ One Oracle VM can serve multiple people:
|
|
|
88
103
|
|
|
89
104
|
## Roadmap
|
|
90
105
|
|
|
91
|
-
- **v0.1**: MVP — SSH SOCKS5 tunnel
|
|
92
|
-
- **v0.2**: TLS tunnel + DNS-over-HTTPS + leak checker
|
|
106
|
+
- **v0.1**: MVP — SSH SOCKS5 tunnel
|
|
93
107
|
- **v0.5**: WireGuard full tunnel + server provisioning
|
|
94
|
-
- **
|
|
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
|
|
95
111
|
|
|
96
112
|
## Contributing
|
|
97
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polaris-vpn",
|
|
3
|
-
"version": "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
|
|
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
|
|
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
|
+
};
|
package/src/commands/start.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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:
|
|
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(
|
|
62
|
-
let
|
|
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(
|
|
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
|
|
82
|
-
PostDown = ufw route delete allow in on
|
|
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
|
|
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(
|
|
94
|
-
res = await sshExec(conn,
|
|
95
|
-
if (res.code !== 0) throw new Error(`Starting
|
|
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,
|
|
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', [
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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(
|
|
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:
|
|
69
|
+
mode: mode,
|
|
65
70
|
startTime: new Date().toISOString()
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
enableKillSwitch(server);
|
|
69
|
-
return { pid, server, port: 0, mode:
|
|
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,33 +29,80 @@ export const generateKeyPair = () => {
|
|
|
27
29
|
};
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
};
|
|
45
|
+
|
|
46
|
+
export const startWgTunnel = (confPath, isJson = false, isAwg = false) => {
|
|
47
|
+
const isWin = os.platform() === 'win32';
|
|
48
|
+
const quickCmd = isAwg ? 'awg-quick' : 'wg-quick';
|
|
49
|
+
const serviceCmd = isAwg ? 'amneziawg' : 'wireguard';
|
|
50
|
+
|
|
51
|
+
if (!isWin) {
|
|
52
|
+
const sudoCheck = spawnSync('sudo', ['-n', 'true']);
|
|
53
|
+
if (sudoCheck.status !== 0 && isJson) {
|
|
54
|
+
throw new Error('Sudo privileges required. Please run with "sudo polaris start ..." for JSON mode.');
|
|
55
|
+
}
|
|
34
56
|
}
|
|
35
57
|
|
|
36
|
-
|
|
58
|
+
if (isWin) {
|
|
59
|
+
const res = spawnSync(serviceCmd, ['/installtunnelservice', confPath], {
|
|
60
|
+
stdio: isJson ? 'ignore' : 'inherit'
|
|
61
|
+
});
|
|
62
|
+
if (res.status !== 0) {
|
|
63
|
+
throw new Error(`${serviceCmd} /installtunnelservice failed (ensure Admin privileges) with code ${res.status}`);
|
|
64
|
+
}
|
|
65
|
+
return process.pid;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const res = spawnSync('sudo', [quickCmd, 'up', confPath], {
|
|
37
69
|
stdio: isJson ? 'ignore' : 'inherit'
|
|
38
70
|
});
|
|
39
71
|
|
|
40
72
|
if (res.status !== 0) {
|
|
41
|
-
throw new Error(
|
|
73
|
+
throw new Error(`${quickCmd} up failed with code ${res.status}`);
|
|
42
74
|
}
|
|
43
75
|
return process.pid;
|
|
44
76
|
};
|
|
45
77
|
|
|
46
|
-
export const stopWgTunnel = (confPath, isJson = false) => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
export const stopWgTunnel = (confPath, isJson = false, isAwg = false) => {
|
|
79
|
+
const isWin = os.platform() === 'win32';
|
|
80
|
+
const quickCmd = isAwg ? 'awg-quick' : 'wg-quick';
|
|
81
|
+
const serviceCmd = isAwg ? 'amneziawg' : 'wireguard';
|
|
82
|
+
|
|
83
|
+
if (!isWin) {
|
|
84
|
+
const sudoCheck = spawnSync('sudo', ['-n', 'true']);
|
|
85
|
+
if (sudoCheck.status !== 0 && isJson) {
|
|
86
|
+
throw new Error('Sudo privileges required. Please run with "sudo polaris stop" for JSON mode.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isWin) {
|
|
91
|
+
const interfaceName = path.parse(confPath).name;
|
|
92
|
+
const res = spawnSync(serviceCmd, ['/uninstalltunnelservice', interfaceName], {
|
|
93
|
+
stdio: isJson ? 'ignore' : 'inherit'
|
|
94
|
+
});
|
|
95
|
+
if (res.status !== 0) {
|
|
96
|
+
throw new Error(`${serviceCmd} /uninstalltunnelservice failed with code ${res.status}`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
|
-
const res = spawnSync('sudo', [
|
|
101
|
+
const res = spawnSync('sudo', [quickCmd, 'down', confPath], {
|
|
53
102
|
stdio: isJson ? 'ignore' : 'inherit'
|
|
54
103
|
});
|
|
55
104
|
|
|
56
105
|
if (res.status !== 0) {
|
|
57
|
-
throw new Error(
|
|
106
|
+
throw new Error(`${quickCmd} down failed with code ${res.status}`);
|
|
58
107
|
}
|
|
59
108
|
};
|