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,1555 @@
|
|
|
1
|
+
# End-to-End Flow: AI Agent to Decentralized VPN
|
|
2
|
+
|
|
3
|
+
> The definitive technical reference. Every step from SDK discovery to encrypted traffic flowing through a decentralized VPN tunnel. Verified against 837+ mainnet node tests, 8 consumer apps, and 165+ hours of production debugging. Enriched with findings from Handshake dVPN (27 problems), Test2 proving ground, Node Tester (780+ nodes), and 22 undiagnosed failure patterns. Every timeout, every field name, every byte count is exact.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Phase 1: Discovery & Installation
|
|
8
|
+
|
|
9
|
+
### Package
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install sentinel-dvpn-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Package name: `sentinel-dvpn-sdk`
|
|
16
|
+
License: MIT
|
|
17
|
+
Entry point: `index.js` (ESM only -- `"type": "module"`)
|
|
18
|
+
Exports: 160+ functions from single entry point
|
|
19
|
+
|
|
20
|
+
### Node.js Requirement
|
|
21
|
+
|
|
22
|
+
**Node.js 20.0.0 or higher.** The SDK uses:
|
|
23
|
+
- ES Modules (`import`/`export`) exclusively
|
|
24
|
+
- `crypto.randomUUID()` (Node 19+)
|
|
25
|
+
- Native `fetch` adapter override via `axios.defaults.adapter = 'http'` (Node 18+ uses undici internally, which produces opaque "fetch failed" errors on self-signed certs -- the SDK forces the classic HTTP adapter)
|
|
26
|
+
|
|
27
|
+
### Postinstall: V2Ray Download
|
|
28
|
+
|
|
29
|
+
`npm install` triggers `node setup.js` which:
|
|
30
|
+
|
|
31
|
+
1. Detects platform: `${process.platform}-${process.arch}` (e.g., `win32-x64`, `linux-x64`, `darwin-arm64`)
|
|
32
|
+
2. Downloads V2Ray v5.2.1 from `https://github.com/v2fly/v2ray-core/releases/download/v5.2.1/v2ray-{platform}.zip`
|
|
33
|
+
3. Verifies SHA256 checksum against hardcoded digests:
|
|
34
|
+
- `win32-x64`: `d9791f911b603437a34219488b0111ae9913f38abe22c0103abce330537dabd6`
|
|
35
|
+
- `win32-ia32`: `dc9f37dbeb32221e62b9a52b79f1842a217f049675872b334e1e5fd96121d0d2`
|
|
36
|
+
- `linux-x64`: `56eb8d4727b058d10f8ff830bb0121381386b0695171767f38ba410f2613fc9a`
|
|
37
|
+
- `linux-arm64`: `63958429e93f24f10f34a64701f70b4f42dfa0bc8120e1c0a426c6161bd2a3c9`
|
|
38
|
+
- `darwin-x64`: `edbb0b94c05570d39a4549186927369853542649eb6b703dd432bda300c5d51a`
|
|
39
|
+
- `darwin-arm64`: `e18c17a79c4585d963395ae6ddafffb18c5d22777f7ac5938c1b40563db88d56`
|
|
40
|
+
4. Extracts to `bin/` directory: `v2ray.exe` (or `v2ray`), `geoip.dat`, `geosite.dat`
|
|
41
|
+
5. If download fails, prints warning but does not block install (V2Ray is only needed for V2Ray nodes)
|
|
42
|
+
|
|
43
|
+
**CRITICAL: V2Ray must be exactly v5.2.1.** Versions 5.44.1+ have observatory/balancer bugs that break multi-outbound configs. The SDK's `verifyDependencies()` function checks the version at connect time and refuses incompatible versions.
|
|
44
|
+
|
|
45
|
+
### WireGuard (Optional)
|
|
46
|
+
|
|
47
|
+
WireGuard is not downloaded by setup. It must be pre-installed:
|
|
48
|
+
- **Windows:** `https://download.wireguard.com/windows-client/wireguard-installer.exe` -- installs to `C:\Program Files\WireGuard\wireguard.exe`
|
|
49
|
+
- **Linux:** `apt install wireguard` or equivalent
|
|
50
|
+
- **macOS:** `brew install wireguard-tools`
|
|
51
|
+
|
|
52
|
+
WireGuard requires **Administrator/root privileges** for tunnel installation. The SDK detects admin status at module load via `IS_ADMIN` export. Approximately 30% of Sentinel nodes are WireGuard; the rest are V2Ray.
|
|
53
|
+
|
|
54
|
+
### File Structure After Install
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
node_modules/sentinel-dvpn-sdk/
|
|
58
|
+
index.js # Single entry point (160+ exports)
|
|
59
|
+
index.d.ts # TypeScript definitions
|
|
60
|
+
defaults.js # Chain IDs, endpoints, timeouts, transport rates
|
|
61
|
+
errors.js # 33 typed error codes with severity classification
|
|
62
|
+
cosmjs-setup.js # Wallet, registry, broadcast, LCD queries
|
|
63
|
+
v3protocol.js # Handshake, protobuf encoders, WireGuard/V2Ray config
|
|
64
|
+
node-connect.js # Connection orchestration (connectDirect, connectAuto, disconnect)
|
|
65
|
+
wireguard.js # Cross-platform WireGuard tunnel management
|
|
66
|
+
speedtest.js # SOCKS5 and direct speed testing
|
|
67
|
+
plan-operations.js # Plan/provider/lease message encoders
|
|
68
|
+
batch.js # Batch session operations (operator/testing)
|
|
69
|
+
preflight.js # Pre-flight system checks
|
|
70
|
+
tls-trust.js # TOFU TLS for self-signed node certificates
|
|
71
|
+
state.js # Crash recovery state persistence
|
|
72
|
+
disk-cache.js # Generic disk cache with age tracking
|
|
73
|
+
session-tracker.js # Payment mode persistence per session
|
|
74
|
+
session-manager.js # Session lifecycle management
|
|
75
|
+
app-settings.js # VPN settings persistence
|
|
76
|
+
app-types.js # App type framework (peer-to-peer, plan-based, all-in-one)
|
|
77
|
+
app-helpers.js # Country map (183 entries), pricing display, UX helpers
|
|
78
|
+
client.js # SentinelClient class (per-instance DI)
|
|
79
|
+
audit.js # Network audit and node testing
|
|
80
|
+
setup.js # Binary download script
|
|
81
|
+
bin/
|
|
82
|
+
v2ray.exe # V2Ray 5.2.1 binary
|
|
83
|
+
geoip.dat # V2Ray GeoIP database
|
|
84
|
+
geosite.dat # V2Ray GeoSite database
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Phase 2: Wallet Setup
|
|
90
|
+
|
|
91
|
+
### Key Generation
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
import { generateWallet, createWallet, privKeyFromMnemonic } from 'sentinel-dvpn-sdk';
|
|
95
|
+
|
|
96
|
+
// Generate a NEW wallet (random mnemonic)
|
|
97
|
+
const { mnemonic, wallet, account } = await generateWallet(128); // 128=12 words, 256=24 words
|
|
98
|
+
|
|
99
|
+
// Restore from existing mnemonic
|
|
100
|
+
const { wallet, account } = await createWallet('your twelve word mnemonic phrase here ...');
|
|
101
|
+
|
|
102
|
+
// Derive raw private key (needed for handshake signatures)
|
|
103
|
+
const privKey = await privKeyFromMnemonic('your twelve word mnemonic ...');
|
|
104
|
+
// privKey is a 32-byte Buffer (secp256k1 private key)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Cryptographic Details
|
|
108
|
+
|
|
109
|
+
| Property | Value |
|
|
110
|
+
|----------|-------|
|
|
111
|
+
| Mnemonic standard | BIP39, English wordlist |
|
|
112
|
+
| Word count | 12 (128-bit entropy) or 24 (256-bit entropy) |
|
|
113
|
+
| Entropy source | `@cosmjs/crypto Random.getBytes()` (CSPRNG) |
|
|
114
|
+
| Seed derivation | BIP39 mnemonic-to-seed (PBKDF2, 2048 rounds) |
|
|
115
|
+
| HD derivation | SLIP-10 (not BIP32) with curve `Secp256k1` |
|
|
116
|
+
| HD path | `m/44'/118'/0'/0/0` (Cosmos standard, `makeCosmoshubPath(0)`) |
|
|
117
|
+
| Key type | secp256k1 (same as Bitcoin, Ethereum, Cosmos) |
|
|
118
|
+
| Address format | Bech32 with prefix `sent` (e.g., `sent1abc...xyz`) |
|
|
119
|
+
| Address length | 47 characters total (5 prefix + 1 separator + 38 data + 6 checksum) |
|
|
120
|
+
| Node address format | Bech32 with prefix `sentnode` (e.g., `sentnode1abc...xyz`) |
|
|
121
|
+
| Provider address format | Bech32 with prefix `sentprov` |
|
|
122
|
+
|
|
123
|
+
### Security Requirements
|
|
124
|
+
|
|
125
|
+
- **Zero private key after use:** `privKey.fill(0)` -- the SDK does this automatically in `connectInternal()` via `try/finally`
|
|
126
|
+
- **NEVER log or store mnemonic in source code** -- use environment variables or secure storage
|
|
127
|
+
- **Wallet cache:** The SDK caches wallet derivation by SHA256(mnemonic) to avoid repeated 300ms BIP39 derivation. Call `clearWalletCache()` after disconnect to release key material from memory.
|
|
128
|
+
|
|
129
|
+
### Address Conversion
|
|
130
|
+
|
|
131
|
+
Same key, different Bech32 prefix. The SDK provides conversion functions:
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
import { sentToSentnode, sentToSentprov, sentprovToSent } from 'sentinel-dvpn-sdk';
|
|
135
|
+
|
|
136
|
+
sentToSentnode('sent1abc...') // -> 'sentnode1abc...'
|
|
137
|
+
sentToSentprov('sent1abc...') // -> 'sentprov1abc...'
|
|
138
|
+
sentprovToSent('sentprov1abc...') // -> 'sent1abc...'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Phase 3: Funding
|
|
144
|
+
|
|
145
|
+
### Token
|
|
146
|
+
|
|
147
|
+
| Property | Value |
|
|
148
|
+
|----------|-------|
|
|
149
|
+
| Display name | **P2P** (NOT "DVPN" in user-facing text) |
|
|
150
|
+
| Chain denom | `udvpn` (micro-DVPN, 1 P2P = 1,000,000 udvpn) |
|
|
151
|
+
| Format function | `formatP2P(amount)` returns "X.XX P2P" |
|
|
152
|
+
| Chain ID | `sentinelhub-2` |
|
|
153
|
+
| Gas price | `0.2udvpn` (chain minimum) |
|
|
154
|
+
|
|
155
|
+
### CRITICAL: BaseValue vs QuoteValue Pricing
|
|
156
|
+
|
|
157
|
+
*(Source: Handshake dVPN -- 3 hours wasted debugging 18-decimal garbage)*
|
|
158
|
+
|
|
159
|
+
Node prices come with TWO values. Use the WRONG one and your UI shows `52573.099722991367791000000000/GB`:
|
|
160
|
+
|
|
161
|
+
| Field | Format | Use? | Example |
|
|
162
|
+
|-------|--------|------|---------|
|
|
163
|
+
| `base_value` | Cosmos `sdk.Dec` (scaled by 10^18) | **NEVER for display** | `"5500000000000000000000000"` |
|
|
164
|
+
| `quote_value` | Integer (`sdk.Int`) in `udvpn` | **ALWAYS for display** | `"40152030"` |
|
|
165
|
+
|
|
166
|
+
**Rule:** Always use `quote_value` for display pricing. Use `base_value` only when constructing the `max_price` protobuf field for `MsgStartSessionRequest`.
|
|
167
|
+
|
|
168
|
+
**Price formatting function:**
|
|
169
|
+
```javascript
|
|
170
|
+
function formatP2PPrice(udvpnStr) {
|
|
171
|
+
const p2p = parseInt(udvpnStr) / 1_000_000;
|
|
172
|
+
if (p2p >= 100) return `${Math.round(p2p)} P2P`;
|
|
173
|
+
if (p2p >= 10) return `${p2p.toFixed(1).replace(/\.0$/, '')} P2P`;
|
|
174
|
+
if (p2p >= 1) return `${p2p.toFixed(2).replace(/0$/, '').replace(/\.$/, '')} P2P`;
|
|
175
|
+
return `${p2p.toFixed(4)} P2P`;
|
|
176
|
+
}
|
|
177
|
+
// formatP2PPrice("40152030") -> "40.15 P2P"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Minimum Balance
|
|
181
|
+
|
|
182
|
+
| Item | Cost |
|
|
183
|
+
|------|------|
|
|
184
|
+
| Minimum for any operation | 1.0 P2P (1,000,000 udvpn) — covers gas + cheapest node |
|
|
185
|
+
| 1 GB (cheapest, varies) | ~0.68 P2P — node operators set their own prices |
|
|
186
|
+
| 1 GB (median, varies) | ~40 P2P — use `estimateCost()` for live pricing |
|
|
187
|
+
| Gas per transaction | ~0.04 P2P (40,000 udvpn) |
|
|
188
|
+
| End session TX | Fixed 0.02 P2P gas (20,000 udvpn, 200,000 gas) |
|
|
189
|
+
|
|
190
|
+
The SDK checks balance before session payment:
|
|
191
|
+
```javascript
|
|
192
|
+
if (bal.udvpn < 1000000) {
|
|
193
|
+
throw new ChainError('INSUFFICIENT_BALANCE',
|
|
194
|
+
`Wallet has ${bal.p2p} P2P -- need at least 1.0 P2P`);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Where to Buy P2P Tokens
|
|
199
|
+
|
|
200
|
+
- **Osmosis DEX:** Swap USDT/USDC/ATOM to P2P
|
|
201
|
+
- **CEX listings:** Check CoinGecko for current exchanges
|
|
202
|
+
- Fund the `sent1...` address shown in `account.address`
|
|
203
|
+
|
|
204
|
+
### Balance Check
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
import { getBalance, createWallet, createClient, DEFAULT_RPC } from 'sentinel-dvpn-sdk';
|
|
208
|
+
|
|
209
|
+
const { wallet, account } = await createWallet(mnemonic);
|
|
210
|
+
const client = await createClient(DEFAULT_RPC, wallet);
|
|
211
|
+
const { udvpn, dvpn } = await getBalance(client, account.address);
|
|
212
|
+
// udvpn = raw micro amount (integer), dvpn = human-readable (float)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**LCD endpoint for direct query:**
|
|
216
|
+
```
|
|
217
|
+
GET /cosmos/bank/v1beta1/balances/{sent1...}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### LCD Failover Chain
|
|
221
|
+
|
|
222
|
+
The SDK tries these endpoints in order. On failure, it automatically falls back to the next:
|
|
223
|
+
|
|
224
|
+
| Priority | URL | Name |
|
|
225
|
+
|----------|-----|------|
|
|
226
|
+
| 1 | `https://lcd.sentinel.co` | Sentinel Official |
|
|
227
|
+
| 2 | `https://sentinel-api.polkachu.com` | Polkachu |
|
|
228
|
+
| 3 | `https://api.sentinel.quokkastake.io` | QuokkaStake |
|
|
229
|
+
| 4 | `https://sentinel-rest.publicnode.com` | PublicNode |
|
|
230
|
+
|
|
231
|
+
**RPC Failover Chain** (for TX broadcast):
|
|
232
|
+
|
|
233
|
+
| Priority | URL | Name |
|
|
234
|
+
|----------|-----|------|
|
|
235
|
+
| 1 | `https://rpc.sentinel.co:443` | Sentinel Official |
|
|
236
|
+
| 2 | `https://sentinel-rpc.polkachu.com` | Polkachu |
|
|
237
|
+
| 3 | `https://rpc.mathnodes.com` | MathNodes |
|
|
238
|
+
| 4 | `https://sentinel-rpc.publicnode.com` | PublicNode |
|
|
239
|
+
| 5 | `https://rpc.sentinel.quokkastake.io` | QuokkaStake |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Phase 4: Node Discovery
|
|
244
|
+
|
|
245
|
+
### Fetching Active Nodes
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
import { queryOnlineNodes, fetchAllNodes } from 'sentinel-dvpn-sdk';
|
|
249
|
+
|
|
250
|
+
// Fast: LCD-only, no per-node checks (900+ nodes, instant)
|
|
251
|
+
const allNodes = await fetchAllNodes();
|
|
252
|
+
|
|
253
|
+
// Thorough: checks each node's online status + quality scoring
|
|
254
|
+
const onlineNodes = await queryOnlineNodes({
|
|
255
|
+
serviceType: 'v2ray', // 'wireguard' | 'v2ray' | null (both)
|
|
256
|
+
maxNodes: 100, // max nodes to probe
|
|
257
|
+
concurrency: 20, // parallel online checks
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### LCD Endpoint for Active Nodes
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
GET /sentinel/node/v3/nodes?status=1&pagination.limit=5000
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### CRITICAL v3 Rules
|
|
268
|
+
|
|
269
|
+
| Rule | Detail |
|
|
270
|
+
|------|--------|
|
|
271
|
+
| **Use v3 paths, NOT v2** | v2 returns "Not Implemented" for ALL endpoints except provider |
|
|
272
|
+
| **Provider is v2 ONLY** | `/sentinel/provider/v2/providers/{sentprov1...}` -- NOT v3 |
|
|
273
|
+
| **Status filter** | `status=1` (integer), NOT `status=STATUS_ACTIVE` (string) |
|
|
274
|
+
| **Node type field** | `service_type` (NOT `type`) -- values: `1` (V2Ray) or `2` (WireGuard) |
|
|
275
|
+
| **Remote URL field** | `remote_addrs` (array of `"IP:PORT"` strings), NOT `remote_url` (string) |
|
|
276
|
+
| **Account address** | `acc_address` (NOT `address`) |
|
|
277
|
+
| **Session nesting** | Session data is under `base_session` -- always use `session.base_session || session` |
|
|
278
|
+
|
|
279
|
+
### CRITICAL: Pagination is Broken
|
|
280
|
+
|
|
281
|
+
*(Source: Test2 confirmed, Node Tester confirmed with 921+ node scans)*
|
|
282
|
+
|
|
283
|
+
**NEVER trust `count_total` or `next_key`** on Sentinel LCD endpoints:
|
|
284
|
+
- Some endpoints return `min(actual_count, limit)` as `count_total`
|
|
285
|
+
- Some endpoints return `null` for `next_key` even when more data exists
|
|
286
|
+
- Test2 verified: `count_total` returned 500 when 921 nodes existed
|
|
287
|
+
- **Solution:** Single request with `limit=5000` -- returns all data in one call
|
|
288
|
+
|
|
289
|
+
### Tiered Caching Pattern
|
|
290
|
+
|
|
291
|
+
*(Source: Handshake dVPN -- bandwidth optimization for consumer apps)*
|
|
292
|
+
|
|
293
|
+
Node data should be cached at multiple tiers to minimize chain queries:
|
|
294
|
+
|
|
295
|
+
| Data | Show From | Refresh When | TTL |
|
|
296
|
+
|------|-----------|-------------|-----|
|
|
297
|
+
| Node list | Disk cache on login | Background after login | 30 min |
|
|
298
|
+
| Node status (peers, location) | Memory from last probe | On user Refresh click | Per session |
|
|
299
|
+
| Balance | Last known value | Every 5 min | -- |
|
|
300
|
+
| Session allocation | Chain query | Every 120s when connected | -- |
|
|
301
|
+
| Country flags | Disk permanent | Never | Forever |
|
|
302
|
+
| Settings | Disk permanent | On save | Forever |
|
|
303
|
+
|
|
304
|
+
**Key rules from Handshake dVPN:**
|
|
305
|
+
- Load on app open -- probing starts immediately, before login
|
|
306
|
+
- Single probe per session -- cache in memory, reuse across login/logout
|
|
307
|
+
- Only re-probe on explicit Refresh button -- user controls when to re-query
|
|
308
|
+
- Progress logged every 200 nodes, not every 100 (reduces log spam)
|
|
309
|
+
- 30 parallel workers, 6s timeout each, ~25s for 1000+ nodes
|
|
310
|
+
- ChainClient usable without wallet (for node loading before login)
|
|
311
|
+
|
|
312
|
+
### Node Object Shape (from LCD)
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"address": "sentnode1...",
|
|
317
|
+
"remote_addrs": ["1.2.3.4:8585"],
|
|
318
|
+
"gigabyte_prices": [
|
|
319
|
+
{ "denom": "udvpn", "base_value": "5500000", "quote_value": "40152030" }
|
|
320
|
+
],
|
|
321
|
+
"hourly_prices": [
|
|
322
|
+
{ "denom": "udvpn", "base_value": "1000000", "quote_value": "0" }
|
|
323
|
+
],
|
|
324
|
+
"service_type": 1
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Resolving Node URL
|
|
329
|
+
|
|
330
|
+
LCD returns `remote_addrs` as an array of bare `"IP:PORT"` strings (no protocol prefix). The SDK's `resolveNodeUrl()` handles both v2 and v3 formats:
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
import { resolveNodeUrl } from 'sentinel-dvpn-sdk';
|
|
334
|
+
const url = resolveNodeUrl(node); // Returns "https://1.2.3.4:8585"
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**NEVER access `node.remote_url` directly** -- it is undefined in v3. Always use `resolveNodeUrl()`.
|
|
338
|
+
|
|
339
|
+
### Country Data
|
|
340
|
+
|
|
341
|
+
Country/city information is NOT on the LCD. It requires querying each node's own status API:
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
import { nodeStatusV3 } from 'sentinel-dvpn-sdk';
|
|
345
|
+
const status = await nodeStatusV3('https://1.2.3.4:8585');
|
|
346
|
+
// status.location = { city, country, country_code, latitude, longitude }
|
|
347
|
+
// status.moniker = "MyNode"
|
|
348
|
+
// status.type = "wireguard" | "v2ray"
|
|
349
|
+
// status.peers = 3
|
|
350
|
+
// status.clockDriftSec = -2 (seconds, negative = node behind)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Use `enrichNodes()` to batch-probe all nodes for country data, or `buildNodeIndex()` to create a geographic lookup.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Phase 5: Node Selection
|
|
358
|
+
|
|
359
|
+
### Service Type Success Rates (from 780-node mainnet scan)
|
|
360
|
+
|
|
361
|
+
| Service Type | Success Rate | Admin Required | Notes |
|
|
362
|
+
|-------------|-------------|----------------|-------|
|
|
363
|
+
| WireGuard | ~100% (when reachable) | Yes | Simpler protocol, fewer failure modes |
|
|
364
|
+
| V2Ray | ~95.6% | No | Multiple transport fallback options |
|
|
365
|
+
|
|
366
|
+
### V2Ray Transport Reliability (from 780-node scan)
|
|
367
|
+
|
|
368
|
+
| Transport | Success Rate | Sample | Notes |
|
|
369
|
+
|-----------|-------------|--------|-------|
|
|
370
|
+
| `tcp` | 100% | 274 | **Best. Always first choice.** |
|
|
371
|
+
| `websocket` | 100% | 23 | Second choice |
|
|
372
|
+
| `http` | 100% | 4 | Third choice |
|
|
373
|
+
| `gun` | 100% | 10 | gun(2) and grpc(3) are DIFFERENT enum values but use same V2Ray config |
|
|
374
|
+
| `mkcp` | 100% | 5 | |
|
|
375
|
+
| `grpc/none` | 87% | 81 | Fixed by serverName TLS fix |
|
|
376
|
+
| `quic` | 0% | 4 | Fixed (security: 'none'), but low node count |
|
|
377
|
+
| `grpc/tls` | **0%** | 0 | **ALWAYS FAILS. Filter before paying.** |
|
|
378
|
+
|
|
379
|
+
### Undiagnosed Failure Categories
|
|
380
|
+
|
|
381
|
+
*(Source: Node Tester -- 22 nodes with active peers that fail for us)*
|
|
382
|
+
|
|
383
|
+
Even after all fixes, ~2.8% of nodes with active peers still fail. These are OUR bugs, not node-side issues. Categories:
|
|
384
|
+
|
|
385
|
+
| Category | Count | Symptom | Likely Cause (Our Side) |
|
|
386
|
+
|----------|-------|---------|------------------------|
|
|
387
|
+
| TCP Port Unreachable | 10 | Pre-check says port closed, but 3-7 peers connected | Probe timeout too short, DNS resolution differs, ISP blocking from our IP |
|
|
388
|
+
| SOCKS5 No Connectivity | 5 | Handshake OK, V2Ray starts, SOCKS5 binds, no internet | V2Ray 5.2.1 grpc/quic bugs, egress policy blocks our test targets |
|
|
389
|
+
| Clock Drift VMess Skip | 4 | >120s drift detected, we skip, but 4-6 peers work fine | Our measurement may be inaccurate, node may have VLess (unaffected) we fail to detect |
|
|
390
|
+
| V2 Format Metadata | 1 | 48 peers, but metadata has v2 fields not v3 | Dual-format node, we should support v2 fallback |
|
|
391
|
+
| Handshake Failures | 2 | ECONNRESET or 30s timeout with 3-4 peers | TLS cipher mismatch, timeout too short for distant nodes |
|
|
392
|
+
|
|
393
|
+
**Iron rule:** Any node with peers > 0 that fails = our bug. NEVER say "node-side" or "can't fix."
|
|
394
|
+
|
|
395
|
+
### Quality Scoring
|
|
396
|
+
|
|
397
|
+
The SDK scores nodes 0-100 based on:
|
|
398
|
+
- **+20** for WireGuard type
|
|
399
|
+
- **-40** for clock drift >120s (VMess AEAD failure zone)
|
|
400
|
+
- **-15** for clock drift >60s
|
|
401
|
+
- **-5** for clock drift >30s
|
|
402
|
+
- **+10** for 0 peers (empty node = fast)
|
|
403
|
+
- **+5** for <5 peers
|
|
404
|
+
- **-10** for >20 peers
|
|
405
|
+
|
|
406
|
+
### Circuit Breaker
|
|
407
|
+
|
|
408
|
+
The SDK tracks node failures and skips nodes that fail repeatedly:
|
|
409
|
+
- Default: 3 failures within 5 minutes = circuit open (node skipped)
|
|
410
|
+
- Configurable via `configureCircuitBreaker({ threshold: 3, ttlMs: 300000 })`
|
|
411
|
+
- Auto-resets after TTL expires
|
|
412
|
+
- Cleared on successful connection
|
|
413
|
+
|
|
414
|
+
### Pre-Selection Checks
|
|
415
|
+
|
|
416
|
+
Before paying for a session, verify:
|
|
417
|
+
1. Node accepts `udvpn` denom: `node.gigabyte_prices.some(p => p.denom === 'udvpn')`
|
|
418
|
+
2. Node has reachable remote URL: `resolveNodeUrl(node)` does not return null
|
|
419
|
+
3. Node is not in the circuit breaker
|
|
420
|
+
4. Node address matches remote URL (prevents address mismatch -- check `nodeStatusV3().address`)
|
|
421
|
+
5. Clock drift <120s for V2Ray nodes (VMess AEAD tolerance)
|
|
422
|
+
6. If WireGuard: admin privileges available (`IS_ADMIN === true`)
|
|
423
|
+
7. If V2Ray: `v2ray.exe` v5.2.1 exists
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Phase 6: Session Creation (Blockchain TX)
|
|
428
|
+
|
|
429
|
+
### Message Type
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
/sentinel.node.v3.MsgStartSessionRequest
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Protobuf Fields
|
|
436
|
+
|
|
437
|
+
| Field # | Name | Type | Description |
|
|
438
|
+
|---------|------|------|-------------|
|
|
439
|
+
| 1 | `from` | string | Signer's `sent1...` address |
|
|
440
|
+
| 2 | `node_address` | string | Target `sentnode1...` address |
|
|
441
|
+
| 3 | `gigabytes` | int64 | Bandwidth to purchase (0 if hourly) |
|
|
442
|
+
| 4 | `hours` | int64 | Duration to purchase (0 if per-GB) |
|
|
443
|
+
| 5 | `max_price` | Price | Maximum price user will pay (from node's gigabyte_prices or hourly_prices) |
|
|
444
|
+
|
|
445
|
+
**Price sub-message:**
|
|
446
|
+
| Field # | Name | Type | Description |
|
|
447
|
+
|---------|------|------|-------------|
|
|
448
|
+
| 1 | `denom` | string | `"udvpn"` |
|
|
449
|
+
| 2 | `base_value` | string | sdk.Dec scaled by 10^18 (e.g., `"5500000"` from LCD) |
|
|
450
|
+
| 3 | `quote_value` | string | sdk.Int (e.g., `"40152030"` from LCD) |
|
|
451
|
+
|
|
452
|
+
### SDK Usage
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
import { connectDirect, registerCleanupHandlers } from 'sentinel-dvpn-sdk';
|
|
456
|
+
|
|
457
|
+
// REQUIRED: Register cleanup handlers BEFORE any connection
|
|
458
|
+
registerCleanupHandlers();
|
|
459
|
+
|
|
460
|
+
const result = await connectDirect({
|
|
461
|
+
mnemonic: process.env.MNEMONIC,
|
|
462
|
+
nodeAddress: 'sentnode1...',
|
|
463
|
+
gigabytes: 1,
|
|
464
|
+
// Optional:
|
|
465
|
+
rpcUrl: 'https://rpc.sentinel.co:443',
|
|
466
|
+
lcdUrl: 'https://lcd.sentinel.co',
|
|
467
|
+
v2rayExePath: './bin/v2ray.exe',
|
|
468
|
+
fullTunnel: true, // Route ALL traffic through VPN
|
|
469
|
+
systemProxy: false, // Set Windows system SOCKS proxy
|
|
470
|
+
killSwitch: false, // Block all traffic if tunnel drops
|
|
471
|
+
onProgress: (step, detail) => console.log(`[${step}] ${detail}`),
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### CRITICAL: ForceNewSession for Consumer Apps
|
|
476
|
+
|
|
477
|
+
*(Source: Handshake dVPN -- stale session 404 errors)*
|
|
478
|
+
|
|
479
|
+
Consumer apps should use `forceNewSession: true` to avoid stale session 404 errors. Without it, the SDK attempts to reuse existing sessions which may have expired or become corrupted on the node side, producing confusing 404 "session does not exist" errors.
|
|
480
|
+
|
|
481
|
+
```javascript
|
|
482
|
+
const result = await connectDirect({
|
|
483
|
+
mnemonic: process.env.MNEMONIC,
|
|
484
|
+
nodeAddress: 'sentnode1...',
|
|
485
|
+
gigabytes: 1,
|
|
486
|
+
forceNewSession: true, // RECOMMENDED for consumer apps
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Exception:** For node testing tools that test hundreds of nodes, session reuse saves tokens. Only use `forceNewSession: false` when you have logic to validate the existing session is still active.
|
|
491
|
+
|
|
492
|
+
### CRITICAL: Fee Grant Detection
|
|
493
|
+
|
|
494
|
+
*(Source: Handshake dVPN -- wrong fee grant auto-detection for direct-connect apps)*
|
|
495
|
+
|
|
496
|
+
The SDK's fee grant auto-detection assumes plan-based subscriptions (where the plan owner pays gas). For direct-connect (P2P) apps where the user pays their own gas, fee grant detection must be **disabled entirely**. Otherwise the SDK incorrectly tries to use a non-existent fee grant and the TX fails.
|
|
497
|
+
|
|
498
|
+
- **Plan-based apps:** Fee grant auto-detection is correct (plan owner grants gas to subscribers)
|
|
499
|
+
- **Direct-connect apps:** Set `feeGrant: false` or remove fee grant logic entirely
|
|
500
|
+
- **All-in-one apps:** Detect payment mode and toggle accordingly
|
|
501
|
+
|
|
502
|
+
### Payment Is Locked Upfront
|
|
503
|
+
|
|
504
|
+
Tokens are escrowed on session creation. They are NOT refundable if the connection fails after payment. This is why pre-verification (Phase 5) is critical.
|
|
505
|
+
|
|
506
|
+
### Sequence Retry Logic
|
|
507
|
+
|
|
508
|
+
The SDK uses `createSafeBroadcaster()` for production apps, which handles:
|
|
509
|
+
- **Error code 32 (sequence mismatch):** Up to 5 retry attempts with exponential backoff (2s, 4s, 6s, 6s)
|
|
510
|
+
- Each retry creates a fresh SigningStargateClient (fresh sequence number from RPC)
|
|
511
|
+
- Broadcasts are serialized through a mutex (one TX at a time)
|
|
512
|
+
|
|
513
|
+
### Code 105 Retry (Node Inactive)
|
|
514
|
+
|
|
515
|
+
If the chain returns code 105 ("invalid status inactive"), it means the node went offline between the LCD query and the payment TX. The SDK:
|
|
516
|
+
1. Waits 15 seconds (LCD data may be stale)
|
|
517
|
+
2. Retries the broadcast once
|
|
518
|
+
3. If still code 105, throws `NodeError('NODE_INACTIVE')`
|
|
519
|
+
|
|
520
|
+
### Session ID Extraction
|
|
521
|
+
|
|
522
|
+
After successful broadcast, the session ID is extracted from TX ABCI events:
|
|
523
|
+
```javascript
|
|
524
|
+
const sessionId = extractId(txResult, /session/i, ['session_id', 'id']);
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Event attributes may be base64-encoded (depends on CosmJS version). The SDK handles both string and base64-encoded keys/values. The extracted ID is a string representation of a uint64.
|
|
528
|
+
|
|
529
|
+
**CRITICAL:** `sessionId` is a BigInt internally. Convert to string before JSON serialization: `sessionId.toString()`. `JSON.stringify({ sessionId: 123n })` throws `TypeError`.
|
|
530
|
+
|
|
531
|
+
### Inter-TX Spacing
|
|
532
|
+
|
|
533
|
+
**Minimum 7 seconds between transactions.** Rapid TX submission causes sequence mismatch cascades. NEVER run parallel chain operations -- rate limits will kill your internet connectivity.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Phase 7: Index Wait
|
|
538
|
+
|
|
539
|
+
After the session TX confirms on-chain, the node needs time to see the block and index the session.
|
|
540
|
+
|
|
541
|
+
### Wait Sequence
|
|
542
|
+
|
|
543
|
+
| Step | Delay | Purpose |
|
|
544
|
+
|------|-------|---------|
|
|
545
|
+
| Post-payment wait | **5 seconds** | Node indexes session from new block |
|
|
546
|
+
| Chain lag retry (handshake 404) | **10 seconds** | Node still processing; retry once |
|
|
547
|
+
| Already-exists retry 1 (409) | **15 seconds** | Session indexing race condition |
|
|
548
|
+
| Already-exists retry 2 (409) | **20 seconds** | Final attempt before fresh payment |
|
|
549
|
+
|
|
550
|
+
### What Happens Without the Wait
|
|
551
|
+
|
|
552
|
+
Without the 5-second post-payment delay:
|
|
553
|
+
- **404 "session does not exist"** -- node hasn't seen the block yet
|
|
554
|
+
- **409 "already exists"** -- node is still indexing the previous state
|
|
555
|
+
|
|
556
|
+
The SDK handles both automatically with retries, but the initial 5s delay prevents most failures.
|
|
557
|
+
|
|
558
|
+
### Inactive Pending Status
|
|
559
|
+
|
|
560
|
+
After TX confirms, the session may be in `inactive_pending` status. The SDK's `waitForSessionActive()` polls every 2 seconds for up to 20 seconds until the status transitions to `active`.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Phase 8: V3 Handshake
|
|
565
|
+
|
|
566
|
+
### Protocol
|
|
567
|
+
|
|
568
|
+
Single HTTPS POST to the node's remote URL (from `remote_addrs`).
|
|
569
|
+
|
|
570
|
+
### Endpoint
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
POST https://{node_ip}:{node_port}/
|
|
574
|
+
Content-Type: application/json
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### TLS
|
|
578
|
+
|
|
579
|
+
All Sentinel nodes use **self-signed certificates**. The SDK uses a TOFU (Trust-On-First-Use) model:
|
|
580
|
+
- First connection: accept any certificate, save fingerprint
|
|
581
|
+
- Subsequent connections: reject if certificate fingerprint changed (possible MITM)
|
|
582
|
+
- Configurable via `tlsTrust: 'tofu'` (default) or `'none'` (insecure)
|
|
583
|
+
|
|
584
|
+
### Request Timeout
|
|
585
|
+
|
|
586
|
+
**90 seconds.** Overloaded nodes can take 60-90s to respond.
|
|
587
|
+
|
|
588
|
+
### Request Body
|
|
589
|
+
|
|
590
|
+
```json
|
|
591
|
+
{
|
|
592
|
+
"data": "<base64_encoded_peer_data>",
|
|
593
|
+
"id": 12345,
|
|
594
|
+
"pub_key": "secp256k1:<base64_compressed_pubkey>",
|
|
595
|
+
"signature": "<base64_signature>"
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Field Construction Details
|
|
600
|
+
|
|
601
|
+
**`data` field (base64-encoded JSON):**
|
|
602
|
+
|
|
603
|
+
For WireGuard:
|
|
604
|
+
```json
|
|
605
|
+
{"public_key": "<base64_x25519_public_key>"}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
For V2Ray:
|
|
609
|
+
```json
|
|
610
|
+
{"uuid": [byte, byte, byte, ...16 bytes]}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The `data` field is `base64(JSON.stringify(peer_request))`.
|
|
614
|
+
|
|
615
|
+
**`id` field:**
|
|
616
|
+
- Session ID as a JavaScript Number (NOT BigInt)
|
|
617
|
+
- CRITICAL: Must pass `Number.isSafeInteger()` check (max 2^53 - 1 = 9007199254740991)
|
|
618
|
+
- The SDK throws if sessionId exceeds safe integer range
|
|
619
|
+
|
|
620
|
+
**`pub_key` field:**
|
|
621
|
+
- Format: `"secp256k1:" + base64(compressed_secp256k1_pubkey)`
|
|
622
|
+
- Compressed public key is 33 bytes (prefix 0x02 or 0x03 + 32 bytes X coordinate)
|
|
623
|
+
|
|
624
|
+
**`signature` field:**
|
|
625
|
+
|
|
626
|
+
```
|
|
627
|
+
signature = base64(secp256k1_sign(SHA256(message))[0:64])
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Where `message` is the concatenation of:
|
|
631
|
+
1. **BigEndian uint64 (8 bytes)** of the session ID
|
|
632
|
+
2. **Raw bytes** of the `data` field (the base64-decoded peer JSON bytes)
|
|
633
|
+
|
|
634
|
+
**CRITICAL: Sign the RAW JSON bytes, NOT the base64 string.**
|
|
635
|
+
|
|
636
|
+
The signature is:
|
|
637
|
+
- SHA256 hash of the concatenated message
|
|
638
|
+
- secp256k1 signature (deterministic, RFC 6979)
|
|
639
|
+
- **Exactly 64 bytes** (r + s, NO recovery byte) -- Go's `VerifySignature` requires `len == 64`
|
|
640
|
+
- The SDK takes the first 64 bytes of the 65-byte `toFixedLength()` output
|
|
641
|
+
|
|
642
|
+
### WireGuard Key Generation
|
|
643
|
+
|
|
644
|
+
```javascript
|
|
645
|
+
import { generateWgKeyPair } from 'sentinel-dvpn-sdk';
|
|
646
|
+
const { privateKey, publicKey } = generateWgKeyPair();
|
|
647
|
+
// privateKey: Buffer(32), publicKey: Buffer(32)
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Key generation:
|
|
651
|
+
1. Generate 32 random bytes (CSPRNG)
|
|
652
|
+
2. Apply WireGuard bit clamping: `priv[0] &= 248; priv[31] &= 127; priv[31] |= 64;`
|
|
653
|
+
3. Derive public key via X25519 scalar base multiplication
|
|
654
|
+
|
|
655
|
+
### V2Ray UUID Generation
|
|
656
|
+
|
|
657
|
+
```javascript
|
|
658
|
+
import { generateV2RayUUID } from 'sentinel-dvpn-sdk';
|
|
659
|
+
const uuid = generateV2RayUUID(); // crypto.randomUUID(), e.g., "550e8400-e29b-41d4-a716-446655440000"
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
The UUID is sent to the node as an integer byte array (16 bytes) in the peer request.
|
|
663
|
+
|
|
664
|
+
### Response (WireGuard)
|
|
665
|
+
|
|
666
|
+
The response `result.data` is base64-encoded JSON containing:
|
|
667
|
+
```json
|
|
668
|
+
{
|
|
669
|
+
"addrs": ["10.8.0.2/24"],
|
|
670
|
+
"metadata": [{ "port": 51820, "public_key": "<base64_server_wg_pubkey>" }]
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
- `result.addrs` = node's WireGuard listening addresses (`["IP:PORT", ...]`)
|
|
675
|
+
- `addPeerResp.addrs` = our assigned IPs (e.g., `["10.8.0.2/24"]`)
|
|
676
|
+
- `addPeerResp.metadata[0].public_key` = server's WireGuard public key
|
|
677
|
+
- `addPeerResp.metadata[0].port` = server's WireGuard port (default 51820)
|
|
678
|
+
|
|
679
|
+
### Response (V2Ray)
|
|
680
|
+
|
|
681
|
+
The response `result.data` is base64-encoded JSON containing V2Ray outbound configuration with metadata entries. Each metadata entry describes one transport option:
|
|
682
|
+
|
|
683
|
+
```json
|
|
684
|
+
{
|
|
685
|
+
"metadata": [
|
|
686
|
+
{
|
|
687
|
+
"proxy_protocol": 1,
|
|
688
|
+
"transport_protocol": 7,
|
|
689
|
+
"transport_security": 0,
|
|
690
|
+
"port": 443
|
|
691
|
+
}
|
|
692
|
+
]
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
- `proxy_protocol`: 1=VLess, 2=VMess
|
|
697
|
+
- `transport_protocol`: 1=domainsocket, 2=gun, 3=grpc, 4=http, 5=mkcp, 6=quic, 7=tcp, 8=websocket
|
|
698
|
+
- `transport_security`: 0=none, 2=tls
|
|
699
|
+
- `port`: server port for this transport
|
|
700
|
+
|
|
701
|
+
### Handshake Error Handling
|
|
702
|
+
|
|
703
|
+
| HTTP Status | Body Pattern | Meaning | Action |
|
|
704
|
+
|-------------|-------------|---------|--------|
|
|
705
|
+
| 404 | "does not exist" | Chain lag -- node hasn't indexed session | Wait 10s, retry once |
|
|
706
|
+
| 409 | "already exists" | Session indexing race | Wait 15s/20s, retry; then pay fresh |
|
|
707
|
+
| 500 | "no such table" / "database is locked" | Corrupted node database | Throw `NODE_DATABASE_CORRUPT`, skip node |
|
|
708
|
+
| Other | Any | Node-level failure | Throw `NODE_OFFLINE` |
|
|
709
|
+
|
|
710
|
+
### Response Validation
|
|
711
|
+
|
|
712
|
+
The SDK validates every handshake response field:
|
|
713
|
+
- Server public key must be non-empty
|
|
714
|
+
- Server port must be 1-65535
|
|
715
|
+
- Assigned addresses must be valid CIDR (IPv4 or IPv6)
|
|
716
|
+
- At least one assigned address must be returned
|
|
717
|
+
- At least one server endpoint address must be returned
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Phase 9: Tunnel Installation
|
|
722
|
+
|
|
723
|
+
### CRITICAL: WireGuard Pre-Cleanup Before Every Connect
|
|
724
|
+
|
|
725
|
+
*(Source: Handshake dVPN -- tunnel orphan crashes)*
|
|
726
|
+
|
|
727
|
+
**Always uninstall stale WireGuard tunnels BEFORE attempting a new connection.** A leftover `wgsent0` tunnel from a crash, force-quit, or previous session will cause the new install to fail silently.
|
|
728
|
+
|
|
729
|
+
```
|
|
730
|
+
# Windows: run before every new connection
|
|
731
|
+
wireguard.exe /uninstalltunnelservice wgsent0
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
The SDK's `registerCleanupHandlers()` does this at startup via `emergencyCleanupSync()`, but consumer apps with dedicated test VPN instances must also clean up independently. For node testing scenarios, clean up BEFORE and AFTER each test node.
|
|
735
|
+
|
|
736
|
+
### WireGuard Path
|
|
737
|
+
|
|
738
|
+
#### Config Generation
|
|
739
|
+
|
|
740
|
+
```ini
|
|
741
|
+
[Interface]
|
|
742
|
+
PrivateKey = <base64_client_private_key>
|
|
743
|
+
Address = 10.8.0.2/24
|
|
744
|
+
MTU = 1420
|
|
745
|
+
DNS = 103.196.38.38, 103.196.38.39
|
|
746
|
+
|
|
747
|
+
[Peer]
|
|
748
|
+
PublicKey = <base64_server_public_key>
|
|
749
|
+
Endpoint = 1.2.3.4:51820
|
|
750
|
+
AllowedIPs = 0.0.0.0/0, ::/0
|
|
751
|
+
PersistentKeepalive = 25
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
#### Config Values
|
|
755
|
+
|
|
756
|
+
| Setting | Value | Why |
|
|
757
|
+
|---------|-------|-----|
|
|
758
|
+
| MTU | `1420` (default, configurable) | WireGuard standard for IPv4. Use `1280` for IPv6/restrictive networks. Sentinel nodes historically used 1280; current SDK defaults to 1420. |
|
|
759
|
+
| DNS | `103.196.38.38, 103.196.38.39` (default: Handshake DNS) | Configurable via `dns` option. Presets: `handshake`, `google` (8.8.8.8), `cloudflare` (1.1.1.1). Only set for full tunnel; split tunnel uses system DNS. |
|
|
760
|
+
| PersistentKeepalive | `25` (default, configurable) | NAT traversal. 15-25s is safe for all NAT routers. |
|
|
761
|
+
| AllowedIPs (full tunnel) | `0.0.0.0/0, ::/0` | Routes ALL traffic. **Kills internet if tunnel fails.** |
|
|
762
|
+
| AllowedIPs (split tunnel) | Explicit IP list | Only routes specified IPs. Safe for testing. |
|
|
763
|
+
|
|
764
|
+
#### Config File Location
|
|
765
|
+
|
|
766
|
+
- **Windows:** `C:\ProgramData\sentinel-wg\wgsent0.conf` (SYSTEM-readable; user temp dirs are not)
|
|
767
|
+
- **Linux/macOS:** `/tmp/sentinel-wg/wgsent0.conf`
|
|
768
|
+
|
|
769
|
+
**Security:** Directory ACL is set BEFORE writing the file (closes the race window where the private key would be world-readable). On Windows, `icacls` restricts to current user + SYSTEM only.
|
|
770
|
+
|
|
771
|
+
#### Verify-Before-Capture (Critical Safety Pattern)
|
|
772
|
+
|
|
773
|
+
The SDK uses a two-phase install to prevent killing the user's internet:
|
|
774
|
+
|
|
775
|
+
1. **Phase 1: Safe install** -- Install with split IPs (`1.1.1.1/32, 1.0.0.1/32`) that only capture verification traffic
|
|
776
|
+
2. **Verify** -- HTTP GET to `https://1.1.1.1` and `https://1.0.0.1` through the tunnel
|
|
777
|
+
3. **Phase 2: Full capture** -- If verified, reinstall with `AllowedIPs = 0.0.0.0/0, ::/0`
|
|
778
|
+
|
|
779
|
+
Without this pattern, a broken node causes ~78 seconds of dead internet while verification loops fail.
|
|
780
|
+
|
|
781
|
+
#### Installation (Windows)
|
|
782
|
+
|
|
783
|
+
```
|
|
784
|
+
wireguard.exe /installtunnelservice C:\ProgramData\sentinel-wg\wgsent0.conf
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
**NEVER use `/installmanagerservice`** -- that starts the WireGuard GUI which takes over tunnel management and conflicts with programmatic control.
|
|
788
|
+
|
|
789
|
+
#### Installation Retry
|
|
790
|
+
|
|
791
|
+
The SDK tries tunnel installation 3 times with escalating delays:
|
|
792
|
+
1. Wait 1.5s, try install
|
|
793
|
+
2. Wait 1.5s, try install
|
|
794
|
+
3. Wait 2.0s, try install (final attempt)
|
|
795
|
+
|
|
796
|
+
This gives the node time to register the peer (most do so within 1-2 seconds).
|
|
797
|
+
|
|
798
|
+
#### Connectivity Verification
|
|
799
|
+
|
|
800
|
+
After installation, the SDK verifies actual traffic flow:
|
|
801
|
+
- 1 attempt, 2 targets (`https://1.1.1.1`, `https://www.cloudflare.com`), 5s timeout each
|
|
802
|
+
- Maximum exposure: ~10 seconds
|
|
803
|
+
- If verification fails: immediately tear down tunnel, throw `WG_NO_CONNECTIVITY`
|
|
804
|
+
- A RUNNING service does NOT guarantee traffic flows
|
|
805
|
+
|
|
806
|
+
### V2Ray Path
|
|
807
|
+
|
|
808
|
+
#### Post-Handshake Delay
|
|
809
|
+
|
|
810
|
+
**Wait 5 seconds after handshake before starting V2Ray.** The node needs time to register the UUID internally. Without this delay, ~8% of V2Ray nodes will reject connections.
|
|
811
|
+
|
|
812
|
+
#### Sequential Outbound Fallback
|
|
813
|
+
|
|
814
|
+
The SDK does NOT use V2Ray's built-in balancer (it's buggy in v5.2.1). Instead, it implements its own sequential fallback:
|
|
815
|
+
|
|
816
|
+
1. Parse all metadata entries from handshake response
|
|
817
|
+
2. Sort outbounds by transport reliability (tcp > websocket > http > gun > mkcp > grpc/none)
|
|
818
|
+
3. For each outbound, one at a time:
|
|
819
|
+
a. Write V2Ray config with single outbound
|
|
820
|
+
b. Spawn `v2ray run -config <path>`
|
|
821
|
+
c. Wait for SOCKS5 port to accept connections (`waitForPort`, 10s timeout)
|
|
822
|
+
d. Test SOCKS5 connectivity via HTTP GET through proxy (see pre-check below)
|
|
823
|
+
e. If connected: keep this outbound, break
|
|
824
|
+
f. If failed: kill V2Ray process, try next outbound
|
|
825
|
+
|
|
826
|
+
#### CRITICAL: V2Ray SOCKS5 Connectivity Pre-Check
|
|
827
|
+
|
|
828
|
+
*(Source: Node Tester -- 411/411 V2Ray speed tests failing until this was fixed)*
|
|
829
|
+
*(Source: Complete Integration Spec -- exact retry pattern)*
|
|
830
|
+
|
|
831
|
+
V2Ray SOCKS5 binding is asynchronous. The proxy may not be ready even after the port accepts TCP connections. The pre-check MUST use retries:
|
|
832
|
+
|
|
833
|
+
```
|
|
834
|
+
Try up to 3 attempts with 5-second pause between:
|
|
835
|
+
For each target in [google.com, cloudflare.com, 1.1.1.1/cdn-cgi/trace,
|
|
836
|
+
httpbin.org/ip, ifconfig.me, ip-api.com/json]:
|
|
837
|
+
HTTP GET via SOCKS5 proxy (15s timeout)
|
|
838
|
+
If ANY target returns HTTP 200 -> tunnel is working, proceed
|
|
839
|
+
|
|
840
|
+
If all 6 targets fail -> wait 5 seconds -> try again (up to 3 attempts)
|
|
841
|
+
|
|
842
|
+
If ALL 3 attempts fail -> throw "SOCKS5 tunnel has no internet connectivity"
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**CRITICAL:** Create a FRESH `SocksProxyAgent` / `HttpClient` per request. V2Ray SOCKS5 connections SILENTLY FAIL with connection reuse. In C#, never reuse `HttpClient` for SOCKS5 -- connection pooling returns stale/empty responses.
|
|
846
|
+
|
|
847
|
+
#### V2Ray Config Rules (Silent Failures If Wrong)
|
|
848
|
+
|
|
849
|
+
| Setting | Correct | Wrong (Fails Silently) |
|
|
850
|
+
|---------|---------|----------------------|
|
|
851
|
+
| VLess encryption | `"none"` | Any other value |
|
|
852
|
+
| VLess flow | **Omit entirely** | Any value including `""` |
|
|
853
|
+
| VMess alterId | `0` | Any other value |
|
|
854
|
+
| VMess user security | **Omit entirely** | Any value |
|
|
855
|
+
| UUID field name | `"uuid"` | `"id"` |
|
|
856
|
+
| UUID format | String `"550e8400-..."` | Array or other format |
|
|
857
|
+
| grpc serviceName | `grpcSettings: { serviceName: '' }` | Omitting grpcSettings |
|
|
858
|
+
| QUIC security | `security: 'none'` | `security: 'chacha20-poly1305'` |
|
|
859
|
+
| Per-outbound transport | **No per-outbound streamSettings** | Putting streamSettings inside outbound |
|
|
860
|
+
|
|
861
|
+
#### SOCKS5 Port Assignment
|
|
862
|
+
|
|
863
|
+
Each outbound attempt gets a unique SOCKS5 port to avoid Windows TIME_WAIT conflicts:
|
|
864
|
+
```
|
|
865
|
+
basePort = 10800 + random(0, 999)
|
|
866
|
+
port_for_outbound_i = basePort + i
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
TIME_WAIT on Windows lasts ~120 seconds. Reusing the same port across fallback attempts guarantees failure.
|
|
870
|
+
|
|
871
|
+
#### SOCKS5 Authentication
|
|
872
|
+
|
|
873
|
+
V2Ray SOCKS5 inbound uses username/password authentication by default. **Windows system proxy cannot pass SOCKS5 credentials.** When `systemProxy: true`, the SDK patches SOCKS5 inbound to `noauth`.
|
|
874
|
+
|
|
875
|
+
#### SOCKS5 Testing
|
|
876
|
+
|
|
877
|
+
```javascript
|
|
878
|
+
// MUST use axios with adapter='http' for SOCKS5 testing
|
|
879
|
+
// Native fetch silently ignores the proxy agent
|
|
880
|
+
const { SocksProxyAgent } = await import('socks-proxy-agent');
|
|
881
|
+
const agent = new SocksProxyAgent(`socks5://user:pass@127.0.0.1:${socksPort}`);
|
|
882
|
+
await axios.get('https://www.google.com', {
|
|
883
|
+
httpAgent: agent,
|
|
884
|
+
httpsAgent: agent,
|
|
885
|
+
timeout: 10000,
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### 7-Layer Speed Test Fallback Chain
|
|
890
|
+
|
|
891
|
+
*(Source: Node Tester -- discovered through 780+ node tests, Complete Integration Spec)*
|
|
892
|
+
|
|
893
|
+
Speed testing must handle a wide range of tunnel capabilities. The SDK uses a 7-layer fallback chain:
|
|
894
|
+
|
|
895
|
+
#### For WireGuard (Direct Through Tunnel)
|
|
896
|
+
|
|
897
|
+
```
|
|
898
|
+
1. PRE-RESOLVE DNS before tunnel install
|
|
899
|
+
Resolve speed.cloudflare.com, proof.ovh.net, speedtest.tele2.net to IP
|
|
900
|
+
Cache resolved IPs for 5 minutes (DNS fails through many WireGuard tunnels)
|
|
901
|
+
|
|
902
|
+
2. PROBE: 1MB download (try in order, 30s timeout each)
|
|
903
|
+
a. Cloudflare via cached IP: https://{cached_cf_ip}/__down?bytes=1048576
|
|
904
|
+
b. Cloudflare via hostname: https://speed.cloudflare.com/__down?bytes=1048576
|
|
905
|
+
c. OVH fallback: https://proof.ovh.net/files/1Mb.dat
|
|
906
|
+
d. Tele2 fallback: https://speedtest.tele2.net/1MB.zip
|
|
907
|
+
e. RESCUE: Cloudflare with 60s keep-alive timeout
|
|
908
|
+
|
|
909
|
+
If ALL fail -> return "speed test failed"
|
|
910
|
+
If probeMbps < 3 -> return { mbps: probeMbps, method: "probe-only" }
|
|
911
|
+
If probeMbps >= 3 -> proceed to multi-request
|
|
912
|
+
|
|
913
|
+
3. MULTI-REQUEST: 5 x 1MB sequential downloads
|
|
914
|
+
FRESH TCP+TLS connection per download (no connection reuse)
|
|
915
|
+
Return average: { mbps: totalMbps, method: "multi-request", chunks: 5 }
|
|
916
|
+
If fails but probe worked -> return { mbps: probeMbps, method: "probe-fallback" }
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
#### For V2Ray (Through SOCKS5 Proxy)
|
|
920
|
+
|
|
921
|
+
Same fallback chain but through SOCKS5, plus two additional layers:
|
|
922
|
+
|
|
923
|
+
```
|
|
924
|
+
4-5. Same as WireGuard layers 2-3 but via SOCKS5 proxy
|
|
925
|
+
CRITICAL: Fresh SocksProxyAgent per request
|
|
926
|
+
|
|
927
|
+
6. GOOGLE FALLBACK: If all speed targets fail, time a Google HEAD request
|
|
928
|
+
Estimate speed from latency: { mbps: estimated, method: "google-fallback" }
|
|
929
|
+
|
|
930
|
+
7. CONNECTED-NO-THROUGHPUT: If connectivity check passed but ALL speed methods fail
|
|
931
|
+
Return: { mbps: 0.01, method: "connected-no-throughput" }
|
|
932
|
+
(Node is reachable but too slow for meaningful speed measurement)
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
#### Speed Test Constants
|
|
936
|
+
|
|
937
|
+
```javascript
|
|
938
|
+
SPEED_THRESHOLDS = {
|
|
939
|
+
PROBE_CUTOFF_MBPS: 3, // Below this, skip multi-request
|
|
940
|
+
PASS_10_MBPS: 10, // SLA threshold ("FAST")
|
|
941
|
+
PASS_15_MBPS: 15, // High quality threshold
|
|
942
|
+
BASELINE_MIN: 30, // Minimum baseline for SLA applicability
|
|
943
|
+
ISP_BOTTLENECK_PCT: 0.85, // 85% of baseline = ISP bottleneck
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
TIMEOUTS = {
|
|
947
|
+
PROBE_MS: 30000,
|
|
948
|
+
RESCUE_MS: 60000,
|
|
949
|
+
GOOGLE_MS: 15000,
|
|
950
|
+
MULTI_CHUNK_MS: 30000,
|
|
951
|
+
CONNECTIVITY_MS: 15000,
|
|
952
|
+
};
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## Phase 10: Verification
|
|
958
|
+
|
|
959
|
+
### IP Verification
|
|
960
|
+
|
|
961
|
+
```javascript
|
|
962
|
+
// Check your VPN IP
|
|
963
|
+
const response = await axios.get('https://api.ipify.org?format=json', { timeout: 10000 });
|
|
964
|
+
console.log(response.data.ip); // Should be the VPN node's IP, not your real IP
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### DNS Leak Check
|
|
968
|
+
|
|
969
|
+
For full-tunnel WireGuard: **Pre-resolve ALL hostnames BEFORE installing full tunnel.** DNS resolution fails through many WireGuard full tunnels because:
|
|
970
|
+
- The tunnel routes ALL traffic (including DNS)
|
|
971
|
+
- The node's internal DNS (10.8.0.1) may not resolve all domains
|
|
972
|
+
- External DNS servers may be unreachable through the tunnel
|
|
973
|
+
|
|
974
|
+
The SDK's `resolveSpeedtestIPs()` pre-resolves speed test hostnames before tunnel installation.
|
|
975
|
+
|
|
976
|
+
### CRITICAL: Fresh HttpClient Per V2Ray SOCKS5 Request
|
|
977
|
+
|
|
978
|
+
*(Source: Handshake dVPN -- 2 hours debugging; Node Tester -- 411 V2Ray tests failing)*
|
|
979
|
+
|
|
980
|
+
**Never reuse HTTP clients for V2Ray SOCKS5 proxy requests.** This is a platform-wide gotcha:
|
|
981
|
+
|
|
982
|
+
| Platform | Problem | Fix |
|
|
983
|
+
|----------|---------|-----|
|
|
984
|
+
| **Node.js** | `SocksProxyAgent` connection reuse causes TLS failures ("socket disconnected before secure connection") | Create fresh `SocksProxyAgent` per request, call `agent.destroy()` after |
|
|
985
|
+
| **C# (.NET)** | `HttpClient` connection pooling silently returns stale/empty responses through SOCKS5 | Create fresh `HttpClient(new HttpClientHandler { Proxy = ... })` per request |
|
|
986
|
+
| **Node.js fetch** | Native `fetch` (undici) **silently ignores** the `agent` option for SOCKS5 | Must use `axios` with `httpAgent`/`httpsAgent`, never native fetch |
|
|
987
|
+
|
|
988
|
+
This applies to ALL V2Ray SOCKS5 operations: connectivity pre-check, speed test, IP verification, Google check.
|
|
989
|
+
|
|
990
|
+
### Connection Verification Function
|
|
991
|
+
|
|
992
|
+
```javascript
|
|
993
|
+
import { verifyConnection } from 'sentinel-dvpn-sdk';
|
|
994
|
+
|
|
995
|
+
const result = await verifyConnection({
|
|
996
|
+
socksPort: 10800, // V2Ray only
|
|
997
|
+
timeout: 10000,
|
|
998
|
+
});
|
|
999
|
+
// result = { connected: true, vpnIp: '1.2.3.4', latencyMs: 150 }
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
---
|
|
1003
|
+
|
|
1004
|
+
## Phase 11: Active Connection
|
|
1005
|
+
|
|
1006
|
+
### V2Ray (SOCKS5 Proxy)
|
|
1007
|
+
|
|
1008
|
+
After successful connection, a SOCKS5 proxy is available at:
|
|
1009
|
+
```
|
|
1010
|
+
127.0.0.1:<socksPort>
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
Route application traffic through this proxy:
|
|
1014
|
+
```javascript
|
|
1015
|
+
const { SocksProxyAgent } = await import('socks-proxy-agent');
|
|
1016
|
+
const agent = new SocksProxyAgent(`socks5://127.0.0.1:${result.socksPort}`);
|
|
1017
|
+
const response = await axios.get('https://example.com', {
|
|
1018
|
+
httpAgent: agent,
|
|
1019
|
+
httpsAgent: agent,
|
|
1020
|
+
});
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
System-wide proxy (optional): `setSystemProxy(socksPort)` modifies Windows registry / macOS networksetup / Linux gsettings.
|
|
1024
|
+
|
|
1025
|
+
### WireGuard (Full Tunnel)
|
|
1026
|
+
|
|
1027
|
+
All system traffic is routed through the VPN. No application configuration needed. The tunnel is managed as a Windows service (`WireGuardTunnel$wgsent0`).
|
|
1028
|
+
|
|
1029
|
+
### Kill Switch
|
|
1030
|
+
|
|
1031
|
+
Blocks all non-tunnel traffic using Windows firewall rules (`netsh advfirewall`):
|
|
1032
|
+
```javascript
|
|
1033
|
+
import { enableKillSwitch, disableKillSwitch, isKillSwitchEnabled } from 'sentinel-dvpn-sdk';
|
|
1034
|
+
enableKillSwitch(serverEndpoint); // Blocks all traffic except to VPN server
|
|
1035
|
+
disableKillSwitch(); // Restores normal routing
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
The kill switch state is persisted to disk. On crash, `recoverOrphans()` at next startup detects and cleans orphaned firewall rules.
|
|
1039
|
+
|
|
1040
|
+
### Auto-Reconnect
|
|
1041
|
+
|
|
1042
|
+
```javascript
|
|
1043
|
+
import { autoReconnect } from 'sentinel-dvpn-sdk';
|
|
1044
|
+
autoReconnect({
|
|
1045
|
+
mnemonic: process.env.MNEMONIC,
|
|
1046
|
+
maxRetries: 5,
|
|
1047
|
+
onReconnecting: (attempt) => console.log(`Reconnecting (${attempt})...`),
|
|
1048
|
+
});
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
### Events
|
|
1052
|
+
|
|
1053
|
+
```javascript
|
|
1054
|
+
import { events } from 'sentinel-dvpn-sdk';
|
|
1055
|
+
events.on('connected', ({ sessionId, serviceType, nodeAddress }) => { });
|
|
1056
|
+
events.on('disconnected', ({ nodeAddress, serviceType, reason }) => { });
|
|
1057
|
+
events.on('progress', ({ event, detail, ts }) => { });
|
|
1058
|
+
events.on('error', (err) => { });
|
|
1059
|
+
events.on('sessionEnded', ({ txHash }) => { });
|
|
1060
|
+
events.on('sessionEndFailed', ({ error }) => { });
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
---
|
|
1064
|
+
|
|
1065
|
+
## Phase 12: Disconnect
|
|
1066
|
+
|
|
1067
|
+
### SDK Disconnect
|
|
1068
|
+
|
|
1069
|
+
```javascript
|
|
1070
|
+
import { disconnect } from 'sentinel-dvpn-sdk';
|
|
1071
|
+
await disconnect();
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
Or use the cleanup function returned by `connectDirect()`:
|
|
1075
|
+
```javascript
|
|
1076
|
+
const conn = await connectDirect({ ... });
|
|
1077
|
+
// ... use VPN ...
|
|
1078
|
+
await conn.cleanup();
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
### Disconnect Sequence
|
|
1082
|
+
|
|
1083
|
+
1. **Signal abort:** Set `_abortConnect = true` to stop any running `connectAuto()` retry loop
|
|
1084
|
+
2. **Release connection lock:** Set `_connectLock = false` so user can reconnect
|
|
1085
|
+
3. **Disable kill switch:** Remove firewall rules (if enabled)
|
|
1086
|
+
4. **Clear system proxy:** Restore previous proxy settings (Windows registry / macOS / Linux)
|
|
1087
|
+
5. **Kill V2Ray:** `process.kill()` on the V2Ray child process
|
|
1088
|
+
6. **Uninstall WireGuard:** `wireguard.exe /uninstalltunnelservice wgsent0`
|
|
1089
|
+
7. **End session on chain:** `MsgCancelSessionRequest` (fire-and-forget, best-effort)
|
|
1090
|
+
8. **Zero mnemonic:** `state._mnemonic = null`
|
|
1091
|
+
9. **Clear connection state:** `state.connection = null`
|
|
1092
|
+
10. **Clear persisted state:** Remove crash recovery files
|
|
1093
|
+
11. **Flush DNS cache:** Clear stale speed test DNS entries
|
|
1094
|
+
12. **Emit event:** `events.emit('disconnected', { ... })`
|
|
1095
|
+
|
|
1096
|
+
### End Session On-Chain
|
|
1097
|
+
|
|
1098
|
+
Message type: `/sentinel.session.v3.MsgCancelSessionRequest`
|
|
1099
|
+
|
|
1100
|
+
| Field # | Name | Type |
|
|
1101
|
+
|---------|------|------|
|
|
1102
|
+
| 1 | `from` | string (signer address) |
|
|
1103
|
+
| 2 | `id` | uint64 (session ID) |
|
|
1104
|
+
|
|
1105
|
+
Gas: Fixed 200,000 gas, 20,000 udvpn fee.
|
|
1106
|
+
|
|
1107
|
+
This is fire-and-forget (never blocks disconnect). If the TX fails, it logs a warning. Unended sessions eventually expire on-chain, but ending them promptly is good practice.
|
|
1108
|
+
|
|
1109
|
+
### CRITICAL: Session Tracker (Chain Doesn't Store Payment Mode)
|
|
1110
|
+
|
|
1111
|
+
*(Source: Handshake dVPN -- built SessionTracker from scratch because this was undocumented)*
|
|
1112
|
+
|
|
1113
|
+
The Sentinel chain does NOT store whether a session was created with GB-based or hourly payment. After disconnect, there is no way to query the chain and determine the payment mode. Consumer apps MUST track this locally.
|
|
1114
|
+
|
|
1115
|
+
**What must be persisted per session:**
|
|
1116
|
+
|
|
1117
|
+
| Field | Source | Why |
|
|
1118
|
+
|-------|--------|-----|
|
|
1119
|
+
| `sessionId` | TX event extraction | Identify the session |
|
|
1120
|
+
| `nodeAddress` | User selection | Reconnection |
|
|
1121
|
+
| `paymentMode` | User choice ("gb" or "hourly") | Chain doesn't expose this |
|
|
1122
|
+
| `gigabytes` or `hours` | User input | Allocation display |
|
|
1123
|
+
| `priceUdvpn` | Node's `quote_value` | Cost tracking |
|
|
1124
|
+
| `createdAt` | Local timestamp | Session age |
|
|
1125
|
+
|
|
1126
|
+
The SDK provides `session-tracker.js` for this. If not using the SDK module, persist to disk (e.g., `%LocalAppData%/AppName/sessions.json`). Clear on successful end-session TX.
|
|
1127
|
+
|
|
1128
|
+
**Why this matters:** Without local tracking, the app cannot:
|
|
1129
|
+
- Show "Per GB" vs "Per Hour" on the session card
|
|
1130
|
+
- Calculate remaining allocation correctly (GB remaining vs time remaining)
|
|
1131
|
+
- Display cost information after reconnection
|
|
1132
|
+
|
|
1133
|
+
### Crash Cleanup
|
|
1134
|
+
|
|
1135
|
+
The SDK registers process handlers via `registerCleanupHandlers()`:
|
|
1136
|
+
|
|
1137
|
+
| Signal | Action |
|
|
1138
|
+
|--------|--------|
|
|
1139
|
+
| `exit` | Kill switch off, clear proxy, kill V2Ray, cleanup WireGuard |
|
|
1140
|
+
| `SIGINT` (Ctrl+C) | Same + `process.exit(130)` |
|
|
1141
|
+
| `SIGTERM` | Same + `process.exit(143)` |
|
|
1142
|
+
| `uncaughtException` | Same + `process.exit(1)` |
|
|
1143
|
+
|
|
1144
|
+
On startup, `registerCleanupHandlers()` also:
|
|
1145
|
+
- Calls `recoverOrphans()` to clean state-tracked orphans from previous crashes
|
|
1146
|
+
- Calls `emergencyCleanupSync()` to remove any stale `wgsent*` WireGuard services
|
|
1147
|
+
- Calls `killOrphanV2Ray()` to terminate abandoned V2Ray processes
|
|
1148
|
+
|
|
1149
|
+
**CRITICAL: `registerCleanupHandlers()` MUST be called before any `connect*()` function.** The SDK throws `INVALID_OPTIONS` if cleanup handlers are not registered, because an unregistered crash leaves WireGuard capturing all traffic with no way to recover.
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## Appendix A: Every LCD Endpoint Path
|
|
1154
|
+
|
|
1155
|
+
### Sentinel v3 Endpoints
|
|
1156
|
+
|
|
1157
|
+
| Query | Method | Path | Notes |
|
|
1158
|
+
|-------|--------|------|-------|
|
|
1159
|
+
| Active nodes | GET | `/sentinel/node/v3/nodes?status=1&pagination.limit=5000` | Use `status=1`, not `STATUS_ACTIVE` |
|
|
1160
|
+
| Single node | GET | `/sentinel/node/v3/nodes/{sentnode1...}` | Direct lookup, no pagination |
|
|
1161
|
+
| Plan nodes | GET | `/sentinel/node/v3/plans/{planId}/nodes?pagination.limit=5000` | Pagination broken (next_key always null) |
|
|
1162
|
+
| Plan by ID | GET | `/sentinel/plan/v3/plans/{planId}` | May return 501 on some endpoints |
|
|
1163
|
+
| Plan subscribers | GET | `/sentinel/plan/v3/plans/{planId}/subscribers?pagination.limit=5000` | |
|
|
1164
|
+
| Subscriptions (by account) | GET | `/sentinel/subscription/v3/accounts/{sent1...}/subscriptions` | **Account-scoped!** |
|
|
1165
|
+
| Subscription by ID | GET | `/sentinel/subscription/v3/subscriptions/{id}` | |
|
|
1166
|
+
| Sessions (by account) | GET | `/sentinel/session/v3/accounts/{sent1...}/sessions` | Add `&status=1` for active only |
|
|
1167
|
+
| Session allocation | GET | `/sentinel/session/v3/sessions/{sessionId}/allocations` | May 404 for expired sessions |
|
|
1168
|
+
|
|
1169
|
+
### Sentinel v2 Endpoints (Still Active)
|
|
1170
|
+
|
|
1171
|
+
| Query | Method | Path | Notes |
|
|
1172
|
+
|-------|--------|------|-------|
|
|
1173
|
+
| Provider | GET | `/sentinel/provider/v2/providers/{sentprov1...}` | **Only provider remains v2** |
|
|
1174
|
+
|
|
1175
|
+
### Cosmos Standard Endpoints
|
|
1176
|
+
|
|
1177
|
+
| Query | Method | Path |
|
|
1178
|
+
|-------|--------|------|
|
|
1179
|
+
| Balance | GET | `/cosmos/bank/v1beta1/balances/{sent1...}` |
|
|
1180
|
+
| Fee grants (for address) | GET | `/cosmos/feegrant/v1beta1/allowances/{sent1...}` |
|
|
1181
|
+
| Authz grants | GET | `/cosmos/authz/v1beta1/grants?granter={addr}&grantee={addr}` |
|
|
1182
|
+
|
|
1183
|
+
### Node Direct API
|
|
1184
|
+
|
|
1185
|
+
| Query | Method | URL | Notes |
|
|
1186
|
+
|-------|--------|-----|-------|
|
|
1187
|
+
| Node status | GET | `https://{ip}:{port}/` | Returns moniker, location, peers, bandwidth |
|
|
1188
|
+
| Handshake | POST | `https://{ip}:{port}/` | Session handshake (see Phase 8) |
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
## Appendix B: Every Error Code with Action
|
|
1193
|
+
|
|
1194
|
+
### Error Classes
|
|
1195
|
+
|
|
1196
|
+
| Class | Base | When |
|
|
1197
|
+
|-------|------|------|
|
|
1198
|
+
| `SentinelError` | `Error` | Generic SDK errors |
|
|
1199
|
+
| `ValidationError` | `SentinelError` | Input validation failures |
|
|
1200
|
+
| `NodeError` | `SentinelError` | Node-level failures |
|
|
1201
|
+
| `ChainError` | `SentinelError` | Chain/transaction failures |
|
|
1202
|
+
| `TunnelError` | `SentinelError` | Tunnel setup failures |
|
|
1203
|
+
| `SecurityError` | `SentinelError` | Security policy violations |
|
|
1204
|
+
|
|
1205
|
+
### All 33 Error Codes
|
|
1206
|
+
|
|
1207
|
+
| Code | Severity | Trigger | User Message | Action |
|
|
1208
|
+
|------|----------|---------|-------------|--------|
|
|
1209
|
+
| `INVALID_OPTIONS` | fatal | Bad options object | "Invalid connection options provided." | Fix input, do not retry |
|
|
1210
|
+
| `INVALID_MNEMONIC` | fatal | Bad mnemonic (<12 words) | "Invalid wallet phrase. Must be 12 or 24 words." | Fix input, do not retry |
|
|
1211
|
+
| `INVALID_NODE_ADDRESS` | fatal | Bad sentnode1... address | "Invalid node address." | Fix input, do not retry |
|
|
1212
|
+
| `INVALID_GIGABYTES` | fatal | gigabytes < 1 or > 100 | "Invalid bandwidth amount. Must be a positive number." | Fix input, do not retry |
|
|
1213
|
+
| `INVALID_URL` | fatal | Malformed URL | "Invalid URL format." | Fix input, do not retry |
|
|
1214
|
+
| `INVALID_PLAN_ID` | fatal | Non-numeric plan ID | "Invalid plan ID." | Fix input, do not retry |
|
|
1215
|
+
| `NODE_OFFLINE` | retryable | Node unreachable or handshake failed | "This node is offline. Try a different server." | Try different node |
|
|
1216
|
+
| `NODE_NO_UDVPN` | retryable | Node doesn't accept udvpn denom | "This node does not accept P2P tokens." | Try different node |
|
|
1217
|
+
| `NODE_NOT_FOUND` | retryable | Node not on chain / address mismatch | "Node not found on chain. It may be inactive." | Try different node |
|
|
1218
|
+
| `NODE_CLOCK_DRIFT` | retryable | VMess node with >120s clock drift | "Node clock is out of sync. Try a different server." | Try different node (or VLess node) |
|
|
1219
|
+
| `NODE_INACTIVE` | retryable | Code 105 after retry | "Node went inactive. Try a different server." | Try different node |
|
|
1220
|
+
| `NODE_DATABASE_CORRUPT` | retryable | HTTP 500 with sqlite errors | "Node has a corrupted database. Try a different server." | Try different node |
|
|
1221
|
+
| `INVALID_ASSIGNED_IP` | retryable | Handshake returned bad IP/CIDR | "Node returned an invalid IP address during handshake. Try a different server." | Try different node |
|
|
1222
|
+
| `INSUFFICIENT_BALANCE` | fatal | Wallet < 1.0 P2P | "Not enough P2P tokens. Fund your wallet to continue." | Fund wallet, do not retry |
|
|
1223
|
+
| `BROADCAST_FAILED` | retryable | TX broadcast network error | "Transaction failed. Check your balance and try again." | Retry after delay |
|
|
1224
|
+
| `TX_FAILED` | retryable | TX returned non-zero code | "Chain transaction rejected. Check balance and gas." | Check error, retry |
|
|
1225
|
+
| `LCD_ERROR` | retryable | LCD query returned error code | "Chain query failed. Try again later." | Retry with fallback LCD |
|
|
1226
|
+
| `UNKNOWN_MSG_TYPE` | fatal | Unregistered protobuf type | "Unknown message type. Check SDK version compatibility." | Update SDK |
|
|
1227
|
+
| `ALL_ENDPOINTS_FAILED` | retryable | All LCD/RPC endpoints down | "All chain endpoints are unreachable. Try again later." | Wait and retry |
|
|
1228
|
+
| `SESSION_EXISTS` | recoverable | Active session found for wallet+node | "An active session already exists. Use recoverSession() to resume." | Call `recoverSession()` |
|
|
1229
|
+
| `SESSION_EXTRACT_FAILED` | recoverable | TX OK but no session ID in events | "Session creation succeeded but ID extraction failed. Use recoverSession()." | Call `recoverSession()` |
|
|
1230
|
+
| `SESSION_POISONED` | fatal | Previously failed session reuse attempt | "Session is poisoned (previously failed). Start a new session." | Use `forceNewSession: true` |
|
|
1231
|
+
| `V2RAY_NOT_FOUND` | infrastructure | v2ray.exe missing | "V2Ray binary not found. Check your installation." | Run `npm run setup` |
|
|
1232
|
+
| `V2RAY_ALL_FAILED` | retryable | All V2Ray transports failed | "Could not establish tunnel. Node may be overloaded." | Try different node |
|
|
1233
|
+
| `WG_NOT_AVAILABLE` | fatal | WireGuard not installed | "WireGuard is not available. Install it or use V2Ray nodes." | Install WireGuard |
|
|
1234
|
+
| `WG_NO_CONNECTIVITY` | retryable | Tunnel installed but no traffic | "VPN tunnel has no internet connectivity." | Try different node |
|
|
1235
|
+
| `TUNNEL_SETUP_FAILED` | retryable | Generic tunnel error | "Tunnel setup failed. Try again or pick another server." | Try different node |
|
|
1236
|
+
| `TLS_CERT_CHANGED` | infrastructure | TOFU certificate mismatch | "Node certificate changed unexpectedly. This could indicate a security issue." | Investigate; clear TOFU store if intentional |
|
|
1237
|
+
| `ABORTED` | -- | AbortController signal / disconnect during connect | "Connection was cancelled." | User-initiated; no action |
|
|
1238
|
+
| `ALL_NODES_FAILED` | retryable | connectAuto exhausted all candidates | "All servers failed. Check your network connection." | Check network; increase maxAttempts |
|
|
1239
|
+
| `ALREADY_CONNECTED` | -- | connect() called while connected | "Already connected. Disconnect first." | Call `disconnect()` first |
|
|
1240
|
+
| `PARTIAL_CONNECTION_FAILED` | recoverable | Payment OK, tunnel failed | "Payment succeeded but connection failed. Use recoverSession() to retry." | Call `recoverSession()` |
|
|
1241
|
+
| `CHAIN_LAG` | retryable | Session not confirmed on node | "Session not yet confirmed on node. Wait a moment and try again." | Wait 10s, retry |
|
|
1242
|
+
|
|
1243
|
+
### Severity Classification
|
|
1244
|
+
|
|
1245
|
+
| Severity | Meaning | Retry? | Action |
|
|
1246
|
+
|----------|---------|--------|--------|
|
|
1247
|
+
| `fatal` | User action required | No | Fix input, fund wallet, install dependency |
|
|
1248
|
+
| `retryable` | Transient failure | Yes, different node | Use `connectAuto()` for automatic fallback |
|
|
1249
|
+
| `recoverable` | Partial success | Yes, same session | Call `recoverSession()` to resume |
|
|
1250
|
+
| `infrastructure` | System issue | No | Fix system state (install binary, check certs) |
|
|
1251
|
+
|
|
1252
|
+
### Usage
|
|
1253
|
+
|
|
1254
|
+
```javascript
|
|
1255
|
+
import { ErrorCodes, ERROR_SEVERITY, isRetryable, userMessage } from 'sentinel-dvpn-sdk';
|
|
1256
|
+
|
|
1257
|
+
try {
|
|
1258
|
+
await connectDirect({ ... });
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
console.log(err.code); // 'V2RAY_ALL_FAILED'
|
|
1261
|
+
console.log(err.name); // 'TunnelError'
|
|
1262
|
+
console.log(userMessage(err)); // 'Could not establish tunnel...'
|
|
1263
|
+
console.log(ERROR_SEVERITY[err.code]); // 'retryable'
|
|
1264
|
+
console.log(isRetryable(err)); // true
|
|
1265
|
+
console.log(err.details); // { sessionId, nodeAddress, failedAt }
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
---
|
|
1270
|
+
|
|
1271
|
+
## Appendix C: Configuration That Silently Fails If Wrong
|
|
1272
|
+
|
|
1273
|
+
These are settings where an incorrect value produces NO error message -- the connection simply doesn't work, traffic doesn't flow, or data is silently wrong. Every one of these has caused real production failures.
|
|
1274
|
+
|
|
1275
|
+
### Protocol Configuration
|
|
1276
|
+
|
|
1277
|
+
| # | Setting | Correct Value | Wrong Value | What Happens |
|
|
1278
|
+
|---|---------|--------------|-------------|-------------|
|
|
1279
|
+
| 1 | VLess encryption | `"none"` | Any other string | Connection accepted but zero traffic flows |
|
|
1280
|
+
| 2 | VLess flow field | **Omit entirely** | `""` or `"xtls-rprx-vision"` | V2Ray silently drops to fallback |
|
|
1281
|
+
| 3 | VMess alterId | `0` | Any non-zero value | Authentication fails silently |
|
|
1282
|
+
| 4 | VMess user security | **Omit from user object** | `"auto"` or `"aes-128-gcm"` | Config parsing fails silently |
|
|
1283
|
+
| 5 | UUID field name | `"uuid"` | `"id"` | V2Ray cannot match incoming connection |
|
|
1284
|
+
| 6 | grpc serviceName | `grpcSettings: { serviceName: '' }` | Omitting grpcSettings entirely | gRPC transport silently fails |
|
|
1285
|
+
| 7 | QUIC security | `security: 'none'` | `security: 'chacha20-poly1305'` | QUIC handshake fails silently |
|
|
1286
|
+
| 8 | gun vs grpc in V2Ray config | Both use `"network": "grpc"` with grpcSettings | Using `"network": "gun"` | V2Ray does not recognize "gun" network |
|
|
1287
|
+
|
|
1288
|
+
### Signature & Handshake
|
|
1289
|
+
|
|
1290
|
+
| # | Setting | Correct Value | Wrong Value | What Happens |
|
|
1291
|
+
|---|---------|--------------|-------------|-------------|
|
|
1292
|
+
| 9 | Signature input | `SHA256(BigEndian_uint64(sessionId) + raw_data_bytes)` | Using base64-encoded data string | Signature verification fails, 401 |
|
|
1293
|
+
| 10 | Signature length | Exactly 64 bytes (r + s) | 65 bytes (with recovery byte) | Go `VerifySignature` returns false |
|
|
1294
|
+
| 11 | Session ID in POST body | JavaScript Number (not BigInt) | BigInt | `JSON.stringify` throws TypeError |
|
|
1295
|
+
| 12 | Public key encoding | `"secp256k1:" + base64(compressed_33_bytes)` | Uncompressed key (65 bytes) | Key format mismatch, rejected |
|
|
1296
|
+
|
|
1297
|
+
### Chain Queries
|
|
1298
|
+
|
|
1299
|
+
| # | Setting | Correct Value | Wrong Value | What Happens |
|
|
1300
|
+
|---|---------|--------------|-------------|-------------|
|
|
1301
|
+
| 13 | LCD API version | v3 paths (`/sentinel/node/v3/...`) | v2 paths | Returns "Not Implemented" |
|
|
1302
|
+
| 14 | Provider API version | v2 path (`/sentinel/provider/v2/...`) | v3 path | Returns 501 |
|
|
1303
|
+
| 15 | Status filter | `status=1` (integer) | `status=STATUS_ACTIVE` (string) | Returns wrong or empty results |
|
|
1304
|
+
| 16 | Node type field | `service_type` (v3) | `type` (v2) | `undefined` -- no error, just missing data |
|
|
1305
|
+
| 17 | Remote URL field | `remote_addrs` (array) | `remote_url` (string) | `undefined` -- all connections fail |
|
|
1306
|
+
| 18 | Session data | `session.base_session.id` | `session.id` | `undefined` -- silent null propagation |
|
|
1307
|
+
| 19 | Account address field | `acc_address` (v3) | `address` (v2) | `undefined` -- subscription parsing fails |
|
|
1308
|
+
| 20 | Pagination | `limit=5000` single request | Trust `count_total` or `next_key` | Missing 400+ nodes silently |
|
|
1309
|
+
|
|
1310
|
+
### Tunnel Configuration
|
|
1311
|
+
|
|
1312
|
+
| # | Setting | Correct Value | Wrong Value | What Happens |
|
|
1313
|
+
|---|---------|--------------|-------------|-------------|
|
|
1314
|
+
| 21 | WireGuard config path (Windows) | `C:\ProgramData\sentinel-wg\` | User temp dir | SYSTEM service cannot read file -- silent fail |
|
|
1315
|
+
| 22 | axios SOCKS5 adapter | `axios` with `adapter: 'http'` | Native `fetch` | Proxy agent silently ignored, direct connection |
|
|
1316
|
+
| 23 | System proxy + SOCKS5 auth | `noauth` when systemProxy is true | Username/password auth | System proxy cannot pass credentials, zero traffic |
|
|
1317
|
+
| 24 | SOCKS5 port reuse in fallback | Unique port per outbound | Same port | TIME_WAIT (120s on Windows) blocks connection |
|
|
1318
|
+
| 25 | Full tunnel default in dev | `fullTunnel: false` for development | `fullTunnel: true` | AI's own internet dies (RPC/LCD/npm unreachable) |
|
|
1319
|
+
| 26 | DNS before full tunnel | Pre-resolve hostnames BEFORE installing tunnel | Resolve after tunnel up | DNS fails through tunnel, speed test returns 0 |
|
|
1320
|
+
| 27 | Cleanup handler registration | Call `registerCleanupHandlers()` before connect | Skip it | Crash leaves WireGuard 0.0.0.0/0 route -- dead internet |
|
|
1321
|
+
|
|
1322
|
+
### Consumer App Integration (from project findings)
|
|
1323
|
+
|
|
1324
|
+
*(Source: Handshake dVPN, Test2, Node Tester -- 22 discovered patterns)*
|
|
1325
|
+
|
|
1326
|
+
| # | Setting | Correct Value | Wrong Value | What Happens |
|
|
1327
|
+
|---|---------|--------------|-------------|-------------|
|
|
1328
|
+
| 28 | Price display field | `quote_value` (integer udvpn) | `base_value` (18-decimal sdk.Dec) | UI shows `52573.099722991367791000000000/GB` |
|
|
1329
|
+
| 29 | Fee grant for direct-connect | Disabled / `feeGrant: false` | Auto-detect (assumes plan-based) | TX fails with invalid fee grant |
|
|
1330
|
+
| 30 | Session creation for consumer apps | `forceNewSession: true` | Default (reuse existing) | Stale session 404 "does not exist" errors |
|
|
1331
|
+
| 31 | V2Ray SOCKS5 HttpClient reuse | Fresh client per request | Reuse connections | Stale/empty responses, TLS disconnects |
|
|
1332
|
+
| 32 | WireGuard pre-connect cleanup | Uninstall `wgsent0` before connect | Skip cleanup | New tunnel install fails silently |
|
|
1333
|
+
| 33 | CancellationToken for speed test (C#) | `CancellationToken.None` | Pass parent CT | Speed test cancelled prematurely |
|
|
1334
|
+
| 34 | Shared VPN client for testing | Dedicated test VPN instance | Share with main app | State corruption, tunnel leftovers |
|
|
1335
|
+
| 35 | V2Ray "context canceled" after success | Ignore (normal cleanup) | Treat as error | False failure reporting |
|
|
1336
|
+
| 36 | WPF emoji country flags | PNG images from flagcdn.com | Unicode emoji | Invisible flags (Windows limitation) |
|
|
1337
|
+
| 37 | Country name normalization | 120+ variant map with fuzzy match | Exact string match | "The Netherlands" != "Netherlands" -> no flag |
|
|
1338
|
+
| 38 | Plan endpoint v3 | May return 501 on some LCD endpoints | Assume always works | Unhandled 501, no plan data |
|
|
1339
|
+
| 39 | JSON BigInt serialization | `sessionId.toString()` before JSON | `JSON.stringify({ id: 123n })` | `TypeError: Do not know how to serialize a BigInt` |
|
|
1340
|
+
| 40 | Progress counter on error | Increment on ALL paths (success, error, cancel) | Only on success | UI freezes, appears stuck |
|
|
1341
|
+
| 41 | Background refresh during test | Cancel refresh before starting test | Let refresh run | Chain client contention, connection timeouts |
|
|
1342
|
+
| 42 | Node status check without wallet | ChainClient usable pre-login | Require wallet for all queries | Cannot load nodes before login |
|
|
1343
|
+
| 43 | `PreferHourly` SDK option | Silently creates GB sessions (known bug) | Trust it works | Wrong session type, wrong billing |
|
|
1344
|
+
|
|
1345
|
+
---
|
|
1346
|
+
|
|
1347
|
+
## Appendix D: Complete Message Type URLs
|
|
1348
|
+
|
|
1349
|
+
All 22 registered protobuf message types in the CosmJS Registry:
|
|
1350
|
+
|
|
1351
|
+
### Consumer App Messages
|
|
1352
|
+
|
|
1353
|
+
| Operation | Type URL |
|
|
1354
|
+
|-----------|----------|
|
|
1355
|
+
| Start direct session | `/sentinel.node.v3.MsgStartSessionRequest` |
|
|
1356
|
+
| End/cancel session | `/sentinel.session.v3.MsgCancelSessionRequest` |
|
|
1357
|
+
| Start subscription | `/sentinel.subscription.v3.MsgStartSubscriptionRequest` |
|
|
1358
|
+
| Start session via subscription | `/sentinel.subscription.v3.MsgStartSessionRequest` |
|
|
1359
|
+
| Start session via plan | `/sentinel.plan.v3.MsgStartSessionRequest` |
|
|
1360
|
+
| Cancel subscription | `/sentinel.subscription.v3.MsgCancelSubscriptionRequest` |
|
|
1361
|
+
| Renew subscription | `/sentinel.subscription.v3.MsgRenewSubscriptionRequest` |
|
|
1362
|
+
| Share subscription | `/sentinel.subscription.v3.MsgShareSubscriptionRequest` |
|
|
1363
|
+
| Update subscription | `/sentinel.subscription.v3.MsgUpdateSubscriptionRequest` |
|
|
1364
|
+
| Update session | `/sentinel.session.v3.MsgUpdateSessionRequest` |
|
|
1365
|
+
|
|
1366
|
+
### Operator/Provider Messages
|
|
1367
|
+
|
|
1368
|
+
| Operation | Type URL |
|
|
1369
|
+
|-----------|----------|
|
|
1370
|
+
| Register provider | `/sentinel.provider.v3.MsgRegisterProviderRequest` |
|
|
1371
|
+
| Update provider details | `/sentinel.provider.v3.MsgUpdateProviderDetailsRequest` |
|
|
1372
|
+
| Update provider status | `/sentinel.provider.v3.MsgUpdateProviderStatusRequest` |
|
|
1373
|
+
| Create plan | `/sentinel.plan.v3.MsgCreatePlanRequest` |
|
|
1374
|
+
| Update plan status | `/sentinel.plan.v3.MsgUpdatePlanStatusRequest` |
|
|
1375
|
+
| Update plan details | `/sentinel.plan.v3.MsgUpdatePlanDetailsRequest` |
|
|
1376
|
+
| Link node to plan | `/sentinel.plan.v3.MsgLinkNodeRequest` |
|
|
1377
|
+
| Unlink node from plan | `/sentinel.plan.v3.MsgUnlinkNodeRequest` |
|
|
1378
|
+
| Register node | `/sentinel.node.v3.MsgRegisterNodeRequest` |
|
|
1379
|
+
| Update node details | `/sentinel.node.v3.MsgUpdateNodeDetailsRequest` |
|
|
1380
|
+
| Update node status | `/sentinel.node.v3.MsgUpdateNodeStatusRequest` |
|
|
1381
|
+
|
|
1382
|
+
### Lease Messages (v1)
|
|
1383
|
+
|
|
1384
|
+
| Operation | Type URL |
|
|
1385
|
+
|-----------|----------|
|
|
1386
|
+
| Start lease | `/sentinel.lease.v1.MsgStartLeaseRequest` |
|
|
1387
|
+
| End lease | `/sentinel.lease.v1.MsgEndLeaseRequest` |
|
|
1388
|
+
|
|
1389
|
+
---
|
|
1390
|
+
|
|
1391
|
+
## Appendix E: Timeouts Reference
|
|
1392
|
+
|
|
1393
|
+
| Operation | Timeout | Configurable | Notes |
|
|
1394
|
+
|-----------|---------|-------------|-------|
|
|
1395
|
+
| Handshake POST | 90,000ms | `timeouts.handshake` | Overloaded nodes need 60-90s |
|
|
1396
|
+
| Node status GET | 12,000ms | `timeouts.nodeStatus` | Quick health check |
|
|
1397
|
+
| LCD query | 15,000ms | `timeouts.lcdQuery` | Chain REST API |
|
|
1398
|
+
| V2Ray port ready | 10,000ms | `timeouts.v2rayReady` | SOCKS5 port acceptance |
|
|
1399
|
+
| WireGuard verify | 5,000ms per target | Hardcoded | 2 targets, 1 attempt |
|
|
1400
|
+
| SOCKS5 connectivity test | 10,000ms | Hardcoded | Through V2Ray proxy |
|
|
1401
|
+
| Post-payment wait | 5,000ms | Hardcoded | Node session indexing |
|
|
1402
|
+
| Chain lag retry | 10,000ms | Hardcoded | Handshake 404 retry |
|
|
1403
|
+
| Already-exists retry 1 | 15,000ms | Hardcoded | Session indexing race |
|
|
1404
|
+
| Already-exists retry 2 | 20,000ms | Hardcoded | Final retry before fresh payment |
|
|
1405
|
+
| V2Ray UUID registration | 5,000ms | Hardcoded | Post-handshake warmup |
|
|
1406
|
+
| WireGuard peer registration | 1,500ms / 1,500ms / 2,000ms | Hardcoded | Exponential install retry |
|
|
1407
|
+
| Sequence mismatch retry | 2,000ms / 4,000ms / 6,000ms / 6,000ms / 4,000ms | Hardcoded | Up to 5 retries + final |
|
|
1408
|
+
| Node inactive retry | 15,000ms | Hardcoded | Code 105 stale LCD data |
|
|
1409
|
+
| Circuit breaker TTL | 300,000ms (5 min) | `configureCircuitBreaker()` | Node skip duration |
|
|
1410
|
+
| Node cache TTL | 300,000ms (5 min) | Hardcoded | Background refresh |
|
|
1411
|
+
| Dynamic rate TTL | 604,800,000ms (7 days) | Hardcoded | Transport success rates |
|
|
1412
|
+
| Inter-TX spacing | 7,000ms minimum | Manual | Prevent sequence mismatch |
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
## Appendix F: Quick Start (Minimal Working Code)
|
|
1417
|
+
|
|
1418
|
+
```javascript
|
|
1419
|
+
import {
|
|
1420
|
+
registerCleanupHandlers,
|
|
1421
|
+
quickConnect,
|
|
1422
|
+
disconnect,
|
|
1423
|
+
events,
|
|
1424
|
+
} from 'sentinel-dvpn-sdk';
|
|
1425
|
+
|
|
1426
|
+
// Listen to events
|
|
1427
|
+
events.on('progress', ({ event, detail }) => console.log(`[${event}] ${detail}`));
|
|
1428
|
+
events.on('connected', ({ serviceType, nodeAddress }) => {
|
|
1429
|
+
console.log(`Connected via ${serviceType} to ${nodeAddress}`);
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// One-call connection (handles everything)
|
|
1433
|
+
const conn = await quickConnect({
|
|
1434
|
+
mnemonic: process.env.SENTINEL_MNEMONIC,
|
|
1435
|
+
countries: ['DE', 'NL'], // Preferred countries
|
|
1436
|
+
serviceType: 'v2ray', // 'wireguard' | 'v2ray' | null
|
|
1437
|
+
maxAttempts: 3, // Try up to 3 nodes
|
|
1438
|
+
fullTunnel: true, // Route all traffic through VPN
|
|
1439
|
+
onProgress: (step, detail) => console.log(`[${step}] ${detail}`),
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
console.log(`Session: ${conn.sessionId}`);
|
|
1443
|
+
console.log(`Type: ${conn.serviceType}`);
|
|
1444
|
+
if (conn.socksPort) console.log(`SOCKS5: 127.0.0.1:${conn.socksPort}`);
|
|
1445
|
+
if (conn.vpnIp) console.log(`VPN IP: ${conn.vpnIp}`);
|
|
1446
|
+
|
|
1447
|
+
// ... use VPN ...
|
|
1448
|
+
|
|
1449
|
+
// Disconnect
|
|
1450
|
+
await disconnect();
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1453
|
+
`quickConnect()` automatically:
|
|
1454
|
+
1. Calls `registerCleanupHandlers()` (idempotent)
|
|
1455
|
+
2. Verifies dependencies (V2Ray, WireGuard)
|
|
1456
|
+
3. Selects best node based on quality scoring
|
|
1457
|
+
4. Connects with automatic fallback
|
|
1458
|
+
5. Verifies IP changed
|
|
1459
|
+
|
|
1460
|
+
For more control, use `connectDirect()` (specific node) or `connectAuto()` (auto-fallback without quickConnect's dependency check overhead).
|
|
1461
|
+
|
|
1462
|
+
---
|
|
1463
|
+
|
|
1464
|
+
## Appendix G: Consumer App Integration Patterns
|
|
1465
|
+
|
|
1466
|
+
*(Source: Handshake dVPN retrospective, Test2 proving ground, Node Tester 780+ node scans)*
|
|
1467
|
+
|
|
1468
|
+
These patterns were discovered through 165+ hours of production debugging across 8 consumer apps. Every pattern listed here caused real failures.
|
|
1469
|
+
|
|
1470
|
+
### Dedicated Test VPN Instance (CRITICAL)
|
|
1471
|
+
|
|
1472
|
+
*(Source: Handshake dVPN -- 2 hours debugging state corruption)*
|
|
1473
|
+
|
|
1474
|
+
**NEVER share the main VPN client with a test/audit feature.** Create a fresh VPN client per test node:
|
|
1475
|
+
- Main app VPN: user's active connection, managed by UI
|
|
1476
|
+
- Test VPN: dedicated instance per test, disposed after each node
|
|
1477
|
+
- The two must share NO state (no shared chain client, no shared tunnel, no shared session)
|
|
1478
|
+
|
|
1479
|
+
In C#: `new SentinelVpnClient()` per test. In JS: separate `connectDirect()` call with independent options.
|
|
1480
|
+
|
|
1481
|
+
### CancellationToken Architecture (C#)
|
|
1482
|
+
|
|
1483
|
+
*(Source: Handshake dVPN -- 1 hour debugging premature cancellation)*
|
|
1484
|
+
|
|
1485
|
+
| Operation | CancellationToken | Why |
|
|
1486
|
+
|-----------|------------------|-----|
|
|
1487
|
+
| Outer test loop (between nodes) | Pass `ct` | Check between nodes, stop cleanly |
|
|
1488
|
+
| Speed test (once started) | `CancellationToken.None` | Let it complete, don't waste the session |
|
|
1489
|
+
| Google connectivity check | `CancellationToken.None` | Let it complete |
|
|
1490
|
+
| Background refresh | Cancel BEFORE starting test | Prevent chain client contention |
|
|
1491
|
+
|
|
1492
|
+
Use `volatile bool _stopRequested` as an additional stop signal -- CancellationToken alone is NOT sufficient because SDK async operations don't respond to cancellation mid-flight.
|
|
1493
|
+
|
|
1494
|
+
### In-App Node Testing (Level 2)
|
|
1495
|
+
|
|
1496
|
+
*(Source: Handshake dVPN AI-NODE-TEST-INTEGRATION spec)*
|
|
1497
|
+
|
|
1498
|
+
Two levels of testing exist:
|
|
1499
|
+
- **Level 1 (CLI/Browser):** Tests raw protocol (handshake, V2Ray config, transport). Finds SDK bugs.
|
|
1500
|
+
- **Level 2 (In-App):** Tests the APP's own `connect()`/`disconnect()`. Finds integration bugs.
|
|
1501
|
+
|
|
1502
|
+
Level 2 is a thin orchestrator:
|
|
1503
|
+
1. Gets node list via the app's own API
|
|
1504
|
+
2. For each node: calls the app's `ConnectDirectAsync()` (not raw SDK)
|
|
1505
|
+
3. Runs connectivity check + speed test through the tunnel
|
|
1506
|
+
4. Calls the app's `Disconnect()`
|
|
1507
|
+
5. Records the result
|
|
1508
|
+
|
|
1509
|
+
**It does NOT reimplement handshake/tunnel logic.** The app is the black box.
|
|
1510
|
+
|
|
1511
|
+
### Bandwidth Optimization for Consumer Apps
|
|
1512
|
+
|
|
1513
|
+
*(Source: Handshake dVPN -- measured actual bandwidth)*
|
|
1514
|
+
|
|
1515
|
+
| Poll Type | Interval | Cost | Notes |
|
|
1516
|
+
|-----------|----------|------|-------|
|
|
1517
|
+
| Status poll (is tunnel alive?) | 3s | FREE (in-memory) | No chain call |
|
|
1518
|
+
| Allocation check (bytes remaining) | 120s | ~2KB chain query | Node posts to chain every ~5min |
|
|
1519
|
+
| IP check (am I still on VPN?) | 60s | ~0.5KB to ipify.org | Detect tunnel drops |
|
|
1520
|
+
| Balance check | 5min | ~2KB chain query | Doesn't change mid-session |
|
|
1521
|
+
| Total daily overhead when connected | -- | ~3MB | Negligible |
|
|
1522
|
+
|
|
1523
|
+
### The 6 Questions Every Feature Must Answer
|
|
1524
|
+
|
|
1525
|
+
*(Source: Handshake dVPN retrospective)*
|
|
1526
|
+
|
|
1527
|
+
1. Does it work on first use? (New user, no data)
|
|
1528
|
+
2. Does it work on return visit? (Data exists from previous session)
|
|
1529
|
+
3. Does it work after restart? (App closed, reopened)
|
|
1530
|
+
4. Does it work after failure? (Crash, network error, cancel)
|
|
1531
|
+
5. Can the user share the output? (Export, copy)
|
|
1532
|
+
6. Can the user investigate issues? (Click, expand, filter, sort)
|
|
1533
|
+
|
|
1534
|
+
### Country Flag Rendering
|
|
1535
|
+
|
|
1536
|
+
*(Source: Handshake dVPN -- 2 hours discovering WPF limitation)*
|
|
1537
|
+
|
|
1538
|
+
| Platform | Method | Notes |
|
|
1539
|
+
|----------|--------|-------|
|
|
1540
|
+
| Web/Electron | `String.fromCodePoint()` emoji | Works in all browsers |
|
|
1541
|
+
| WPF (.NET) | PNG images from `flagcdn.com/w40/{code}.png` | WPF CANNOT render emoji flags |
|
|
1542
|
+
| macOS/iOS (Swift) | Native emoji in NSImage/UILabel | Works natively |
|
|
1543
|
+
|
|
1544
|
+
For WPF: three-layer cache (memory -> disk -> download). Cache permanently (flags don't change). Requires 120+ country name variant map with fuzzy matching (e.g., "The Netherlands" -> NL, "Turkiye"/"Turkey" -> TR).
|
|
1545
|
+
|
|
1546
|
+
### Results Must Survive Restarts
|
|
1547
|
+
|
|
1548
|
+
*(Source: Handshake dVPN, Node Tester -- both had blank dashboards after restart)*
|
|
1549
|
+
|
|
1550
|
+
- On app startup, load cached results from disk BEFORE rendering any UI
|
|
1551
|
+
- Save results to disk every N nodes (5 recommended) AND on completion AND on stop
|
|
1552
|
+
- Users will force-kill your app -- don't lose 50 results because you only save at the end
|
|
1553
|
+
- NEVER show "No results yet" when results exist on disk
|
|
1554
|
+
- Handle corruption gracefully (reset to empty on parse error, don't crash)
|
|
1555
|
+
- Cap stored results at 2000 entries to prevent unbounded growth
|