@omindu/yaksha 1.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/LICENSE +21 -0
- package/bin/yaksha.js +329 -0
- package/package.json +28 -0
- package/src/bypass/firewall-evasion.js +354 -0
- package/src/client/index.js +544 -0
- package/src/core/connection.js +393 -0
- package/src/core/encryption.js +299 -0
- package/src/core/protocol.js +268 -0
- package/src/features/dns-override.js +403 -0
- package/src/features/multi-path.js +394 -0
- package/src/features/sni-spoof.js +355 -0
- package/src/features/tls-camouflage.js +369 -0
- package/src/features/traffic-obfuscation.js +338 -0
- package/src/index.js +106 -0
- package/src/security/auth.js +441 -0
- package/src/security/levels.js +316 -0
- package/src/server/index.js +551 -0
- package/src/utils/buffer-pool.js +150 -0
- package/src/utils/config.js +205 -0
- package/src/utils/logger.js +105 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Yaksha SNI Spoofing
|
|
5
|
+
* Intercept and replace Server Name Indication in TLS ClientHello
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
// Popular domains for SNI spoofing (categorized)
|
|
11
|
+
const DOMAIN_LISTS = {
|
|
12
|
+
tech: [
|
|
13
|
+
'google.com', 'microsoft.com', 'apple.com', 'amazon.com',
|
|
14
|
+
'facebook.com', 'twitter.com', 'linkedin.com', 'github.com'
|
|
15
|
+
],
|
|
16
|
+
cdn: [
|
|
17
|
+
'cloudflare.com', 'fastly.net', 'akamai.net', 'cloudfront.net',
|
|
18
|
+
'cdn77.com', 'stackpath.com', 'bunny.net', 'jsdelivr.net'
|
|
19
|
+
],
|
|
20
|
+
popular: [
|
|
21
|
+
'youtube.com', 'netflix.com', 'spotify.com', 'instagram.com',
|
|
22
|
+
'reddit.com', 'wikipedia.org', 'twitch.tv', 'zoom.us'
|
|
23
|
+
],
|
|
24
|
+
news: [
|
|
25
|
+
'nytimes.com', 'bbc.com', 'cnn.com', 'reuters.com',
|
|
26
|
+
'theguardian.com', 'wsj.com', 'bloomberg.com', 'forbes.com'
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Flatten all domains into single array
|
|
31
|
+
const ALL_DOMAINS = Object.values(DOMAIN_LISTS).flat();
|
|
32
|
+
|
|
33
|
+
class SNISpoofing {
|
|
34
|
+
constructor(securityLevel = 'medium', options = {}) {
|
|
35
|
+
this.securityLevel = securityLevel;
|
|
36
|
+
this.mode = this._selectMode(securityLevel);
|
|
37
|
+
this.customDomains = options.customDomains || [];
|
|
38
|
+
this.bugHost = options.bugHost || null; // Custom bug host for network bypass
|
|
39
|
+
this.sniMap = new Map(); // original -> fake mapping
|
|
40
|
+
this.enabled = options.enabled !== false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Select SNI spoofing mode based on security level
|
|
45
|
+
*/
|
|
46
|
+
_selectMode(level) {
|
|
47
|
+
switch (level) {
|
|
48
|
+
case 'low':
|
|
49
|
+
return 'static';
|
|
50
|
+
case 'medium':
|
|
51
|
+
return 'random';
|
|
52
|
+
case 'high':
|
|
53
|
+
return 'context-aware';
|
|
54
|
+
default:
|
|
55
|
+
return 'random';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get static fake SNI (always returns the same domain)
|
|
61
|
+
*/
|
|
62
|
+
_getStaticSNI() {
|
|
63
|
+
// Use bug host if specified, otherwise use default
|
|
64
|
+
return this.bugHost || 'cloudflare.com';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get random fake SNI from domain list
|
|
69
|
+
*/
|
|
70
|
+
_getRandomSNI() {
|
|
71
|
+
const domains = this.customDomains.length > 0
|
|
72
|
+
? this.customDomains
|
|
73
|
+
: ALL_DOMAINS;
|
|
74
|
+
|
|
75
|
+
const randomIndex = crypto.randomInt(0, domains.length);
|
|
76
|
+
return domains[randomIndex];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get context-aware fake SNI based on time and other factors
|
|
81
|
+
*/
|
|
82
|
+
_getContextAwareSNI() {
|
|
83
|
+
const hour = new Date().getHours();
|
|
84
|
+
|
|
85
|
+
// Select domain category based on time of day
|
|
86
|
+
let category;
|
|
87
|
+
if (hour >= 9 && hour < 17) {
|
|
88
|
+
// Business hours: use tech/professional domains
|
|
89
|
+
category = Math.random() < 0.7 ? 'tech' : 'news';
|
|
90
|
+
} else if (hour >= 17 && hour < 23) {
|
|
91
|
+
// Evening: use entertainment domains
|
|
92
|
+
category = Math.random() < 0.7 ? 'popular' : 'cdn';
|
|
93
|
+
} else {
|
|
94
|
+
// Night: mixed
|
|
95
|
+
category = Math.random() < 0.5 ? 'popular' : 'tech';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const domains = DOMAIN_LISTS[category];
|
|
99
|
+
const randomIndex = crypto.randomInt(0, domains.length);
|
|
100
|
+
|
|
101
|
+
// Add timing jitter to avoid patterns
|
|
102
|
+
const jitter = crypto.randomInt(0, 100);
|
|
103
|
+
setTimeout(() => {}, jitter);
|
|
104
|
+
|
|
105
|
+
return domains[randomIndex];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get fake SNI based on mode
|
|
110
|
+
*/
|
|
111
|
+
getFakeSNI(originalSNI) {
|
|
112
|
+
if (!this.enabled) {
|
|
113
|
+
return originalSNI;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If bug host is specified, always use it (highest priority)
|
|
117
|
+
if (this.bugHost) {
|
|
118
|
+
this.sniMap.set(originalSNI, this.bugHost);
|
|
119
|
+
return this.bugHost;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if we already have a mapping
|
|
123
|
+
if (this.sniMap.has(originalSNI)) {
|
|
124
|
+
return this.sniMap.get(originalSNI);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let fakeSNI;
|
|
128
|
+
|
|
129
|
+
switch (this.mode) {
|
|
130
|
+
case 'static':
|
|
131
|
+
fakeSNI = this._getStaticSNI();
|
|
132
|
+
break;
|
|
133
|
+
case 'random':
|
|
134
|
+
fakeSNI = this._getRandomSNI();
|
|
135
|
+
break;
|
|
136
|
+
case 'context-aware':
|
|
137
|
+
fakeSNI = this._getContextAwareSNI();
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
fakeSNI = this._getRandomSNI();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Store mapping
|
|
144
|
+
this.sniMap.set(originalSNI, fakeSNI);
|
|
145
|
+
|
|
146
|
+
return fakeSNI;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get original SNI from fake SNI
|
|
151
|
+
*/
|
|
152
|
+
getOriginalSNI(fakeSNI) {
|
|
153
|
+
for (const [original, fake] of this.sniMap.entries()) {
|
|
154
|
+
if (fake === fakeSNI) {
|
|
155
|
+
return original;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse TLS ClientHello to extract SNI
|
|
163
|
+
*/
|
|
164
|
+
parseSNI(clientHello) {
|
|
165
|
+
try {
|
|
166
|
+
// TLS record header: 5 bytes
|
|
167
|
+
// Handshake header: 4 bytes
|
|
168
|
+
// Client version: 2 bytes
|
|
169
|
+
// Random: 32 bytes
|
|
170
|
+
// Session ID length: 1 byte
|
|
171
|
+
|
|
172
|
+
let offset = 0;
|
|
173
|
+
|
|
174
|
+
// Skip TLS record header
|
|
175
|
+
offset += 5;
|
|
176
|
+
|
|
177
|
+
// Skip handshake header
|
|
178
|
+
offset += 4;
|
|
179
|
+
|
|
180
|
+
// Skip client version
|
|
181
|
+
offset += 2;
|
|
182
|
+
|
|
183
|
+
// Skip random
|
|
184
|
+
offset += 32;
|
|
185
|
+
|
|
186
|
+
// Read session ID length and skip session ID
|
|
187
|
+
const sessionIdLength = clientHello[offset];
|
|
188
|
+
offset += 1 + sessionIdLength;
|
|
189
|
+
|
|
190
|
+
// Read cipher suites length and skip cipher suites
|
|
191
|
+
const cipherSuitesLength = clientHello.readUInt16BE(offset);
|
|
192
|
+
offset += 2 + cipherSuitesLength;
|
|
193
|
+
|
|
194
|
+
// Read compression methods length and skip compression methods
|
|
195
|
+
const compressionMethodsLength = clientHello[offset];
|
|
196
|
+
offset += 1 + compressionMethodsLength;
|
|
197
|
+
|
|
198
|
+
// Read extensions length
|
|
199
|
+
if (offset + 2 > clientHello.length) {
|
|
200
|
+
return null; // No extensions
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const extensionsLength = clientHello.readUInt16BE(offset);
|
|
204
|
+
offset += 2;
|
|
205
|
+
|
|
206
|
+
const extensionsEnd = offset + extensionsLength;
|
|
207
|
+
|
|
208
|
+
// Parse extensions to find SNI (type 0x0000)
|
|
209
|
+
while (offset + 4 <= extensionsEnd) {
|
|
210
|
+
const extensionType = clientHello.readUInt16BE(offset);
|
|
211
|
+
const extensionLength = clientHello.readUInt16BE(offset + 2);
|
|
212
|
+
offset += 4;
|
|
213
|
+
|
|
214
|
+
if (extensionType === 0x0000) { // SNI extension
|
|
215
|
+
// SNI extension format:
|
|
216
|
+
// - Server Name List Length: 2 bytes
|
|
217
|
+
// - Server Name Type: 1 byte (0 = host_name)
|
|
218
|
+
// - Server Name Length: 2 bytes
|
|
219
|
+
// - Server Name: variable
|
|
220
|
+
|
|
221
|
+
offset += 2; // Skip list length
|
|
222
|
+
const nameType = clientHello[offset];
|
|
223
|
+
offset += 1;
|
|
224
|
+
|
|
225
|
+
if (nameType === 0) { // host_name
|
|
226
|
+
const nameLength = clientHello.readUInt16BE(offset);
|
|
227
|
+
offset += 2;
|
|
228
|
+
const serverName = clientHello.toString('utf8', offset, offset + nameLength);
|
|
229
|
+
return serverName;
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
offset += extensionLength;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Replace SNI in TLS ClientHello
|
|
244
|
+
*/
|
|
245
|
+
replaceSNI(clientHello, newSNI) {
|
|
246
|
+
try {
|
|
247
|
+
// Create a mutable copy
|
|
248
|
+
const modified = Buffer.from(clientHello);
|
|
249
|
+
|
|
250
|
+
// Find SNI extension and replace
|
|
251
|
+
// This is a simplified implementation
|
|
252
|
+
// In production, use a proper TLS parser
|
|
253
|
+
|
|
254
|
+
const originalSNI = this.parseSNI(clientHello);
|
|
255
|
+
if (!originalSNI) {
|
|
256
|
+
return modified; // No SNI to replace
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Find and replace SNI string
|
|
260
|
+
const originalSNIBuffer = Buffer.from(originalSNI, 'utf8');
|
|
261
|
+
const newSNIBuffer = Buffer.from(newSNI, 'utf8');
|
|
262
|
+
|
|
263
|
+
const sniIndex = modified.indexOf(originalSNIBuffer);
|
|
264
|
+
if (sniIndex === -1) {
|
|
265
|
+
return modified;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// If new SNI is same length, simple replacement
|
|
269
|
+
if (originalSNIBuffer.length === newSNIBuffer.length) {
|
|
270
|
+
newSNIBuffer.copy(modified, sniIndex);
|
|
271
|
+
return modified;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Different lengths require rebuilding the packet
|
|
275
|
+
// For simplicity, we'll pad/truncate to same length
|
|
276
|
+
if (newSNIBuffer.length < originalSNIBuffer.length) {
|
|
277
|
+
// Pad with zeros
|
|
278
|
+
newSNIBuffer.copy(modified, sniIndex);
|
|
279
|
+
modified.fill(0, sniIndex + newSNIBuffer.length, sniIndex + originalSNIBuffer.length);
|
|
280
|
+
} else {
|
|
281
|
+
// Truncate
|
|
282
|
+
newSNIBuffer.copy(modified, sniIndex, 0, originalSNIBuffer.length);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update SNI length field (2 bytes before SNI)
|
|
286
|
+
modified.writeUInt16BE(Math.min(newSNIBuffer.length, originalSNIBuffer.length), sniIndex - 2);
|
|
287
|
+
|
|
288
|
+
return modified;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// On error, return original
|
|
291
|
+
return clientHello;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Spoof SNI in ClientHello packet
|
|
297
|
+
*/
|
|
298
|
+
spoof(clientHello, originalSNI = null) {
|
|
299
|
+
if (!this.enabled) {
|
|
300
|
+
return clientHello;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract original SNI if not provided
|
|
304
|
+
if (!originalSNI) {
|
|
305
|
+
originalSNI = this.parseSNI(clientHello);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!originalSNI) {
|
|
309
|
+
return clientHello; // No SNI found
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get fake SNI
|
|
313
|
+
const fakeSNI = this.getFakeSNI(originalSNI);
|
|
314
|
+
|
|
315
|
+
// Replace SNI in ClientHello
|
|
316
|
+
return this.replaceSNI(clientHello, fakeSNI);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Enable SNI spoofing
|
|
321
|
+
*/
|
|
322
|
+
enable() {
|
|
323
|
+
this.enabled = true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Disable SNI spoofing
|
|
328
|
+
*/
|
|
329
|
+
disable() {
|
|
330
|
+
this.enabled = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Clear SNI mapping cache
|
|
335
|
+
*/
|
|
336
|
+
clearCache() {
|
|
337
|
+
this.sniMap.clear();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get statistics
|
|
342
|
+
*/
|
|
343
|
+
getStats() {
|
|
344
|
+
return {
|
|
345
|
+
enabled: this.enabled,
|
|
346
|
+
mode: this.mode,
|
|
347
|
+
mappings: this.sniMap.size,
|
|
348
|
+
domains: this.customDomains.length || ALL_DOMAINS.length
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = SNISpoofing;
|
|
354
|
+
module.exports.DOMAIN_LISTS = DOMAIN_LISTS;
|
|
355
|
+
module.exports.ALL_DOMAINS = ALL_DOMAINS;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Yaksha TLS 1.3 Camouflage
|
|
5
|
+
* Make VPN traffic look like legitimate HTTPS/TLS traffic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const Logger = require('../utils/logger');
|
|
10
|
+
|
|
11
|
+
// TLS 1.3 cipher suites
|
|
12
|
+
const CIPHER_SUITES = [
|
|
13
|
+
0x1301, // TLS_AES_128_GCM_SHA256
|
|
14
|
+
0x1302, // TLS_AES_256_GCM_SHA384
|
|
15
|
+
0x1303, // TLS_CHACHA20_POLY1305_SHA256
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// TLS extensions
|
|
19
|
+
const EXTENSIONS = {
|
|
20
|
+
SERVER_NAME: 0x0000,
|
|
21
|
+
MAX_FRAGMENT_LENGTH: 0x0001,
|
|
22
|
+
STATUS_REQUEST: 0x0005,
|
|
23
|
+
SUPPORTED_GROUPS: 0x000a,
|
|
24
|
+
EC_POINT_FORMATS: 0x000b,
|
|
25
|
+
SIGNATURE_ALGORITHMS: 0x000d,
|
|
26
|
+
ALPN: 0x0010,
|
|
27
|
+
SIGNED_CERTIFICATE_TIMESTAMP: 0x0012,
|
|
28
|
+
EXTENDED_MASTER_SECRET: 0x0017,
|
|
29
|
+
SESSION_TICKET: 0x0023,
|
|
30
|
+
SUPPORTED_VERSIONS: 0x002b,
|
|
31
|
+
PSK_KEY_EXCHANGE_MODES: 0x002d,
|
|
32
|
+
KEY_SHARE: 0x0033
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ALPN protocols
|
|
36
|
+
const ALPN_PROTOCOLS = ['h2', 'http/1.1'];
|
|
37
|
+
|
|
38
|
+
class TLSCamouflage {
|
|
39
|
+
constructor(securityLevel = 'medium', options = {}) {
|
|
40
|
+
this.securityLevel = securityLevel;
|
|
41
|
+
this.mode = this._selectMode(securityLevel);
|
|
42
|
+
this.enabled = options.enabled !== false;
|
|
43
|
+
this.serverName = options.serverName || 'cloudflare.com';
|
|
44
|
+
this.alpnProtocols = options.alpnProtocols || ALPN_PROTOCOLS;
|
|
45
|
+
|
|
46
|
+
this.logger = new Logger({ prefix: 'TLS-Camouflage' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Select camouflage mode based on security level
|
|
51
|
+
*/
|
|
52
|
+
_selectMode(level) {
|
|
53
|
+
switch (level) {
|
|
54
|
+
case 'low':
|
|
55
|
+
return 'basic';
|
|
56
|
+
case 'medium':
|
|
57
|
+
return 'full';
|
|
58
|
+
case 'high':
|
|
59
|
+
return 'nested';
|
|
60
|
+
default:
|
|
61
|
+
return 'full';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build TLS ClientHello packet
|
|
67
|
+
*/
|
|
68
|
+
buildClientHello(options = {}) {
|
|
69
|
+
const serverName = options.serverName || this.serverName;
|
|
70
|
+
const sessionId = options.sessionId || crypto.randomBytes(32);
|
|
71
|
+
|
|
72
|
+
// TLS Record Header (5 bytes)
|
|
73
|
+
const recordHeader = Buffer.allocUnsafe(5);
|
|
74
|
+
recordHeader.writeUInt8(0x16, 0); // Content Type: Handshake
|
|
75
|
+
recordHeader.writeUInt16BE(0x0301, 1); // Legacy Version: TLS 1.0
|
|
76
|
+
// Length will be filled later
|
|
77
|
+
|
|
78
|
+
// Handshake Header (4 bytes)
|
|
79
|
+
const handshakeHeader = Buffer.allocUnsafe(4);
|
|
80
|
+
handshakeHeader.writeUInt8(0x01, 0); // Handshake Type: ClientHello
|
|
81
|
+
// Length will be filled later
|
|
82
|
+
|
|
83
|
+
// ClientHello
|
|
84
|
+
const clientHello = [];
|
|
85
|
+
|
|
86
|
+
// Client Version (2 bytes) - TLS 1.2 for compatibility
|
|
87
|
+
const clientVersion = Buffer.allocUnsafe(2);
|
|
88
|
+
clientVersion.writeUInt16BE(0x0303, 0);
|
|
89
|
+
clientHello.push(clientVersion);
|
|
90
|
+
|
|
91
|
+
// Random (32 bytes)
|
|
92
|
+
const random = crypto.randomBytes(32);
|
|
93
|
+
clientHello.push(random);
|
|
94
|
+
|
|
95
|
+
// Session ID Length (1 byte) + Session ID
|
|
96
|
+
const sessionIdLength = Buffer.allocUnsafe(1);
|
|
97
|
+
sessionIdLength.writeUInt8(sessionId.length, 0);
|
|
98
|
+
clientHello.push(sessionIdLength, sessionId);
|
|
99
|
+
|
|
100
|
+
// Cipher Suites Length (2 bytes) + Cipher Suites
|
|
101
|
+
const cipherSuitesLength = Buffer.allocUnsafe(2);
|
|
102
|
+
cipherSuitesLength.writeUInt16BE(CIPHER_SUITES.length * 2, 0);
|
|
103
|
+
clientHello.push(cipherSuitesLength);
|
|
104
|
+
|
|
105
|
+
for (const suite of CIPHER_SUITES) {
|
|
106
|
+
const suiteBuffer = Buffer.allocUnsafe(2);
|
|
107
|
+
suiteBuffer.writeUInt16BE(suite, 0);
|
|
108
|
+
clientHello.push(suiteBuffer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Compression Methods Length (1 byte) + Compression Methods
|
|
112
|
+
const compressionMethods = Buffer.from([0x01, 0x00]); // No compression
|
|
113
|
+
clientHello.push(compressionMethods);
|
|
114
|
+
|
|
115
|
+
// Extensions
|
|
116
|
+
const extensions = this._buildExtensions(serverName);
|
|
117
|
+
clientHello.push(extensions);
|
|
118
|
+
|
|
119
|
+
// Combine ClientHello
|
|
120
|
+
const clientHelloBuffer = Buffer.concat(clientHello);
|
|
121
|
+
|
|
122
|
+
// Update handshake length
|
|
123
|
+
handshakeHeader.writeUIntBE(clientHelloBuffer.length, 1, 3);
|
|
124
|
+
|
|
125
|
+
// Combine handshake
|
|
126
|
+
const handshake = Buffer.concat([handshakeHeader, clientHelloBuffer]);
|
|
127
|
+
|
|
128
|
+
// Update record length
|
|
129
|
+
recordHeader.writeUInt16BE(handshake.length, 3);
|
|
130
|
+
|
|
131
|
+
// Complete TLS record
|
|
132
|
+
return Buffer.concat([recordHeader, handshake]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build TLS extensions
|
|
137
|
+
*/
|
|
138
|
+
_buildExtensions(serverName) {
|
|
139
|
+
const extensions = [];
|
|
140
|
+
|
|
141
|
+
// Server Name Indication (SNI)
|
|
142
|
+
const sniExtension = this._buildSNIExtension(serverName);
|
|
143
|
+
extensions.push(sniExtension);
|
|
144
|
+
|
|
145
|
+
// Supported Versions
|
|
146
|
+
const supportedVersions = Buffer.from([
|
|
147
|
+
0x00, 0x2b, // Extension Type: supported_versions
|
|
148
|
+
0x00, 0x03, // Length: 3
|
|
149
|
+
0x02, // Supported Versions Length: 2
|
|
150
|
+
0x03, 0x04 // TLS 1.3
|
|
151
|
+
]);
|
|
152
|
+
extensions.push(supportedVersions);
|
|
153
|
+
|
|
154
|
+
// Supported Groups (Elliptic Curves)
|
|
155
|
+
const supportedGroups = Buffer.from([
|
|
156
|
+
0x00, 0x0a, // Extension Type: supported_groups
|
|
157
|
+
0x00, 0x08, // Length: 8
|
|
158
|
+
0x00, 0x06, // Supported Groups Length: 6
|
|
159
|
+
0x00, 0x1d, // x25519
|
|
160
|
+
0x00, 0x17, // secp256r1
|
|
161
|
+
0x00, 0x18 // secp384r1
|
|
162
|
+
]);
|
|
163
|
+
extensions.push(supportedGroups);
|
|
164
|
+
|
|
165
|
+
// Signature Algorithms
|
|
166
|
+
const signatureAlgorithms = Buffer.from([
|
|
167
|
+
0x00, 0x0d, // Extension Type: signature_algorithms
|
|
168
|
+
0x00, 0x08, // Length: 8
|
|
169
|
+
0x00, 0x06, // Signature Algorithms Length: 6
|
|
170
|
+
0x04, 0x03, // ecdsa_secp256r1_sha256
|
|
171
|
+
0x05, 0x03, // ecdsa_secp384r1_sha384
|
|
172
|
+
0x06, 0x03 // ecdsa_secp521r1_sha512
|
|
173
|
+
]);
|
|
174
|
+
extensions.push(signatureAlgorithms);
|
|
175
|
+
|
|
176
|
+
// ALPN
|
|
177
|
+
const alpnExtension = this._buildALPNExtension();
|
|
178
|
+
extensions.push(alpnExtension);
|
|
179
|
+
|
|
180
|
+
// Key Share
|
|
181
|
+
const keyShareExtension = this._buildKeyShareExtension();
|
|
182
|
+
extensions.push(keyShareExtension);
|
|
183
|
+
|
|
184
|
+
// Extended Master Secret
|
|
185
|
+
const extendedMasterSecret = Buffer.from([
|
|
186
|
+
0x00, 0x17, // Extension Type: extended_master_secret
|
|
187
|
+
0x00, 0x00 // Length: 0
|
|
188
|
+
]);
|
|
189
|
+
extensions.push(extendedMasterSecret);
|
|
190
|
+
|
|
191
|
+
// Session Ticket
|
|
192
|
+
const sessionTicket = Buffer.from([
|
|
193
|
+
0x00, 0x23, // Extension Type: session_ticket
|
|
194
|
+
0x00, 0x00 // Length: 0
|
|
195
|
+
]);
|
|
196
|
+
extensions.push(sessionTicket);
|
|
197
|
+
|
|
198
|
+
// Combine all extensions
|
|
199
|
+
const extensionsBuffer = Buffer.concat(extensions);
|
|
200
|
+
|
|
201
|
+
// Extensions length prefix
|
|
202
|
+
const extensionsLength = Buffer.allocUnsafe(2);
|
|
203
|
+
extensionsLength.writeUInt16BE(extensionsBuffer.length, 0);
|
|
204
|
+
|
|
205
|
+
return Buffer.concat([extensionsLength, extensionsBuffer]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build SNI extension
|
|
210
|
+
*/
|
|
211
|
+
_buildSNIExtension(serverName) {
|
|
212
|
+
const serverNameBuffer = Buffer.from(serverName, 'utf8');
|
|
213
|
+
|
|
214
|
+
const extension = Buffer.allocUnsafe(9 + serverNameBuffer.length);
|
|
215
|
+
extension.writeUInt16BE(EXTENSIONS.SERVER_NAME, 0); // Extension Type
|
|
216
|
+
extension.writeUInt16BE(5 + serverNameBuffer.length, 2); // Extension Length
|
|
217
|
+
extension.writeUInt16BE(3 + serverNameBuffer.length, 4); // Server Name List Length
|
|
218
|
+
extension.writeUInt8(0x00, 6); // Server Name Type: host_name
|
|
219
|
+
extension.writeUInt16BE(serverNameBuffer.length, 7); // Server Name Length
|
|
220
|
+
serverNameBuffer.copy(extension, 9);
|
|
221
|
+
|
|
222
|
+
return extension;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build ALPN extension
|
|
227
|
+
*/
|
|
228
|
+
_buildALPNExtension() {
|
|
229
|
+
const protocols = [];
|
|
230
|
+
let totalLength = 0;
|
|
231
|
+
|
|
232
|
+
for (const protocol of this.alpnProtocols) {
|
|
233
|
+
const protocolBuffer = Buffer.from(protocol, 'utf8');
|
|
234
|
+
const protocolEntry = Buffer.allocUnsafe(1 + protocolBuffer.length);
|
|
235
|
+
protocolEntry.writeUInt8(protocolBuffer.length, 0);
|
|
236
|
+
protocolBuffer.copy(protocolEntry, 1);
|
|
237
|
+
protocols.push(protocolEntry);
|
|
238
|
+
totalLength += protocolEntry.length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const protocolsBuffer = Buffer.concat(protocols);
|
|
242
|
+
|
|
243
|
+
const extension = Buffer.allocUnsafe(6 + totalLength);
|
|
244
|
+
extension.writeUInt16BE(EXTENSIONS.ALPN, 0); // Extension Type
|
|
245
|
+
extension.writeUInt16BE(2 + totalLength, 2); // Extension Length
|
|
246
|
+
extension.writeUInt16BE(totalLength, 4); // ALPN List Length
|
|
247
|
+
protocolsBuffer.copy(extension, 6);
|
|
248
|
+
|
|
249
|
+
return extension;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build Key Share extension
|
|
254
|
+
*/
|
|
255
|
+
_buildKeyShareExtension() {
|
|
256
|
+
// Generate ephemeral X25519 key pair
|
|
257
|
+
const keyPair = crypto.generateKeyPairSync('x25519');
|
|
258
|
+
const publicKey = keyPair.publicKey.export({ type: 'spki', format: 'der' });
|
|
259
|
+
|
|
260
|
+
// Extract raw public key (last 32 bytes of SPKI)
|
|
261
|
+
const rawPublicKey = publicKey.slice(-32);
|
|
262
|
+
|
|
263
|
+
const extension = Buffer.allocUnsafe(40);
|
|
264
|
+
extension.writeUInt16BE(EXTENSIONS.KEY_SHARE, 0); // Extension Type
|
|
265
|
+
extension.writeUInt16BE(36, 2); // Extension Length
|
|
266
|
+
extension.writeUInt16BE(34, 4); // Key Share List Length
|
|
267
|
+
extension.writeUInt16BE(0x001d, 6); // Group: x25519
|
|
268
|
+
extension.writeUInt16BE(32, 8); // Key Exchange Length
|
|
269
|
+
rawPublicKey.copy(extension, 10);
|
|
270
|
+
|
|
271
|
+
return extension;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Wrap data in TLS Application Data record
|
|
276
|
+
*/
|
|
277
|
+
wrapApplicationData(data) {
|
|
278
|
+
// TLS Record Header
|
|
279
|
+
const recordHeader = Buffer.allocUnsafe(5);
|
|
280
|
+
recordHeader.writeUInt8(0x17, 0); // Content Type: Application Data
|
|
281
|
+
recordHeader.writeUInt16BE(0x0303, 1); // Version: TLS 1.2
|
|
282
|
+
recordHeader.writeUInt16BE(data.length, 3); // Length
|
|
283
|
+
|
|
284
|
+
return Buffer.concat([recordHeader, data]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Camouflage VPN data
|
|
289
|
+
*/
|
|
290
|
+
camouflage(data, options = {}) {
|
|
291
|
+
if (!this.enabled) {
|
|
292
|
+
return data;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
switch (this.mode) {
|
|
296
|
+
case 'basic':
|
|
297
|
+
// Just wrap in TLS record
|
|
298
|
+
return this.wrapApplicationData(data);
|
|
299
|
+
|
|
300
|
+
case 'full':
|
|
301
|
+
// Add realistic TLS patterns
|
|
302
|
+
if (options.isHandshake) {
|
|
303
|
+
return this.buildClientHello(options);
|
|
304
|
+
}
|
|
305
|
+
return this.wrapApplicationData(data);
|
|
306
|
+
|
|
307
|
+
case 'nested':
|
|
308
|
+
// Real TLS tunnel with nested VPN encryption
|
|
309
|
+
// This would require actual TLS implementation
|
|
310
|
+
// For now, use full mode
|
|
311
|
+
return this.wrapApplicationData(data);
|
|
312
|
+
|
|
313
|
+
default:
|
|
314
|
+
return data;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Extract data from TLS record
|
|
320
|
+
*/
|
|
321
|
+
extract(tlsRecord) {
|
|
322
|
+
if (!this.enabled || tlsRecord.length < 5) {
|
|
323
|
+
return tlsRecord;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if this is a TLS record
|
|
327
|
+
const contentType = tlsRecord.readUInt8(0);
|
|
328
|
+
|
|
329
|
+
if (contentType !== 0x17) { // Application Data
|
|
330
|
+
return tlsRecord;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Extract payload (skip 5-byte header)
|
|
334
|
+
return tlsRecord.slice(5);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Enable camouflage
|
|
339
|
+
*/
|
|
340
|
+
enable() {
|
|
341
|
+
this.enabled = true;
|
|
342
|
+
this.logger.info('TLS camouflage enabled');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Disable camouflage
|
|
347
|
+
*/
|
|
348
|
+
disable() {
|
|
349
|
+
this.enabled = false;
|
|
350
|
+
this.logger.info('TLS camouflage disabled');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get statistics
|
|
355
|
+
*/
|
|
356
|
+
getStats() {
|
|
357
|
+
return {
|
|
358
|
+
enabled: this.enabled,
|
|
359
|
+
mode: this.mode,
|
|
360
|
+
serverName: this.serverName,
|
|
361
|
+
alpnProtocols: this.alpnProtocols
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = TLSCamouflage;
|
|
367
|
+
module.exports.CIPHER_SUITES = CIPHER_SUITES;
|
|
368
|
+
module.exports.EXTENSIONS = EXTENSIONS;
|
|
369
|
+
module.exports.ALPN_PROTOCOLS = ALPN_PROTOCOLS;
|