feature: 新增 `UDP` 和 `TCP` 类型的DNS服务,并修复 `TLS` 类型的DNS服务地址不生效的问题

release-2.0.0.2
王良 2025-03-03 22:26:47 +08:00
parent 93e09ed4b8
commit c3c13fef18
8 changed files with 396 additions and 130 deletions

View File

@ -1,8 +1,19 @@
const LRUCache = require('lru-cache')
const log = require('../../utils/util.log.server')
const matchUtil = require('../../utils/util.match')
const { DynamicChoice } = require('../choice/index')
const cacheSize = 1024
function mapToList (ipMap) {
const ipList = []
for (const key in ipMap) {
if (ipMap[key]) { // 配置为 ture 时才生效
ipList.push(key)
}
}
return ipList
}
const defaultCacheSize = 1024
class IpCache extends DynamicChoice {
constructor (hostname) {
@ -22,10 +33,11 @@ class IpCache extends DynamicChoice {
}
module.exports = class BaseDNS {
constructor (dnsName) {
constructor (dnsName, cacheSize, preSetIpList) {
this.dnsName = dnsName
this.preSetIpList = preSetIpList
this.cache = new LRUCache({
maxSize: cacheSize,
maxSize: (cacheSize > 0 ? cacheSize : defaultCacheSize),
sizeCalculation: () => {
return 1
},
@ -53,7 +65,7 @@ module.exports = class BaseDNS {
}
const t = new Date()
let ipList = await this._lookup(hostname)
let ipList = await this._lookupInternal(hostname)
if (ipList == null) {
// 没有获取到ipv4地址
ipList = []
@ -69,4 +81,46 @@ module.exports = class BaseDNS {
return hostname
}
}
async _lookupInternal (hostname) {
// 获取当前域名的预设IP列表
let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, 'matched preSetIpList')
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
return hostnamePreSetIpList
}
}
return await this._lookup(hostname)
}
async _lookup (hostname) {
const start = Date.now()
try {
const response = await this._doDnsQuery(hostname)
const cost = Date.now() - start
if (response == null || response.answers == null || response.answers.length == null || response.answers.length === 0) {
// 说明没有获取到ip
log.warn(`DNS '${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 '${this.dnsName}' 没有该域名的IPv4地址: ${hostname}, cost: ${cost} ms`)
} else {
log.info(`DNS '${this.dnsName}' 获取到该域名的IPv4地址 ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`)
}
return ret
} catch (e) {
log.error(`DNS query error: ${hostname}, dns: ${this.dnsName}${this.dnsServer ? (`, dnsServer: ${this.dnsServer}`) : ''}, cost: ${Date.now() - start} ms, error:`, e)
return []
}
}
}

View File

@ -1,64 +1,16 @@
const { promisify } = require('node:util')
const doh = require('dns-over-http')
const log = require('../../utils/util.log.server')
const matchUtil = require('../../utils/util.match')
const BaseDNS = require('./base')
const dohQueryAsync = promisify(doh.query)
function mapToList (ipMap) {
const ipList = []
for (const key in ipMap) {
if (ipMap[key]) { // 配置为 ture 时才生效
ipList.push(key)
}
}
return ipList
}
module.exports = class DNSOverHTTPS extends BaseDNS {
constructor (dnsName, dnsServer, preSetIpList) {
super(dnsName)
constructor (dnsName, cacheSize, preSetIpList, dnsServer) {
super(dnsName, cacheSize, preSetIpList)
this.dnsServer = dnsServer
this.preSetIpList = preSetIpList
}
async _lookup (hostname) {
// 获取当前域名的预设IP列表
let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, 'matched preSetIpList')
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
return hostnamePreSetIpList
}
}
// 未预设当前域名的IP列表则从dns服务器获取
const start = new Date()
try {
const result = await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }])
const cost = new Date() - start
if (result.answers.length === 0) {
// 说明没有获取到ip
log.info(`DNS '${this.dnsName}' 没有该域名的IP地址: ${hostname}, cost: ${cost} ms`)
return []
}
const ret = result.answers.filter(item => item.type === 'A').map(item => item.data)
if (ret.length === 0) {
log.info(`DNS '${this.dnsName}' 没有该域名的IPv4地址: ${hostname}, cost: ${cost} ms`)
} else {
log.info(`DNS '${this.dnsName}' 获取到该域名的IPv4地址 ${hostname} ${JSON.stringify(ret)}, cost: ${cost} ms`)
}
return ret
} catch (e) {
log.warn(`DNS query error: ${hostname}, dns: ${this.dnsName}, dnsServer: ${this.dnsServer}, cost: ${new Date() - start} ms, error:`, e)
return []
}
async _doDnsQuery (hostname) {
return await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }])
}
}

View File

@ -1,8 +1,10 @@
const matchUtil = require('../../utils/util.match')
const DNSOverHTTPS = require('./https.js')
const DNSOverIpAddress = require('./ipaddress.js')
const DNSOverPreSetIpList = require('./preset.js')
const DNSOverIpAddress = require('./ipaddress.js')
const DNSOverHTTPS = require('./https.js')
const DNSOverTLS = require('./tls.js')
const DNSOverTCP = require('./tcp.js')
const DNSOverUDP = require('./udp.js')
module.exports = {
initDNS (dnsProviders, preSetIpList) {
@ -12,12 +14,50 @@ module.exports = {
for (const provider in dnsProviders) {
const conf = dnsProviders[provider]
if (conf.type === 'ipaddress') {
dnsMap[provider] = new DNSOverIpAddress(provider)
} else if (conf.type === 'https') {
dnsMap[provider] = new DNSOverHTTPS(provider, conf.server, preSetIpList)
let server = conf.server || conf.host
if (server != null) {
server = server.replace(/\s+/, '')
}
if (!server) {
continue
}
// 获取DNS类型
if (conf.type == null) {
if (server.startsWith('https://')) {
conf.type = 'https'
} else if (server.startsWith('tls://')) {
conf.type = 'tls'
} else if (server.startsWith('tcp://')) {
conf.type = 'tcp'
} else if (server.includes('://') && !server.startsWith('udp://')) {
throw new Error(`Unknown type DNS: ${server}, provider: ${provider}`)
} else {
conf.type = 'udp'
}
} else {
dnsMap[provider] = new DNSOverTLS(provider)
conf.type = conf.type.toLowerCase()
}
if (conf.type === 'ipaddress') {
dnsMap[provider] = new DNSOverIpAddress(provider, conf.cacheSize, preSetIpList)
} else if (conf.type === 'https') {
dnsMap[provider] = new DNSOverHTTPS(provider, conf.cacheSize, preSetIpList, server)
} else if (conf.type === 'tls') {
if (server.startsWith('tls://')) {
server = server.substring(6)
}
dnsMap[provider] = new DNSOverTLS(provider, conf.cacheSize, preSetIpList, server, conf.port, conf.servername)
} else if (conf.type === 'tcp') {
if (server.startsWith('tcp://')) {
server = server.substring(6)
}
dnsMap[provider] = new DNSOverTCP(provider, conf.cacheSize, preSetIpList, server, conf.port)
} else { // udp
if (server.startsWith('udp://')) {
server = server.substring(6)
}
dnsMap[provider] = new DNSOverUDP(provider, conf.cacheSize, preSetIpList, server, conf.port)
}
// 设置DNS名称到name属性中

View File

@ -25,14 +25,5 @@ module.exports = class DNSOverIpAddress extends BaseDNS {
}
log.warn(`[dns] get ${hostname} ipaddress: error`)
return null
// const { answers } = await dnstls.query(hostname)
//
// const answer = answers.find(answer => answer.type === 'A' && answer.class === 'IN')
//
// log.info('dns lookup', hostname, answer)
// if (answer) {
// return answer.data
// }
}
}

View File

@ -0,0 +1,51 @@
const net = require('node:net')
const { Buffer } = require('node:buffer')
const dnsPacket = require('dns-packet')
const randi = require('random-int')
const BaseDNS = require('./base')
const defaultPort = 53 // UDP类型的DNS服务默认端口号
module.exports = class DNSOverTCP extends BaseDNS {
constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) {
super(dnsName, cacheSize, preSetIpList)
this.dnsServer = dnsServer
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
}
_doDnsQuery (hostname) {
return new Promise((resolve, reject) => {
// 构造 DNS 查询报文
const packet = dnsPacket.encode({
flags: dnsPacket.RECURSION_DESIRED,
type: 'query',
id: randi(0x0, 0xFFFF),
questions: [{
type: 'A',
name: hostname,
}],
})
// --- TCP 查询 ---
const tcpClient = net.createConnection({
host: this.dnsServer,
port: this.dnsServerPort,
}, () => {
// TCP DNS 报文前需添加 2 字节长度头
const lengthBuffer = Buffer.alloc(2)
lengthBuffer.writeUInt16BE(packet.length)
tcpClient.write(Buffer.concat([lengthBuffer, packet]))
})
tcpClient.on('data', (data) => {
const length = data.readUInt16BE(0)
const response = dnsPacket.decode(data.subarray(2, 2 + length))
resolve(response)
})
tcpClient.on('error', (err) => {
reject(err)
})
})
}
}

View File

@ -1,16 +1,27 @@
const dnstls = require('dns-over-tls')
const log = require('../../utils/util.log.server')
const BaseDNS = require('./base')
const defaultPort = 853
module.exports = class DNSOverTLS extends BaseDNS {
async _lookup (hostname) {
const { answers } = await dnstls.query(hostname)
constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort, dnsServerName) {
super(dnsName, cacheSize, preSetIpList)
this.dnsServer = dnsServer
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
this.dnsServerName = dnsServerName
}
const answer = answers.find(answer => answer.type === 'A' && answer.class === 'IN')
async _doDnsQuery (hostname) {
const options = {
host: this.dnsServer,
port: this.dnsServerPort,
servername: this.dnsServerName || this.dnsServer,
log.info('DNS lookup', hostname, answer)
if (answer) {
return answer.data
name: hostname,
klass: 'IN',
type: 'A',
}
return await dnstls.query(options)
}
}

View File

@ -0,0 +1,44 @@
const dgram = require('node:dgram')
const dnsPacket = require('dns-packet')
const randi = require('random-int')
const BaseDNS = require('./base')
const udpClient = dgram.createSocket('udp4')
const defaultPort = 53 // UDP类型的DNS服务默认端口号
module.exports = class DNSOverUDP extends BaseDNS {
constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) {
super(dnsName, cacheSize, preSetIpList)
this.dnsServer = dnsServer
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
}
_doDnsQuery (hostname) {
return new Promise((resolve, reject) => {
// 构造 DNS 查询报文
const packet = dnsPacket.encode({
flags: dnsPacket.RECURSION_DESIRED,
type: 'query',
id: randi(0x0, 0xFFFF),
questions: [{
type: 'A',
name: hostname,
}],
})
// 发送 UDP 查询
udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err) => {
if (err) {
reject(err)
}
})
// 接收 UDP 响应
udpClient.on('message', (msg) => {
const response = dnsPacket.decode(msg)
resolve(response)
})
})
}
}

View File

@ -1,56 +1,99 @@
import assert from 'node:assert'
import dns from '../src/lib/dns/index.js'
const presetIp = '100.100.100.100'
const dnsProviders = dns.initDNS({
aliyun: {
type: 'https',
server: 'https://dns.alidns.com/dns-query',
cacheSize: 1000,
},
cloudflare: {
type: 'https',
server: 'https://1.1.1.1/dns-query',
cacheSize: 1000,
},
ipaddress: {
type: 'ipaddress',
server: 'ipaddress',
cacheSize: 1000,
},
quad9: {
// 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,
},
rubyfish: {
aliyun: {
type: 'https',
server: 'https://dns.alidns.com/dns-query',
cacheSize: 1000,
},
safe360: {
server: 'https://doh.360.cn/dns-query',
cacheSize: 1000,
},
rubyfish: {
server: 'https://rubyfish.cn/dns-query',
cacheSize: 1000,
},
py233: {
type: 'https',
server: ' https://i.233py.com/dns-query',
cacheSize: 1000,
},
// sb: {
// type: 'https',
// server: 'https://doh.dns.sb/dns-query',
// cacheSize: 1000
// },
// adguard: {
// type: 'https',
// server: ' https://dns.adguard.com/dns-query',
// cacheSize: 1000
// }
// 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: {
type: 'tls',
server: '223.5.5.5',
cacheSize: 1000,
},
aliyunTLS2: {
server: 'tls://223.6.6.6',
cacheSize: 1000,
},
safe360TLS: {
server: 'tls://dot.360.cn',
cacheSize: 1000,
},
// tcp
googleTCP: {
type: 'tcp',
server: '8.8.8.8',
cacheSize: 1000,
},
aliyunTCP: {
server: 'tcp://223.5.5.5',
cacheSize: 1000,
},
// udp
googleUDP: {
type: 'udp',
server: '8.8.8.8',
cacheSize: 1000,
},
aliyunUDP: {
server: 'udp://223.5.5.5',
cacheSize: 1000,
},
}, {
origin: {
'xxx.com': [
presetIp
]
}
})
// const test = '111<tr><th>IP Address</th><td><ul class="comma-separated"><li>140.82.113.4</li></ul></td></tr>2222'
// // <tr><th>IP Address</th><td><ul class="comma-separated"><li>140.82.113.4</li></ul></td></tr>
// // <tr><th>IP Address</th><td><ul class="comma-separated"><li>(.*)</li></ul></td></tr>
// const regexp = /<tr><th>IP Address<\/th><td><ul class="comma-separated"><li>(.*)<\/li><\/ul><\/td><\/tr>/
// const matched = regexp.exec(test)
// console.log('data:', matched)
const presetHostname = 'xxx.com'
const hostname1 = 'github.com'
const hostname2 = 'api.github.com'
const hostname3 = 'hk.docmirror.cn'
@ -60,25 +103,105 @@ const hostname6 = 'gh2.docmirror.top'
let ip
// console.log('test cloudflare')
// ip = await dnsProviders.cloudflare.lookup(hostname1)
// console.log('ip:', ip)
// ip = await dnsProviders.cloudflare.lookup(hostname2)
// console.log('ip:', ip)
// ip = await dnsProviders.cloudflare.lookup(hostname3)
// console.log('ip:', ip)
// ip = await dnsProviders.cloudflare.lookup(hostname4)
// console.log('ip:', ip)
// ip = await dnsProviders.cloudflare.lookup(hostname5)
// console.log('ip:', ip)
// ip = await dnsProviders.cloudflare.lookup(hostname6)
// console.log('ip:', ip)
// console.log('test py233')
// ip = await dnsProviders.py233.lookup(hostname1)
// console.log('ip:', ip)
// console.log('test ipaddress')
// const test = '111<tr><th>IP Address</th><td><ul class="comma-separated"><li>140.82.113.4</li></ul></td></tr>2222'
// // <tr><th>IP Address</th><td><ul class="comma-separated"><li>140.82.113.4</li></ul></td></tr>
// // <tr><th>IP Address</th><td><ul class="comma-separated"><li>(.*)</li></ul></td></tr>
// const regexp = /<tr><th>IP Address<\/th><td><ul class="comma-separated"><li>(.*)<\/li><\/ul><\/td><\/tr>/
// const matched = regexp.exec(test)
// console.log('data:', matched)
//
// console.log('\n--------------- test ipaddress ---------------\n')
// ip = await dnsProviders.ipaddress.lookup(hostname1)
// console.log('ip:', ip)
// console.log('===> test ipaddress:', ip, '\n\n')
assert.strictEqual(dnsProviders.ipaddress.type, 'ipaddress')
console.log('\n--------------- test PreSet ---------------\n')
ip = await dnsProviders.PreSet.lookup(presetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('===> test PreSet:', ip, '\n\n')
console.log('\n\n')
console.log('\n--------------- test https ---------------\n')
ip = await dnsProviders.cloudflare.lookup(presetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
assert.strictEqual(dnsProviders.cloudflare.type, 'https')
ip = await dnsProviders.cloudflare.lookup(hostname1)
console.log('===> test cloudflare:', ip, '\n\n')
assert.strictEqual(dnsProviders.quad9.type, 'https')
ip = await dnsProviders.quad9.lookup(hostname1)
console.log('===> test quad9:', ip, '\n\n')
assert.strictEqual(dnsProviders.aliyun.type, 'https')
ip = await dnsProviders.aliyun.lookup(hostname1)
console.log('===> test aliyun:', ip, '\n\n')
assert.strictEqual(dnsProviders.safe360.type, 'https')
ip = await dnsProviders.safe360.lookup(hostname1)
console.log('===> test safe360:', ip, '\n\n')
assert.strictEqual(dnsProviders.rubyfish.type, 'https')
ip = await dnsProviders.rubyfish.lookup(hostname1)
console.log('===> test rubyfish:', ip, '\n\n')
assert.strictEqual(dnsProviders.py233.type, 'https')
ip = await dnsProviders.py233.lookup(hostname1)
console.log('===> test py233:', ip, '\n\n')
console.log('\n--------------- test TLS ---------------\n')
ip = await dnsProviders.cloudflareTLS.lookup(presetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
assert.strictEqual(dnsProviders.cloudflareTLS.type, 'tls')
ip = await dnsProviders.cloudflareTLS.lookup(hostname1)
console.log('===> test cloudflareTLS:', ip, '\n\n')
assert.strictEqual(dnsProviders.quad9TLS.type, 'tls')
ip = await dnsProviders.quad9TLS.lookup(hostname1)
console.log('===> test quad9TLS:', ip, '\n\n')
assert.strictEqual(dnsProviders.aliyunTLS.type, 'tls')
ip = await dnsProviders.aliyunTLS.lookup(hostname1)
console.log('===> test aliyunTLS:', ip, '\n\n')
assert.strictEqual(dnsProviders.aliyunTLS2.type, 'tls')
ip = await dnsProviders.aliyunTLS2.lookup(hostname1)
console.log('===> test aliyunTLS2:', ip, '\n\n')
assert.strictEqual(dnsProviders.safe360TLS.type, 'tls')
ip = await dnsProviders.safe360TLS.lookup(hostname1)
console.log('===> test safe360TLS:', ip, '\n\n')
console.log('\n--------------- test TCP ---------------\n')
ip = await dnsProviders.googleTCP.lookup(presetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
assert.strictEqual(dnsProviders.googleTCP.type, 'tcp')
ip = await dnsProviders.googleTCP.lookup(hostname1)
console.log('===> test googleTCP:', ip, '\n\n')
assert.strictEqual(dnsProviders.aliyunTCP.type, 'tcp')
ip = await dnsProviders.aliyunTCP.lookup(hostname1)
console.log('===> test aliyunTCP:', ip, '\n\n')
console.log('\n--------------- test UDP ---------------\n')
ip = await dnsProviders.googleUDP.lookup(presetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
assert.strictEqual(dnsProviders.googleUDP.type, 'udp')
ip = await dnsProviders.googleUDP.lookup(hostname1)
console.log('===> test googleUDP:', ip, '\n\n')
assert.strictEqual(dnsProviders.aliyunUDP.type, 'udp')
ip = await dnsProviders.aliyunUDP.lookup(hostname1)
console.log('===> test aliyunUDP:', ip, '\n\n')