Core 09 dns
Core 09 - DNS Module¶
Basic Idea¶
DNS resolves human-readable hostnames to machine IPs Without DNS you're memorizing 32-character IPv6 addresses dns module does resolution and reverse lookups - two different APIs for two different use cases
dns.lookup() vs dns.resolve()¶
const dns = require('dns')
const dnsPromises = require('dns/promises')
// dns.lookup() - uses libuv (same as ping, curl, getaddrinfo)
// Honors /etc/hosts and NSS configuration
const { address, family } = await dnsPromises.lookup('google.com')
console.log('lookup:', address, '(IPv' + family + ')')
// dns.resolve() - direct DNS query, skips system config
// Does NOT read /etc/hosts or nsswitch.conf
const records = await dnsPromises.resolve('google.com')
console.log('resolve:', records)
lookup() goes through getaddrinfo() - same as every other program on your system It uses libuv's thread pool and respects /etc/hosts resolve() talks directly to DNS servers - bypasses host files and NSS
Which one should you use? - lookup() for most cases - it's what http.get() and net.connect() use internally - resolve() when you need specific record types (MX , TXT , SRV) that lookup() doesn't return
dns.resolve4(), resolve6(), resolveMx(), resolveTxt()¶
const dns = require('dns/promises')
// A records - IPv4
const a = await dns.resolve4('google.com')
console.log('A records:', a)
// AAAA records - IPv6
const aaaa = await dns.resolve6('google.com')
console.log('AAAA records:', aaaa)
// MX records - mail servers + priority
const mx = await dns.resolveMx('gmail.com')
console.log('Mail servers:')
mx.sort((a, b) => a.priority - b.priority)
for (const record of mx) {
console.log(` ${record.priority} ${record.exchange}`)
}
// TXT records - SPF, DKIM, verification tokens
const txt = await dns.resolveTxt('google.com')
console.log('TXT records:', txt.flat())
// CNAME
const cname = await dns.resolveCname('www.github.com')
console.log('CNAME:', cname)
// NS records - name servers
const ns = await dns.resolveNs('google.com')
console.log('Name servers:', ns)
Different record types serve different use cases MX for email routing , TXT for SPF/DKIM/domain verification , CNAME for aliases resolveTxt() returns an array of arrays - each sub-array is one quoted string in the record
dns.reverse() - PTR Lookups¶
const dns = require('dns/promises')
// reverse DNS - IP to hostname
const hostnames = await dns.reverse('8.8.8.8')
console.log('PTR for 8.8.8.8:', hostnames)
// ['dns.google']
// practical - log reverse DNS for connections
const net = require('net')
const server = net.createServer(async (socket) => {
const ip = socket.remoteAddress
try {
const names = await dns.reverse(ip)
console.log(`connection from ${ip} (${names[0] || 'unknown'})`)
} catch {
console.log(`connection from ${ip} (no PTR record)`)
}
})
PTR records map IPs back to hostnames Useful for logging , spam filtering , and recognizing known IPs Not all IPs have PTR records - handle the error gracefully PTR lookups are slow - don't block request handling , do it async
Caching DNS Results¶
// DNS results are not cached by default
// Every call to dns.lookup() hits the network or libuv's cache
// Simple in-memory cache with TTL
const dns = require('dns/promises')
class DNSCache {
constructor(ttlMs = 60000) {
this.cache = new Map()
this.ttl = ttlMs
}
async resolve(hostname) {
const now = Date.now()
const cached = this.cache.get(hostname)
if (cached && now - cached.timestamp < this.ttl) {
return cached.addresses
}
try {
const addresses = await dns.resolve4(hostname)
this.cache.set(hostname, { addresses, timestamp: now })
return addresses
} catch (err) {
// return stale cache on failure - better than crashing
if (cached) return cached.addresses
throw err
}
}
clear() {
this.cache.clear()
}
}
const cache = new DNSCache()
const ips = await cache.resolve('google.com')
Node doesn't cache DNS results by default (libuv caches briefly on some platforms) DNS queries are network calls - slow and sometimes unreliable A simple cache with TTL avoids repeated lookups for the same hostname Fall back to stale cache on DNS failure - your service should stay up even when DNS is down
Security: DNS Rebinding Attacks¶
// DNS rebinding attack pattern:
// 1. Attacker registers domain (evil.xyz) with very short TTL
// 2. First DNS query returns 1.2.3.4 (attacker's server)
// 3. App validates the hostname, allows the connection
// 4. DNS TTL expires, second query returns 127.0.0.1
// 5. App now connects to localhost - SSRF to internal services
// DEFENSE - validate the resolved IP, not the hostname
async function validateTarget(hostname, allowedRanges) {
const { address } = await dnsPromises.lookup(hostname)
const ip = require('ipaddr.js').parse(address)
// check against private IP ranges
if (ip.range() === 'private' || ip.range() === 'loopback') {
throw new Error('target resolves to internal IP')
}
// or check against explicit allowlist
if (!allowedRanges.some(range => ip.match(range))) {
throw new Error('target not in allowed ranges')
}
return address
}
DNS rebinding exploits the gap between DNS resolution time and IP validation time Validate the resolved IP address - not the hostname string Never trust that hostname === 'localhost' means it resolves to 127.0.0.1 Pin DNS results by caching the first resolution and using the cached IP for the connection
Security: Spoofed DNS Responses¶
// DNS responses can be spoofed on the local network
// An attacker with ARP spoofing can intercept DNS queries and return fake IPs
// Mitigation: use DNS-over-HTTPS (DoH) or DNS-over-TLS (DoT)
// Node doesn't have built-in DoH - use a library like 'dohjs'
// Alternative: validate TLS certificates after connecting
const https = require('https')
const tls = require('tls')
// Even if DNS is spoofed to point to a malicious IP
// the TLS certificate validation will fail
// (unless the attacker has a valid cert - which requires CA compromise)
DNS over cleartext (UDP port 53) is trivially spoofable on the same network segment TLS certificate validation is your backup - a spoofed DNS response can redirect the connection but the attacker can't forge the TLS certificate Unless they also control the CA chain - in which case you have bigger problems
Summary¶
lookup()uses libuv + system config ;resolve()queries DNS directlyresolveMx()for mail servers ,resolveTxt()for SPF/DKIM- Cache DNS results - repeated queries waste time and bandwidth
- DNS rebinding is real - validate resolved IPs , not hostnames
- TLS is your defense against DNS spoofing - trust the certificate chain
Prerequisites¶
next -> core_10_util.md