tangerine 1.4.5 → 1.4.7
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 +10 -0
- package/index.js +166 -42
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
* [`tangerine.setServers(servers)`](#tangerinesetserversservers)
|
|
59
59
|
* [Options](#options)
|
|
60
60
|
* [Cache](#cache)
|
|
61
|
+
* [Compatibility](#compatibility)
|
|
61
62
|
* [Debugging](#debugging)
|
|
62
63
|
* [Benchmarks](#benchmarks)
|
|
63
64
|
* [Tangerine Benchmarks](#tangerine-benchmarks)
|
|
@@ -430,6 +431,15 @@ await tangerine.resolve('forwardemail.net'); // uses cached value
|
|
|
430
431
|
This purge cache feature is useful for DNS records that have recently changed and have had their caches purged at the relevant DNS provider (e.g. [Cloudflare's Purge Cache tool](https://1.1.1.1/purge-cache/)).
|
|
431
432
|
|
|
432
433
|
|
|
434
|
+
## Compatibility
|
|
435
|
+
|
|
436
|
+
The only known compatibility issue is for locally running DNS servers that have wildcard DNS matching.
|
|
437
|
+
|
|
438
|
+
If you are using `dnsmasq` with a wildcard match on "localhost" to "127.0.0.1", then the results may vary. For example, if your `dnsmasq` configuration has `address=/localhost/127.0.0.1`, then any match of `localhost` will resolve to `127.0.0.1`. This means that `dns.promises.lookup('foo.localhost')` will return `127.0.0.1` – however with :tangerine: Tangerine it will not return a value.
|
|
439
|
+
|
|
440
|
+
The reason is because :tangerine: Tangerine only looks at either `/etc/hosts` (macOS/Linux) and `C:/Windows/System32/drivers/etc/hosts` (Windows). It does not lookup BIND, dnsmasq, or other configurations running locally. We would welcome a PR to resolve this (see `isCI` usage in test folder) – however it is a non-issue, as the workaround is to simply append a new line to the hostfile of `127.0.0.1 foo.localhost`.
|
|
441
|
+
|
|
442
|
+
|
|
433
443
|
## Debugging
|
|
434
444
|
|
|
435
445
|
If you run into issues while using :tangerine: Tangerine, then these recommendations may help:
|
package/index.js
CHANGED
|
@@ -7,8 +7,11 @@ 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 { toASCII } = require('punycode/');
|
|
11
|
+
|
|
10
12
|
const autoBind = require('auto-bind');
|
|
11
13
|
const getStream = require('get-stream');
|
|
14
|
+
const hostile = require('hostile');
|
|
12
15
|
const ipaddr = require('ipaddr.js');
|
|
13
16
|
const mergeOptions = require('merge-options');
|
|
14
17
|
const pMap = require('p-map');
|
|
@@ -18,8 +21,6 @@ const packet = require('dns-packet');
|
|
|
18
21
|
const semver = require('semver');
|
|
19
22
|
const structuredClone = require('@ungap/structured-clone').default;
|
|
20
23
|
const { getService } = require('port-numbers');
|
|
21
|
-
// eslint-disable-next-line import/order
|
|
22
|
-
const { toASCII } = require('punycode/');
|
|
23
24
|
|
|
24
25
|
const pkg = require('./package.json');
|
|
25
26
|
|
|
@@ -32,8 +33,32 @@ import('dohdec').then((obj) => {
|
|
|
32
33
|
dohdec = obj;
|
|
33
34
|
});
|
|
34
35
|
|
|
36
|
+
// dynamically import private-ip
|
|
37
|
+
let isPrivateIP;
|
|
38
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
39
|
+
import('private-ip').then((obj) => {
|
|
40
|
+
isPrivateIP = obj.default;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const HOSTFILE = hostile
|
|
44
|
+
.get(true)
|
|
45
|
+
.map((s) => (Array.isArray(s) ? s.join(' ') : s))
|
|
46
|
+
.join('\n');
|
|
47
|
+
|
|
48
|
+
const HOSTS = [];
|
|
49
|
+
const hosts = hostile.get();
|
|
50
|
+
for (const line of hosts) {
|
|
51
|
+
const [ip, str] = line;
|
|
52
|
+
const hosts = str.split(' ');
|
|
53
|
+
HOSTS.push({ ip, hosts });
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
// <https://github.com/szmarczak/cacheable-lookup/pull/76>
|
|
36
57
|
class Tangerine extends dns.promises.Resolver {
|
|
58
|
+
static HOSTFILE = HOSTFILE;
|
|
59
|
+
|
|
60
|
+
static HOSTS = HOSTS;
|
|
61
|
+
|
|
37
62
|
static isValidPort(port) {
|
|
38
63
|
return Number.isSafeInteger(port) && port >= 0 && port <= 65535;
|
|
39
64
|
}
|
|
@@ -148,7 +173,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
148
173
|
dns.NOTINITIALIZED,
|
|
149
174
|
dns.REFUSED,
|
|
150
175
|
dns.SERVFAIL,
|
|
151
|
-
dns.TIMEOUT
|
|
176
|
+
dns.TIMEOUT,
|
|
177
|
+
'EINVAL'
|
|
152
178
|
]);
|
|
153
179
|
|
|
154
180
|
static DNS_TYPES = new Set([
|
|
@@ -606,6 +632,16 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
606
632
|
throw err;
|
|
607
633
|
}
|
|
608
634
|
|
|
635
|
+
if (name === '.') {
|
|
636
|
+
const err = this.constructor.createError(name, '', dns.NOTFOUND);
|
|
637
|
+
// remap and perform syscall
|
|
638
|
+
err.syscall = 'getaddrinfo';
|
|
639
|
+
err.message = err.message.replace('query', 'getaddrinfo');
|
|
640
|
+
err.errno = -3008; // <-- ?
|
|
641
|
+
// err.errno = -3007;
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
644
|
+
|
|
609
645
|
// purge cache support
|
|
610
646
|
let purgeCache;
|
|
611
647
|
if (options?.purgeCache) {
|
|
@@ -639,50 +675,102 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
639
675
|
}
|
|
640
676
|
}
|
|
641
677
|
|
|
678
|
+
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L407>
|
|
679
|
+
// <https://www.rfc-editor.org/rfc/rfc6761.html#section-6.3>
|
|
680
|
+
//
|
|
681
|
+
// > 'localhost and any domains falling within .localhost'
|
|
682
|
+
//
|
|
683
|
+
// if no system loopback match, then revert to the default
|
|
684
|
+
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares__addrinfo_localhost.c#L224-L229>
|
|
685
|
+
// - IPv4 = '127.0.0.1"
|
|
686
|
+
// - IPv6 = "::1"
|
|
687
|
+
//
|
|
688
|
+
let resolve4;
|
|
689
|
+
let resolve6;
|
|
690
|
+
|
|
691
|
+
const lower = name.toLowerCase();
|
|
692
|
+
|
|
693
|
+
for (const rule of this.constructor.HOSTS) {
|
|
694
|
+
if (rule.hosts.every((h) => h.toLowerCase() !== lower)) continue;
|
|
695
|
+
const type = isIP(rule.ip);
|
|
696
|
+
if (!resolve4 && type === 4) {
|
|
697
|
+
if (!Array.isArray(resolve4)) resolve4 = [rule.ip];
|
|
698
|
+
else if (!resolve4.includes(rule.ip)) resolve4.push([rule.ip]);
|
|
699
|
+
} else if (!resolve6 && type === 6) {
|
|
700
|
+
if (!Array.isArray(resolve6)) resolve6 = [rule.ip];
|
|
701
|
+
else if (!resolve6.includes(rule.ip)) resolve6.push(rule.ip);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// safeguard (matches c-ares)
|
|
706
|
+
if (lower === 'localhost' || lower === 'localhost.') {
|
|
707
|
+
if (!resolve4) resolve4 = ['127.0.0.1'];
|
|
708
|
+
if (!resolve6) resolve6 = ['::1'];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (isIPv4(name)) {
|
|
712
|
+
resolve4 = [name];
|
|
713
|
+
resolve6 = [];
|
|
714
|
+
} else if (isIPv6(name)) {
|
|
715
|
+
resolve6 = [name];
|
|
716
|
+
resolve4 = [];
|
|
717
|
+
}
|
|
718
|
+
|
|
642
719
|
// resolve the first A or AAAA record (conditionally)
|
|
643
|
-
|
|
720
|
+
const results = await Promise.all(
|
|
721
|
+
[
|
|
722
|
+
Array.isArray(resolve4)
|
|
723
|
+
? Promise.resolve(resolve4)
|
|
724
|
+
: this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
|
|
725
|
+
Array.isArray(resolve6)
|
|
726
|
+
? Promise.resolve(resolve6)
|
|
727
|
+
: this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
|
|
728
|
+
].map((p) => p.catch((err) => err))
|
|
729
|
+
);
|
|
644
730
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
|
|
648
|
-
this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
|
|
649
|
-
]);
|
|
650
|
-
// default node behavior seems to return IPv4 by default always regardless
|
|
651
|
-
answers =
|
|
652
|
-
answers[0].length > 0 &&
|
|
653
|
-
(typeof options.family === 'undefined' || options.family === 0)
|
|
654
|
-
? answers[0]
|
|
655
|
-
: answers.flat();
|
|
656
|
-
} catch (_err) {
|
|
657
|
-
debug(_err);
|
|
658
|
-
|
|
659
|
-
// this will most likely be instanceof AggregateError
|
|
660
|
-
if (_err instanceof AggregateError) {
|
|
661
|
-
const err = this.constructor.combineErrors(_err.errors);
|
|
662
|
-
err.hostname = name;
|
|
663
|
-
// remap and perform syscall
|
|
664
|
-
err.syscall = 'getaddrinfo';
|
|
665
|
-
if (!err.code)
|
|
666
|
-
err.code = _err.errors.find((e) => e.code)?.code || dns.BADRESP;
|
|
667
|
-
if (!err.errno)
|
|
668
|
-
err.errno = _err.errors.find((e) => e.errno)?.errno || undefined;
|
|
731
|
+
const errors = [];
|
|
732
|
+
let answers = [];
|
|
669
733
|
|
|
670
|
-
|
|
734
|
+
for (const result of results) {
|
|
735
|
+
if (result instanceof Error) {
|
|
736
|
+
errors.push(result);
|
|
737
|
+
} else {
|
|
738
|
+
answers.push(result);
|
|
671
739
|
}
|
|
740
|
+
}
|
|
672
741
|
|
|
673
|
-
|
|
742
|
+
if (
|
|
743
|
+
answers.length === 0 &&
|
|
744
|
+
errors.length > 0 &&
|
|
745
|
+
errors.every((e) => e.code === errors[0].code)
|
|
746
|
+
) {
|
|
747
|
+
const err = this.constructor.createError(
|
|
748
|
+
name,
|
|
749
|
+
'',
|
|
750
|
+
errors[0].code === dns.BADNAME ? dns.NOTFOUND : errors[0].code
|
|
751
|
+
);
|
|
674
752
|
// remap and perform syscall
|
|
675
753
|
err.syscall = 'getaddrinfo';
|
|
676
|
-
err.
|
|
754
|
+
err.message = err.message.replace('query', 'getaddrinfo');
|
|
755
|
+
err.errno = -3008;
|
|
677
756
|
throw err;
|
|
678
757
|
}
|
|
679
758
|
|
|
759
|
+
// default node behavior seems to return IPv4 by default always regardless
|
|
760
|
+
if (answers.length > 0)
|
|
761
|
+
answers =
|
|
762
|
+
answers[0].length > 0 &&
|
|
763
|
+
(typeof options.family === 'undefined' || options.family === 0)
|
|
764
|
+
? answers[0]
|
|
765
|
+
: answers.flat();
|
|
766
|
+
|
|
680
767
|
// if no results then throw ENODATA
|
|
681
768
|
if (answers.length === 0) {
|
|
682
769
|
const err = this.constructor.createError(name, '', dns.NODATA);
|
|
683
770
|
// remap and perform syscall
|
|
684
771
|
err.syscall = 'getaddrinfo';
|
|
685
|
-
|
|
772
|
+
err.message = err.message.replace('query', 'getaddrinfo');
|
|
773
|
+
err.errno = -3008;
|
|
686
774
|
throw err;
|
|
687
775
|
}
|
|
688
776
|
|
|
@@ -835,15 +923,37 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
835
923
|
}
|
|
836
924
|
|
|
837
925
|
if (!isIP(ip)) {
|
|
838
|
-
const err = this.constructor.createError(ip, '',
|
|
926
|
+
const err = this.constructor.createError(ip, '', 'EINVAL');
|
|
927
|
+
err.message = `getHostByAddr EINVAL ${err.hostname}`;
|
|
839
928
|
err.syscall = 'getHostByAddr';
|
|
840
|
-
|
|
929
|
+
err.errno = -22;
|
|
841
930
|
if (!ip) delete err.hostname;
|
|
842
931
|
throw err;
|
|
843
932
|
}
|
|
844
933
|
|
|
934
|
+
// edge case where localhost IP returns matches
|
|
935
|
+
if (!isPrivateIP) await pWaitFor(() => Boolean(isPrivateIP));
|
|
936
|
+
|
|
937
|
+
const answers = new Set();
|
|
938
|
+
let match = false;
|
|
939
|
+
|
|
940
|
+
for (const rule of this.constructor.HOSTS) {
|
|
941
|
+
if (rule.ip === ip) {
|
|
942
|
+
match = true;
|
|
943
|
+
for (const host of rule.hosts.slice(1)) {
|
|
944
|
+
answers.add(host);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (answers.size > 0 || match) return [...answers];
|
|
950
|
+
|
|
951
|
+
// NOTE: we can prob remove this (?)
|
|
952
|
+
// if (ip === '::1' || ip === '127.0.0.1') return [];
|
|
953
|
+
|
|
845
954
|
// reverse the IP address
|
|
846
955
|
if (!dohdec) await pWaitFor(() => Boolean(dohdec));
|
|
956
|
+
|
|
847
957
|
const name = dohdec.DNSoverHTTPS.reverse(ip);
|
|
848
958
|
|
|
849
959
|
// perform resolvePTR
|
|
@@ -1270,13 +1380,22 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1270
1380
|
);
|
|
1271
1381
|
}
|
|
1272
1382
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1383
|
+
try {
|
|
1384
|
+
const results = await pMap(
|
|
1385
|
+
this.constructor.ANY_TYPES,
|
|
1386
|
+
this.#resolveByType(name, options, abortController),
|
|
1387
|
+
// <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
|
|
1388
|
+
{
|
|
1389
|
+
concurrency: this.options.concurrency,
|
|
1390
|
+
signal: abortController.signal
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
return results.flat().filter(Boolean);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
err.syscall = 'queryAny';
|
|
1396
|
+
err.message = `queryAny ${err.code} ${name}`;
|
|
1397
|
+
throw err;
|
|
1398
|
+
}
|
|
1280
1399
|
}
|
|
1281
1400
|
|
|
1282
1401
|
setDefaultResultOrder(dnsOrder) {
|
|
@@ -1330,6 +1449,11 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1330
1449
|
throw err;
|
|
1331
1450
|
}
|
|
1332
1451
|
|
|
1452
|
+
// edge case where c-ares detects "." as start of string
|
|
1453
|
+
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
|
|
1454
|
+
if (name !== '.' && (name.startsWith('.') || name.includes('..')))
|
|
1455
|
+
throw this.constructor.createError(name, rrtype, dns.BADNAME);
|
|
1456
|
+
|
|
1333
1457
|
// purge cache support
|
|
1334
1458
|
let purgeCache;
|
|
1335
1459
|
if (options?.purgeCache) {
|
|
@@ -1712,7 +1836,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1712
1836
|
: obj.certificate_type.toString();
|
|
1713
1837
|
return obj;
|
|
1714
1838
|
} catch (err) {
|
|
1715
|
-
|
|
1839
|
+
this.options.logger.error(err, { name, rrtype, options, answer });
|
|
1716
1840
|
throw err;
|
|
1717
1841
|
}
|
|
1718
1842
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tangerine",
|
|
3
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 and purge support).",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.7",
|
|
5
5
|
"author": "Forward Email (https://forwardemail.net)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/forwardemail/tangerine/issues"
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
"dns-packet": "^5.4.0",
|
|
16
16
|
"dohdec": "^5.0.3",
|
|
17
17
|
"get-stream": "6",
|
|
18
|
+
"hostile": "^1.3.3",
|
|
18
19
|
"ipaddr.js": "^2.0.1",
|
|
19
20
|
"merge-options": "3.0.4",
|
|
20
21
|
"p-map": "4",
|
|
21
22
|
"p-timeout": "4",
|
|
22
23
|
"p-wait-for": "3",
|
|
23
24
|
"port-numbers": "^6.0.1",
|
|
25
|
+
"private-ip": "^3.0.0",
|
|
24
26
|
"punycode": "^2.3.0",
|
|
25
27
|
"semver": "^7.3.8"
|
|
26
28
|
},
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"husky": "^8.0.3",
|
|
40
42
|
"ioredis": "^5.3.1",
|
|
41
43
|
"ioredis-mock": "^8.2.6",
|
|
44
|
+
"is-ci": "^3.0.1",
|
|
42
45
|
"lint-staged": "^13.1.2",
|
|
43
46
|
"lodash": "^4.17.21",
|
|
44
47
|
"nock": "^13.3.0",
|