@blamejs/core 0.9.28 → 0.9.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,500 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.network.dns.resolver
4
+ * @nav Network
5
+ * @title DNS Resolver
6
+ * @order 215
7
+ *
8
+ * @intro
9
+ * Validating stub resolver that composes `b.network.dns` transport
10
+ * (DoT / DoH / system) with `b.safeDns` parsing and a TTL-aware
11
+ * cache. Used by every framework consumer that walks the DNS:
12
+ * DKIM TXT lookup, MTA-STS verify, DANE TLSA fetch, BIMI / VMC
13
+ * discovery, SVCB / HTTPS discovery, RBL queries, AutoConfig /
14
+ * AutoDiscover endpoint resolution, future MX lookup at submission.
15
+ *
16
+ * Stub-mode resolver — every query goes to the operator-configured
17
+ * upstream recursive resolver (default `cloudflare-dns.com` over
18
+ * DoH per `b.network.dns.useDnsOverHttps()`). DNSSEC validation is
19
+ * delegated to the upstream resolver and surfaced via the AD bit
20
+ * (RFC 4035 §3.2.3); per-call `validate: true` opt-in re-checks
21
+ * the AD bit per-query, refusing responses with `AD=0`. Full local
22
+ * RRSIG signature verification is deferred — it requires IANA root
23
+ * trust anchor distribution + management which has its own lifecycle
24
+ * (root KSK rollover, RFC 5011) that doesn't belong in a stub. An
25
+ * operator that needs local RRSIG verify points the resolver at
26
+ * their own validating recursive (Unbound / BIND9) and the AD bit
27
+ * surfaces here; alternatively, `b.safeDns.parseResponse` exposes
28
+ * the parsed DNSKEY + RRSIG + DS records for an operator-supplied
29
+ * verifier.
30
+ *
31
+ * QNAME minimization (RFC 9156) is a recursive-resolver concern —
32
+ * the framework runs in stub mode so the operator's upstream
33
+ * recursive (Cloudflare / Google / Unbound) implements QNAME-min;
34
+ * our queries always carry the full QNAME and there's nothing to
35
+ * minimize. Documented so operators don't pass `qnameMin: true` and
36
+ * expect a behavior change.
37
+ *
38
+ * ## TTL cache + serve-stale (RFC 8767)
39
+ *
40
+ * Every successful response caches by `{ name, type }` keyed on the
41
+ * minimum TTL across the answer RRs (RFC 2181 §5.2 — RRset TTL is
42
+ * the minimum of the included RR TTLs). On expiry the entry is
43
+ * removed from the live cache; with `serveStale: <ms>` configured,
44
+ * expired entries are retained for that additional window and
45
+ * returned on upstream failure or malformed response (RFC 8767 —
46
+ * stale-bread-is-better-than-no-bread for resolver resiliency
47
+ * under DoS / authoritative outage). Returned entries carry
48
+ * `{ stale: true }` so consumer code can decide whether to use the
49
+ * data or hard-fail. RFC 8767 §6 recommends a 7-day max stale
50
+ * window; we default to 6h.
51
+ *
52
+ * ## CNAME chain following (RFC 1912 §2.4)
53
+ *
54
+ * `followCnames(name, type)` walks CNAME redirections until the
55
+ * target record arrives or the chain depth cap from `b.safeDns`
56
+ * trips. Each hop is its own resolver query; each hop's response
57
+ * parses through `b.safeDns.parseResponse` independently. Default
58
+ * cap = 8 (matches BIND9's canonical-name-translation cap). RFC
59
+ * 1912 §2.4 warns against long CNAME chains; the cap defends
60
+ * redirect-loop DoS regardless of upstream resolver behavior.
61
+ *
62
+ * ## CVE / threat-model coverage
63
+ *
64
+ * The resolver layer's defenses (parser-level + cache-level —
65
+ * transport-level defenses live in `b.network.dns`):
66
+ *
67
+ * - Cache-poisoning resilience: every parse routes through
68
+ * `b.safeDns` which caps response bytes, RR counts, name
69
+ * lengths, and pointer-chain depth — bounds the attacker's
70
+ * inflation surface for poisoning attempts (CVE-2008-1447
71
+ * Kaminsky class; the random query ID + TLS-encrypted DoH
72
+ * transport defend transport-side, this layer defends parse-
73
+ * side).
74
+ * - CVE-2022-3204 (NRDelegationAttack): per-section RR caps in
75
+ * `b.safeDns` bound the authority + additional sections that
76
+ * back a malicious non-responsive delegation.
77
+ * - CVE-2023-50387 (KeyTrap) + CVE-2023-50868 (NSEC3-encloser):
78
+ * DNSKEY + RRSIG + NSEC3 record counts bounded at parse time;
79
+ * validators downstream don't see the inflated set.
80
+ * - CVE-2024-1737 (BIND9 large-RRset exhaustion): RR-count caps
81
+ * refuse responses with abnormally large RRsets per hostname.
82
+ * - CNAME redirect loops: `safeDns.checkCnameChainDepth` at every
83
+ * hop in `followCnames`; matches BIND9's operational cap of 8.
84
+ * - TTL pinning of poisoned entries: operator-configurable
85
+ * `maxTtlMs` ceiling (default 24h) caps any TTL the upstream
86
+ * returns; a 2^31-second TTL (RFC 2181 absolute max) can't
87
+ * persist past the ceiling.
88
+ *
89
+ * ## Why it exists
90
+ *
91
+ * `node:dns` returns parsed values but doesn't bound any of the
92
+ * dimensions an attacker can inflate — RR count, CNAME depth,
93
+ * compression-pointer chain, TXT rdata length. The validating
94
+ * resolver routes every parse through `b.safeDns` and exposes one
95
+ * shape every framework consumer can compose, replacing the
96
+ * scattered `node:dns` reach-throughs across `lib/mail-*.js`,
97
+ * `lib/mtla-sts*.js`, `lib/dane*.js`, and future MX / BIMI / SVCB
98
+ * primitives. Audit + posture rides through the resolver instance.
99
+ *
100
+ * @card
101
+ * Validating stub resolver — composes b.network.dns transport,
102
+ * b.safeDns parsing, TTL-aware cache with serve-stale on failure
103
+ * (RFC 8767), CNAME chain following with safeDns depth cap, DNSSEC
104
+ * AD-bit surface (RFC 4035 §3.2.3).
105
+ */
106
+
107
+ var C = require("./constants");
108
+ var https = require("node:https");
109
+ var nodeCrypto = require("node:crypto");
110
+ var { defineClass } = require("./framework-error");
111
+ var networkDns = require("./network-dns");
112
+ var safeDns = require("./safe-dns");
113
+ var safeUrl = require("./safe-url");
114
+ var safeBuffer = require("./safe-buffer");
115
+ var lazyRequire = require("./lazy-require");
116
+
117
+ var audit = lazyRequire(function () { return require("./audit"); });
118
+
119
+ var ResolverError = defineClass("ResolverError", { alwaysPermanent: true });
120
+
121
+ // Default cache TTL ceiling — RFC 1035 §3.2.1 allows TTLs up to 2^31
122
+ // seconds but real-world records cap much lower. We cap operator-side
123
+ // to 24h so a long-TTL response can't pin a stale value past a working
124
+ // day. Refresh on next query.
125
+ var DEFAULT_MAX_TTL_MS = C.TIME.hours(24);
126
+ var DEFAULT_MIN_TTL_MS = C.TIME.seconds(60);
127
+ var DEFAULT_STALE_WINDOW = C.TIME.hours(6);
128
+ var DEFAULT_PROFILE = "strict";
129
+
130
+ var QTYPE_BY_NAME = Object.freeze({
131
+ A: 1,
132
+ NS: 2,
133
+ CNAME: 5, // allow:raw-byte-literal — IANA DNS qtype code
134
+ SOA: 6, // allow:raw-byte-literal — IANA DNS qtype code
135
+ PTR: 12, // allow:raw-byte-literal — IANA DNS qtype code
136
+ MX: 15, // allow:raw-byte-literal — IANA DNS qtype code
137
+ TXT: 16, // allow:raw-byte-literal — IANA DNS qtype code
138
+ AAAA: 28, // allow:raw-byte-literal — IANA DNS qtype code
139
+ SRV: 33, // allow:raw-byte-literal — IANA DNS qtype code
140
+ DS: 43, // allow:raw-byte-literal — IANA DNS qtype code
141
+ DNSKEY: 48, // allow:raw-byte-literal — IANA DNS qtype code
142
+ TLSA: 52, // allow:raw-byte-literal — IANA DNS qtype code
143
+ SVCB: 64, // allow:raw-byte-literal — IANA DNS qtype code
144
+ HTTPS: 65, // allow:raw-byte-literal — IANA DNS qtype code
145
+ });
146
+
147
+ /**
148
+ * @primitive b.network.dns.resolver.create
149
+ * @signature b.network.dns.resolver.create(opts?)
150
+ * @since 0.9.31
151
+ * @status stable
152
+ * @related b.safeDns.parseResponse, b.network.dns.querySvcb
153
+ *
154
+ * Build a resolver instance with the given options. Returns an
155
+ * instance with `.query(name, type, opts) → Promise<{ rrs, ttl,
156
+ * fromCache, stale, validated, response }>` plus per-type shortcuts.
157
+ *
158
+ * @opts
159
+ * profile: "strict" | "balanced" | "permissive",
160
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
161
+ * maxTtlMs: number, // cap any TTL from upstream; default 24h
162
+ * minTtlMs: number, // floor short-TTL records; default 60s
163
+ * serveStale: number | false, // ms to retain expired entries; default 6h
164
+ * transport: { lookup(name, qtype) → Promise<Buffer> }, // operator override
165
+ * audit: b.audit namespace,
166
+ *
167
+ * @example
168
+ * var resolver = b.network.dns.resolver.create({ profile: "strict" });
169
+ * var r = await resolver.queryTxt("_dmarc.example.com");
170
+ * console.log(r.rrs.map(function (rr) { return rr.decoded.join(""); }));
171
+ */
172
+ function create(opts) {
173
+ opts = opts || {};
174
+ var profile = opts.profile || (opts.posture && safeDns.compliancePosture(opts.posture)) || DEFAULT_PROFILE;
175
+ if (!safeDns.PROFILES[profile]) {
176
+ throw new ResolverError("resolver/bad-profile",
177
+ "create: unknown profile '" + profile + "'");
178
+ }
179
+ var maxTtlMs = typeof opts.maxTtlMs === "number" ? opts.maxTtlMs : DEFAULT_MAX_TTL_MS;
180
+ var minTtlMs = typeof opts.minTtlMs === "number" ? opts.minTtlMs : DEFAULT_MIN_TTL_MS;
181
+ var serveStale = opts.serveStale === false ? 0 :
182
+ typeof opts.serveStale === "number" ? opts.serveStale : DEFAULT_STALE_WINDOW;
183
+ var transport = opts.transport || _defaultTransport();
184
+ var auditImpl = opts.audit || audit();
185
+
186
+ if (typeof transport.lookup !== "function") {
187
+ throw new ResolverError("resolver/bad-transport",
188
+ "create: transport.lookup must be a function");
189
+ }
190
+ if (!isFinite(maxTtlMs) || maxTtlMs <= 0) {
191
+ throw new ResolverError("resolver/bad-input",
192
+ "create: maxTtlMs must be a positive finite number");
193
+ }
194
+ if (!isFinite(minTtlMs) || minTtlMs < 0) {
195
+ throw new ResolverError("resolver/bad-input",
196
+ "create: minTtlMs must be a non-negative finite number");
197
+ }
198
+ if (!isFinite(serveStale) || serveStale < 0) {
199
+ throw new ResolverError("resolver/bad-input",
200
+ "create: serveStale must be a non-negative finite number or false");
201
+ }
202
+
203
+ var cache = new Map(); // key → { response, parsed, ttl, expiresAt, staleUntil }
204
+
205
+ function _key(name, qtype) {
206
+ return name.toLowerCase() + "|" + qtype;
207
+ }
208
+
209
+ function _safeEmit(action, metadata) {
210
+ try {
211
+ if (auditImpl && typeof auditImpl.safeEmit === "function") {
212
+ auditImpl.safeEmit({ action: "network.dns.resolver." + action, outcome: "success", metadata: metadata });
213
+ }
214
+ } catch (_e) { /* audit drop-silent per validation tier policy */ }
215
+ }
216
+
217
+ async function query(name, type, qopts) {
218
+ qopts = qopts || {};
219
+ if (typeof name !== "string" || name.length === 0) {
220
+ throw new ResolverError("resolver/bad-input",
221
+ "query: name must be a non-empty string");
222
+ }
223
+ var qtype = typeof type === "number" ? type :
224
+ typeof type === "string" ? QTYPE_BY_NAME[type.toUpperCase()] :
225
+ null;
226
+ if (!qtype) {
227
+ throw new ResolverError("resolver/bad-input",
228
+ "query: unknown qtype '" + type + "'");
229
+ }
230
+ var validate = qopts.validate === true;
231
+ var key = _key(name, qtype);
232
+
233
+ // Cache hit fresh?
234
+ var now = Date.now();
235
+ var hit = cache.get(key);
236
+ if (hit && hit.expiresAt > now) {
237
+ // validate: true refuses cached responses that weren't AD=1 when
238
+ // first cached — a non-validating call mustn't poison a later
239
+ // validating call's verdict. RFC 4035 §3.2.3: AD is per-response,
240
+ // not per-record, so we honor the original verdict on every hit.
241
+ if (validate && !hit.validated) {
242
+ throw new ResolverError("resolver/validate-failed",
243
+ "query: validate: true but cached response was AD=0 for " +
244
+ name + "/" + qtype);
245
+ }
246
+ return _result(hit.parsed, hit.ttl, true, false, hit.validated);
247
+ }
248
+
249
+ // Cache miss / expired — fetch from upstream.
250
+ var wireResponse;
251
+ try {
252
+ wireResponse = await transport.lookup(name, qtype);
253
+ } catch (e) {
254
+ // Upstream failure — serve stale if we have it within window.
255
+ if (hit && serveStale > 0 && hit.staleUntil > now) {
256
+ _safeEmit("served_stale", { name: name, qtype: qtype, reason: "upstream-failure" });
257
+ return _result(hit.parsed, hit.ttl, true, true, hit.validated);
258
+ }
259
+ throw new ResolverError("resolver/upstream-failed",
260
+ "query: upstream lookup failed for " + name + "/" + qtype + ": " + (e && e.message || String(e)));
261
+ }
262
+
263
+ var parsed;
264
+ try {
265
+ parsed = safeDns.parseResponse(wireResponse, { profile: profile });
266
+ } catch (e) {
267
+ // Malformed upstream response — serve stale if within window.
268
+ if (hit && serveStale > 0 && hit.staleUntil > now) {
269
+ _safeEmit("served_stale", { name: name, qtype: qtype, reason: "parse-failed" });
270
+ return _result(hit.parsed, hit.ttl, true, true, hit.validated);
271
+ }
272
+ throw e;
273
+ }
274
+
275
+ if (parsed.rcode !== 0) {
276
+ // RFC 1035 §4.1.1 — non-zero RCODE. Surface and refuse caching.
277
+ throw new ResolverError("resolver/nxdomain-or-error",
278
+ "query: upstream RCODE=" + parsed.rcode + " for " + name + "/" + qtype);
279
+ }
280
+
281
+ // AD bit (RFC 4035 §3.2.3) — set by upstream after chain validation.
282
+ // Bit 5 of byte 3 of header; parsed.flags is the full 16-bit flags
283
+ // field at offset 2..3. AD is bit 5 within byte 3 = bit 5 of the
284
+ // low byte of the 16-bit flags value.
285
+ var ad = (parsed.flags & 0x0020) !== 0; // allow:raw-byte-literal — RFC 4035 §3.2.3 AD-bit mask within DNS header flags
286
+ if (validate && !ad) {
287
+ throw new ResolverError("resolver/validate-failed",
288
+ "query: validate: true but upstream returned AD=0 for " + name + "/" + qtype);
289
+ }
290
+
291
+ // Compute effective TTL — min across answer RRs (RFC 2181 §5.2:
292
+ // RRset TTL is the minimum of the included RR TTLs), then clamped
293
+ // to [minTtlMs, maxTtlMs] to bound any single RR's TTL from
294
+ // pinning a poisoned entry past operator policy.
295
+ var rrTtl = _minTtl(parsed.answer);
296
+ var ttlMs = Math.max(minTtlMs, Math.min(maxTtlMs, rrTtl * C.TIME.seconds(1)));
297
+ var expiresAt = now + ttlMs;
298
+ var staleUntil = serveStale > 0 ? expiresAt + serveStale : expiresAt;
299
+ cache.set(key, {
300
+ parsed: parsed,
301
+ ttl: ttlMs,
302
+ expiresAt: expiresAt,
303
+ staleUntil: staleUntil,
304
+ validated: ad,
305
+ });
306
+ _safeEmit("cached", { name: name, qtype: qtype, ttlMs: ttlMs, adBit: ad });
307
+ return _result(parsed, ttlMs, false, false, ad);
308
+ }
309
+
310
+ function _result(parsed, ttlMs, fromCache, stale, validated) {
311
+ return {
312
+ rrs: parsed.answer,
313
+ authority: parsed.authority,
314
+ additional: parsed.additional,
315
+ ttl: ttlMs,
316
+ fromCache: fromCache,
317
+ stale: stale,
318
+ validated: validated,
319
+ response: parsed,
320
+ };
321
+ }
322
+
323
+ /**
324
+ * @primitive b.network.dns.resolver.followCnames
325
+ * @signature b.network.dns.resolver.followCnames(name, type, opts?)
326
+ * @since 0.9.31
327
+ * @status stable
328
+ *
329
+ * Walk CNAME redirections until the target record arrives or the
330
+ * chain depth cap from `b.safeDns` trips. Returns the same shape as
331
+ * `query()` plus `chain: [name, name, ...]` listing each hop.
332
+ *
333
+ * @opts
334
+ * validate: boolean, // per-call: refuse if upstream AD=0
335
+ *
336
+ * @example
337
+ * var r = await resolver.followCnames("alias.example.com", "A");
338
+ * console.log(r.chain, r.rrs.map(function (rr) { return rr.decoded; }));
339
+ */
340
+ async function followCnames(name, type, qopts) {
341
+ var depth = 0;
342
+ var chain = [name];
343
+ var current = name;
344
+ while (true) {
345
+ safeDns.checkCnameChainDepth(depth, { profile: profile });
346
+ var r = await query(current, type, qopts);
347
+ // If any RR matches the target type, we're done.
348
+ var typeCode = typeof type === "number" ? type : QTYPE_BY_NAME[type.toUpperCase()];
349
+ var hasTarget = r.rrs.some(function (rr) { return rr.type === typeCode; });
350
+ if (hasTarget) {
351
+ r.chain = chain;
352
+ return r;
353
+ }
354
+ // Otherwise look for a CNAME to follow.
355
+ var cnameRr = r.rrs.find(function (rr) { return rr.type === QTYPE_BY_NAME.CNAME; });
356
+ if (!cnameRr) {
357
+ // No matching type and no CNAME — return empty result.
358
+ r.chain = chain;
359
+ return r;
360
+ }
361
+ depth += 1;
362
+ current = cnameRr.decoded;
363
+ chain.push(current);
364
+ }
365
+ }
366
+
367
+ function _typed(typeName) {
368
+ return function (name, qopts) { return query(name, typeName, qopts); };
369
+ }
370
+
371
+ function clearCache() { cache.clear(); }
372
+
373
+ function cacheSize() { return cache.size; }
374
+
375
+ return {
376
+ query: query,
377
+ followCnames: followCnames,
378
+ queryA: _typed("A"),
379
+ queryAaaa: _typed("AAAA"),
380
+ queryCname: _typed("CNAME"),
381
+ queryMx: _typed("MX"),
382
+ queryNs: _typed("NS"),
383
+ queryTxt: _typed("TXT"),
384
+ querySrv: _typed("SRV"),
385
+ queryTlsa: _typed("TLSA"),
386
+ queryDs: _typed("DS"),
387
+ queryDnskey: _typed("DNSKEY"),
388
+ querySvcb: _typed("SVCB"),
389
+ queryHttps: _typed("HTTPS"),
390
+ clearCache: clearCache,
391
+ cacheSize: cacheSize,
392
+ profile: profile,
393
+ ResolverError: ResolverError,
394
+ };
395
+ }
396
+
397
+ function _defaultTransport() {
398
+ // Default transport — compose b.network.dns.useDnsOverHttps()'s
399
+ // existing DoH path. We use the wire-format DoH endpoint directly
400
+ // so the response arrives as raw bytes for safeDns parsing.
401
+ return {
402
+ lookup: function (name, qtype) {
403
+ return _wireLookup(name, qtype);
404
+ },
405
+ };
406
+ }
407
+
408
+ // _wireLookup — fetch a wire-format DNS response via the framework's
409
+ // existing DoH path. Returns the raw response bytes for safeDns to
410
+ // parse. Distinct from the existing network-dns DoH path which returns
411
+ // already-decoded address strings — we need the raw bytes here.
412
+ async function _wireLookup(name, qtype) {
413
+ var url = networkDns._getDohUrlForTest ? networkDns._getDohUrlForTest() : "https://cloudflare-dns.com/dns-query";
414
+ // Encode a wire-format query for the target qtype.
415
+ var qbuf = _encodeWireQuery(name, qtype);
416
+ var b64 = qbuf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
417
+ var getUrl = url + (url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
418
+ var u = safeUrl.parse(getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
419
+ return new Promise(function (resolve, reject) {
420
+ // Raw DoH wire-format request — bypasses b.httpClient envelope
421
+ // because we need the raw binary response bytes for safeDns to
422
+ // parse (httpClient assumes JSON/text shapes).
423
+ var req = https.request({ // allow:raw-outbound-http — DoH wire-format response bytes; b.httpClient envelopes assume text/JSON, and httpClient → ssrfGuard → DNS → DoH would form a cycle
424
+ hostname: u.hostname,
425
+ port: u.port || 443, // allow:raw-byte-literal — HTTPS port
426
+ path: u.pathname + u.search,
427
+ method: "GET",
428
+ headers: { "accept": "application/dns-message" },
429
+ minVersion: "TLSv1.3",
430
+ ecdhCurve: C.TLS_GROUP_CURVE_STR,
431
+ }, function (res) {
432
+ var collector = safeBuffer.boundedChunkCollector({
433
+ maxBytes: C.BYTES.kib(64),
434
+ errorClass: ResolverError,
435
+ sizeCode: "resolver/upstream-too-large",
436
+ sizeMessage: "DoH response exceeds 64 KiB",
437
+ });
438
+ var pushFailed = null;
439
+ res.on("data", function (c) { if (!pushFailed) { try { collector.push(c); } catch (e) { pushFailed = e; } } });
440
+ res.on("end", function () {
441
+ try {
442
+ if (pushFailed) { reject(pushFailed); return; }
443
+ if (res.statusCode !== 200) { // allow:raw-byte-literal — HTTP 200 OK
444
+ reject(new ResolverError("resolver/upstream-http",
445
+ "DoH HTTP " + res.statusCode + " for " + name));
446
+ return;
447
+ }
448
+ resolve(collector.result());
449
+ } catch (e) { reject(e); }
450
+ });
451
+ });
452
+ req.on("error", function (e) {
453
+ reject(new ResolverError("resolver/upstream-failed",
454
+ "DoH request failed: " + e.message));
455
+ });
456
+ req.end();
457
+ });
458
+ }
459
+
460
+ // _encodeWireQuery — assemble a wire-format DNS query for (name, qtype).
461
+ // Mirrors the encoder in network-dns.js but accepts an explicit qtype
462
+ // (the existing function hardcodes A/AAAA based on family).
463
+ function _encodeWireQuery(name, qtype) {
464
+ var parts = name.split(".").filter(Boolean);
465
+ var nameLen = 1;
466
+ for (var i = 0; i < parts.length; i += 1) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
467
+ var buf = Buffer.alloc(12 + nameLen + 4); // allow:raw-byte-literal — RFC 1035 §4.1.1 header (12) + question tail (4) + name
468
+ var id = nodeCrypto.randomInt(0, 0x10000);
469
+ buf.writeUInt16BE(id, 0);
470
+ buf.writeUInt16BE(0x0100, 2); // allow:raw-byte-literal — RFC 1035 §4.1.1 RD=1 flags
471
+ buf.writeUInt16BE(1, 4); // allow:raw-byte-literal — RFC 1035 §4.1.1 qdcount
472
+ var off = 12; // allow:raw-byte-literal — RFC 1035 §4.1.1 header end / question start
473
+ for (var p = 0; p < parts.length; p += 1) {
474
+ var s = parts[p];
475
+ buf.writeUInt8(Buffer.byteLength(s, "ascii"), off);
476
+ off += 1;
477
+ off += buf.write(s, off, "ascii");
478
+ }
479
+ buf.writeUInt8(0, off);
480
+ off += 1;
481
+ buf.writeUInt16BE(qtype, off);
482
+ off += 2; // allow:raw-byte-literal — RFC 1035 §4.1.2 QTYPE width
483
+ buf.writeUInt16BE(1, off); // allow:raw-byte-literal — RFC 1035 §4.1.2 QCLASS=IN
484
+ return buf;
485
+ }
486
+
487
+ function _minTtl(rrs) {
488
+ if (!rrs || rrs.length === 0) return 0;
489
+ var min = Infinity;
490
+ for (var i = 0; i < rrs.length; i += 1) {
491
+ if (rrs[i].ttl < min) min = rrs[i].ttl;
492
+ }
493
+ return min === Infinity ? 0 : min;
494
+ }
495
+
496
+ module.exports = {
497
+ create: create,
498
+ ResolverError: ResolverError,
499
+ QTYPE_BY_NAME: QTYPE_BY_NAME,
500
+ };
package/lib/network.js CHANGED
@@ -34,6 +34,7 @@ var byteQuota = require("./network-byte-quota");
34
34
  var ntpCheck = require("./ntp-check");
35
35
  var nts = require("./network-nts");
36
36
  var networkDns = require("./network-dns");
37
+ networkDns.resolver = require("./network-dns-resolver");
37
38
  var networkProxy = require("./network-proxy");
38
39
  var networkTls = require("./network-tls");
39
40
  var heartbeat = require("./network-heartbeat");
@@ -30,6 +30,7 @@ var nodeUrl = require("node:url");
30
30
  var C = require("./constants");
31
31
  var safeAsync = require("./safe-async");
32
32
  var validateOpts = require("./validate-opts");
33
+ var ipUtils = require("./ip-utils");
33
34
  var { RedisError } = require("./framework-error");
34
35
 
35
36
  var _err = RedisError.factory;
@@ -173,7 +174,7 @@ function create(opts) {
173
174
  // SNI is only legal for hostnames; IP literals must omit servername.
174
175
  var servername = opts.servername;
175
176
  if (servername === undefined) {
176
- servername = (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host.indexOf(":") !== -1)
177
+ servername = (ipUtils.isIPv4Shape(host) || host.indexOf(":") !== -1)
177
178
  ? undefined : host;
178
179
  }
179
180