|
1 | 1 | import { expect } from 'chai'; |
2 | 2 | import { describe, it, beforeEach, afterEach } from 'mocha'; |
| 3 | +import dnsPacket from 'dns-packet'; |
3 | 4 |
|
4 | 5 | import $ from '@/core/app'; |
5 | 6 | import PROCESSORS, { ApplyProcessor } from '@/core/proxy-utils/processors'; |
6 | 7 | import { SETTINGS_KEY } from '@/constants'; |
7 | 8 | import resourceCache from '@/utils/resource-cache'; |
| 9 | +import { parseDnsResolver } from '@/utils/dns'; |
8 | 10 | import { hex_md5 } from '@/vendor/md5'; |
9 | 11 |
|
10 | 12 | const ResolveDomainOperator = PROCESSORS['Resolve Domain Operator']; |
| 13 | +const dgram = eval("require('dgram')"); |
| 14 | +const net = eval("require('net')"); |
11 | 15 |
|
12 | 16 | function sleep(ms) { |
13 | 17 | return new Promise((resolve) => setTimeout(resolve, ms)); |
14 | 18 | } |
15 | 19 |
|
| 20 | +function createDnsResponse(query, data = '192.0.2.55') { |
| 21 | + const question = query.questions[0]; |
| 22 | + return dnsPacket.encode({ |
| 23 | + type: 'response', |
| 24 | + id: query.id, |
| 25 | + flags: dnsPacket.RECURSION_DESIRED, |
| 26 | + questions: query.questions, |
| 27 | + answers: [ |
| 28 | + { |
| 29 | + type: question.type, |
| 30 | + class: 'IN', |
| 31 | + name: question.name, |
| 32 | + ttl: 60, |
| 33 | + data, |
| 34 | + }, |
| 35 | + ], |
| 36 | + }); |
| 37 | +} |
| 38 | + |
| 39 | +function getEdnsClientSubnet(query) { |
| 40 | + return query.additionals |
| 41 | + ?.find((item) => item.type === 'OPT') |
| 42 | + ?.options?.find( |
| 43 | + (option) => |
| 44 | + option.code === 'CLIENT_SUBNET' || option.code === 8, |
| 45 | + )?.ip; |
| 46 | +} |
| 47 | + |
| 48 | +function startUdpDnsServer(onQuery) { |
| 49 | + return new Promise((resolve, reject) => { |
| 50 | + const server = dgram.createSocket('udp4'); |
| 51 | + server.once('error', reject); |
| 52 | + server.on('message', (message, rinfo) => { |
| 53 | + const query = dnsPacket.decode(message); |
| 54 | + onQuery(query); |
| 55 | + server.send(createDnsResponse(query), rinfo.port, rinfo.address); |
| 56 | + }); |
| 57 | + server.bind(0, '127.0.0.1', () => { |
| 58 | + server.removeListener('error', reject); |
| 59 | + resolve({ |
| 60 | + host: '127.0.0.1', |
| 61 | + port: server.address().port, |
| 62 | + close: () => |
| 63 | + new Promise((resolveClose) => server.close(resolveClose)), |
| 64 | + }); |
| 65 | + }); |
| 66 | + }); |
| 67 | +} |
| 68 | + |
| 69 | +function startTcpDnsServer(onQuery) { |
| 70 | + return new Promise((resolve, reject) => { |
| 71 | + const server = net.createServer((socket) => { |
| 72 | + let request = Buffer.alloc(0); |
| 73 | + socket.on('data', (chunk) => { |
| 74 | + request = Buffer.concat([request, chunk]); |
| 75 | + if (request.length < 2) return; |
| 76 | + const requestLength = request.readUInt16BE(0); |
| 77 | + if (request.length < requestLength + 2) return; |
| 78 | + |
| 79 | + const query = dnsPacket.decode( |
| 80 | + request.slice(2, 2 + requestLength), |
| 81 | + ); |
| 82 | + onQuery(query); |
| 83 | + const response = createDnsResponse(query, '192.0.2.56'); |
| 84 | + const responseLength = Buffer.alloc(2); |
| 85 | + responseLength.writeUInt16BE(response.length, 0); |
| 86 | + socket.end(Buffer.concat([responseLength, response])); |
| 87 | + }); |
| 88 | + }); |
| 89 | + server.once('error', reject); |
| 90 | + server.listen(0, '127.0.0.1', () => { |
| 91 | + server.removeListener('error', reject); |
| 92 | + resolve({ |
| 93 | + host: '127.0.0.1', |
| 94 | + port: server.address().port, |
| 95 | + close: () => |
| 96 | + new Promise((resolveClose) => server.close(resolveClose)), |
| 97 | + }); |
| 98 | + }); |
| 99 | + }); |
| 100 | +} |
| 101 | + |
16 | 102 | describe('Resolve Domain Operator', function () { |
17 | 103 | let originalGoogleResolver; |
18 | 104 | let originalRead; |
@@ -248,4 +334,96 @@ describe('Resolve Domain Operator', function () { |
248 | 334 | $.warn = originalWarn; |
249 | 335 | } |
250 | 336 | }); |
| 337 | + |
| 338 | + it('parses Custom DNS resolver formats', function () { |
| 339 | + expect(parseDnsResolver('https://1.1.1.1/dns-query')).to.deep.equal({ |
| 340 | + protocol: 'doh', |
| 341 | + url: 'https://1.1.1.1/dns-query', |
| 342 | + }); |
| 343 | + expect(parseDnsResolver('1.1.1.1:5353')).to.deep.equal({ |
| 344 | + protocol: 'udp', |
| 345 | + host: '1.1.1.1', |
| 346 | + port: 5353, |
| 347 | + }); |
| 348 | + expect(parseDnsResolver('1.1.1.1')).to.deep.equal({ |
| 349 | + protocol: 'udp', |
| 350 | + host: '1.1.1.1', |
| 351 | + port: 53, |
| 352 | + }); |
| 353 | + expect(parseDnsResolver('udp://1.1.1.1')).to.deep.equal({ |
| 354 | + protocol: 'udp', |
| 355 | + host: '1.1.1.1', |
| 356 | + port: 53, |
| 357 | + }); |
| 358 | + expect(parseDnsResolver('tcp://1.1.1.1')).to.deep.equal({ |
| 359 | + protocol: 'tcp', |
| 360 | + host: '1.1.1.1', |
| 361 | + port: 53, |
| 362 | + }); |
| 363 | + expect(parseDnsResolver('tcp://1.1.1.1:5353')).to.deep.equal({ |
| 364 | + protocol: 'tcp', |
| 365 | + host: '1.1.1.1', |
| 366 | + port: 5353, |
| 367 | + }); |
| 368 | + }); |
| 369 | + |
| 370 | + it('resolves Custom UDP DNS and sends EDNS', async function () { |
| 371 | + const queries = []; |
| 372 | + const server = await startUdpDnsServer((query) => queries.push(query)); |
| 373 | + const url = `${server.host}:${server.port}`; |
| 374 | + cacheKeys.push(hex_md5(`CUSTOM:${url}:udp.example.com:IPv4`)); |
| 375 | + |
| 376 | + try { |
| 377 | + const processor = ResolveDomainOperator({ |
| 378 | + provider: 'Custom', |
| 379 | + type: 'IPv4', |
| 380 | + url, |
| 381 | + edns: '198.51.100.1', |
| 382 | + cache: 'disabled', |
| 383 | + }); |
| 384 | + const output = await ApplyProcessor(processor, [ |
| 385 | + { name: 'UDP Custom', server: 'udp.example.com', port: 443 }, |
| 386 | + ]); |
| 387 | + |
| 388 | + expect(output[0].server).to.equal('192.0.2.55'); |
| 389 | + expect(queries).to.have.length(1); |
| 390 | + expect(queries[0].questions[0]).to.include({ |
| 391 | + type: 'A', |
| 392 | + name: 'udp.example.com', |
| 393 | + }); |
| 394 | + expect(getEdnsClientSubnet(queries[0])).to.equal('198.51.100.0'); |
| 395 | + } finally { |
| 396 | + await server.close(); |
| 397 | + } |
| 398 | + }); |
| 399 | + |
| 400 | + it('resolves Custom TCP DNS and sends EDNS', async function () { |
| 401 | + const queries = []; |
| 402 | + const server = await startTcpDnsServer((query) => queries.push(query)); |
| 403 | + const url = `tcp://${server.host}:${server.port}`; |
| 404 | + cacheKeys.push(hex_md5(`CUSTOM:${url}:tcp.example.com:IPv4`)); |
| 405 | + |
| 406 | + try { |
| 407 | + const processor = ResolveDomainOperator({ |
| 408 | + provider: 'Custom', |
| 409 | + type: 'IPv4', |
| 410 | + url, |
| 411 | + edns: '198.51.101.2', |
| 412 | + cache: 'disabled', |
| 413 | + }); |
| 414 | + const output = await ApplyProcessor(processor, [ |
| 415 | + { name: 'TCP Custom', server: 'tcp.example.com', port: 443 }, |
| 416 | + ]); |
| 417 | + |
| 418 | + expect(output[0].server).to.equal('192.0.2.56'); |
| 419 | + expect(queries).to.have.length(1); |
| 420 | + expect(queries[0].questions[0]).to.include({ |
| 421 | + type: 'A', |
| 422 | + name: 'tcp.example.com', |
| 423 | + }); |
| 424 | + expect(getEdnsClientSubnet(queries[0])).to.equal('198.51.101.0'); |
| 425 | + } finally { |
| 426 | + await server.close(); |
| 427 | + } |
| 428 | + }); |
251 | 429 | }); |
0 commit comments