From 5e18126c8185b3a6aa5e869ec629a8edcb34a2c9 Mon Sep 17 00:00:00 2001 From: 8odream Date: Wed, 19 Aug 2026 17:41:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Ddns=E5=9B=9E=E4=BC=A0?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lib/proxy/mitmproxy/dnsLookup.js | 56 ++++++---- .../mitmproxy/test/dnsDefaultLookupTest.mjs | 103 ++++++++++++++++++ 2 files changed, 136 insertions(+), 23 deletions(-) create mode 100644 packages/mitmproxy/test/dnsDefaultLookupTest.mjs diff --git a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js index 456f5b024a..b621da5f69 100644 --- a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js +++ b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js @@ -205,31 +205,41 @@ module.exports = { return (hostname, options, callback) => { defaultDns.lookup(hostname, options, (err, address, family) => { - if (!err) { - markBlockedIfAllZero(hostname, address) - if (Array.isArray(address)) { - address = address.map((item) => { - const newIp = rewriteCloudflareIp(hostname, item.address) - return { address: newIp, family: getAddressFamily(newIp) } - }) - } else { - address = rewriteCloudflareIp(hostname, address) - family = getAddressFamily(address) - } - const first = Array.isArray(address) ? address[0] : { address, family } - const ip = first && first.address - const fam = first && first.family - if (ip && isValidIpAddress(ip)) { - log.info(`----- ${action}: ${hostname}, use default DNS: ${ip}(family: ${fam})${target} -----`) - if (res) { - res.setHeader('DS-DNS', `default: ${ip} (IPv${fam})`) - } - } - callback(null, ip, family) - } else { + if (err) { log.error(`----- ${action}: ${hostname}, default dns lookup error${target}, error:`, err) + callback(err, address, family) + return + } + markBlockedIfAllZero(hostname, address) + if (Array.isArray(address)) { + address = address.map((item) => { + const newIp = rewriteCloudflareIp(hostname, item.address) + return { address: newIp, family: getAddressFamily(newIp) } + }) + } else { + address = rewriteCloudflareIp(hostname, address) + family = getAddressFamily(address) + } + const first = Array.isArray(address) ? address[0] : { address, family } + const ip = first && first.address + const fam = first && first.family + if (ip && isValidIpAddress(ip)) { + log.info(`----- ${action}: ${hostname}, use default DNS: ${ip}(family: ${fam})${target} -----`) + if (res) { + res.setHeader('DS-DNS', `default: ${ip} (IPv${fam})`) + } + } + // Node 20+ 默认开启 autoSelectFamily,会以 { all: true } 调用自定义 lookup, + // 此时必须回传完整的地址数组;只回传单个 IP 会被 Node 当作数组解析, + // 导致 ERR_INVALID_IP_ADDRESS: Invalid IP address: undefined + if (Array.isArray(address) && address.length === 0) { + // 防御:all 模式下解析结果为空时,按 ENOTFOUND 处理,避免回传空数组 + const notFound = new Error(`getaddrinfo ENOTFOUND ${hostname}`) + notFound.code = 'ENOTFOUND' + callback(notFound) + return } - callback(err, address, family) + callback(null, Array.isArray(address) ? address : ip, family) }) } }, diff --git a/packages/mitmproxy/test/dnsDefaultLookupTest.mjs b/packages/mitmproxy/test/dnsDefaultLookupTest.mjs new file mode 100644 index 0000000000..709822db8b --- /dev/null +++ b/packages/mitmproxy/test/dnsDefaultLookupTest.mjs @@ -0,0 +1,103 @@ +import assert from 'node:assert/strict' +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) + +// 回归测试:Node 20+ 默认开启 autoSelectFamily,会以 { all: true } 调用自定义 lookup, +// 期望回传完整地址数组。旧实现只回传单个 IP 字符串,导致 +// ERR_INVALID_IP_ADDRESS: Invalid IP address: undefined。 +const dnsModulePath = require.resolve('node:dns') +const dnsLookupModulePath = require.resolve('../src/lib/proxy/mitmproxy/dnsLookup.js') + +const IPV6 = '2603:1030:a07:e::102' +const IPV4 = '20.202.1.1' + +function mockDnsLookup (dnsModule) { + const originalLookup = dnsModule.lookup + dnsModule.lookup = (hostname, options, callback) => { + if (options && options.all === true) { + callback(null, [ + { address: IPV6, family: 6 }, + { address: IPV4, family: 4 }, + ]) + } else { + callback(null, IPV4, 4) + } + } + return () => { + dnsModule.lookup = originalLookup + } +} + +function lookupOnce (lookup, options) { + return new Promise((resolve, reject) => { + let callCount = 0 + lookup('example.com', options, (err, address, family) => { + callCount++ + if (callCount > 1) { + reject(new Error('lookup callback 被重复调用')) + return + } + if (err) { + reject(err) + return + } + resolve({ address, family, callCount }) + }) + // 回调是同步触发的,这里兜底防止异步时无限等待 + setTimeout(() => reject(new Error('lookup 回调超时')), 2000) + }) +} + +async function main () { + const dnsModule = require('node:dns') + const restoreDns = mockDnsLookup(dnsModule) + + try { + delete require.cache[dnsLookupModulePath] + const dnsLookup = require(dnsLookupModulePath) + const lookup = dnsLookup.createDefaultLookupFunc(null, 'test', 'target') + + // 1. all: true 时必须回传完整地址数组(修复前的关键回归点) + const allResult = await lookupOnce(lookup, { all: true }) + assert.deepStrictEqual(allResult.address, [ + { address: IPV6, family: 6 }, + { address: IPV4, family: 4 }, + ]) + assert.strictEqual(allResult.callCount, 1) + + // 2. 非 all 模式仍回传单个 IP + family + const singleResult = await lookupOnce(lookup, { family: 4 }) + assert.strictEqual(singleResult.address, IPV4) + assert.strictEqual(singleResult.family, 4) + assert.strictEqual(singleResult.callCount, 1) + + // 3. all 模式解析为空数组时按 ENOTFOUND 处理,不回传空数组 + dnsModule.lookup = (hostname, options, callback) => { + callback(null, []) + } + delete require.cache[dnsLookupModulePath] + const dnsLookup2 = require(dnsLookupModulePath) + const lookup2 = dnsLookup2.createDefaultLookupFunc(null, 'test', 'target') + const emptyResult = await new Promise((resolve) => { + let settled = false + lookup2('example.com', { all: true }, (err, address) => { + if (settled) return + settled = true + resolve({ err, address }) + }) + }) + assert.strictEqual(emptyResult.err.code, 'ENOTFOUND') + assert.strictEqual(emptyResult.address, undefined) + } finally { + restoreDns() + delete require.cache[dnsLookupModulePath] + } + + console.log('dnsDefaultLookupTest passed') +} + +main().catch((error) => { + console.error(error) + process.exit(1) +})