tangerine 1.2.2 → 1.3.0
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/README.md +223 -213
- package/index.js +73 -61
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
<br />
|
|
13
13
|
<div align="center">
|
|
14
|
-
🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends
|
|
14
|
+
🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends (with TTL support).
|
|
15
15
|
</div>
|
|
16
16
|
<hr />
|
|
17
17
|
<div align="center">
|
|
18
|
-
⚡ <
|
|
18
|
+
⚡ <a href="#tangerine-benchmarks"><i><u><strong>AS FAST AS</strong></u></i></a> native <a href="https://nodejs.org/api/dns.html" target="_blank">Node.js <code>dns</code></a>! 🚀 • Supports Node v16+ with ESM/CJS • Made for <a href="https://forwardemail.net" target="_blank"><strong>Forward Email</strong></a>.
|
|
19
19
|
</div>
|
|
20
20
|
<hr />
|
|
21
21
|
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
|
|
56
56
|
* [`tangerine.setServers(servers)`](#tangerinesetserversservers)
|
|
57
57
|
* [Options](#options)
|
|
58
|
+
* [Cache](#cache)
|
|
58
59
|
* [Debugging](#debugging)
|
|
59
60
|
* [Benchmarks](#benchmarks)
|
|
60
61
|
* [Tangerine Benchmarks](#tangerine-benchmarks)
|
|
@@ -96,7 +97,7 @@ Our team at [Forward Email](https://forwardemail.net) (100% open-source and priv
|
|
|
96
97
|
* Once popular packages such as [native-dns](https://github.com/tjfontaine/node-dns/issues/111) and [dnscached](https://github.com/yahoo/dnscache/issues/28) are archived or deprecated.
|
|
97
98
|
* [Other packages](https://www.npmjs.com/search?q=dns%20cache) only provide `lookup` functions, have a limited sub-set of methods such as [@zeit/dns-cached-resolver](https://github.com/vercel/dns-cached-resolve), or are unmaintained.
|
|
98
99
|
* Act as a 1:1 drop-in replacement for `dns.promises.Resolver` with DNS over HTTPS ("DoH").
|
|
99
|
-
* Support caching for multiple backends
|
|
100
|
+
* Support caching for multiple backends (with TTL support), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
|
|
100
101
|
* Provide out of the box support for both ECMAScript modules (ESM) **and** CommonJS (CJS) (see discussions [for](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) and [against](https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3)).
|
|
101
102
|
* The native Node.js `dns` module does not support caching out of the box – which is a [highly requested feature](https://github.com/nodejs/node/issues/5893) (but belongs in userland).
|
|
102
103
|
* Writing tests against DNS-related infrastructure requires either hacky DNS mocking or a DNS server (manipulating cache is much easier).
|
|
@@ -276,27 +277,86 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
|
|
|
276
277
|
|
|
277
278
|
Similar to the `options` argument from `new dns.promises.Resolver(options)` invocation – :tangerine: Tangerine also has its own options with default `dns` behavior mirrored. See [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight into how these options work.
|
|
278
279
|
|
|
279
|
-
| Property | Type
|
|
280
|
-
| ------------------------- |
|
|
281
|
-
| `timeout` | `Number`
|
|
282
|
-
| `tries` | `Number`
|
|
283
|
-
| `servers` | `Set` or `Array`
|
|
284
|
-
| `requestOptions` | `Object`
|
|
285
|
-
| `requestOptions.method` | `String`
|
|
286
|
-
| `requestOptions.headers` | `Object`
|
|
287
|
-
| `protocol` | `String`
|
|
288
|
-
| `dnsOrder` | `String`
|
|
289
|
-
| `logger` | `Object`
|
|
290
|
-
| `id` | `Number` or `Function`
|
|
291
|
-
| `concurrency` | `Number`
|
|
292
|
-
| `ipv4` | `String`
|
|
293
|
-
| `ipv6` | `String`
|
|
294
|
-
| `ipv4Port` | `Number`
|
|
295
|
-
| `ipv6Port` | `Number`
|
|
296
|
-
| `cache` | `Map` or `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
280
|
+
| Property | Type | Default Value | Description |
|
|
281
|
+
| ------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
282
|
+
| `timeout` | `Number` | `5000` | Number of milliseconds for requests to timeout. |
|
|
283
|
+
| `tries` | `Number` | `4` | Number of tries per `server` in `servers` to attempt. |
|
|
284
|
+
| `servers` | `Set` or `Array` | `new Set(['1.1.1.1', '1.0.0.1'])` | A Set or Array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted addresses for DNS queries (matches default Node.js dns module behavior). Duplicates will be removed as this is converted to a `Set` internally. Defaults to Cloudflare's of `1.1.1.1` and `1.0.0.1`. If an `Array` is passed, then it will be converted to a `Set`. |
|
|
285
|
+
| `requestOptions` | `Object` | Defaults to an Object with `requestOptions.method` and `requestOptions.headers` properties and values below | Default options to pass to [undici](https://github.com/nodejs/undici) (or your custom HTTP library function passed as `request`). |
|
|
286
|
+
| `requestOptions.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`, case-insensitive depending on library you use). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
|
|
287
|
+
| `requestOptions.headers` | `Object` | Defaults to `{ 'content-type': 'application/dns-message', 'user-agent': pkg.name + "/" + pkg.version, accept: 'application/dns-message' }`. | Default HTTP headers to use for DNS over HTTP ("DoH") requests. |
|
|
288
|
+
| `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
|
|
289
|
+
| `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v17.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
|
|
290
|
+
| `logger` | `Object` | `false` | This is the default logger. We recommend using [Cabin](https://github.com/cabinjs) instead of using `console` as your default logger. Set this value to `false` to disable logging entirely (uses noop function). |
|
|
291
|
+
| `id` | `Number` or `Function` | `0` | Default `id` to be passed for DNS packet creation. This could alternatively be a synchronous or asynchronous function that returns a `Number` (e.g. `id: () => Tangerine.getRandomInt(1, 65534)`). |
|
|
292
|
+
| `concurrency` | `Number` | `os.cpus().length` | Default concurrency to use for `resolveAny` lookup via [p-map](https://github.com/sindresorhus/p-map). The default value is the number of CPU's available to the system using the Node.js `os` module [os.cpus()](https://nodejs.org/api/os.html#oscpus) method. |
|
|
293
|
+
| `ipv4` | `String` | `"0.0.0.0"` | Default IPv4 address to use for HTTP agent `localAddress` if DNS `server` was an IPv4 address. |
|
|
294
|
+
| `ipv6` | `String` | `"::0"` | Default IPv6 address to use for HTTP agent `localAddress` if DNS `server` was an IPv6 address. |
|
|
295
|
+
| `ipv4Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv4 address. |
|
|
296
|
+
| `ipv6Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv6 address. |
|
|
297
|
+
| `cache` | `Map`, `Boolean`, or custom cache implementation with `get` and `set` methods | `new Map()` | Set this to `false` in order to disable caching. By default or if you pass `cache: true`, it will use a new `Map` instance for caching. See [Cache](#cache) documentation and the options `defaultTTLSeconds`, `maxTTLSeconds`, and `setCacheArgs` below. |
|
|
298
|
+
| `defaultTTLSeconds` | `Number` (seconds) | `300` | The default number of seconds to use for storing results in cache (defaults to [Cloudflare's recommendation](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/) of 300 seconds – 5 minutes). |
|
|
299
|
+
| `maxTTLSeconds` | `Number` (seconds) | `86400` | The maximum number of seconds to use for storing results in cache (defaults to [Cloudflare's recommendation](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/) of 86,400 seconds – 24 hours – 1 day). |
|
|
300
|
+
| `setCacheArgs` | `Function` | `(key, result) => []` | This is a helper function used for cache store providers such as [ioredis](https://github.com/luin/ioredis) or [lru-cache](https://github.com/isaacs/node-lru-cache) which support more than two arguments to `cache.set()` function. See [Cache](#cache) documentation below for more insight and examples into how this works. You may want to set this to something such as `(key, result) => [ 'PX', Math.round(result.ttl * 1000) ]` if you are using `ioredis`. |
|
|
301
|
+
| `returnHTTPErrors` | `Boolean` | `false` | Whether to return HTTP errors instead of mapping them to corresponding DNS errors. |
|
|
302
|
+
| `smartRotate` | `Boolean` | `true` | Whether to do smart server rotation if servers fail. |
|
|
303
|
+
| `defaultHTTPErrorMessage` | `String` | `"Unsuccessful HTTP response"` | Default fallback message if `statusCode` returned from HTTP request was not found in [http.STATUS_CODES](https://nodejs.org/api/http.html#httpstatus_codes). |
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## Cache
|
|
307
|
+
|
|
308
|
+
:tangerine: Tangerine supports custom cache implementations, such as with [ioredis](https://github.com/luin/ioredis) or any other cache store that has a Map-like implementation with `set(key, value)` and `get(key)` methods. If your cache implementation allows a third argument to `set()`, such as `set(key, value, ttl)` or `set(key, value, { maxAge })`, then you must set the `setCacheArgs` option respectively (see below examples). A third argument with TTL argument support is optional as it is already built-in to :tangerine: Tangerine out of the box (cached results store their TTL and expiration time on the objects themselves – view source code for insight).
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
npm install tangerine undici ioredis
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
// app.js
|
|
316
|
+
|
|
317
|
+
const Redis = require('ioredis');
|
|
318
|
+
const Tangerine = require('tangerine');
|
|
319
|
+
|
|
320
|
+
// <https://github.com/luin/ioredis/issues/1179>
|
|
321
|
+
Redis.Command.setArgumentTransformer('set', (args) => {
|
|
322
|
+
if (typeof args[1] === 'object') args[1] = JSON.stringify(args[1]);
|
|
323
|
+
return args;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
Redis.Command.setReplyTransformer('get', (value) => {
|
|
327
|
+
if (value && typeof value === 'string') {
|
|
328
|
+
try {
|
|
329
|
+
value = JSON.parse(value);
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return value;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const cache = new Redis();
|
|
337
|
+
const tangerine = new Tangerine({
|
|
338
|
+
cache,
|
|
339
|
+
setCacheArgs(key, result) {
|
|
340
|
+
return ['PX', Math.round(result.ttl * 1000)];
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
(async () => {
|
|
345
|
+
console.time('without cache');
|
|
346
|
+
await tangerine.resolve('forwardemail.net'); // <-- cached
|
|
347
|
+
console.timeEnd('without cache');
|
|
348
|
+
|
|
349
|
+
console.time('with cache');
|
|
350
|
+
await tangerine.resolve('forwardemail.net'); // <-- uses cached value
|
|
351
|
+
console.timeEnd('with cache');
|
|
352
|
+
})();
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```sh
|
|
356
|
+
❯ node app
|
|
357
|
+
without cache: 98.25ms
|
|
358
|
+
with cache: 0.091ms
|
|
359
|
+
```
|
|
300
360
|
|
|
301
361
|
|
|
302
362
|
## Debugging
|
|
@@ -333,107 +393,83 @@ BENCHMARK_PROTOCOL="http" BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCH
|
|
|
333
393
|
|
|
334
394
|
### Tangerine Benchmarks
|
|
335
395
|
|
|
336
|
-
We have written extensive benchmarks to show that :tangerine: Tangerine
|
|
337
|
-
|
|
338
|
-
The initial release v1.0.0 had these benchmark results, which [you can publicly view on GitHub CI actions logs](https://github.com/forwardemail/tangerine/actions?query=event%3Apush):
|
|
396
|
+
We have written extensive benchmarks to show that :tangerine: Tangerine is as fast as the native Node.js DNS module (with the exception of the `lookup` command). Note that performance is opinionated – since rate limiting plays a factor dependent on the DNS servers you are using and since caching is most likely going to takeover.
|
|
339
397
|
|
|
340
|
-
|
|
398
|
+
The latest benchmark results are viewable on GitHub under this repository's [GitHub CI actions logs](https://github.com/forwardemail/tangerine/actions?query=event%3Apush):
|
|
341
399
|
|
|
342
|
-
|
|
343
|
-
> node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
|
|
400
|
+
> [Node 16 on ubuntu-latest](https://github.com/forwardemail/tangerine/actions/runs/4297805550/jobs/7491228635#step:6:1)
|
|
344
401
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
402
|
+
```diff
|
|
403
|
+
> node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
|
|
404
|
+
|
|
405
|
+
Started: lookup
|
|
406
|
+
tangerine.lookup POST with caching using Cloudflare x 735 ops/sec ±195.35% (88 runs sampled)
|
|
407
|
+
tangerine.lookup POST without caching using Cloudflare x 142 ops/sec ±0.58% (84 runs sampled)
|
|
408
|
+
tangerine.lookup GET with caching using Cloudflare x 222,397 ops/sec ±0.52% (88 runs sampled)
|
|
409
|
+
+tangerine.lookup GET without caching using Cloudflare x 142 ops/sec ±0.46% (83 runs sampled)
|
|
410
|
+
dns.promises.lookup with caching using Cloudflare x 6,169,417 ops/sec ±1.67% (84 runs sampled)
|
|
411
|
+
-dns.promises.lookup without caching using Cloudflare x 4,186 ops/sec ±0.58% (89 runs sampled)
|
|
351
412
|
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
352
|
-
tangerine.resolve POST with caching using Cloudflare x 734 ops/sec ±190.07% (84 runs sampled)
|
|
353
|
-
tangerine.resolve POST without caching using Cloudflare x 234 ops/sec ±3.75% (82 runs sampled)
|
|
354
|
-
tangerine.resolve GET with caching using Cloudflare x 24,040 ops/sec ±1.93% (83 runs sampled)
|
|
355
|
-
tangerine.resolve GET without caching using Cloudflare x 215 ops/sec ±16.62% (75 runs sampled)
|
|
356
|
-
tangerine.resolve POST with caching using Google x 23,937 ops/sec ±2.04% (81 runs sampled)
|
|
357
|
-
tangerine.resolve POST without caching using Google x 213 ops/sec ±9.51% (71 runs sampled)
|
|
358
|
-
tangerine.resolve GET with caching using Google x 24,272 ops/sec ±1.74% (83 runs sampled)
|
|
359
|
-
tangerine.resolve GET without caching using Google x 257 ops/sec ±4.02% (80 runs sampled)
|
|
360
|
-
resolver.resolve with caching using Cloudflare x 158,842 ops/sec ±2.57% (84 runs sampled)
|
|
361
|
-
resolver.resolve without caching using Cloudflare x 8.02 ops/sec ±191.78% (41 runs sampled)
|
|
362
|
-
Fastest without caching is: tangerine.resolve GET without caching using Google <--------
|
|
363
|
-
tangerine.reverse GET with caching x 694 ops/sec ±189.48% (76 runs sampled)
|
|
364
|
-
tangerine.reverse GET without caching x 123 ops/sec ±90.74% (81 runs sampled)
|
|
365
|
-
resolver.reverse x 0.24 ops/sec ±86.12% (10 runs sampled)
|
|
366
|
-
dns.promises.reverse x 0.70 ops/sec ±164.50% (42 runs sampled)
|
|
367
|
-
Fastest without caching is: tangerine.reverse GET without caching <--------
|
|
368
|
-
http.request POST request x 384 ops/sec ±1.08% (84 runs sampled)
|
|
369
|
-
http.request GET request x 398 ops/sec ±0.83% (83 runs sampled)
|
|
370
|
-
undici GET request x 206 ops/sec ±5.59% (58 runs sampled)
|
|
371
|
-
undici POST request x 211 ops/sec ±4.44% (74 runs sampled)
|
|
372
|
-
axios GET request x 343 ops/sec ±1.97% (82 runs sampled)
|
|
373
|
-
axios POST request x 350 ops/sec ±3.35% (82 runs sampled)
|
|
374
|
-
got GET request x 325 ops/sec ±1.61% (81 runs sampled)
|
|
375
|
-
got POST request x 341 ops/sec ±2.86% (84 runs sampled)
|
|
376
|
-
fetch GET request x 657 ops/sec ±1.42% (82 runs sampled)
|
|
377
|
-
fetch POST request x 680 ops/sec ±1.21% (84 runs sampled)
|
|
378
|
-
request GET request x 370 ops/sec ±1.08% (85 runs sampled)
|
|
379
|
-
request POST request x 370 ops/sec ±0.88% (84 runs sampled)
|
|
380
|
-
superagent GET request x 380 ops/sec ±1.14% (83 runs sampled)
|
|
381
|
-
superagent POST request x 386 ops/sec ±1.04% (83 runs sampled)
|
|
382
|
-
phin GET request x 396 ops/sec ±0.86% (84 runs sampled)
|
|
383
|
-
phin POST request x 398 ops/sec ±0.83% (85 runs sampled)
|
|
384
|
-
Fastest is fetch POST request
|
|
385
|
-
```
|
|
386
413
|
|
|
387
|
-
|
|
414
|
+
Started: resolve
|
|
415
|
+
tangerine.resolve POST with caching using Cloudflare x 951 ops/sec ±195.84% (87 runs sampled)
|
|
416
|
+
tangerine.resolve POST without caching using Cloudflare x 135 ops/sec ±1.27% (79 runs sampled)
|
|
417
|
+
tangerine.resolve GET with caching using Cloudflare x 1,134,724 ops/sec ±0.27% (87 runs sampled)
|
|
418
|
+
+tangerine.resolve GET without caching using Cloudflare x 135 ops/sec ±1.34% (81 runs sampled)
|
|
419
|
+
tangerine.resolve POST with caching using Google x 1,103,189 ops/sec ±0.44% (86 runs sampled)
|
|
420
|
+
tangerine.resolve POST without caching using Google x 55.76 ops/sec ±3.57% (80 runs sampled)
|
|
421
|
+
tangerine.resolve GET with caching using Google x 1,140,499 ops/sec ±0.32% (87 runs sampled)
|
|
422
|
+
tangerine.resolve GET without caching using Google x 70.51 ops/sec ±0.93% (84 runs sampled)
|
|
423
|
+
resolver.resolve with caching using Cloudflare x 4,790,171 ops/sec ±0.43% (87 runs sampled)
|
|
424
|
+
-resolver.resolve without caching using Cloudflare x 158 ops/sec ±1.26% (83 runs sampled)
|
|
425
|
+
Fastest without caching is: resolver.resolve without caching using Cloudflare
|
|
426
|
+
|
|
427
|
+
Started: reverse
|
|
428
|
+
tangerine.reverse GET with caching x 771 ops/sec ±195.37% (85 runs sampled)
|
|
429
|
+
+tangerine.reverse GET without caching x 135 ops/sec ±0.74% (81 runs sampled)
|
|
430
|
+
resolver.reverse with caching x 5,353,130 ops/sec ±0.36% (89 runs sampled)
|
|
431
|
+
-resolver.reverse without caching x 1.90 ops/sec ±210.52% (16 runs sampled)
|
|
432
|
+
dns.promises.reverse with caching x 5,123,900 ops/sec ±0.96% (85 runs sampled)
|
|
433
|
+
-dns.promises.reverse without caching x 0.29 ops/sec ±171.85% (18 runs sampled)
|
|
434
|
+
+Fastest without caching is: tangerine.reverse GET without caching
|
|
435
|
+
```
|
|
388
436
|
|
|
389
|
-
> [Node 18 on ubuntu latest](https://github.com/forwardemail/tangerine/actions/runs/
|
|
437
|
+
> [Node 18 on ubuntu latest](https://github.com/forwardemail/tangerine/actions/runs/4297805550/jobs/7491228742#step:6:1)
|
|
390
438
|
|
|
391
|
-
```
|
|
439
|
+
```diff
|
|
392
440
|
> node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
|
|
393
441
|
|
|
394
|
-
|
|
395
|
-
tangerine.lookup POST
|
|
396
|
-
tangerine.lookup
|
|
397
|
-
tangerine.lookup GET
|
|
398
|
-
|
|
399
|
-
dns.promises.lookup
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
tangerine.resolve POST without caching using Cloudflare x 59.66 ops/sec ±5.69% (74 runs sampled)
|
|
403
|
-
tangerine.resolve GET with caching using Cloudflare x 33,813 ops/sec ±0.33% (90 runs sampled)
|
|
404
|
-
tangerine.resolve GET without caching using Cloudflare x 60.16 ops/sec ±4.03% (73 runs sampled)
|
|
405
|
-
tangerine.resolve POST with caching using Google x 1,184 ops/sec ±189.17% (90 runs sampled)
|
|
406
|
-
tangerine.resolve POST without caching using Google x 41.23 ops/sec ±7.30% (70 runs sampled)
|
|
407
|
-
tangerine.resolve GET with caching using Google x 33,811 ops/sec ±0.56% (91 runs sampled)
|
|
408
|
-
tangerine.resolve GET without caching using Google x 54.34 ops/sec ±5.71% (69 runs sampled)
|
|
409
|
-
resolver.resolve with caching using Cloudflare x 202,804 ops/sec ±0.39% (88 runs sampled)
|
|
410
|
-
resolver.resolve without caching using Cloudflare x 61.93 ops/sec ±5.76% (76 runs sampled)
|
|
411
|
-
Fastest without caching is: resolver.resolve without caching using Cloudflare <--------
|
|
412
|
-
tangerine.reverse GET with caching x 594 ops/sec ±192.60% (86 runs sampled)
|
|
413
|
-
tangerine.reverse GET without caching x 60.73 ops/sec ±3.06% (74 runs sampled)
|
|
414
|
-
resolver.reverse x 66.00 ops/sec ±0.91% (78 runs sampled)
|
|
415
|
-
dns.promises.reverse x 1.84 ops/sec ±190.54% (71 runs sampled)
|
|
416
|
-
Fastest without caching is: tangerine.reverse GET without caching <--------
|
|
417
|
-
http.request POST request x 438 ops/sec ±0.61% (86 runs sampled)
|
|
418
|
-
http.request GET request x 442 ops/sec ±0.64% (87 runs sampled)
|
|
419
|
-
undici GET request x 203 ops/sec ±3.67% (42 runs sampled)
|
|
420
|
-
undici POST request x 194 ops/sec ±3.77% (62 runs sampled)
|
|
421
|
-
axios GET request x 403 ops/sec ±1.67% (86 runs sampled)
|
|
422
|
-
axios POST request x 414 ops/sec ±0.65% (88 runs sampled)
|
|
423
|
-
got GET request x 391 ops/sec ±1.63% (85 runs sampled)
|
|
424
|
-
got POST request x 403 ops/sec ±0.90% (85 runs sampled)
|
|
425
|
-
fetch GET request x 794 ops/sec ±2.32% (84 runs sampled)
|
|
426
|
-
fetch POST request x 821 ops/sec ±0.89% (86 runs sampled)
|
|
427
|
-
request GET request x 423 ops/sec ±0.75% (86 runs sampled)
|
|
428
|
-
request POST request x 426 ops/sec ±0.78% (86 runs sampled)
|
|
429
|
-
superagent GET request x 435 ops/sec ±0.79% (87 runs sampled)
|
|
430
|
-
superagent POST request x 437 ops/sec ±0.82% (88 runs sampled)
|
|
431
|
-
phin GET request x 443 ops/sec ±0.64% (86 runs sampled)
|
|
432
|
-
phin POST request x 445 ops/sec ±0.60% (86 runs sampled)
|
|
433
|
-
Fastest is fetch POST request
|
|
434
|
-
```
|
|
442
|
+
Started: lookup
|
|
443
|
+
tangerine.lookup POST with caching using Cloudflare x 666 ops/sec ±195.48% (87 runs sampled)
|
|
444
|
+
tangerine.lookup POST without caching using Cloudflare x 90.81 ops/sec ±8.06% (89 runs sampled)
|
|
445
|
+
tangerine.lookup GET with caching using Cloudflare x 256,141 ops/sec ±1.72% (87 runs sampled)
|
|
446
|
+
+tangerine.lookup GET without caching using Cloudflare x 96.39 ops/sec ±0.31% (89 runs sampled)
|
|
447
|
+
dns.promises.lookup with caching using Cloudflare x 1,473 ops/sec ±195.95% (87 runs sampled)
|
|
448
|
+
-dns.promises.lookup without caching using Cloudflare x 4,191 ops/sec ±0.54% (85 runs sampled)
|
|
449
|
+
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
435
450
|
|
|
436
|
-
|
|
451
|
+
Started: resolve
|
|
452
|
+
tangerine.resolve POST with caching using Cloudflare x 683 ops/sec ±195.88% (87 runs sampled)
|
|
453
|
+
tangerine.resolve POST without caching using Cloudflare x 93.37 ops/sec ±0.48% (87 runs sampled)
|
|
454
|
+
tangerine.resolve GET with caching using Cloudflare x 1,146,727 ops/sec ±0.58% (88 runs sampled)
|
|
455
|
+
+tangerine.resolve GET without caching using Cloudflare x 93.33 ops/sec ±0.51% (87 runs sampled)
|
|
456
|
+
tangerine.resolve POST with caching using Google x 1,133,683 ops/sec ±2.74% (89 runs sampled)
|
|
457
|
+
tangerine.resolve POST without caching using Google x 83.91 ops/sec ±6.32% (76 runs sampled)
|
|
458
|
+
tangerine.resolve GET with caching using Google x 1,147,212 ops/sec ±0.32% (90 runs sampled)
|
|
459
|
+
tangerine.resolve GET without caching using Google x 79.73 ops/sec ±4.02% (77 runs sampled)
|
|
460
|
+
resolver.resolve with caching using Cloudflare x 5,318,406 ops/sec ±0.67% (86 runs sampled)
|
|
461
|
+
-resolver.resolve without caching using Cloudflare x 100 ops/sec ±1.55% (79 runs sampled)
|
|
462
|
+
Fastest without caching is: resolver.resolve without caching using Cloudflare
|
|
463
|
+
|
|
464
|
+
Started: reverse
|
|
465
|
+
tangerine.reverse GET with caching x 722 ops/sec ±195.42% (88 runs sampled)
|
|
466
|
+
+tangerine.reverse GET without caching x 93.19 ops/sec ±0.74% (87 runs sampled)
|
|
467
|
+
resolver.reverse with caching x 5,520,569 ops/sec ±0.59% (85 runs sampled)
|
|
468
|
+
-resolver.reverse without caching x 17.42 ops/sec ±162.63% (70 runs sampled)
|
|
469
|
+
dns.promises.reverse with caching x 5,164,258 ops/sec ±0.96% (86 runs sampled)
|
|
470
|
+
-dns.promises.reverse without caching x 0.20 ops/sec ±184.87% (25 runs sampled)
|
|
471
|
+
+Fastest without caching is: tangerine.reverse GET without caching
|
|
472
|
+
```
|
|
437
473
|
|
|
438
474
|
---
|
|
439
475
|
|
|
@@ -443,84 +479,84 @@ You can also [run the benchmarks yourself](#benchmarks).
|
|
|
443
479
|
|
|
444
480
|
Provided below are additional benchmark tests we have run:
|
|
445
481
|
|
|
446
|
-
> Node
|
|
482
|
+
> Node v18.14.2 on MacBook Air M1 16GB (without VPN):
|
|
447
483
|
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
❯ node benchmarks/resolve
|
|
453
|
-
tangerine POST with caching using Cloudflare x 1,044 ops/sec ±193.21% (90 runs sampled)
|
|
454
|
-
tangerine POST without caching using Cloudflare x 40.93 ops/sec ±53.83% (50 runs sampled)
|
|
455
|
-
tangerine GET with caching using Cloudflare x 73,896 ops/sec ±0.27% (90 runs sampled)
|
|
456
|
-
tangerine GET without caching using Cloudflare x 38.66 ops/sec ±21.98% (55 runs sampled)
|
|
457
|
-
tangerine POST with caching using Google x 992 ops/sec ±193.33% (87 runs sampled)
|
|
458
|
-
tangerine POST without caching using Google x 31.98 ops/sec ±21.35% (58 runs sampled)
|
|
459
|
-
tangerine GET with caching using Google x 74,410 ops/sec ±0.22% (91 runs sampled)
|
|
460
|
-
tangerine GET without caching using Google x 41.52 ops/sec ±18.91% (56 runs sampled)
|
|
461
|
-
dns.promises.resolve without caching using Cloudflare x 25.46 ops/sec ±100.19% (50 runs sampled)
|
|
462
|
-
dns.promises.resolve with caching using Cloudflare x 505,956 ops/sec ±2.34% (89 runs sampled)
|
|
463
|
-
Fastest without caching is: tangerine GET without caching using Google, tangerine GET without caching using Cloudflare
|
|
464
|
-
```
|
|
484
|
+
```diff
|
|
485
|
+
> node --version
|
|
486
|
+
v18.14.2
|
|
465
487
|
|
|
466
|
-
>
|
|
488
|
+
> node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
|
|
467
489
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
tangerine POST
|
|
479
|
-
tangerine
|
|
480
|
-
tangerine GET
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
490
|
+
Started: lookup
|
|
491
|
+
tangerine.lookup POST with caching using Cloudflare x 1,035 ops/sec ±195.73% (91 runs sampled)
|
|
492
|
+
tangerine.lookup POST without caching using Cloudflare x 52.76 ops/sec ±51.29% (53 runs sampled)
|
|
493
|
+
tangerine.lookup GET with caching using Cloudflare x 694,910 ops/sec ±1.54% (87 runs sampled)
|
|
494
|
+
+tangerine.lookup GET without caching using Cloudflare x 40.18 ops/sec ±60.19% (49 runs sampled)
|
|
495
|
+
dns.promises.lookup with caching using Cloudflare x 12,645,103 ops/sec ±0.26% (90 runs sampled)
|
|
496
|
+
-dns.promises.lookup without caching using Cloudflare x 2,664 ops/sec ±0.54% (88 runs sampled)
|
|
497
|
+
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
498
|
+
|
|
499
|
+
Started: resolve
|
|
500
|
+
tangerine.resolve POST with caching using Cloudflare x 1,005 ops/sec ±195.93% (91 runs sampled)
|
|
501
|
+
tangerine.resolve POST without caching using Cloudflare x 55.52 ops/sec ±46.26% (57 runs sampled)
|
|
502
|
+
tangerine.resolve GET with caching using Cloudflare x 2,879,865 ops/sec ±0.35% (86 runs sampled)
|
|
503
|
+
+tangerine.resolve GET without caching using Cloudflare x 71.11 ops/sec ±2.94% (74 runs sampled)
|
|
504
|
+
tangerine.resolve POST with caching using Google x 1,292 ops/sec ±195.91% (88 runs sampled)
|
|
505
|
+
tangerine.resolve POST without caching using Google x 36.88 ops/sec ±41.76% (53 runs sampled)
|
|
506
|
+
tangerine.resolve GET with caching using Google x 2,885,428 ops/sec ±0.22% (88 runs sampled)
|
|
507
|
+
tangerine.resolve GET without caching using Google x 70.38 ops/sec ±3.72% (68 runs sampled)
|
|
508
|
+
resolver.resolve with caching using Cloudflare x 10,645,813 ops/sec ±0.23% (91 runs sampled)
|
|
509
|
+
-resolver.resolve without caching using Cloudflare x 71.80 ops/sec ±2.84% (67 runs sampled)
|
|
510
|
+
+Fastest without caching is: resolver.resolve without caching using Cloudflare, tangerine.resolve GET without caching using Cloudflare, tangerine.resolve GET without caching using Google, tangerine.resolve POST without caching using Cloudflare
|
|
511
|
+
|
|
512
|
+
Started: reverse
|
|
513
|
+
tangerine.reverse GET with caching x 917 ops/sec ±195.78% (88 runs sampled)
|
|
514
|
+
+tangerine.reverse GET without caching x 51.15 ops/sec ±51.92% (61 runs sampled)
|
|
515
|
+
resolver.reverse with caching x 11,058,579 ops/sec ±0.37% (88 runs sampled)
|
|
516
|
+
-resolver.reverse without caching x 62.30 ops/sec ±24.83% (64 runs sampled)
|
|
517
|
+
dns.promises.reverse with caching x 11,276,123 ops/sec ±0.17% (90 runs sampled)
|
|
518
|
+
-dns.promises.reverse without caching x 73.46 ops/sec ±1.99% (69 runs sampled)
|
|
519
|
+
Fastest without caching is: dns.promises.reverse without caching, resolver.reverse without caching
|
|
484
520
|
```
|
|
485
521
|
|
|
486
|
-
> Node v18.
|
|
522
|
+
> Node v18.14.2 on MacBook Air M1 16GB (with DNS blackholed VPN) – **this highlights the DNS blackhole problem**:
|
|
487
523
|
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
v18.
|
|
491
|
-
|
|
492
|
-
❯ node benchmarks/resolve
|
|
493
|
-
tangerine POST with caching using Cloudflare x 817 ops/sec ±193.86% (89 runs sampled)
|
|
494
|
-
tangerine POST without caching using Cloudflare x 42.57 ops/sec ±38.18% (62 runs sampled)
|
|
495
|
-
tangerine GET with caching using Cloudflare x 853 ops/sec ±193.79% (91 runs sampled)
|
|
496
|
-
tangerine GET without caching using Cloudflare x 41.13 ops/sec ±57.37% (48 runs sampled)
|
|
497
|
-
tangerine POST with caching using Google x 1,488 ops/sec ±192.10% (90 runs sampled)
|
|
498
|
-
tangerine POST without caching using Google x 38.46 ops/sec ±12.08% (59 runs sampled)
|
|
499
|
-
tangerine GET with caching using Google x 74,240 ops/sec ±0.31% (90 runs sampled)
|
|
500
|
-
tangerine GET without caching using Google x 39.20 ops/sec ±23.52% (58 runs sampled)
|
|
501
|
-
dns.promises.resolve without caching using Cloudflare x 59.11 ops/sec ±13.96% (63 runs sampled)
|
|
502
|
-
dns.promises.resolve with caching using Cloudflare x 529,961 ops/sec ±0.33% (91 runs sampled)
|
|
503
|
-
Fastest without caching is: dns.promises.resolve without caching using Cloudflare, tangerine GET without caching using Cloudflare
|
|
504
|
-
```
|
|
524
|
+
```diff
|
|
525
|
+
> node --version
|
|
526
|
+
v18.14.2
|
|
505
527
|
|
|
506
|
-
>
|
|
528
|
+
> node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
|
|
507
529
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
tangerine POST
|
|
519
|
-
tangerine
|
|
520
|
-
tangerine GET
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
530
|
+
Started: lookup
|
|
531
|
+
tangerine.lookup POST with caching using Cloudflare x 1,327 ops/sec ±195.65% (89 runs sampled)
|
|
532
|
+
tangerine.lookup POST without caching using Cloudflare x 71.11 ops/sec ±8.24% (71 runs sampled)
|
|
533
|
+
tangerine.lookup GET with caching using Cloudflare x 759,816 ops/sec ±0.46% (90 runs sampled)
|
|
534
|
+
+tangerine.lookup GET without caching using Cloudflare x 73.98 ops/sec ±1.78% (69 runs sampled)
|
|
535
|
+
dns.promises.lookup with caching using Cloudflare x 1,744 ops/sec ±195.97% (88 runs sampled)
|
|
536
|
+
-dns.promises.lookup without caching using Cloudflare x 2,717 ops/sec ±0.82% (87 runs sampled)
|
|
537
|
+
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
538
|
+
|
|
539
|
+
Started: resolve
|
|
540
|
+
tangerine.resolve POST with caching using Cloudflare x 947 ops/sec ±195.93% (91 runs sampled)
|
|
541
|
+
tangerine.resolve POST without caching using Cloudflare x 44.33 ops/sec ±73.30% (75 runs sampled)
|
|
542
|
+
tangerine.resolve GET with caching using Cloudflare x 2,814,737 ops/sec ±0.17% (91 runs sampled)
|
|
543
|
+
+tangerine.resolve GET without caching using Cloudflare x 57.25 ops/sec ±51.61% (73 runs sampled)
|
|
544
|
+
tangerine.resolve POST with caching using Google x 1,087 ops/sec ±195.92% (91 runs sampled)
|
|
545
|
+
tangerine.resolve POST without caching using Google x 36.84 ops/sec ±7.04% (62 runs sampled)
|
|
546
|
+
tangerine.resolve GET with caching using Google x 2,784,199 ops/sec ±0.15% (92 runs sampled)
|
|
547
|
+
tangerine.resolve GET without caching using Google x 47.55 ops/sec ±5.66% (76 runs sampled)
|
|
548
|
+
resolver.resolve with caching using Cloudflare x 0.09 ops/sec ±6.41% (5 runs sampled)
|
|
549
|
+
-resolver.resolve without caching using Cloudflare x 0.10 ops/sec ±6.52% (5 runs sampled)
|
|
550
|
+
+Fastest without caching is: tangerine.resolve GET without caching using Google
|
|
551
|
+
|
|
552
|
+
Started: reverse
|
|
553
|
+
tangerine.reverse GET with caching x 1,345 ops/sec ±195.66% (92 runs sampled)
|
|
554
|
+
+tangerine.reverse GET without caching x 71.73 ops/sec ±3.03% (73 runs sampled)
|
|
555
|
+
resolver.reverse with caching x 0.10 ops/sec ±6.54% (5 runs sampled)
|
|
556
|
+
-resolver.reverse without caching x 0.10 ops/sec ±0.01% (5 runs sampled)
|
|
557
|
+
dns.promises.reverse with caching x 0.10 ops/sec ±6.54% (5 runs sampled)
|
|
558
|
+
-dns.promises.reverse without caching x 0.10 ops/sec ±0.01% (5 runs sampled)
|
|
559
|
+
+Fastest without caching is: tangerine.reverse GET without caching
|
|
524
560
|
```
|
|
525
561
|
|
|
526
562
|
Also see this [write-up](https://samknows.com/blog/dns-over-https-performance) on UDP-based DNS versus DNS over HTTPS ("DoH") benchmarks.
|
|
@@ -531,39 +567,13 @@ Also see this [write-up](https://samknows.com/blog/dns-over-https-performance) o
|
|
|
531
567
|
|
|
532
568
|
Originally we wrote this library using [got](https://github.com/sindresorhus/got) – however after running benchmarks and learning of [how performant](https://github.com/sindresorhus/got/issues/1419) undici is, we weren't happy – and we rewrote it with [undici](https://github.com/nodejs/undici). Here are test results from the latest versions of all HTTP libraries against our real-world API (both client and server running locally):
|
|
533
569
|
|
|
534
|
-
> Node v16.18.1 on MacBook Air M1 16GB (using real-world API server):
|
|
535
|
-
|
|
536
|
-
```sh
|
|
537
|
-
❯ node --version
|
|
538
|
-
v16.18.1
|
|
539
|
-
|
|
540
|
-
❯ BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
|
|
541
|
-
http.request POST request x 860 ops/sec ±6.33% (75 runs sampled)
|
|
542
|
-
http.request GET request x 978 ops/sec ±5.17% (83 runs sampled)
|
|
543
|
-
undici GET request x 2,732 ops/sec ±4.14% (83 runs sampled)
|
|
544
|
-
undici POST request x 1,204 ops/sec ±5.01% (81 runs sampled)
|
|
545
|
-
axios GET request x 855 ops/sec ±5.45% (81 runs sampled)
|
|
546
|
-
axios POST request x 723 ops/sec ±15.28% (71 runs sampled)
|
|
547
|
-
got GET request x 1,355 ops/sec ±16.60% (63 runs sampled)
|
|
548
|
-
got POST request x 93.65 ops/sec ±181.51% (29 runs sampled)
|
|
549
|
-
fetch GET request x 949 ops/sec ±40.26% (45 runs sampled)
|
|
550
|
-
fetch POST request x 672 ops/sec ±22.43% (67 runs sampled)
|
|
551
|
-
request GET request x 960 ops/sec ±50.90% (48 runs sampled)
|
|
552
|
-
request POST request x 612 ops/sec ±45.48% (57 runs sampled)
|
|
553
|
-
superagent GET request x 126 ops/sec ±188.34% (29 runs sampled)
|
|
554
|
-
superagent POST request x 747 ops/sec ±18.16% (67 runs sampled)
|
|
555
|
-
phin GET request x 374 ops/sec ±147.42% (57 runs sampled)
|
|
556
|
-
phin POST request x 566 ops/sec ±38.08% (51 runs sampled)
|
|
557
|
-
Fastest is undici GET request
|
|
558
|
-
```
|
|
559
|
-
|
|
560
570
|
> Node v18.14.2 on MacBook Air M1 16GB (using real-world API server):
|
|
561
571
|
|
|
562
572
|
```sh
|
|
563
|
-
|
|
573
|
+
> node --version
|
|
564
574
|
v18.14.2
|
|
565
575
|
|
|
566
|
-
|
|
576
|
+
> BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
|
|
567
577
|
http.request POST request x 765 ops/sec ±9.83% (72 runs sampled)
|
|
568
578
|
http.request GET request x 1,000 ops/sec ±3.88% (85 runs sampled)
|
|
569
579
|
undici GET request x 2,740 ops/sec ±5.92% (78 runs sampled)
|
package/index.js
CHANGED
|
@@ -7,7 +7,6 @@ const { debuglog } = require('node:util');
|
|
|
7
7
|
const { getEventListeners, setMaxListeners } = require('node:events');
|
|
8
8
|
const { isIP, isIPv4, isIPv6 } = require('node:net');
|
|
9
9
|
|
|
10
|
-
const Keyv = require('keyv');
|
|
11
10
|
const getStream = require('get-stream');
|
|
12
11
|
const ipaddr = require('ipaddr.js');
|
|
13
12
|
const mergeOptions = require('merge-options');
|
|
@@ -16,6 +15,7 @@ const pTimeout = require('p-timeout');
|
|
|
16
15
|
const pWaitFor = require('p-wait-for');
|
|
17
16
|
const packet = require('dns-packet');
|
|
18
17
|
const semver = require('semver');
|
|
18
|
+
const structuredClone = require('@ungap/structured-clone').default;
|
|
19
19
|
const { getService } = require('port-numbers');
|
|
20
20
|
|
|
21
21
|
const pkg = require('./package.json');
|
|
@@ -287,8 +287,19 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
287
287
|
ipv6: '::0',
|
|
288
288
|
ipv4Port: undefined,
|
|
289
289
|
ipv6Port: undefined,
|
|
290
|
-
// cache mapping (e.g. txt -> keyv instance) - see below
|
|
290
|
+
// cache mapping (e.g. txt -> Map/keyv/redis instance) - see below
|
|
291
291
|
cache: new Map(),
|
|
292
|
+
// <https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/>
|
|
293
|
+
defaultTTLSeconds: 300,
|
|
294
|
+
maxTTLSeconds: 86400,
|
|
295
|
+
// default is to support ioredis
|
|
296
|
+
// setCacheArgs(key, result) {
|
|
297
|
+
setCacheArgs() {
|
|
298
|
+
// also you have access to `result.expires` which is is ms since epoch
|
|
299
|
+
// (can be converted to Date via `new Date(result.expires)`)
|
|
300
|
+
// return ['PX', Math.round(result.ttl * 1000)];
|
|
301
|
+
return [];
|
|
302
|
+
},
|
|
292
303
|
// whether to do 1:1 HTTP -> DNS error mapping
|
|
293
304
|
returnHTTPErrors: false,
|
|
294
305
|
// whether to smart rotate and bump-to-end servers that have issues
|
|
@@ -337,19 +348,6 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
337
348
|
// so to turn that off, you need to supply `dnsCache: undefined` in `got` object (?)
|
|
338
349
|
if (this.options.cache === true) this.options.cache = new Map();
|
|
339
350
|
|
|
340
|
-
if (this.options.cache instanceof Map) {
|
|
341
|
-
// each of the types have their own Keyv with prefix
|
|
342
|
-
for (const type of this.constructor.TYPES) {
|
|
343
|
-
if (!this.options.cache.get(type))
|
|
344
|
-
this.options.cache.set(
|
|
345
|
-
type,
|
|
346
|
-
new Keyv({
|
|
347
|
-
namespace: `dns:${type.toLowerCase()}`
|
|
348
|
-
})
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
351
|
// convert `false` logger option into noop
|
|
354
352
|
// <https://github.com/breejs/bree/issues/147>
|
|
355
353
|
if (this.options.logger === false)
|
|
@@ -1086,7 +1084,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1086
1084
|
}
|
|
1087
1085
|
|
|
1088
1086
|
//
|
|
1089
|
-
//
|
|
1087
|
+
// NOTE: every address must be ipv4 or ipv6 (use `new URL` to parse and check)
|
|
1090
1088
|
// servers [ string ] - array of RFC 5952 formatted addresses
|
|
1091
1089
|
//
|
|
1092
1090
|
|
|
@@ -1123,51 +1121,59 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1123
1121
|
delete options.ecsSubnet;
|
|
1124
1122
|
}
|
|
1125
1123
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const key = ecsSubnet ? `${ecsSubnet}:${name}` : name;
|
|
1124
|
+
const key = (
|
|
1125
|
+
ecsSubnet ? `${rrtype}:${ecsSubnet}:${name}` : `${rrtype}:${name}`
|
|
1126
|
+
).toLowerCase();
|
|
1131
1127
|
|
|
1132
1128
|
let result;
|
|
1133
1129
|
let data;
|
|
1134
|
-
if (cache) {
|
|
1135
|
-
//
|
|
1136
|
-
// <https://github.com/jaredwray/keyv/issues/106>
|
|
1130
|
+
if (this.options.cache) {
|
|
1137
1131
|
//
|
|
1138
|
-
// NOTE: we store `result.
|
|
1132
|
+
// NOTE: we store `result.ttl` which was the lowest TTL determined
|
|
1139
1133
|
// (this saves us from duplicating the same `...sort().filter(Number.isFinite)` logic)
|
|
1140
1134
|
//
|
|
1141
|
-
data = await cache.get(key
|
|
1142
|
-
|
|
1143
|
-
|
|
1135
|
+
data = await this.options.cache.get(key);
|
|
1136
|
+
// safeguard in case cache pollution
|
|
1137
|
+
if (data && typeof data === 'object') {
|
|
1138
|
+
debug('cache retrieved', data);
|
|
1144
1139
|
const now = Date.now();
|
|
1140
|
+
// safeguard in case cache pollution
|
|
1145
1141
|
if (
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
data.
|
|
1150
|
-
now <= data.expires
|
|
1142
|
+
!Number.isFinite(data.expires) ||
|
|
1143
|
+
data.expires < now ||
|
|
1144
|
+
!Number.isFinite(data.ttl) ||
|
|
1145
|
+
data.ttl < 1
|
|
1151
1146
|
) {
|
|
1147
|
+
data = undefined;
|
|
1148
|
+
} else if (options?.ttl) {
|
|
1149
|
+
// clone the data so that we don't mutate cache (e.g. if it's in-memory)
|
|
1150
|
+
// <https://nodejs.org/api/globals.html#structuredclonevalue-options>
|
|
1151
|
+
// <https://github.com/ungap/structured-clone>
|
|
1152
|
+
data = structuredClone(data);
|
|
1153
|
+
|
|
1152
1154
|
// returns ms -> s conversion
|
|
1153
1155
|
const ttl = Math.round((data.expires - now) / 1000);
|
|
1154
|
-
const diff =
|
|
1156
|
+
const diff = data.ttl - ttl;
|
|
1155
1157
|
|
|
1156
|
-
for (let i = 0; i <
|
|
1158
|
+
for (let i = 0; i < data.answers.length; i++) {
|
|
1157
1159
|
// eslint-disable-next-line max-depth
|
|
1158
|
-
if (typeof
|
|
1160
|
+
if (typeof data.answers[i].ttl === 'number') {
|
|
1159
1161
|
// subtract ttl from answer
|
|
1160
|
-
|
|
1162
|
+
data.answers[i].ttl = Math.round(data.answers[i].ttl - diff);
|
|
1161
1163
|
|
|
1162
1164
|
// eslint-disable-next-line max-depth
|
|
1163
|
-
if (
|
|
1164
|
-
result = undefined;
|
|
1165
|
+
if (data.answers[i].ttl <= 0) {
|
|
1165
1166
|
data = undefined;
|
|
1166
1167
|
break;
|
|
1167
1168
|
}
|
|
1168
1169
|
}
|
|
1169
1170
|
}
|
|
1170
1171
|
}
|
|
1172
|
+
|
|
1173
|
+
// will only use cache if it's still set after parsing ttl
|
|
1174
|
+
result = data;
|
|
1175
|
+
} else {
|
|
1176
|
+
data = undefined;
|
|
1171
1177
|
}
|
|
1172
1178
|
}
|
|
1173
1179
|
|
|
@@ -1200,8 +1206,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1200
1206
|
// - The DoH service could not contact Google Public DNS resolvers.
|
|
1201
1207
|
// - In the case of a 502 response, although retrying on an alternate Google Public DNS address might help, a more effective fallback response would be to try another DoH service, or to switch to traditional UDP or TCP DNS at 8.8.8.8.
|
|
1202
1208
|
//
|
|
1203
|
-
if (cache && result) {
|
|
1204
|
-
debug(`cached result found for "${
|
|
1209
|
+
if (this.options.cache && result) {
|
|
1210
|
+
debug(`cached result found for "${key}"`);
|
|
1205
1211
|
} else {
|
|
1206
1212
|
if (!abortController) {
|
|
1207
1213
|
abortController = new AbortController();
|
|
@@ -1241,31 +1247,37 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1241
1247
|
//
|
|
1242
1248
|
switch (result.rcode) {
|
|
1243
1249
|
case 'NOERROR': {
|
|
1244
|
-
//
|
|
1245
|
-
// NOTE: if the answer was truncated then unset results (?)
|
|
1246
|
-
// <https://github.com/EduardoRuizM/native-dnssec-dns/blob/fc27face6c64ab53675840bafc81f70bab48a743/lib/client.js#L354>
|
|
1247
|
-
// <https://github.com/hildjj/dohdec/issues/40>
|
|
1248
|
-
// if (result.flag_tc) throw createError(name, rrtype, dns.BADRESP);
|
|
1250
|
+
// <https://github.com/hildjj/dohdec/issues/40#issuecomment-1445554626>
|
|
1249
1251
|
if (result.flag_tc) {
|
|
1250
|
-
this.options.logger.error(
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1252
|
+
this.options.logger.error(
|
|
1253
|
+
new Error(
|
|
1254
|
+
'Truncated DNS response; Defer to https://github.com/hildjj/dohdec/issues/40#issuecomment-1445554626 for insight.'
|
|
1255
|
+
),
|
|
1256
|
+
{
|
|
1257
|
+
name,
|
|
1258
|
+
rrtype,
|
|
1259
|
+
result
|
|
1260
|
+
}
|
|
1261
|
+
);
|
|
1262
|
+
} else if (this.options.cache && !data) {
|
|
1256
1263
|
// store in cache based off lowest ttl
|
|
1257
|
-
|
|
1264
|
+
let ttl = result.answers
|
|
1258
1265
|
.map((answer) => answer.ttl)
|
|
1259
1266
|
.sort()
|
|
1260
1267
|
.find((ttl) => Number.isFinite(ttl));
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1268
|
+
// if TTL is not a number or is < 1 or is > max then set to default
|
|
1269
|
+
if (
|
|
1270
|
+
!Number.isFinite(ttl) ||
|
|
1271
|
+
ttl < 1 ||
|
|
1272
|
+
ttl > this.options.maxTTLSeconds
|
|
1273
|
+
)
|
|
1274
|
+
ttl = this.options.defaultTTLSeconds;
|
|
1275
|
+
result.ttl = ttl;
|
|
1276
|
+
// this supports both redis-based key/value/ttl and simple key/value implementations
|
|
1277
|
+
result.expires = Date.now() + Math.round(result.ttl * 1000);
|
|
1278
|
+
const args = [key, result, ...this.options.setCacheArgs(key, result)];
|
|
1279
|
+
debug('setting cache', [key, result, ...args]);
|
|
1280
|
+
await this.options.cache.set(...args);
|
|
1269
1281
|
}
|
|
1270
1282
|
|
|
1271
1283
|
break;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tangerine",
|
|
3
|
-
"description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends
|
|
4
|
-
"version": "1.
|
|
3
|
+
"description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends (with TTL support).",
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"author": "Forward Email (https://forwardemail.net)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/forwardemail/tangerine/issues"
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"Forward Email (https://forwardemail.net)"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@ungap/structured-clone": "^1.0.2",
|
|
13
14
|
"dns-packet": "^5.4.0",
|
|
14
15
|
"dohdec": "^5.0.3",
|
|
15
16
|
"get-stream": "6",
|
|
16
17
|
"ipaddr.js": "^2.0.1",
|
|
17
|
-
"keyv": "^4.5.2",
|
|
18
18
|
"merge-options": "3.0.4",
|
|
19
19
|
"p-map": "4",
|
|
20
20
|
"p-timeout": "4",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"fixpack": "^4.0.0",
|
|
36
36
|
"got": "11",
|
|
37
37
|
"husky": "^8.0.3",
|
|
38
|
+
"ioredis": "^5.3.1",
|
|
39
|
+
"ioredis-mock": "^8.2.6",
|
|
38
40
|
"lint-staged": "^13.1.2",
|
|
39
41
|
"lodash": "^4.17.21",
|
|
40
42
|
"nock": "^13.3.0",
|
|
@@ -150,7 +152,7 @@
|
|
|
150
152
|
},
|
|
151
153
|
"scripts": {
|
|
152
154
|
"ava": "cross-env NODE_ENV=test ava",
|
|
153
|
-
"benchmarks": "node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
|
|
155
|
+
"benchmarks": "node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse",
|
|
154
156
|
"lint": "xo --fix && remark . -qfo && fixpack",
|
|
155
157
|
"nyc": "cross-env NODE_ENV=test nyc ava",
|
|
156
158
|
"prepare": "husky install",
|