polaris-vpn 1.1.1 → 1.2.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 +23 -0
- package/README.md +24 -21
- package/docs/SETUP.md +41 -6
- package/package.json +1 -1
- package/src/cli.js +85 -0
- package/src/commands/benchmark.js +53 -0
- package/src/commands/bypass.js +63 -0
- package/src/commands/check.js +3 -2
- package/src/commands/dashboard.js +10 -5
- package/src/commands/deploy.js +3 -2
- package/src/commands/import-export.js +63 -0
- package/src/commands/monitor.js +1 -1
- package/src/commands/start.js +34 -7
- package/src/commands/status.js +22 -9
- package/src/commands/stop.js +7 -0
- package/src/commands/tui.js +21 -4
- package/src/core/benchmark-service.js +79 -0
- package/src/core/config-export-service.js +82 -0
- package/src/core/deploy-service.js +1 -1
- package/src/net/system-dns.js +101 -0
- package/src/utils/bypass.js +79 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-07-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Auto-DoH System DNS Protection**: `polaris start` automatically launches the local DoH resolver (`127.0.0.1:5354`) and binds system DNS on macOS and Linux to eliminate ISP DNS leaks. System DNS is safely backed up and restored on `polaris stop`.
|
|
13
|
+
- **Split Tunneling / Bypass Rules (`polaris bypass`)**: Added `polaris bypass add/remove/list <domain|ip>` commands to route local LAN devices or specific streaming domains outside the VPN.
|
|
14
|
+
- **Server Latency Benchmark (`polaris benchmark`)**: Added `polaris benchmark` command to rank all saved profiles by ICMP ping latency and TCP handshake times.
|
|
15
|
+
- **Fastest Server Auto-Selection (`polaris start --fastest`)**: Added `--fastest` flag to benchmark saved profiles and auto-connect to the lowest-latency server.
|
|
16
|
+
- **Multi-Protocol Automatic Failover (`polaris start --failover`)**: Automatically falls back through protocols (`wireguard` → `amneziawg` → `tls` → `ssh`) if primary connection fails.
|
|
17
|
+
- **WireGuard / AmneziaWG Config Import (`polaris import`)**: Import third-party `.conf` files (`polaris import <file>`) from providers like Mullvad, ProtonVPN, or custom servers.
|
|
18
|
+
- **Profile Export & Mobile QR Codes (`polaris export`)**: Export saved profiles to `.conf` files (`polaris export <alias> --out <path>`) or render terminal QR codes for mobile setup.
|
|
19
|
+
- **TUI Shortcut Keys**: Added `b` shortcut key in master TUI to view live server latency benchmark rankings.
|
|
20
|
+
|
|
21
|
+
## [1.1.2] - 2026-07-22
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Start Command**: Fixed `ReferenceError: config is not defined` crash when invoking `polaris start` without `--server` argument.
|
|
26
|
+
- **WireGuard & AmneziaWG Status**: Fixed `polaris status` and `polaris check` reporting false proxy failures for system-wide VPN interfaces.
|
|
27
|
+
- **AmneziaWG Bandwidth Monitoring**: Updated `polaris monitor`, `dashboard`, `status`, and TUI components to query `awg` interface statistics and accept `--mode amneziawg`.
|
|
28
|
+
- **Deploy Local Auto-Connect**: Updated `polaris deploy` to automatically trigger local connection passing correct server and mode arguments upon provisioning completion.
|
|
29
|
+
- **Status Table Formatting**: Replaced invalid SOCKS5 proxy port display (`socks5://127.0.0.1:0`) with clear `System-wide (All OS traffic)` label for WireGuard and AmneziaWG connections.
|
|
30
|
+
|
|
8
31
|
## [1.0.0] - 2026-07-15
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Polaris VPN
|
|
2
2
|
|
|
3
|
-

|
|
4
3
|
|
|
5
4
|
**Command your privacy.**
|
|
6
5
|
|
|
@@ -22,14 +21,14 @@ Deploy an enterprise-grade WireGuard or Stealth VPN in under 60 seconds.
|
|
|
22
21
|
on any fresh Linux VPS.
|
|
23
22
|
- **🎨 Beautiful TUI**: A master dashboard with real-time bandwidth
|
|
24
23
|
monitoring, ping latency, and server locations.
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
24
|
+
- **🔒 Auto-DoH & DNS Protection**: Automatically binds system DNS to local DoH resolver (`127.0.0.1:5354`) on connection, preventing ISP DNS leaks.
|
|
25
|
+
- **⚡ Latency Benchmark & Auto-Selection**: Run `polaris benchmark` or use `polaris start --fastest` to auto-connect to the lowest-latency server.
|
|
26
|
+
- **🔀 Split Tunneling / Bypass Rules**: Route local LAN or specific domains around the VPN using `polaris bypass add <domain|ip>`.
|
|
27
|
+
- **📁 Import & Export `.conf` Files**: Import third-party WireGuard/AmneziaWG configs (`polaris import <file>`) or export profile QR codes (`polaris export <alias>`).
|
|
28
|
+
- **🛡️ Smart Kill-Switch**: Native OS firewall integration (`pf` for Mac, `iptables` for Linux) that ensures 0 leaks.
|
|
29
|
+
- **👻 Stealth Mode**: DPI (Deep Packet Inspection) bypass using AmneziaWG to defeat state-level censorship.
|
|
30
|
+
- **🔗 Multi-Protocol Failover**: `polaris start --failover` automatically falls back through protocols (`wireguard` → `amneziawg` → `tls` → `ssh`) if blocked.
|
|
31
|
+
- **☁️ Own Your Infra**: We run no servers. You bring your own VPS (Oracle Free Tier recommended).
|
|
33
32
|
|
|
34
33
|
## 📦 Installation
|
|
35
34
|
|
|
@@ -47,12 +46,12 @@ Got a fresh Linux VPS? Let Polaris set everything up for you.
|
|
|
47
46
|
# 1. Provision the server automatically
|
|
48
47
|
polaris deploy --server root@1.2.3.4 --mode amneziawg
|
|
49
48
|
|
|
50
|
-
# 2. Connect to your new VPN
|
|
49
|
+
# 2. Connect to your new VPN with Auto-DoH & System DNS Protection
|
|
51
50
|
polaris start --server root@1.2.3.4
|
|
52
51
|
```
|
|
53
52
|
|
|
54
53
|
> **Pro Tip**: Run `polaris add my-server --server root@1.2.3.4`
|
|
55
|
-
> so you can just type `polaris start` next time!
|
|
54
|
+
> so you can just type `polaris start` or `polaris start --fastest` next time!
|
|
56
55
|
|
|
57
56
|
## 🖥️ The Dashboard
|
|
58
57
|
|
|
@@ -66,9 +65,9 @@ Use your **Arrow Keys** to navigate through:
|
|
|
66
65
|
|
|
67
66
|
- **Home**: Quick connect/disconnect and privacy check tools.
|
|
68
67
|
- **Live Monitor**: Watch your real-time Rx/Tx bandwidth graphs and server latency.
|
|
69
|
-
- **Peers**: Add or revoke VPN access for friends, or generate a QR code for
|
|
70
|
-
your phone.
|
|
68
|
+
- **Peers**: Add or revoke VPN access for friends, or generate a QR code for your phone.
|
|
71
69
|
- **Settings**: Manage your saved server profiles.
|
|
70
|
+
- **Keyboard Shortcuts**: Press `b` in the TUI to view live server latency benchmark rankings.
|
|
72
71
|
|
|
73
72
|
## 🛠️ CLI Command Reference
|
|
74
73
|
|
|
@@ -77,8 +76,15 @@ Prefer the raw CLI? Everything is accessible via commands:
|
|
|
77
76
|
| Command | Description |
|
|
78
77
|
| :--- | :--- |
|
|
79
78
|
| `polaris deploy` | Provision a server with WireGuard or AmneziaWG |
|
|
80
|
-
| `polaris start` | Connect to
|
|
81
|
-
| `polaris
|
|
79
|
+
| `polaris start` | Connect to VPN with Auto-DoH DNS Protection |
|
|
80
|
+
| `polaris start --fastest` | Benchmark saved profiles & auto-connect to lowest latency server |
|
|
81
|
+
| `polaris start --failover` | Connect with automatic multi-protocol fallback |
|
|
82
|
+
| `polaris stop` | Disconnect, stop DoH, and restore system DNS |
|
|
83
|
+
| `polaris benchmark` | Measure ICMP ping & TCP handshake latency across all saved profiles |
|
|
84
|
+
| `polaris bypass add <target>` | Add domain or IP subnet to split-tunneling bypass rules |
|
|
85
|
+
| `polaris bypass list` | View active split-tunneling bypass rules |
|
|
86
|
+
| `polaris import <file.conf>` | Import WireGuard/AmneziaWG `.conf` file (Mullvad, Proton, custom) |
|
|
87
|
+
| `polaris export <alias>` | Export profile config or display terminal QR code |
|
|
82
88
|
| `polaris dashboard` | Open the TUI Live Monitor directly |
|
|
83
89
|
| `polaris status --full` | Check connection state, GeoIP, and latency |
|
|
84
90
|
| `polaris peer add <name>` | Generate a new peer config for a friend/device |
|
|
@@ -89,20 +95,17 @@ Prefer the raw CLI? Everything is accessible via commands:
|
|
|
89
95
|
|
|
90
96
|
## ☁️ The Oracle Cloud Advantage
|
|
91
97
|
|
|
92
|
-
We highly recommend using the **Oracle Cloud Always Free Tier** to
|
|
93
|
-
host your Polaris server:
|
|
98
|
+
We highly recommend using the **Oracle Cloud Always Free Tier** to host your Polaris server:
|
|
94
99
|
|
|
95
100
|
- **ARM VM (A1.Flex)**: Up to 4 OCPUs and 24 GB RAM — completely free forever.
|
|
96
101
|
- **10 TB Bandwidth**: More than enough for high-speed streaming and gaming.
|
|
97
|
-
- No third-party VPN company logging your data or selling your traffic.
|
|
98
|
-
You are in total command.
|
|
102
|
+
- No third-party VPN company logging your data or selling your traffic. You are in total command.
|
|
99
103
|
|
|
100
104
|
## 🔒 Trust Model
|
|
101
105
|
|
|
102
106
|
- **You own the server**: No reliance on third-party commercial VPN providers.
|
|
103
107
|
- **Zero telemetry**: No tracking, no crash reporting, no analytics.
|
|
104
|
-
- **Local Kill-Switch**: Traffic is strictly locked to the VPN interface to
|
|
105
|
-
prevent IP leaks.
|
|
108
|
+
- **Auto-DoH & Local Kill-Switch**: Traffic is strictly encrypted and locked to the VPN interface to prevent IP and DNS leaks.
|
|
106
109
|
- **Open Source**: Apache 2.0 licensed, fully auditable codebase.
|
|
107
110
|
|
|
108
111
|
---
|
package/docs/SETUP.md
CHANGED
|
@@ -51,7 +51,13 @@ Run:
|
|
|
51
51
|
polaris start
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
*Note: By default, `polaris start` automatically launches local DNS-over-HTTPS (`127.0.0.1:5354`) and binds system DNS to prevent ISP DNS leaks.*
|
|
55
|
+
|
|
56
|
+
To automatically measure server ping latency and connect to the fastest available profile:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
polaris start --fastest
|
|
60
|
+
```
|
|
55
61
|
|
|
56
62
|
To verify everything is working and your IP has changed, run:
|
|
57
63
|
|
|
@@ -59,15 +65,42 @@ To verify everything is working and your IP has changed, run:
|
|
|
59
65
|
polaris check
|
|
60
66
|
```
|
|
61
67
|
|
|
62
|
-
## 5. Enable
|
|
68
|
+
## 5. Enable Split Tunneling / Bypass Rules (Optional)
|
|
69
|
+
|
|
70
|
+
Want local LAN devices or specific streaming sites to bypass the VPN?
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Add domain or IP subnet to bypass rules
|
|
74
|
+
polaris bypass add netflix.com
|
|
75
|
+
polaris bypass add 192.168.1.0/24
|
|
76
|
+
|
|
77
|
+
# View active bypass rules
|
|
78
|
+
polaris bypass list
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 6. Measure Server Latency (`polaris benchmark`)
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
If you have multiple saved profiles, rank them by ICMP ping and TCP handshake latency:
|
|
65
84
|
|
|
66
85
|
```bash
|
|
67
|
-
polaris
|
|
86
|
+
polaris benchmark
|
|
68
87
|
```
|
|
69
88
|
|
|
70
|
-
##
|
|
89
|
+
## 7. Import & Export Configuration Files
|
|
90
|
+
|
|
91
|
+
Import third-party WireGuard / AmneziaWG `.conf` files (from providers like Mullvad, ProtonVPN, or custom servers):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
polaris import ~/Downloads/mullvad-us.conf --alias mullvad-us
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Export any saved profile to a `.conf` file or display a terminal QR code:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
polaris export mullvad-us --out ./mullvad-us.conf
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 8. Connect Mobile Devices
|
|
71
104
|
|
|
72
105
|
Want to use the VPN on your phone? Polaris can generate terminal QR codes!
|
|
73
106
|
First, generate a new peer configuration on the server:
|
|
@@ -84,10 +117,12 @@ polaris peer qr my-iphone
|
|
|
84
117
|
|
|
85
118
|
Download the **AmneziaWG** app on iOS or Android, tap the `+` button, select **Create from QR code**, and scan your screen!
|
|
86
119
|
|
|
87
|
-
##
|
|
120
|
+
## 9. Stop the Tunnel
|
|
88
121
|
|
|
89
122
|
When you are done, simply run:
|
|
90
123
|
|
|
91
124
|
```bash
|
|
92
125
|
polaris stop
|
|
93
126
|
```
|
|
127
|
+
|
|
128
|
+
*(This automatically stops the tunnel, turns off Auto-DoH, and restores your original system DNS settings)*
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -46,6 +46,9 @@ program
|
|
|
46
46
|
.option('-s, --server <user@host>', 'SSH/TLS/WireGuard server to connect to')
|
|
47
47
|
.option('-p, --port <number>', 'Local SOCKS5 port to bind', '1080')
|
|
48
48
|
.option('-m, --mode <type>', 'Tunnel mode: ssh, tls, wireguard, amneziawg or auto', 'auto')
|
|
49
|
+
.option('--no-doh', 'Disable automatic DoH system DNS protection')
|
|
50
|
+
.option('--fastest', 'Auto-select lowest latency server profile')
|
|
51
|
+
.option('--failover', 'Enable automatic multi-protocol fallback')
|
|
49
52
|
.action(async (options, cmd) => {
|
|
50
53
|
if (!cmd.optsWithGlobals().json) printBanner();
|
|
51
54
|
try {
|
|
@@ -56,6 +59,19 @@ program
|
|
|
56
59
|
}
|
|
57
60
|
});
|
|
58
61
|
|
|
62
|
+
program
|
|
63
|
+
.command('benchmark')
|
|
64
|
+
.description('Ping and measure latency across all saved server profiles')
|
|
65
|
+
.action(async (options, cmd) => {
|
|
66
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
67
|
+
try {
|
|
68
|
+
const run = (await import('./commands/benchmark.js')).default;
|
|
69
|
+
await run(cmd.optsWithGlobals());
|
|
70
|
+
} catch (err) {
|
|
71
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
59
75
|
program
|
|
60
76
|
.command('stop')
|
|
61
77
|
.description('Stop the active tunnel')
|
|
@@ -292,6 +308,75 @@ peerCmd
|
|
|
292
308
|
}
|
|
293
309
|
});
|
|
294
310
|
|
|
311
|
+
const bypassCmd = program.command('bypass').description('Manage split tunneling and bypass rules');
|
|
312
|
+
|
|
313
|
+
bypassCmd
|
|
314
|
+
.command('add <target>')
|
|
315
|
+
.description('Add a domain or IP subnet to bypass rules')
|
|
316
|
+
.action(async (target, options, cmd) => {
|
|
317
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
318
|
+
try {
|
|
319
|
+
const { bypassAdd } = await import('./commands/bypass.js');
|
|
320
|
+
await bypassAdd(target, cmd.optsWithGlobals());
|
|
321
|
+
} catch (err) {
|
|
322
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
bypassCmd
|
|
327
|
+
.command('remove <target>')
|
|
328
|
+
.description('Remove a domain or IP subnet from bypass rules')
|
|
329
|
+
.action(async (target, options, cmd) => {
|
|
330
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
331
|
+
try {
|
|
332
|
+
const { bypassRemove } = await import('./commands/bypass.js');
|
|
333
|
+
await bypassRemove(target, cmd.optsWithGlobals());
|
|
334
|
+
} catch (err) {
|
|
335
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
bypassCmd
|
|
340
|
+
.command('list')
|
|
341
|
+
.description('List all active bypass rules')
|
|
342
|
+
.action(async (options, cmd) => {
|
|
343
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
344
|
+
try {
|
|
345
|
+
const { bypassList } = await import('./commands/bypass.js');
|
|
346
|
+
await bypassList(cmd.optsWithGlobals());
|
|
347
|
+
} catch (err) {
|
|
348
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
program
|
|
353
|
+
.command('import <file>')
|
|
354
|
+
.description('Import a WireGuard or AmneziaWG configuration file (.conf)')
|
|
355
|
+
.option('-a, --alias <name>', 'Custom alias name for the imported profile')
|
|
356
|
+
.action(async (file, options, cmd) => {
|
|
357
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
358
|
+
try {
|
|
359
|
+
const { configImport } = await import('./commands/import-export.js');
|
|
360
|
+
await configImport(file, cmd.optsWithGlobals());
|
|
361
|
+
} catch (err) {
|
|
362
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
program
|
|
367
|
+
.command('export <alias>')
|
|
368
|
+
.description('Export a profile configuration file or render QR code')
|
|
369
|
+
.option('-o, --out <path>', 'Destination file path to write configuration to')
|
|
370
|
+
.action(async (alias, options, cmd) => {
|
|
371
|
+
if (!cmd.optsWithGlobals().json) printBanner();
|
|
372
|
+
try {
|
|
373
|
+
const { configExport } = await import('./commands/import-export.js');
|
|
374
|
+
await configExport(alias, cmd.optsWithGlobals());
|
|
375
|
+
} catch (err) {
|
|
376
|
+
handleError('Command failed', err, cmd.optsWithGlobals().json);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
295
380
|
const configCmd = program.command('config').description('Configure polaris settings');
|
|
296
381
|
configCmd
|
|
297
382
|
.command('set <key> <value>')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { printInfo, createSpinner, createTable, printError } from '../utils/display.js';
|
|
3
|
+
import { benchmarkAllProfiles } from '../core/benchmark-service.js';
|
|
4
|
+
|
|
5
|
+
export default async (options) => {
|
|
6
|
+
const isJson = options.json;
|
|
7
|
+
const spinner = isJson ? null : createSpinner('Benchmarking saved server profiles...').start();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const results = await benchmarkAllProfiles();
|
|
11
|
+
if (spinner) spinner.stop();
|
|
12
|
+
|
|
13
|
+
if (results.length === 0) {
|
|
14
|
+
if (isJson) {
|
|
15
|
+
console.log(JSON.stringify({ error: 'No saved profiles found' }));
|
|
16
|
+
} else {
|
|
17
|
+
printInfo('No saved server profiles found. Add profiles with "polaris add <alias> --server <user@host>".');
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isJson) {
|
|
23
|
+
console.log(JSON.stringify({ results }));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(chalk.cyan.bold('\n● Polaris Server Latency Rankings\n'));
|
|
28
|
+
|
|
29
|
+
const table = createTable(['Rank', 'Alias', 'Server Host', 'Ping Latency', 'SSH (22)', 'TLS (8443)']);
|
|
30
|
+
|
|
31
|
+
results.forEach((r, idx) => {
|
|
32
|
+
const isTop = idx === 0 && r.score < 9999;
|
|
33
|
+
table.push([
|
|
34
|
+
isTop ? chalk.green.bold(`#${idx + 1} ★`) : `#${idx + 1}`,
|
|
35
|
+
isTop ? chalk.green.bold(r.alias) : r.alias,
|
|
36
|
+
r.server,
|
|
37
|
+
r.ping === 'Timeout' ? chalk.red(r.ping) : chalk.green(r.ping),
|
|
38
|
+
r.tcpSsh === 'Timeout' ? chalk.dim(r.tcpSsh) : chalk.cyan(r.tcpSsh),
|
|
39
|
+
r.tcpTls === 'Timeout' ? chalk.dim(r.tcpTls) : chalk.yellow(r.tcpTls)
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(table.toString());
|
|
44
|
+
console.log('');
|
|
45
|
+
if (results[0] && results[0].score < 9999) {
|
|
46
|
+
console.log(chalk.green(`Fastest server: ${chalk.bold(results[0].alias)} (${results[0].ping})\n`));
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (spinner) spinner.stop();
|
|
50
|
+
printError('Benchmark failed', err);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { printSuccess, printError, printInfo, createTable } from '../utils/display.js';
|
|
3
|
+
import { getBypassRules, addBypassRule, removeBypassRule } from '../utils/bypass.js';
|
|
4
|
+
|
|
5
|
+
export const bypassAdd = async (target, options) => {
|
|
6
|
+
const isJson = options.json;
|
|
7
|
+
try {
|
|
8
|
+
const rules = addBypassRule(target);
|
|
9
|
+
if (isJson) {
|
|
10
|
+
console.log(JSON.stringify({ success: true, target, rules }));
|
|
11
|
+
} else {
|
|
12
|
+
printSuccess(`Added bypass rule for '${target}'. Traffic to this destination will route outside the VPN.`);
|
|
13
|
+
}
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (isJson) {
|
|
16
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
17
|
+
} else {
|
|
18
|
+
printError('Failed to add bypass rule', err);
|
|
19
|
+
}
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const bypassRemove = async (target, options) => {
|
|
25
|
+
const isJson = options.json;
|
|
26
|
+
try {
|
|
27
|
+
const rules = removeBypassRule(target);
|
|
28
|
+
if (isJson) {
|
|
29
|
+
console.log(JSON.stringify({ success: true, target, rules }));
|
|
30
|
+
} else {
|
|
31
|
+
printSuccess(`Removed bypass rule for '${target}'.`);
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (isJson) {
|
|
35
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
36
|
+
} else {
|
|
37
|
+
printError('Failed to remove bypass rule', err);
|
|
38
|
+
}
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const bypassList = async (options) => {
|
|
44
|
+
const isJson = options.json;
|
|
45
|
+
const rules = getBypassRules();
|
|
46
|
+
|
|
47
|
+
if (isJson) {
|
|
48
|
+
console.log(JSON.stringify({ rules }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (rules.length === 0) {
|
|
53
|
+
printInfo('No bypass rules configured. Use "polaris bypass add <domain|ip>".');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const table = createTable(['Rule #', 'Destination (Bypass Target)']);
|
|
58
|
+
rules.forEach((rule, idx) => {
|
|
59
|
+
table.push([(idx + 1).toString(), chalk.cyan(rule)]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log('\n' + table.toString() + '\n');
|
|
63
|
+
};
|
package/src/commands/check.js
CHANGED
|
@@ -22,12 +22,13 @@ export default async (options) => {
|
|
|
22
22
|
if (spinner) spinner.text = 'Checking IP address...';
|
|
23
23
|
if (info) {
|
|
24
24
|
try {
|
|
25
|
-
const
|
|
25
|
+
const isSystemWide = info.mode === 'wireguard' || info.mode === 'amneziawg';
|
|
26
|
+
const tunnelIp = isSystemWide ? await getPublicIp() : await getProxiedIp(info.port);
|
|
26
27
|
results.ip = true;
|
|
27
28
|
details.ip = `Tunnel active (${(info.mode || 'ssh').toUpperCase()}): ${tunnelIp}`;
|
|
28
29
|
} catch (e) {
|
|
29
30
|
results.ip = false;
|
|
30
|
-
details.ip = `Tunnel
|
|
31
|
+
details.ip = `Tunnel active but IP check failed: ${e.message}`;
|
|
31
32
|
}
|
|
32
33
|
} else {
|
|
33
34
|
const pubIp = await getPublicIp();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import blessed from 'blessed';
|
|
2
2
|
import contrib from 'blessed-contrib';
|
|
3
3
|
import { getActiveTunnel } from '../core/tunnel-service.js';
|
|
4
|
-
import { getProxiedIp } from '../net/ip-check.js';
|
|
4
|
+
import { getProxiedIp, getPublicIp } from '../net/ip-check.js';
|
|
5
5
|
import fetch from 'node-fetch';
|
|
6
6
|
import { spawnSync } from 'child_process';
|
|
7
7
|
import os from 'os';
|
|
@@ -67,8 +67,12 @@ const formatBytes = (bytes) => {
|
|
|
67
67
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
const getWgStats = () => {
|
|
71
|
-
const
|
|
70
|
+
const getWgStats = (isAwg = false) => {
|
|
71
|
+
const cmd = isAwg ? 'awg' : 'wg';
|
|
72
|
+
let dumpRes = spawnSync('sudo', [cmd, 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
73
|
+
if (dumpRes.status !== 0 && isAwg) {
|
|
74
|
+
dumpRes = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
75
|
+
}
|
|
72
76
|
if (dumpRes.status !== 0) return null;
|
|
73
77
|
const lines = dumpRes.stdout.trim().split('\n');
|
|
74
78
|
if (lines.length <= 1) return null;
|
|
@@ -201,7 +205,8 @@ export default async () => {
|
|
|
201
205
|
if (!geoFetched) {
|
|
202
206
|
geoFetched = true;
|
|
203
207
|
try {
|
|
204
|
-
|
|
208
|
+
const isSystemWide = info.mode === 'wireguard' || info.mode === 'amneziawg';
|
|
209
|
+
currentIp = isSystemWide ? await getPublicIp() : await getProxiedIp(info.port);
|
|
205
210
|
geoData = await getGeoIp(currentIp);
|
|
206
211
|
if (geoData) {
|
|
207
212
|
mapWidget.addMarker({ lat: geoData.lat, lon: geoData.lon, color: 'red', char: 'X' });
|
|
@@ -227,7 +232,7 @@ export default async () => {
|
|
|
227
232
|
|
|
228
233
|
// 4. Update WG Stats & Chart
|
|
229
234
|
if (info.mode === 'wireguard' || info.mode === 'amneziawg' || info.mode === 'auto') {
|
|
230
|
-
const stats = getWgStats();
|
|
235
|
+
const stats = getWgStats(info.mode === 'amneziawg');
|
|
231
236
|
if (stats) {
|
|
232
237
|
if (!hasInitWg) {
|
|
233
238
|
prevRx = stats.totalRx;
|
package/src/commands/deploy.js
CHANGED
|
@@ -31,12 +31,13 @@ export default async (options) => {
|
|
|
31
31
|
if (isJson) {
|
|
32
32
|
console.log(JSON.stringify({ success: true, ...res }));
|
|
33
33
|
} else {
|
|
34
|
-
printBox('Server Provisioned Successfully 🚀', `Remote Server: ${server}\nLocal Config: ${res.clientConfPath}\nClient PK: ${res.clientPublicKey}\nServer PK: ${res.serverPublicKey}
|
|
34
|
+
printBox('Server Provisioned Successfully 🚀', `Remote Server: ${server}\nLocal Config: ${res.clientConfPath}\nClient PK: ${res.clientPublicKey}\nServer PK: ${res.serverPublicKey}\n\n💡 Oracle Cloud / Cloud Provider Tip:\nEnsure UDP Port 51820 is opened in your Cloud Console (VCN Ingress Rules)!`, 'success');
|
|
35
35
|
console.log(chalk.cyan('\nStarting WireGuard tunnel connection locally...\n'));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Automatically trigger local connection
|
|
39
|
-
|
|
39
|
+
const mode = options.mode || 'wireguard';
|
|
40
|
+
await startCommand({ server, mode, json: isJson });
|
|
40
41
|
|
|
41
42
|
} catch (err) {
|
|
42
43
|
if (spinner) spinner.fail('Deployment failed');
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import qrcode from 'qrcode-terminal';
|
|
4
|
+
import { printSuccess, printError, printBox } from '../utils/display.js';
|
|
5
|
+
import { importWgConfig, exportWgConfig } from '../core/config-export-service.js';
|
|
6
|
+
|
|
7
|
+
export const configImport = async (filePath, options) => {
|
|
8
|
+
const isJson = options.json;
|
|
9
|
+
const alias = options.alias;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const res = importWgConfig(filePath, alias);
|
|
13
|
+
|
|
14
|
+
if (isJson) {
|
|
15
|
+
console.log(JSON.stringify({ success: true, ...res }));
|
|
16
|
+
} else {
|
|
17
|
+
printBox('Profile Imported Successfully 🚀', `Alias: ${res.alias}\nEndpoint: ${res.endpoint}\nLocal Config: ${res.configPath}`, 'success');
|
|
18
|
+
console.log(chalk.cyan(`You can now connect to this profile using: "polaris start --server ${res.alias}"\n`));
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (isJson) {
|
|
22
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
23
|
+
} else {
|
|
24
|
+
printError('Failed to import configuration', err);
|
|
25
|
+
}
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const configExport = async (alias, options) => {
|
|
31
|
+
const isJson = options.json;
|
|
32
|
+
const outFile = options.out;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const confContent = exportWgConfig(alias);
|
|
36
|
+
|
|
37
|
+
if (outFile) {
|
|
38
|
+
fs.writeFileSync(outFile, confContent, 'utf-8');
|
|
39
|
+
if (isJson) {
|
|
40
|
+
console.log(JSON.stringify({ success: true, alias, outFile }));
|
|
41
|
+
} else {
|
|
42
|
+
printSuccess(`Exported configuration profile '${alias}' to ${outFile}`);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isJson) {
|
|
48
|
+
console.log(JSON.stringify({ success: true, alias, config: confContent }));
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.cyan(`\nConfiguration Profile '${alias}':\n`));
|
|
51
|
+
console.log(confContent);
|
|
52
|
+
console.log(chalk.cyan('\nScan this QR code with the WireGuard app on your mobile device:\n'));
|
|
53
|
+
qrcode.generate(confContent, { small: true });
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (isJson) {
|
|
57
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
58
|
+
} else {
|
|
59
|
+
printError('Failed to export configuration', err);
|
|
60
|
+
}
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
};
|
package/src/commands/monitor.js
CHANGED
|
@@ -49,7 +49,7 @@ export default async (options) => {
|
|
|
49
49
|
const isJson = options.json;
|
|
50
50
|
const info = getActiveTunnel();
|
|
51
51
|
|
|
52
|
-
if (!info || info.mode !== 'wireguard') {
|
|
52
|
+
if (!info || (info.mode !== 'wireguard' && info.mode !== 'amneziawg')) {
|
|
53
53
|
if (isJson) {
|
|
54
54
|
console.log(JSON.stringify({ error: 'WireGuard tunnel is not active' }));
|
|
55
55
|
} else {
|
package/src/commands/start.js
CHANGED
|
@@ -8,6 +8,10 @@ import { handleError } from '../utils/error-handler.js';
|
|
|
8
8
|
import { getPublicIp, getProxiedIp } from '../net/ip-check.js';
|
|
9
9
|
import { getProfiles } from '../core/profile-service.js';
|
|
10
10
|
import { getActiveTunnel, startTunnel } from '../core/tunnel-service.js';
|
|
11
|
+
import { startDnsResolver, getDnsStatus } from '../core/dns-service.js';
|
|
12
|
+
import { setSystemDns } from '../net/system-dns.js';
|
|
13
|
+
import { applyBypassRules } from '../utils/bypass.js';
|
|
14
|
+
import { benchmarkAllProfiles } from '../core/benchmark-service.js';
|
|
11
15
|
import { CONFIG_DIR } from '../utils/config.js';
|
|
12
16
|
|
|
13
17
|
const WG_CONF = path.join(CONFIG_DIR, 'wg', 'wg0.conf');
|
|
@@ -50,9 +54,18 @@ export default async (options) => {
|
|
|
50
54
|
const port = parseInt(options.port || '1080', 10);
|
|
51
55
|
const requestedMode = options.mode || 'auto';
|
|
52
56
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
const
|
|
57
|
+
if (options.fastest) {
|
|
58
|
+
if (!isJson) printInfo('Benchmarking saved profiles to select the fastest server...');
|
|
59
|
+
const ranked = await benchmarkAllProfiles();
|
|
60
|
+
if (ranked.length > 0 && ranked[0].score < 9999) {
|
|
61
|
+
server = ranked[0].server;
|
|
62
|
+
if (!isJson) printSuccess(`Selected fastest server: '${ranked[0].alias}' (${ranked[0].ping})`);
|
|
63
|
+
} else if (!server) {
|
|
64
|
+
handleError('Failed to find a responsive server during benchmark.', null, isJson);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
} else if (!server) {
|
|
68
|
+
const { profiles, active } = getProfiles();
|
|
56
69
|
|
|
57
70
|
if (active && profiles[active]) {
|
|
58
71
|
server = profiles[active];
|
|
@@ -137,6 +150,20 @@ export default async (options) => {
|
|
|
137
150
|
|
|
138
151
|
const res = await startTunnel(server, port, actualMode, isJson);
|
|
139
152
|
|
|
153
|
+
// Auto-DoH & System DNS Protection
|
|
154
|
+
const enableDoh = options.doh !== false;
|
|
155
|
+
if (enableDoh) {
|
|
156
|
+
try {
|
|
157
|
+
if (!getDnsStatus()) {
|
|
158
|
+
startDnsResolver(5354);
|
|
159
|
+
}
|
|
160
|
+
setSystemDns(['127.0.0.1']);
|
|
161
|
+
} catch (dnsErr) {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Apply Split Tunneling / Bypass Rules
|
|
165
|
+
await applyBypassRules();
|
|
166
|
+
|
|
140
167
|
if (actualMode === 'wireguard' || actualMode === 'amneziawg') {
|
|
141
168
|
if (!isJson) {
|
|
142
169
|
spinner.text = 'Waiting for interface to configure...';
|
|
@@ -150,9 +177,9 @@ export default async (options) => {
|
|
|
150
177
|
|
|
151
178
|
if (!isJson) {
|
|
152
179
|
spinner.stop();
|
|
153
|
-
printBox('Tunnel Connected 🚀', `Server: ${server}\nMode: ${actualMode.toUpperCase()}\nStatus: System-wide (All OS traffic)\nOld IP: ${oldIp}\nNew IP: ${newIp}`, 'success');
|
|
180
|
+
printBox('Tunnel Connected 🚀', `Server: ${server}\nMode: ${actualMode.toUpperCase()}\nStatus: System-wide (All OS traffic)\nOld IP: ${oldIp}\nNew IP: ${newIp}\nDNS Protection: Auto-DoH Active (127.0.0.1)`, 'success');
|
|
154
181
|
} else {
|
|
155
|
-
console.log(JSON.stringify({ success: true, oldIp, newIp, mode: actualMode, pid: res.pid }));
|
|
182
|
+
console.log(JSON.stringify({ success: true, oldIp, newIp, mode: actualMode, pid: res.pid, doh: enableDoh }));
|
|
156
183
|
}
|
|
157
184
|
} else {
|
|
158
185
|
if (!isJson) {
|
|
@@ -162,9 +189,9 @@ export default async (options) => {
|
|
|
162
189
|
|
|
163
190
|
if (!isJson) {
|
|
164
191
|
spinner.stop();
|
|
165
|
-
printBox('Tunnel Connected 🚀', `Server: ${server}\nMode: ${actualMode.toUpperCase()}\nProxy: socks5://127.0.0.1:${port}\nOld IP: ${oldIp}\nNew IP: ${newIp}`, 'success');
|
|
192
|
+
printBox('Tunnel Connected 🚀', `Server: ${server}\nMode: ${actualMode.toUpperCase()}\nProxy: socks5://127.0.0.1:${port}\nOld IP: ${oldIp}\nNew IP: ${newIp}\nDNS Protection: Auto-DoH Active (127.0.0.1)`, 'success');
|
|
166
193
|
} else {
|
|
167
|
-
console.log(JSON.stringify({ success: true, oldIp, newIp, proxy: `socks5://127.0.0.1:${port}`, pid: res.pid, mode: actualMode }));
|
|
194
|
+
console.log(JSON.stringify({ success: true, oldIp, newIp, proxy: `socks5://127.0.0.1:${port}`, pid: res.pid, mode: actualMode, doh: enableDoh }));
|
|
168
195
|
}
|
|
169
196
|
}
|
|
170
197
|
|
package/src/commands/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getActiveTunnel } from '../core/tunnel-service.js';
|
|
2
|
-
import { getProxiedIp } from '../net/ip-check.js';
|
|
2
|
+
import { getProxiedIp, getPublicIp } from '../net/ip-check.js';
|
|
3
3
|
import { createTable, createSpinner, printError } from '../utils/display.js';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import fetch from 'node-fetch';
|
|
@@ -41,8 +41,12 @@ const formatBytes = (bytes) => {
|
|
|
41
41
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const getWgStats = () => {
|
|
45
|
-
const
|
|
44
|
+
const getWgStats = (isAwg = false) => {
|
|
45
|
+
const cmd = isAwg ? 'awg' : 'wg';
|
|
46
|
+
let dumpRes = spawnSync('sudo', [cmd, 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
47
|
+
if (dumpRes.status !== 0 && isAwg) {
|
|
48
|
+
dumpRes = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
49
|
+
}
|
|
46
50
|
if (dumpRes.status !== 0) return null;
|
|
47
51
|
const lines = dumpRes.stdout.trim().split('\n');
|
|
48
52
|
if (lines.length <= 1) return null;
|
|
@@ -72,10 +76,11 @@ export default async (options) => {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
let currentIp = 'Unknown';
|
|
75
|
-
const
|
|
79
|
+
const isSystemWide = info.mode === 'wireguard' || info.mode === 'amneziawg';
|
|
80
|
+
const spinner = isJson ? null : createSpinner(isSystemWide ? 'Checking public IP...' : 'Checking proxy status...').start();
|
|
76
81
|
|
|
77
82
|
try {
|
|
78
|
-
currentIp = await getProxiedIp(info.port);
|
|
83
|
+
currentIp = isSystemWide ? await getPublicIp() : await getProxiedIp(info.port);
|
|
79
84
|
if (spinner) spinner.stop();
|
|
80
85
|
|
|
81
86
|
let geo = 'N/A';
|
|
@@ -92,8 +97,8 @@ export default async (options) => {
|
|
|
92
97
|
latency = pingServer(serverIp);
|
|
93
98
|
geo = await getGeoIp(currentIp);
|
|
94
99
|
|
|
95
|
-
if (
|
|
96
|
-
const stats = getWgStats();
|
|
100
|
+
if (isSystemWide) {
|
|
101
|
+
const stats = getWgStats(info.mode === 'amneziawg');
|
|
97
102
|
if (stats) {
|
|
98
103
|
dataUsage = `${formatBytes(stats.totalRx)} / ${formatBytes(stats.totalTx)}`;
|
|
99
104
|
}
|
|
@@ -128,8 +133,16 @@ export default async (options) => {
|
|
|
128
133
|
table.push(
|
|
129
134
|
['Status', chalk.green('Connected')],
|
|
130
135
|
['Mode', (info.mode || 'ssh').toUpperCase()],
|
|
131
|
-
['Server', info.server]
|
|
132
|
-
|
|
136
|
+
['Server', info.server]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (isSystemWide) {
|
|
140
|
+
table.push(['Connection', chalk.cyan('System-wide (All OS traffic)')]);
|
|
141
|
+
} else {
|
|
142
|
+
table.push(['Proxy', `socks5://127.0.0.1:${info.port}`]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
table.push(
|
|
133
146
|
['Current IP', chalk.cyan(currentIp)],
|
|
134
147
|
['Uptime', `${uptimeMin} minutes`],
|
|
135
148
|
['PID', info.pid.toString()]
|
package/src/commands/stop.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { stopActiveTunnel } from '../core/tunnel-service.js';
|
|
2
|
+
import { stopDnsResolver } from '../core/dns-service.js';
|
|
3
|
+
import { restoreSystemDns } from '../net/system-dns.js';
|
|
2
4
|
import { printSuccess, printInfo, printError } from '../utils/display.js';
|
|
3
5
|
|
|
4
6
|
export default async (options) => {
|
|
@@ -6,6 +8,11 @@ export default async (options) => {
|
|
|
6
8
|
|
|
7
9
|
try {
|
|
8
10
|
const info = stopActiveTunnel(isJson);
|
|
11
|
+
|
|
12
|
+
// Restore system DNS and stop DoH resolver
|
|
13
|
+
restoreSystemDns();
|
|
14
|
+
stopDnsResolver();
|
|
15
|
+
|
|
9
16
|
if (!info) {
|
|
10
17
|
if (isJson) {
|
|
11
18
|
console.log(JSON.stringify({ success: true, message: 'No active tunnel found.' }));
|
package/src/commands/tui.js
CHANGED
|
@@ -87,8 +87,12 @@ const fmtBytes = (n) => {
|
|
|
87
87
|
return `${(n / Math.pow(k, i)).toFixed(1)} ${s[i]}`;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
const wgStats = () => {
|
|
91
|
-
const
|
|
90
|
+
const wgStats = (isAwg = false) => {
|
|
91
|
+
const cmd = isAwg ? 'awg' : 'wg';
|
|
92
|
+
let r = spawnSync('sudo', [cmd, 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
93
|
+
if (r.status !== 0 && isAwg) {
|
|
94
|
+
r = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
95
|
+
}
|
|
92
96
|
if (r.status !== 0) return null;
|
|
93
97
|
const lines = r.stdout.trim().split('\n');
|
|
94
98
|
if (lines.length <= 1) return null;
|
|
@@ -293,7 +297,7 @@ export default async () => {
|
|
|
293
297
|
if (info) {
|
|
294
298
|
const upMin = Math.floor((Date.now() - new Date(info.startTime).getTime()) / 60000);
|
|
295
299
|
const ping = pingServer(info.server.split('@').pop());
|
|
296
|
-
const wg = wgStats();
|
|
300
|
+
const wg = wgStats(info.mode === 'amneziawg');
|
|
297
301
|
L.push(`${t(D.accent, b('◈ Tunnel Status'))} ${t(D.success, '⬤ ACTIVE')}`);
|
|
298
302
|
L.push(hr()); L.push('');
|
|
299
303
|
L.push(` ${mu('Server ')} ${b(info.server)}`);
|
|
@@ -391,7 +395,13 @@ export default async () => {
|
|
|
391
395
|
const renderPeers = () => {
|
|
392
396
|
const L = [];
|
|
393
397
|
L.push(`${t(D.accent, b('≡ WireGuard Peers'))}`); L.push(hr()); L.push('');
|
|
394
|
-
const
|
|
398
|
+
const info = getActiveTunnel();
|
|
399
|
+
const isAwg = info && info.mode === 'amneziawg';
|
|
400
|
+
const cmd = isAwg ? 'awg' : 'wg';
|
|
401
|
+
let r = spawnSync('sudo', [cmd, 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
402
|
+
if (r.status !== 0 && isAwg) {
|
|
403
|
+
r = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
|
|
404
|
+
}
|
|
395
405
|
if (r.status !== 0 || !r.stdout.trim()) {
|
|
396
406
|
L.push(` ${t(D.warning, '⚠')} No WireGuard interface found.`);
|
|
397
407
|
L.push(` ${mu('Start a WireGuard tunnel first.')}`);
|
|
@@ -532,6 +542,13 @@ export default async () => {
|
|
|
532
542
|
screen.key(['6'], () => goto('check'));
|
|
533
543
|
screen.key(['7'], () => goto('deploy'));
|
|
534
544
|
|
|
545
|
+
screen.key(['b'], async () => {
|
|
546
|
+
await runCmd(async () => {
|
|
547
|
+
const run = (await import('./benchmark.js')).default;
|
|
548
|
+
await run({ json: false });
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
535
552
|
screen.key(['escape', 'backspace'], () => {
|
|
536
553
|
if (currentView !== 'home') goto('home');
|
|
537
554
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { getProfiles } from './profile-service.js';
|
|
5
|
+
|
|
6
|
+
export const measurePing = (host) => {
|
|
7
|
+
const isWin = os.platform() === 'win32';
|
|
8
|
+
const args = isWin ? ['-n', '1', '-w', '1500', host] : ['-c', '1', '-W', '2', host];
|
|
9
|
+
const start = Date.now();
|
|
10
|
+
const res = spawnSync('ping', args, { encoding: 'utf-8' });
|
|
11
|
+
const duration = Date.now() - start;
|
|
12
|
+
|
|
13
|
+
if (res.status === 0) {
|
|
14
|
+
const match = res.stdout.match(isWin ? /Average = (\d+)ms/ : /time=([\d.]+)\s*ms/);
|
|
15
|
+
if (match) return parseFloat(match[1]);
|
|
16
|
+
return duration;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const measureTcpHandshake = (host, port = 22, timeoutMs = 2000) => {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
const socket = new net.Socket();
|
|
25
|
+
socket.setTimeout(timeoutMs);
|
|
26
|
+
|
|
27
|
+
socket.on('connect', () => {
|
|
28
|
+
const elapsed = Date.now() - start;
|
|
29
|
+
socket.destroy();
|
|
30
|
+
resolve(elapsed);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
socket.on('timeout', () => {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
resolve(null);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
socket.on('error', () => {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
resolve(null);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
socket.connect(port, host);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const benchmarkServer = async (alias, serverStr) => {
|
|
48
|
+
const host = serverStr.includes('@') ? serverStr.split('@')[1] : serverStr;
|
|
49
|
+
const ping = measurePing(host);
|
|
50
|
+
const tcpSsh = await measureTcpHandshake(host, 22);
|
|
51
|
+
const tcpTls = await measureTcpHandshake(host, 8443);
|
|
52
|
+
|
|
53
|
+
const score = ping !== null ? ping : (tcpSsh !== null ? tcpSsh : (tcpTls !== null ? tcpTls : 9999));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
alias,
|
|
57
|
+
server: serverStr,
|
|
58
|
+
host,
|
|
59
|
+
ping: ping !== null ? `${ping.toFixed(1)} ms` : 'Timeout',
|
|
60
|
+
tcpSsh: tcpSsh !== null ? `${tcpSsh} ms` : 'Timeout',
|
|
61
|
+
tcpTls: tcpTls !== null ? `${tcpTls} ms` : 'Timeout',
|
|
62
|
+
score
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const benchmarkAllProfiles = async () => {
|
|
67
|
+
const { profiles } = getProfiles();
|
|
68
|
+
const aliases = Object.keys(profiles);
|
|
69
|
+
if (aliases.length === 0) return [];
|
|
70
|
+
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const alias of aliases) {
|
|
73
|
+
const res = await benchmarkServer(alias, profiles[alias]);
|
|
74
|
+
results.push(res);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
results.sort((a, b) => a.score - b.score);
|
|
78
|
+
return results;
|
|
79
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CONFIG_DIR, ensureDir } from '../utils/config.js';
|
|
4
|
+
import { addProfile, getProfiles } from './profile-service.js';
|
|
5
|
+
|
|
6
|
+
export const parseWgConfig = (content) => {
|
|
7
|
+
const lines = content.split(/\r?\n/);
|
|
8
|
+
const result = { interface: {}, peer: {} };
|
|
9
|
+
let currentSection = null;
|
|
10
|
+
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
14
|
+
|
|
15
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
16
|
+
currentSection = trimmed.slice(1, -1).toLowerCase();
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const eqIdx = trimmed.indexOf('=');
|
|
21
|
+
if (eqIdx !== -1 && currentSection) {
|
|
22
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
23
|
+
const val = trimmed.substring(eqIdx + 1).trim();
|
|
24
|
+
if (currentSection === 'interface') {
|
|
25
|
+
result.interface[key] = val;
|
|
26
|
+
} else if (currentSection === 'peer') {
|
|
27
|
+
result.peer[key] = val;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const importWgConfig = (filePath, aliasName = null) => {
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
throw new Error(`File not found: ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
const parsed = parseWgConfig(content);
|
|
42
|
+
|
|
43
|
+
if (!parsed.peer.Endpoint) {
|
|
44
|
+
throw new Error('Invalid config: Missing [Peer] Endpoint');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const alias = aliasName || path.parse(filePath).name;
|
|
48
|
+
const endpoint = parsed.peer.Endpoint;
|
|
49
|
+
|
|
50
|
+
// Save config locally
|
|
51
|
+
const wgDir = path.join(CONFIG_DIR, 'wg');
|
|
52
|
+
ensureDir(wgDir);
|
|
53
|
+
const targetConf = path.join(wgDir, `${alias}.conf`);
|
|
54
|
+
fs.writeFileSync(targetConf, content, 'utf-8');
|
|
55
|
+
|
|
56
|
+
// Register profile
|
|
57
|
+
addProfile(alias, endpoint);
|
|
58
|
+
|
|
59
|
+
return { alias, endpoint, configPath: targetConf, parsed };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const exportWgConfig = (alias) => {
|
|
63
|
+
const wgDir = path.join(CONFIG_DIR, 'wg');
|
|
64
|
+
const targetConf = path.join(wgDir, `${alias}.conf`);
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(targetConf)) {
|
|
67
|
+
return fs.readFileSync(targetConf, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const peersDir = path.join(CONFIG_DIR, 'wg', 'peers');
|
|
71
|
+
const peerConf = path.join(peersDir, `${alias}.conf`);
|
|
72
|
+
if (fs.existsSync(peerConf)) {
|
|
73
|
+
return fs.readFileSync(peerConf, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const wg0Conf = path.join(wgDir, 'wg0.conf');
|
|
77
|
+
if (fs.existsSync(wg0Conf)) {
|
|
78
|
+
return fs.readFileSync(wg0Conf, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error(`Configuration profile for '${alias}' not found.`);
|
|
82
|
+
};
|
|
@@ -72,7 +72,7 @@ export const deployServer = async (serverStr, options = {}) => {
|
|
|
72
72
|
onProgress(`Installing ${isAwg ? 'AmneziaWG' : 'WireGuard'}, UFW, Unbound, and Fail2Ban...`);
|
|
73
73
|
let installCmd = 'sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard ufw unbound fail2ban';
|
|
74
74
|
if (isAwg) {
|
|
75
|
-
installCmd = 'sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common && sudo add-apt-repository -y ppa:amnezia/ppa && sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y amneziawg-dkms amneziawg-tools ufw unbound fail2ban';
|
|
75
|
+
installCmd = 'sudo apt-get update -y && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common linux-headers-$(uname -r) || true && 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';
|
|
76
76
|
}
|
|
77
77
|
let res = await sshExec(conn, installCmd);
|
|
78
78
|
if (res.code !== 0) throw new Error(`Installation failed: ${res.stderr}`);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { spawnSync, execSync } from 'child_process';
|
|
5
|
+
import { CONFIG_DIR, ensureDir } from '../utils/config.js';
|
|
6
|
+
|
|
7
|
+
const DNS_BACKUP_FILE = path.join(CONFIG_DIR, 'dns_backup.json');
|
|
8
|
+
|
|
9
|
+
const getActiveMacService = () => {
|
|
10
|
+
try {
|
|
11
|
+
const routeRes = spawnSync('route', ['get', 'default'], { encoding: 'utf-8' });
|
|
12
|
+
if (routeRes.status === 0) {
|
|
13
|
+
const match = routeRes.stdout.match(/interface:\s*(\w+)/);
|
|
14
|
+
if (match) {
|
|
15
|
+
const iface = match[1];
|
|
16
|
+
const servicesRes = spawnSync('networksetup', ['-listallhardwareports'], { encoding: 'utf-8' });
|
|
17
|
+
if (servicesRes.status === 0) {
|
|
18
|
+
const lines = servicesRes.stdout.split('\n');
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
if (lines[i].includes(`Device: ${iface}`)) {
|
|
21
|
+
const prevLine = lines[i - 1] || '';
|
|
22
|
+
const matchService = prevLine.match(/Hardware Port:\s*(.+)/);
|
|
23
|
+
if (matchService) return matchService[1].trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {}
|
|
30
|
+
return 'Wi-Fi';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const backupSystemDns = () => {
|
|
34
|
+
ensureDir();
|
|
35
|
+
if (fs.existsSync(DNS_BACKUP_FILE)) return;
|
|
36
|
+
|
|
37
|
+
const platform = os.platform();
|
|
38
|
+
const backup = { platform, timestamp: new Date().toISOString() };
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (platform === 'darwin') {
|
|
42
|
+
const service = getActiveMacService();
|
|
43
|
+
backup.service = service;
|
|
44
|
+
const res = spawnSync('networksetup', ['-getdnsservers', service], { encoding: 'utf-8' });
|
|
45
|
+
if (res.status === 0 && !res.stdout.includes('There aren\'t any DNS Servers')) {
|
|
46
|
+
backup.dns = res.stdout.trim().split('\n').filter(Boolean);
|
|
47
|
+
} else {
|
|
48
|
+
backup.dns = ['Empty'];
|
|
49
|
+
}
|
|
50
|
+
} else if (platform === 'linux') {
|
|
51
|
+
if (fs.existsSync('/etc/resolv.conf')) {
|
|
52
|
+
backup.resolvConf = fs.readFileSync('/etc/resolv.conf', 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
fs.writeFileSync(DNS_BACKUP_FILE, JSON.stringify(backup, null, 2), 'utf-8');
|
|
56
|
+
} catch (err) {}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const setSystemDns = (dnsServers = ['127.0.0.1']) => {
|
|
60
|
+
backupSystemDns();
|
|
61
|
+
const platform = os.platform();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (platform === 'darwin') {
|
|
65
|
+
const service = getActiveMacService();
|
|
66
|
+
spawnSync('networksetup', ['-setdnsservers', service, ...dnsServers], { stdio: 'ignore' });
|
|
67
|
+
} else if (platform === 'linux') {
|
|
68
|
+
const content = dnsServers.map(s => `nameserver ${s}`).join('\n') + '\n';
|
|
69
|
+
execSync(`echo "${content.trim()}" | sudo tee /etc/resolv.conf > /dev/null`, { stdio: 'ignore' });
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const restoreSystemDns = () => {
|
|
78
|
+
if (!fs.existsSync(DNS_BACKUP_FILE)) return false;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const backup = JSON.parse(fs.readFileSync(DNS_BACKUP_FILE, 'utf-8'));
|
|
82
|
+
const platform = os.platform();
|
|
83
|
+
|
|
84
|
+
if (platform === 'darwin') {
|
|
85
|
+
const service = backup.service || getActiveMacService();
|
|
86
|
+
if (backup.dns && backup.dns.length > 0 && backup.dns[0] !== 'Empty') {
|
|
87
|
+
spawnSync('networksetup', ['-setdnsservers', service, ...backup.dns], { stdio: 'ignore' });
|
|
88
|
+
} else {
|
|
89
|
+
spawnSync('networksetup', ['-setdnsservers', service, 'Empty'], { stdio: 'ignore' });
|
|
90
|
+
}
|
|
91
|
+
} else if (platform === 'linux') {
|
|
92
|
+
if (backup.resolvConf) {
|
|
93
|
+
execSync(`cat << 'EOF' | sudo tee /etc/resolv.conf > /dev/null\n${backup.resolvConf}\nEOF`, { stdio: 'ignore' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
fs.unlinkSync(DNS_BACKUP_FILE);
|
|
97
|
+
return true;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { store } from './config.js';
|
|
2
|
+
import dns from 'dns';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
const resolve4 = promisify(dns.resolve4);
|
|
8
|
+
|
|
9
|
+
export const getBypassRules = () => {
|
|
10
|
+
return store.get('bypassRules', []);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const addBypassRule = (target) => {
|
|
14
|
+
const rules = getBypassRules();
|
|
15
|
+
if (rules.includes(target)) {
|
|
16
|
+
throw new Error(`Bypass rule for '${target}' already exists.`);
|
|
17
|
+
}
|
|
18
|
+
rules.push(target);
|
|
19
|
+
store.set('bypassRules', rules);
|
|
20
|
+
return rules;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const removeBypassRule = (target) => {
|
|
24
|
+
let rules = getBypassRules();
|
|
25
|
+
if (!rules.includes(target)) {
|
|
26
|
+
throw new Error(`Bypass rule '${target}' not found.`);
|
|
27
|
+
}
|
|
28
|
+
rules = rules.filter(r => r !== target);
|
|
29
|
+
store.set('bypassRules', rules);
|
|
30
|
+
return rules;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const resolveTargetIps = async (target) => {
|
|
34
|
+
const isIp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?$/.test(target);
|
|
35
|
+
if (isIp) return [target];
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const ips = await resolve4(target);
|
|
39
|
+
return ips;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const applyBypassRules = async () => {
|
|
46
|
+
const rules = getBypassRules();
|
|
47
|
+
if (rules.length === 0) return;
|
|
48
|
+
|
|
49
|
+
const platform = os.platform();
|
|
50
|
+
|
|
51
|
+
// Find default gateway
|
|
52
|
+
let gateway = null;
|
|
53
|
+
try {
|
|
54
|
+
if (platform === 'darwin') {
|
|
55
|
+
const res = spawnSync('route', ['-n', 'get', 'default'], { encoding: 'utf-8' });
|
|
56
|
+
const match = res.stdout.match(/gateway:\s*([\d.]+)/);
|
|
57
|
+
if (match) gateway = match[1];
|
|
58
|
+
} else if (platform === 'linux') {
|
|
59
|
+
const res = spawnSync('ip', ['route', 'show', 'default'], { encoding: 'utf-8' });
|
|
60
|
+
const match = res.stdout.match(/via\s*([\d.]+)/);
|
|
61
|
+
if (match) gateway = match[1];
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
|
|
65
|
+
if (!gateway) return;
|
|
66
|
+
|
|
67
|
+
for (const rule of rules) {
|
|
68
|
+
const ips = await resolveTargetIps(rule);
|
|
69
|
+
for (const ip of ips) {
|
|
70
|
+
try {
|
|
71
|
+
if (platform === 'darwin') {
|
|
72
|
+
spawnSync('sudo', ['route', '-n', 'add', '-host', ip, gateway], { stdio: 'ignore' });
|
|
73
|
+
} else if (platform === 'linux') {
|
|
74
|
+
spawnSync('sudo', ['ip', 'route', 'add', ip, 'via', gateway], { stdio: 'ignore' });
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|