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,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel V2Ray Config Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds V2Ray client configs from node handshake metadata.
|
|
5
|
+
* Handles transport mapping, outbound sorting, observatory config,
|
|
6
|
+
* and SOCKS5 proxy setup.
|
|
7
|
+
*
|
|
8
|
+
* proxy_protocol: 1=VLess 2=VMess
|
|
9
|
+
* transport_protocol:1=domainsocket 2=gun 3=grpc 4=http 5=mkcp 6=quic 7=tcp 8=websocket
|
|
10
|
+
* transport_security:0=unspecified 1=none 2=TLS (per sentinel-go-sdk transport.go iota)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import net from 'net';
|
|
14
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
15
|
+
import { getDynamicRate, resolveDnsServers } from '../defaults.js';
|
|
16
|
+
import { NodeError, TunnelError, ErrorCodes } from '../errors.js';
|
|
17
|
+
|
|
18
|
+
// ─── UUID Generation ────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export function generateV2RayUUID() {
|
|
21
|
+
return randomUUID();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Port Readiness Probe ───────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wait until a TCP port is accepting connections (SOCKS5 readiness probe).
|
|
28
|
+
* V2Ray takes variable time to bind its SOCKS5 inbound — a fixed sleep is unreliable.
|
|
29
|
+
* Returns true when ready, false if timeout.
|
|
30
|
+
* @param {number} port - Port to probe (e.g. SOCKS5 port)
|
|
31
|
+
* @param {number} timeoutMs - Max wait time (default: 10000)
|
|
32
|
+
* @param {string} host - Host to probe (default: '127.0.0.1')
|
|
33
|
+
* @param {number} intervalMs - Probe interval (default: 500)
|
|
34
|
+
*/
|
|
35
|
+
export async function waitForPort(port, timeoutMs = 10000, host = '127.0.0.1', intervalMs = 500) {
|
|
36
|
+
const deadline = Date.now() + timeoutMs;
|
|
37
|
+
while (Date.now() < deadline) {
|
|
38
|
+
const ok = await new Promise(resolve => {
|
|
39
|
+
const sock = net.createConnection({ host, port }, () => {
|
|
40
|
+
sock.destroy();
|
|
41
|
+
resolve(true);
|
|
42
|
+
});
|
|
43
|
+
sock.on('error', () => resolve(false));
|
|
44
|
+
sock.setTimeout(1000, () => { sock.destroy(); resolve(false); });
|
|
45
|
+
});
|
|
46
|
+
if (ok) return true;
|
|
47
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── V2Ray Config Helpers ──────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// Transport names — MUST match sentinel-go-sdk transport.go String() output exactly.
|
|
55
|
+
// CRITICAL: "gun" and "grpc" are DIFFERENT in V2Ray 5.x (gun = raw H2, grpc = gRPC lib).
|
|
56
|
+
const NETWORK_MAP = { 2: 'gun', 3: 'grpc', 4: 'http', 5: 'mkcp', 6: 'quic', 7: 'tcp', 8: 'websocket' };
|
|
57
|
+
|
|
58
|
+
// Sort by transport reliability so the first outbound (used by default routing) is most likely to work.
|
|
59
|
+
// Observed success rates from 780-node test (2026-03-09):
|
|
60
|
+
// tcp=100%, websocket=100%, http=100%, gun=100%, mkcp=100%
|
|
61
|
+
// grpc/none=87% (70/81), quic=0% (0/4), grpc/tls=0%
|
|
62
|
+
const TRANSPORT_PRIORITY = { 7: 0, 8: 1, 4: 2, 2: 3, 5: 4 }; // tcp, ws, http, gun, kcp
|
|
63
|
+
|
|
64
|
+
function transportSortKey(tp, ts) {
|
|
65
|
+
// Check dynamic rate first (in-memory, from actual runtime connections)
|
|
66
|
+
const network = NETWORK_MAP[tp] || 'tcp';
|
|
67
|
+
const key = ts === 2 ? `${network}/tls` : (tp === 3 ? 'grpc/none' : network);
|
|
68
|
+
const dynamicRate = getDynamicRate(key);
|
|
69
|
+
if (dynamicRate !== null) {
|
|
70
|
+
// Map rate [0,1] → sort key [0,10] (lower = better priority)
|
|
71
|
+
return Math.round((1 - dynamicRate) * 10);
|
|
72
|
+
}
|
|
73
|
+
// Fall back to hardcoded priority order
|
|
74
|
+
if (TRANSPORT_PRIORITY[tp] !== undefined) return TRANSPORT_PRIORITY[tp];
|
|
75
|
+
if (tp === 3 && ts !== 2) return 5; // grpc/none
|
|
76
|
+
if (tp === 3 && ts === 2) return 8; // grpc/tls
|
|
77
|
+
if (tp === 6 && ts === 2) return 9; // quic/tls
|
|
78
|
+
if (tp === 6 && ts !== 2) return 10; // quic/none
|
|
79
|
+
return 7;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Map v2-format metadata to v3 fields. Mutates entries in place. */
|
|
83
|
+
function normalizeV2Metadata(entries) {
|
|
84
|
+
const hasV2Format = entries.some(e => e.ca !== undefined || (e.protocol !== undefined && e.proxy_protocol === undefined));
|
|
85
|
+
if (!hasV2Format) return entries;
|
|
86
|
+
return entries.map(e => {
|
|
87
|
+
if (e.proxy_protocol !== undefined) return e; // already v3
|
|
88
|
+
const copy = { ...e };
|
|
89
|
+
// v2: protocol 1=VMess, 2=VLess → v3: proxy_protocol 2=VMess, 1=VLess (swapped)
|
|
90
|
+
if (copy.protocol !== undefined && copy.proxy_protocol === undefined) {
|
|
91
|
+
copy.proxy_protocol = copy.protocol === 2 ? 1 : 2;
|
|
92
|
+
}
|
|
93
|
+
if (copy.transport_protocol === undefined) copy.transport_protocol = 7; // default tcp
|
|
94
|
+
if (copy.transport_security === undefined) {
|
|
95
|
+
copy.transport_security = (copy.tls === 1 || copy.tls === true) ? 2 : 1;
|
|
96
|
+
}
|
|
97
|
+
return copy;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Filter out unsupported transports and sort by reliability. */
|
|
102
|
+
function filterAndSortTransports(entries) {
|
|
103
|
+
// Remove domainsocket (1 — unix sockets, can't work remotely)
|
|
104
|
+
// Remove QUIC (6 — 0% success rate from 780+ node scan, chacha20 mismatch)
|
|
105
|
+
const supported = entries.filter(e => e.transport_protocol !== 1 && e.transport_protocol !== 6);
|
|
106
|
+
if (supported.length === 0) {
|
|
107
|
+
const allDs = entries.every(e => e.transport_protocol === 1);
|
|
108
|
+
const allQuic = entries.every(e => e.transport_protocol === 6);
|
|
109
|
+
const reason = allDs ? 'All transports are domainsocket — unusable remotely'
|
|
110
|
+
: allQuic ? 'All transports are QUIC — 0% success rate on current network'
|
|
111
|
+
: 'No usable transport entries (domainsocket + QUIC filtered)';
|
|
112
|
+
throw new TunnelError(ErrorCodes.V2RAY_ALL_FAILED, reason);
|
|
113
|
+
}
|
|
114
|
+
// gun (2) and grpc (3) are different protocols in V2Ray 5.x — some nodes accept
|
|
115
|
+
// one but not the other. For every grpc entry, add a gun variant (and vice versa)
|
|
116
|
+
// so the sequential fallback loop can try both. ~13% of grpc nodes only accept gun.
|
|
117
|
+
const withDualGrpc = [];
|
|
118
|
+
for (const e of supported) {
|
|
119
|
+
withDualGrpc.push(e);
|
|
120
|
+
if (e.transport_protocol === 3) {
|
|
121
|
+
withDualGrpc.push({ ...e, transport_protocol: 2, _derived: 'gun-from-grpc' });
|
|
122
|
+
} else if (e.transport_protocol === 2) {
|
|
123
|
+
withDualGrpc.push({ ...e, transport_protocol: 3, _derived: 'grpc-from-gun' });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...withDualGrpc].sort((a, b) => {
|
|
127
|
+
return transportSortKey(a.transport_protocol, a.transport_security)
|
|
128
|
+
- transportSortKey(b.transport_protocol, b.transport_security);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Build a single V2Ray outbound from a metadata entry.
|
|
133
|
+
* @param {object} entry - metadata entry with transport/security info
|
|
134
|
+
* @param {string} serverHost - node IP/hostname
|
|
135
|
+
* @param {string} uuid - V2Ray UUID
|
|
136
|
+
* @param {object} [opts] - { clockDriftSec } for VMess alterId adjustment
|
|
137
|
+
*/
|
|
138
|
+
function buildOutbound(entry, serverHost, uuid, opts = {}) {
|
|
139
|
+
const port = parseInt(entry.port, 10);
|
|
140
|
+
if (!port || port < 1 || port > 65535) return null;
|
|
141
|
+
const protocol = entry.proxy_protocol === 1 ? 'vless' : 'vmess';
|
|
142
|
+
const network = NETWORK_MAP[entry.transport_protocol] || 'tcp';
|
|
143
|
+
const security = entry.transport_security === 2 ? 'tls' : 'none';
|
|
144
|
+
const tag = `${serverHost}_${port}_${protocol}_${network}_${security}`;
|
|
145
|
+
|
|
146
|
+
const streamSettings = { network, security };
|
|
147
|
+
if (security === 'tls') streamSettings.tlsSettings = { allowInsecure: true, serverName: serverHost };
|
|
148
|
+
if (network === 'grpc' || network === 'gun') streamSettings.grpcSettings = { serviceName: '' };
|
|
149
|
+
if (network === 'quic') streamSettings.quicSettings = { security: 'none', key: '', header: { type: 'none' } };
|
|
150
|
+
|
|
151
|
+
// VMess alterId: 0 (AEAD) by default. For nodes with >120s clock drift,
|
|
152
|
+
// use alterId: 64 (legacy VMess) which is more tolerant of time differences.
|
|
153
|
+
// VLess is unaffected by clock drift — no timestamp in the protocol.
|
|
154
|
+
const drifted = Math.abs(opts?.clockDriftSec || 0) > 120;
|
|
155
|
+
const vmessAlterId = drifted ? 64 : 0;
|
|
156
|
+
|
|
157
|
+
const settings = protocol === 'vmess'
|
|
158
|
+
? { vnext: [{ address: serverHost, port, users: [{ id: uuid, alterId: vmessAlterId }] }] }
|
|
159
|
+
: { vnext: [{ address: serverHost, port, users: [{ id: uuid, encryption: 'none', flow: '' }] }] };
|
|
160
|
+
|
|
161
|
+
return { tag, protocol, settings, streamSettings };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Main Config Builder ──────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build a complete V2Ray client JSON config from the node's handshake metadata.
|
|
168
|
+
*
|
|
169
|
+
* The node returns a metadata blob like:
|
|
170
|
+
* {"metadata":[{"port":"55215","proxy_protocol":2,"transport_protocol":3,"transport_security":1},...]}
|
|
171
|
+
*
|
|
172
|
+
* We must convert this into a proper V2Ray config with inbounds + outbounds.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} serverHost - Hostname of the node (e.g. "us04.quinz.top")
|
|
175
|
+
* @param {string} metadataJson - JSON string returned from handshake (hs.config)
|
|
176
|
+
* @param {string} uuid - UUID/UID we generated for the session
|
|
177
|
+
* @param {number} socksPort - Local SOCKS5 port to listen on (default 1080)
|
|
178
|
+
* @param {object} [opts] - { clockDriftSec, socksAuth, dns, dnsPreset }
|
|
179
|
+
* @returns {object} - Complete V2Ray config object (call JSON.stringify to write)
|
|
180
|
+
*/
|
|
181
|
+
export function buildV2RayClientConfig(serverHost, metadataJson, uuid, socksPort = 1080, opts = {}) {
|
|
182
|
+
const parsed = typeof metadataJson === 'string' ? JSON.parse(metadataJson) : metadataJson;
|
|
183
|
+
const entries = parsed.metadata || [];
|
|
184
|
+
|
|
185
|
+
if (entries.length === 0) throw new NodeError(ErrorCodes.NODE_OFFLINE, 'No metadata entries in V2Ray handshake response');
|
|
186
|
+
|
|
187
|
+
const normalized = normalizeV2Metadata(entries);
|
|
188
|
+
const sorted = filterAndSortTransports(normalized);
|
|
189
|
+
const obOpts = { clockDriftSec: opts?.clockDriftSec || 0 };
|
|
190
|
+
const outbounds = sorted.map(e => buildOutbound(e, serverHost, uuid, obOpts)).filter(Boolean);
|
|
191
|
+
if (outbounds.length === 0) throw new TunnelError(ErrorCodes.V2RAY_ALL_FAILED, 'All V2Ray outbounds filtered out (all ports invalid or transports unsupported)');
|
|
192
|
+
|
|
193
|
+
// SOCKS5 auth: 'noauth' by default — proxy binds to 127.0.0.1 (localhost only),
|
|
194
|
+
// so password auth adds complexity without security benefit. Any local process can
|
|
195
|
+
// already read V2Ray's config from memory. noauth is the industry standard for
|
|
196
|
+
// localhost SOCKS proxies (Tor, shadowsocks, clash all use noauth by default).
|
|
197
|
+
// Opt into password auth with opts.socksAuth = true for extra defense-in-depth.
|
|
198
|
+
const usePasswordAuth = opts?.socksAuth === true;
|
|
199
|
+
const socksUser = usePasswordAuth ? randomBytes(8).toString('hex') : null;
|
|
200
|
+
const socksPass = usePasswordAuth ? randomBytes(16).toString('hex') : null;
|
|
201
|
+
|
|
202
|
+
// Match the official sentinel-go-sdk client.json.tmpl structure exactly:
|
|
203
|
+
// - API inbound (dokodemo-door) for StatsService
|
|
204
|
+
// - SOCKS inbound with sniffing
|
|
205
|
+
// - ALL metadata entries as separate outbounds
|
|
206
|
+
// - Routing: API → api tag, proxy → first outbound (most reliable transport)
|
|
207
|
+
// - NEVER use balancer/observatory — causes session poisoning (see known-issues.md)
|
|
208
|
+
// - Policy with uplinkOnly/downlinkOnly = 0
|
|
209
|
+
// - Global transport section with QUIC security=none (matches sentinel-go-sdk server)
|
|
210
|
+
// Random API port — avoids Windows TIME_WAIT collisions when v2ray is killed and respawned.
|
|
211
|
+
// Port 2080 (fixed) caused cascading bind failures across sequential node tests.
|
|
212
|
+
const apiPort = 10000 + Math.floor(Math.random() * 50000);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
api: {
|
|
216
|
+
services: ['StatsService'],
|
|
217
|
+
tag: 'api',
|
|
218
|
+
},
|
|
219
|
+
inbounds: [
|
|
220
|
+
{
|
|
221
|
+
listen: '127.0.0.1',
|
|
222
|
+
port: apiPort,
|
|
223
|
+
protocol: 'dokodemo-door',
|
|
224
|
+
settings: { address: '127.0.0.1' },
|
|
225
|
+
tag: 'api',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
listen: '127.0.0.1',
|
|
229
|
+
port: socksPort,
|
|
230
|
+
protocol: 'socks',
|
|
231
|
+
settings: usePasswordAuth
|
|
232
|
+
? { auth: 'password', accounts: [{ user: socksUser, pass: socksPass }], ip: '127.0.0.1', udp: true }
|
|
233
|
+
: { auth: 'noauth', ip: '127.0.0.1', udp: true },
|
|
234
|
+
sniffing: { enabled: true, destOverride: ['http', 'tls'] },
|
|
235
|
+
tag: 'proxy',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
log: { loglevel: 'info' },
|
|
239
|
+
outbounds,
|
|
240
|
+
routing: {
|
|
241
|
+
domainStrategy: 'IPIfNonMatch',
|
|
242
|
+
rules: [
|
|
243
|
+
{ inboundTag: ['api'], outboundTag: 'api', type: 'field' },
|
|
244
|
+
{ inboundTag: ['proxy'], outboundTag: outbounds[0].tag, type: 'field' },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
policy: {
|
|
248
|
+
levels: { '0': { downlinkOnly: 0, uplinkOnly: 0 } },
|
|
249
|
+
system: { statsOutboundDownlink: true, statsOutboundUplink: true },
|
|
250
|
+
},
|
|
251
|
+
dns: {
|
|
252
|
+
servers: (opts?.dns || resolveDnsServers(opts?.dnsPreset)).split(',').map(s => s.trim()),
|
|
253
|
+
},
|
|
254
|
+
stats: {},
|
|
255
|
+
transport: {
|
|
256
|
+
dsSettings: {},
|
|
257
|
+
grpcSettings: {},
|
|
258
|
+
gunSettings: {},
|
|
259
|
+
httpSettings: {},
|
|
260
|
+
kcpSettings: {},
|
|
261
|
+
quicSettings: { security: 'none', key: '', header: { type: 'none' } },
|
|
262
|
+
tcpSettings: {},
|
|
263
|
+
wsSettings: {},
|
|
264
|
+
},
|
|
265
|
+
// SOCKS5 auth credentials — use these when creating SocksProxyAgent
|
|
266
|
+
_socksAuth: { user: socksUser, pass: socksPass },
|
|
267
|
+
};
|
|
268
|
+
}
|