Skip to content

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 directly
  • resolveMx() 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