blue-js-sdk 2.0.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 +446 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/ai-path/ADMIN-ELEVATION.md +116 -0
- package/ai-path/AI-MANIFESTO.md +185 -0
- package/ai-path/BREAKING.md +74 -0
- package/ai-path/CHECKLIST.md +619 -0
- package/ai-path/CONNECTION-STEPS.md +724 -0
- package/ai-path/DECISION-TREE.md +378 -0
- package/ai-path/DEPENDENCIES.md +459 -0
- package/ai-path/E2E-FLOW.md +1555 -0
- package/ai-path/FAILURES.md +403 -0
- package/ai-path/GUIDE.md +1217 -0
- package/ai-path/README.md +558 -0
- package/ai-path/SPLIT-TUNNEL.md +266 -0
- package/ai-path/cli.js +535 -0
- package/ai-path/connect.js +884 -0
- package/ai-path/discover.js +178 -0
- package/ai-path/environment.js +266 -0
- package/ai-path/errors.js +86 -0
- package/ai-path/examples/autonomous-agent.mjs +220 -0
- package/ai-path/examples/multi-region.mjs +174 -0
- package/ai-path/examples/one-shot.mjs +31 -0
- package/ai-path/index.js +60 -0
- package/ai-path/pricing.js +136 -0
- package/ai-path/recommend.js +413 -0
- package/ai-path/run-admin.vbs +25 -0
- package/ai-path/setup.js +291 -0
- package/ai-path/wallet.js +137 -0
- package/app-helpers.js +363 -0
- package/app-settings.js +95 -0
- package/app-types.js +267 -0
- package/audit.js +847 -0
- package/batch.js +293 -0
- package/bin/setup.js +376 -0
- package/chain/authz.js +109 -0
- package/chain/broadcast.js +472 -0
- package/chain/client.js +160 -0
- package/chain/fee-grants.js +305 -0
- package/chain/index.js +891 -0
- package/chain/lcd.js +313 -0
- package/chain/queries.js +547 -0
- package/chain/rpc.js +408 -0
- package/chain/wallet.js +141 -0
- package/cli/config.js +143 -0
- package/cli/index.js +463 -0
- package/cli/output.js +182 -0
- package/cli.js +491 -0
- package/client/index.js +251 -0
- package/client.js +271 -0
- package/config/index.js +255 -0
- package/connection/connect.js +849 -0
- package/connection/disconnect.js +180 -0
- package/connection/discovery.js +321 -0
- package/connection/index.js +76 -0
- package/connection/proxy.js +148 -0
- package/connection/resilience.js +428 -0
- package/connection/security.js +232 -0
- package/connection/state.js +369 -0
- package/connection/tunnel.js +691 -0
- package/consumer.js +132 -0
- package/cosmjs-setup.js +1884 -0
- package/defaults.js +366 -0
- package/disk-cache.js +107 -0
- package/dist/client.d.ts +108 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +400 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/errors/index.js +112 -0
- package/errors.js +218 -0
- package/examples/README.md +64 -0
- package/examples/connect-direct.mjs +106 -0
- package/examples/connect-plan.mjs +125 -0
- package/examples/error-handling.mjs +109 -0
- package/examples/query-nodes.mjs +94 -0
- package/examples/wallet-basics.mjs +61 -0
- package/generated/amino/amino.ts +9 -0
- package/generated/cosmos/base/v1beta1/coin.ts +365 -0
- package/generated/cosmos_proto/cosmos.ts +323 -0
- package/generated/gogoproto/gogo.ts +9 -0
- package/generated/google/protobuf/descriptor.ts +7601 -0
- package/generated/google/protobuf/duration.ts +208 -0
- package/generated/google/protobuf/timestamp.ts +238 -0
- package/generated/sentinel/lease/v1/events.ts +924 -0
- package/generated/sentinel/lease/v1/lease.ts +292 -0
- package/generated/sentinel/lease/v1/msg.ts +949 -0
- package/generated/sentinel/lease/v1/params.ts +164 -0
- package/generated/sentinel/node/v3/events.ts +881 -0
- package/generated/sentinel/node/v3/msg.ts +1002 -0
- package/generated/sentinel/node/v3/node.ts +263 -0
- package/generated/sentinel/node/v3/params.ts +183 -0
- package/generated/sentinel/plan/v3/events.ts +675 -0
- package/generated/sentinel/plan/v3/msg.ts +1191 -0
- package/generated/sentinel/plan/v3/plan.ts +283 -0
- package/generated/sentinel/provider/v2/events.ts +171 -0
- package/generated/sentinel/provider/v2/msg.ts +480 -0
- package/generated/sentinel/provider/v2/params.ts +131 -0
- package/generated/sentinel/provider/v2/provider.ts +246 -0
- package/generated/sentinel/session/v3/events.ts +480 -0
- package/generated/sentinel/session/v3/msg.ts +616 -0
- package/generated/sentinel/session/v3/params.ts +260 -0
- package/generated/sentinel/session/v3/proof.ts +180 -0
- package/generated/sentinel/session/v3/session.ts +384 -0
- package/generated/sentinel/subscription/v3/events.ts +1181 -0
- package/generated/sentinel/subscription/v3/msg.ts +1305 -0
- package/generated/sentinel/subscription/v3/params.ts +167 -0
- package/generated/sentinel/subscription/v3/subscription.ts +315 -0
- package/generated/sentinel/types/v1/bandwidth.ts +124 -0
- package/generated/sentinel/types/v1/price.ts +149 -0
- package/generated/sentinel/types/v1/renewal.ts +87 -0
- package/generated/sentinel/types/v1/status.ts +54 -0
- package/generated/typeRegistry.ts +27 -0
- package/index.js +486 -0
- package/node-connect.js +3015 -0
- package/operator.js +134 -0
- package/package.json +113 -0
- package/plan-operations.js +199 -0
- package/preflight.js +352 -0
- package/pricing/index.js +262 -0
- package/proto/amino/amino.proto +84 -0
- package/proto/cosmos/base/v1beta1/coin.proto +61 -0
- package/proto/cosmos_proto/cosmos.proto +112 -0
- package/proto/gogoproto/gogo.proto +145 -0
- package/proto/google/api/annotations.proto +31 -0
- package/proto/google/api/http.proto +370 -0
- package/proto/google/protobuf/any.proto +106 -0
- package/proto/google/protobuf/duration.proto +115 -0
- package/proto/google/protobuf/timestamp.proto +145 -0
- package/proto/sentinel/lease/v1/events.proto +52 -0
- package/proto/sentinel/lease/v1/genesis.proto +15 -0
- package/proto/sentinel/lease/v1/lease.proto +25 -0
- package/proto/sentinel/lease/v1/msg.proto +62 -0
- package/proto/sentinel/lease/v1/params.proto +17 -0
- package/proto/sentinel/node/v3/events.proto +50 -0
- package/proto/sentinel/node/v3/genesis.proto +15 -0
- package/proto/sentinel/node/v3/msg.proto +63 -0
- package/proto/sentinel/node/v3/node.proto +27 -0
- package/proto/sentinel/node/v3/params.proto +21 -0
- package/proto/sentinel/node/v3/querier.proto +63 -0
- package/proto/sentinel/plan/v3/events.proto +41 -0
- package/proto/sentinel/plan/v3/genesis.proto +21 -0
- package/proto/sentinel/plan/v3/msg.proto +83 -0
- package/proto/sentinel/plan/v3/plan.proto +32 -0
- package/proto/sentinel/plan/v3/querier.proto +53 -0
- package/proto/sentinel/provider/v2/events.proto +16 -0
- package/proto/sentinel/provider/v2/genesis.proto +15 -0
- package/proto/sentinel/provider/v2/msg.proto +35 -0
- package/proto/sentinel/provider/v2/params.proto +17 -0
- package/proto/sentinel/provider/v2/provider.proto +24 -0
- package/proto/sentinel/provider/v3/genesis.proto +15 -0
- package/proto/sentinel/provider/v3/params.proto +13 -0
- package/proto/sentinel/session/v3/events.proto +30 -0
- package/proto/sentinel/session/v3/genesis.proto +15 -0
- package/proto/sentinel/session/v3/msg.proto +50 -0
- package/proto/sentinel/session/v3/params.proto +25 -0
- package/proto/sentinel/session/v3/proof.proto +25 -0
- package/proto/sentinel/session/v3/querier.proto +100 -0
- package/proto/sentinel/session/v3/session.proto +50 -0
- package/proto/sentinel/subscription/v2/allocation.proto +21 -0
- package/proto/sentinel/subscription/v2/payout.proto +22 -0
- package/proto/sentinel/subscription/v3/events.proto +65 -0
- package/proto/sentinel/subscription/v3/genesis.proto +17 -0
- package/proto/sentinel/subscription/v3/msg.proto +83 -0
- package/proto/sentinel/subscription/v3/params.proto +21 -0
- package/proto/sentinel/subscription/v3/subscription.proto +33 -0
- package/proto/sentinel/types/v1/bandwidth.proto +19 -0
- package/proto/sentinel/types/v1/price.proto +21 -0
- package/proto/sentinel/types/v1/renewal.proto +21 -0
- package/proto/sentinel/types/v1/status.proto +16 -0
- package/protocol/encoding.js +341 -0
- package/protocol/events.js +361 -0
- package/protocol/handshake.js +297 -0
- package/protocol/index.js +15 -0
- package/protocol/messages.js +346 -0
- package/protocol/plans.js +199 -0
- package/protocol/v2ray.js +268 -0
- package/protocol/v3.js +723 -0
- package/protocol/wireguard.js +125 -0
- package/security/index.js +132 -0
- package/session-manager.js +329 -0
- package/session-tracker.js +80 -0
- package/setup.js +376 -0
- package/speedtest/index.js +528 -0
- package/speedtest.js +567 -0
- package/src/client.ts +502 -0
- package/src/index.ts +20 -0
- package/state/index.js +347 -0
- package/state.js +516 -0
- package/test-all-chain-ops.js +493 -0
- package/test-all-logic.js +199 -0
- package/test-all-msg-types.js +292 -0
- package/test-every-connection.js +208 -0
- package/test-feegrant-connect.js +98 -0
- package/test-logic.js +148 -0
- package/test-mainnet.js +176 -0
- package/test-plan-lifecycle.js +335 -0
- package/tls-trust.js +132 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +34 -0
- package/types/chain.d.ts +746 -0
- package/types/connection.d.ts +425 -0
- package/types/errors.d.ts +174 -0
- package/types/index.d.ts +1380 -0
- package/types/nodes.d.ts +187 -0
- package/types/pricing.d.ts +156 -0
- package/types/protocol.d.ts +332 -0
- package/types/session.d.ts +236 -0
- package/types/settings.d.ts +192 -0
- package/v3protocol.js +1053 -0
- package/wallet/index.js +153 -0
- package/wireguard.js +307 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security — kill switch, DNS leak prevention.
|
|
3
|
+
*
|
|
4
|
+
* Controls firewall rules and DNS settings to prevent traffic leaks
|
|
5
|
+
* when the VPN tunnel is active.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
9
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
10
|
+
|
|
11
|
+
import { _defaultState } from './state.js';
|
|
12
|
+
import { saveState } from '../state.js';
|
|
13
|
+
import { TunnelError } from '../errors.js';
|
|
14
|
+
|
|
15
|
+
// ─── Kill Switch (Firewall / Packet Filter) ────────────────────────────────
|
|
16
|
+
|
|
17
|
+
let _killSwitchEnabled = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enable kill switch — blocks all non-tunnel traffic.
|
|
21
|
+
* Windows: netsh advfirewall, macOS: pfctl, Linux: iptables.
|
|
22
|
+
* Call after WireGuard tunnel is installed.
|
|
23
|
+
* @param {string} serverEndpoint - WireGuard server "IP:PORT"
|
|
24
|
+
* @param {string} [tunnelName='wgsent0'] - WireGuard interface name
|
|
25
|
+
*/
|
|
26
|
+
export function enableKillSwitch(serverEndpoint, tunnelName = 'wgsent0') {
|
|
27
|
+
const [serverIp, serverPort] = serverEndpoint.split(':');
|
|
28
|
+
|
|
29
|
+
if (process.platform === 'win32') {
|
|
30
|
+
// Windows: netsh advfirewall
|
|
31
|
+
// Block all outbound by default
|
|
32
|
+
execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,blockoutbound'], { stdio: 'pipe' });
|
|
33
|
+
|
|
34
|
+
// Wrap allow rules in try-catch — if any fail after block-all, restore default policy
|
|
35
|
+
// to prevent permanent internet loss from partial firewall state.
|
|
36
|
+
try {
|
|
37
|
+
// Allow tunnel interface
|
|
38
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-Tunnel', 'dir=out', `interface=${tunnelName}`, 'action=allow'], { stdio: 'pipe' });
|
|
39
|
+
|
|
40
|
+
// Allow WireGuard endpoint (UDP to server)
|
|
41
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-WG-Endpoint', 'dir=out', 'action=allow', 'protocol=udp', `remoteip=${serverIp}`, `remoteport=${serverPort}`], { stdio: 'pipe' });
|
|
42
|
+
|
|
43
|
+
// Allow loopback
|
|
44
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-Loopback', 'dir=out', 'action=allow', 'remoteip=127.0.0.1'], { stdio: 'pipe' });
|
|
45
|
+
|
|
46
|
+
// Allow DHCP
|
|
47
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-DHCP', 'dir=out', 'action=allow', 'protocol=udp', 'localport=68', 'remoteport=67'], { stdio: 'pipe' });
|
|
48
|
+
|
|
49
|
+
// Allow DNS only through tunnel
|
|
50
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-DNS-Tunnel', 'dir=out', 'action=allow', 'protocol=udp', 'remoteip=10.8.0.1', 'remoteport=53'], { stdio: 'pipe' });
|
|
51
|
+
|
|
52
|
+
// Block IPv6 (prevent leaks)
|
|
53
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Block-IPv6', 'dir=out', 'action=block', 'protocol=any', 'remoteip=::/0'], { stdio: 'pipe' });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Emergency restore — unblock outbound so user isn't locked out
|
|
56
|
+
try { execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,allowoutbound'], { stdio: 'pipe' }); } catch { /* last resort */ }
|
|
57
|
+
_killSwitchEnabled = false;
|
|
58
|
+
throw new TunnelError('KILL_SWITCH_FAILED', `Kill switch failed: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
} else if (process.platform === 'darwin') {
|
|
62
|
+
// macOS: pfctl (packet filter)
|
|
63
|
+
const pfRules = [
|
|
64
|
+
'# Sentinel VPN Kill Switch',
|
|
65
|
+
'block out all',
|
|
66
|
+
`pass out on ${tunnelName} all`,
|
|
67
|
+
`pass out proto udp from any to ${serverIp} port ${serverPort}`,
|
|
68
|
+
'pass out on lo0 all',
|
|
69
|
+
'pass out proto udp from any port 68 to any port 67',
|
|
70
|
+
'pass out proto udp from any to 10.8.0.1 port 53',
|
|
71
|
+
'block out inet6 all',
|
|
72
|
+
].join('\n') + '\n';
|
|
73
|
+
|
|
74
|
+
const pfPath = '/tmp/sentinel-killswitch.conf';
|
|
75
|
+
writeFileSync(pfPath, pfRules, { mode: 0o600 });
|
|
76
|
+
|
|
77
|
+
// Save current pf state for restore
|
|
78
|
+
try { execFileSync('pfctl', ['-sr'], { encoding: 'utf8', stdio: 'pipe' }); } catch { /* may not have existing rules */ }
|
|
79
|
+
|
|
80
|
+
// Load rules and enable pf
|
|
81
|
+
execFileSync('pfctl', ['-f', pfPath], { stdio: 'pipe' });
|
|
82
|
+
execFileSync('pfctl', ['-e'], { stdio: 'pipe' });
|
|
83
|
+
|
|
84
|
+
} else {
|
|
85
|
+
// Linux: iptables
|
|
86
|
+
// Flush existing sentinel rules first
|
|
87
|
+
try { execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
|
|
88
|
+
|
|
89
|
+
// Allow loopback
|
|
90
|
+
execFileSync('iptables', ['-A', 'OUTPUT', '-o', 'lo', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
|
|
91
|
+
|
|
92
|
+
// Allow tunnel interface
|
|
93
|
+
execFileSync('iptables', ['-A', 'OUTPUT', '-o', tunnelName, '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
|
|
94
|
+
|
|
95
|
+
// Allow WireGuard server endpoint
|
|
96
|
+
execFileSync('iptables', ['-A', 'OUTPUT', '-d', serverIp, '-p', 'udp', '--dport', serverPort, '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
|
|
97
|
+
|
|
98
|
+
// Allow DHCP
|
|
99
|
+
execFileSync('iptables', ['-A', 'OUTPUT', '-p', 'udp', '--sport', '68', '--dport', '67', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
|
|
100
|
+
|
|
101
|
+
// Allow DNS only through tunnel
|
|
102
|
+
execFileSync('iptables', ['-A', 'OUTPUT', '-d', '10.8.0.1', '-p', 'udp', '--dport', '53', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
|
|
103
|
+
|
|
104
|
+
// Block everything else
|
|
105
|
+
execFileSync('iptables', ['-A', 'OUTPUT', '-j', 'DROP', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
|
|
106
|
+
|
|
107
|
+
// Block IPv6
|
|
108
|
+
try { execFileSync('ip6tables', ['-A', 'OUTPUT', '-j', 'DROP', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' }); } catch { /* ip6tables may not be available */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_killSwitchEnabled = true;
|
|
112
|
+
// Persist kill switch state — survives crash so recoverOrphans() can restore internet
|
|
113
|
+
try {
|
|
114
|
+
const conn = _defaultState.connection || {};
|
|
115
|
+
saveState({ sessionId: conn.sessionId, serviceType: conn.serviceType, nodeAddress: conn.nodeAddress, killSwitchEnabled: true });
|
|
116
|
+
} catch {} // best-effort
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Disable kill switch — restore normal routing.
|
|
121
|
+
* Windows: removes netsh rules, macOS: disables pfctl, Linux: removes iptables rules.
|
|
122
|
+
*/
|
|
123
|
+
export function disableKillSwitch() {
|
|
124
|
+
if (!_killSwitchEnabled) return;
|
|
125
|
+
|
|
126
|
+
if (process.platform === 'win32') {
|
|
127
|
+
// Windows: remove firewall rules
|
|
128
|
+
const rules = [
|
|
129
|
+
'SentinelVPN-Allow-Tunnel',
|
|
130
|
+
'SentinelVPN-Allow-WG-Endpoint',
|
|
131
|
+
'SentinelVPN-Allow-Loopback',
|
|
132
|
+
'SentinelVPN-Allow-DHCP',
|
|
133
|
+
'SentinelVPN-Allow-DNS-Tunnel',
|
|
134
|
+
'SentinelVPN-Block-IPv6',
|
|
135
|
+
];
|
|
136
|
+
for (const rule of rules) {
|
|
137
|
+
try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', `name=${rule}`], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Restore default outbound policy
|
|
141
|
+
try { execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,allowoutbound'], { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
142
|
+
|
|
143
|
+
} else if (process.platform === 'darwin') {
|
|
144
|
+
// macOS: disable pf and remove temp rules
|
|
145
|
+
try { execFileSync('pfctl', ['-d'], { stdio: 'pipe' }); } catch { /* pf may already be disabled */ }
|
|
146
|
+
try { unlinkSync('/tmp/sentinel-killswitch.conf'); } catch { /* file may not exist */ }
|
|
147
|
+
|
|
148
|
+
} else {
|
|
149
|
+
// Linux: remove all sentinel-vpn rules
|
|
150
|
+
let hasRules = true;
|
|
151
|
+
while (hasRules) {
|
|
152
|
+
try {
|
|
153
|
+
execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'ACCEPT'], { stdio: 'pipe' });
|
|
154
|
+
} catch {
|
|
155
|
+
hasRules = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try { execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
|
|
159
|
+
try { execFileSync('ip6tables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_killSwitchEnabled = false;
|
|
163
|
+
// Persist cleared kill switch state
|
|
164
|
+
try {
|
|
165
|
+
const conn = _defaultState.connection || {};
|
|
166
|
+
saveState({ sessionId: conn.sessionId, serviceType: conn.serviceType, nodeAddress: conn.nodeAddress, killSwitchEnabled: false });
|
|
167
|
+
} catch {} // best-effort
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Check if kill switch is enabled */
|
|
171
|
+
export function isKillSwitchEnabled() { return _killSwitchEnabled; }
|
|
172
|
+
|
|
173
|
+
// ─── DNS Leak Prevention ────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Enable DNS leak prevention by forcing all DNS through the VPN tunnel.
|
|
177
|
+
* Windows: netsh interface ipv4 set dnsservers + firewall rules
|
|
178
|
+
* macOS: networksetup -setdnsservers
|
|
179
|
+
* Linux: write /etc/resolv.conf
|
|
180
|
+
* @param {string} [dnsServer='10.8.0.1'] - DNS server inside the tunnel
|
|
181
|
+
* @param {string} [tunnelInterface='wgsent0'] - WireGuard tunnel interface name
|
|
182
|
+
*/
|
|
183
|
+
export function enableDnsLeakPrevention(dnsServer = '10.8.0.1', tunnelInterface = 'wgsent0') {
|
|
184
|
+
const platform = process.platform;
|
|
185
|
+
if (platform === 'win32') {
|
|
186
|
+
// Set DNS on all interfaces to tunnel DNS
|
|
187
|
+
execFileSync('netsh', ['interface', 'ipv4', 'set', 'dnsservers', tunnelInterface, 'static', dnsServer, 'primary'], { stdio: 'pipe' });
|
|
188
|
+
// Block DNS on non-tunnel interfaces
|
|
189
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule',
|
|
190
|
+
'name=SentinelDNSBlock', 'dir=out', 'protocol=udp', 'remoteport=53',
|
|
191
|
+
'action=block'], { stdio: 'pipe' });
|
|
192
|
+
execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule',
|
|
193
|
+
'name=SentinelDNSAllow', 'dir=out', 'protocol=udp', 'remoteport=53',
|
|
194
|
+
'interface=' + tunnelInterface, 'action=allow'], { stdio: 'pipe' });
|
|
195
|
+
} else if (platform === 'darwin') {
|
|
196
|
+
// macOS: set DNS via networksetup for all services
|
|
197
|
+
const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8' })
|
|
198
|
+
.split('\n').filter(s => s && !s.startsWith('*'));
|
|
199
|
+
for (const svc of services) {
|
|
200
|
+
try { execFileSync('networksetup', ['-setdnsservers', svc.trim(), dnsServer], { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
// Linux: backup and overwrite resolv.conf
|
|
204
|
+
try { execFileSync('cp', ['/etc/resolv.conf', '/etc/resolv.conf.sentinel.bak'], { stdio: 'pipe' }); } catch { /* backup may fail if file missing */ }
|
|
205
|
+
writeFileSync('/etc/resolv.conf', `nameserver ${dnsServer}\n`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Disable DNS leak prevention and restore normal DNS resolution.
|
|
211
|
+
* Windows: removes firewall rules, resets DNS to DHCP
|
|
212
|
+
* macOS: clears DNS overrides
|
|
213
|
+
* Linux: restores /etc/resolv.conf from backup
|
|
214
|
+
*/
|
|
215
|
+
export function disableDnsLeakPrevention() {
|
|
216
|
+
const platform = process.platform;
|
|
217
|
+
if (platform === 'win32') {
|
|
218
|
+
try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', 'name=SentinelDNSBlock'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
|
|
219
|
+
try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', 'name=SentinelDNSAllow'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
|
|
220
|
+
// Reset DNS to DHCP
|
|
221
|
+
try { execFileSync('netsh', ['interface', 'ipv4', 'set', 'dnsservers', 'Wi-Fi', 'dhcp'], { stdio: 'pipe' }); } catch { /* interface may not exist */ }
|
|
222
|
+
try { execFileSync('netsh', ['interface', 'ipv4', 'set', 'dnsservers', 'Ethernet', 'dhcp'], { stdio: 'pipe' }); } catch { /* interface may not exist */ }
|
|
223
|
+
} else if (platform === 'darwin') {
|
|
224
|
+
const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8' })
|
|
225
|
+
.split('\n').filter(s => s && !s.startsWith('*'));
|
|
226
|
+
for (const svc of services) {
|
|
227
|
+
try { execFileSync('networksetup', ['-setdnsservers', svc.trim(), 'empty'], { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
try { execFileSync('cp', ['/etc/resolv.conf.sentinel.bak', '/etc/resolv.conf'], { stdio: 'pipe' }); } catch { /* backup may not exist */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection State — shared state, event emitter, metrics, wallet cache, and helpers.
|
|
3
|
+
*
|
|
4
|
+
* This module owns all mutable state that other connection modules depend on.
|
|
5
|
+
* Import ConnectionState, _defaultState, events, etc. from here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { execFileSync } from 'child_process';
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { sha256 as _sha256 } from '@cosmjs/crypto';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createWallet, createClient, broadcast, buildEndSessionMsg,
|
|
15
|
+
} from '../cosmjs-setup.js';
|
|
16
|
+
import {
|
|
17
|
+
SentinelError, NodeError, ErrorCodes,
|
|
18
|
+
} from '../errors.js';
|
|
19
|
+
import {
|
|
20
|
+
saveState, clearState,
|
|
21
|
+
} from '../state.js';
|
|
22
|
+
import {
|
|
23
|
+
sleep, RPC_ENDPOINTS, tryWithFallback,
|
|
24
|
+
} from '../defaults.js';
|
|
25
|
+
|
|
26
|
+
// ─── Event Emitter ───────────────────────────────────────────────────────────
|
|
27
|
+
// Subscribe to SDK lifecycle events without polling:
|
|
28
|
+
// import { events } from './connection/state.js';
|
|
29
|
+
// events.on('connected', ({ sessionId, serviceType }) => updateUI());
|
|
30
|
+
// events.on('disconnected', ({ reason }) => showNotification());
|
|
31
|
+
// events.on('progress', ({ step, detail }) => updateProgressBar());
|
|
32
|
+
|
|
33
|
+
export const events = new EventEmitter();
|
|
34
|
+
|
|
35
|
+
// ─── Cleanup Safety ──────────────────────────────────────────────────────────
|
|
36
|
+
// Track whether registerCleanupHandlers() has been called. If a developer calls
|
|
37
|
+
// connect() without registering, they risk orphaning WireGuard adapters or V2Ray
|
|
38
|
+
// processes on crash/SIGINT — the "Dead Internet" bug.
|
|
39
|
+
let _cleanupRegistered = false;
|
|
40
|
+
|
|
41
|
+
export function isCleanupRegistered() { return _cleanupRegistered; }
|
|
42
|
+
export function markCleanupRegistered() { _cleanupRegistered = true; }
|
|
43
|
+
|
|
44
|
+
export function warnIfNoCleanup(fnName) {
|
|
45
|
+
if (!_cleanupRegistered) {
|
|
46
|
+
throw new SentinelError(ErrorCodes.INVALID_OPTIONS,
|
|
47
|
+
`${fnName}() called without registerCleanupHandlers(). ` +
|
|
48
|
+
`If your app crashes, WireGuard/V2Ray tunnels will orphan and kill the user's internet. ` +
|
|
49
|
+
`Call registerCleanupHandlers() once at app startup, or use quickConnect() which does it automatically.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Connection Mutex ─────────────────────────────────────────────────────────
|
|
55
|
+
// v27: Prevent concurrent connection attempts (backported from C# SemaphoreSlim).
|
|
56
|
+
// Only one connect call may be in-flight at a time. quickConnect inherits via connectAuto.
|
|
57
|
+
let _connectLock = false;
|
|
58
|
+
|
|
59
|
+
// v30: Abort flag — disconnect() sets this to stop a running connectAuto() retry loop.
|
|
60
|
+
// Without this, disconnect() clears tunnel state but connectAuto() keeps retrying,
|
|
61
|
+
// paying for new sessions. The user cannot reconnect because _connectLock stays held.
|
|
62
|
+
let _abortConnect = false;
|
|
63
|
+
|
|
64
|
+
/** Check if a connection attempt is currently in progress. */
|
|
65
|
+
export function isConnecting() { return _connectLock; }
|
|
66
|
+
export function getConnectLock() { return _connectLock; }
|
|
67
|
+
export function setConnectLock(v) { _connectLock = v; }
|
|
68
|
+
export function getAbortConnect() { return _abortConnect; }
|
|
69
|
+
export function setAbortConnect(v) { _abortConnect = v; }
|
|
70
|
+
|
|
71
|
+
// ─── Connection State ─────────────────────────────────────────────────────────
|
|
72
|
+
// v22: Encapsulated state enables per-instance connections via SentinelClient.
|
|
73
|
+
// Module-level functions use _defaultState for backward compatibility.
|
|
74
|
+
|
|
75
|
+
// Global registry of active states — used by exit handlers to clean up all instances
|
|
76
|
+
export const _activeStates = new Set();
|
|
77
|
+
|
|
78
|
+
export class ConnectionState {
|
|
79
|
+
constructor() {
|
|
80
|
+
this.v2rayProc = null;
|
|
81
|
+
this.wgTunnel = null;
|
|
82
|
+
this.systemProxy = false;
|
|
83
|
+
this.connection = null; // { nodeAddress, serviceType, sessionId, connectedAt, socksPort? }
|
|
84
|
+
this.savedProxyState = null;
|
|
85
|
+
this._mnemonic = null; // Stored for session-end TX on disconnect (zeroed after use)
|
|
86
|
+
_activeStates.add(this);
|
|
87
|
+
}
|
|
88
|
+
get isConnected() { return !!(this.v2rayProc || this.wgTunnel); }
|
|
89
|
+
destroy() { _activeStates.delete(this); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const _defaultState = new ConnectionState();
|
|
93
|
+
|
|
94
|
+
// Default logger — can be overridden per-call via opts.log
|
|
95
|
+
export let defaultLog = console.log;
|
|
96
|
+
|
|
97
|
+
// ─── Wallet Cache ────────────────────────────────────────────────────────────
|
|
98
|
+
// v21: Cache wallet derivation (BIP39 → SLIP-10 is CPU-bound, ~300ms).
|
|
99
|
+
// Same mnemonic always produces the same wallet — safe to cache.
|
|
100
|
+
// Keyed by full SHA256 of mnemonic to avoid storing the raw mnemonic.
|
|
101
|
+
|
|
102
|
+
const _walletCache = new Map();
|
|
103
|
+
|
|
104
|
+
export async function cachedCreateWallet(mnemonic) {
|
|
105
|
+
const key = Buffer.from(_sha256(Buffer.from(mnemonic))).toString('hex'); // full SHA256 — no truncation
|
|
106
|
+
if (_walletCache.has(key)) return _walletCache.get(key);
|
|
107
|
+
const result = await createWallet(mnemonic);
|
|
108
|
+
_walletCache.set(key, result);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Clear the wallet derivation cache. Call after disconnect to release key material from memory. */
|
|
113
|
+
export function clearWalletCache() {
|
|
114
|
+
_walletCache.clear();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Connection Metrics (v25) ────────────────────────────────────────────────
|
|
118
|
+
// Track per-node connection stats for reliability tracking over time.
|
|
119
|
+
|
|
120
|
+
const _connectionMetrics = new Map(); // nodeAddress -> { attempts, successes, failures, avgTimeMs, lastAttempt }
|
|
121
|
+
|
|
122
|
+
export function _recordMetric(nodeAddress, success, durationMs) {
|
|
123
|
+
const entry = _connectionMetrics.get(nodeAddress) || { attempts: 0, successes: 0, failures: 0, totalTimeMs: 0, lastAttempt: 0 };
|
|
124
|
+
entry.attempts++;
|
|
125
|
+
if (success) entry.successes++; else entry.failures++;
|
|
126
|
+
entry.totalTimeMs += durationMs || 0;
|
|
127
|
+
entry.lastAttempt = Date.now();
|
|
128
|
+
_connectionMetrics.set(nodeAddress, entry);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get connection metrics for observability.
|
|
133
|
+
* @param {string} [nodeAddress] - Specific node, or omit for all.
|
|
134
|
+
* @returns {object} Per-node stats: { attempts, successes, failures, successRate, avgTimeMs, lastAttempt }
|
|
135
|
+
*/
|
|
136
|
+
export function getConnectionMetrics(nodeAddress) {
|
|
137
|
+
const format = (entry) => ({
|
|
138
|
+
...entry,
|
|
139
|
+
successRate: entry.attempts > 0 ? entry.successes / entry.attempts : 0,
|
|
140
|
+
avgTimeMs: entry.attempts > 0 ? Math.round(entry.totalTimeMs / entry.attempts) : 0,
|
|
141
|
+
});
|
|
142
|
+
if (nodeAddress) {
|
|
143
|
+
const entry = _connectionMetrics.get(nodeAddress);
|
|
144
|
+
return entry ? format(entry) : null;
|
|
145
|
+
}
|
|
146
|
+
const result = {};
|
|
147
|
+
for (const [addr, entry] of _connectionMetrics) result[addr] = format(entry);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Abort helper ────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export function checkAborted(signal) {
|
|
154
|
+
if (signal?.aborted) {
|
|
155
|
+
throw new SentinelError(ErrorCodes.ABORTED, 'Connection aborted', { reason: signal.reason });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Progress helper ─────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export function progress(cb, logFn, step, detail, meta = {}) {
|
|
162
|
+
const entry = { event: `sdk.${step}`, detail, ts: Date.now(), ...meta };
|
|
163
|
+
events.emit('progress', entry);
|
|
164
|
+
if (logFn) try { logFn(`[${step}] ${detail}`); } catch {} // user callback may throw — don't crash SDK
|
|
165
|
+
if (cb) try { cb(step, detail, entry); } catch {} // user callback may throw — don't crash SDK
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Node Inactive Retry Helper ──────────────────────────────────────────────
|
|
169
|
+
// LCD may show node as active, but chain rejects TX with code 105 ("invalid
|
|
170
|
+
// status inactive") if the node went offline between query and payment.
|
|
171
|
+
// Retry once after 15s in case LCD data was stale.
|
|
172
|
+
|
|
173
|
+
export function _isNodeInactiveError(err) {
|
|
174
|
+
const msg = String(err?.message || '');
|
|
175
|
+
const code = err?.details?.code;
|
|
176
|
+
return msg.includes('invalid status inactive') || code === 105;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function broadcastWithInactiveRetry(client, address, msgs, logFn, onProgress) {
|
|
180
|
+
try {
|
|
181
|
+
return await broadcast(client, address, msgs);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (_isNodeInactiveError(err)) {
|
|
184
|
+
progress(onProgress, logFn, 'session', 'Node reported inactive (code 105) — LCD stale data. Retrying in 15s...');
|
|
185
|
+
await sleep(15000);
|
|
186
|
+
try {
|
|
187
|
+
return await broadcast(client, address, msgs);
|
|
188
|
+
} catch (retryErr) {
|
|
189
|
+
if (_isNodeInactiveError(retryErr)) {
|
|
190
|
+
throw new NodeError(ErrorCodes.NODE_INACTIVE, 'Node went inactive between query and payment (code 105). LCD stale data confirmed after retry.', {
|
|
191
|
+
original: retryErr.message,
|
|
192
|
+
code: 105,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
throw retryErr;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Uptime Formatter ────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export function formatUptime(ms) {
|
|
205
|
+
const s = Math.floor(ms / 1000);
|
|
206
|
+
const h = Math.floor(s / 3600);
|
|
207
|
+
const m = Math.floor((s % 3600) / 60);
|
|
208
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
209
|
+
if (m > 0) return `${m}m ${s % 60}s`;
|
|
210
|
+
return `${s}s`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Session End (on-chain cleanup) ──────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* End a session on-chain. Best-effort, fire-and-forget.
|
|
217
|
+
* Prevents stale session accumulation on nodes.
|
|
218
|
+
* @param {string|bigint} sessionId - Session ID to end
|
|
219
|
+
* @param {string} mnemonic - BIP39 mnemonic for signing the TX
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
export async function _endSessionOnChain(sessionId, mnemonic) {
|
|
223
|
+
const { wallet, account } = await cachedCreateWallet(mnemonic);
|
|
224
|
+
const client = await tryWithFallback(
|
|
225
|
+
RPC_ENDPOINTS,
|
|
226
|
+
async (url) => createClient(url, wallet),
|
|
227
|
+
'RPC connect (session end)',
|
|
228
|
+
).then(r => r.result);
|
|
229
|
+
const msg = buildEndSessionMsg(account.address, sessionId);
|
|
230
|
+
const fee = { amount: [{ denom: 'udvpn', amount: '20000' }], gas: '200000' };
|
|
231
|
+
const result = await client.signAndBroadcast(account.address, [msg], fee);
|
|
232
|
+
if (result.code !== 0) {
|
|
233
|
+
console.warn(`[sentinel-sdk] End session TX failed (code ${result.code}): ${result.rawLog}`);
|
|
234
|
+
} else {
|
|
235
|
+
console.log(`[sentinel-sdk] Session ${sessionId} ended on chain (TX ${result.transactionHash})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Connection Status (VPN UX: user must always know if they're connected) ─
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if a VPN tunnel is currently active.
|
|
243
|
+
* Use this to show connected/disconnected state in UI — like the VPN icon.
|
|
244
|
+
*/
|
|
245
|
+
export function isConnected() {
|
|
246
|
+
return _defaultState.isConnected;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get current connection status. Returns null if disconnected.
|
|
251
|
+
* Apps should poll this (e.g. every 5s) to update UI — like NordVPN's status bar.
|
|
252
|
+
* v25: Includes healthChecks for tunnel/proxy liveness.
|
|
253
|
+
*/
|
|
254
|
+
export function getStatus() {
|
|
255
|
+
if (!_defaultState.connection) return null;
|
|
256
|
+
|
|
257
|
+
// v29: Cross-check tunnel liveness FIRST — if connection object exists but neither
|
|
258
|
+
// tunnel handle is truthy, the state is phantom (tunnel torn down, connection stale).
|
|
259
|
+
// This prevents IP leak where user thinks they're connected but traffic goes direct.
|
|
260
|
+
if (!_defaultState.wgTunnel && !_defaultState.v2rayProc) {
|
|
261
|
+
const stale = _defaultState.connection;
|
|
262
|
+
_defaultState.connection = null;
|
|
263
|
+
// End session on chain (fire-and-forget) to prevent stale session leaks
|
|
264
|
+
if (stale?.sessionId && _defaultState._mnemonic) {
|
|
265
|
+
_endSessionOnChain(stale.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
266
|
+
}
|
|
267
|
+
clearState();
|
|
268
|
+
events.emit('disconnected', { nodeAddress: stale.nodeAddress, serviceType: stale.serviceType, reason: 'phantom_state' });
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const conn = _defaultState.connection;
|
|
273
|
+
const uptimeMs = Date.now() - conn.connectedAt;
|
|
274
|
+
|
|
275
|
+
// v25: Health checks — distinguish tunnel states
|
|
276
|
+
const healthChecks = {
|
|
277
|
+
tunnelActive: false,
|
|
278
|
+
proxyListening: false,
|
|
279
|
+
systemProxyValid: _defaultState.systemProxy,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (_defaultState.wgTunnel) {
|
|
283
|
+
// WireGuard: check if adapter exists
|
|
284
|
+
if (process.platform === 'win32') {
|
|
285
|
+
try {
|
|
286
|
+
const out = execFileSync('netsh', ['interface', 'show', 'interface', 'name=wgsent0'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
287
|
+
healthChecks.tunnelActive = out.includes('Connected');
|
|
288
|
+
} catch {
|
|
289
|
+
// Adapter gone — tunnel is dead
|
|
290
|
+
healthChecks.tunnelActive = false;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// Non-Windows: trust state (no easy check)
|
|
294
|
+
healthChecks.tunnelActive = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (_defaultState.v2rayProc) {
|
|
299
|
+
// V2Ray: check if process is alive
|
|
300
|
+
healthChecks.tunnelActive = !_defaultState.v2rayProc.killed && _defaultState.v2rayProc.exitCode === null;
|
|
301
|
+
// Proxy listening = process alive (async port check removed — was broken, fired after return)
|
|
302
|
+
healthChecks.proxyListening = healthChecks.tunnelActive;
|
|
303
|
+
if (conn.socksPort) {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// v28: Auto-clear phantom state — if connection exists but tunnel is dead,
|
|
308
|
+
// clean up stale state. Prevents ghost "connected" status after tunnel dies.
|
|
309
|
+
if (!healthChecks.tunnelActive && !_defaultState.v2rayProc && !_defaultState.wgTunnel) {
|
|
310
|
+
// Both tunnel handles are null — connection state is stale
|
|
311
|
+
if (conn?.sessionId && _defaultState._mnemonic) {
|
|
312
|
+
_endSessionOnChain(conn.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
313
|
+
}
|
|
314
|
+
_defaultState.connection = null;
|
|
315
|
+
clearState();
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (_defaultState.wgTunnel && !healthChecks.tunnelActive) {
|
|
319
|
+
// WireGuard state says connected but tunnel is dead — auto-cleanup
|
|
320
|
+
if (conn?.sessionId && _defaultState._mnemonic) {
|
|
321
|
+
_endSessionOnChain(conn.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
322
|
+
}
|
|
323
|
+
_defaultState.wgTunnel = null;
|
|
324
|
+
_defaultState.connection = null;
|
|
325
|
+
clearState();
|
|
326
|
+
events.emit('disconnected', { nodeAddress: conn.nodeAddress, serviceType: conn.serviceType, reason: 'tunnel_died' });
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
if (_defaultState.v2rayProc && !healthChecks.tunnelActive) {
|
|
330
|
+
// V2Ray process died — auto-cleanup
|
|
331
|
+
if (conn?.sessionId && _defaultState._mnemonic) {
|
|
332
|
+
_endSessionOnChain(conn.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
|
|
333
|
+
}
|
|
334
|
+
_defaultState.v2rayProc = null;
|
|
335
|
+
_defaultState.connection = null;
|
|
336
|
+
clearState();
|
|
337
|
+
events.emit('disconnected', { nodeAddress: conn.nodeAddress, serviceType: conn.serviceType, reason: 'tunnel_died' });
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
connected: _defaultState.isConnected,
|
|
343
|
+
...conn,
|
|
344
|
+
uptimeMs,
|
|
345
|
+
uptimeFormatted: formatUptime(uptimeMs),
|
|
346
|
+
healthChecks,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Verify Connection (v26c) ────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Verify VPN is working by checking if IP has changed.
|
|
354
|
+
* Fetches public IP via ipify.org and compares to a direct (non-VPN) fetch.
|
|
355
|
+
*
|
|
356
|
+
* @param {object} [opts]
|
|
357
|
+
* @param {number} [opts.timeoutMs=8000]
|
|
358
|
+
* @returns {Promise<{ working: boolean, vpnIp: string|null, error?: string }>}
|
|
359
|
+
*/
|
|
360
|
+
export async function verifyConnection(opts = {}) {
|
|
361
|
+
const timeout = opts.timeoutMs || 8000;
|
|
362
|
+
try {
|
|
363
|
+
const res = await axios.get('https://api.ipify.org?format=json', { timeout });
|
|
364
|
+
const vpnIp = res.data?.ip || null;
|
|
365
|
+
return { working: !!vpnIp, vpnIp };
|
|
366
|
+
} catch (err) {
|
|
367
|
+
return { working: false, vpnIp: null, error: err.message };
|
|
368
|
+
}
|
|
369
|
+
}
|