tangerine 0.0.1 → 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +576 -0
  3. package/index.js +1354 -0
  4. package/package.json +147 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Forward Email (https://forwardemail.net)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,576 @@
1
+ <h1 align="center">
2
+ <a href="https://github.com/forwardemail/tangerine"><img src="https://raw.githubusercontent.com/forwardemail/tangerine/main/media/header.png" alt="Tangerine" /></a>
3
+ </h1>
4
+ <div align="center">
5
+ <a href="https://github.com/forwardemail/tangerine/actions/workflows/ci.yml"><img src="https://github.com/forwardemail/tangerine/actions/workflows/ci.yml/badge.svg" alt="build status" /></a>
6
+ <a href="https://github.com/sindresorhus/xo"><img src="https://img.shields.io/badge/code_style-XO-5ed9c7.svg" alt="code style" /></a>
7
+ <a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/styled_with-prettier-ff69b4.svg" alt="styled with prettier" /></a>
8
+ <a href="https://lass.js.org"><img src="https://img.shields.io/badge/made_with-lass-95CC28.svg" alt="made with lass" /></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/forwardemail/tangerine.svg" alt="license" /></a>
10
+ <a href="https://npm.im/tangerine"><img src="https://img.shields.io/npm/dt/tangerine.svg" alt="npm downloads" /></a>
11
+ </div>
12
+ <br />
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 via <a href="https://github.com/jaredwray/keyv" target="_blank">Keyv</a>.
15
+ </div>
16
+ <hr />
17
+ <div align="center">
18
+ ⚡ <mark><a href="#tangerine-benchmarks"><i><u><strong>FASTER</strong></u></i></a></mark> than <a href="https://nodejs.org/api/dns.html" target="_blank">Node.js <code>dns</code></a>! 🚀 &bull; Supports Node v16+ with ESM/CJS &bull; Made for <a href="https://forwardemail.net" target="_blank"><strong>Forward Email</strong></a>.
19
+ </div>
20
+ <hr />
21
+
22
+
23
+ ## Table of Contents
24
+
25
+ * [Install](#install)
26
+ * [Foreword](#foreword)
27
+ * [What is this project about](#what-is-this-project-about)
28
+ * [Why integrate DNS over HTTPS](#why-integrate-dns-over-https)
29
+ * [What does this mean](#what-does-this-mean)
30
+ * [What projects were used for inspiration](#what-projects-were-used-for-inspiration)
31
+ * [Features](#features)
32
+ * [Usage and Examples](#usage-and-examples)
33
+ * [ECMAScript modules (ESM)](#ecmascript-modules-esm)
34
+ * [CommonJS (CJS)](#commonjs-cjs)
35
+ * [API](#api)
36
+ * [`new Tangerine(options)`](#new-tangerineoptions)
37
+ * [`tangerine.cancel()`](#tangerinecancel)
38
+ * [`tangerine.getServers()`](#tangerinegetservers)
39
+ * [`tangerine.lookup(hostname[, options])`](#tangerinelookuphostname-options)
40
+ * [`tangerine.lookupService(address, port, abortController)`](#tangerinelookupserviceaddress-port-abortcontroller)
41
+ * [`tangerine.resolve(hostname[, rrtype, options, abortController])`](#tangerineresolvehostname-rrtype-options-abortcontroller)
42
+ * [`tangerine.resolve4(hostname[, options, abortController])`](#tangerineresolve4hostname-options-abortcontroller)
43
+ * [`tangerine.resolve6(hostname[, options, abortController])`](#tangerineresolve6hostname-options-abortcontroller)
44
+ * [`tangerine.resolveAny(hostname[, abortController])`](#tangerineresolveanyhostname-abortcontroller)
45
+ * [`tangerine.resolveCaa(hostname[, abortController]))`](#tangerineresolvecaahostname-abortcontroller)
46
+ * [`tangerine.resolveCname(hostname[, abortController]))`](#tangerineresolvecnamehostname-abortcontroller)
47
+ * [`tangerine.resolveMx(hostname[, abortController]))`](#tangerineresolvemxhostname-abortcontroller)
48
+ * [`tangerine.resolveNaptr(hostname[, abortController]))`](#tangerineresolvenaptrhostname-abortcontroller)
49
+ * [`tangerine.resolveNs(hostname[, abortController]))`](#tangerineresolvenshostname-abortcontroller)
50
+ * [`tangerine.resolvePtr(hostname[, abortController]))`](#tangerineresolveptrhostname-abortcontroller)
51
+ * [`tangerine.resolveSoa(hostname[, abortController]))`](#tangerineresolvesoahostname-abortcontroller)
52
+ * [`tangerine.resolveSrv(hostname[, abortController]))`](#tangerineresolvesrvhostname-abortcontroller)
53
+ * [`tangerine.resolveTxt(hostname[, abortController]))`](#tangerineresolvetxthostname-abortcontroller)
54
+ * [`tangerine.reverse(ip[, abortController])`](#tangerinereverseip-abortcontroller)
55
+ * [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
56
+ * [`tangerine.setServers(servers)`](#tangerinesetserversservers)
57
+ * [Options](#options)
58
+ * [Debugging](#debugging)
59
+ * [Benchmarks](#benchmarks)
60
+ * [Tangerine Benchmarks](#tangerine-benchmarks)
61
+ * [HTTP Library Benchmarks](#http-library-benchmarks)
62
+ * [Contributors](#contributors)
63
+ * [License](#license)
64
+
65
+
66
+ ## Install
67
+
68
+ ```sh
69
+ npm install tangerine
70
+ ```
71
+
72
+ ```diff
73
+ -import dns from 'dns';
74
+ +import Tangerine from 'tangerine';
75
+
76
+ - const resolver = new dns.promises.Resolver();
77
+ +const resolver = new Tangerine();
78
+ ```
79
+
80
+
81
+ ## Foreword
82
+
83
+ ### What is this project about
84
+
85
+ Our team at [Forward Email](https://forwardemail.net) (100% open-source and privacy-focused email service) needed a better solution for DNS.
86
+
87
+ <details>
88
+ <summary>After years of using the Node.js internal DNS module, we ran into these recurring patterns:</summary>
89
+
90
+ * [Cloudflare](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/) and [Google](https://developers.google.com/speed/public-dns/docs/doh/) now have DNS over HTTPS servers ("DoH") available – and browsers such as Mozilla Firefox now have it [enabled by default](https://support.mozilla.org/en-US/kb/firefox-dns-over-https).
91
+ * DNS cache consistency across multiple servers cannot be easily accomplished using packages such as `unbound`, `dnsmasq`, and `bind` – and configuring `/etc/resolv.conf` across multiple Ubuntu versions is not enjoyable (even with Ansible). Maintaining logic at the application layer is much easier from a development, deployment, and maintenance perspective.
92
+ * Privacy, security, and caching approaches needed to be constantly scaled, re-written, and re-configured.
93
+ * Our development teams would encounter unexpected 75 second delays while making DNS requests (if they were connected to a VPN and forgot they were behind blackholed DNS servers – and attempting to use patterns such as `dns.setServers(['1.1.1.1'])`). The default timeout if you are behind a blackholed DNS server in Node.js is 75 seconds (due to `c-ares` under the hood with `5`, `10`, `20`, and `40` second retry backoff timeout strategy).
94
+ * There are **zero existing** DNS over HTTPS ("DoH") Node.js npm packages that:
95
+ * Utilize modern open-source software under the MIT license and are currently maintained.
96
+ * 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
+ * [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
+ * Act as a 1:1 drop-in replacement for `dns.promises.Resolver` with DNS over HTTPS ("DoH").
99
+ * Support caching for multiple backends with [Keyv](https://github.com/jaredwray/keyv) (with respect to DNS answer TTL too!), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
100
+ * 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
+ * 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
+ * Writing tests against DNS-related infrastructure requires either hacky DNS mocking or a DNS server (manipulating cache is much easier).
103
+ * <u>**The Node.js community is lacking a high-quality and dummy-proof userland DNS package with sensible defaults.**</u>
104
+
105
+ </details>
106
+
107
+ ### Why integrate DNS over HTTPS
108
+
109
+ > With DNS over HTTPS (DoH), DNS queries and responses are encrypted and sent via the HTTP or HTTP/2 protocols. DoH ensures that attackers cannot forge or alter DNS traffic. DoH uses port 443, which is the standard HTTPS traffic port, to wrap the DNS query in an HTTPS request. DNS queries and responses are camouflaged within other HTTPS traffic, since it all comes and goes from the same port. – [Cloudflare](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/)
110
+
111
+ > DNS over HTTPS (DoH) is a protocol for performing remote [Domain Name System](https://en.wikipedia.org/wiki/Domain_Name_System) (DNS) resolution via the [HTTPS](https://en.wikipedia.org/wiki/HTTPS) protocol. A goal of the method is to increase user privacy and security by preventing eavesdropping and manipulation of DNS data by [man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attacks) by using the HTTPS protocol to [encrypt](https://en.wikipedia.org/wiki/Encrypt) the data between the DoH client and the DoH-based [DNS resolver](https://en.wikipedia.org/wiki/DNS_resolver). – [Wikipedia](https://en.wikipedia.org/wiki/DNS_over_HTTPS)
112
+
113
+ ### What does this mean
114
+
115
+ [We're](https://forwardemail.net) the <i>only</i> email service provider that is 100% open-source *and* uses DNS over HTTPS ("DoH") throughout their entire infrastructure. We've open-sourced this project – which means you can integrate DNS over HTTPS ("DoH") by simply using :tangerine: Tangerine. Its documentation below includes [Features](#features), [Usage and Examples](#usage-and-examples), [API](#api), [Options](#options), and [Benchmarks](#tangerine-benchmarks).
116
+
117
+ ### What projects were used for inspiration
118
+
119
+ Thanks to the authors of [dohdec](https://github.com/hildjj/dohdec), [dns-packet](https://github.com/mafintosh/dns-packet), [dns2](https://github.com/song940/node-dns), and [native-dnssec-dns](https://github.com/EduardoRuizM/native-dnssec-dns) – which made this project possible and were used for inspiration.
120
+
121
+
122
+ ## Features
123
+
124
+ :tangerine: Tangerine is a 1:1 **drop-in replacement with DNS over HTTPS ("DoH")** for [dns.promises.Resolver](https://nodejs.org/api/dns.html#resolveroptions):
125
+
126
+ * All options and defaults for `new dns.promises.Resolver()` are available in `new Tangerine()`.
127
+ * Instances of `Tangerine` are also instances of `dns.promises.Resolver` as this class `extends` from it. This makes it compatible with [cacheable-lookup](https://github.com/szmarczak/cacheable-lookup).
128
+ * HTTP error codes are mapped to DNS error codes (the error `code` and `errno` properties will appear as if they're from `dns` usage). This is a configurable option enabled by default (see `returnHTTPErrors` option).
129
+ * If you need callbacks, then use [util.callbackify](https://nodejs.org/api/util.html#utilcallbackifyoriginal) (e.g. `const resolveTxt = callbackify(tangerine.resolveTxt)`).
130
+
131
+ <details>
132
+ <summary>We have also added several improvements and new features:</summary>
133
+
134
+ * Default name servers used have been set to [Cloudflare's](https://1.1.1.1/) (`['1.1.1.1', '1.0.0.1']`) (as opposed to the system default – which is often set to a default which is not privacy-focused or simply forgotten to be set by DevOps teams). You may also want to use [Cloudflare's Malware and Adult Content Blocking](https://blog.cloudflare.com/introducing-1-1-1-1-for-families/) DNS server addresses instead.
135
+ * You can pass a custom `servers` option (as opposed to having to invoke `dns.setServers(...)` or `resolver.setServers(...)`).
136
+ * `lookup` and `lookupService` methods have been added (these are not in the original `dns.promises.Resolver` instance methods).
137
+ * [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) support has been added to all DNS request methods (you can also pass your own).
138
+ * The method `cancel()` will signal `"abort"` to all AbortController signals created for existing requests and handle cleanup.
139
+ * An `ecsClientSubnet` option has been added to all methods accepting an `options` object for [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) client subnet querying (this includes `resolve4` and `resolve6`).
140
+ * If you have multiple DNS servers configured (e.g. `tangerine.setServers(['1.1.1.1', '1.0.0.1', '8.8.8.8', '8.8.4.4'])`) – and if any of these servers have repeated errors, then they will be bumped to the end of the list (e.g. if `1.1.1.1` has errors, then the updated in-memory `Set` for future requests will be `['1.0.0.1', '8.8.8.8', '8.8.4.4', '1.1.1.1']`). This "smart server rotation" behavior can be disabled (see `smartRotate` option) – but it is discouraged, as the original behavior of [c-ares](https://c-ares.org/) does not rotate as such.
141
+ * Debug via `NODE_DEBUG=tangerine node app.js` flag (uses [util.debuglog](https://nodejs.org/api/util.html#utildebuglogsection-callback)).
142
+ * The method `setLocalAddress()` will parse the IP address and port properly to pass along for use with the agent as `localAddress` and `localPort`. If you require IPv6 addresses with ports, you must encode it as `[IPv6]:PORT` ([similar to RFC 3986](https://serverfault.com/a/205794)).
143
+
144
+ </details>
145
+
146
+ <details>
147
+ <summary>All existing <code>syscall</code> values have been preserved:</summary>
148
+
149
+ * `resolveAny` → `queryAny`
150
+ * `resolve4` → `queryA`
151
+ * `resolve6` → `queryAaaa`
152
+ * `resolveCaa` → `queryCaa`
153
+ * `resolveCname` → `queryCname`
154
+ * `resolveMx` → `queryMx`
155
+ * `resolveNs` → `queryNs`
156
+ * `resolveNs` → `queryNs`
157
+ * `resolveTxt` → `queryTxt`
158
+ * `resolveSrv` → `querySrv`
159
+ * `resolvePtr` → `queryPtr`
160
+ * `resolveNaptr` → `queryNaptr`
161
+ * `resolveSoa` → `querySoa`
162
+ * `reverse` → `getHostByAddr`
163
+
164
+ </details>
165
+
166
+
167
+ ## Usage and Examples
168
+
169
+ ### ECMAScript modules (ESM)
170
+
171
+ ```js
172
+ // app.mjs
173
+
174
+ import Tangerine from 'tangerine';
175
+
176
+ const tangerine = new Tangerine();
177
+ // or `const resolver = new Tangerine()`
178
+
179
+ tangerine.resolve('forwardemail.net', 'A').then(console.log);
180
+ ```
181
+
182
+ ### CommonJS (CJS)
183
+
184
+ ```js
185
+ // app.js
186
+
187
+ const Tangerine = require('tangerine');
188
+
189
+ const tangerine = new Tangerine();
190
+ // or `const resolver = new Tangerine()`
191
+
192
+ tangerine.resolve('forwardemail.net', 'A').then(console.log);
193
+ ```
194
+
195
+
196
+ ## API
197
+
198
+ ### `new Tangerine(options)`
199
+
200
+ * Instance methods of [dns.promises.Resolver](https://nodejs.org/api/dns.html) are mirrored to :tangerine: Tangerine.
201
+ * Resolver methods accept an optional `abortController` argument, which is an instance of [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). Note that :tangerine: Tangerine manages `AbortController` usage internally – so you most likely won't need to pass your own (see [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight).
202
+ * See the complete list of [Options](#options) below.
203
+ * Instances of `new Tangerine()` are instances of `dns.promises.Resolver` via `class Tangerine extends dns.promises.Resolver { ... }` (namely for compatibility with projects such as [cacheable-lookup](https://github.com/szmarczak/cacheable-lookup)).
204
+
205
+ ### `tangerine.cancel()`
206
+
207
+ ### `tangerine.getServers()`
208
+
209
+ ### `tangerine.lookup(hostname[, options])`
210
+
211
+ ### `tangerine.lookupService(address, port, abortController)`
212
+
213
+ ### `tangerine.resolve(hostname[, rrtype, options, abortController])`
214
+
215
+ ### `tangerine.resolve4(hostname[, options, abortController])`
216
+
217
+ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
218
+
219
+ ### `tangerine.resolve6(hostname[, options, abortController])`
220
+
221
+ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
222
+
223
+ ### `tangerine.resolveAny(hostname[, abortController])`
224
+
225
+ ### `tangerine.resolveCaa(hostname[, abortController]))`
226
+
227
+ ### `tangerine.resolveCname(hostname[, abortController]))`
228
+
229
+ ### `tangerine.resolveMx(hostname[, abortController]))`
230
+
231
+ ### `tangerine.resolveNaptr(hostname[, abortController]))`
232
+
233
+ ### `tangerine.resolveNs(hostname[, abortController]))`
234
+
235
+ ### `tangerine.resolvePtr(hostname[, abortController]))`
236
+
237
+ ### `tangerine.resolveSoa(hostname[, abortController]))`
238
+
239
+ ### `tangerine.resolveSrv(hostname[, abortController]))`
240
+
241
+ ### `tangerine.resolveTxt(hostname[, abortController]))`
242
+
243
+ ### `tangerine.reverse(ip[, abortController])`
244
+
245
+ ### `tangerine.setDefaultResultOrder(order)`
246
+
247
+ ### `tangerine.setServers(servers)`
248
+
249
+
250
+ ## Options
251
+
252
+ 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.
253
+
254
+ | Property | Type | Default Value | Description |
255
+ | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
256
+ | `timeout` | `Number` | `5000` | Number of milliseconds for requests to timeout. |
257
+ | `tries` | `Number` | `4` | Number of tries per `server` in `servers` to attempt. |
258
+ | `servers` | `Set` | `new Set(['1.1.1.1', '1.0.0.1'])` | A set containing IP addresses for DNS queries. Defaults to Cloudflare's of `1.1.1.1` and `1.0.0.1`. |
259
+ | `undici` | `Object` | Defaults to an Object with `undici.method` and `undici.headers` properties and values below | Default options to pass to [undici](https://github.com/nodejs/undici). |
260
+ | `undici.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
261
+ | `undici.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. |
262
+ | `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
263
+ | `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). |
264
+ | `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). |
265
+ | `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)`). |
266
+ | `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. |
267
+ | `ipv4` | `String` | `"0.0.0.0"` | Default IPv4 address to use for HTTP agent `localAddress` if DNS `server` was an IPv4 address. |
268
+ | `ipv6` | `String` | `"::0"` | Default IPv6 address to use for HTTP agent `localAddress` if DNS `server` was an IPv6 address. |
269
+ | `ipv4Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv4 address. |
270
+ | `ipv6Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv6 address. |
271
+ | `cache` | `Map` or `Boolean` | `new Map()` | Set this to `false` in order to disable caching. Default `Map` instance to use for caching. Entries are by type, e.g. `map.set('TXT', new Keyv({})`). If cache set values are not provided, then they will default to a new instance of `Keyv`. See cache setup and usage in [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight. You can iterate over `Tangerine.TYPES` if necessary to create a similar cache setup. |
272
+ | `returnHTTPErrors` | `Boolean` | `false` | Whether to return HTTP errors instead of mapping them to corresponding DNS errors. |
273
+ | `smartRotate` | `Boolean` | `true` | Whether to do smart server rotation if servers fail. |
274
+ | `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). |
275
+
276
+
277
+ ## Debugging
278
+
279
+ If you run into issues while using :tangerine: Tangerine, then these recommendations may help:
280
+
281
+ * Set `NODE_DEBUG=tangerine` environment variable flag when you start your app:
282
+
283
+ ```sh
284
+ NODE_DEBUG=tangerine node app.js
285
+ ```
286
+
287
+ * Pass a verbose logger as the `logger` option, e.g. `logger: console` (see [Options](#options) above).
288
+
289
+ * Assuming you are not allergic, try eating a [nutritious](https://en.wikipedia.org/wiki/Tangerine#Nutrition) :tangerine: tangerine.
290
+
291
+
292
+ ## Benchmarks
293
+
294
+ Contributors can run benchmarks locally by cloning the repository, installing dependencies, and running the benchmarks script:
295
+
296
+ ```sh
297
+ git clone https://github.com/forwardemail/tangerine.git
298
+ cd tangerine
299
+ npm install
300
+ npm run benchmarks
301
+ ```
302
+
303
+ You can also specify optional custom environment variables to test against real-world or locally running servers (instead of using mocked in-memory servers):
304
+
305
+ ```sh
306
+ BENCHMARK_PROTOCOL="http" BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" npm run benchmarks
307
+ ```
308
+
309
+ ### Tangerine Benchmarks
310
+
311
+ We have written extensive benchmarks to show that :tangerine: Tangerine has a <u>**faster `resolve()` and `reverse()`**</u> (and fast enough `lookup()`) versus the native Node.js DNS module.
312
+
313
+ 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):
314
+
315
+ > [Node 16 on ubuntu-latest](https://github.com/forwardemail/tangerine/actions/runs/4265467648/jobs/7424828382#step:6:1)
316
+
317
+ ```sh
318
+ > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
319
+
320
+ tangerine.lookup POST with caching using Cloudflare x 521 ops/sec ±186.87% (79 runs sampled)
321
+ tangerine.lookup POST without caching using Cloudflare x 252 ops/sec ±1.52% (79 runs sampled)
322
+ tangerine.lookup GET with caching using Cloudflare x 11,217 ops/sec ±1.75% (78 runs sampled)
323
+ tangerine.lookup GET without caching using Cloudflare x 259 ops/sec ±1.28% (84 runs sampled) <--------
324
+ dns.promises.lookup with caching using Cloudflare x 206,286 ops/sec ±0.92% (82 runs sampled)
325
+ dns.promises.lookup without caching using Cloudflare x 2,330 ops/sec ±1.86% (80 runs sampled)
326
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
327
+ tangerine.resolve POST with caching using Cloudflare x 734 ops/sec ±190.07% (84 runs sampled)
328
+ tangerine.resolve POST without caching using Cloudflare x 234 ops/sec ±3.75% (82 runs sampled)
329
+ tangerine.resolve GET with caching using Cloudflare x 24,040 ops/sec ±1.93% (83 runs sampled)
330
+ tangerine.resolve GET without caching using Cloudflare x 215 ops/sec ±16.62% (75 runs sampled)
331
+ tangerine.resolve POST with caching using Google x 23,937 ops/sec ±2.04% (81 runs sampled)
332
+ tangerine.resolve POST without caching using Google x 213 ops/sec ±9.51% (71 runs sampled)
333
+ tangerine.resolve GET with caching using Google x 24,272 ops/sec ±1.74% (83 runs sampled)
334
+ tangerine.resolve GET without caching using Google x 257 ops/sec ±4.02% (80 runs sampled)
335
+ resolver.resolve with caching using Cloudflare x 158,842 ops/sec ±2.57% (84 runs sampled)
336
+ resolver.resolve without caching using Cloudflare x 8.02 ops/sec ±191.78% (41 runs sampled)
337
+ Fastest without caching is: tangerine.resolve GET without caching using Google <--------
338
+ tangerine.reverse GET with caching x 694 ops/sec ±189.48% (76 runs sampled)
339
+ tangerine.reverse GET without caching x 123 ops/sec ±90.74% (81 runs sampled)
340
+ resolver.reverse x 0.24 ops/sec ±86.12% (10 runs sampled)
341
+ dns.promises.reverse x 0.70 ops/sec ±164.50% (42 runs sampled)
342
+ Fastest without caching is: tangerine.reverse GET without caching <--------
343
+ http.request POST request x 384 ops/sec ±1.08% (84 runs sampled)
344
+ http.request GET request x 398 ops/sec ±0.83% (83 runs sampled)
345
+ undici GET request x 206 ops/sec ±5.59% (58 runs sampled)
346
+ undici POST request x 211 ops/sec ±4.44% (74 runs sampled)
347
+ axios GET request x 343 ops/sec ±1.97% (82 runs sampled)
348
+ axios POST request x 350 ops/sec ±3.35% (82 runs sampled)
349
+ got GET request x 325 ops/sec ±1.61% (81 runs sampled)
350
+ got POST request x 341 ops/sec ±2.86% (84 runs sampled)
351
+ fetch GET request x 657 ops/sec ±1.42% (82 runs sampled)
352
+ fetch POST request x 680 ops/sec ±1.21% (84 runs sampled)
353
+ request GET request x 370 ops/sec ±1.08% (85 runs sampled)
354
+ request POST request x 370 ops/sec ±0.88% (84 runs sampled)
355
+ superagent GET request x 380 ops/sec ±1.14% (83 runs sampled)
356
+ superagent POST request x 386 ops/sec ±1.04% (83 runs sampled)
357
+ phin GET request x 396 ops/sec ±0.86% (84 runs sampled)
358
+ phin POST request x 398 ops/sec ±0.83% (85 runs sampled)
359
+ Fastest is fetch POST request
360
+ ```
361
+
362
+ > **NOTE**: HTTP library benchmark tests above are not based on real-world usage; instead they are using mock libraries such as `nock` and undici's `MockAgent`. An actual HTTP server could be implemented in these benchmarks (pull request is welcome). See [HTTP Library Benchmarks](#http-library-benchmarks) below for more insight into results with real-world servers.
363
+
364
+ > [Node 18 on ubuntu latest](https://github.com/forwardemail/tangerine/actions/runs/4265467648/jobs/7424828575#step:6:1)
365
+
366
+ ```sh
367
+ > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
368
+
369
+ tangerine.lookup POST with caching using Cloudflare x 576 ops/sec ±188.97% (84 runs sampled)
370
+ tangerine.lookup POST without caching using Cloudflare x 62.80 ops/sec ±0.34% (75 runs sampled)
371
+ tangerine.lookup GET with caching using Cloudflare x 16,710 ops/sec ±0.27% (83 runs sampled)
372
+ tangerine.lookup GET without caching using Cloudflare x 60.59 ops/sec ±6.15% (75 runs sampled) <--------
373
+ dns.promises.lookup with caching using Cloudflare x 251,300 ops/sec ±0.66% (89 runs sampled)
374
+ dns.promises.lookup without caching using Cloudflare x 4,189 ops/sec ±0.65% (89 runs sampled)
375
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare <--------
376
+ tangerine.resolve POST with caching using Cloudflare x 627 ops/sec ±192.36% (90 runs sampled)
377
+ tangerine.resolve POST without caching using Cloudflare x 59.66 ops/sec ±5.69% (74 runs sampled)
378
+ tangerine.resolve GET with caching using Cloudflare x 33,813 ops/sec ±0.33% (90 runs sampled)
379
+ tangerine.resolve GET without caching using Cloudflare x 60.16 ops/sec ±4.03% (73 runs sampled)
380
+ tangerine.resolve POST with caching using Google x 1,184 ops/sec ±189.17% (90 runs sampled)
381
+ tangerine.resolve POST without caching using Google x 41.23 ops/sec ±7.30% (70 runs sampled)
382
+ tangerine.resolve GET with caching using Google x 33,811 ops/sec ±0.56% (91 runs sampled)
383
+ tangerine.resolve GET without caching using Google x 54.34 ops/sec ±5.71% (69 runs sampled)
384
+ resolver.resolve with caching using Cloudflare x 202,804 ops/sec ±0.39% (88 runs sampled)
385
+ resolver.resolve without caching using Cloudflare x 61.93 ops/sec ±5.76% (76 runs sampled)
386
+ Fastest without caching is: resolver.resolve without caching using Cloudflare <--------
387
+ tangerine.reverse GET with caching x 594 ops/sec ±192.60% (86 runs sampled)
388
+ tangerine.reverse GET without caching x 60.73 ops/sec ±3.06% (74 runs sampled)
389
+ resolver.reverse x 66.00 ops/sec ±0.91% (78 runs sampled)
390
+ dns.promises.reverse x 1.84 ops/sec ±190.54% (71 runs sampled)
391
+ Fastest without caching is: tangerine.reverse GET without caching <--------
392
+ http.request POST request x 438 ops/sec ±0.61% (86 runs sampled)
393
+ http.request GET request x 442 ops/sec ±0.64% (87 runs sampled)
394
+ undici GET request x 203 ops/sec ±3.67% (42 runs sampled)
395
+ undici POST request x 194 ops/sec ±3.77% (62 runs sampled)
396
+ axios GET request x 403 ops/sec ±1.67% (86 runs sampled)
397
+ axios POST request x 414 ops/sec ±0.65% (88 runs sampled)
398
+ got GET request x 391 ops/sec ±1.63% (85 runs sampled)
399
+ got POST request x 403 ops/sec ±0.90% (85 runs sampled)
400
+ fetch GET request x 794 ops/sec ±2.32% (84 runs sampled)
401
+ fetch POST request x 821 ops/sec ±0.89% (86 runs sampled)
402
+ request GET request x 423 ops/sec ±0.75% (86 runs sampled)
403
+ request POST request x 426 ops/sec ±0.78% (86 runs sampled)
404
+ superagent GET request x 435 ops/sec ±0.79% (87 runs sampled)
405
+ superagent POST request x 437 ops/sec ±0.82% (88 runs sampled)
406
+ phin GET request x 443 ops/sec ±0.64% (86 runs sampled)
407
+ phin POST request x 445 ops/sec ±0.60% (86 runs sampled)
408
+ Fastest is fetch POST request
409
+ ```
410
+
411
+ > **NOTE**: HTTP library benchmark tests above are not based on real-world usage; instead they are using mock libraries such as `nock` and undici's `MockAgent`. An actual HTTP server could be implemented in these benchmarks (pull request is welcome). See [HTTP Library Benchmarks](#http-library-benchmarks) below for more insight into results with real-world servers.
412
+
413
+ ---
414
+
415
+ You can also [run the benchmarks yourself](#benchmarks).
416
+
417
+ ---
418
+
419
+ Provided below are additional benchmark tests we have run:
420
+
421
+ > Node v16.18.1 on MacBook Air M1 16GB (without VPN):
422
+
423
+ ```sh
424
+ ❯ node --version
425
+ v16.18.1
426
+
427
+ ❯ node benchmarks/resolve
428
+ tangerine POST with caching using Cloudflare x 1,044 ops/sec ±193.21% (90 runs sampled)
429
+ tangerine POST without caching using Cloudflare x 40.93 ops/sec ±53.83% (50 runs sampled)
430
+ tangerine GET with caching using Cloudflare x 73,896 ops/sec ±0.27% (90 runs sampled)
431
+ tangerine GET without caching using Cloudflare x 38.66 ops/sec ±21.98% (55 runs sampled)
432
+ tangerine POST with caching using Google x 992 ops/sec ±193.33% (87 runs sampled)
433
+ tangerine POST without caching using Google x 31.98 ops/sec ±21.35% (58 runs sampled)
434
+ tangerine GET with caching using Google x 74,410 ops/sec ±0.22% (91 runs sampled)
435
+ tangerine GET without caching using Google x 41.52 ops/sec ±18.91% (56 runs sampled)
436
+ dns.promises.resolve without caching using Cloudflare x 25.46 ops/sec ±100.19% (50 runs sampled)
437
+ dns.promises.resolve with caching using Cloudflare x 505,956 ops/sec ±2.34% (89 runs sampled)
438
+ Fastest without caching is: tangerine GET without caching using Google, tangerine GET without caching using Cloudflare
439
+ ```
440
+
441
+ > Node v16.18.1 on MacBook Air M1 16GB (with DNS blackholed VPN) – <mark>this highlights the DNS blackhole problem</mark>:
442
+
443
+ ```sh
444
+ ❯ node --version
445
+ v16.18.1
446
+
447
+ ❯ node benchmarks/resolve
448
+ tangerine POST with caching using Cloudflare x 185 ops/sec ±195.50% (88 runs sampled)
449
+ tangerine POST without caching using Cloudflare x 6.48 ops/sec ±35.98% (35 runs sampled)
450
+ tangerine GET with caching using Cloudflare x 824 ops/sec ±193.77% (90 runs sampled)
451
+ tangerine GET without caching using Cloudflare x 8.66 ops/sec ±8.22% (46 runs sampled)
452
+ tangerine POST with caching using Google x 205 ops/sec ±195.45% (88 runs sampled)
453
+ tangerine POST without caching using Google x 7.20 ops/sec ±12.28% (40 runs sampled)
454
+ tangerine GET with caching using Google x 690 ops/sec ±194.12% (90 runs sampled)
455
+ tangerine GET without caching using Google x 7.85 ops/sec ±9.53% (42 runs sampled)
456
+ dns.promises.resolve without caching using Cloudflare x 0.09 ops/sec ±5.10% (5 runs sampled) <--------
457
+ dns.promises.resolve with caching using Cloudflare x 0.09 ops/sec ±5.13% (5 runs sampled) <--------
458
+ Fastest without caching is: tangerine GET without caching using Cloudflare
459
+ ```
460
+
461
+ > Node v18.4.2 on MacBook Air M1 16GB (without VPN):
462
+
463
+ ```sh
464
+ ❯ node --version
465
+ v18.4.2
466
+
467
+ ❯ node benchmarks/resolve
468
+ tangerine POST with caching using Cloudflare x 817 ops/sec ±193.86% (89 runs sampled)
469
+ tangerine POST without caching using Cloudflare x 42.57 ops/sec ±38.18% (62 runs sampled)
470
+ tangerine GET with caching using Cloudflare x 853 ops/sec ±193.79% (91 runs sampled)
471
+ tangerine GET without caching using Cloudflare x 41.13 ops/sec ±57.37% (48 runs sampled)
472
+ tangerine POST with caching using Google x 1,488 ops/sec ±192.10% (90 runs sampled)
473
+ tangerine POST without caching using Google x 38.46 ops/sec ±12.08% (59 runs sampled)
474
+ tangerine GET with caching using Google x 74,240 ops/sec ±0.31% (90 runs sampled)
475
+ tangerine GET without caching using Google x 39.20 ops/sec ±23.52% (58 runs sampled)
476
+ dns.promises.resolve without caching using Cloudflare x 59.11 ops/sec ±13.96% (63 runs sampled)
477
+ dns.promises.resolve with caching using Cloudflare x 529,961 ops/sec ±0.33% (91 runs sampled)
478
+ Fastest without caching is: dns.promises.resolve without caching using Cloudflare, tangerine GET without caching using Cloudflare
479
+ ```
480
+
481
+ > Node v18.4.2 on MacBook Air M1 16GB (with DNS blackholed VPN) – <mark>this highlights the DNS blackhole problem</mark>:
482
+
483
+ ```sh
484
+ ❯ node --version
485
+ v18.4.2
486
+
487
+ ❯ node benchmarks/resolve
488
+ tangerine POST with caching using Cloudflare x 193 ops/sec ±195.49% (91 runs sampled)
489
+ tangerine POST without caching using Cloudflare x 8.44 ops/sec ±9.34% (45 runs sampled)
490
+ tangerine GET with caching using Cloudflare x 829 ops/sec ±193.83% (88 runs sampled)
491
+ tangerine GET without caching using Cloudflare x 7.44 ops/sec ±24.67% (45 runs sampled)
492
+ tangerine POST with caching using Google x 255 ops/sec ±195.33% (91 runs sampled)
493
+ tangerine POST without caching using Google x 4.59 ops/sec ±77.88% (26 runs sampled)
494
+ tangerine GET with caching using Google x 794 ops/sec ±193.95% (92 runs sampled)
495
+ tangerine GET without caching using Google x 7.69 ops/sec ±11.23% (42 runs sampled)
496
+ dns.promises.resolve without caching using Cloudflare x 0.09 ops/sec ±6.41% (5 runs sampled) <--------
497
+ dns.promises.resolve with caching using Cloudflare x 0.09 ops/sec ±6.41% (5 runs sampled) <--------
498
+ Fastest without caching is: tangerine POST without caching using Cloudflare, tangerine GET without caching using Cloudflare
499
+ ```
500
+
501
+ Also see this [write-up](https://samknows.com/blog/dns-over-https-performance) on UDP-based DNS versus DNS over HTTPS ("DoH") benchmarks.
502
+
503
+ **Speed could be increased** by switching to use [undici streams](https://undici.nodejs.org/#/?id=undicistreamurl-options-factory-promise) and [getStream.buffer](https://github.com/sindresorhus/get-stream) (pull request is welcome).
504
+
505
+ ### HTTP Library Benchmarks
506
+
507
+ 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):
508
+
509
+ > Node v16.18.1 on MacBook Air M1 16GB (using real-world API server):
510
+
511
+ ```sh
512
+ ❯ node --version
513
+ v16.18.1
514
+
515
+ ❯ BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
516
+ http.request POST request x 860 ops/sec ±6.33% (75 runs sampled)
517
+ http.request GET request x 978 ops/sec ±5.17% (83 runs sampled)
518
+ undici GET request x 2,732 ops/sec ±4.14% (83 runs sampled)
519
+ undici POST request x 1,204 ops/sec ±5.01% (81 runs sampled)
520
+ axios GET request x 855 ops/sec ±5.45% (81 runs sampled)
521
+ axios POST request x 723 ops/sec ±15.28% (71 runs sampled)
522
+ got GET request x 1,355 ops/sec ±16.60% (63 runs sampled)
523
+ got POST request x 93.65 ops/sec ±181.51% (29 runs sampled)
524
+ fetch GET request x 949 ops/sec ±40.26% (45 runs sampled)
525
+ fetch POST request x 672 ops/sec ±22.43% (67 runs sampled)
526
+ request GET request x 960 ops/sec ±50.90% (48 runs sampled)
527
+ request POST request x 612 ops/sec ±45.48% (57 runs sampled)
528
+ superagent GET request x 126 ops/sec ±188.34% (29 runs sampled)
529
+ superagent POST request x 747 ops/sec ±18.16% (67 runs sampled)
530
+ phin GET request x 374 ops/sec ±147.42% (57 runs sampled)
531
+ phin POST request x 566 ops/sec ±38.08% (51 runs sampled)
532
+ Fastest is undici GET request
533
+ ```
534
+
535
+ > Node v18.14.2 on MacBook Air M1 16GB (using real-world API server):
536
+
537
+ ```sh
538
+ ❯ node --version
539
+ v18.14.2
540
+
541
+ ❯ BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
542
+ http.request POST request x 765 ops/sec ±9.83% (72 runs sampled)
543
+ http.request GET request x 1,000 ops/sec ±3.88% (85 runs sampled)
544
+ undici GET request x 2,740 ops/sec ±5.92% (78 runs sampled)
545
+ undici POST request x 1,247 ops/sec ±0.61% (88 runs sampled)
546
+ axios GET request x 792 ops/sec ±7.78% (76 runs sampled)
547
+ axios POST request x 717 ops/sec ±13.85% (69 runs sampled)
548
+ got GET request x 1,234 ops/sec ±21.10% (67 runs sampled)
549
+ got POST request x 113 ops/sec ±168.45% (37 runs sampled)
550
+ fetch GET request x 977 ops/sec ±38.12% (51 runs sampled)
551
+ fetch POST request x 708 ops/sec ±23.64% (65 runs sampled)
552
+ request GET request x 1,152 ops/sec ±40.48% (49 runs sampled)
553
+ request POST request x 947 ops/sec ±1.35% (86 runs sampled)
554
+ superagent GET request x 148 ops/sec ±139.32% (31 runs sampled)
555
+ superagent POST request x 571 ops/sec ±40.14% (54 runs sampled)
556
+ phin GET request x 252 ops/sec ±158.51% (50 runs sampled)
557
+ phin POST request x 714 ops/sec ±17.39% (62 runs sampled)
558
+ Fastest is undici GET request
559
+ ```
560
+
561
+
562
+ ## Contributors
563
+
564
+ | Name | Website |
565
+ | ----------------- | -------------------------- |
566
+ | **Forward Email** | <https://forwardemail.net> |
567
+
568
+
569
+ ## License
570
+
571
+ [MIT](LICENSE) © [Forward Email](https://forwardemail.net)
572
+
573
+
574
+ ##
575
+
576
+ <a href="#"><img src="https://raw.githubusercontent.com/forwardemail/tangerine/main/media/footer.png" alt="#" /></a>