diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index c9e8024..b2b0051 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -393,6 +393,7 @@ const defaultConfig = { type: 'https', server: 'https://doh.360.cn/dns-query', cacheSize: 1000, + forSNI: true, }, rubyfish: { type: 'https', diff --git a/packages/core/src/utils/util.version.js b/packages/core/src/utils/util.version.js index 12a0b8f..265ef34 100644 --- a/packages/core/src/utils/util.version.js +++ b/packages/core/src/utils/util.version.js @@ -14,7 +14,7 @@ function parseVersion (version) { * @param log 日志对象 * @returns {number} 比较线上版本号是否为更新的版本,大于0=是|0=相等|小于0=否|-999=出现异常,比较结果未知 */ -export function isNewVersion (onlineVersion, currentVersion, log = console) { +export function isNewVersion (onlineVersion, currentVersion, log = null) { if (onlineVersion === currentVersion) { return 0 } diff --git a/packages/gui/extra/icons/512x512-2.png b/packages/gui/extra/icons/512x512-2.png new file mode 100644 index 0000000..0702bbd Binary files /dev/null and b/packages/gui/extra/icons/512x512-2.png differ diff --git a/packages/gui/src/background.js b/packages/gui/src/background.js index 5be8601..b7d5593 100644 --- a/packages/gui/src/background.js +++ b/packages/gui/src/background.js @@ -410,7 +410,7 @@ function registerShowHideShortcut (showHideShortcut) { function initApp () { if (isMac) { app.whenReady().then(() => { - app.dock.setIcon(path.join(__dirname, '../extra/icons/512x512.png')) + app.dock.setIcon(path.join(__dirname, '../extra/icons/512x512-2.png')) }) } diff --git a/packages/gui/src/bridge/error/front.js b/packages/gui/src/bridge/error/front.js index ab1efbd..ef5d6e3 100644 --- a/packages/gui/src/bridge/error/front.js +++ b/packages/gui/src/bridge/error/front.js @@ -1,3 +1,5 @@ +let latestConfirmTime = null + function install (app, api) { api.ipc.on('error.core', (event, message) => { console.error('view on error', message) @@ -13,11 +15,17 @@ function install (app, api) { function handleServerStartError (message, err, app, api) { if (message.value === 'EADDRINUSE') { + // 避免重复弹窗 + const now = Date.now() + if (latestConfirmTime != null && now - latestConfirmTime < 1000) { + return + } + latestConfirmTime = now + app.$confirm({ title: '端口被占用,代理服务启动失败', content: '是否要杀掉占用进程?您也可以点击取消,然后前往加速服务->基本设置中修改代理端口', onOk () { - // TODO 杀掉进程 api.config.get().then((config) => { console.log('config:', config) api.shell.killByPort({ port: config.server.port }).then((ret) => { diff --git a/packages/gui/src/view/mixins/plugin.js b/packages/gui/src/view/mixins/plugin.js index 7142dd0..81797e2 100644 --- a/packages/gui/src/view/mixins/plugin.js +++ b/packages/gui/src/view/mixins/plugin.js @@ -142,6 +142,19 @@ export default { const dir = await this.$api.info.getLogDir() this.$api.ipc.openPath(dir) }, + async focusFirst (ref) { + if (ref && ref.length != null) { + setTimeout(() => { + if (ref.length > 0) { + try { + ref[0].$el.querySelector('.ant-input').focus() + } catch (e) { + console.error('获取输入框焦点失败:', e) + } + } + }, 100) + } + }, handleHostname (hostname) { if (this.isNotHostname(hostname)) { return '' diff --git a/packages/gui/src/view/pages/plugin/git.vue b/packages/gui/src/view/pages/plugin/git.vue index 9f65ba3..03b2518 100644 --- a/packages/gui/src/view/pages/plugin/git.vue +++ b/packages/gui/src/view/pages/plugin/git.vue @@ -50,6 +50,7 @@ export default { }, addNoProxyUrl () { this.noProxyUrls.unshift({ key: '' }) + this.focusFirst(this.$refs.noProxyUrls) }, delNoProxyUrl (item, index) { this.noProxyUrls.splice(index, 1) @@ -108,7 +109,7 @@ export default { - + diff --git a/packages/gui/src/view/pages/plugin/overwall.vue b/packages/gui/src/view/pages/plugin/overwall.vue index d9a1387..c69db7e 100644 --- a/packages/gui/src/view/pages/plugin/overwall.vue +++ b/packages/gui/src/view/pages/plugin/overwall.vue @@ -60,6 +60,7 @@ export default { }, addTarget () { this.targets.unshift({ key: '', value: 'true' }) + this.focusFirst(this.$refs.targets) }, deleteTarget (item, index) { this.targets.splice(index, 1) @@ -88,14 +89,17 @@ export default { }) } if (this.servers.length === 0) { - this.addServer() + this.addServer(false) } }, deleteServer (item, index) { this.servers.splice(index, 1) }, - addServer () { + addServer (needFocus = true) { this.servers.unshift({ key: '', value: { type: 'path' } }) + if (needFocus) { + this.focusFirst(this.$refs.servers) + } }, submitServer () { const map = {} @@ -169,7 +173,7 @@ export default { - + @@ -196,7 +200,7 @@ export default { - + diff --git a/packages/gui/src/view/pages/proxy.vue b/packages/gui/src/view/pages/proxy.vue index 02d2f2f..b720573 100644 --- a/packages/gui/src/view/pages/proxy.vue +++ b/packages/gui/src/view/pages/proxy.vue @@ -62,6 +62,7 @@ export default { }, addExcludeIp () { this.excludeIpList.unshift({ key: '', value: 'true' }) + this.focusFirst(this.$refs.excludeIpList) }, delExcludeIp (item, index) { this.excludeIpList.splice(index, 1) @@ -166,7 +167,7 @@ export default { - + diff --git a/packages/gui/src/view/pages/server.vue b/packages/gui/src/view/pages/server.vue index 7ceb64f..96b2a5f 100644 --- a/packages/gui/src/view/pages/server.vue +++ b/packages/gui/src/view/pages/server.vue @@ -108,6 +108,7 @@ export default { }, addDnsMapping () { this.dnsMappings.unshift({ key: '', value: 'quad9' }) + this.focusFirst(this.$refs.dnsMappings) }, // whiteList @@ -123,6 +124,7 @@ export default { }, addWhiteList () { this.whiteList.unshift({ key: '', value: 'true' }) + this.focusFirst(this.$refs.whiteList) }, deleteWhiteList (item, index) { this.whiteList.splice(index, 1) @@ -144,6 +146,7 @@ export default { }, addSpeedHostname () { this.getSpeedTestConfig().hostnameList.unshift('') + this.focusFirst(this.$refs.hostnameList) }, delSpeedHostname (item, index) { this.getSpeedTestConfig().hostnameList.splice(index, 1) @@ -310,13 +313,13 @@ export default {
-
这里配置的域名不会通过代理
+
配置为不代理的域名不会通过代理
- + @@ -374,7 +377,7 @@ export default { - + @@ -422,7 +425,7 @@ export default { - + @@ -452,9 +455,9 @@ export default { - {{ element.host }} {{ element.time }}{{ element.time ? 'ms' : '' }} {{ element.dns }} + {{ element.host }} {{ element.time ? `${element.time}ms` : (element.title ? '' : '测速中') }} {{ element.dns }} diff --git a/packages/gui/src/view/style/theme/dark.scss b/packages/gui/src/view/style/theme/dark.scss index 77f8a2b..a31b81e 100644 --- a/packages/gui/src/view/style/theme/dark.scss +++ b/packages/gui/src/view/style/theme/dark.scss @@ -152,6 +152,12 @@ $dark-input: #777; //输入框:背景色 border-color: #5a5750; color: #cfa572; } + /* 标签:未知 */ + .ant-tag:not(.ant-tag-red, .ant-tag-green, .ant-tag-orange) { + background-color: #5a5a5a; + border-color: #5a5a5a; + color: #ccc; + } /* 按钮 */ .ant-btn:not(.ant-btn-danger, .ant-btn-primary) { diff --git a/packages/mitmproxy/package.json b/packages/mitmproxy/package.json index b2d53a3..9367be8 100644 --- a/packages/mitmproxy/package.json +++ b/packages/mitmproxy/package.json @@ -18,7 +18,6 @@ "axios": "^1.7.7", "baidu-aip-sdk": "^4.16.16", "dns-over-http": "^0.2.0", - "dns-over-tls": "^0.0.9", "is-browser": "^2.1.0", "json5": "^2.2.3", "lodash": "^4.17.21", diff --git a/packages/mitmproxy/src/lib/dns/base.js b/packages/mitmproxy/src/lib/dns/base.js index fb44844..298b73c 100644 --- a/packages/mitmproxy/src/lib/dns/base.js +++ b/packages/mitmproxy/src/lib/dns/base.js @@ -52,21 +52,26 @@ module.exports = class BaseDNS { } } - async lookup (hostname) { + async lookup (hostname, ipChecker) { try { let ipCache = this.cache.get(hostname) if (ipCache) { - if (ipCache.value != null) { - ipCache.doCount(ipCache.value, false) - return ipCache.value + const ip = ipCache.value + if (ip != null) { + if (ipChecker && ipChecker(ip)) { + ipCache.doCount(ip, false) + return ip + } else { + return hostname + } } } else { ipCache = new IpCache(hostname) this.cache.set(hostname, ipCache) } - const t = new Date() - let ipList = await this._lookupInternal(hostname) + const t = Date.now() + let ipList = await this._lookupWithPreSetIpList(hostname) if (ipList == null) { // 没有获取到ipv4地址 ipList = [] @@ -74,29 +79,45 @@ module.exports = class BaseDNS { ipList.push(hostname) // 把原域名加入到统计里去 ipCache.setBackupList(ipList) - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] ${hostname} ➜ ${ipCache.value} (${new Date() - t} ms), ipList: ${JSON.stringify(ipList)}, ipCache:`, JSON.stringify(ipCache)) - return ipCache.value + const ip = ipCache.value + log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] ${hostname} ➜ ${ip} (${Date.now() - t} ms), ipList: ${JSON.stringify(ipList)}, ipCache:`, JSON.stringify(ipCache)) + + if (ipChecker) { + if (ip != null && ip !== hostname && ipChecker(ip)) { + return ip + } + + for (const ip of ipList) { + if (ip !== hostname && ipChecker(ip)) { + return ip + } + } + } + + return ip != null ? ip : hostname } catch (error) { log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] cannot resolve hostname ${hostname}, error:`, error) return hostname } } - async _lookupInternal (hostname) { - // 获取当前域名的预设IP列表 - let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`) - if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) { - if (hostnamePreSetIpList.length > 0) { - hostnamePreSetIpList = hostnamePreSetIpList.slice() - } else { - hostnamePreSetIpList = mapToList(hostnamePreSetIpList) - } + async _lookupWithPreSetIpList (hostname) { + if (this.preSetIpList) { + // 获取当前域名的预设IP列表 + let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`) + if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) { + if (hostnamePreSetIpList.length > 0) { + hostnamePreSetIpList = hostnamePreSetIpList.slice() // 复制一份列表数据,避免配置数据被覆盖 + } else { + hostnamePreSetIpList = mapToList(hostnamePreSetIpList) + } - if (hostnamePreSetIpList.length > 0) { - hostnamePreSetIpList.isPreSet = true - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的预设IP列表: ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`) - return hostnamePreSetIpList + if (hostnamePreSetIpList.length > 0) { + hostnamePreSetIpList.isPreSet = true + log.info(`[DNS-over-PreSet '${this.dnsName}'] 获取到该域名的预设IP列表: ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`) + return hostnamePreSetIpList + } } } @@ -105,24 +126,79 @@ module.exports = class BaseDNS { async _lookup (hostname) { const start = Date.now() + + let response + try { + // 执行DNS查询 + log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query start: ${hostname}`) + response = await this._doDnsQuery(hostname, 'A', start) + } catch { + // 异常日志在 _doDnsQuery已经打印过,这里就不再打印了 + return [] + } + try { - const response = await this._doDnsQuery(hostname) const cost = Date.now() - start + log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query end: ${hostname}, cost: ${cost} ms, response:`, response) + if (response == null || response.answers == null || response.answers.length == null || response.answers.length === 0) { - // 说明没有获取到ip log.warn(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms, response:`, response) return [] } + const ret = response.answers.filter(item => item.type === 'A').map(item => item.data) if (ret.length === 0) { - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IPv4地址: ${hostname}, cost: ${cost} ms`) + log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms`) } else { - log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IPv4地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`) + log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IP地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`) } + return ret } catch (e) { - log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS query error, hostname: ${hostname}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}, cost: ${Date.now() - start} ms, error:`, e) + log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] 解读响应失败,response:`, response, ', error:', e) return [] } } + + _doDnsQuery (hostname, type = 'A', start) { + if (start == null) { + start = Date.now() + } + + return new Promise((resolve, reject) => { + // 设置超时任务 + let isOver = false + const timeout = 8000 + const timeoutId = setTimeout(() => { + if (!isOver) { + log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询超时, hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms`) + reject(new Error('DNS查询超时')) + } + }, timeout) + + try { + this._dnsQueryPromise(hostname, type) + .then((response) => { + isOver = true + clearTimeout(timeoutId) + resolve(response) + }) + .catch((e) => { + isOver = true + clearTimeout(timeoutId) + if (e.message === 'DNS查询超时') { + log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询超时. hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms`) + } else { + log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询错误, hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms, error:`, e) + } + reject(e) + }) + } catch (e) { + isOver = true + clearTimeout(timeoutId) + log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询异常, hostname: ${hostname}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms, error:`, e) + reject(e) + } + }) + } } diff --git a/packages/mitmproxy/src/lib/dns/https.js b/packages/mitmproxy/src/lib/dns/https.js index 3ab889c..07f2639 100644 --- a/packages/mitmproxy/src/lib/dns/https.js +++ b/packages/mitmproxy/src/lib/dns/https.js @@ -1,16 +1,45 @@ const { promisify } = require('node:util') const doh = require('dns-over-http') const BaseDNS = require('./base') +const HttpsAgent = require('../proxy/common/ProxyHttpsAgent') +const Agent = require('../proxy/common/ProxyHttpAgent') const dohQueryAsync = promisify(doh.query) +function createAgent (dnsServer) { + return new (dnsServer.startsWith('https:') ? HttpsAgent : Agent)({ + keepAlive: true, + timeout: 4000, + }) +} + module.exports = class DNSOverHTTPS extends BaseDNS { - constructor (dnsName, cacheSize, preSetIpList, dnsServer) { + constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerName) { super(dnsName, 'HTTPS', cacheSize, preSetIpList) - this.dnsServer = dnsServer + this.dnsServer = dnsServer.replace(/\s+/, '') + this.dnsServerName = dnsServerName } - async _doDnsQuery (hostname) { - return await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }]) + _dnsQueryPromise (hostname, type = 'A') { + // 请求参数 + const options = { + url: this.dnsServer, + agent: createAgent(this.dnsServer), + } + if (this.dnsServerName) { + // 设置SNI + options.servername = this.dnsServerName + options.rejectUnauthorized = false + } + + // DNS查询参数 + const questions = [ + { + type, + name: hostname, + }, + ] + + return dohQueryAsync(options, questions) } } diff --git a/packages/mitmproxy/src/lib/dns/index.js b/packages/mitmproxy/src/lib/dns/index.js index ee3e40c..7a36d6f 100644 --- a/packages/mitmproxy/src/lib/dns/index.js +++ b/packages/mitmproxy/src/lib/dns/index.js @@ -1,4 +1,5 @@ const matchUtil = require('../../utils/util.match') +const log = require('../../utils/util.log.server') const DNSOverPreSetIpList = require('./preset.js') const DNSOverHTTPS = require('./https.js') const DNSOverTLS = require('./tls.js') @@ -27,7 +28,7 @@ module.exports = { if (type == null) { if (server.startsWith('https://') || server.startsWith('http://')) { type = 'https' - } else if (server.startsWith('tls://')) { + } else if (server.startsWith('tls://') || server.startsWith('dot://')) { type = 'tls' } else if (server.startsWith('tcp://')) { type = 'tcp' @@ -47,7 +48,7 @@ module.exports = { } // 基于 https - dnsMap[provider] = new DNSOverHTTPS(provider, conf.cacheSize, preSetIpList, server) + dnsMap[provider] = new DNSOverHTTPS(provider, conf.cacheSize, preSetIpList, server, conf.sni || conf.servername) } else { // 获取DNS端口 let port = conf.port @@ -63,8 +64,8 @@ module.exports = { if (type === 'tls' || type === 'dot' || type === 'dns-over-tls') { // 基于 tls - dnsMap[provider] = new DNSOverTLS(provider, conf.cacheSize, preSetIpList, server, port, conf.servername) - } else if (type === 'tcp' || type === 'dns-over-tcp') { + dnsMap[provider] = new DNSOverTLS(provider, conf.cacheSize, preSetIpList, server, port, conf.sni || conf.servername) + } else if (type === 'tcp') { // 基于 tcp dnsMap[provider] = new DNSOverTCP(provider, conf.cacheSize, preSetIpList, server, port) } else { @@ -72,10 +73,19 @@ module.exports = { dnsMap[provider] = new DNSOverUDP(provider, conf.cacheSize, preSetIpList, server, port) } } + + if (conf.forSNI || conf.forSni) { + dnsMap.ForSNI = dnsMap[provider] + } } // 创建预设IP的DNS dnsMap.PreSet = new DNSOverPreSetIpList(preSetIpList) + if (dnsMap.ForSNI == null) { + dnsMap.ForSNI = dnsMap.PreSet + } + + log.info(`设置SNI默认使用的DNS为 '${dnsMap.ForSNI.dnsName}'(注:当某个域名配置了SNI但未配置DNS时,将默认使用该DNS)`) return dnsMap }, diff --git a/packages/mitmproxy/src/lib/dns/tcp.js b/packages/mitmproxy/src/lib/dns/tcp.js index 6141609..f3e5f5c 100644 --- a/packages/mitmproxy/src/lib/dns/tcp.js +++ b/packages/mitmproxy/src/lib/dns/tcp.js @@ -4,16 +4,16 @@ const dnsPacket = require('dns-packet') const randi = require('random-int') const BaseDNS = require('./base') -const defaultPort = 53 // UDP类型的DNS服务默认端口号 +const defaultPort = 53 // TCP类型的DNS服务默认端口号 module.exports = class DNSOverTCP extends BaseDNS { constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) { super(dnsName, 'TCP', cacheSize, preSetIpList) - this.dnsServer = dnsServer + this.dnsServer = dnsServer.replace(/\s+/, '') this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort } - _doDnsQuery (hostname) { + _dnsQueryPromise (hostname, type = 'A') { return new Promise((resolve, reject) => { // 构造 DNS 查询报文 const packet = dnsPacket.encode({ @@ -21,7 +21,7 @@ module.exports = class DNSOverTCP extends BaseDNS { type: 'query', id: randi(0x0, 0xFFFF), questions: [{ - type: 'A', + type, name: hostname, }], }) diff --git a/packages/mitmproxy/src/lib/dns/tls.js b/packages/mitmproxy/src/lib/dns/tls.js index b4b5d99..0194b4a 100644 --- a/packages/mitmproxy/src/lib/dns/tls.js +++ b/packages/mitmproxy/src/lib/dns/tls.js @@ -1,4 +1,4 @@ -const dnstls = require('dns-over-tls') +const dnstls = require('./util/dns-over-tls') const BaseDNS = require('./base') const defaultPort = 853 @@ -6,22 +6,25 @@ const defaultPort = 853 module.exports = class DNSOverTLS extends BaseDNS { constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort, dnsServerName) { super(dnsName, 'TLS', cacheSize, preSetIpList) - this.dnsServer = dnsServer + this.dnsServer = dnsServer.replace(/\s+/, '') this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort this.dnsServerName = dnsServerName } - async _doDnsQuery (hostname) { + _dnsQueryPromise (hostname, type = 'A') { const options = { host: this.dnsServer, port: this.dnsServerPort, servername: this.dnsServerName || this.dnsServer, + rejectUnauthorized: !this.dnsServerName, name: hostname, klass: 'IN', - type: 'A', + type, + + timeout: 4000, } - return await dnstls.query(options) + return dnstls.query(options) } } diff --git a/packages/mitmproxy/src/lib/dns/udp.js b/packages/mitmproxy/src/lib/dns/udp.js index 86d9b30..cd97a7d 100644 --- a/packages/mitmproxy/src/lib/dns/udp.js +++ b/packages/mitmproxy/src/lib/dns/udp.js @@ -8,28 +8,35 @@ const defaultPort = 53 // UDP类型的DNS服务默认端口号 module.exports = class DNSOverUDP extends BaseDNS { constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) { super(dnsName, 'UDP', cacheSize, preSetIpList) - this.dnsServer = dnsServer + this.dnsServer = dnsServer.replace(/\s+/, '') this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']') this.socketType = this.isIPv6 ? 'udp6' : 'udp4' } - _doDnsQuery (hostname) { + _dnsQueryPromise (hostname, type = 'A') { return new Promise((resolve, reject) => { + let isOver = false + const timeout = 5000 + let timeoutId = null + // 构造 DNS 查询报文 const packet = dnsPacket.encode({ flags: dnsPacket.RECURSION_DESIRED, type: 'query', id: randi(0x0, 0xFFFF), questions: [{ - type: 'A', + type, name: hostname, }], }) // 创建客户端 const udpClient = dgram.createSocket(this.socketType, (msg, _rinfo) => { + isOver = true + clearTimeout(timeoutId) + const response = dnsPacket.decode(msg) resolve(response) udpClient.close() @@ -38,10 +45,20 @@ module.exports = class DNSOverUDP extends BaseDNS { // 发送 UDP 查询 udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err, _bytes) => { if (err) { + isOver = true + clearTimeout(timeoutId) reject(err) udpClient.close() } }) + + // 设置超时任务 + timeoutId = setTimeout(() => { + if (!isOver) { + reject(new Error('DNS查询超时')) + udpClient.close() + } + }, timeout) }) } } diff --git a/packages/mitmproxy/src/lib/dns/util/dns-over-tls.js b/packages/mitmproxy/src/lib/dns/util/dns-over-tls.js new file mode 100644 index 0000000..8194f00 --- /dev/null +++ b/packages/mitmproxy/src/lib/dns/util/dns-over-tls.js @@ -0,0 +1,81 @@ +/** + * 由于组件 `dns-over-tls@0.0.9` 不支持 `rejectUnauthorized` 和 `timeout` 两个参数,所以将源码复制过来,并简化了代码。 + */ +const dnsPacket = require('dns-packet') +const tls_1 = require('node:tls') +const randi = require('random-int') + +const TWO_BYTES = 2 + +function getDnsQuery ({ type, name, klass, id }) { + return { + id, + type: 'query', + flags: dnsPacket.RECURSION_DESIRED, + questions: [{ class: klass, name, type }], + } +} + +function query ({ host, servername, type, name, klass, port, rejectUnauthorized, timeout }) { + return new Promise((resolve, reject) => { + if (!host || !servername || !name) { + throw new Error('At least host, servername and name must be set.') + } + + let response = Buffer.alloc(0) + let packetLength = 0 + const dnsQuery = getDnsQuery({ id: randi(0x0, 0xFFFF), type, name, klass }) + const dnsQueryBuf = dnsPacket.streamEncode(dnsQuery) + const socket = tls_1.connect({ host, port, servername, rejectUnauthorized, timeout }) + + // 超时处理 + let isFinished = false + let interval + if (timeout > 0) { + interval = setInterval(() => { + if (!isFinished) { + socket.destroy((...args) => { + console.info('socket destory callback args:', args) + }) + + reject(new Error('DNS查询超时')) + } + }, timeout) + } + + socket.on('secureConnect', () => socket.write(dnsQueryBuf)) + socket.on('data', (data) => { + if (timeout) { + isFinished = true + clearInterval(interval) + } + + if (response.length === 0) { + packetLength = data.readUInt16BE(0) + if (packetLength < 12) { + reject(new Error('Below DNS minimum packet length (DNS Header is 12 bytes)')) + } + response = Buffer.from(data) + } else { + response = Buffer.concat([response, data]) + } + + if (response.length === packetLength + TWO_BYTES) { + socket.destroy() + resolve(dnsPacket.streamDecode(response)) + } else { + reject(new Error('响应长度不正确')) + } + }) + socket.on('error', (err) => { + if (timeout) { + isFinished = true + clearInterval(interval) + } + reject(err) + }) + }) +} + +exports.query = query +exports.default = { query } diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/abort.js b/packages/mitmproxy/src/lib/interceptor/impl/req/abort.js index 73eb827..68c455b 100644 --- a/packages/mitmproxy/src/lib/interceptor/impl/req/abort.js +++ b/packages/mitmproxy/src/lib/interceptor/impl/req/abort.js @@ -5,10 +5,18 @@ module.exports = { const { rOptions, log } = context if (interceptOpt.abort === true || interceptOpt.abort === 'true') { - res.writeHead(403, { + const headers = { 'Content-Type': 'text/plain; charset=utf-8', 'DS-Interceptor': 'abort', - }) + } + + // headers.Access-Control-Allow-*:避免跨域问题 + if (rOptions.headers.origin) { + headers['Access-Control-Allow-Credentials'] = 'true' + headers['Access-Control-Allow-Origin'] = rOptions.headers.origin + } + + res.writeHead(403, headers) res.write( 'DevSidecar 403: Request abort.\n\n' + ' This request is matched by abort intercept.\n\n' diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js b/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js index 063b811..d7bf308 100644 --- a/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js +++ b/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js @@ -111,7 +111,7 @@ function checkIsLimitConfig (id, api) { module.exports = { name: 'baiduOcr', priority: 131, - requestIntercept (context, interceptOpt, req, res, ssl, next, matched) { + requestIntercept (context, interceptOpt, req, res, ssl, next) { const { rOptions, log } = context const headers = { diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js b/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js index f558471..1e265c7 100644 --- a/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js +++ b/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js @@ -1,16 +1,29 @@ const url = require('node:url') const lodash = require('lodash') +function replacePlaceholder0 (url, matched, pre) { + if (matched) { + for (let i = 0; i < matched.length; i++) { + url = url.replace(`\${${pre}[${i}]}`, matched[i] || '') + } + if (matched.groups) { + for (const key in matched.groups) { + url = url.replace(`\${${key}}`, matched.groups[key] || '') + } + } + } + return url +} + // 替换占位符 -function replacePlaceholder (url, rOptions, matched) { +function replacePlaceholder (url, rOptions, pathMatched, hostnameMatched) { if (url.includes('${')) { // eslint-disable-next-line no-template-curly-in-string url = url.replace('${host}', rOptions.hostname) - if (matched && url.includes('${')) { - for (let i = 0; i < matched.length; i++) { - url = url.replace(`\${m[${i}]}`, matched[i] == null ? '' : matched[i]) - } + if (url.includes('${')) { + url = replacePlaceholder0(url, pathMatched, 'p') + url = replacePlaceholder0(url, hostnameMatched, 'h') } // 移除多余的占位符 @@ -22,7 +35,7 @@ function replacePlaceholder (url, rOptions, matched) { return url } -function buildTargetUrl (rOptions, urlConf, interceptOpt, matched) { +function buildTargetUrl (rOptions, urlConf, interceptOpt, matched, hostnameMatched) { let targetUrl if (interceptOpt && interceptOpt.replace) { const regexp = new RegExp(interceptOpt.replace) @@ -40,7 +53,7 @@ function buildTargetUrl (rOptions, urlConf, interceptOpt, matched) { } // 替换占位符 - targetUrl = replacePlaceholder(targetUrl, rOptions, matched) + targetUrl = replacePlaceholder(targetUrl, rOptions, matched, hostnameMatched) // 拼接协议 targetUrl = targetUrl.indexOf('http:') === 0 || targetUrl.indexOf('https:') === 0 ? targetUrl : `${rOptions.protocol}//${targetUrl}` @@ -48,9 +61,9 @@ function buildTargetUrl (rOptions, urlConf, interceptOpt, matched) { return targetUrl } -function doProxy (proxyConf, rOptions, req, interceptOpt, matched) { +function doProxy (proxyConf, rOptions, req, interceptOpt, matched, hostnameMatched) { // 获取代理目标地址 - const proxyTarget = buildTargetUrl(rOptions, proxyConf, interceptOpt, matched) + const proxyTarget = buildTargetUrl(rOptions, proxyConf, interceptOpt, matched, hostnameMatched) // 替换rOptions的属性 // eslint-disable-next-line node/no-deprecated-api @@ -76,7 +89,7 @@ module.exports = { replacePlaceholder, buildTargetUrl, doProxy, - requestIntercept (context, interceptOpt, req, res, ssl, next, matched) { + requestIntercept (context, interceptOpt, req, res, ssl, next, matched, hostnameMatched) { const { rOptions, log, RequestCounter } = context const originHostname = rOptions.hostname @@ -107,7 +120,7 @@ module.exports = { } // 替换 rOptions 中的地址,并返回代理目标地址 - const proxyTarget = doProxy(proxyConf, rOptions, req, interceptOpt, matched) + const proxyTarget = doProxy(proxyConf, rOptions, req, interceptOpt, matched, hostnameMatched) if (context.requestCount) { log.info('proxy choice:', JSON.stringify(context.requestCount)) diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js b/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js index 1812abd..85f0a68 100644 --- a/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js +++ b/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js @@ -3,16 +3,24 @@ const proxyApi = require('./proxy') module.exports = { name: 'redirect', priority: 105, - requestIntercept (context, interceptOpt, req, res, ssl, next, matched) { + requestIntercept (context, interceptOpt, req, res, ssl, next, matched, hostnameMatched) { const { rOptions, log } = context // 获取重定向目标地址 - const redirect = proxyApi.buildTargetUrl(rOptions, interceptOpt.redirect, interceptOpt, matched) + const redirect = proxyApi.buildTargetUrl(rOptions, interceptOpt.redirect, interceptOpt, matched, hostnameMatched) - res.writeHead(302, { + const headers = { 'Location': redirect, 'DS-Interceptor': 'redirect', - }) + } + + // headers.Access-Control-Allow-*:避免跨域问题 + if (rOptions.headers.origin) { + headers['Access-Control-Allow-Credentials'] = 'true' + headers['Access-Control-Allow-Origin'] = rOptions.headers.origin + } + + res.writeHead(302, headers) res.end() const url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${req.url}` diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/success.js b/packages/mitmproxy/src/lib/interceptor/impl/req/success.js index 179f745..84cff4a 100644 --- a/packages/mitmproxy/src/lib/interceptor/impl/req/success.js +++ b/packages/mitmproxy/src/lib/interceptor/impl/req/success.js @@ -5,10 +5,18 @@ module.exports = { const { rOptions, log } = context if (interceptOpt.success === true || interceptOpt.success === 'true') { - res.writeHead(200, { + const headers = { 'Content-Type': 'text/plain; charset=utf-8', 'DS-Interceptor': 'success', - }) + } + + // headers.Access-Control-Allow-*:避免跨域问题 + if (rOptions.headers.origin) { + headers['Access-Control-Allow-Credentials'] = 'true' + headers['Access-Control-Allow-Origin'] = rOptions.headers.origin + } + + res.writeHead(200, headers) res.write( 'DevSidecar 200: Request success.\n\n' + ' This request is matched by success intercept.\n\n' diff --git a/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js b/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js index 559c8be..05b9f4c 100644 --- a/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js +++ b/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js @@ -92,9 +92,9 @@ module.exports = { // 如果未手动配置需要缓存,则不允许使用缓存 const maxAge = cacheReq.getMaxAge(interceptOpt) if (maxAge == null || maxAge <= 0) { - replaceHeaders['cache-control'] = '[remove]' - replaceHeaders['last-modified'] = '[remove]' - replaceHeaders.expires = '[remove]' + replaceHeaders['cache-control'] = REMOVE + replaceHeaders['last-modified'] = REMOVE + replaceHeaders.expires = REMOVE } actions += `${actions ? ',' : ''}download:${filename}` diff --git a/packages/mitmproxy/src/lib/proxy/mitmproxy/createConnectHandler.js b/packages/mitmproxy/src/lib/proxy/mitmproxy/createConnectHandler.js index 0ac8f4b..3f1e0be 100644 --- a/packages/mitmproxy/src/lib/proxy/mitmproxy/createConnectHandler.js +++ b/packages/mitmproxy/src/lib/proxy/mitmproxy/createConnectHandler.js @@ -55,7 +55,7 @@ module.exports = function createConnectHandler (sslConnectInterceptor, middlewar function connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDirect = false, target = null) { // tunneling https // log.info('connect:', hostname, port) - const start = new Date() + const start = Date.now() const isDnsIntercept = {} const hostport = `${hostname}:${port}` @@ -114,7 +114,7 @@ function connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDire if (dnsConfig && dnsConfig.dnsMap) { const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname) if (dns) { - options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, isDnsIntercept) + options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, port, isDnsIntercept) } } // 代理连接事件监听 @@ -134,7 +134,7 @@ function connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDire cltSocket.pipe(proxySocket) }) proxySocket.on('timeout', () => { - const cost = new Date() - start + const cost = Date.now() - start const errorMsg = `${isDirect ? '直连' : '代理连接'}超时: ${hostport}, cost: ${cost} ms` log.error(errorMsg) @@ -148,7 +148,7 @@ function connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDire }) proxySocket.on('error', (e) => { // 连接失败,可能被GFW拦截,或者服务端拥挤 - const cost = new Date() - start + const cost = Date.now() - start const errorMsg = `${isDirect ? '直连' : '代理连接'}失败: ${hostport}, cost: ${cost} ms, errorMsg: ${e.message}` log.error(`${errorMsg}\r\n`, e) diff --git a/packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js b/packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js index b3c27f9..a8876f6 100644 --- a/packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js +++ b/packages/mitmproxy/src/lib/proxy/mitmproxy/createRequestHandler.js @@ -109,20 +109,22 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e function onFree () { url = `${rOptions.method} ➜ ${rOptions.protocol}//${rOptions.hostname}:${rOptions.port}${rOptions.path}` - const start = new Date() + const start = Date.now() log.info('发起代理请求:', url, (rOptions.servername ? `, sni: ${rOptions.servername}` : ''), ', headers:', jsonApi.stringify2(rOptions.headers)) const isDnsIntercept = {} if (dnsConfig && dnsConfig.dnsMap) { let dns = DnsUtil.hasDnsLookup(dnsConfig, rOptions.hostname) if (!dns && rOptions.servername) { - dns = dnsConfig.dnsMap.quad9 + dns = dnsConfig.dnsMap.ForSNI if (dns) { - log.info(`域名 ${rOptions.hostname} 在dns中未配置,但使用了 sni: ${rOptions.servername}, 必须使用dns,现默认使用 'quad9' DNS.`) + log.info(`域名 ${rOptions.hostname} 在dns中未配置,但使用了 sni: ${rOptions.servername}, 必须使用dns,现默认使用 '${dns.dnsName}' DNS.`) + } else { + log.warn(`域名 ${rOptions.hostname} 在dns中未配置,但使用了 sni: ${rOptions.servername},且DNS服务管理中,也未指定SNI默认使用的DNS。`) } } if (dns) { - rOptions.lookup = dnsLookup.createLookupFunc(res, dns, 'request url', url, isDnsIntercept) + rOptions.lookup = dnsLookup.createLookupFunc(res, dns, 'request url', url, rOptions.port, isDnsIntercept) log.debug(`域名 ${rOptions.hostname} DNS: ${dns.dnsName}`) res.setHeader('DS-DNS', dns.dnsName) } else { @@ -154,7 +156,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e } proxyReq = (rOptions.protocol === 'https:' ? https : http).request(rOptions, (proxyRes) => { - const cost = new Date() - start + const cost = Date.now() - start if (rOptions.protocol === 'https:') { log.info(`代理请求返回: 【${proxyRes.statusCode}】${url}, cost: ${cost} ms`) } else { @@ -171,7 +173,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e // 代理请求的事件监听 proxyReq.on('timeout', () => { - const cost = new Date() - start + const cost = Date.now() - start const errorMsg = `代理请求超时: ${url}, cost: ${cost} ms` log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions)) countSlow(isDnsIntercept, `代理请求超时, cost: ${cost} ms`) @@ -182,7 +184,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e reject(error) }) proxyReq.on('error', (e) => { - const cost = new Date() - start + const cost = Date.now() - start log.error(`代理请求错误: ${url}, cost: ${cost} ms, error:`, e, ', rOptions:', jsonApi.stringify2(rOptions)) countSlow(isDnsIntercept, `代理请求错误: ${e.message}`) reject(e) @@ -193,7 +195,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e } }) proxyReq.on('aborted', () => { - const cost = new Date() - start + const cost = Date.now() - start const errorMsg = `代理请求被取消: ${url}, cost: ${cost} ms` log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions)) @@ -209,7 +211,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e // 原始请求的事件监听 req.on('aborted', () => { - const cost = new Date() - start + const cost = Date.now() - start const errorMsg = `请求被取消: ${url}, cost: ${cost} ms` log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions)) proxyReq.abort() @@ -219,12 +221,12 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e reject(new Error(errorMsg)) }) req.on('error', (e, req, res) => { - const cost = new Date() - start + const cost = Date.now() - start log.error(`请求错误: ${url}, cost: ${cost} ms, error:`, e, ', rOptions:', jsonApi.stringify2(rOptions)) reject(e) }) req.on('timeout', () => { - const cost = new Date() - start + const cost = Date.now() - start const errorMsg = `请求超时: ${url}, cost: ${cost} ms` log.error(errorMsg, ', rOptions:', jsonApi.stringify2(rOptions)) reject(new Error(errorMsg)) diff --git a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js index 6668ac0..ddc25e4 100644 --- a/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js +++ b/packages/mitmproxy/src/lib/proxy/mitmproxy/dnsLookup.js @@ -2,12 +2,35 @@ const defaultDns = require('node:dns') const log = require('../../../utils/util.log.server') const speedTest = require('../../speed') +function createIpChecker (tester) { + if (!tester || tester.backupList == null || tester.backupList.length === 0) { + return null + } + + return (ip) => { + for (let i = 0; i < tester.backupList.length; i++) { + const item = tester.backupList[i] + if (item.host === ip) { + if (item.time > 0) { + return true // IP测速成功 + } + if (item.status === 'failed') { + return false // IP测速失败 + } + break + } + } + + return true // IP测速未知 + } +} + module.exports = { - createLookupFunc (res, dns, action, target, isDnsIntercept) { + createLookupFunc (res, dns, action, target, port, isDnsIntercept) { target = target ? (`, target: ${target}`) : '' return (hostname, options, callback) => { - const tester = speedTest.getSpeedTester(hostname) + const tester = speedTest.getSpeedTester(hostname, port) if (tester) { const aliveIpObj = tester.pickFastAliveIpObj() if (aliveIpObj) { @@ -21,7 +44,10 @@ module.exports = { log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { "ready": ${tester.ready}, "backupList": ${JSON.stringify(tester.backupList)} }`) } } - dns.lookup(hostname).then((ip) => { + + const ipChecker = createIpChecker(tester) + + dns.lookup(hostname, ipChecker).then((ip) => { if (isDnsIntercept) { isDnsIntercept.dns = dns isDnsIntercept.hostname = hostname @@ -29,35 +55,16 @@ module.exports = { } if (ip !== hostname) { - // 判断是否为测速失败的IP,如果是,则不使用当前IP - let isTestFailedIp = false - if (tester && tester.ready && tester.backupList && tester.backupList.length > 0) { - for (let i = 0; i < tester.backupList.length; i++) { - const item = tester.backupList[i] - if (item.host === ip) { - if (item.time == null) { - isTestFailedIp = true - } - break - } - } - } - if (isTestFailedIp === false) { - log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`) - if (res) { - res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) - } - callback(null, ip, 4) - return - } else { - // 使用默认dns - log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}, skip test failed ip from dns '${dns.dnsName}: ${ip}'${target}, options:`, options) + log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`) + if (res) { + res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) } + callback(null, ip, 4) } else { // 使用默认dns - log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns) + log.info(`----- ${action}: ${hostname}, use default DNS: ${hostname}${target}, options:`, options, ', dns:', dns) + defaultDns.lookup(hostname, options, callback) } - defaultDns.lookup(hostname, options, callback) }) } }, diff --git a/packages/mitmproxy/src/lib/proxy/tls/tlsUtils.js b/packages/mitmproxy/src/lib/proxy/tls/tlsUtils.js index 2e44900..b8b02e8 100644 --- a/packages/mitmproxy/src/lib/proxy/tls/tlsUtils.js +++ b/packages/mitmproxy/src/lib/proxy/tls/tlsUtils.js @@ -22,8 +22,8 @@ utils.createCA = function (CN) { const keys = pki.rsa.generateKeyPair(2048) const cert = pki.createCertificate() cert.publicKey = keys.publicKey - cert.serialNumber = `${(new Date()).getTime()}` - cert.validity.notBefore = new Date(new Date() - (60 * 60 * 1000)) + cert.serialNumber = `${Date.now()}` + cert.validity.notBefore = new Date(Date.now() - (60 * 60 * 1000)) cert.validity.notAfter = new Date() cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 20) const attrs = [{ @@ -87,7 +87,7 @@ utils.createFakeCertificateByDomain = function (caKey, caCert, domain, mappingHo const cert = pki.createCertificate() cert.publicKey = keys.publicKey - cert.serialNumber = `${(new Date()).getTime()}` + cert.serialNumber = `${Date.now()}` cert.validity.notBefore = new Date() cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1) cert.validity.notAfter = new Date() diff --git a/packages/mitmproxy/src/lib/speed/SpeedTester.js b/packages/mitmproxy/src/lib/speed/SpeedTester.js index 59f4995..cbbeaf0 100644 --- a/packages/mitmproxy/src/lib/speed/SpeedTester.js +++ b/packages/mitmproxy/src/lib/speed/SpeedTester.js @@ -1,31 +1,44 @@ -// 1个小时不访问,取消获取 +// const { exec } = require('node:child_process') const net = require('node:net') const _ = require('lodash') const log = require('../../utils/util.log.server') const config = require('./config.js') +// const isWindows = process.platform === 'win32' + const DISABLE_TIMEOUT = 60 * 60 * 1000 + class SpeedTester { - constructor ({ hostname }) { + constructor ({ hostname, port }) { this.dnsMap = config.getConfig().dnsMap + this.hostname = hostname - this.lastReadTime = Date.now() + this.port = port || 443 + this.ready = false this.alive = [] this.backupList = [] - this.keepCheckId = false - - this.loadingIps = false - this.loadingTest = false this.testCount = 0 - this.test() + this.lastReadTime = Date.now() + this.keepCheckIntervalId = false + + this.tryTestCount = 0 + + this.test() // 异步:初始化完成后先测速一次 } pickFastAliveIpObj () { this.touch() + if (this.alive.length === 0) { - this.test() // 异步 + if (this.backupList.length > 0 && this.tryTestCount % 10 > 0) { + this.testBackups() // 异步 + } else if (this.tryTestCount % 10 === 0) { + this.test() // 异步 + } + this.tryTestCount++ + return null } return this.alive[0] @@ -33,26 +46,27 @@ class SpeedTester { touch () { this.lastReadTime = Date.now() - if (!this.keepCheckId) { + if (!this.keepCheckIntervalId) { this.startChecker() } } startChecker () { - if (this.keepCheckId) { - clearInterval(this.keepCheckId) + if (this.keepCheckIntervalId) { + clearInterval(this.keepCheckIntervalId) } - this.keepCheckId = setInterval(() => { + this.keepCheckIntervalId = setInterval(() => { if (Date.now() - DISABLE_TIMEOUT > this.lastReadTime) { // 超过很长时间没有访问,取消测试 - clearInterval(this.keepCheckId) + clearInterval(this.keepCheckIntervalId) + this.keepCheckIntervalId = false return } if (this.alive.length > 0) { - this.testBackups() - return + this.testBackups() // 异步 + } else { + this.test() // 异步 } - this.test() }, config.getConfig().interval) } @@ -71,44 +85,56 @@ class SpeedTester { promiseList.push(one) } await Promise.all(promiseList) + const items = [] for (const ip in ips) { - items.push({ host: ip, port: 443, dns: ips[ip].dns }) + items.push({ host: ip, dns: ips[ip].dns }) } return items } async getFromOneDns (dns) { - return await dns._lookupInternal(this.hostname) + return await dns._lookupWithPreSetIpList(this.hostname) } async test () { - const newList = await this.getIpListFromDns(this.dnsMap) - const newBackupList = [...newList, ...this.backupList] - this.backupList = _.unionBy(newBackupList, 'host') this.testCount++ + log.debug(`[speed] test start: ${this.hostname}, testCount: ${this.testCount}`) - log.info('[speed]', this.hostname, '➜ ip-list:', this.backupList) - await this.testBackups() - if (config.notify) { - config.notify({ key: 'test' }) + try { + const newList = await this.getIpListFromDns(this.dnsMap) + const newBackupList = [...newList, ...this.backupList] + this.backupList = _.unionBy(newBackupList, 'host') + await this.testBackups() + log.info(`[speed] test end: ${this.hostname} ➜ ip-list:`, this.backupList, `, testCount: ${this.testCount}`) + if (config.notify) { + config.notify({ key: 'test' }) + } + } catch (e) { + log.error(`[speed] test failed: ${this.hostname}, testCount: ${this.testCount}, error:`, e) } } async testBackups () { - const testAll = [] - const aliveList = [] - for (const item of this.backupList) { - testAll.push(this.doTest(item, aliveList)) + if (this.backupList.length > 0) { + const aliveList = [] + + const testAll = [] + for (const item of this.backupList) { + testAll.push(this.doTest(item, aliveList)) + } + await Promise.all(testAll) + this.alive = aliveList } - await Promise.all(testAll) - this.alive = aliveList + this.ready = true } async doTest (item, aliveList) { try { const ret = await this.testOne(item) + item.title = `${ret.by}测速成功:${ret.target}` + log.info(`[speed] test success: ${this.hostname} ➜ ${item.host}:${this.port} from DNS '${item.dns}'`) _.merge(item, ret) aliveList.push({ ...ret, ...item }) aliveList.sort((a, b) => a.time - b.time) @@ -125,48 +151,131 @@ class SpeedTester { return a.time - b.time }) } catch (e) { - if (e.message !== 'timeout') { - log.warn('[speed] test error: ', this.hostname, `➜ ${item.host}:${item.port} from DNS '${item.dns}'`, ', errorMsg:', e.message) + if (item.time == null) { + item.title = e.message + item.status = 'failed' + } + if (!e.message.includes('timeout')) { + log.warn(`[speed] test error: ${this.hostname} ➜ ${item.host}:${this.port} from DNS '${item.dns}', errorMsg: ${e.message}`) } } } - testOne (item) { - const timeout = 5000 - const { host, port, dns } = item - const startTime = Date.now() - let isOver = false + testByTCP (item) { return new Promise((resolve, reject) => { + const { host, dns } = item + const startTime = Date.now() + + let isOver = false + const timeout = 5000 let timeoutId = null - const client = net.createConnection({ host, port }, () => { - // 'connect' 监听器 - const connectionTime = Date.now() + + const client = net.createConnection({ host, port: this.port }, () => { isOver = true clearTimeout(timeoutId) - resolve({ status: 'success', time: connectionTime - startTime }) + + const connectionTime = Date.now() + resolve({ status: 'success', by: 'TCP', target: `${host}:${this.port}`, time: connectionTime - startTime }) client.end() }) - client.on('end', () => { - }) client.on('error', (e) => { - if (e.message !== 'timeout') { - log.warn('[speed] test error: ', this.hostname, `➜ ${host}:${port} from DNS '${dns}', cost: ${Date.now() - startTime} ms, errorMsg:`, e.message) - } isOver = true clearTimeout(timeoutId) + + log.warn('[speed] test by TCP error: ', this.hostname, `➜ ${host}:${this.port} from DNS '${dns}', cost: ${Date.now() - startTime} ms, errorMsg:`, e.message) reject(e) + client.end() }) timeoutId = setTimeout(() => { if (isOver) { return } - log.warn('[speed] test timeout:', this.hostname, `➜ ${host}:${port} from DNS '${dns}', cost: ${Date.now() - startTime} ms`) + + log.warn('[speed] test by TCP timeout:', this.hostname, `➜ ${host}:${this.port} from DNS '${dns}', cost: ${Date.now() - startTime} ms`) reject(new Error('timeout')) client.end() }, timeout) }) } + + // 暂不使用 + // testByPing (item) { + // return new Promise((resolve, reject) => { + // const { host, dns } = item + // const startTime = Date.now() + // + // // 设置超时程序 + // let isOver = false + // const timeout = 5000 + // const timeoutId = setTimeout(() => { + // if (!isOver) { + // log.warn('[speed] test by PING timeout:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms`) + // reject(new Error('timeout')) + // } + // }, timeout) + // + // // 协议选择(如强制ping6) + // const usePing6 = !isWindows && host.includes(':') // Windows无ping6命令 + // const cmd = usePing6 + // ? `ping6 -c 2 ${host}` + // : isWindows + // ? `ping -n 2 ${host}` + // : `ping -c 2 ${host}` + // + // log.debug('[speed] test by PING start:', this.hostname, `➜ ${host} from DNS '${dns}'`) + // exec(cmd, (error, stdout, _stderr) => { + // isOver = true + // clearTimeout(timeoutId) + // + // if (error) { + // log.warn('[speed] test by PING error:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms, error: 目标不可达或超时`) + // reject(new Error('目标不可达或超时')) + // return + // } + // + // // 提取延迟数据(正则匹配) + // const regex = /[=<](\d+(?:\.\d*)?)ms/gi // 适配Linux/Windows + // const times = [] + // let match + // // eslint-disable-next-line no-cond-assign + // while ((match = regex.exec(stdout)) !== null) { + // times.push(Number.parseFloat(match[1])) + // } + // + // if (times.length === 0) { + // log.warn('[speed] test by PING error:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms, error: 无法解析延迟`) + // reject(new Error('无法解析延迟')) + // } else { + // // 计算平均延迟 + // const avg = times.reduce((a, b) => a + b, 0) / times.length + // resolve({ status: 'success', by: 'PING', target: host, time: Math.round(avg) }) + // } + // }) + // }) + // } + + testOne (item) { + return new Promise((resolve, reject) => { + const thenFun = (ret) => { + resolve(ret) + } + + // 先用TCP测速 + this.testByTCP(item) + .then(thenFun) + .catch((e) => { + // // TCP测速失败,再用 PING 测速 + // this.testByPing(item) + // .then(thenFun) + // .catch((e2) => { + // reject(new Error(`TCP测速失败:${e.message};PING测速失败:${e2.message};`)) + // }) + + reject(new Error(`TCP测速失败:${item.host}:${this.port} ${e.message}`)) + }) + }) + } } module.exports = SpeedTester diff --git a/packages/mitmproxy/src/lib/speed/config.js b/packages/mitmproxy/src/lib/speed/config.js index ce8d3a8..dd7a0a2 100644 --- a/packages/mitmproxy/src/lib/speed/config.js +++ b/packages/mitmproxy/src/lib/speed/config.js @@ -1,9 +1,9 @@ const config = { - notify () {}, dnsMap: {}, } module.exports = { getConfig () { return config }, + notify: null, } diff --git a/packages/mitmproxy/src/lib/speed/index.js b/packages/mitmproxy/src/lib/speed/index.js index 0d31e55..e67071f 100644 --- a/packages/mitmproxy/src/lib/speed/index.js +++ b/packages/mitmproxy/src/lib/speed/index.js @@ -6,6 +6,28 @@ const SpeedTester = require('./SpeedTester.js') const SpeedTestPool = { } +function addSpeedTest (hostname, port) { + if (!port) { + const idx = hostname.indexOf(':') + if (idx > 0 && idx === hostname.lastIndexOf(':')) { + const arr = hostname.split(':') + hostname = arr[0] + port = Number.parseInt(arr[1]) || 443 + } else { + port = 443 + } + } + + // 443端口不拼接在key上 + const key = port === 443 ? hostname : `${hostname}:${port}` + + if (SpeedTestPool[key] == null) { + return SpeedTestPool[key] = new SpeedTester({ hostname, port }) + } + + return SpeedTestPool[key] +} + function initSpeedTest (runtimeConfig) { const { enabled, hostnameList } = runtimeConfig const conf = config.getConfig() @@ -14,45 +36,42 @@ function initSpeedTest (runtimeConfig) { return } _.forEach(hostnameList, (hostname) => { - SpeedTestPool[hostname] = new SpeedTester({ hostname }) + addSpeedTest(hostname) }) - log.info('[speed] enabled') + log.info('[speed] enabled,SpeedTestPool:', SpeedTestPool) } function getAllSpeedTester () { const allSpeed = {} - if (!config.getConfig().enabled) { - return allSpeed + + if (config.getConfig().enabled) { + _.forEach(SpeedTestPool, (item, key) => { + allSpeed[key] = { + hostname: item.hostname, + port: item.port, + alive: item.alive, + backupList: item.backupList, + } + }) } - _.forEach(SpeedTestPool, (item, key) => { - allSpeed[key] = { - hostname: key, - alive: item.alive, - backupList: item.backupList, - } - }) + return allSpeed } -function getSpeedTester (hostname) { +function getSpeedTester (hostname, port) { if (!config.getConfig().enabled) { - return + return null } - let instance = SpeedTestPool[hostname] - if (instance == null) { - instance = new SpeedTester({ hostname }) - SpeedTestPool[hostname] = instance - } - return instance + return addSpeedTest(hostname, port) } -function registerNotify (notify) { - config.notify = notify -} +// function registerNotify (notify) { +// config.notify = notify +// } function reSpeedTest () { - _.forEach(SpeedTestPool, (item, key) => { - item.test() + _.forEach(SpeedTestPool, (item, _key) => { + item.test() // 异步 }) } @@ -68,8 +87,8 @@ module.exports = { SpeedTester, initSpeedTest, getSpeedTester, - getAllSpeedTester, - registerNotify, + // getAllSpeedTester, + // registerNotify, reSpeedTest, action, } diff --git a/packages/mitmproxy/src/options.js b/packages/mitmproxy/src/options.js index 6504dee..b9c5f12 100644 --- a/packages/mitmproxy/src/options.js +++ b/packages/mitmproxy/src/options.js @@ -1,7 +1,6 @@ const fs = require('node:fs') const path = require('node:path') const lodash = require('lodash') -const jsonApi = require('./json') const dnsUtil = require('./lib/dns') const interceptorImpls = require('./lib/interceptor') const scriptInterceptor = require('./lib/interceptor/impl/res/script') @@ -110,7 +109,7 @@ module.exports = (serverConfig) => { // 配置了白名单的域名,将跳过代理 const inWhiteList = !!matchUtil.matchHostname(whiteList, hostname, 'in whiteList') if (inWhiteList) { - log.info(`为白名单域名,不拦截: ${hostname}, headers:`, jsonApi.stringify2(req.headers)) + log.info(`为白名单域名,不拦截: ${hostname}`) return false // 不拦截 } @@ -187,12 +186,12 @@ module.exports = (serverConfig) => { if (impl.requestIntercept) { // req拦截器 interceptor.requestIntercept = (context, req, res, ssl, next) => { - return impl.requestIntercept(context, interceptOpt, req, res, ssl, next, matched) + return impl.requestIntercept(context, interceptOpt, req, res, ssl, next, matched, interceptOpts.matched) } } else if (impl.responseIntercept) { // res拦截器 interceptor.responseIntercept = (context, req, res, proxyReq, proxyRes, ssl, next) => { - return impl.responseIntercept(context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next, matched) + return impl.responseIntercept(context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next, matched, interceptOpts.matched) } } diff --git a/packages/mitmproxy/src/utils/util.match.js b/packages/mitmproxy/src/utils/util.match.js index 46bf246..e990699 100644 --- a/packages/mitmproxy/src/utils/util.match.js +++ b/packages/mitmproxy/src/utils/util.match.js @@ -1,5 +1,6 @@ const lodash = require('lodash') const log = require('./util.log.server') +const mergeApi = require('@docmirror/dev-sidecar/src/merge') function isMatched (url, regexp) { if (regexp === '.*' || regexp === '*' || regexp === 'true' || regexp === true) { @@ -113,16 +114,6 @@ function merge (oldObj, newObj) { } }) } -function deleteNullItems (target) { - lodash.forEach(target, (item, key) => { - if (item == null || item === '[delete]') { - delete target[key] - } - if (lodash.isObject(item)) { - deleteNullItems(item) - } - }) -} function matchHostnameAll (hostMap, hostname, action) { // log.debug('matchHostname-all:', action, hostMap) @@ -150,10 +141,29 @@ function matchHostnameAll (hostMap, hostname, action) { // } // 正则表达式匹配 - if (hostname.match(regexp)) { + const matched = hostname.match(regexp) + if (matched) { value = hostMap[regexp] log.debug(`matchHostname-one: ${action}: '${hostname}' -> { "${regexp}": ${JSON.stringify(value)} }`) values = merge(values, value) + + // 设置matched + if (matched.length > 1) { + if (values.matched) { + // 合并array + matched.shift() + values.matched = [...values.matched, ...matched] // 拼接上多个matched + + // 合并groups + if (matched.groups) { + values.matched.groups = merge(values.matched.groups, matched.groups) + } else { + values.matched.groups = matched.groups + } + } else { + values.matched = matched + } + } } } @@ -178,7 +188,7 @@ function matchHostnameAll (hostMap, hostname, action) { } if (!lodash.isEmpty(values)) { - deleteNullItems(values) + mergeApi.deleteNullItems(values) log.info(`matchHostname-all: ${action}: '${hostname}':`, JSON.stringify(values)) return values } else { diff --git a/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs b/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs new file mode 100644 index 0000000..163bb1c --- /dev/null +++ b/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs @@ -0,0 +1,90 @@ +import DNSOverHTTPS from "../src/lib/dns/https.js"; + +// 境外DNS的DoH配置sni测试 +const servers = [ + 'https://dns.quad9.net/dns-query', + 'https://max.rethinkdns.com/dns-query', + 'https://sky.rethinkdns.com/dns-query', + 'https://doh.opendns.com/dns-query', + 'https://cloudflare-dns.com/dns-query', + 'https://dns.google/dns-query', + 'https://dns.bebasid.com/unfiltered', + 'https://0ms.dev/dns-query', + 'https://dns.decloudus.com/dns-query', + 'https://wikimedia-dns.org/dns-query', + 'https://doh.applied-privacy.net/query', + 'https://private.canadianshield.cira.ca/dns-query', + // 'https://dns.controld.com/comss', // 可直连,无需SNI + 'https://kaitain.restena.lu/dns-query', + 'https://doh.libredns.gr/dns-query', + 'https://doh.libredns.gr/ads', + 'https://dns.switch.ch/dns-query', + 'https://doh.nl.ahadns.net/dns-query', + 'https://doh.la.ahadns.net/dns-query', + 'https://dns.dnswarden.com/uncensored', + 'https://doh.ffmuc.net/dns-query', + 'https://dns.oszx.co/dns-query', + 'https://doh.tiarap.org/dns-query', + 'https://jp.tiarap.org/dns-query', + 'https://dns.adguard.com/dns-query', + 'https://rubyfish.cn/dns-query', + 'https://i.233py.com/dns-query', +] + +const hostnames = [ + 'github.com', + 'mvnrepository.com', +] +const sni = 'baidu.com' +// const sni = '' + +console.log(`\n--------------- 测试DoH的SNI功能:共 ${servers.length} 个服务,${hostnames.length} 个域名,SNI: ${sni || '无'} ---------------\n`) + +let n = 0 +let success = 0 +let error = 0 +const arr = [] + +function count (isSuccess, hostname, idx, dns, result, cost) { + if (isSuccess) { + success++ + const ipList = [] + for (const answer of result.answers) { + ipList[ipList.length] = answer.data; + } + arr[idx] = `${dns.dnsServer} : ${hostname} -> [ ${ipList.join(', ')} ] , cost: ${cost} ms`; + } else { + error++ + } + + n++ + + if (n === servers.length * hostnames.length) { + console.info(`\n\n=============================================================================\n全部测完:总计:${servers.length * hostnames.length}, 成功:${success},失败:${error}`); + for (const item of arr) { + if (item) { + console.info(item); + } + } + console.info('=============================================================================\n\n') + } +} + +let x = 0; +for (let i = 0; i < servers.length; i++) { + for (const hostname of hostnames) { + const dns = new DNSOverHTTPS(`dns-${i}-${hostname}`, null, null, servers[i], sni) + const start = Date.now() + const idx = x; + dns._doDnsQuery(hostname) + .then((result) => { + console.info(`===> ${dns.dnsServer}: ${hostname} ->`, result.answers, '\n\n') + count(true, hostname, idx, dns, result, Date.now() - start) + }) + .catch((e) => { + console.error(`===> ${dns.dnsServer}: ${hostname} 失败:`, e, '\n\n') + count(false, hostname) + }) + x++; + } +} diff --git a/packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs b/packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs new file mode 100644 index 0000000..c6cfddb --- /dev/null +++ b/packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs @@ -0,0 +1,107 @@ +import DNSOverTLS from "../src/lib/dns/tls.js"; + +// 境外DNS的DoT配置sni测试 +const servers = [ + // 'dot.360.cn', + + '1.1.1.1', // 可直连,无需SNI(有时候可以,有时候不行) + 'one.one.one.one', + 'cloudflare-dns.com', + 'security.cloudflare-dns.com', + 'family.cloudflare-dns.com', + '1dot1dot1dot1.cloudflare-dns.com', + + 'dot.sb', + '185.222.222.222', + '45.11.45.11', + + 'dns.adguard.com', + 'dns.adguard-dns.com', + 'dns-family.adguard.com', + 'family.adguard-dns.com', + 'dns-unfiltered.adguard.com', + 'unfiltered.adguard-dns.com', + 'dns.bebasid.com', + 'unfiltered.dns.bebasid.com', + 'antivirus.bebasid.com', + 'internetsehat.bebasid.com', + 'family-adblock.bebasid.com', + 'oisd.dns.bebasid.com', + 'hagezi.dns.bebasid.com', + 'dns.cfiec.net', + 'dns.opendns.com', + 'familyshield.opendns.com', + 'sandbox.opendns.com', + 'family-filter-dns.cleanbrowsing.org', + 'adult-filter-dns.cleanbrowsing.org', + 'security-filter-dns.cleanbrowsing.org', + 'p0.freedns.controld.com', + 'p1.freedns.controld.com', + 'p2.freedns.controld.com', + 'p3.freedns.controld.com', + 'dns.decloudus.com', + 'getdnsapi.net', + 'dnsovertls.sinodun.com', + 'dnsovertls1.sinodun.com', + 'dns.de.futuredns.eu.org', + 'dns.us.futuredns.eu.org', + 'unicast.censurfridns.dk', +] + +const hostnames = [ + 'github.com', + 'mvnrepository.com', +] +const sni = 'baidu.com' +// const sni = '' + +console.log(`\n--------------- 测试DoT的SNI功能:共 ${servers.length} 个服务,${hostnames.length} 个域名,SNI: ${sni || '无'} ---------------\n`) + +let n = 0 +let success = 0 +let error = 0 +const arr = [] + +function count (isSuccess, hostname, idx, dns, result, cost) { + if (isSuccess) { + success++ + const ipList = [] + for (const answer of result.answers) { + ipList[ipList.length] = answer.data; + } + arr[idx] = `${dns.dnsServer} : ${hostname} -> [ ${ipList.join(', ')} ] , cost: ${cost} ms`; + } else { + error++ + } + + n++ + + if (n === servers.length * hostnames.length) { + console.info(`\n\n=============================================================================\n全部测完:总计:${servers.length * hostnames.length}, 成功:${success},失败:${error}`); + for (const item of arr) { + if (item) { + console.info(item); + } + } + console.info('=============================================================================\n\n') + } +} + +let x = 0; +for (let i = 0; i < servers.length; i++) { + for (const hostname of hostnames) { + const dns = new DNSOverTLS(`dns-${i}-${hostname}`, null, null, servers[i], null, sni) + const start = Date.now() + const idx = x; + dns._doDnsQuery(hostname) + .then((result) => { + console.info(`===> ${dns.dnsServer}: ${hostname} ->`, result.answers, '\n\n') + count(true, hostname, idx, dns, result, Date.now() - start) + }) + .catch((e) => { + console.error(`===> ${dns.dnsServer}: ${hostname} 失败:`, e, '\n\n') + count(false, hostname) + }) + x++; + } +} diff --git a/packages/mitmproxy/test/dnsTest-abroad.mjs b/packages/mitmproxy/test/dnsTest-abroad.mjs new file mode 100644 index 0000000..7e8a70e --- /dev/null +++ b/packages/mitmproxy/test/dnsTest-abroad.mjs @@ -0,0 +1,149 @@ +import assert from 'node:assert' +import dns from '../src/lib/dns/index.js' +import matchUtil from '../src/utils/util.match.js' + +const presetIp = '100.100.100.100' +const preSetIpList = matchUtil.domainMapRegexply({ + 'xxx.com': [ + presetIp + ] +}) + +// 境外DNS测试 +const dnsProviders = dns.initDNS({ + // udp + cloudflareUdp: { + server: 'udp://1.1.1.1', + }, + quad9Udp: { + server: 'udp://9.9.9.9', + }, + + // tcp + cloudflareTcp: { + server: 'tcp://1.1.1.1', + }, + quad9Tcp: { + server: 'tcp://9.9.9.9', + }, + + // https + cloudflare: { + server: 'https://1.1.1.1/dns-query', + }, + quad9: { + server: 'https://9.9.9.9/dns-query', + forSNI: true, + }, + rubyfish: { + server: 'https://rubyfish.cn/dns-query', + }, + py233: { + server: ' https://i.233py.com/dns-query', + }, + + // tls + cloudflareTLS: { + type: 'tls', + server: '1.1.1.1', + servername: 'cloudflare-dns.com', + }, + quad9TLS: { + server: 'tls://9.9.9.9', + servername: 'dns.quad9.net', + }, +}, preSetIpList) + + +const hasPresetHostname = 'xxx.com' +const noPresetHostname = 'yyy.com' + +const hostname1 = 'github.com' +const hostname2 = 'api.github.com' +const hostname3 = 'hk.docmirror.cn' +const hostname4 = 'github.docmirror.cn' +const hostname5 = 'gh.docmirror.top' +const hostname6 = 'gh2.docmirror.top' + +let ip + + +console.log('\n--------------- test ForSNI ---------------\n') +console.log(`===> test ForSNI: ${dnsProviders.ForSNI.dnsName}`, '\n\n') +assert.strictEqual(dnsProviders.ForSNI, dnsProviders.quad9) + + +console.log('\n--------------- test PreSet ---------------\n') +ip = await dnsProviders.PreSet.lookup(hasPresetHostname) +console.log(`===> test PreSet: ${hasPresetHostname} ->`, ip, '\n\n') +console.log('\n\n') +assert.strictEqual(ip, presetIp) // 预设过IP,等于预设的IP + +ip = await dnsProviders.PreSet.lookup(noPresetHostname) +console.log(`===> test PreSet: ${noPresetHostname} ->`, ip, '\n\n') +console.log('\n\n') +assert.strictEqual(ip, noPresetHostname) // 未预设IP,等于域名自己 + + +console.log('\n--------------- test udp ---------------\n') +ip = await dnsProviders.cloudflareUdp.lookup(hasPresetHostname) +assert.strictEqual(ip, presetIp) // test preset +console.log('\n\n') + +assert.strictEqual(dnsProviders.cloudflareUdp.dnsType, 'UDP') +ip = await dnsProviders.cloudflareUdp.lookup(hostname1) +console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n') + +assert.strictEqual(dnsProviders.quad9Udp.dnsType, 'UDP') +ip = await dnsProviders.quad9Udp.lookup(hostname1) +console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n') + + +console.log('\n--------------- test tcp ---------------\n') +ip = await dnsProviders.cloudflareTcp.lookup(hasPresetHostname) +assert.strictEqual(ip, presetIp) // test preset +console.log('\n\n') + +assert.strictEqual(dnsProviders.cloudflareTcp.dnsType, 'TCP') +ip = await dnsProviders.cloudflareTcp.lookup(hostname1) +console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n') + +assert.strictEqual(dnsProviders.quad9Tcp.dnsType, 'TCP') +ip = await dnsProviders.quad9Tcp.lookup(hostname1) +console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n') + + +console.log('\n--------------- test https ---------------\n') +ip = await dnsProviders.cloudflare.lookup(hasPresetHostname) +assert.strictEqual(ip, presetIp) // test preset +console.log('\n\n') + +assert.strictEqual(dnsProviders.cloudflare.dnsType, 'HTTPS') +ip = await dnsProviders.cloudflare.lookup(hostname1) +console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n') + +assert.strictEqual(dnsProviders.quad9.dnsType, 'HTTPS') +ip = await dnsProviders.quad9.lookup(hostname1) +console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n') + +assert.strictEqual(dnsProviders.rubyfish.dnsType, 'HTTPS') +ip = await dnsProviders.rubyfish.lookup(hostname1) +console.log(`===> test rubyfish: ${hostname1} ->`, ip, '\n\n') + +assert.strictEqual(dnsProviders.py233.dnsType, 'HTTPS') +ip = await dnsProviders.py233.lookup(hostname1) +console.log(`===> test py233: ${hostname1} ->`, ip, '\n\n') + + +console.log('\n--------------- test TLS ---------------\n') +ip = await dnsProviders.cloudflareTLS.lookup(hasPresetHostname) +assert.strictEqual(ip, presetIp) // test preset +console.log('\n\n') + +assert.strictEqual(dnsProviders.cloudflareTLS.dnsType, 'TLS') +ip = await dnsProviders.cloudflareTLS.lookup(hostname1) +console.log(`===> test cloudflareTLS: ${hostname1} ->`, ip, '\n\n') + +assert.strictEqual(dnsProviders.quad9TLS.dnsType, 'TLS') +ip = await dnsProviders.quad9TLS.lookup(hostname1) +console.log(`===> test quad9TLS: ${hostname1} ->`, ip, '\n\n') diff --git a/packages/mitmproxy/test/dnsTest.mjs b/packages/mitmproxy/test/dnsTest.mjs index 75a36b0..65df397 100644 --- a/packages/mitmproxy/test/dnsTest.mjs +++ b/packages/mitmproxy/test/dnsTest.mjs @@ -9,17 +9,9 @@ const preSetIpList = matchUtil.domainMapRegexply({ ] }) +// 常用DNS测试 const dnsProviders = dns.initDNS({ // https - cloudflare: { - type: 'https', - server: 'https://1.1.1.1/dns-query', - cacheSize: 1000, - }, - quad9: { - server: 'https://9.9.9.9/dns-query', - cacheSize: 1000, - }, aliyun: { type: 'https', server: 'https://dns.alidns.com/dns-query', @@ -33,28 +25,10 @@ const dnsProviders = dns.initDNS({ safe360: { server: 'https://doh.360.cn/dns-query', cacheSize: 1000, - }, - rubyfish: { - server: 'https://rubyfish.cn/dns-query', - cacheSize: 1000, - }, - py233: { - server: ' https://i.233py.com/dns-query', - cacheSize: 1000, + forSNI: true, }, // tls - cloudflareTLS: { - type: 'tls', - server: '1.1.1.1', - servername: 'cloudflare-dns.com', - cacheSize: 1000, - }, - quad9TLS: { - server: 'tls://9.9.9.9', - servername: 'dns.quad9.net', - cacheSize: 1000, - }, aliyunTLS: { server: 'tls://223.5.5.5:853', cacheSize: 1000, @@ -93,7 +67,9 @@ const dnsProviders = dns.initDNS({ }, preSetIpList) -const presetHostname = 'xxx.com' +const hasPresetHostname = 'xxx.com' +const noPresetHostname = 'yyy.com' + const hostname1 = 'github.com' const hostname2 = 'api.github.com' const hostname3 = 'hk.docmirror.cn' @@ -104,26 +80,36 @@ const hostname6 = 'gh2.docmirror.top' let ip +console.log('\n--------------- test ForSNI ---------------\n') +console.log(`===> test ForSNI: ${dnsProviders.ForSNI.dnsName}`, '\n\n') +assert.strictEqual(dnsProviders.ForSNI, dnsProviders.safe360) + +const dnsProviders2 = dns.initDNS({ + aliyun: { + server: 'udp://223.5.5.5', + }, +}, {}) +console.log(`===> test ForSNI2: ${dnsProviders2.ForSNI.dnsName}`, '\n\n') +assert.strictEqual(dnsProviders2.ForSNI, dnsProviders2.PreSet) // 未配置forSNI的DNS时,默认使用PreSet作为ForSNI + + console.log('\n--------------- test PreSet ---------------\n') -ip = await dnsProviders.PreSet.lookup(presetHostname) -console.log('===> test PreSet:', ip, '\n\n') +ip = await dnsProviders.PreSet.lookup(hasPresetHostname) +console.log(`===> test PreSet: ${hasPresetHostname} ->`, ip, '\n\n') console.log('\n\n') -assert.strictEqual(ip, presetIp) // test preset +assert.strictEqual(ip, presetIp) // 预设过IP,等于预设的IP + +ip = await dnsProviders.PreSet.lookup(noPresetHostname) +console.log(`===> test PreSet: ${noPresetHostname} ->`, ip, '\n\n') +console.log('\n\n') +assert.strictEqual(ip, noPresetHostname) // 未预设IP,等于域名自己 console.log('\n--------------- test https ---------------\n') -ip = await dnsProviders.cloudflare.lookup(presetHostname) +ip = await dnsProviders.aliyun.lookup(hasPresetHostname) assert.strictEqual(ip, presetIp) // test preset console.log('\n\n') -assert.strictEqual(dnsProviders.cloudflare.dnsType, 'HTTPS') -// ip = await dnsProviders.cloudflare.lookup(hostname1) -// console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n') - -assert.strictEqual(dnsProviders.quad9.dnsType, 'HTTPS') -// ip = await dnsProviders.quad9.lookup(hostname1) -// console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n') - assert.strictEqual(dnsProviders.aliyun.dnsType, 'HTTPS') ip = await dnsProviders.aliyun.lookup(hostname1) console.log(`===> test aliyun: ${hostname1} ->`, ip, '\n\n') @@ -136,28 +122,12 @@ assert.strictEqual(dnsProviders.safe360.dnsType, 'HTTPS') ip = await dnsProviders.safe360.lookup(hostname1) console.log(`===> test safe360: ${hostname1} ->`, ip, '\n\n') -assert.strictEqual(dnsProviders.rubyfish.dnsType, 'HTTPS') -// ip = await dnsProviders.rubyfish.lookup(hostname1) -// console.log(`===> test rubyfish: ${hostname1} ->`, ip, '\n\n') - -assert.strictEqual(dnsProviders.py233.dnsType, 'HTTPS') -// ip = await dnsProviders.py233.lookup(hostname1) -// console.log(`===> test py233: ${hostname1} ->`, ip, '\n\n') - console.log('\n--------------- test TLS ---------------\n') -ip = await dnsProviders.cloudflareTLS.lookup(presetHostname) +ip = await dnsProviders.aliyunTLS.lookup(hasPresetHostname) assert.strictEqual(ip, presetIp) // test preset console.log('\n\n') -assert.strictEqual(dnsProviders.cloudflareTLS.dnsType, 'TLS') -// ip = await dnsProviders.cloudflareTLS.lookup(hostname1) -// console.log(`===> test cloudflareTLS: ${hostname1} ->`, ip, '\n\n') - -assert.strictEqual(dnsProviders.quad9TLS.dnsType, 'TLS') -// ip = await dnsProviders.quad9TLS.lookup(hostname1) -// console.log(`===> test quad9TLS: ${hostname1} ->`, ip, '\n\n') - assert.strictEqual(dnsProviders.aliyunTLS.dnsType, 'TLS') ip = await dnsProviders.aliyunTLS.lookup(hostname1) console.log(`===> test aliyunTLS: ${hostname1} ->`, ip, '\n\n') @@ -172,7 +142,7 @@ console.log(`===> test safe360TLS: ${hostname1} ->`, ip, '\n\n') console.log('\n--------------- test TCP ---------------\n') -ip = await dnsProviders.googleTCP.lookup(presetHostname) +ip = await dnsProviders.googleTCP.lookup(hasPresetHostname) assert.strictEqual(ip, presetIp) // test preset console.log('\n\n') @@ -186,7 +156,7 @@ console.log(`===> test aliyunTCP: ${hostname1} ->`, ip, '\n\n') console.log('\n--------------- test UDP ---------------\n') -ip = await dnsProviders.googleUDP.lookup(presetHostname) +ip = await dnsProviders.googleUDP.lookup(hasPresetHostname) assert.strictEqual(ip, presetIp) // test preset console.log('\n\n') @@ -200,17 +170,13 @@ console.log(`===> test aliyunUDP: ${hostname1} ->`, ip, '\n\n') dnsProviders.aliyunUDP.lookup(hostname1).then(ip0 => { console.log(`===> test aliyunUDP: ${hostname1} ->`, ip0, '\n\n') - assert.strictEqual(ip0, ip) }) dnsProviders.aliyunUDP.lookup(hostname2).then(ip0 => { console.log(`===> test aliyunUDP: ${hostname2} ->`, ip0, '\n\n') - assert.notStrictEqual(ip0, ip) }) dnsProviders.aliyunUDP.lookup('baidu.com').then(ip0 => { console.log('===> test aliyunUDP: baidu.com ->', ip0, '\n\n') - assert.notStrictEqual(ip0, ip) }) dnsProviders.aliyunUDP.lookup('gitee.com').then(ip0 => { console.log('===> test aliyunUDP: gitee.com ->', ip0, '\n\n') - assert.notStrictEqual(ip0, ip) }) diff --git a/packages/mitmproxy/test/lodashTest.js b/packages/mitmproxy/test/lodashTest.js index b9db645..f3a7a4f 100644 --- a/packages/mitmproxy/test/lodashTest.js +++ b/packages/mitmproxy/test/lodashTest.js @@ -26,3 +26,12 @@ assert.strictEqual(isEmpty(0), false) assert.strictEqual(isEmpty(-1), false) assert.strictEqual(isEmpty(''), false) assert.strictEqual(isEmpty('1'), false) + +// test lodash.unionBy +const list = [ + { host: 1, port: 1, dns: 2 }, + { host: 1, port: 1, dns: 3 }, + { host: 1, port: 2, dns: 3 }, + { host: 1, port: 2, dns: 3 }, +] +console.info(lodash.unionBy(list, 'host', 'port')) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e46e3cc..6f06d8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,9 +169,6 @@ importers: dns-over-http: specifier: ^0.2.0 version: 0.2.0 - dns-over-tls: - specifier: ^0.0.9 - version: 0.0.9 is-browser: specifier: ^2.1.0 version: 2.1.0 @@ -3045,9 +3042,6 @@ packages: dns-over-http@0.2.0: resolution: {integrity: sha512-K+SyN2L3ljxJ2MFtOv/vRS+3/YEMLvOuH7MrmO5ejaubi4w02/DLqzoK1kBGKlQrT9ND57pbapeDf+ue8AElEA==} - dns-over-tls@0.0.9: - resolution: {integrity: sha512-IdI/Qgku2KQPLtUBsC6HDdK6bWIZADQMOYWtqJzRivV5Z+EDKIVAaC+tWE6lJ9vn11qj5L39ZaIaTj/14Lzgkw==} - dns-packet@4.2.0: resolution: {integrity: sha512-bn1AKpfkFbm0MIioOMHZ5qJzl2uypdBwI4nYNsqvhjsegBhcKJUlCrMPWLx6JEezRjxZmxhtIz/FkBEur2l8Cw==} engines: {node: '>=4'} @@ -10553,10 +10547,6 @@ snapshots: transitivePeerDependencies: - supports-color - dns-over-tls@0.0.9: - dependencies: - dns-packet: 5.6.1 - dns-packet@4.2.0: dependencies: ip: 1.1.9