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 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
- ![Polaris VPN Terminal Demo](./docs/demo.gif)
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
- - **🛡️ Smart Kill-Switch**: Native OS firewall integration (`pf` for Mac,
26
- `iptables` for Linux) that ensures 0 leaks.
27
- - **👻 Stealth Mode**: DPI (Deep Packet Inspection) bypass using AmneziaWG to
28
- defeat state-level censorship.
29
- - **🔗 SSH Fallback**: Can't install WireGuard? Polaris seamlessly falls back
30
- to an encrypted SOCKS5 SSH tunnel.
31
- - **☁️ Own Your Infra**: We run no servers. You bring your own VPS (Oracle
32
- Free Tier recommended).
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 the VPN and engage the Kill-Switch |
81
- | `polaris stop` | Disconnect and restore normal internet routing |
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
- You should see a success message. All your internet traffic is now encrypted and routed through your VPS!
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 the Kill Switch (Optional)
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
- To ensure your real IP is never leaked if the VPN connection drops, enable the system kill switch:
83
+ If you have multiple saved profiles, rank them by ICMP ping and TCP handshake latency:
65
84
 
66
85
  ```bash
67
- polaris killswitch on
86
+ polaris benchmark
68
87
  ```
69
88
 
70
- ## 6. Connect Mobile Devices
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
- ## 7. Stop the Tunnel
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
5
5
  "type": "module",
6
6
  "bin": {
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
+ };
@@ -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 tunnelIp = await getProxiedIp(info.port);
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 process running but proxy failed: ${e.message}`;
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 dumpRes = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
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
- currentIp = await getProxiedIp(info.port);
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;
@@ -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}`, 'success');
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
- await startCommand({ mode: 'wireguard', json: isJson });
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
+ };
@@ -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 {
@@ -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 (!server) {
54
- const active = config.get('activeProfile');
55
- const profiles = config.get('profiles') || {};
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
 
@@ -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 dumpRes = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
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 spinner = isJson ? null : createSpinner('Checking proxy status...').start();
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 (info.mode === 'wireguard') {
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
- ['Proxy', `socks5://127.0.0.1:${info.port}`],
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()]
@@ -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.' }));
@@ -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 r = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
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 r = spawnSync('sudo', ['wg', 'show', 'all', 'dump'], { encoding: 'utf-8' });
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
+ };