@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,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Yaksha Protocol Handler
|
|
5
|
+
* Custom protocol for packet parsing and handling
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { globalPool } = require('../utils/buffer-pool');
|
|
10
|
+
|
|
11
|
+
// Packet types
|
|
12
|
+
const PACKET_TYPES = {
|
|
13
|
+
HANDSHAKE: 0x01,
|
|
14
|
+
DATA: 0x02,
|
|
15
|
+
ACK: 0x03,
|
|
16
|
+
CLOSE: 0x04,
|
|
17
|
+
KEEPALIVE: 0x05
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Protocol version
|
|
21
|
+
const PROTOCOL_VERSION = 0x01;
|
|
22
|
+
|
|
23
|
+
// Header size (fixed 16 bytes)
|
|
24
|
+
const HEADER_SIZE = 16;
|
|
25
|
+
|
|
26
|
+
class Protocol {
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.version = PROTOCOL_VERSION;
|
|
29
|
+
this.maxPayloadSize = options.maxPayloadSize || 65535;
|
|
30
|
+
this.enablePadding = options.enablePadding !== false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a packet header
|
|
35
|
+
* Header structure (16 bytes):
|
|
36
|
+
* - Version (1 byte)
|
|
37
|
+
* - Type (1 byte)
|
|
38
|
+
* - Length (2 bytes) - payload length
|
|
39
|
+
* - Session ID (4 bytes)
|
|
40
|
+
* - Sequence (4 bytes)
|
|
41
|
+
* - Checksum (4 bytes)
|
|
42
|
+
*/
|
|
43
|
+
createHeader(type, payloadLength, sessionId, sequence) {
|
|
44
|
+
const header = globalPool.acquire(HEADER_SIZE, true);
|
|
45
|
+
|
|
46
|
+
// Version
|
|
47
|
+
header.writeUInt8(this.version, 0);
|
|
48
|
+
|
|
49
|
+
// Type
|
|
50
|
+
header.writeUInt8(type, 1);
|
|
51
|
+
|
|
52
|
+
// Length (uint16, max 65535)
|
|
53
|
+
header.writeUInt16BE(payloadLength, 2);
|
|
54
|
+
|
|
55
|
+
// Session ID (uint32)
|
|
56
|
+
header.writeUInt32BE(sessionId, 4);
|
|
57
|
+
|
|
58
|
+
// Sequence (uint32)
|
|
59
|
+
header.writeUInt32BE(sequence, 8);
|
|
60
|
+
|
|
61
|
+
// Checksum placeholder (will be filled later)
|
|
62
|
+
header.writeUInt32BE(0, 12);
|
|
63
|
+
|
|
64
|
+
return header;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate CRC32 checksum
|
|
69
|
+
*/
|
|
70
|
+
calculateChecksum(data) {
|
|
71
|
+
// Simple CRC32 implementation
|
|
72
|
+
let crc = 0xFFFFFFFF;
|
|
73
|
+
for (let i = 0; i < data.length; i++) {
|
|
74
|
+
crc ^= data[i];
|
|
75
|
+
for (let j = 0; j < 8; j++) {
|
|
76
|
+
crc = (crc >>> 1) ^ (0xEDB88320 & -(crc & 1));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add random padding for traffic obfuscation
|
|
84
|
+
*/
|
|
85
|
+
addPadding(data, maxPaddingSize = 255) {
|
|
86
|
+
if (!this.enablePadding) {
|
|
87
|
+
return { data, paddingSize: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const paddingSize = crypto.randomInt(0, Math.min(maxPaddingSize + 1, 256));
|
|
91
|
+
if (paddingSize === 0) {
|
|
92
|
+
return { data, paddingSize: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const padding = crypto.randomBytes(paddingSize);
|
|
96
|
+
const paddedData = Buffer.concat([data, padding]);
|
|
97
|
+
|
|
98
|
+
return { data: paddedData, paddingSize };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Remove padding from data
|
|
103
|
+
*/
|
|
104
|
+
removePadding(data, paddingSize) {
|
|
105
|
+
if (paddingSize === 0) {
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
return data.slice(0, -paddingSize);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Serialize packet
|
|
113
|
+
*/
|
|
114
|
+
serialize(type, payload, sessionId, sequence) {
|
|
115
|
+
// Add padding to payload
|
|
116
|
+
const { data: paddedPayload, paddingSize } = this.addPadding(payload);
|
|
117
|
+
|
|
118
|
+
// Create header
|
|
119
|
+
const header = this.createHeader(type, paddedPayload.length, sessionId, sequence);
|
|
120
|
+
|
|
121
|
+
// Combine header and payload
|
|
122
|
+
const packet = Buffer.concat([header, paddedPayload]);
|
|
123
|
+
|
|
124
|
+
// Calculate and update checksum (checksum covers header + payload)
|
|
125
|
+
const checksum = this.calculateChecksum(packet);
|
|
126
|
+
packet.writeUInt32BE(checksum, 12);
|
|
127
|
+
|
|
128
|
+
return { packet, paddingSize };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse packet header
|
|
133
|
+
*/
|
|
134
|
+
parseHeader(buffer) {
|
|
135
|
+
if (buffer.length < HEADER_SIZE) {
|
|
136
|
+
throw new Error('Buffer too small for header');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const version = buffer.readUInt8(0);
|
|
140
|
+
const type = buffer.readUInt8(1);
|
|
141
|
+
const length = buffer.readUInt16BE(2);
|
|
142
|
+
const sessionId = buffer.readUInt32BE(4);
|
|
143
|
+
const sequence = buffer.readUInt32BE(8);
|
|
144
|
+
const checksum = buffer.readUInt32BE(12);
|
|
145
|
+
|
|
146
|
+
// Validate version
|
|
147
|
+
if (version !== this.version) {
|
|
148
|
+
throw new Error(`Unsupported protocol version: ${version}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate type
|
|
152
|
+
if (!Object.values(PACKET_TYPES).includes(type)) {
|
|
153
|
+
throw new Error(`Invalid packet type: ${type}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate length
|
|
157
|
+
if (length > this.maxPayloadSize) {
|
|
158
|
+
throw new Error(`Payload too large: ${length}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
version,
|
|
163
|
+
type,
|
|
164
|
+
length,
|
|
165
|
+
sessionId,
|
|
166
|
+
sequence,
|
|
167
|
+
checksum
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deserialize packet
|
|
173
|
+
*/
|
|
174
|
+
deserialize(buffer, paddingSize = 0) {
|
|
175
|
+
// Parse header
|
|
176
|
+
const header = this.parseHeader(buffer);
|
|
177
|
+
|
|
178
|
+
// Check if we have complete packet
|
|
179
|
+
const expectedSize = HEADER_SIZE + header.length;
|
|
180
|
+
if (buffer.length < expectedSize) {
|
|
181
|
+
throw new Error(`Incomplete packet: expected ${expectedSize}, got ${buffer.length}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Verify checksum
|
|
185
|
+
const storedChecksum = header.checksum;
|
|
186
|
+
|
|
187
|
+
// Create a copy of packet and zero out checksum field for verification
|
|
188
|
+
const packetCopy = Buffer.from(buffer.slice(0, expectedSize));
|
|
189
|
+
packetCopy.writeUInt32BE(0, 12);
|
|
190
|
+
|
|
191
|
+
const calculatedChecksum = this.calculateChecksum(packetCopy);
|
|
192
|
+
|
|
193
|
+
if (storedChecksum !== calculatedChecksum) {
|
|
194
|
+
throw new Error('Checksum verification failed');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Extract payload
|
|
198
|
+
let payload = buffer.slice(HEADER_SIZE, expectedSize);
|
|
199
|
+
|
|
200
|
+
// Remove padding if present
|
|
201
|
+
payload = this.removePadding(payload, paddingSize);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
header,
|
|
205
|
+
payload,
|
|
206
|
+
totalSize: expectedSize
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create HANDSHAKE packet
|
|
212
|
+
*/
|
|
213
|
+
createHandshake(sessionId, sequence, data) {
|
|
214
|
+
return this.serialize(PACKET_TYPES.HANDSHAKE, data, sessionId, sequence);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create DATA packet
|
|
219
|
+
*/
|
|
220
|
+
createData(sessionId, sequence, data) {
|
|
221
|
+
return this.serialize(PACKET_TYPES.DATA, data, sessionId, sequence);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create ACK packet
|
|
226
|
+
*/
|
|
227
|
+
createAck(sessionId, sequence, ackSequence) {
|
|
228
|
+
const ackBuffer = Buffer.allocUnsafe(4);
|
|
229
|
+
ackBuffer.writeUInt32BE(ackSequence, 0);
|
|
230
|
+
return this.serialize(PACKET_TYPES.ACK, ackBuffer, sessionId, sequence);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create CLOSE packet
|
|
235
|
+
*/
|
|
236
|
+
createClose(sessionId, sequence, reason = '') {
|
|
237
|
+
const reasonBuffer = Buffer.from(reason, 'utf8');
|
|
238
|
+
return this.serialize(PACKET_TYPES.CLOSE, reasonBuffer, sessionId, sequence);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create KEEPALIVE packet
|
|
243
|
+
*/
|
|
244
|
+
createKeepalive(sessionId, sequence) {
|
|
245
|
+
const timestamp = Buffer.allocUnsafe(8);
|
|
246
|
+
timestamp.writeBigUInt64BE(BigInt(Date.now()), 0);
|
|
247
|
+
return this.serialize(PACKET_TYPES.KEEPALIVE, timestamp, sessionId, sequence);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get packet type name
|
|
252
|
+
*/
|
|
253
|
+
getTypeName(type) {
|
|
254
|
+
const typeNames = {
|
|
255
|
+
[PACKET_TYPES.HANDSHAKE]: 'HANDSHAKE',
|
|
256
|
+
[PACKET_TYPES.DATA]: 'DATA',
|
|
257
|
+
[PACKET_TYPES.ACK]: 'ACK',
|
|
258
|
+
[PACKET_TYPES.CLOSE]: 'CLOSE',
|
|
259
|
+
[PACKET_TYPES.KEEPALIVE]: 'KEEPALIVE'
|
|
260
|
+
};
|
|
261
|
+
return typeNames[type] || 'UNKNOWN';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = Protocol;
|
|
266
|
+
module.exports.PACKET_TYPES = PACKET_TYPES;
|
|
267
|
+
module.exports.HEADER_SIZE = HEADER_SIZE;
|
|
268
|
+
module.exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Yaksha DNS Override and Tunneling
|
|
5
|
+
* Prevent DNS leaks by tunneling all DNS queries through VPN
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const dns = require('dns');
|
|
9
|
+
const dgram = require('dgram');
|
|
10
|
+
const EventEmitter = require('events');
|
|
11
|
+
const Logger = require('../utils/logger');
|
|
12
|
+
|
|
13
|
+
// DNS providers
|
|
14
|
+
const DNS_PROVIDERS = {
|
|
15
|
+
CLOUDFLARE: { primary: '1.1.1.1', secondary: '1.0.0.1', doh: 'https://cloudflare-dns.com/dns-query' },
|
|
16
|
+
GOOGLE: { primary: '8.8.8.8', secondary: '8.8.4.4', doh: 'https://dns.google/dns-query' },
|
|
17
|
+
QUAD9: { primary: '9.9.9.9', secondary: '149.112.112.112', doh: 'https://dns.quad9.net/dns-query' },
|
|
18
|
+
OPENDNS: { primary: '208.67.222.222', secondary: '208.67.220.220' }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class DNSOverride extends EventEmitter {
|
|
22
|
+
constructor(securityLevel = 'medium', options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
this.securityLevel = securityLevel;
|
|
26
|
+
this.mode = this._selectMode(securityLevel);
|
|
27
|
+
this.provider = options.provider || 'CLOUDFLARE';
|
|
28
|
+
this.customResolver = options.customResolver || null;
|
|
29
|
+
this.enabled = options.enabled !== false;
|
|
30
|
+
this.cacheTTL = options.cacheTTL || 300000; // 5 minutes
|
|
31
|
+
|
|
32
|
+
this.cache = new Map(); // domain -> { records, timestamp }
|
|
33
|
+
this.logger = new Logger({ prefix: 'DNS' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Select DNS mode based on security level
|
|
38
|
+
*/
|
|
39
|
+
_selectMode(level) {
|
|
40
|
+
switch (level) {
|
|
41
|
+
case 'low':
|
|
42
|
+
return 'override';
|
|
43
|
+
case 'medium':
|
|
44
|
+
return 'doh';
|
|
45
|
+
case 'high':
|
|
46
|
+
return 'dot-dnssec';
|
|
47
|
+
default:
|
|
48
|
+
return 'doh';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get DNS provider configuration
|
|
54
|
+
*/
|
|
55
|
+
_getProvider() {
|
|
56
|
+
if (this.customResolver) {
|
|
57
|
+
return this.customResolver;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return DNS_PROVIDERS[this.provider] || DNS_PROVIDERS.CLOUDFLARE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check DNS cache
|
|
65
|
+
*/
|
|
66
|
+
_checkCache(domain) {
|
|
67
|
+
if (!this.cache.has(domain)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cached = this.cache.get(domain);
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
|
|
74
|
+
// Check if cache expired
|
|
75
|
+
if (now - cached.timestamp > this.cacheTTL) {
|
|
76
|
+
this.cache.delete(domain);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return cached.records;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Update DNS cache
|
|
85
|
+
*/
|
|
86
|
+
_updateCache(domain, records) {
|
|
87
|
+
this.cache.set(domain, {
|
|
88
|
+
records,
|
|
89
|
+
timestamp: Date.now()
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Basic DNS override (replace system DNS)
|
|
95
|
+
*/
|
|
96
|
+
async _resolveOverride(domain, type = 'A') {
|
|
97
|
+
const provider = this._getProvider();
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const resolver = new dns.Resolver();
|
|
101
|
+
resolver.setServers([provider.primary, provider.secondary]);
|
|
102
|
+
|
|
103
|
+
const method = type === 'A' ? 'resolve4' :
|
|
104
|
+
type === 'AAAA' ? 'resolve6' :
|
|
105
|
+
type === 'MX' ? 'resolveMx' :
|
|
106
|
+
type === 'TXT' ? 'resolveTxt' :
|
|
107
|
+
'resolve4';
|
|
108
|
+
|
|
109
|
+
resolver[method](domain, (error, records) => {
|
|
110
|
+
if (error) {
|
|
111
|
+
reject(error);
|
|
112
|
+
} else {
|
|
113
|
+
resolve(records);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* DNS over HTTPS (DoH)
|
|
121
|
+
*/
|
|
122
|
+
async _resolveDoH(domain, type = 'A') {
|
|
123
|
+
const provider = this._getProvider();
|
|
124
|
+
|
|
125
|
+
if (!provider.doh) {
|
|
126
|
+
throw new Error('Provider does not support DoH');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const https = require('https');
|
|
130
|
+
const url = `${provider.doh}?name=${encodeURIComponent(domain)}&type=${type}`;
|
|
131
|
+
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
https.get(url, {
|
|
134
|
+
headers: {
|
|
135
|
+
'Accept': 'application/dns-json'
|
|
136
|
+
}
|
|
137
|
+
}, (res) => {
|
|
138
|
+
let data = '';
|
|
139
|
+
|
|
140
|
+
res.on('data', chunk => {
|
|
141
|
+
data += chunk;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
res.on('end', () => {
|
|
145
|
+
try {
|
|
146
|
+
const response = JSON.parse(data);
|
|
147
|
+
|
|
148
|
+
if (response.Status !== 0) {
|
|
149
|
+
reject(new Error(`DNS query failed: ${response.Status}`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const records = response.Answer
|
|
154
|
+
? response.Answer.map(answer => answer.data)
|
|
155
|
+
: [];
|
|
156
|
+
|
|
157
|
+
resolve(records);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
reject(error);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}).on('error', reject);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* DNS over TLS (DoT) with DNSSEC
|
|
168
|
+
*/
|
|
169
|
+
async _resolveDoT(domain, type = 'A') {
|
|
170
|
+
// Simplified DoT implementation
|
|
171
|
+
// In production, use a proper DoT library
|
|
172
|
+
|
|
173
|
+
const tls = require('tls');
|
|
174
|
+
const provider = this._getProvider();
|
|
175
|
+
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const socket = tls.connect({
|
|
178
|
+
host: provider.primary,
|
|
179
|
+
port: 853,
|
|
180
|
+
servername: provider.primary
|
|
181
|
+
}, () => {
|
|
182
|
+
// Build DNS query packet (simplified)
|
|
183
|
+
// In production, use proper DNS packet formatting
|
|
184
|
+
|
|
185
|
+
const query = Buffer.from([
|
|
186
|
+
0x00, 0x1c, // Length
|
|
187
|
+
0xaa, 0xaa, // Transaction ID
|
|
188
|
+
0x01, 0x00, // Flags: standard query
|
|
189
|
+
0x00, 0x01, // Questions: 1
|
|
190
|
+
0x00, 0x00, // Answer RRs
|
|
191
|
+
0x00, 0x00, // Authority RRs
|
|
192
|
+
0x00, 0x00, // Additional RRs
|
|
193
|
+
// Question section would follow
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
socket.write(query);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
socket.on('data', (data) => {
|
|
200
|
+
// Parse DNS response (simplified)
|
|
201
|
+
// For this implementation, fall back to DoH
|
|
202
|
+
socket.end();
|
|
203
|
+
resolve(this._resolveDoH(domain, type));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
socket.on('error', reject);
|
|
207
|
+
|
|
208
|
+
socket.setTimeout(5000);
|
|
209
|
+
socket.on('timeout', () => {
|
|
210
|
+
socket.destroy();
|
|
211
|
+
reject(new Error('DNS query timeout'));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve domain name
|
|
218
|
+
*/
|
|
219
|
+
async resolve(domain, type = 'A') {
|
|
220
|
+
if (!this.enabled) {
|
|
221
|
+
// Use system DNS
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
dns.resolve(domain, type, (error, records) => {
|
|
224
|
+
if (error) reject(error);
|
|
225
|
+
else resolve(records);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check cache
|
|
231
|
+
const cacheKey = `${domain}:${type}`;
|
|
232
|
+
const cached = this._checkCache(cacheKey);
|
|
233
|
+
|
|
234
|
+
if (cached) {
|
|
235
|
+
this.logger.debug(`DNS cache hit for ${domain}`);
|
|
236
|
+
return cached;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.logger.debug(`Resolving ${domain} (${type}) via ${this.mode}`);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
let records;
|
|
243
|
+
|
|
244
|
+
switch (this.mode) {
|
|
245
|
+
case 'override':
|
|
246
|
+
records = await this._resolveOverride(domain, type);
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case 'doh':
|
|
250
|
+
records = await this._resolveDoH(domain, type);
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'dot-dnssec':
|
|
254
|
+
records = await this._resolveDoT(domain, type);
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
default:
|
|
258
|
+
records = await this._resolveDoH(domain, type);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Update cache
|
|
262
|
+
this._updateCache(cacheKey, records);
|
|
263
|
+
|
|
264
|
+
this.emit('resolved', domain, type, records);
|
|
265
|
+
return records;
|
|
266
|
+
|
|
267
|
+
} catch (error) {
|
|
268
|
+
this.logger.error(`DNS resolution failed for ${domain}:`, error.message);
|
|
269
|
+
this.emit('error', domain, error);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Reverse DNS lookup
|
|
276
|
+
*/
|
|
277
|
+
async reverse(ip) {
|
|
278
|
+
if (!this.enabled) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
dns.reverse(ip, (error, hostnames) => {
|
|
281
|
+
if (error) reject(error);
|
|
282
|
+
else resolve(hostnames);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check cache
|
|
288
|
+
const cached = this._checkCache(`reverse:${ip}`);
|
|
289
|
+
if (cached) {
|
|
290
|
+
return cached;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const hostnames = await this._resolveDoH(ip, 'PTR');
|
|
295
|
+
this._updateCache(`reverse:${ip}`, hostnames);
|
|
296
|
+
return hostnames;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
this.logger.error(`Reverse DNS lookup failed for ${ip}:`, error.message);
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Lookup (resolve A or AAAA records)
|
|
305
|
+
*/
|
|
306
|
+
async lookup(domain, options = {}) {
|
|
307
|
+
const family = options.family || 4;
|
|
308
|
+
const type = family === 6 ? 'AAAA' : 'A';
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const records = await this.resolve(domain, type);
|
|
312
|
+
|
|
313
|
+
if (records.length === 0) {
|
|
314
|
+
throw new Error(`No ${type} records found for ${domain}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Return first record
|
|
318
|
+
return {
|
|
319
|
+
address: records[0],
|
|
320
|
+
family
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (family === 4) {
|
|
324
|
+
// Try IPv6 as fallback
|
|
325
|
+
try {
|
|
326
|
+
const ipv6Records = await this.resolve(domain, 'AAAA');
|
|
327
|
+
if (ipv6Records.length > 0) {
|
|
328
|
+
return {
|
|
329
|
+
address: ipv6Records[0],
|
|
330
|
+
family: 6
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
} catch (ipv6Error) {
|
|
334
|
+
// Ignore IPv6 error
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Clear DNS cache
|
|
343
|
+
*/
|
|
344
|
+
clearCache() {
|
|
345
|
+
this.cache.clear();
|
|
346
|
+
this.logger.info('DNS cache cleared');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get cache statistics
|
|
351
|
+
*/
|
|
352
|
+
getCacheStats() {
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
let valid = 0;
|
|
355
|
+
let expired = 0;
|
|
356
|
+
|
|
357
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
358
|
+
if (now - entry.timestamp > this.cacheTTL) {
|
|
359
|
+
expired++;
|
|
360
|
+
} else {
|
|
361
|
+
valid++;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
total: this.cache.size,
|
|
367
|
+
valid,
|
|
368
|
+
expired,
|
|
369
|
+
ttl: this.cacheTTL
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Enable DNS override
|
|
375
|
+
*/
|
|
376
|
+
enable() {
|
|
377
|
+
this.enabled = true;
|
|
378
|
+
this.logger.info('DNS override enabled');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Disable DNS override
|
|
383
|
+
*/
|
|
384
|
+
disable() {
|
|
385
|
+
this.enabled = false;
|
|
386
|
+
this.logger.info('DNS override disabled');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get statistics
|
|
391
|
+
*/
|
|
392
|
+
getStats() {
|
|
393
|
+
return {
|
|
394
|
+
enabled: this.enabled,
|
|
395
|
+
mode: this.mode,
|
|
396
|
+
provider: this.provider,
|
|
397
|
+
cache: this.getCacheStats()
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = DNSOverride;
|
|
403
|
+
module.exports.DNS_PROVIDERS = DNS_PROVIDERS;
|