Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dns cache #2440 #2552

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/core/connect.js
Expand Up @@ -4,6 +4,7 @@
const assert = require('assert')
const util = require('./util')
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
const lookup = require('./lookup')

let tls // include tls conditionally since it is not always available

Expand Down Expand Up @@ -105,7 +106,8 @@
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
socket: httpSocket, // upgrade socket connection
port: port || 443,
host: hostname
host: hostname,
lookup: process.env.LOOKUP ? lookup : undefined,

Check failure on line 110 in lib/core/connect.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected trailing comma
})

socket
Expand All @@ -120,7 +122,8 @@
...options,
localAddress,
port: port || 80,
host: hostname
host: hostname,
lookup: process.env.LOOKUP ? lookup : undefined
})
}

Expand Down
154 changes: 154 additions & 0 deletions lib/core/lookup.js
@@ -0,0 +1,154 @@
const { promises: dnsPromises } = require('node:dns')
const os = require('node:os')

const { Resolver: AsyncResolver } = dnsPromises

const cache = new Map()

const resolver = new AsyncResolver()
const maxTtl = Infinity
let _nextRemovalTime = false
let _removalTimeout = undefined

Check failure on line 11 in lib/core/lookup.js

View workflow job for this annotation

GitHub Actions / lint

It's not necessary to initialize '_removalTimeout' to undefined

const getIfaceInfo = () => {
let has4 = false
let has6 = false

for (const device of Object.values(os.networkInterfaces())) {
for (const iface of device) {
if (iface.internal) {
continue
}

if (iface.family === 'IPv6') {
has6 = true
} else {
has4 = true
}

if (has4 && has6) {
return { has4, has6 }
}
}
}
return { has4, has6 }
}

const ttl = { ttl: true }

function lookup(hostname, options, callback) {

Check failure on line 39 in lib/core/lookup.js

View workflow job for this annotation

GitHub Actions / lint

Missing space before function parentheses
if (typeof options === 'function') {
callback = options
options = {}
} else if (typeof options === 'number') {
options = { family: options }
}

if (!callback) {
throw new Error('Function needs callback')
}

lookupAsync(hostname).then((result) => {
callback(null, result.address, result.family, result.expires, result.ttl)
}, callback)
}

async function lookupAsync(hostname) {

Check failure on line 56 in lib/core/lookup.js

View workflow job for this annotation

GitHub Actions / lint

Missing space before function parentheses
const cached = cache.get(hostname)
if (cached) {
return cached
}

const { has6 } = getIfaceInfo()

const [A, AAAA] = await Promise.all([
resolver.resolve4(hostname, ttl),
resolver.resolve6(hostname, ttl)
])

let cacheTtl = 0
let aTtl = 0
let aaaaTtl = 0

for (const entry of AAAA) {
entry.family = 6
entry.expires = Date.now + entry.ttl * 1000
aaaaTtl = Math.max(aaaaTtl, entry.ttl)
}

for (const entry of A) {
entry.family = 4
entry.expires = Date.now + entry.ttl * 1000
aTtl = Math.max(aTtl, entry.ttl)
}

if (A.length > 0) {
if (AAAA.length > 0) {
cacheTtl = Math.min(aTtl, aaaaTtl)
} else {
cacheTtl = aTtl
}
} else {
cacheTtl = aaaaTtl
}

// favourite ipv6 if available
let result
if (has6 && AAAA.length) {
result = AAAA[0]
} else {
result = A[0]
}

set(hostname, result, cacheTtl)

return result
}

function set(hostname, data, cacheTtl) {

Check failure on line 108 in lib/core/lookup.js

View workflow job for this annotation

GitHub Actions / lint

Missing space before function parentheses
if (maxTtl > 0 && cacheTtl > 0) {
cacheTtl = Math.min(cacheTtl, maxTtl) * 1000
data.expires = Date.now() + cacheTtl
cache.set(hostname, data)
tick(cacheTtl)
}
}

function tick(ms) {

Check failure on line 117 in lib/core/lookup.js

View workflow job for this annotation

GitHub Actions / lint

Missing space before function parentheses
const nextRemovalTime = _nextRemovalTime

if (!nextRemovalTime || ms < nextRemovalTime) {
clearTimeout(_removalTimeout)

_nextRemovalTime = ms

_removalTimeout = setTimeout(() => {
_nextRemovalTime = false

let nextExpiry = Infinity

const now = Date.now()

for (const [hostname, entries] of this._cache) {
const expires = entries.expires

if (now >= expires) {
cache.delete(hostname)
} else if (expires < nextExpiry) {
nextExpiry = expires
}
}

if (nextExpiry !== Infinity) {
tick(nextExpiry - now)
}
}, ms)

/* istanbul ignore next: There is no `timeout.unref()` when running inside an Electron renderer */
if (_removalTimeout.unref) {
_removalTimeout.unref()
}
}
}

module.exports = lookup