polaris-vpn 0.1.0 → 0.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,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-06-28
9
+
10
+ ### Added
11
+ - Companion `polaris-server` dynamic HTTP/TLS forwarding proxy on port 8443.
12
+ - local DNS-over-HTTPS (DoH) resolver command namespace (`polaris dns start/stop/status`) on port 5354.
13
+ - TLS tunnel client option (`polaris start --mode tls`).
14
+ - Auto-reconnect with exponential backoff & keepalive monitoring.
15
+ - Desktop alerts/notifications via `node-notifier`.
16
+ - Refactored codebase to highly modular MVC structure with centralized configurations and daemon manager.
17
+ - Static runner scripts to ensure full compatibility with global NPM installation permissions.
18
+
8
19
  ## [0.1.0] - 2026-06-27
9
20
 
10
21
  ### Added
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "polaris-vpn",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Your True North in Digital Privacy. A self-hosted VPN CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "polaris": "src/cli.js"
7
+ "polaris": "src/cli.js",
8
+ "polaris-server": "src/server/server.js"
8
9
  },
9
10
  "files": [
10
11
  "src",
@@ -35,7 +36,9 @@
35
36
  "cli-table3": "^0.6.5",
36
37
  "commander": "^15.0.0",
37
38
  "conf": "^15.1.0",
39
+ "dns2": "^3.0.0",
38
40
  "node-fetch": "^3.3.2",
41
+ "node-notifier": "^10.0.1",
39
42
  "ora": "^9.4.1",
40
43
  "socks-proxy-agent": "^10.1.0"
41
44
  }
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import { readFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import path from 'path';
6
- import { printBanner, printError } from './ui/display.js';
6
+ import { printBanner, printError } from './utils/display.js';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
@@ -22,9 +22,10 @@ const willPrintJson = () => process.argv.includes('--json');
22
22
 
23
23
  program
24
24
  .command('start')
25
- .description('Start the encrypted SSH tunnel')
26
- .option('-s, --server <user@host>', 'SSH server to connect to')
25
+ .description('Start the encrypted SSH or TLS tunnel')
26
+ .option('-s, --server <user@host>', 'SSH/TLS server to connect to')
27
27
  .option('-p, --port <number>', 'Local SOCKS5 port to bind', '1080')
28
+ .option('-m, --mode <type>', 'Tunnel mode: ssh, tls or auto', 'auto')
28
29
  .action(async (options, cmd) => {
29
30
  if (!cmd.optsWithGlobals().json) printBanner();
30
31
  try {
@@ -121,6 +122,52 @@ program
121
122
  }
122
123
  });
123
124
 
125
+ const dnsCmd = program.command('dns').description('Manage local DNS-over-HTTPS resolver');
126
+
127
+ dnsCmd
128
+ .command('start')
129
+ .description('Start the local DoH resolver')
130
+ .option('-p, --port <number>', 'Local port to bind', '5354')
131
+ .option('-u, --upstream <provider>', 'Upstream DoH provider (cloudflare, google, or custom URL)', 'cloudflare')
132
+ .action(async (options, cmd) => {
133
+ if (!cmd.optsWithGlobals().json) printBanner();
134
+ try {
135
+ const { dnsStart } = await import('./commands/dns.js');
136
+ await dnsStart(cmd.optsWithGlobals());
137
+ } catch (err) {
138
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
139
+ process.exit(1);
140
+ }
141
+ });
142
+
143
+ dnsCmd
144
+ .command('stop')
145
+ .description('Stop the local DoH resolver')
146
+ .action(async (options, cmd) => {
147
+ if (!cmd.optsWithGlobals().json) printBanner();
148
+ try {
149
+ const { dnsStop } = await import('./commands/dns.js');
150
+ await dnsStop(cmd.optsWithGlobals());
151
+ } catch (err) {
152
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
153
+ process.exit(1);
154
+ }
155
+ });
156
+
157
+ dnsCmd
158
+ .command('status')
159
+ .description('Show local DoH resolver status')
160
+ .action(async (options, cmd) => {
161
+ if (!cmd.optsWithGlobals().json) printBanner();
162
+ try {
163
+ const { dnsStatus } = await import('./commands/dns.js');
164
+ await dnsStatus(cmd.optsWithGlobals());
165
+ } catch (err) {
166
+ if (!cmd.optsWithGlobals().json) printError('Command failed', err);
167
+ process.exit(1);
168
+ }
169
+ });
170
+
124
171
  program.parseAsync(process.argv).catch(err => {
125
172
  if (!willPrintJson()) printError('Fatal error', err);
126
173
  process.exit(1);
@@ -1,11 +1,13 @@
1
1
  import chalk from 'chalk';
2
- import { getTunnelInfo } from '../tunnel/ssh.js';
3
- import { getPublicIp, getProxiedIp, checkDns, checkIpv6Leak } from '../net/ip.js';
4
- import { createTable, createSpinner, printError } from '../ui/display.js';
2
+ import { getActiveTunnel } from '../core/tunnel-service.js';
3
+ import { getDnsStatus } from '../core/dns-service.js';
4
+ import { getPublicIp, getProxiedIp, checkDns, checkIpv6Leak } from '../net/ip-check.js';
5
+ import { createTable, createSpinner, printError } from '../utils/display.js';
5
6
 
6
7
  export default async (options) => {
7
8
  const isJson = options.json;
8
- const info = getTunnelInfo();
9
+ const info = getActiveTunnel();
10
+ const dnsStatus = getDnsStatus();
9
11
 
10
12
  if (!isJson) {
11
13
  console.log(chalk.cyan.bold('\nRunning Privacy Check...\n'));
@@ -22,7 +24,7 @@ export default async (options) => {
22
24
  try {
23
25
  const tunnelIp = await getProxiedIp(info.port);
24
26
  results.ip = true;
25
- details.ip = `Tunnel active: ${tunnelIp}`;
27
+ details.ip = `Tunnel active (${(info.mode || 'ssh').toUpperCase()}): ${tunnelIp}`;
26
28
  } catch (e) {
27
29
  results.ip = false;
28
30
  details.ip = `Tunnel process running but proxy failed: ${e.message}`;
@@ -40,15 +42,14 @@ export default async (options) => {
40
42
  results.dns = false;
41
43
  details.dns = 'DNS resolution failed';
42
44
  } else {
43
- // In a real full VPN this would check if it's our VPN's DNS.
44
- // Since this is just a SOCKS5 proxy, system DNS is usually used.
45
- // We flag it as warning/fail if it's likely an ISP DNS, but for MVP
46
- // we'll just check if we can resolve and list the servers used.
47
- const servers = dnsResult.servers.join(', ');
48
- // SOCKS5 doesn't automatically tunnel DNS for all OS commands,
49
- // but browsers can be configured to use SOCKS5 for DNS.
50
- results.dns = true;
51
- details.dns = `Using system DNS: ${servers}`;
45
+ if (dnsStatus) {
46
+ results.dns = true;
47
+ details.dns = `DoH Resolver Active: 127.0.0.1:${dnsStatus.port} (Upstream: ${dnsStatus.upstream})`;
48
+ } else {
49
+ const servers = dnsResult.servers.join(', ');
50
+ results.dns = false;
51
+ details.dns = `Using system/ISP DNS: ${servers} (Potential DNS Leak)`;
52
+ }
52
53
  }
53
54
 
54
55
  // 3. IPv6 Leak Check
@@ -65,7 +66,12 @@ export default async (options) => {
65
66
  if (spinner) spinner.stop();
66
67
 
67
68
  if (isJson) {
68
- console.log(JSON.stringify({ results, details }));
69
+ console.log(JSON.stringify({
70
+ results,
71
+ details,
72
+ tunnelMode: info ? (info.mode || 'ssh') : 'none',
73
+ dnsResolver: dnsStatus ? 'doh' : 'system'
74
+ }));
69
75
  } else {
70
76
  const table = createTable(['Check', 'Status', 'Details']);
71
77
 
@@ -73,9 +79,9 @@ export default async (options) => {
73
79
 
74
80
  table.push(
75
81
  ['IP Address', formatResult(results.ip), details.ip],
76
- // If dns is "pass" but we know it's a SOCKS proxy, it's actually a warning that DNS isn't tunneled OS-wide
77
- ['DNS Leak', results.dns ? chalk.yellow('⚠ WARN') : formatResult(results.dns), details.dns],
78
- ['IPv6 Leak', formatResult(results.ipv6), details.ipv6]
82
+ ['DNS Leak', dnsStatus ? chalk.green('✓ PASS') : chalk.yellow(' WARN'), details.dns],
83
+ ['IPv6 Leak', formatResult(results.ipv6), details.ipv6],
84
+ ['WebRTC Leak', chalk.cyan('ⓘ INFO'), 'WebRTC can bypass proxies. Check browserleaks.com/webrtc']
79
85
  );
80
86
 
81
87
  console.log(table.toString());
@@ -85,7 +91,9 @@ export default async (options) => {
85
91
  console.log(chalk.yellow('Note: Your traffic is currently exposed. Run "polaris start" to connect.'));
86
92
  } else {
87
93
  console.log(chalk.cyan('Note: SOCKS5 proxies only tunnel apps configured to use them (like your browser).'));
88
- console.log(chalk.cyan(' System-wide DNS leaks are expected until v0.5 (WireGuard).'));
94
+ if (!dnsStatus) {
95
+ console.log(chalk.yellow(' Run "polaris dns start" to prevent DNS queries from leaking to your ISP.'));
96
+ }
89
97
  }
90
98
  console.log();
91
99
  }
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import { printError, printSuccess, printInfo, printWarning } from '../utils/display.js';
3
+ import { startDnsResolver, stopDnsResolver, getDnsStatus } from '../core/dns-service.js';
4
+
5
+ export const dnsStart = async (options) => {
6
+ const port = parseInt(options.port || '5354', 10);
7
+ const upstreamMap = {
8
+ 'cloudflare': 'https://cloudflare-dns.com/dns-query',
9
+ 'google': 'https://dns.google/resolve'
10
+ };
11
+
12
+ const provider = options.upstream || 'cloudflare';
13
+ const upstream = upstreamMap[provider.toLowerCase()] || provider;
14
+ const isJson = options.json;
15
+
16
+ try {
17
+ const res = startDnsResolver(port, upstream);
18
+ if (isJson) {
19
+ console.log(JSON.stringify({ success: true, ...res }));
20
+ } else {
21
+ printSuccess(`DNS-over-HTTPS resolver started on 127.0.0.1:${port}`);
22
+ printInfo(`Forwarding to: ${upstream}`);
23
+ }
24
+ } catch (err) {
25
+ if (isJson) {
26
+ console.log(JSON.stringify({ error: err.message }));
27
+ } else {
28
+ printError('Failed to start DNS resolver', err);
29
+ }
30
+ process.exitCode = 1;
31
+ }
32
+ };
33
+
34
+ export const dnsStop = async (options) => {
35
+ const isJson = options.json;
36
+
37
+ try {
38
+ const res = stopDnsResolver();
39
+ if (!res) {
40
+ if (isJson) {
41
+ console.log(JSON.stringify({ error: 'DNS resolver is not running' }));
42
+ } else {
43
+ printWarning('DNS resolver is not running.');
44
+ }
45
+ return;
46
+ }
47
+
48
+ if (isJson) {
49
+ console.log(JSON.stringify({ success: true }));
50
+ } else {
51
+ printSuccess('DNS-over-HTTPS resolver stopped.');
52
+ }
53
+ } catch (err) {
54
+ if (isJson) {
55
+ console.log(JSON.stringify({ error: err.message }));
56
+ } else {
57
+ printError('Failed to stop DNS resolver', err);
58
+ }
59
+ process.exitCode = 1;
60
+ }
61
+ };
62
+
63
+ export const dnsStatus = async (options) => {
64
+ const isJson = options.json;
65
+ const status = getDnsStatus();
66
+
67
+ if (!status) {
68
+ if (isJson) {
69
+ console.log(JSON.stringify({ running: false }));
70
+ } else {
71
+ console.log(`${chalk.red('✗')} DNS Resolver: ${chalk.red('Stopped')}`);
72
+ }
73
+ return;
74
+ }
75
+
76
+ if (isJson) {
77
+ console.log(JSON.stringify({ running: true, ...status }));
78
+ } else {
79
+ console.log(`${chalk.green('✓')} DNS Resolver: ${chalk.green('Running')}`);
80
+ console.log(` ${chalk.dim('PID :')} ${status.pid}`);
81
+ console.log(` ${chalk.dim('Port :')} ${status.port}`);
82
+ console.log(` ${chalk.dim('Upstream:')} ${status.upstream}`);
83
+ console.log(` ${chalk.dim('Uptime :')} ${Math.floor((Date.now() - new Date(status.startTime)) / 1000)}s`);
84
+ }
85
+ };
@@ -1,8 +1,6 @@
1
- import Conf from 'conf';
2
- import { printSuccess, printError, printInfo, createTable } from '../ui/display.js';
3
1
  import chalk from 'chalk';
4
-
5
- const config = new Conf({ projectName: 'polaris' });
2
+ import { printSuccess, printError, printInfo, createTable } from '../utils/display.js';
3
+ import { addProfile, getProfiles, setActiveProfile } from '../core/profile-service.js';
6
4
 
7
5
  export const addServer = async (alias, options) => {
8
6
  const isJson = options.json;
@@ -18,31 +16,30 @@ export const addServer = async (alias, options) => {
18
16
  return;
19
17
  }
20
18
 
21
- const profiles = config.get('servers', {});
22
- profiles[alias] = server;
23
- config.set('servers', profiles);
24
-
25
- // Auto-set as active if it's the first one
26
- if (!config.get('activeServer')) {
27
- config.set('activeServer', alias);
28
- }
29
-
30
- if (isJson) {
31
- console.log(JSON.stringify({ success: true, alias, server }));
32
- } else {
33
- printSuccess(`Saved profile '${alias}' -> ${server}`);
19
+ try {
20
+ const res = addProfile(alias, server);
21
+ if (isJson) {
22
+ console.log(JSON.stringify({ success: true, ...res }));
23
+ } else {
24
+ printSuccess(`Saved profile '${alias}' -> ${server}`);
25
+ }
26
+ } catch (err) {
27
+ if (isJson) {
28
+ console.log(JSON.stringify({ error: err.message }));
29
+ } else {
30
+ printError('Failed to add profile', err);
31
+ }
32
+ process.exitCode = 1;
34
33
  }
35
34
  };
36
35
 
37
36
  export const listServers = async (options) => {
38
37
  const isJson = options.json;
39
- const profiles = config.get('servers', {});
40
- const activeAlias = config.get('activeServer');
41
-
38
+ const { profiles, active } = getProfiles();
42
39
  const aliases = Object.keys(profiles);
43
40
 
44
41
  if (isJson) {
45
- console.log(JSON.stringify({ servers: profiles, active: activeAlias }));
42
+ console.log(JSON.stringify({ servers: profiles, active }));
46
43
  return;
47
44
  }
48
45
 
@@ -54,7 +51,7 @@ export const listServers = async (options) => {
54
51
  const table = createTable(['Alias', 'Server', 'Active']);
55
52
 
56
53
  for (const alias of aliases) {
57
- const isActive = alias === activeAlias;
54
+ const isActive = alias === active;
58
55
  table.push([
59
56
  isActive ? chalk.green.bold(alias) : alias,
60
57
  profiles[alias],
@@ -67,23 +64,20 @@ export const listServers = async (options) => {
67
64
 
68
65
  export const useServer = async (alias, options) => {
69
66
  const isJson = options.json;
70
- const profiles = config.get('servers', {});
71
67
 
72
- if (!profiles[alias]) {
68
+ try {
69
+ const res = setActiveProfile(alias);
70
+ if (isJson) {
71
+ console.log(JSON.stringify({ success: true, active: alias, server: res.server }));
72
+ } else {
73
+ printSuccess(`Set '${alias}' as the active profile.`);
74
+ }
75
+ } catch (err) {
73
76
  if (isJson) {
74
- console.log(JSON.stringify({ error: `Profile '${alias}' not found` }));
77
+ console.log(JSON.stringify({ error: err.message }));
75
78
  } else {
76
- printError(`Profile '${alias}' not found. Run "polaris list" to see saved profiles.`);
79
+ printError(err.message);
77
80
  }
78
81
  process.exitCode = 1;
79
- return;
80
- }
81
-
82
- config.set('activeServer', alias);
83
-
84
- if (isJson) {
85
- console.log(JSON.stringify({ success: true, active: alias, server: profiles[alias] }));
86
- } else {
87
- printSuccess(`Set '${alias}' as the active profile.`);
88
82
  }
89
83
  };
@@ -1,22 +1,43 @@
1
1
  import chalk from 'chalk';
2
- import Conf from 'conf';
3
- import { printError, printSuccess, printInfo, createSpinner, printWarning } from '../ui/display.js';
4
- import { getPublicIp, getProxiedIp } from '../net/ip.js';
5
- import { spawnSSH, waitForSocks, saveTunnelInfo, getTunnelInfo } from '../tunnel/ssh.js';
2
+ import tls from 'tls';
3
+ import { printError, printSuccess, createSpinner } from '../utils/display.js';
4
+ import { getPublicIp, getProxiedIp } from '../net/ip-check.js';
5
+ import { getProfiles } from '../core/profile-service.js';
6
+ import { getActiveTunnel, startTunnel } from '../core/tunnel-service.js';
6
7
 
7
- const config = new Conf({ projectName: 'polaris' });
8
+ const detectTlsServer = (host, port = 8443) => {
9
+ return new Promise((resolve) => {
10
+ const socket = tls.connect({
11
+ host,
12
+ port,
13
+ rejectUnauthorized: false,
14
+ timeout: 3000
15
+ }, () => {
16
+ socket.destroy();
17
+ resolve(true);
18
+ });
19
+
20
+ socket.on('error', () => {
21
+ resolve(false);
22
+ });
23
+
24
+ socket.on('timeout', () => {
25
+ socket.destroy();
26
+ resolve(false);
27
+ });
28
+ });
29
+ };
8
30
 
9
31
  export default async (options) => {
10
32
  const isJson = options.json;
11
33
  let server = options.server;
12
34
  const port = parseInt(options.port || '1080', 10);
35
+ const requestedMode = options.mode || 'auto';
13
36
 
14
37
  if (!server) {
15
- // Try to load default from profile if not provided
16
- const profiles = config.get('servers', {});
17
- const activeAlias = config.get('activeServer');
18
- if (activeAlias && profiles[activeAlias]) {
19
- server = profiles[activeAlias];
38
+ const { profiles, active } = getProfiles();
39
+ if (active && profiles[active]) {
40
+ server = profiles[active];
20
41
  } else {
21
42
  if (isJson) {
22
43
  console.log(JSON.stringify({ error: 'No server specified' }));
@@ -29,7 +50,7 @@ export default async (options) => {
29
50
  }
30
51
 
31
52
  // Check if already running
32
- const info = getTunnelInfo();
53
+ const info = getActiveTunnel();
33
54
  if (info) {
34
55
  if (isJson) {
35
56
  console.log(JSON.stringify({ error: 'Tunnel already running', pid: info.pid }));
@@ -47,18 +68,33 @@ export default async (options) => {
47
68
  oldIp = await getPublicIp();
48
69
  if (!isJson) {
49
70
  spinner.succeed(`Current IP: ${chalk.cyan(oldIp)}`);
50
- spinner.text = 'Starting SSH SOCKS5 tunnel...';
51
- spinner.start();
52
71
  }
53
-
54
- const pid = spawnSSH(server, port);
55
- saveTunnelInfo(pid, server, port);
56
-
72
+
73
+ const hostPart = server.includes('@') ? server.split('@')[1] : server;
74
+ let actualMode = requestedMode;
75
+
76
+ if (requestedMode === 'auto') {
77
+ if (!isJson) {
78
+ spinner.text = 'Checking server for TLS support...';
79
+ spinner.start();
80
+ }
81
+ const hasTls = await detectTlsServer(hostPart, 8443);
82
+ if (hasTls) {
83
+ actualMode = 'tls';
84
+ } else {
85
+ actualMode = 'ssh';
86
+ if (!isJson) {
87
+ spinner.info('TLS server not detected. Falling back to SSH mode...');
88
+ }
89
+ }
90
+ }
91
+
57
92
  if (!isJson) {
58
- spinner.text = 'Waiting for proxy to become ready...';
93
+ spinner.text = `Starting ${actualMode.toUpperCase()} tunnel...`;
94
+ spinner.start();
59
95
  }
60
-
61
- await waitForSocks(port);
96
+
97
+ const res = await startTunnel(server, port, actualMode);
62
98
 
63
99
  if (!isJson) {
64
100
  spinner.text = 'Verifying IP through proxy...';
@@ -68,7 +104,7 @@ export default async (options) => {
68
104
 
69
105
  if (!isJson) {
70
106
  spinner.stop();
71
- printSuccess(`Tunnel established successfully to ${server}`);
107
+ printSuccess(`Tunnel established successfully to ${server} (${actualMode.toUpperCase()} mode)`);
72
108
  console.log(`\n ${chalk.dim('Old IP:')} ${chalk.red(oldIp)}`);
73
109
  console.log(` ${chalk.dim('New IP:')} ${chalk.green(newIp)}`);
74
110
  console.log(` ${chalk.dim('Proxy :')} ${chalk.cyan(`socks5://127.0.0.1:${port}`)}`);
@@ -76,7 +112,7 @@ export default async (options) => {
76
112
  console.log(chalk.dim('\nTunnel is running in the background. Leave this terminal or close it, it will stay alive.'));
77
113
  console.log(chalk.dim('Run "polaris status" to check, or "polaris stop" to end.'));
78
114
  } else {
79
- console.log(JSON.stringify({ success: true, oldIp, newIp, proxy: `socks5://127.0.0.1:${port}`, pid }));
115
+ console.log(JSON.stringify({ success: true, oldIp, newIp, proxy: `socks5://127.0.0.1:${port}`, pid: res.pid, mode: actualMode }));
80
116
  }
81
117
 
82
118
  } catch (err) {
@@ -1,11 +1,11 @@
1
- import { getTunnelInfo } from '../tunnel/ssh.js';
2
- import { getProxiedIp, getPublicIp } from '../net/ip.js';
3
- import { createTable, createSpinner, printError } from '../ui/display.js';
1
+ import { getActiveTunnel } from '../core/tunnel-service.js';
2
+ import { getProxiedIp } from '../net/ip-check.js';
3
+ import { createTable, createSpinner, printError } from '../utils/display.js';
4
4
  import chalk from 'chalk';
5
5
 
6
6
  export default async (options) => {
7
7
  const isJson = options.json;
8
- const info = getTunnelInfo();
8
+ const info = getActiveTunnel();
9
9
 
10
10
  if (!info) {
11
11
  if (isJson) {
@@ -30,6 +30,7 @@ export default async (options) => {
30
30
  server: info.server,
31
31
  port: info.port,
32
32
  pid: info.pid,
33
+ mode: info.mode || 'ssh',
33
34
  ip: currentIp,
34
35
  uptimeMs
35
36
  }));
@@ -41,6 +42,7 @@ export default async (options) => {
41
42
 
42
43
  table.push(
43
44
  ['Status', chalk.green('Connected')],
45
+ ['Mode', (info.mode || 'ssh').toUpperCase()],
44
46
  ['Server', info.server],
45
47
  ['Proxy', `socks5://127.0.0.1:${info.port}`],
46
48
  ['Current IP', chalk.cyan(currentIp)],
@@ -1,21 +1,20 @@
1
- import { stopTunnel, getTunnelInfo } from '../tunnel/ssh.js';
2
- import { printSuccess, printInfo, printError } from '../ui/display.js';
1
+ import { stopActiveTunnel } from '../core/tunnel-service.js';
2
+ import { printSuccess, printInfo, printError } from '../utils/display.js';
3
3
 
4
4
  export default async (options) => {
5
5
  const isJson = options.json;
6
- const info = getTunnelInfo();
7
-
8
- if (!info) {
9
- if (isJson) {
10
- console.log(JSON.stringify({ success: true, message: 'No active tunnel found.' }));
11
- } else {
12
- printInfo('No active tunnel found.');
13
- }
14
- return;
15
- }
16
6
 
17
7
  try {
18
- stopTunnel();
8
+ const info = stopActiveTunnel();
9
+ if (!info) {
10
+ if (isJson) {
11
+ console.log(JSON.stringify({ success: true, message: 'No active tunnel found.' }));
12
+ } else {
13
+ printInfo('No active tunnel found.');
14
+ }
15
+ return;
16
+ }
17
+
19
18
  if (isJson) {
20
19
  console.log(JSON.stringify({ success: true, message: 'Tunnel stopped successfully.' }));
21
20
  } else {
@@ -0,0 +1,46 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { spawnDaemon, loadDaemonState, clearDaemonState, killPid, saveDaemonState } from '../utils/daemon.js';
4
+ import { DNS_PID_FILE, DNS_CONFIG_FILE, ensureDir } from '../utils/config.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export const getDnsStatus = () => {
10
+ return loadDaemonState(DNS_PID_FILE, DNS_CONFIG_FILE);
11
+ };
12
+
13
+ export const startDnsResolver = (port = 5354, upstream = 'https://cloudflare-dns.com/dns-query') => {
14
+ const active = getDnsStatus();
15
+ if (active) {
16
+ throw new Error(`DNS resolver is already running (PID: ${active.pid})`);
17
+ }
18
+
19
+ ensureDir();
20
+
21
+ // Point to static runner relative to this file
22
+ const serverScript = path.resolve(__dirname, '..', 'net', 'dns-runner.js');
23
+
24
+ const pid = spawnDaemon(serverScript, [], {
25
+ POLARIS_DNS_PORT: String(port),
26
+ POLARIS_DNS_UPSTREAM: upstream
27
+ });
28
+
29
+ saveDaemonState(DNS_PID_FILE, DNS_CONFIG_FILE, {
30
+ pid,
31
+ port,
32
+ upstream,
33
+ startTime: new Date().toISOString()
34
+ });
35
+
36
+ return { pid, port, upstream };
37
+ };
38
+
39
+ export const stopDnsResolver = () => {
40
+ const active = getDnsStatus();
41
+ if (active) {
42
+ killPid(active.pid);
43
+ }
44
+ clearDaemonState(DNS_PID_FILE, DNS_CONFIG_FILE);
45
+ return active;
46
+ };
@@ -0,0 +1,30 @@
1
+ import { store } from '../utils/config.js';
2
+
3
+ export const addProfile = (alias, server) => {
4
+ if (!server) {
5
+ throw new Error('Server address is required');
6
+ }
7
+ const profiles = store.get('servers', {});
8
+ profiles[alias] = server;
9
+ store.set('servers', profiles);
10
+
11
+ if (!store.get('activeServer')) {
12
+ store.set('activeServer', alias);
13
+ }
14
+ return { alias, server };
15
+ };
16
+
17
+ export const getProfiles = () => {
18
+ const profiles = store.get('servers', {});
19
+ const active = store.get('activeServer');
20
+ return { profiles, active };
21
+ };
22
+
23
+ export const setActiveProfile = (alias) => {
24
+ const profiles = store.get('servers', {});
25
+ if (!profiles[alias]) {
26
+ throw new Error(`Profile '${alias}' not found`);
27
+ }
28
+ store.set('activeServer', alias);
29
+ return { alias, server: profiles[alias] };
30
+ };
@@ -0,0 +1,39 @@
1
+ import { spawnSSH, waitForSocks } from '../tunnel/ssh.js';
2
+ import { startTlsBackground } from '../tunnel/tls.js';
3
+ import { loadDaemonState, clearDaemonState, killPid, saveDaemonState } from '../utils/daemon.js';
4
+ import { TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, ensureDir } from '../utils/config.js';
5
+
6
+ export const getActiveTunnel = () => {
7
+ return loadDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
8
+ };
9
+
10
+ export const stopActiveTunnel = () => {
11
+ const info = getActiveTunnel();
12
+ if (info) {
13
+ killPid(info.pid);
14
+ }
15
+ clearDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE);
16
+ return info;
17
+ };
18
+
19
+ export const startTunnel = async (server, port, mode = 'ssh') => {
20
+ const hostPart = server.includes('@') ? server.split('@')[1] : server;
21
+ ensureDir();
22
+
23
+ let pid;
24
+ if (mode === 'tls') {
25
+ pid = startTlsBackground(port, hostPart, 8443);
26
+ } else {
27
+ pid = spawnSSH(server, port);
28
+ saveDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, {
29
+ pid,
30
+ server,
31
+ port,
32
+ mode: 'ssh',
33
+ startTime: new Date().toISOString()
34
+ });
35
+ }
36
+
37
+ await waitForSocks(port);
38
+ return { pid, server, port, mode };
39
+ };
@@ -0,0 +1,72 @@
1
+ import dns2 from 'dns2';
2
+ import fetch from 'node-fetch';
3
+
4
+ export const resolveViaDoh = async (name, typeNum, upstreamUrl) => {
5
+ const typeMap = {
6
+ 1: 'A',
7
+ 28: 'AAAA',
8
+ 15: 'MX',
9
+ 16: 'TXT',
10
+ 5: 'CNAME',
11
+ 2: 'NS',
12
+ 6: 'SOA',
13
+ 12: 'PTR'
14
+ };
15
+ const typeName = typeMap[typeNum] || 'A';
16
+
17
+ const url = `${upstreamUrl}?name=${encodeURIComponent(name)}&type=${typeName}`;
18
+ const response = await fetch(url, {
19
+ headers: { 'accept': 'application/dns-json' }
20
+ });
21
+
22
+ if (!response.ok) {
23
+ throw new Error(`Upstream returned ${response.status}`);
24
+ }
25
+
26
+ return await response.json();
27
+ };
28
+
29
+ export const startDnsServerInstance = (port = 5354, upstream = 'https://cloudflare-dns.com/dns-query') => {
30
+ const { Packet } = dns2;
31
+
32
+ const server = dns2.createServer({
33
+ udp: true,
34
+ handle: async (request, send) => {
35
+ const response = Packet.createResponseFromRequest(request);
36
+
37
+ for (const question of request.questions) {
38
+ try {
39
+ const dohRes = await resolveViaDoh(question.name, question.type, upstream);
40
+ if (dohRes.Answer) {
41
+ for (const ans of dohRes.Answer) {
42
+ response.answers.push({
43
+ name: ans.name,
44
+ type: ans.type,
45
+ class: Packet.CLASS.IN,
46
+ ttl: ans.TTL || 300,
47
+ address: ans.data
48
+ });
49
+ }
50
+ }
51
+ } catch (err) {
52
+ // Ignore resolution failure on single record
53
+ }
54
+ }
55
+
56
+ send(response);
57
+ }
58
+ });
59
+
60
+ server.listen({ udp: port }).then(() => {
61
+ console.log(`Polaris DNS-over-HTTPS local resolver listening on 127.0.0.1:${port}`);
62
+ console.log(`Using upstream: ${upstream}`);
63
+ }).catch((err) => {
64
+ console.error('DNS server failed to start:', err);
65
+ });
66
+
67
+ server.on('error', (err) => {
68
+ console.error('DNS server error:', err);
69
+ });
70
+
71
+ return server;
72
+ };
@@ -0,0 +1,6 @@
1
+ import { startDnsServerInstance } from './dns-resolver.js';
2
+
3
+ const port = parseInt(process.env.POLARIS_DNS_PORT || '5354', 10);
4
+ const upstream = process.env.POLARIS_DNS_UPSTREAM || 'https://cloudflare-dns.com/dns-query';
5
+
6
+ startDnsServerInstance(port, upstream);
@@ -20,7 +20,6 @@ export const getProxiedIp = async (port) => {
20
20
  return data.ip;
21
21
  };
22
22
 
23
- // Simple DNS leak check by resolving via system dns
24
23
  export const checkDns = async () => {
25
24
  try {
26
25
  const servers = dns.getServers();
@@ -31,7 +30,6 @@ export const checkDns = async () => {
31
30
  }
32
31
  };
33
32
 
34
- // Returns IPv6 address if leaked, null otherwise
35
33
  export const checkIpv6Leak = async () => {
36
34
  try {
37
35
  const controller = new AbortController();
@@ -45,6 +43,6 @@ export const checkIpv6Leak = async () => {
45
43
  }
46
44
  return null;
47
45
  } catch (err) {
48
- return null; // Timeout or network error typically means IPv6 is disabled/unreachable
46
+ return null;
49
47
  }
50
48
  };
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import tls from 'tls';
3
+ import net from 'net';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import { execSync } from 'child_process';
8
+
9
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'polaris-server');
10
+ const KEY_FILE = path.join(CONFIG_DIR, 'server.key');
11
+ const CERT_FILE = path.join(CONFIG_DIR, 'server.crt');
12
+
13
+ const ensureConfigDir = () => {
14
+ if (!fs.existsSync(CONFIG_DIR)) {
15
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
16
+ }
17
+ };
18
+
19
+ const generateCerts = () => {
20
+ ensureConfigDir();
21
+ if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
22
+ return;
23
+ }
24
+ console.log('Generating self-signed TLS certificates...');
25
+ try {
26
+ execSync(
27
+ `openssl req -x509 -newkey rsa:2048 -nodes -sha256 -keyout "${KEY_FILE}" -out "${CERT_FILE}" -days 365 -subj "/CN=polaris"`,
28
+ { stdio: 'ignore' }
29
+ );
30
+ console.log('Certificates generated successfully.');
31
+ } catch (err) {
32
+ console.error('Failed to generate TLS certificates using openssl CLI:', err.message);
33
+ process.exit(1);
34
+ }
35
+ };
36
+
37
+ const startServer = (port = 8443) => {
38
+ generateCerts();
39
+
40
+ const options = {
41
+ key: fs.readFileSync(KEY_FILE),
42
+ cert: fs.readFileSync(CERT_FILE),
43
+ minVersion: 'TLSv1.3'
44
+ };
45
+
46
+ const server = tls.createServer(options, (cleartextStream) => {
47
+ let buffer = '';
48
+
49
+ const onData = (chunk) => {
50
+ buffer += chunk.toString('utf8');
51
+ const lineEnd = buffer.indexOf('\n');
52
+ if (lineEnd !== -1) {
53
+ const line = buffer.substring(0, lineEnd).trim();
54
+ buffer = buffer.substring(lineEnd + 1);
55
+
56
+ cleartextStream.removeListener('data', onData);
57
+
58
+ const match = line.match(/^CONNECT\s+(.+):(\d+)$/);
59
+ if (!match) {
60
+ cleartextStream.write('ERR Invalid command\n');
61
+ cleartextStream.end();
62
+ return;
63
+ }
64
+
65
+ const host = match[1];
66
+ const targetPort = parseInt(match[2], 10);
67
+
68
+ const targetSocket = net.connect(targetPort, host, () => {
69
+ cleartextStream.write('OK\n');
70
+ if (buffer.length > 0) {
71
+ targetSocket.write(buffer);
72
+ }
73
+ cleartextStream.pipe(targetSocket);
74
+ targetSocket.pipe(cleartextStream);
75
+ });
76
+
77
+ targetSocket.on('error', (err) => {
78
+ try {
79
+ cleartextStream.write(`ERR ${err.message}\n`);
80
+ cleartextStream.end();
81
+ } catch (e) {}
82
+ });
83
+
84
+ cleartextStream.on('error', () => {
85
+ targetSocket.destroy();
86
+ });
87
+ }
88
+ };
89
+
90
+ cleartextStream.on('data', onData);
91
+ });
92
+
93
+ server.listen(port, () => {
94
+ console.log(`Polaris TLS Server listening on port ${port}`);
95
+ });
96
+
97
+ server.on('error', (err) => {
98
+ console.error('Server error:', err);
99
+ });
100
+ };
101
+
102
+ const args = process.argv.slice(2);
103
+ const portOptIdx = args.indexOf('--port');
104
+ let port = 8443;
105
+ if (portOptIdx !== -1 && args[portOptIdx + 1]) {
106
+ port = parseInt(args[portOptIdx + 1], 10);
107
+ }
108
+
109
+ startServer(port);
package/src/tunnel/ssh.js CHANGED
@@ -1,27 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import net from 'net';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import os from 'os';
6
-
7
- const CONFIG_DIR = path.join(os.homedir(), '.config', 'polaris');
8
- const PID_FILE = path.join(CONFIG_DIR, 'tunnel.pid');
9
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
10
-
11
- const ensureDir = () => {
12
- if (!fs.existsSync(CONFIG_DIR)) {
13
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
14
- }
15
- };
16
3
 
17
4
  export const spawnSSH = (server, port) => {
18
- // We want to run SSH as a detached child process so it survives terminal close.
19
- // -D port: Dynamic SOCKS5 forwarding
20
- // -N: Do not execute a remote command
21
- // -o StrictHostKeyChecking=no: Don't prompt for host key verification
22
- // -o ServerAliveInterval=30: Keep connection alive
23
- // -o ExitOnForwardFailure=yes: Exit if port forwarding fails
24
-
25
5
  const args = [
26
6
  '-D', String(port),
27
7
  '-N',
@@ -33,15 +13,13 @@ export const spawnSSH = (server, port) => {
33
13
 
34
14
  const child = spawn('ssh', args, {
35
15
  detached: true,
36
- stdio: 'ignore' // We don't want to capture stdio, keep it completely detached
16
+ stdio: 'ignore'
37
17
  });
38
18
 
39
- child.unref(); // Allow the parent process to exit independently
40
-
19
+ child.unref();
41
20
  return child.pid;
42
21
  };
43
22
 
44
- // Poll the local port until it accepts connections
45
23
  export const waitForSocks = async (port, timeoutMs = 30000) => {
46
24
  const start = Date.now();
47
25
 
@@ -49,7 +27,6 @@ export const waitForSocks = async (port, timeoutMs = 30000) => {
49
27
  try {
50
28
  await new Promise((resolve, reject) => {
51
29
  const socket = new net.Socket();
52
-
53
30
  socket.setTimeout(1000);
54
31
 
55
32
  socket.on('connect', () => {
@@ -69,66 +46,11 @@ export const waitForSocks = async (port, timeoutMs = 30000) => {
69
46
 
70
47
  socket.connect(port, '127.0.0.1');
71
48
  });
72
- return true; // Connection successful
49
+ return true;
73
50
  } catch (err) {
74
- // Wait 500ms before retrying
75
51
  await new Promise(r => setTimeout(r, 500));
76
52
  }
77
53
  }
78
54
 
79
55
  throw new Error(`Timed out waiting for SOCKS5 proxy on port ${port} to become ready`);
80
56
  };
81
-
82
- export const saveTunnelInfo = (pid, server, port) => {
83
- ensureDir();
84
- fs.writeFileSync(PID_FILE, String(pid), 'utf-8');
85
-
86
- const config = {
87
- server,
88
- port,
89
- startTime: new Date().toISOString()
90
- };
91
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
92
- };
93
-
94
- export const getTunnelInfo = () => {
95
- if (!fs.existsSync(PID_FILE) || !fs.existsSync(CONFIG_FILE)) {
96
- return null;
97
- }
98
-
99
- try {
100
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
101
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
102
-
103
- // Check if process actually exists
104
- try {
105
- process.kill(pid, 0); // test signal 0 to check if alive
106
- } catch (e) {
107
- if (e.code === 'ESRCH') {
108
- // Process doesn't exist, tunnel is dead, cleanup
109
- clearTunnelInfo();
110
- return null;
111
- }
112
- }
113
-
114
- return { pid, ...config };
115
- } catch (err) {
116
- return null;
117
- }
118
- };
119
-
120
- export const stopTunnel = () => {
121
- const info = getTunnelInfo();
122
- if (info) {
123
- try {
124
- process.kill(info.pid, 'SIGTERM');
125
- } catch (e) {
126
- // Ignore errors if process already dead
127
- }
128
- }
129
- clearTunnelInfo();
130
- };
131
-
132
- export const clearTunnelInfo = () => {
133
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
134
- };
@@ -0,0 +1,85 @@
1
+ import { createSocksToTlsProxy } from './tls.js';
2
+ import tls from 'tls';
3
+ import notifier from 'node-notifier';
4
+
5
+ const localPort = parseInt(process.env.POLARIS_LOCAL_PORT || '1080', 10);
6
+ const remoteServer = process.env.POLARIS_REMOTE_SERVER;
7
+ const remotePort = parseInt(process.env.POLARIS_REMOTE_PORT || '8443', 10);
8
+
9
+ let proxyServer = createSocksToTlsProxy(localPort, remoteServer, remotePort);
10
+
11
+ // Keepalive pings & Auto-reconnect
12
+ const checkConnection = async () => {
13
+ return new Promise((resolve) => {
14
+ const socket = tls.connect({
15
+ host: remoteServer,
16
+ port: remotePort,
17
+ rejectUnauthorized: false,
18
+ timeout: 5000
19
+ }, () => {
20
+ socket.destroy();
21
+ resolve(true);
22
+ });
23
+
24
+ socket.on('error', () => {
25
+ socket.destroy();
26
+ resolve(false);
27
+ });
28
+
29
+ socket.on('timeout', () => {
30
+ socket.destroy();
31
+ resolve(false);
32
+ });
33
+ });
34
+ };
35
+
36
+ const reconnect = async (attempt = 1) => {
37
+ if (attempt > 5) {
38
+ notifier.notify({
39
+ title: 'Polaris VPN',
40
+ message: 'Failed to reconnect after 5 attempts. Tunnel stopped.'
41
+ });
42
+ process.exit(1);
43
+ }
44
+
45
+ notifier.notify({
46
+ title: 'Polaris VPN',
47
+ message: `Connection lost. Reconnecting (attempt ${attempt}/5)...`
48
+ });
49
+
50
+ try {
51
+ proxyServer.close();
52
+ } catch (e) {}
53
+
54
+ // Wait with exponential backoff
55
+ await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
56
+
57
+ try {
58
+ proxyServer = createSocksToTlsProxy(localPort, remoteServer, remotePort);
59
+ const alive = await checkConnection();
60
+ if (alive) {
61
+ notifier.notify({
62
+ title: 'Polaris VPN',
63
+ message: 'Reconnected successfully.'
64
+ });
65
+ startInterval();
66
+ } else {
67
+ reconnect(attempt + 1);
68
+ }
69
+ } catch (err) {
70
+ reconnect(attempt + 1);
71
+ }
72
+ };
73
+
74
+ let intervalId;
75
+ const startInterval = () => {
76
+ intervalId = setInterval(async () => {
77
+ const isAlive = await checkConnection();
78
+ if (!isAlive) {
79
+ clearInterval(intervalId);
80
+ reconnect();
81
+ }
82
+ }, 30000);
83
+ };
84
+
85
+ startInterval();
@@ -0,0 +1,131 @@
1
+ import net from 'net';
2
+ import tls from 'tls';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { spawnDaemon, saveDaemonState } from '../utils/daemon.js';
6
+ import { TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, ensureDir } from '../utils/config.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ // SOCKS5 to TLS proxy server instance logic
12
+ export const createSocksToTlsProxy = (localPort, remoteServer, remotePort = 8443) => {
13
+ const server = net.createServer((clientSocket) => {
14
+ let stage = 0; // 0: Greeting, 1: Request, 2: Connected
15
+
16
+ clientSocket.on('data', (data) => {
17
+ if (stage === 0) {
18
+ if (data[0] !== 0x05) {
19
+ clientSocket.destroy();
20
+ return;
21
+ }
22
+ clientSocket.write(Buffer.from([0x05, 0x00]));
23
+ stage = 1;
24
+ } else if (stage === 1) {
25
+ if (data[0] !== 0x05 || data[1] !== 0x01) {
26
+ clientSocket.write(Buffer.from([0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
27
+ clientSocket.destroy();
28
+ return;
29
+ }
30
+
31
+ const atyp = data[3];
32
+ let host = '';
33
+ let offset = 4;
34
+
35
+ if (atyp === 0x01) {
36
+ host = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
37
+ offset = 8;
38
+ } else if (atyp === 0x03) {
39
+ const len = data[4];
40
+ host = data.toString('utf8', 5, 5 + len);
41
+ offset = 5 + len;
42
+ } else if (atyp === 0x04) {
43
+ host = data.slice(4, 20).toString('hex');
44
+ offset = 20;
45
+ } else {
46
+ clientSocket.write(Buffer.from([0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
47
+ clientSocket.destroy();
48
+ return;
49
+ }
50
+
51
+ const port = data.readUInt16BE(offset);
52
+
53
+ // Connect to remote TLS server
54
+ const tlsSocket = tls.connect({
55
+ host: remoteServer,
56
+ port: remotePort,
57
+ rejectUnauthorized: false,
58
+ minVersion: 'TLSv1.3'
59
+ }, () => {
60
+ tlsSocket.write(`CONNECT ${host}:${port}\n`);
61
+ });
62
+
63
+ let responseBuffer = '';
64
+ const onTlsData = (chunk) => {
65
+ responseBuffer += chunk.toString('utf8');
66
+ const lineEnd = responseBuffer.indexOf('\n');
67
+ if (lineEnd !== -1) {
68
+ const line = responseBuffer.substring(0, lineEnd).trim();
69
+ tlsSocket.removeListener('data', onTlsData);
70
+
71
+ if (line === 'OK') {
72
+ clientSocket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
73
+ stage = 2;
74
+
75
+ const rest = responseBuffer.substring(lineEnd + 1);
76
+ if (rest.length > 0) {
77
+ clientSocket.write(Buffer.from(rest, 'utf8'));
78
+ }
79
+
80
+ clientSocket.pipe(tlsSocket);
81
+ tlsSocket.pipe(clientSocket);
82
+ } else {
83
+ clientSocket.write(Buffer.from([0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
84
+ clientSocket.destroy();
85
+ tlsSocket.destroy();
86
+ }
87
+ }
88
+ };
89
+
90
+ tlsSocket.on('data', onTlsData);
91
+
92
+ tlsSocket.on('error', () => {
93
+ try {
94
+ clientSocket.write(Buffer.from([0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
95
+ clientSocket.destroy();
96
+ } catch (e) {}
97
+ });
98
+
99
+ clientSocket.on('error', () => {
100
+ tlsSocket.destroy();
101
+ });
102
+ }
103
+ });
104
+ });
105
+
106
+ server.listen(localPort, '127.0.0.1');
107
+ return server;
108
+ };
109
+
110
+ // Spawn background proxy with keepalive/reconnect checks
111
+ export const startTlsBackground = (localPort, remoteServer, remotePort = 8443) => {
112
+ ensureDir();
113
+
114
+ const runnerScript = path.join(__dirname, 'tls-runner.js');
115
+
116
+ const pid = spawnDaemon(runnerScript, [], {
117
+ POLARIS_LOCAL_PORT: String(localPort),
118
+ POLARIS_REMOTE_SERVER: remoteServer,
119
+ POLARIS_REMOTE_PORT: String(remotePort)
120
+ });
121
+
122
+ saveDaemonState(TUNNEL_PID_FILE, TUNNEL_CONFIG_FILE, {
123
+ pid,
124
+ server: remoteServer,
125
+ port: localPort,
126
+ mode: 'tls',
127
+ startTime: new Date().toISOString()
128
+ });
129
+
130
+ return pid;
131
+ };
@@ -0,0 +1,18 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import Conf from 'conf';
5
+
6
+ export const CONFIG_DIR = path.join(os.homedir(), '.config', 'polaris');
7
+ export const TUNNEL_PID_FILE = path.join(CONFIG_DIR, 'tunnel.pid');
8
+ export const TUNNEL_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ export const DNS_PID_FILE = path.join(CONFIG_DIR, 'dns.pid');
10
+ export const DNS_CONFIG_FILE = path.join(CONFIG_DIR, 'dns.json');
11
+
12
+ export const ensureDir = (dir = CONFIG_DIR) => {
13
+ if (!fs.existsSync(dir)) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+ };
17
+
18
+ export const store = new Conf({ projectName: 'polaris' });
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import { spawn } from 'child_process';
3
+
4
+ export const isPidAlive = (pid) => {
5
+ if (!pid) return false;
6
+ try {
7
+ process.kill(pid, 0);
8
+ return true;
9
+ } catch (e) {
10
+ return false;
11
+ }
12
+ };
13
+
14
+ export const killPid = (pid, signal = 'SIGTERM') => {
15
+ if (!pid) return;
16
+ try {
17
+ process.kill(pid, signal);
18
+ } catch (e) {
19
+ // Ignore if already dead
20
+ }
21
+ };
22
+
23
+ export const spawnDaemon = (scriptPath, args = [], env = {}) => {
24
+ const child = spawn('node', [scriptPath, ...args], {
25
+ detached: true,
26
+ stdio: 'ignore',
27
+ env: {
28
+ ...process.env,
29
+ ...env
30
+ }
31
+ });
32
+ child.unref();
33
+ return child.pid;
34
+ };
35
+
36
+ export const saveDaemonState = (pidFile, configFile, data) => {
37
+ fs.writeFileSync(pidFile, String(data.pid), 'utf-8');
38
+ fs.writeFileSync(configFile, JSON.stringify(data, null, 2), 'utf-8');
39
+ };
40
+
41
+ export const loadDaemonState = (pidFile, configFile) => {
42
+ if (!fs.existsSync(pidFile) || !fs.existsSync(configFile)) {
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
48
+ const config = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
49
+
50
+ if (!isPidAlive(pid)) {
51
+ clearDaemonState(pidFile, configFile);
52
+ return null;
53
+ }
54
+
55
+ return { pid, ...config };
56
+ } catch (err) {
57
+ return null;
58
+ }
59
+ };
60
+
61
+ export const clearDaemonState = (pidFile, configFile) => {
62
+ try {
63
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
64
+ } catch (e) {}
65
+ try {
66
+ if (fs.existsSync(configFile)) fs.unlinkSync(configFile);
67
+ } catch (e) {}
68
+ };
File without changes