1)feature: IP测速功能,针对请求的端口进行测速,不再固定 `443` 端口进行测速;
2)bugfix: IP测速功能,域名长时间未访问时,再访问后不会重启自动测速功能的问题修复; 3)optimize: 优化DNS获取的IP再经过IpTester校验的逻辑。develop
parent
ab74a44e83
commit
ab76d03f0b
|
@ -455,9 +455,9 @@ export default {
|
||||||
</a>
|
</a>
|
||||||
<a-tag
|
<a-tag
|
||||||
v-for="(element, index) of item.backupList" :key="index" style="margin:2px;"
|
v-for="(element, index) of item.backupList" :key="index" style="margin:2px;"
|
||||||
:title="element.dns" :color="element.time ? (element.time > config.server.setting.lowSpeedDelay ? 'orange' : 'green') : 'red'"
|
:title="element.title || `测速中:${element.host}`" :color="element.time ? (element.time > config.server.setting.lowSpeedDelay ? 'orange' : 'green') : (element.title ? 'red' : '')"
|
||||||
>
|
>
|
||||||
{{ element.host }} {{ element.time }}{{ element.time ? 'ms' : '' }} {{ element.dns }}
|
{{ element.host }} {{ element.time ? `${element.time}ms` : (element.title ? '' : '测速中') }} {{ element.dns }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
|
@ -152,6 +152,12 @@ $dark-input: #777; //输入框:背景色
|
||||||
border-color: #5a5750;
|
border-color: #5a5750;
|
||||||
color: #cfa572;
|
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) {
|
.ant-btn:not(.ant-btn-danger, .ant-btn-primary) {
|
||||||
|
|
|
@ -52,13 +52,18 @@ module.exports = class BaseDNS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async lookup (hostname) {
|
async lookup (hostname, ipChecker) {
|
||||||
try {
|
try {
|
||||||
let ipCache = this.cache.get(hostname)
|
let ipCache = this.cache.get(hostname)
|
||||||
if (ipCache) {
|
if (ipCache) {
|
||||||
if (ipCache.value != null) {
|
const ip = ipCache.value
|
||||||
ipCache.doCount(ipCache.value, false)
|
if (ip != null) {
|
||||||
return ipCache.value
|
if (ipChecker && ipChecker(ip)) {
|
||||||
|
ipCache.doCount(ip, false)
|
||||||
|
return ip
|
||||||
|
} else {
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ipCache = new IpCache(hostname)
|
ipCache = new IpCache(hostname)
|
||||||
|
@ -66,7 +71,7 @@ module.exports = class BaseDNS {
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = new Date()
|
const t = new Date()
|
||||||
let ipList = await this._lookupInternal(hostname)
|
let ipList = await this._lookupWithPreSetIpList(hostname)
|
||||||
if (ipList == null) {
|
if (ipList == null) {
|
||||||
// 没有获取到ipv4地址
|
// 没有获取到ipv4地址
|
||||||
ipList = []
|
ipList = []
|
||||||
|
@ -74,28 +79,42 @@ module.exports = class BaseDNS {
|
||||||
ipList.push(hostname) // 把原域名加入到统计里去
|
ipList.push(hostname) // 把原域名加入到统计里去
|
||||||
|
|
||||||
ipCache.setBackupList(ipList)
|
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} (${new Date() - 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) {
|
} catch (error) {
|
||||||
log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] cannot resolve hostname ${hostname}, error:`, error)
|
log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] cannot resolve hostname ${hostname}, error:`, error)
|
||||||
return hostname
|
return hostname
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _lookupInternal (hostname) {
|
async _lookupWithPreSetIpList (hostname) {
|
||||||
// 获取当前域名的预设IP列表
|
// 获取当前域名的预设IP列表
|
||||||
let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`)
|
let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`)
|
||||||
if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) {
|
if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) {
|
||||||
if (hostnamePreSetIpList.length > 0) {
|
if (hostnamePreSetIpList.length > 0) {
|
||||||
hostnamePreSetIpList = hostnamePreSetIpList.slice()
|
hostnamePreSetIpList = hostnamePreSetIpList.slice() // 复制一份列表数据,避免配置数据被覆盖
|
||||||
} else {
|
} else {
|
||||||
hostnamePreSetIpList = mapToList(hostnamePreSetIpList)
|
hostnamePreSetIpList = mapToList(hostnamePreSetIpList)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostnamePreSetIpList.length > 0) {
|
if (hostnamePreSetIpList.length > 0) {
|
||||||
hostnamePreSetIpList.isPreSet = true
|
hostnamePreSetIpList.isPreSet = true
|
||||||
log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的预设IP列表: ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`)
|
log.info(`[DNS-over-PreSet '${this.dnsName}'] 获取到该域名的预设IP列表: ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`)
|
||||||
return hostnamePreSetIpList
|
return hostnamePreSetIpList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,23 +125,59 @@ module.exports = class BaseDNS {
|
||||||
async _lookup (hostname) {
|
async _lookup (hostname) {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
try {
|
try {
|
||||||
|
// 执行DNS查询
|
||||||
|
log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query start: ${hostname}`)
|
||||||
const response = await this._doDnsQuery(hostname)
|
const response = await this._doDnsQuery(hostname)
|
||||||
const cost = Date.now() - start
|
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) {
|
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)
|
log.warn(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms, response:`, response)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret = response.answers.filter(item => item.type === 'A').map(item => item.data)
|
const ret = response.answers.filter(item => item.type === 'A').map(item => item.data)
|
||||||
if (ret.length === 0) {
|
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 {
|
} 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
|
return ret
|
||||||
} catch (e) {
|
} 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}'] DNS query error, hostname: ${hostname}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}, cost: ${Date.now() - start} ms, error:`, e)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_doDnsQuery (hostname, type) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 设置超时任务
|
||||||
|
let isOver = false
|
||||||
|
const timeout = 6000
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (!isOver) {
|
||||||
|
reject(new Error('DNS查询超时'))
|
||||||
|
}
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._dnsQueryPromise(hostname, type)
|
||||||
|
.then((response) => {
|
||||||
|
isOver = true
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
resolve(response)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
isOver = true
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
reject(e)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
isOver = true
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ module.exports = class DNSOverHTTPS extends BaseDNS {
|
||||||
this.dnsServer = dnsServer
|
this.dnsServer = dnsServer
|
||||||
}
|
}
|
||||||
|
|
||||||
async _doDnsQuery (hostname) {
|
_dnsQueryPromise (hostname, type = 'A') {
|
||||||
return await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }])
|
return dohQueryAsync({ url: this.dnsServer }, [{ type, name: hostname }])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ module.exports = class DNSOverTCP extends BaseDNS {
|
||||||
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
|
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
|
||||||
}
|
}
|
||||||
|
|
||||||
_doDnsQuery (hostname) {
|
_dnsQueryPromise (hostname, type = 'A') {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 构造 DNS 查询报文
|
// 构造 DNS 查询报文
|
||||||
const packet = dnsPacket.encode({
|
const packet = dnsPacket.encode({
|
||||||
|
@ -21,7 +21,7 @@ module.exports = class DNSOverTCP extends BaseDNS {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
id: randi(0x0, 0xFFFF),
|
id: randi(0x0, 0xFFFF),
|
||||||
questions: [{
|
questions: [{
|
||||||
type: 'A',
|
type,
|
||||||
name: hostname,
|
name: hostname,
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@ module.exports = class DNSOverTLS extends BaseDNS {
|
||||||
this.dnsServerName = dnsServerName
|
this.dnsServerName = dnsServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
async _doDnsQuery (hostname) {
|
_dnsQueryPromise (hostname, type = 'A') {
|
||||||
const options = {
|
const options = {
|
||||||
host: this.dnsServer,
|
host: this.dnsServer,
|
||||||
port: this.dnsServerPort,
|
port: this.dnsServerPort,
|
||||||
|
@ -19,9 +19,9 @@ module.exports = class DNSOverTLS extends BaseDNS {
|
||||||
|
|
||||||
name: hostname,
|
name: hostname,
|
||||||
klass: 'IN',
|
klass: 'IN',
|
||||||
type: 'A',
|
type,
|
||||||
}
|
}
|
||||||
|
|
||||||
return await dnstls.query(options)
|
return dnstls.query(options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,21 +15,28 @@ module.exports = class DNSOverUDP extends BaseDNS {
|
||||||
this.socketType = this.isIPv6 ? 'udp6' : 'udp4'
|
this.socketType = this.isIPv6 ? 'udp6' : 'udp4'
|
||||||
}
|
}
|
||||||
|
|
||||||
_doDnsQuery (hostname) {
|
_dnsQueryPromise (hostname, type = 'A') {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
let isOver = false
|
||||||
|
const timeout = 5000
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
// 构造 DNS 查询报文
|
// 构造 DNS 查询报文
|
||||||
const packet = dnsPacket.encode({
|
const packet = dnsPacket.encode({
|
||||||
flags: dnsPacket.RECURSION_DESIRED,
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
type: 'query',
|
type: 'query',
|
||||||
id: randi(0x0, 0xFFFF),
|
id: randi(0x0, 0xFFFF),
|
||||||
questions: [{
|
questions: [{
|
||||||
type: 'A',
|
type,
|
||||||
name: hostname,
|
name: hostname,
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 创建客户端
|
// 创建客户端
|
||||||
const udpClient = dgram.createSocket(this.socketType, (msg, _rinfo) => {
|
const udpClient = dgram.createSocket(this.socketType, (msg, _rinfo) => {
|
||||||
|
isOver = true
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
const response = dnsPacket.decode(msg)
|
const response = dnsPacket.decode(msg)
|
||||||
resolve(response)
|
resolve(response)
|
||||||
udpClient.close()
|
udpClient.close()
|
||||||
|
@ -38,10 +45,20 @@ module.exports = class DNSOverUDP extends BaseDNS {
|
||||||
// 发送 UDP 查询
|
// 发送 UDP 查询
|
||||||
udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err, _bytes) => {
|
udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err, _bytes) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
isOver = true
|
||||||
|
clearTimeout(timeoutId)
|
||||||
reject(err)
|
reject(err)
|
||||||
udpClient.close()
|
udpClient.close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置超时任务
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
if (!isOver) {
|
||||||
|
reject(new Error('查询超时'))
|
||||||
|
udpClient.close()
|
||||||
|
}
|
||||||
|
}, timeout)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ function connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDire
|
||||||
if (dnsConfig && dnsConfig.dnsMap) {
|
if (dnsConfig && dnsConfig.dnsMap) {
|
||||||
const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname)
|
const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname)
|
||||||
if (dns) {
|
if (dns) {
|
||||||
options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, isDnsIntercept)
|
options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, port, isDnsIntercept)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 代理连接事件监听
|
// 代理连接事件监听
|
||||||
|
|
|
@ -124,7 +124,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (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}`)
|
log.debug(`域名 ${rOptions.hostname} DNS: ${dns.dnsName}`)
|
||||||
res.setHeader('DS-DNS', dns.dnsName)
|
res.setHeader('DS-DNS', dns.dnsName)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,12 +2,35 @@ const defaultDns = require('node:dns')
|
||||||
const log = require('../../../utils/util.log.server')
|
const log = require('../../../utils/util.log.server')
|
||||||
const speedTest = require('../../speed')
|
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 = {
|
module.exports = {
|
||||||
createLookupFunc (res, dns, action, target, isDnsIntercept) {
|
createLookupFunc (res, dns, action, target, port, isDnsIntercept) {
|
||||||
target = target ? (`, target: ${target}`) : ''
|
target = target ? (`, target: ${target}`) : ''
|
||||||
|
|
||||||
return (hostname, options, callback) => {
|
return (hostname, options, callback) => {
|
||||||
const tester = speedTest.getSpeedTester(hostname)
|
const tester = speedTest.getSpeedTester(hostname, port)
|
||||||
if (tester) {
|
if (tester) {
|
||||||
const aliveIpObj = tester.pickFastAliveIpObj()
|
const aliveIpObj = tester.pickFastAliveIpObj()
|
||||||
if (aliveIpObj) {
|
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)} }`)
|
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) {
|
if (isDnsIntercept) {
|
||||||
isDnsIntercept.dns = dns
|
isDnsIntercept.dns = dns
|
||||||
isDnsIntercept.hostname = hostname
|
isDnsIntercept.hostname = hostname
|
||||||
|
@ -29,35 +55,16 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ip !== hostname) {
|
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} -----`)
|
log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`)
|
||||||
if (res) {
|
if (res) {
|
||||||
res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`)
|
res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`)
|
||||||
}
|
}
|
||||||
callback(null, ip, 4)
|
callback(null, ip, 4)
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
// 使用默认dns
|
// 使用默认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 default DNS: ${hostname}${target}, options:`, options, ', dns:', dns)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 使用默认dns
|
|
||||||
log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns)
|
|
||||||
}
|
|
||||||
defaultDns.lookup(hostname, options, callback)
|
defaultDns.lookup(hostname, options, callback)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,31 +1,44 @@
|
||||||
// 1个小时不访问,取消获取
|
// const { exec } = require('node:child_process')
|
||||||
const net = require('node:net')
|
const net = require('node:net')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const log = require('../../utils/util.log.server')
|
const log = require('../../utils/util.log.server')
|
||||||
const config = require('./config.js')
|
const config = require('./config.js')
|
||||||
|
|
||||||
|
// const isWindows = process.platform === 'win32'
|
||||||
|
|
||||||
const DISABLE_TIMEOUT = 60 * 60 * 1000
|
const DISABLE_TIMEOUT = 60 * 60 * 1000
|
||||||
|
|
||||||
class SpeedTester {
|
class SpeedTester {
|
||||||
constructor ({ hostname }) {
|
constructor ({ hostname, port }) {
|
||||||
this.dnsMap = config.getConfig().dnsMap
|
this.dnsMap = config.getConfig().dnsMap
|
||||||
|
|
||||||
this.hostname = hostname
|
this.hostname = hostname
|
||||||
this.lastReadTime = Date.now()
|
this.port = port || 443
|
||||||
|
|
||||||
this.ready = false
|
this.ready = false
|
||||||
this.alive = []
|
this.alive = []
|
||||||
this.backupList = []
|
this.backupList = []
|
||||||
this.keepCheckId = false
|
|
||||||
|
|
||||||
this.loadingIps = false
|
|
||||||
this.loadingTest = false
|
|
||||||
|
|
||||||
this.testCount = 0
|
this.testCount = 0
|
||||||
this.test()
|
this.lastReadTime = Date.now()
|
||||||
|
this.keepCheckIntervalId = false
|
||||||
|
|
||||||
|
this.tryTestCount = 0
|
||||||
|
|
||||||
|
this.test() // 异步:初始化完成后先测速一次
|
||||||
}
|
}
|
||||||
|
|
||||||
pickFastAliveIpObj () {
|
pickFastAliveIpObj () {
|
||||||
this.touch()
|
this.touch()
|
||||||
|
|
||||||
if (this.alive.length === 0) {
|
if (this.alive.length === 0) {
|
||||||
|
if (this.backupList.length > 0 && this.tryTestCount % 10 > 0) {
|
||||||
|
this.testBackups() // 异步
|
||||||
|
} else if (this.tryTestCount % 10 === 0) {
|
||||||
this.test() // 异步
|
this.test() // 异步
|
||||||
|
}
|
||||||
|
this.tryTestCount++
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return this.alive[0]
|
return this.alive[0]
|
||||||
|
@ -33,26 +46,27 @@ class SpeedTester {
|
||||||
|
|
||||||
touch () {
|
touch () {
|
||||||
this.lastReadTime = Date.now()
|
this.lastReadTime = Date.now()
|
||||||
if (!this.keepCheckId) {
|
if (!this.keepCheckIntervalId) {
|
||||||
this.startChecker()
|
this.startChecker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startChecker () {
|
startChecker () {
|
||||||
if (this.keepCheckId) {
|
if (this.keepCheckIntervalId) {
|
||||||
clearInterval(this.keepCheckId)
|
clearInterval(this.keepCheckIntervalId)
|
||||||
}
|
}
|
||||||
this.keepCheckId = setInterval(() => {
|
this.keepCheckIntervalId = setInterval(() => {
|
||||||
if (Date.now() - DISABLE_TIMEOUT > this.lastReadTime) {
|
if (Date.now() - DISABLE_TIMEOUT > this.lastReadTime) {
|
||||||
// 超过很长时间没有访问,取消测试
|
// 超过很长时间没有访问,取消测试
|
||||||
clearInterval(this.keepCheckId)
|
clearInterval(this.keepCheckIntervalId)
|
||||||
|
this.keepCheckIntervalId = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.alive.length > 0) {
|
if (this.alive.length > 0) {
|
||||||
this.testBackups()
|
this.testBackups() // 异步
|
||||||
return
|
} else {
|
||||||
|
this.test() // 异步
|
||||||
}
|
}
|
||||||
this.test()
|
|
||||||
}, config.getConfig().interval)
|
}, config.getConfig().interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,44 +85,56 @@ class SpeedTester {
|
||||||
promiseList.push(one)
|
promiseList.push(one)
|
||||||
}
|
}
|
||||||
await Promise.all(promiseList)
|
await Promise.all(promiseList)
|
||||||
|
|
||||||
const items = []
|
const items = []
|
||||||
for (const ip in ips) {
|
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
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFromOneDns (dns) {
|
async getFromOneDns (dns) {
|
||||||
return await dns._lookupInternal(this.hostname)
|
return await dns._lookupWithPreSetIpList(this.hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
async test () {
|
async test () {
|
||||||
|
this.testCount++
|
||||||
|
log.debug(`[speed] test start: ${this.hostname}, testCount: ${this.testCount}`)
|
||||||
|
|
||||||
|
try {
|
||||||
const newList = await this.getIpListFromDns(this.dnsMap)
|
const newList = await this.getIpListFromDns(this.dnsMap)
|
||||||
const newBackupList = [...newList, ...this.backupList]
|
const newBackupList = [...newList, ...this.backupList]
|
||||||
this.backupList = _.unionBy(newBackupList, 'host')
|
this.backupList = _.unionBy(newBackupList, 'host')
|
||||||
this.testCount++
|
|
||||||
|
|
||||||
log.info('[speed]', this.hostname, '➜ ip-list:', this.backupList)
|
|
||||||
await this.testBackups()
|
await this.testBackups()
|
||||||
|
log.info(`[speed] test end: ${this.hostname} ➜ ip-list:`, this.backupList, `, testCount: ${this.testCount}`)
|
||||||
if (config.notify) {
|
if (config.notify) {
|
||||||
config.notify({ key: 'test' })
|
config.notify({ key: 'test' })
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`[speed] test failed: ${this.hostname}, testCount: ${this.testCount}, error:`, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async testBackups () {
|
async testBackups () {
|
||||||
const testAll = []
|
if (this.backupList.length > 0) {
|
||||||
const aliveList = []
|
const aliveList = []
|
||||||
|
|
||||||
|
const testAll = []
|
||||||
for (const item of this.backupList) {
|
for (const item of this.backupList) {
|
||||||
testAll.push(this.doTest(item, aliveList))
|
testAll.push(this.doTest(item, aliveList))
|
||||||
}
|
}
|
||||||
await Promise.all(testAll)
|
await Promise.all(testAll)
|
||||||
this.alive = aliveList
|
this.alive = aliveList
|
||||||
|
}
|
||||||
|
|
||||||
this.ready = true
|
this.ready = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async doTest (item, aliveList) {
|
async doTest (item, aliveList) {
|
||||||
try {
|
try {
|
||||||
const ret = await this.testOne(item)
|
const ret = await this.testOne(item)
|
||||||
|
item.title = `${ret.by}测速成功:${item.host}`
|
||||||
|
log.info(`[speed] test success: ${this.hostname} ➜ ${item.host}:${this.port} from DNS '${item.dns}'`)
|
||||||
_.merge(item, ret)
|
_.merge(item, ret)
|
||||||
aliveList.push({ ...ret, ...item })
|
aliveList.push({ ...ret, ...item })
|
||||||
aliveList.sort((a, b) => a.time - b.time)
|
aliveList.sort((a, b) => a.time - b.time)
|
||||||
|
@ -125,48 +151,131 @@ class SpeedTester {
|
||||||
return a.time - b.time
|
return a.time - b.time
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== 'timeout') {
|
if (item.time == null) {
|
||||||
log.warn('[speed] test error: ', this.hostname, `➜ ${item.host}:${item.port} from DNS '${item.dns}'`, ', errorMsg:', e.message)
|
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) {
|
testByTCP (item) {
|
||||||
const timeout = 5000
|
|
||||||
const { host, port, dns } = item
|
|
||||||
const startTime = Date.now()
|
|
||||||
let isOver = false
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const { host, dns } = item
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
let isOver = false
|
||||||
|
const timeout = 5000
|
||||||
let timeoutId = null
|
let timeoutId = null
|
||||||
const client = net.createConnection({ host, port }, () => {
|
|
||||||
// 'connect' 监听器
|
const client = net.createConnection({ host, port: this.port }, () => {
|
||||||
const connectionTime = Date.now()
|
|
||||||
isOver = true
|
isOver = true
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
resolve({ status: 'success', time: connectionTime - startTime })
|
|
||||||
|
const connectionTime = Date.now()
|
||||||
|
resolve({ status: 'success', by: 'TCP', time: connectionTime - startTime })
|
||||||
client.end()
|
client.end()
|
||||||
})
|
})
|
||||||
client.on('end', () => {
|
|
||||||
})
|
|
||||||
client.on('error', (e) => {
|
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
|
isOver = true
|
||||||
clearTimeout(timeoutId)
|
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)
|
reject(e)
|
||||||
|
client.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
if (isOver) {
|
if (isOver) {
|
||||||
return
|
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'))
|
reject(new Error('timeout'))
|
||||||
client.end()
|
client.end()
|
||||||
}, timeout)
|
}, 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', 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测速失败:${e.message}`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SpeedTester
|
module.exports = SpeedTester
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const config = {
|
const config = {
|
||||||
notify () {},
|
|
||||||
dnsMap: {},
|
dnsMap: {},
|
||||||
}
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getConfig () {
|
getConfig () {
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
|
notify: null,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,28 @@ const SpeedTester = require('./SpeedTester.js')
|
||||||
const SpeedTestPool = {
|
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) {
|
function initSpeedTest (runtimeConfig) {
|
||||||
const { enabled, hostnameList } = runtimeConfig
|
const { enabled, hostnameList } = runtimeConfig
|
||||||
const conf = config.getConfig()
|
const conf = config.getConfig()
|
||||||
|
@ -14,45 +36,42 @@ function initSpeedTest (runtimeConfig) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_.forEach(hostnameList, (hostname) => {
|
_.forEach(hostnameList, (hostname) => {
|
||||||
SpeedTestPool[hostname] = new SpeedTester({ hostname })
|
addSpeedTest(hostname)
|
||||||
})
|
})
|
||||||
log.info('[speed] enabled')
|
log.info('[speed] enabled,SpeedTestPool:', SpeedTestPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSpeedTester () {
|
function getAllSpeedTester () {
|
||||||
const allSpeed = {}
|
const allSpeed = {}
|
||||||
if (!config.getConfig().enabled) {
|
|
||||||
return allSpeed
|
if (config.getConfig().enabled) {
|
||||||
}
|
|
||||||
_.forEach(SpeedTestPool, (item, key) => {
|
_.forEach(SpeedTestPool, (item, key) => {
|
||||||
allSpeed[key] = {
|
allSpeed[key] = {
|
||||||
hostname: key,
|
hostname: item.hostname,
|
||||||
|
port: item.port,
|
||||||
alive: item.alive,
|
alive: item.alive,
|
||||||
backupList: item.backupList,
|
backupList: item.backupList,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return allSpeed
|
return allSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSpeedTester (hostname) {
|
function getSpeedTester (hostname, port) {
|
||||||
if (!config.getConfig().enabled) {
|
if (!config.getConfig().enabled) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
let instance = SpeedTestPool[hostname]
|
return addSpeedTest(hostname, port)
|
||||||
if (instance == null) {
|
|
||||||
instance = new SpeedTester({ hostname })
|
|
||||||
SpeedTestPool[hostname] = instance
|
|
||||||
}
|
|
||||||
return instance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerNotify (notify) {
|
// function registerNotify (notify) {
|
||||||
config.notify = notify
|
// config.notify = notify
|
||||||
}
|
// }
|
||||||
|
|
||||||
function reSpeedTest () {
|
function reSpeedTest () {
|
||||||
_.forEach(SpeedTestPool, (item, key) => {
|
_.forEach(SpeedTestPool, (item, _key) => {
|
||||||
item.test()
|
item.test() // 异步
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +87,8 @@ module.exports = {
|
||||||
SpeedTester,
|
SpeedTester,
|
||||||
initSpeedTest,
|
initSpeedTest,
|
||||||
getSpeedTester,
|
getSpeedTester,
|
||||||
getAllSpeedTester,
|
// getAllSpeedTester,
|
||||||
registerNotify,
|
// registerNotify,
|
||||||
reSpeedTest,
|
reSpeedTest,
|
||||||
action,
|
action,
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,3 +26,12 @@ assert.strictEqual(isEmpty(0), false)
|
||||||
assert.strictEqual(isEmpty(-1), false)
|
assert.strictEqual(isEmpty(-1), false)
|
||||||
assert.strictEqual(isEmpty(''), false)
|
assert.strictEqual(isEmpty(''), false)
|
||||||
assert.strictEqual(isEmpty('1'), 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'))
|
||||||
|
|
Loading…
Reference in New Issue