js/net/dns
js/net/dns.ts
fino:net/dns — async DNS resolution via the RFC 1035 wire protocol.
DNS protocol specification: https://www.rfc-editor.org/rfc/rfc1035
This module implements DNS resolution entirely in JS by speaking the DNS
wire protocol directly over UDP sockets (from fino:socket) and falls back
to DNS-over-TCP when a UDP response is marked truncated. It reads nameservers
from /etc/resolv.conf via fino:file. No blocking libc calls are made —
the entire resolution path is async and event-loop driven.
Why not use libc getaddrinfo?
getaddrinfo(3) is the standard libc function for DNS resolution, but it
blocks the calling thread. Calling it from Fino would freeze the entire
event loop for the duration of the DNS query — potentially hundreds of
milliseconds. Using raw sockets lets us await readiness asynchronously via
loop.readable() / loop.writable(), keeping the process responsive to
other I/O while the query is in flight.
DNS wire protocol overview (RFC 1035)
A DNS query packet is:
Header (12 bytes, big-endian): [0-1 ID — 16-bit transaction ID (random per query) [2-3 Flags — 0x0100 = standard query with recursion desired [4-5 QDCOUNT — number of questions (always 1) [6-7 ANCOUNT — answer count (0 in queries) [8-9 NSCOUNT — authority count (0 in queries) [10-11 ARCOUNT — additional count (0 in queries)
Question section: QNAME — domain name in wire format (length-prefixed labels, null-terminated) QTYPE — record type (A=1, AAAA=28, MX=15, etc.) QCLASS — always IN=1 (Internet)
Wire-format domain names encode each label (dot-separated part) as a length byte followed by the label's bytes. "example.com" becomes: [7 'e','x','a','m','p','l','e' [3 'c','o','m' [0 Non-ASCII query labels are normalized to IDNA-style A-labels before wire encoding, so "café.example" is sent as "xn--caf-dma.example".
DNS responses use compression pointers to avoid repeating domain names: a two-byte sequence starting with bits 11xxxxxx is a pointer to an earlier offset in the message. The internal decoder follows these pointers recursively, with a hop limit to prevent infinite loops from malformed responses.
Resource record parsing
Each answer/authority/additional record is: NAME — domain name (may use compression) TYPE — u16 big-endian CLASS — u16 big-endian (always 1) TTL — u32 big-endian (seconds) RDLENGTH— u16 big-endian (length of RDATA) RDATA — variable; format depends on TYPE
parseResourceRecord() handles A, AAAA, CNAME, NS, PTR, MX, TXT, SOA, and
SRV, plus DNSSEC DS, DNSKEY, RRSIG, NSEC, NSEC3, and NSEC3PARAM records.
Unknown types return raw bytes. Every parsed record also preserves exact
rawData RDATA bytes for canonicalization and future validator use.
DNSSEC wire support
Passing { dnssec: true } to new Resolver() or lookup() makes queries
include an EDNS(0) OPT pseudo-record with the DNSSEC OK (DO) bit and a
conservative 1232-byte UDP payload size. Responses parse EDNS metadata and
DNSSEC record payloads locally. Signed positive answers are validated from
embedded IANA root trust anchors. Bogus or indeterminate signed data, broken
DS/DNSKEY chains, expired signatures, unsupported-only signatures, and
missing denial proofs reject with code: 'EDNSSEC'. Provably insecure
unsigned delegations are accepted after a signed DS-negative proof. The
resolver never trusts upstream AD bits as proof. Unsupported DNSSEC signing
algorithms still reject unless another supported signature validates.
CNAME chain following
For A and AAAA queries, resolve() follows CNAME chains automatically (up
to 10 hops) by re-querying the CNAME target if the response contains no
direct A/AAAA records. This is how most real resolvers work — the CNAME
leads to the actual IP.
Nameserver discovery
On first use, #loadServers() reads /etc/resolv.conf and parses
nameserver <ip> lines. If the file is unreadable or has no nameserver
lines, it falls back to Google's public DNS servers (8.8.8.8, 8.8.4.4).
Callers can override nameservers with resolver.setServers(['1.1.1.1']).
Search domains, ndots, rotate, sortlist, TTL-return APIs, and the broader
Node dns.Resolver option matrix are intentionally outside this release
baseline; this resolver sends explicit absolute names supplied by callers.
Release validation keeps live resolver behavior behind an explicit
FINO_DNS_LIVE=1 test lane so the normal suite stays deterministic. That
lane should exercise at least one public recursive resolver and one
user-configured resolver from setServers(). The transport implementation
intentionally speaks classic DNS over UDP with TCP fallback for truncated
responses; DNS-over-TLS and DNS-over-HTTPS are not implemented by this
module.
TTL and cache policy
Parsed resource records preserve the authoritative TTL from the packet, but
ordinary Resolver lookups do not cache answers by TTL. Every resolve()
call sends a fresh query unless it follows an in-response CNAME chain. The
only cache maintained by this module is the DNSSEC delegation cache used
while validating signed chains, and that cache stores DNSKEY/DS chain state
separately from user-visible answer records.
IPv6 formatting via sock.decodeAddr
Rather than implementing IPv6 string formatting from scratch (handling ::,
leading zeros, etc.), formatIPv6() builds a synthetic sockaddr_in6
buffer and passes it to sock.decodeAddr(), which calls inet_ntop(3).
This gives correct RFC 5952 compressed output (e.g. 2001:db8::1) without
extra code.
Query timeout and retry
Each query races loop.readable(lp, fd) against loop.timeout(lp, ms).
If the timeout fires first, we close the socket and try the next nameserver.
After all servers have been tried retries times without a valid response,
we throw an ETIMEOUT error.
Contributing
- DNS packets are big-endian. All header fields and record metadata use the
readU16()/readU32()big-endian helpers defined at the top of the file. - Transaction IDs are generated with
crypto.getRandomValues()and checked against responses to reduce spoofing risk. - UDP datagrams may arrive out of order or be duplicates. We check that the response ID matches the query ID and discard non-matching packets.
- DNSSEC validation is opt-in. DNSSEC-enabled queries request validation
material with EDNS(0) DO, verify chains locally, and reject bogus or
indeterminate data with
EDNSSEC.
import { Resolver } from 'fino:net/dns';
const resolver = new Resolver({ timeout: 1_000, retries: 2 });
resolver.setServers(['1.1.1.1', '8.8.8.8']);
const addresses = await resolver.resolve('example.com', 'A');
Types
type DnsServerFamily = 'ipv4' | 'ipv6'
Address family for a DNS nameserver endpoint.
The resolver uses this value to choose the socket family when sending UDP
queries or opening TCP fallback connections. ipv4 addresses are dotted
decimal literals, and ipv6 addresses are numeric IPv6 literals.
import type { DnsServerFamily } from 'fino:net/dns';
const family: DnsServerFamily = 'ipv4';
type DnsRecordData = string | string[] | MxRecord | SoaRecord | SrvRecord | DsRecord | DnskeyRecord | RrsigRecord | NsecRecord | Nsec3Record | Nsec3ParamRecord | Uint8Array | null
Decoded DNS record payload for supported record types; unknown data is raw
bytes and malformed or empty RDATA can surface as null.
const records = await new Resolver().resolve('example.com', 'TXT');
for (const data of records) {
if (Array.isArray(data)) console.log(data.join(''));
}
Interfaces
interface DnsServer {
Parsed DNS nameserver endpoint used by Resolver.
Resolver.setServers() accepts string forms such as 1.1.1.1,
[2606:4700:4700::1111]:53, or 8.8.8.8:53 and normalizes them into this
endpoint shape internally. Resolver.getServers() returns the string form
again for compatibility with familiar DNS resolver APIs.
import type { DnsServer } from 'fino:net/dns';
const server: DnsServer = { ip: '1.1.1.1', family: 'ipv4', port: 53 };
Properties
ip: string
Numeric IPv4 or IPv6 address literal.
Host names are not accepted here; recursive resolver endpoints must already be concrete addresses.
family: DnsServerFamily
Socket address family for ip.
The family controls whether the resolver opens an IPv4 or IPv6 UDP/TCP socket for this endpoint.
port: number
UDP and TCP DNS port.
Classic DNS uses 53. The same port is used for the initial UDP query and
TCP fallback when a response is truncated.
interface MxRecord {
Mail-exchanger DNS record payload returned for MX lookups.
priority is the preference value from the DNS response; lower numbers are
preferred. exchange is the mail host name exactly as decoded from the DNS
packet and is not resolved to an address automatically.
import { Resolver } from 'fino:net/dns';
const resolver = new Resolver();
const [mx] = await resolver.resolve('example.com', 'MX');
console.log(mx.priority, mx.exchange);
Properties
priority: number
MX preference value; lower values should be tried first.
const best = records.sort((a, b) => a.priority - b.priority)[0];
exchange: string
Mail exchanger host name. Resolve it separately with A or AAAA if needed.
const addr = await resolver.resolve(mx.exchange, 'A');
interface SoaRecord {
Start-of-authority DNS record payload returned for SOA lookups.
Timing fields are seconds from the authoritative record. Values are not interpreted or clamped by the resolver.
const [soa] = await new Resolver().resolve('example.com', 'SOA');
console.log(soa.nsname, soa.serial);
Properties
nsname: string
Primary authoritative nameserver for the zone.
console.log(soa.nsname);
hostmaster: string
Responsible mailbox encoded as a DNS name.
console.log(soa.hostmaster);
serial: number
Zone serial number used by secondary nameservers.
console.log(soa.serial);
refresh: number
Suggested refresh interval in seconds.
console.log(soa.refresh);
retry: number
Suggested retry interval in seconds.
console.log(soa.retry);
expire: number
Zone expiry interval in seconds.
console.log(soa.expire);
minttl: number
Minimum TTL field from the SOA record.
console.log(soa.minttl);
interface SrvRecord {
Service-location DNS record payload returned for SRV lookups.
The resolver returns records in wire order; callers that need RFC 2782 selection should sort/group by priority and apply weighted selection.
const records = await new Resolver().resolve('_xmpp-server._tcp.example.com', 'SRV');
for (const srv of records) console.log(srv.name, srv.port);
Properties
priority: number
SRV priority; lower values are preferred.
const firstPriority = srv.priority;
weight: number
SRV weight used for load distribution within a priority group.
console.log(srv.weight);
port: number
TCP or UDP port advertised by the service.
console.log(srv.port);
name: string
Target host name for the service.
const addrs = await resolver.resolve(srv.name, 'AAAA');
interface DsRecord {
Delegation signer DNSSEC record payload.
DS records bind a child zone DNSKEY to its parent zone. The digest is the raw hash from wire format; callers that display it commonly hex-encode it.
import { Resolver } from 'fino:net/dns';
const [ds] = await new Resolver({ dnssec: true }).resolve('example.com', 'DS');
const hex = Array.from(ds.digest, (b) => b.toString(16).padStart(2, '0')).join('');
console.log(ds.keyTag, ds.algorithm, ds.digestType, hex);
Properties
keyTag: number
Key tag of the child DNSKEY this DS record identifies.
The value is copied from the wire record and is used with algorithm and
digestType to select the matching DNSKEY during validation.
algorithm: number
DNSSEC algorithm number for the referenced child DNSKEY.
Algorithm numbers follow the IANA DNS Security Algorithm registry.
digestType: number
Digest algorithm number used to hash the child DNSKEY owner name and key.
Common values include SHA-1 and SHA-256, but unsupported digest algorithms remain visible here so callers can inspect the delegation.
digest: Uint8Array
Raw DS digest bytes from RDATA.
Display tools usually hex-encode this value. The resolver keeps bytes unchanged for DNSSEC chain validation.
interface DnskeyRecord {
DNSSEC DNSKEY record payload.
DNSKEY records publish a zone's public signing keys. The resolver parses the raw key material and uses it with DS and RRSIG records when DNSSEC validation is enabled.
import { Resolver } from 'fino:net/dns';
const keys = await new Resolver({ dnssec: true }).resolve('example.com', 'DNSKEY');
for (const key of keys) {
const isKsk = (key.flags & 0x0001) !== 0;
console.log(key.algorithm, key.publicKey.byteLength, isKsk ? 'KSK' : 'ZSK');
}
Properties
flags: number
DNSKEY flags field.
This includes bits such as Zone Key and Secure Entry Point. The resolver exposes the numeric field unchanged.
protocol: number
DNSKEY protocol field.
DNSSEC uses protocol value 3; other values remain visible for malformed
or experimental records.
algorithm: number
DNSSEC algorithm number for this key.
publicKey: Uint8Array
Raw public key bytes from RDATA.
The encoding is algorithm-specific and is not converted to a WebCrypto key by the parser.
interface RrsigRecord {
DNSSEC RRSIG record payload.
RRSIG records authenticate an RRset. Time fields are Unix epoch seconds from
the wire record; the parser does not convert them to Date objects.
import { Resolver } from 'fino:net/dns';
const [sig] = await new Resolver({ dnssec: true }).resolve('example.com', 'RRSIG');
console.log(sig.typeCovered, sig.keyTag, new Date(sig.expiration * 1000).toISOString());
Properties
typeCovered: number
Numeric record type covered by this signature.
algorithm: number
DNSSEC algorithm number used by the signing key.
labels: number
Number of labels in the original signed owner name.
This is used by DNSSEC wildcard handling.
originalTtl: number
Original TTL of the signed RRset in seconds.
expiration: number
Signature expiration time as Unix epoch seconds.
inception: number
Signature inception time as Unix epoch seconds.
keyTag: number
Key tag of the DNSKEY that produced this signature.
signerName: string
Signer name from RDATA.
signature: Uint8Array
Raw signature bytes.
The byte format depends on algorithm.
interface NsecRecord {
DNSSEC NSEC authenticated-denial record payload.
NSEC records prove non-existence by naming the next existing owner and a type bitmap for the current owner.
import { Resolver, RECORD_TYPES } from 'fino:net/dns';
const [nsec] = await new Resolver({ dnssec: true }).resolve('example.com', 'NSEC');
console.log(nsec.nextDomainName, nsec.types.includes(RECORD_TYPES.A));
Properties
nextDomainName: string
Next existing owner name in canonical order.
types: number[]
Numeric RR types present at the NSEC owner name.
interface Nsec3Record {
DNSSEC NSEC3 authenticated-denial record payload.
NSEC3 records prove non-existence with hashed owner names. The parser exposes salt and hashed owner bytes directly so validation code can recompute the covered interval.
import { Resolver } from 'fino:net/dns';
const [nsec3] = await new Resolver({ dnssec: true }).resolve('example.com', 'NSEC3');
const optOut = (nsec3.flags & 0x01) !== 0;
console.log(nsec3.hashAlgorithm, nsec3.iterations, nsec3.salt.byteLength, optOut);
Properties
hashAlgorithm: number
Hash algorithm number used for owner names.
flags: number
NSEC3 flags field.
This may include the opt-out bit for insecure delegations.
iterations: number
Number of additional hash iterations.
salt: Uint8Array
Salt bytes used by the NSEC3 hash, or an empty array when no salt is set.
nextHashedOwnerName: Uint8Array
Next hashed owner name bytes from RDATA.
types: number[]
Numeric RR types present at the original owner name.
interface Nsec3ParamRecord {
DNSSEC NSEC3PARAM record payload.
NSEC3PARAM records publish the hash parameters a zone uses for NSEC3 authenticated denial.
import { Resolver } from 'fino:net/dns';
const [params] = await new Resolver({ dnssec: true }).resolve('example.com', 'NSEC3PARAM');
console.log(params.hashAlgorithm, params.iterations, params.salt.byteLength);
Properties
hashAlgorithm: number
Hash algorithm number used for NSEC3 owner hashes.
flags: number
NSEC3 parameter flags.
iterations: number
Number of additional hash iterations.
salt: Uint8Array
Salt bytes used by the zone, or an empty array when no salt is configured.
interface DnsEdnsMetadata {
EDNS(0) metadata parsed from an OPT pseudo-record.
EDNS extends the DNS header with a larger UDP payload size, an extended
response code, a version, and feature flags such as DNSSEC OK. This metadata
is present only when the response contains an OPT record, and it surfaces on
the edns field of a parsed DnsResponse.
import type { DnsResponse } from 'fino:net/dns';
function reportEdns(response: DnsResponse) {
if (!response.edns) return;
console.log(response.edns.udpPayloadSize, response.edns.dnssecOk);
}
Properties
udpPayloadSize: number
UDP payload size advertised by the responder.
dnssecOk: boolean
Whether the DNSSEC OK bit was set.
A true value means the peer was willing to send DNSSEC records; it is not proof that the answer was validated.
extendedRcode: number
Extended response-code bits from the OPT record.
version: number
EDNS version number.
flags: number
Raw EDNS flags field.
interface DnsResourceRecord {
Decoded DNS resource record from a response section.
type is the numeric QTYPE, ttl is seconds, and data follows the shape
documented by DnsRecordData. The resolver does not cache by TTL.
const answers = await new Resolver().resolve('example.com', 'A');
for (const answer of answers) console.log(answer);
Properties
name: string
Owner name for this record.
console.log(record.name);
type: number
Numeric DNS record type, such as RECORD_TYPES.A.
if (record.type === RECORD_TYPES.A) console.log(record.data);
ttl: number
Record TTL in seconds.
console.log(`cacheable for ${record.ttl}s`);
classCode: number
DNS class code with protocol-specific high bits masked off.
For ordinary IN records this is 1. mDNS responses may set the high
cache-flush bit in the wire class field; this property stores only the
actual class value.
cacheFlush: boolean
Whether the mDNS cache-flush bit was set in the RR class field.
rawData: Uint8Array
Exact RDATA bytes from the packet.
data: DnsRecordData
Parsed record payload, or raw bytes for unsupported record types.
if (record.data instanceof Uint8Array) console.log(record.data.byteLength);
interface DnsResponse {
Parsed DNS response packet.
rcode exposes the low four bits of the DNS flags field. A non-zero value
is converted to an error by Resolver.resolve, but parser tests can inspect
it directly.
const records = await new Resolver().resolve('example.com', 'MX');
console.log(records);
Properties
id: number
Transaction ID copied from the DNS header.
if (response.id !== queryId) throw new Error('spoofed response');
flags: number
Raw DNS flags field.
const authoritative = Boolean(response.flags & 0x0400);
rcode: number
DNS response code; zero means no DNS-layer error.
if (response.rcode === 3) console.log('not found');
truncated: boolean
True when the DNS server marked the UDP response as truncated.
if (response.truncated) console.log('retry over TCP if needed');
answers: DnsResourceRecord[]
Answer section records.
for (const answer of response.answers) console.log(answer.data);
authorities: DnsResourceRecord[]
Authority section records.
console.log(response.authorities.length);
additionals: DnsResourceRecord[]
Additional section records.
console.log(response.additionals.length);
edns?: DnsEdnsMetadata
Parsed EDNS(0) metadata when the response contains an OPT pseudo-RR.
Omitted for classic DNS responses without EDNS.
interface ResolverOptions {
Resolver timeout and retry controls.
Defaults are timeout: 5000 milliseconds and retries: 2. The timeout is
applied per query attempt; after all servers and retries fail, resolution
throws an ETIMEOUT error with the hostname attached.
const resolver = new Resolver({ timeout: 1000, retries: 1 });
Properties
timeout?: number
Per-attempt timeout in milliseconds.
const resolver = new Resolver({ timeout: 750 });
retries?: number
Number of retry rounds across the configured nameserver list.
const resolver = new Resolver({ retries: 3 });
dnssec?: boolean
Enable local DNSSEC validation and request DNSSEC records with EDNS(0) DO.
interface LookupOptions {
Address-family preference for lookup.
Omit family to prefer IPv4 first and then fall back to IPv6. Passing 4
or 6 queries only that family and throws if no address is found.
const result = await lookup('example.com', { family: 6 });
Properties
family?: 4 | 6
Requested address family, or omitted for IPv4-then-IPv6 fallback.
await lookup('example.com', { family: 4 });
dnssec?: boolean
Enable local DNSSEC validation for this lookup.
interface LookupResult {
Primary address returned by lookup.
The address string is already formatted for the family. IPv6 addresses use
system inet_ntop formatting.
const { address, family } = await lookup('example.com');
console.log(`${address} is IPv${family}`);
Properties
address: string
IP address string.
console.log(result.address);
family: 4 | 6
Address family for address.
if (result.family === 6) console.log('IPv6');
Constants
const RECORD_TYPES
Map from supported DNS record type names to numeric QTYPE values.
These constants are useful when building or parsing packets manually. Unknown record types can still be parsed, but public resolution is limited to these keys.
const aaaa = RECORD_TYPES.AAAA;
console.log(aaaa);
Properties
A
NS
CNAME
SOA
PTR
MX
TXT
AAAA
SRV
DS
RRSIG
NSEC
DNSKEY
NSEC3
NSEC3PARAM
Classes
class Resolver {
DNS resolver with configurable nameservers, timeout, and retries.
The resolver lazily reads /etc/resolv.conf on first use, falls back to
public IPv4 DNS servers if none are found, follows CNAME chains for A and
AAAA lookups up to a fixed hop limit, and retries over TCP when a UDP
response has the DNS truncated bit set. Set dnssec: true to request DNSSEC
records with EDNS(0) DO and validate signed answers from the root trust
anchor before returning them.
import { Resolver } from 'fino:net/dns';
const resolver = new Resolver({ timeout: 1500 });
const addrs = await resolver.resolve('example.com', 'A');
Constructors
constructor(opts: ResolverOptions = {})
Create a resolver.
timeout defaults to 5000 ms and retries defaults to 2. Nameservers are
loaded lazily, so constructing a resolver performs no I/O.
const resolver = new Resolver({ timeout: 1000, retries: 1 });
Methods
getServers(): string[]
Return the current list of nameserver IP addresses.
If setServers() has not been called and /etc/resolv.conf has not been
loaded yet, this returns the built-in fallback server list. Non-default
ports are formatted as ip:port for IPv4 or [ip]:port for IPv6.
const resolver = new Resolver();
console.log(resolver.getServers());
setServers(servers: string[]): void
Override the nameserver list. Each entry is an IPv4 or IPv6 address string, optionally with a port: '1.1.1.1', '1.1.1.1:5353', '[::1:5353'.
Passing an empty array throws. The entries are trusted as IP literals and used for future queries; existing in-flight queries are not cancelled.
const resolver = new Resolver();
resolver.setServers(['1.1.1.1', '[2606:4700:4700::1111]:53']);
async resolve(hostname: string, rrtype: RecordTypeName = 'A'): Promise<DnsRecordData[]>
Resolve a hostname for the given record type.
Supported record names are the keys of RECORD_TYPES. A and AAAA lookups
follow CNAME chains up to 10 hops. DNS-layer errors throw with code and
hostname properties; no-answer responses resolve to an empty array.
const resolver = new Resolver();
const addresses = await resolver.resolve('example.com', 'A');
async resolve4(hostname: string)
Resolve IPv4 A records for a hostname.
Returns an empty array when the name exists but has no A records. Throws on DNS errors, timeout, malformed packets, or excessive CNAME hops.
const addrs = await new Resolver().resolve4('example.com');
async resolve6(hostname: string)
Resolve IPv6 AAAA records for a hostname.
The returned strings are formatted through inet_ntop; no zone IDs are
added. Throws on DNS errors or timeout.
const addrs = await new Resolver().resolve6('example.com');
async resolveMx(hostname: string)
Resolve MX records for a hostname.
Records are returned in DNS response order, not sorted by priority.
const mx = await new Resolver().resolveMx('example.com');
async resolveTxt(hostname: string)
Resolve TXT records for a hostname.
Each DNS TXT record is returned as an array of character strings because a single TXT record can contain multiple length-prefixed strings.
const txt = await new Resolver().resolveTxt('example.com');
async resolveNs(hostname: string)
Resolve authoritative nameserver records for a hostname.
const ns = await new Resolver().resolveNs('example.com');
async resolveSrv(hostname: string)
Resolve SRV service-location records.
The resolver does not perform weighted target selection; callers should apply SRV priority and weight rules themselves.
const srv = await new Resolver().resolveSrv('_xmpp-server._tcp.example.com');
async resolveSoa(hostname: string)
Resolve SOA records for a zone name.
Most zones return one SOA record, but the return shape is still an array to match the generic resolver API.
const [soa] = await new Resolver().resolveSoa('example.com');
async resolveCname(hostname: string)
Resolve CNAME records for a hostname.
This returns only CNAME answers; it does not follow the target to A or AAAA addresses.
const aliases = await new Resolver().resolveCname('www.example.com');
async reverse(ip: string): Promise<DnsRecordData[]>
Reverse DNS lookup.
Converts IPv4 or IPv6 addresses to the appropriate PTR query name and resolves PTR records. Invalid IP strings are not fully validated before the query name is built.
const names = await new Resolver().reverse('8.8.8.8');
Functions
async function lookup(hostname: string, opts: LookupOptions = {}): Promise<LookupResult>
Look up the primary address for a hostname.
This module-level helper lazily creates a shared Resolver. IP literals are
returned without DNS I/O. When family is omitted, this implementation
queries IPv4. Missing records throw ENOTFOUND; malformed address responses
throw ENODATA.
import { lookup } from 'fino:net/dns';
const { address, family } = await lookup('example.com', { family: 4 });