支持ipv6查询

pull/491/head
8odream 2025-06-13 17:48:49 +08:00
parent 8a722282fb
commit a19a73813c
8 changed files with 140 additions and 58 deletions

View File

@ -161,11 +161,7 @@ export default {
}, },
registerSpeedTestEvent () { registerSpeedTestEvent () {
const listener = async (event, message) => { const listener = async (event, message) => {
console.log('get speed event', event, message)
if (message.key === 'getList') { if (message.key === 'getList') {
//
console.log('speedTestList raw data:', JSON.stringify(message.value, null, 2))
// //
const validatedData = {} const validatedData = {}
for (const hostname in message.value) { for (const hostname in message.value) {
@ -185,21 +181,12 @@ export default {
dns: ipObj.dns || 'unknown', dns: ipObj.dns || 'unknown',
time: ipObj.time || null time: ipObj.time || null
} }
// IPv6
if (ipObj.host.includes(':')) {
console.log('Found IPv6 address:', {
original: ipObj.host,
standardized: standardized.host
})
}
return standardized return standardized
}) })
} }
} }
this.speedTestList = validatedData this.speedTestList = validatedData
console.log('Validated speed test data:', JSON.stringify(validatedData, null, 2))
} }
} }
this.$api.ipc.on('speed', listener) this.$api.ipc.on('speed', listener)
@ -549,16 +536,101 @@ export default {
} }
.ipv6-tag { .ipv6-tag {
position: relative; position: relative;
padding-right: 40px; padding-right: 45px !important;
margin-right: 5px !important;
display: inline-flex !important;
align-items: center !important;
min-width: 200px !important;
} }
.ipv6-badge { .ipv6-badge {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 2px; top: 50%;
transform: translateY(-50%);
font-size: 10px; font-size: 10px;
background: #1890ff; background: #1890ff;
color: white; color: white;
padding: 0 4px; padding: 0 4px;
border-radius: 3px; border-radius: 3px;
line-height: 16px;
height: 16px;
}
.ip-box {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px;
background-color: #fafafa;
border-radius: 4px;
margin-top: 8px;
max-width: 100%;
overflow: hidden;
}
.ip-item {
display: flex;
align-items: center;
padding: 4px 8px;
background-color: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
font-size: 12px;
color: #666;
word-break: break-all;
max-width: calc(100% - 16px);
flex: 1 1 auto;
min-width: 0;
}
.ip-item .ip-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ip-item .ip-speed {
margin-left: 8px;
white-space: nowrap;
}
.ip-item .ip-speed.success {
color: #52c41a;
}
.ip-item .ip-speed.warning {
color: #faad14;
}
.ip-item .ip-speed.error {
color: #ff4d4f;
}
.domain-box {
margin-bottom: 16px;
padding: 12px;
background-color: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.domain-box .domain-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.domain-box .domain-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin: 0;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.domain-box .domain-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 8px;
flex-shrink: 0;
} }
</style> </style>

View File

@ -52,7 +52,7 @@ module.exports = class BaseDNS {
} }
} }
async lookup (hostname) { async lookup (hostname, options = {}) {
try { try {
let ipCache = this.cache.get(hostname) let ipCache = this.cache.get(hostname)
if (ipCache) { if (ipCache) {
@ -66,9 +66,9 @@ module.exports = class BaseDNS {
} }
const t = new Date() const t = new Date()
let ipList = await this._lookupInternal(hostname) let ipList = await this._lookupInternal(hostname, options)
if (ipList == null) { if (ipList == null) {
// 没有获取到ipv4地址 // 没有获取到ip
ipList = [] ipList = []
} }
ipList.push(hostname) // 把原域名加入到统计里去 ipList.push(hostname) // 把原域名加入到统计里去
@ -83,7 +83,7 @@ module.exports = class BaseDNS {
} }
} }
async _lookupInternal (hostname) { async _lookupInternal (hostname, options = {}) {
// 获取当前域名的预设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)) {
@ -100,24 +100,28 @@ module.exports = class BaseDNS {
} }
} }
return await this._lookup(hostname) return await this._lookup(hostname, options)
} }
async _lookup (hostname) { async _lookup (hostname, options = {}) {
const start = Date.now() const start = Date.now()
try { try {
const response = await this._doDnsQuery(hostname) const response = await this._doDnsQuery(hostname, options)
const cost = Date.now() - start const cost = Date.now() - start
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 // 说明没有获取到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 type = options.family === 6 ? 'AAAA' : 'A'
const ret = response.answers.filter(item => item.type === type).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}'] 没有该域名的IPv${options.family === 6 ? '6' : '4'}地址: ${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}'] 获取到该域名的IPv${options.family === 6 ? '6' : '4'}地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`)
} }
return ret return ret
} catch (e) { } catch (e) {

View File

@ -8,9 +8,19 @@ module.exports = class DNSOverHTTPS extends BaseDNS {
constructor (dnsName, cacheSize, preSetIpList, dnsServer) { constructor (dnsName, cacheSize, preSetIpList, dnsServer) {
super(dnsName, 'HTTPS', cacheSize, preSetIpList) super(dnsName, 'HTTPS', cacheSize, preSetIpList)
this.dnsServer = dnsServer this.dnsServer = dnsServer
this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']')
} }
async _doDnsQuery (hostname) { async _doDnsQuery (hostname, options = {}) {
return await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }]) return await dohQueryAsync(
{
url: this.dnsServer,
family: this.isIPv6 ? 6 : 4
},
[{
type: options.family === 6 ? 'AAAA' : 'A',
name: hostname
}]
)
} }
} }

View File

@ -4,16 +4,17 @@ const dnsPacket = require('dns-packet')
const randi = require('random-int') const randi = require('random-int')
const BaseDNS = require('./base') const BaseDNS = require('./base')
const defaultPort = 53 // UDP类型的DNS服务默认端口号 const defaultPort = 53 // TCP类型的DNS服务默认端口号
module.exports = class DNSOverTCP extends BaseDNS { module.exports = class DNSOverTCP extends BaseDNS {
constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) { constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) {
super(dnsName, 'TCP', cacheSize, preSetIpList) super(dnsName, 'TCP', cacheSize, preSetIpList)
this.dnsServer = dnsServer this.dnsServer = dnsServer
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']')
} }
_doDnsQuery (hostname) { _doDnsQuery (hostname, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 构造 DNS 查询报文 // 构造 DNS 查询报文
const packet = dnsPacket.encode({ const packet = dnsPacket.encode({
@ -21,7 +22,7 @@ module.exports = class DNSOverTCP extends BaseDNS {
type: 'query', type: 'query',
id: randi(0x0, 0xFFFF), id: randi(0x0, 0xFFFF),
questions: [{ questions: [{
type: 'A', type: options.family === 6 ? 'AAAA' : 'A',
name: hostname, name: hostname,
}], }],
}) })
@ -30,6 +31,7 @@ module.exports = class DNSOverTCP extends BaseDNS {
const tcpClient = net.createConnection({ const tcpClient = net.createConnection({
host: this.dnsServer, host: this.dnsServer,
port: this.dnsServerPort, port: this.dnsServerPort,
family: this.isIPv6 ? 6 : 4
}, () => { }, () => {
// TCP DNS 报文前需添加 2 字节长度头 // TCP DNS 报文前需添加 2 字节长度头
const lengthBuffer = Buffer.alloc(2) const lengthBuffer = Buffer.alloc(2)

View File

@ -9,19 +9,21 @@ module.exports = class DNSOverTLS extends BaseDNS {
this.dnsServer = dnsServer this.dnsServer = dnsServer
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
this.dnsServerName = dnsServerName this.dnsServerName = dnsServerName
this.isIPv6 = dnsServer.includes(':') && dnsServer.includes('[') && dnsServer.includes(']')
} }
async _doDnsQuery (hostname) { async _doDnsQuery (hostname, options = {}) {
const options = { const queryOptions = {
host: this.dnsServer, host: this.dnsServer,
port: this.dnsServerPort, port: this.dnsServerPort,
servername: this.dnsServerName || this.dnsServer, servername: this.dnsServerName || this.dnsServer,
family: this.isIPv6 ? 6 : 4,
name: hostname, name: hostname,
klass: 'IN', klass: 'IN',
type: 'A', type: options.family === 6 ? 'AAAA' : 'A',
} }
return await dnstls.query(options) return await dnstls.query(queryOptions)
} }
} }

View File

@ -15,7 +15,7 @@ module.exports = class DNSOverUDP extends BaseDNS {
this.socketType = this.isIPv6 ? 'udp6' : 'udp4' this.socketType = this.isIPv6 ? 'udp6' : 'udp4'
} }
_doDnsQuery (hostname) { _doDnsQuery (hostname, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 构造 DNS 查询报文 // 构造 DNS 查询报文
const packet = dnsPacket.encode({ const packet = dnsPacket.encode({
@ -23,7 +23,7 @@ module.exports = class DNSOverUDP extends BaseDNS {
type: 'query', type: 'query',
id: randi(0x0, 0xFFFF), id: randi(0x0, 0xFFFF),
questions: [{ questions: [{
type: 'A', type: options.family === 6 ? 'AAAA' : 'A',
name: hostname, name: hostname,
}], }],
}) })

View File

@ -12,14 +12,11 @@ module.exports = {
const aliveIpObj = tester.pickFastAliveIpObj() const aliveIpObj = tester.pickFastAliveIpObj()
if (aliveIpObj) { if (aliveIpObj) {
const family = aliveIpObj.host.includes(':') ? 6 : 4 const family = aliveIpObj.host.includes(':') ? 6 : 4
log.info(`----- ${action}: ${hostname}, use alive ip from dns '${aliveIpObj.dns}': ${aliveIpObj.host}${target} -----`)
if (res) { if (res) {
res.setHeader('DS-DNS-Lookup', `IpTester: ${aliveIpObj.host} ${aliveIpObj.dns === '预设IP' ? 'PreSet' : aliveIpObj.dns}`) res.setHeader('DS-DNS-Lookup', `IpTester: ${aliveIpObj.host} ${aliveIpObj.dns === '预设IP' ? 'PreSet' : aliveIpObj.dns}`)
} }
callback(null, aliveIpObj.host, family) callback(null, aliveIpObj.host, family)
return return
} else {
log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { "ready": ${tester.ready}, "backupList": ${JSON.stringify(tester.backupList)} }`)
} }
} }
@ -31,7 +28,6 @@ module.exports = {
isDnsIntercept.hostname = hostname isDnsIntercept.hostname = hostname
isDnsIntercept.ip = ip isDnsIntercept.ip = ip
} }
log.info(`----- ${action}: ${hostname}, use ipv6 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}`)
} }
@ -44,7 +40,6 @@ module.exports = {
}).then((ip) => { }).then((ip) => {
if (!ip || ip === hostname) { if (!ip || ip === hostname) {
// 使用默认dns // 使用默认dns
log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns)
return defaultDns.lookup(hostname, options, callback) return defaultDns.lookup(hostname, options, callback)
} }
@ -70,14 +65,12 @@ module.exports = {
if (!isTestFailedIp) { if (!isTestFailedIp) {
const family = ip.includes(':') ? 6 : 4 const family = ip.includes(':') ? 6 : 4
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, family) callback(null, ip, family)
} 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)
defaultDns.lookup(hostname, options, callback) defaultDns.lookup(hostname, options, callback)
} }
}).catch((e) => { }).catch((e) => {

View File

@ -79,6 +79,8 @@ class SpeedTester {
} }
async getFromOneDns (dns) { async getFromOneDns (dns) {
const results = []
// 优先尝试IPv6查询 // 优先尝试IPv6查询
try { try {
const ipv6Result = await dns._lookupInternal(this.hostname, { family: 6 }) const ipv6Result = await dns._lookupInternal(this.hostname, { family: 6 })
@ -91,19 +93,23 @@ class SpeedTester {
} }
return ip return ip
}) })
log.debug(`[dns] Got IPv6 addresses for ${this.hostname}:`, standardized) results.push(...standardized)
return standardized
} }
} catch (e) { } catch (e) {
log.debug(`[dns] IPv6 lookup failed for ${this.hostname}: ${e.message}`) // IPv6查询失败继续尝试IPv4
} }
// 回退到IPv4查询 // 尝试IPv4查询
const ipv4Result = await dns._lookupInternal(this.hostname) try {
if (ipv4Result) { const ipv4Result = await dns._lookupInternal(this.hostname)
log.debug(`[dns] Got IPv4 addresses for ${this.hostname}:`, ipv4Result) if (ipv4Result) {
results.push(...ipv4Result)
}
} catch (e) {
// IPv4查询失败
} }
return ipv4Result
return results
} }
async test () { async test () {
@ -111,13 +117,6 @@ class SpeedTester {
const newBackupList = [...newList, ...this.backupList] const newBackupList = [...newList, ...this.backupList]
this.backupList = _.unionBy(newBackupList, 'host') this.backupList = _.unionBy(newBackupList, 'host')
this.testCount++ this.testCount++
// 详细记录IPv6地址
const ipv6List = this.backupList.filter(item => item.host.includes(':'))
if (ipv6List.length > 0) {
log.info('[speed] IPv6 addresses found for', this.hostname, ':', ipv6List)
}
log.info('[speed]', this.hostname, '➜ ip-list:', this.backupList)
await this.testBackups() await this.testBackups()
if (config.notify) { if (config.notify) {
config.notify({ key: 'test' }) config.notify({ key: 'test' })