@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.
- package/CHANGELOG.md +885 -875
- package/index.js +18 -1
- package/lib/agent-snapshot.js +346 -0
- package/lib/agent-trace.js +218 -0
- package/lib/guard-all.js +1 -0
- package/lib/guard-dsn.js +379 -0
- package/lib/guard-envelope.js +294 -0
- package/lib/guard-smtp-command.js +484 -0
- package/lib/guard-snapshot-envelope.js +168 -0
- package/lib/guard-trace-context.js +172 -0
- package/lib/ip-utils.js +102 -0
- package/lib/mail-auth.js +4 -35
- package/lib/mail-greylist.js +448 -0
- package/lib/mail-helo.js +473 -0
- package/lib/mail-rbl.js +392 -0
- package/lib/mail.js +2 -1
- package/lib/network-dns-resolver.js +500 -0
- package/lib/network.js +1 -0
- package/lib/redis-client.js +2 -1
- package/lib/safe-dns.js +665 -0
- package/lib/tracing.js +36 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -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");
|
package/lib/redis-client.js
CHANGED
|
@@ -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 = (
|
|
177
|
+
servername = (ipUtils.isIPv4Shape(host) || host.indexOf(":") !== -1)
|
|
177
178
|
? undefined : host;
|
|
178
179
|
}
|
|
179
180
|
|