tangerine 1.5.9 → 2.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.
- package/README.md +211 -120
- package/index.js +374 -172
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</div>
|
|
16
16
|
<hr />
|
|
17
17
|
<div align="center">
|
|
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
|
|
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 v17+ 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
|
|
|
@@ -471,6 +471,19 @@ This purge cache feature is useful for DNS records that have recently changed an
|
|
|
471
471
|
|
|
472
472
|
## Compatibility
|
|
473
473
|
|
|
474
|
+
> \[!NOTE]
|
|
475
|
+
> **Node.js v24+ DNS Record Type Property**
|
|
476
|
+
>
|
|
477
|
+
> Starting with Node.js v24, the native DNS resolver adds a `type` property to certain DNS record objects (MX, CAA, SRV, SOA, and NAPTR records). Tangerine automatically includes this property when running on Node.js v24+ to maintain 1:1 compatibility with the native `dns` module. For example:
|
|
478
|
+
>
|
|
479
|
+
> ```js
|
|
480
|
+
> // Node.js v22 and earlier
|
|
481
|
+
> { exchange: 'smtp.google.com', priority: 10 }
|
|
482
|
+
>
|
|
483
|
+
> // Node.js v24+
|
|
484
|
+
> { exchange: 'smtp.google.com', priority: 10, type: 'MX' }
|
|
485
|
+
> ```
|
|
486
|
+
|
|
474
487
|
The only known compatibility issue is for locally running DNS servers that have wildcard DNS matching.
|
|
475
488
|
|
|
476
489
|
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.
|
|
@@ -514,170 +527,248 @@ BENCHMARK_PROTOCOL="http" BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCH
|
|
|
514
527
|
|
|
515
528
|
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.
|
|
516
529
|
|
|
517
|
-
|
|
530
|
+
---
|
|
518
531
|
|
|
519
|
-
|
|
532
|
+
<!-- BENCHMARK_RESULTS_START -->
|
|
520
533
|
|
|
521
|
-
|
|
522
|
-
|
|
534
|
+
#### Latest Automated Benchmark Results
|
|
535
|
+
|
|
536
|
+
**Last Updated:** 2025-12-21
|
|
537
|
+
|
|
538
|
+
| Node Version | Platform | Arch | Timestamp |
|
|
539
|
+
| ------------ | -------- | ---- | ------------ |
|
|
540
|
+
| v18.20.8 | linux | x64 | Dec 21, 2025 |
|
|
541
|
+
| v20.19.6 | linux | x64 | Dec 21, 2025 |
|
|
542
|
+
| v22.21.1 | linux | x64 | Dec 21, 2025 |
|
|
543
|
+
| v24.12.0 | linux | x64 | Dec 21, 2025 |
|
|
544
|
+
| v25.2.1 | linux | x64 | Dec 21, 2025 |
|
|
523
545
|
|
|
546
|
+
<details>
|
|
547
|
+
<summary>Click to expand detailed benchmark results</summary>
|
|
548
|
+
|
|
549
|
+
##### Node.js v18.20.8
|
|
550
|
+
|
|
551
|
+
**lookup:**
|
|
552
|
+
|
|
553
|
+
```
|
|
524
554
|
Started: lookup
|
|
525
|
-
tangerine.lookup POST with caching using Cloudflare x
|
|
526
|
-
tangerine.lookup POST without caching using Cloudflare x
|
|
527
|
-
tangerine.lookup GET with caching using Cloudflare x
|
|
528
|
-
|
|
529
|
-
dns.promises.lookup with caching using Cloudflare x
|
|
530
|
-
|
|
555
|
+
tangerine.lookup POST with caching using Cloudflare x 757 ops/sec ±195.51% (88 runs sampled)
|
|
556
|
+
tangerine.lookup POST without caching using Cloudflare x 120 ops/sec ±1.43% (81 runs sampled)
|
|
557
|
+
tangerine.lookup GET with caching using Cloudflare x 287,666 ops/sec ±1.59% (87 runs sampled)
|
|
558
|
+
tangerine.lookup GET without caching using Cloudflare x 114 ops/sec ±1.25% (78 runs sampled)
|
|
559
|
+
dns.promises.lookup with caching using Cloudflare x 8,803,764 ops/sec ±0.56% (87 runs sampled)
|
|
560
|
+
dns.promises.lookup without caching using Cloudflare x 3,214 ops/sec ±0.63% (84 runs sampled)
|
|
531
561
|
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**resolve:**
|
|
532
565
|
|
|
566
|
+
```
|
|
533
567
|
Started: resolve
|
|
534
|
-
tangerine.resolve POST with caching using Cloudflare x
|
|
535
|
-
tangerine.resolve POST without caching using Cloudflare x
|
|
536
|
-
tangerine.resolve GET with caching using Cloudflare x 1,
|
|
537
|
-
|
|
538
|
-
tangerine.resolve POST with caching using Google x 1,
|
|
539
|
-
tangerine.resolve POST without caching using Google x
|
|
540
|
-
tangerine.resolve GET with caching using Google x
|
|
541
|
-
tangerine.resolve GET without caching using Google x
|
|
542
|
-
resolver.resolve with caching using Cloudflare x
|
|
543
|
-
|
|
544
|
-
Fastest without caching is:
|
|
568
|
+
tangerine.resolve POST with caching using Cloudflare x 953 ops/sec ±195.82% (88 runs sampled)
|
|
569
|
+
tangerine.resolve POST without caching using Cloudflare x 116 ops/sec ±1.16% (80 runs sampled)
|
|
570
|
+
tangerine.resolve GET with caching using Cloudflare x 1,004,568 ops/sec ±0.26% (87 runs sampled)
|
|
571
|
+
tangerine.resolve GET without caching using Cloudflare x 120 ops/sec ±0.99% (81 runs sampled)
|
|
572
|
+
tangerine.resolve POST with caching using Google x 1,001,702 ops/sec ±0.27% (88 runs sampled)
|
|
573
|
+
tangerine.resolve POST without caching using Google x 126 ops/sec ±1.83% (85 runs sampled)
|
|
574
|
+
tangerine.resolve GET with caching using Google x 998,942 ops/sec ±0.34% (89 runs sampled)
|
|
575
|
+
tangerine.resolve GET without caching using Google x 116 ops/sec ±3.46% (79 runs sampled)
|
|
576
|
+
resolver.resolve with caching using Cloudflare x 6,830,596 ops/sec ±1.01% (86 runs sampled)
|
|
577
|
+
resolver.resolve without caching using Cloudflare x 5.39 ops/sec ±235.57% (7 runs sampled)
|
|
578
|
+
Fastest without caching is: tangerine.resolve POST without caching using Google
|
|
579
|
+
```
|
|
545
580
|
|
|
581
|
+
**reverse:**
|
|
582
|
+
|
|
583
|
+
```
|
|
546
584
|
Started: reverse
|
|
547
|
-
tangerine.reverse GET with caching x
|
|
548
|
-
|
|
549
|
-
resolver.reverse with caching x
|
|
550
|
-
|
|
551
|
-
dns.promises.reverse with caching x
|
|
552
|
-
|
|
553
|
-
|
|
585
|
+
tangerine.reverse GET with caching x 628 ops/sec ±195.51% (84 runs sampled)
|
|
586
|
+
tangerine.reverse GET without caching x 118 ops/sec ±1.12% (80 runs sampled)
|
|
587
|
+
resolver.reverse with caching x 0.10 ops/sec ±0.02% (5 runs sampled)
|
|
588
|
+
resolver.reverse without caching x 0.11 ops/sec ±30.81% (5 runs sampled)
|
|
589
|
+
dns.promises.reverse with caching x 4.56 ops/sec ±196.00% (82 runs sampled)
|
|
590
|
+
dns.promises.reverse without caching x 145 ops/sec ±1.36% (86 runs sampled)
|
|
591
|
+
Fastest without caching is: dns.promises.reverse without caching
|
|
554
592
|
```
|
|
555
593
|
|
|
556
|
-
|
|
594
|
+
##### Node.js v20.19.6
|
|
557
595
|
|
|
558
|
-
|
|
559
|
-
node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
|
|
596
|
+
**lookup:**
|
|
560
597
|
|
|
598
|
+
```
|
|
561
599
|
Started: lookup
|
|
562
|
-
tangerine.lookup POST with caching using Cloudflare x
|
|
563
|
-
tangerine.lookup POST without caching using Cloudflare x
|
|
564
|
-
tangerine.lookup GET with caching using Cloudflare x
|
|
565
|
-
|
|
566
|
-
dns.promises.lookup with caching using Cloudflare x
|
|
567
|
-
|
|
600
|
+
tangerine.lookup POST with caching using Cloudflare x 795 ops/sec ±195.51% (87 runs sampled)
|
|
601
|
+
tangerine.lookup POST without caching using Cloudflare x 306 ops/sec ±1.81% (83 runs sampled)
|
|
602
|
+
tangerine.lookup GET with caching using Cloudflare x 298,127 ops/sec ±0.28% (90 runs sampled)
|
|
603
|
+
tangerine.lookup GET without caching using Cloudflare x 307 ops/sec ±1.94% (83 runs sampled)
|
|
604
|
+
dns.promises.lookup with caching using Cloudflare x 9,509,305 ops/sec ±0.47% (88 runs sampled)
|
|
605
|
+
dns.promises.lookup without caching using Cloudflare x 3,250 ops/sec ±0.73% (85 runs sampled)
|
|
568
606
|
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**resolve:**
|
|
569
610
|
|
|
611
|
+
```
|
|
570
612
|
Started: resolve
|
|
571
|
-
tangerine.resolve POST with caching using Cloudflare x
|
|
572
|
-
tangerine.resolve POST without caching using Cloudflare x
|
|
573
|
-
tangerine.resolve GET with caching using Cloudflare x 1,
|
|
574
|
-
|
|
575
|
-
tangerine.resolve POST with caching using Google x 1,
|
|
576
|
-
tangerine.resolve POST without caching using Google x
|
|
577
|
-
tangerine.resolve GET with caching using Google x 1,
|
|
578
|
-
tangerine.resolve GET without caching using Google x
|
|
579
|
-
resolver.resolve with caching using Cloudflare x
|
|
580
|
-
|
|
581
|
-
Fastest without caching is:
|
|
613
|
+
tangerine.resolve POST with caching using Cloudflare x 1,016 ops/sec ±195.82% (89 runs sampled)
|
|
614
|
+
tangerine.resolve POST without caching using Cloudflare x 303 ops/sec ±3.25% (79 runs sampled)
|
|
615
|
+
tangerine.resolve GET with caching using Cloudflare x 1,071,513 ops/sec ±0.32% (89 runs sampled)
|
|
616
|
+
tangerine.resolve GET without caching using Cloudflare x 339 ops/sec ±1.13% (84 runs sampled)
|
|
617
|
+
tangerine.resolve POST with caching using Google x 1,031,970 ops/sec ±0.28% (90 runs sampled)
|
|
618
|
+
tangerine.resolve POST without caching using Google x 356 ops/sec ±13.06% (79 runs sampled)
|
|
619
|
+
tangerine.resolve GET with caching using Google x 1,032,838 ops/sec ±0.28% (90 runs sampled)
|
|
620
|
+
tangerine.resolve GET without caching using Google x 364 ops/sec ±5.78% (77 runs sampled)
|
|
621
|
+
resolver.resolve with caching using Cloudflare x 7,820,907 ops/sec ±0.65% (86 runs sampled)
|
|
622
|
+
resolver.resolve without caching using Cloudflare x 16.20 ops/sec ±189.31% (79 runs sampled)
|
|
623
|
+
Fastest without caching is: tangerine.resolve GET without caching using Google
|
|
624
|
+
```
|
|
582
625
|
|
|
626
|
+
**reverse:**
|
|
627
|
+
|
|
628
|
+
```
|
|
583
629
|
Started: reverse
|
|
584
|
-
tangerine.reverse GET with caching x
|
|
585
|
-
|
|
586
|
-
resolver.reverse with caching x
|
|
587
|
-
|
|
588
|
-
dns.promises.reverse with caching x
|
|
589
|
-
|
|
590
|
-
|
|
630
|
+
tangerine.reverse GET with caching x 1,002 ops/sec ±195.36% (87 runs sampled)
|
|
631
|
+
tangerine.reverse GET without caching x 323 ops/sec ±1.36% (85 runs sampled)
|
|
632
|
+
resolver.reverse with caching x 78.33 ops/sec ±196.00% (88 runs sampled)
|
|
633
|
+
resolver.reverse without caching x 0.10 ops/sec ±23.12% (5 runs sampled)
|
|
634
|
+
dns.promises.reverse with caching x 7,759,048 ops/sec ±1.08% (79 runs sampled)
|
|
635
|
+
dns.promises.reverse without caching x 1.25 ops/sec ±163.63% (80 runs sampled)
|
|
636
|
+
Fastest without caching is: tangerine.reverse GET without caching
|
|
591
637
|
```
|
|
592
638
|
|
|
593
|
-
|
|
639
|
+
##### Node.js v22.21.1
|
|
594
640
|
|
|
595
|
-
|
|
641
|
+
**lookup:**
|
|
596
642
|
|
|
597
|
-
|
|
643
|
+
```
|
|
644
|
+
Started: lookup
|
|
645
|
+
tangerine.lookup POST with caching using Cloudflare x 330,006 ops/sec ±7.57% (90 runs sampled)
|
|
646
|
+
tangerine.lookup POST without caching using Cloudflare x 287 ops/sec ±1.96% (84 runs sampled)
|
|
647
|
+
tangerine.lookup GET with caching using Cloudflare x 324,567 ops/sec ±0.28% (89 runs sampled)
|
|
648
|
+
tangerine.lookup GET without caching using Cloudflare x 311 ops/sec ±2.03% (79 runs sampled)
|
|
649
|
+
dns.promises.lookup with caching using Cloudflare x 9,729,406 ops/sec ±0.59% (87 runs sampled)
|
|
650
|
+
dns.promises.lookup without caching using Cloudflare x 3,169 ops/sec ±0.81% (87 runs sampled)
|
|
651
|
+
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
652
|
+
```
|
|
598
653
|
|
|
599
|
-
|
|
654
|
+
**resolve:**
|
|
600
655
|
|
|
601
|
-
|
|
656
|
+
```
|
|
657
|
+
Started: resolve
|
|
658
|
+
tangerine.resolve POST with caching using Cloudflare x 1,150,690 ops/sec ±0.43% (90 runs sampled)
|
|
659
|
+
tangerine.resolve POST without caching using Cloudflare x 284 ops/sec ±1.36% (82 runs sampled)
|
|
660
|
+
tangerine.resolve GET with caching using Cloudflare x 1,123,967 ops/sec ±0.24% (89 runs sampled)
|
|
661
|
+
tangerine.resolve GET without caching using Cloudflare x 335 ops/sec ±1.95% (80 runs sampled)
|
|
662
|
+
tangerine.resolve POST with caching using Google x 1,125,905 ops/sec ±0.21% (89 runs sampled)
|
|
663
|
+
tangerine.resolve POST without caching using Google x 318 ops/sec ±11.57% (77 runs sampled)
|
|
664
|
+
tangerine.resolve GET with caching using Google x 1,128,787 ops/sec ±0.22% (89 runs sampled)
|
|
665
|
+
tangerine.resolve GET without caching using Google x 462 ops/sec ±5.05% (80 runs sampled)
|
|
666
|
+
resolver.resolve with caching using Cloudflare x 8,204,435 ops/sec ±0.57% (86 runs sampled)
|
|
667
|
+
resolver.resolve without caching using Cloudflare x 55.98 ops/sec ±172.36% (78 runs sampled)
|
|
668
|
+
Fastest without caching is: tangerine.resolve GET without caching using Google
|
|
669
|
+
```
|
|
602
670
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
671
|
+
**reverse:**
|
|
672
|
+
|
|
673
|
+
```
|
|
674
|
+
spawnSync /bin/sh ETIMEDOUT
|
|
675
|
+
```
|
|
606
676
|
|
|
607
|
-
|
|
677
|
+
##### Node.js v24.12.0
|
|
608
678
|
|
|
679
|
+
**lookup:**
|
|
680
|
+
|
|
681
|
+
```
|
|
609
682
|
Started: lookup
|
|
610
|
-
tangerine.lookup POST with caching using Cloudflare x 1,
|
|
611
|
-
tangerine.lookup POST without caching using Cloudflare x
|
|
612
|
-
tangerine.lookup GET with caching using Cloudflare x
|
|
613
|
-
|
|
614
|
-
dns.promises.lookup with caching using Cloudflare x
|
|
615
|
-
|
|
683
|
+
tangerine.lookup POST with caching using Cloudflare x 1,775 ops/sec ±194.98% (90 runs sampled)
|
|
684
|
+
tangerine.lookup POST without caching using Cloudflare x 295 ops/sec ±10.41% (81 runs sampled)
|
|
685
|
+
tangerine.lookup GET with caching using Cloudflare x 328,666 ops/sec ±0.25% (90 runs sampled)
|
|
686
|
+
tangerine.lookup GET without caching using Cloudflare x 318 ops/sec ±2.96% (80 runs sampled)
|
|
687
|
+
dns.promises.lookup with caching using Cloudflare x 10,219,888 ops/sec ±0.98% (84 runs sampled)
|
|
688
|
+
dns.promises.lookup without caching using Cloudflare x 3,280 ops/sec ±0.70% (84 runs sampled)
|
|
616
689
|
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
690
|
+
```
|
|
617
691
|
|
|
618
|
-
|
|
619
|
-
tangerine.resolve POST with caching using Cloudflare x 1,005 ops/sec ±195.93% (91 runs sampled)
|
|
620
|
-
tangerine.resolve POST without caching using Cloudflare x 55.52 ops/sec ±46.26% (57 runs sampled)
|
|
621
|
-
tangerine.resolve GET with caching using Cloudflare x 2,879,865 ops/sec ±0.35% (86 runs sampled)
|
|
622
|
-
+tangerine.resolve GET without caching using Cloudflare x 71.11 ops/sec ±2.94% (74 runs sampled)
|
|
623
|
-
tangerine.resolve POST with caching using Google x 1,292 ops/sec ±195.91% (88 runs sampled)
|
|
624
|
-
tangerine.resolve POST without caching using Google x 36.88 ops/sec ±41.76% (53 runs sampled)
|
|
625
|
-
tangerine.resolve GET with caching using Google x 2,885,428 ops/sec ±0.22% (88 runs sampled)
|
|
626
|
-
tangerine.resolve GET without caching using Google x 70.38 ops/sec ±3.72% (68 runs sampled)
|
|
627
|
-
resolver.resolve with caching using Cloudflare x 10,645,813 ops/sec ±0.23% (91 runs sampled)
|
|
628
|
-
-resolver.resolve without caching using Cloudflare x 71.80 ops/sec ±2.84% (67 runs sampled)
|
|
629
|
-
+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
|
|
692
|
+
**resolve:**
|
|
630
693
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
694
|
+
```
|
|
695
|
+
Started: resolve
|
|
696
|
+
tangerine.resolve POST with caching using Cloudflare x 1,164,729 ops/sec ±0.27% (90 runs sampled)
|
|
697
|
+
tangerine.resolve POST without caching using Cloudflare x 316 ops/sec ±1.55% (82 runs sampled)
|
|
698
|
+
tangerine.resolve GET with caching using Cloudflare x 1,135,170 ops/sec ±0.25% (90 runs sampled)
|
|
699
|
+
tangerine.resolve GET without caching using Cloudflare x 355 ops/sec ±1.42% (83 runs sampled)
|
|
700
|
+
tangerine.resolve POST with caching using Google x 1,120,904 ops/sec ±0.27% (90 runs sampled)
|
|
701
|
+
tangerine.resolve POST without caching using Google x 427 ops/sec ±6.35% (78 runs sampled)
|
|
702
|
+
tangerine.resolve GET with caching using Google x 1,104,301 ops/sec ±0.50% (90 runs sampled)
|
|
703
|
+
tangerine.resolve GET without caching using Google x 418 ops/sec ±2.35% (79 runs sampled)
|
|
704
|
+
resolver.resolve with caching using Cloudflare x 8,667,172 ops/sec ±0.64% (87 runs sampled)
|
|
705
|
+
resolver.resolve without caching using Cloudflare x 0.14 ops/sec ±85.32% (5 runs sampled)
|
|
706
|
+
Fastest without caching is: tangerine.resolve GET without caching using Google
|
|
639
707
|
```
|
|
640
708
|
|
|
641
|
-
|
|
709
|
+
**reverse:**
|
|
642
710
|
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
|
|
711
|
+
```
|
|
712
|
+
spawnSync /bin/sh ETIMEDOUT
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
##### Node.js v25.2.1
|
|
646
716
|
|
|
647
|
-
|
|
717
|
+
**lookup:**
|
|
648
718
|
|
|
719
|
+
```
|
|
649
720
|
Started: lookup
|
|
650
|
-
tangerine.lookup POST with caching using Cloudflare x 1,
|
|
651
|
-
tangerine.lookup POST without caching using Cloudflare x
|
|
652
|
-
tangerine.lookup GET with caching using Cloudflare x
|
|
653
|
-
|
|
654
|
-
dns.promises.lookup with caching using Cloudflare x
|
|
655
|
-
|
|
721
|
+
tangerine.lookup POST with caching using Cloudflare x 1,504 ops/sec ±195.19% (89 runs sampled)
|
|
722
|
+
tangerine.lookup POST without caching using Cloudflare x 118 ops/sec ±2.46% (81 runs sampled)
|
|
723
|
+
tangerine.lookup GET with caching using Cloudflare x 341,247 ops/sec ±0.36% (90 runs sampled)
|
|
724
|
+
tangerine.lookup GET without caching using Cloudflare x 119 ops/sec ±6.76% (84 runs sampled)
|
|
725
|
+
dns.promises.lookup with caching using Cloudflare x 10,273,047 ops/sec ±1.94% (84 runs sampled)
|
|
726
|
+
dns.promises.lookup without caching using Cloudflare x 3,255 ops/sec ±1.11% (86 runs sampled)
|
|
656
727
|
Fastest without caching is: dns.promises.lookup without caching using Cloudflare
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**resolve:**
|
|
657
731
|
|
|
732
|
+
```
|
|
658
733
|
Started: resolve
|
|
659
|
-
tangerine.resolve POST with caching using Cloudflare x
|
|
660
|
-
tangerine.resolve POST without caching using Cloudflare x
|
|
661
|
-
tangerine.resolve GET with caching using Cloudflare x
|
|
662
|
-
|
|
663
|
-
tangerine.resolve POST with caching using Google x 1,
|
|
664
|
-
tangerine.resolve POST without caching using Google x
|
|
665
|
-
tangerine.resolve GET with caching using Google x
|
|
666
|
-
tangerine.resolve GET without caching using Google x
|
|
667
|
-
resolver.resolve with caching using Cloudflare x
|
|
668
|
-
|
|
669
|
-
|
|
734
|
+
tangerine.resolve POST with caching using Cloudflare x 1,200,168 ops/sec ±1.70% (90 runs sampled)
|
|
735
|
+
tangerine.resolve POST without caching using Cloudflare x 132 ops/sec ±0.46% (88 runs sampled)
|
|
736
|
+
tangerine.resolve GET with caching using Cloudflare x 1,179,037 ops/sec ±0.31% (89 runs sampled)
|
|
737
|
+
tangerine.resolve GET without caching using Cloudflare x 120 ops/sec ±0.89% (81 runs sampled)
|
|
738
|
+
tangerine.resolve POST with caching using Google x 1,159,414 ops/sec ±2.50% (89 runs sampled)
|
|
739
|
+
tangerine.resolve POST without caching using Google x 122 ops/sec ±3.58% (83 runs sampled)
|
|
740
|
+
tangerine.resolve GET with caching using Google x 1,166,324 ops/sec ±3.01% (88 runs sampled)
|
|
741
|
+
tangerine.resolve GET without caching using Google x 120 ops/sec ±0.48% (81 runs sampled)
|
|
742
|
+
resolver.resolve with caching using Cloudflare x 8,890,562 ops/sec ±2.49% (82 runs sampled)
|
|
743
|
+
resolver.resolve without caching using Cloudflare x 147 ops/sec ±1.08% (86 runs sampled)
|
|
744
|
+
Fastest without caching is: resolver.resolve without caching using Cloudflare
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**reverse:**
|
|
670
748
|
|
|
749
|
+
```
|
|
671
750
|
Started: reverse
|
|
672
|
-
tangerine.reverse GET with caching x
|
|
673
|
-
|
|
674
|
-
resolver.reverse with caching x
|
|
675
|
-
|
|
676
|
-
dns.promises.reverse with caching x
|
|
677
|
-
|
|
678
|
-
|
|
751
|
+
tangerine.reverse GET with caching x 342,190 ops/sec ±8.40% (90 runs sampled)
|
|
752
|
+
tangerine.reverse GET without caching x 128 ops/sec ±0.83% (86 runs sampled)
|
|
753
|
+
resolver.reverse with caching x 9,145,218 ops/sec ±0.55% (85 runs sampled)
|
|
754
|
+
resolver.reverse without caching x 155 ops/sec ±0.63% (82 runs sampled)
|
|
755
|
+
dns.promises.reverse with caching x 9,157,969 ops/sec ±0.45% (89 runs sampled)
|
|
756
|
+
dns.promises.reverse without caching x 5.11 ops/sec ±189.52% (76 runs sampled)
|
|
757
|
+
Fastest without caching is: resolver.reverse without caching, dns.promises.reverse without caching
|
|
679
758
|
```
|
|
680
759
|
|
|
760
|
+
</details>
|
|
761
|
+
|
|
762
|
+
<!-- BENCHMARK_RESULTS_END -->
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
The benchmarks above are automatically updated daily via `.github/workflows/daily-benchmarks.yml`.
|
|
767
|
+
|
|
768
|
+
You can also [run the benchmarks yourself](#benchmarks).
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
681
772
|
Also see this [write-up](https://samknows.com/blog/dns-over-https-performance) on UDP-based DNS versus DNS over HTTPS ("DoH") benchmarks.
|
|
682
773
|
|
|
683
774
|
**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).
|
package/index.js
CHANGED
|
@@ -17,7 +17,6 @@ const pMap = require('p-map');
|
|
|
17
17
|
const pWaitFor = require('p-wait-for');
|
|
18
18
|
const packet = require('dns-packet');
|
|
19
19
|
const semver = require('semver');
|
|
20
|
-
const structuredClone = require('@ungap/structured-clone').default;
|
|
21
20
|
const { getService } = require('port-numbers');
|
|
22
21
|
const pkg = require('./package.json');
|
|
23
22
|
|
|
@@ -50,6 +49,14 @@ for (const line of hosts) {
|
|
|
50
49
|
HOSTS.push({ ip, hosts });
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
// Node.js v24+ adds a 'type' property to certain DNS record objects (MX, CAA, SRV, NAPTR)
|
|
53
|
+
// We need to match this behavior for compatibility
|
|
54
|
+
// See: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V24.md
|
|
55
|
+
const NODE_MAJOR_VERSION = Number.parseInt(
|
|
56
|
+
process.versions.node.split('.')[0],
|
|
57
|
+
10
|
|
58
|
+
);
|
|
59
|
+
|
|
53
60
|
// <https://github.com/szmarczak/cacheable-lookup/pull/76>
|
|
54
61
|
class Tangerine extends dns.promises.Resolver {
|
|
55
62
|
static HOSTFILE = HOSTFILE;
|
|
@@ -633,8 +640,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
633
640
|
// remap and perform syscall
|
|
634
641
|
err.syscall = 'getaddrinfo';
|
|
635
642
|
err.message = err.message.replace('query', 'getaddrinfo');
|
|
636
|
-
|
|
637
|
-
|
|
643
|
+
// errno -3007 is for invalid hostnames (like ".")
|
|
644
|
+
err.errno = -3007;
|
|
638
645
|
throw err;
|
|
639
646
|
}
|
|
640
647
|
|
|
@@ -735,19 +742,27 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
735
742
|
}
|
|
736
743
|
}
|
|
737
744
|
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
745
|
+
if (answers.length === 0 && errors.length > 0) {
|
|
746
|
+
// For lookup, if any error is ENOTFOUND, return ENOTFOUND
|
|
747
|
+
// For .localhost subdomains, BADNAME, and ENODATA errors, return ENOTFOUND
|
|
748
|
+
// This matches c-ares behavior for lookup
|
|
749
|
+
let errorCode = errors[0].code;
|
|
750
|
+
const hasNotFound = errors.some((e) => e.code === dns.NOTFOUND);
|
|
751
|
+
if (
|
|
752
|
+
hasNotFound ||
|
|
753
|
+
errorCode === dns.BADNAME ||
|
|
754
|
+
errorCode === dns.NODATA ||
|
|
755
|
+
lower.endsWith('.localhost') ||
|
|
756
|
+
lower.endsWith('.localhost.')
|
|
757
|
+
) {
|
|
758
|
+
errorCode = dns.NOTFOUND;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const err = this.constructor.createError(name, '', errorCode);
|
|
748
762
|
// remap and perform syscall
|
|
749
763
|
err.syscall = 'getaddrinfo';
|
|
750
764
|
err.message = err.message.replace('query', 'getaddrinfo');
|
|
765
|
+
// errno -3008 is the standard ENOTFOUND errno
|
|
751
766
|
err.errno = -3008;
|
|
752
767
|
throw err;
|
|
753
768
|
}
|
|
@@ -936,7 +951,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
936
951
|
for (const rule of this.constructor.HOSTS) {
|
|
937
952
|
if (rule.ip === ip) {
|
|
938
953
|
match = true;
|
|
939
|
-
|
|
954
|
+
// Include all hosts (c-ares includes all hosts from /etc/hosts)
|
|
955
|
+
for (const host of rule.hosts) {
|
|
940
956
|
answers.add(host);
|
|
941
957
|
}
|
|
942
958
|
}
|
|
@@ -1030,7 +1046,32 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1030
1046
|
// (this means it's a drop-in replacement for `dns`)
|
|
1031
1047
|
// <https://github.com/nodejs/node/blob/9bbde3d7baef584f14569ef79f116e9d288c7aaa/lib/internal/dns/utils.js#L87-L95>
|
|
1032
1048
|
getServers() {
|
|
1033
|
-
|
|
1049
|
+
// Normalize IPv6 addresses to match native dns.Resolver behavior
|
|
1050
|
+
// e.g., '[::0]' -> '::' but '[2001:db8::1]:8080' stays as-is
|
|
1051
|
+
return [...this.options.servers].map((server) => {
|
|
1052
|
+
// Check if it's a bracketed IPv6 address
|
|
1053
|
+
if (server.startsWith('[')) {
|
|
1054
|
+
// Extract the IPv6 address and optional port
|
|
1055
|
+
const match = server.match(/^\[([^\]]+)](?::(\d+))?$/);
|
|
1056
|
+
if (match) {
|
|
1057
|
+
const ipv6 = match[1];
|
|
1058
|
+
const port = match[2];
|
|
1059
|
+
// Normalize the IPv6 address using ipaddr.js
|
|
1060
|
+
try {
|
|
1061
|
+
const parsed = ipaddr.parse(ipv6);
|
|
1062
|
+
const normalized = parsed.toString();
|
|
1063
|
+
// If there's a port, keep the bracket format like native DNS does
|
|
1064
|
+
// Otherwise just return the normalized address
|
|
1065
|
+
return port ? `[${normalized}]:${port}` : normalized;
|
|
1066
|
+
} catch {
|
|
1067
|
+
// If parsing fails, return as-is
|
|
1068
|
+
return server;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return server;
|
|
1074
|
+
});
|
|
1034
1075
|
}
|
|
1035
1076
|
|
|
1036
1077
|
//
|
|
@@ -1072,7 +1113,9 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1072
1113
|
}
|
|
1073
1114
|
|
|
1074
1115
|
debug('request', { url, options });
|
|
1075
|
-
const t = setTimeout(() =>
|
|
1116
|
+
const t = setTimeout(() => {
|
|
1117
|
+
if (!abortController?.signal?.aborted) abortController.abort('ETIMEOUT');
|
|
1118
|
+
}, timeout);
|
|
1076
1119
|
const response = await this.request(url, options);
|
|
1077
1120
|
clearTimeout(t);
|
|
1078
1121
|
return response;
|
|
@@ -1216,7 +1259,12 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1216
1259
|
if (errors.length > 0) throw this.constructor.combineErrors(errors);
|
|
1217
1260
|
// if no errors and no response
|
|
1218
1261
|
// that must indicate that it was aborted
|
|
1219
|
-
|
|
1262
|
+
// Check if this was a timeout abort (reason will be 'ETIMEOUT')
|
|
1263
|
+
const abortCode =
|
|
1264
|
+
abortController?.signal?.reason === 'ETIMEOUT'
|
|
1265
|
+
? dns.TIMEOUT
|
|
1266
|
+
: dns.CANCELLED;
|
|
1267
|
+
throw this.constructor.createError(name, rrtype, abortCode);
|
|
1220
1268
|
}
|
|
1221
1269
|
|
|
1222
1270
|
// without logging an error here, one might not know
|
|
@@ -1233,10 +1281,20 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1233
1281
|
} catch (_err) {
|
|
1234
1282
|
debug(_err, { name, rrtype, ecsSubnet });
|
|
1235
1283
|
if (this.options.returnHTTPErrors) throw _err;
|
|
1284
|
+
// Check if this was a timeout abort (reason will be 'ETIMEOUT')
|
|
1285
|
+
// or if the error name is AbortError with a numeric code (undici behavior)
|
|
1286
|
+
let errorCode = _err.code;
|
|
1287
|
+
if (
|
|
1288
|
+
(_err.name === 'AbortError' || typeof _err.code === 'number') &&
|
|
1289
|
+
abortController?.signal?.reason === 'ETIMEOUT'
|
|
1290
|
+
) {
|
|
1291
|
+
errorCode = 'ETIMEOUT';
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1236
1294
|
const err = this.constructor.createError(
|
|
1237
1295
|
name,
|
|
1238
1296
|
rrtype,
|
|
1239
|
-
|
|
1297
|
+
errorCode,
|
|
1240
1298
|
_err.errno
|
|
1241
1299
|
);
|
|
1242
1300
|
// then map it to dns.CONNREFUSED
|
|
@@ -1247,6 +1305,23 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1247
1305
|
}
|
|
1248
1306
|
}
|
|
1249
1307
|
|
|
1308
|
+
// #createAbortController and #releaseAbortController manage all AbortController instances created by this resolver
|
|
1309
|
+
// - to support cancel() and
|
|
1310
|
+
// - to avoid keeping references in this.abortControllers after a query is finished (which would create a memory leak)
|
|
1311
|
+
#createAbortController() {
|
|
1312
|
+
const abortController = new AbortController();
|
|
1313
|
+
this.abortControllers.add(abortController);
|
|
1314
|
+
return abortController;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
#releaseAbortController(abortController) {
|
|
1318
|
+
try {
|
|
1319
|
+
this.abortControllers.delete(abortController);
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
this.options.logger.debug(err);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1250
1325
|
// Cancel all outstanding DNS queries made by this resolver
|
|
1251
1326
|
// NOTE: callbacks not currently called with ECANCELLED (prob need to alter got options)
|
|
1252
1327
|
// (instead they are called with "ABORT_ERR"; see ABORT_ERROR_CODES)
|
|
@@ -1264,120 +1339,124 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1264
1339
|
|
|
1265
1340
|
#resolveByType(name, options = {}, parentAbortController) {
|
|
1266
1341
|
return async (type) => {
|
|
1267
|
-
const abortController =
|
|
1268
|
-
this.abortControllers.add(abortController);
|
|
1269
|
-
abortController.signal.addEventListener(
|
|
1270
|
-
'abort',
|
|
1271
|
-
() => {
|
|
1272
|
-
this.abortControllers.delete(abortController);
|
|
1273
|
-
},
|
|
1274
|
-
{ once: true }
|
|
1275
|
-
);
|
|
1276
|
-
parentAbortController.signal.addEventListener(
|
|
1277
|
-
'abort',
|
|
1278
|
-
() => {
|
|
1279
|
-
try {
|
|
1280
|
-
abortController.abort('Parent abort controller aborted');
|
|
1281
|
-
} catch (err) {
|
|
1282
|
-
this.options.logger.debug(err);
|
|
1283
|
-
}
|
|
1284
|
-
},
|
|
1285
|
-
{ once: true }
|
|
1286
|
-
);
|
|
1287
|
-
// wrap with try/catch because ENODATA shouldn't cause errors
|
|
1342
|
+
const abortController = this.#createAbortController();
|
|
1288
1343
|
try {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1344
|
+
parentAbortController.signal.addEventListener(
|
|
1345
|
+
'abort',
|
|
1346
|
+
() => {
|
|
1347
|
+
try {
|
|
1348
|
+
abortController.abort('Parent abort controller aborted');
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
this.options.logger.debug(err);
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
{ once: true }
|
|
1354
|
+
);
|
|
1355
|
+
// wrap with try/catch because ENODATA shouldn't cause errors
|
|
1356
|
+
try {
|
|
1357
|
+
switch (type) {
|
|
1358
|
+
case 'A': {
|
|
1359
|
+
const result = await this.resolve4(
|
|
1360
|
+
name,
|
|
1361
|
+
{ ...options, ttl: true },
|
|
1362
|
+
abortController
|
|
1363
|
+
);
|
|
1364
|
+
return result.map((r) => ({ type, ...r }));
|
|
1365
|
+
}
|
|
1298
1366
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1367
|
+
case 'AAAA': {
|
|
1368
|
+
const result = await this.resolve6(
|
|
1369
|
+
name,
|
|
1370
|
+
{ ...options, ttl: true },
|
|
1371
|
+
abortController
|
|
1372
|
+
);
|
|
1373
|
+
return result.map((r) => ({ type, ...r }));
|
|
1374
|
+
}
|
|
1307
1375
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1376
|
+
case 'CNAME': {
|
|
1377
|
+
const result = await this.resolveCname(
|
|
1378
|
+
name,
|
|
1379
|
+
options,
|
|
1380
|
+
abortController
|
|
1381
|
+
);
|
|
1382
|
+
return result.map((value) => ({ type, value }));
|
|
1383
|
+
}
|
|
1316
1384
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1385
|
+
case 'MX': {
|
|
1386
|
+
const result = await this.resolveMx(
|
|
1387
|
+
name,
|
|
1388
|
+
options,
|
|
1389
|
+
abortController
|
|
1390
|
+
);
|
|
1391
|
+
return result.map((r) => ({ type, ...r }));
|
|
1392
|
+
}
|
|
1321
1393
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1394
|
+
case 'NAPTR': {
|
|
1395
|
+
const result = await this.resolveNaptr(
|
|
1396
|
+
name,
|
|
1397
|
+
options,
|
|
1398
|
+
abortController
|
|
1399
|
+
);
|
|
1400
|
+
return result.map((value) => ({ type, value }));
|
|
1401
|
+
}
|
|
1330
1402
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1403
|
+
case 'NS': {
|
|
1404
|
+
const result = await this.resolveNs(
|
|
1405
|
+
name,
|
|
1406
|
+
options,
|
|
1407
|
+
abortController
|
|
1408
|
+
);
|
|
1409
|
+
return result.map((value) => ({ type, value }));
|
|
1410
|
+
}
|
|
1335
1411
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1412
|
+
case 'PTR': {
|
|
1413
|
+
const result = await this.resolvePtr(
|
|
1414
|
+
name,
|
|
1415
|
+
options,
|
|
1416
|
+
abortController
|
|
1417
|
+
);
|
|
1418
|
+
return result.map((value) => ({ type, value }));
|
|
1419
|
+
}
|
|
1344
1420
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1421
|
+
case 'SOA': {
|
|
1422
|
+
const result = await this.resolveSoa(
|
|
1423
|
+
name,
|
|
1424
|
+
options,
|
|
1425
|
+
abortController
|
|
1426
|
+
);
|
|
1427
|
+
return { type, ...result };
|
|
1428
|
+
}
|
|
1353
1429
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1430
|
+
case 'SRV': {
|
|
1431
|
+
const result = await this.resolveSrv(
|
|
1432
|
+
name,
|
|
1433
|
+
options,
|
|
1434
|
+
abortController
|
|
1435
|
+
);
|
|
1436
|
+
return result.map((value) => ({ type, value }));
|
|
1437
|
+
}
|
|
1362
1438
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1439
|
+
case 'TXT': {
|
|
1440
|
+
const result = await this.resolveTxt(
|
|
1441
|
+
name,
|
|
1442
|
+
options,
|
|
1443
|
+
abortController
|
|
1444
|
+
);
|
|
1445
|
+
return result.map((entries) => ({ type, entries }));
|
|
1446
|
+
}
|
|
1371
1447
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1448
|
+
default: {
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1374
1451
|
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
debug(err);
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
debug(err);
|
|
1378
1454
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1455
|
+
if (err.code === dns.NODATA) return;
|
|
1456
|
+
throw err;
|
|
1457
|
+
}
|
|
1458
|
+
} finally {
|
|
1459
|
+
this.#releaseAbortController(abortController);
|
|
1381
1460
|
}
|
|
1382
1461
|
};
|
|
1383
1462
|
}
|
|
@@ -1393,23 +1472,22 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1393
1472
|
// <https://gist.github.com/andrewcourtice/ef1b8f14935b409cfe94901558ba5594#file-task-ts-L37>
|
|
1394
1473
|
// <https://github.com/nodejs/undici/blob/0badd390ad5aa531a66aacee54da664468aa1577/lib/api/api-fetch/request.js#L280-L295>
|
|
1395
1474
|
// <https://github.com/nodejs/node/issues/40849>
|
|
1475
|
+
let mustReleaseAbortController = false;
|
|
1396
1476
|
if (!abortController) {
|
|
1397
|
-
abortController =
|
|
1398
|
-
|
|
1399
|
-
abortController.signal.addEventListener(
|
|
1400
|
-
'abort',
|
|
1401
|
-
() => {
|
|
1402
|
-
this.abortControllers.delete(abortController);
|
|
1403
|
-
},
|
|
1404
|
-
{ once: true }
|
|
1405
|
-
);
|
|
1477
|
+
abortController = this.#createAbortController();
|
|
1478
|
+
mustReleaseAbortController = true;
|
|
1406
1479
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1480
|
+
try {
|
|
1481
|
+
// <https://github.com/nodejs/undici/pull/1910/commits/7615308a92d3c8c90081fb99c55ab8bd59212396>
|
|
1482
|
+
setMaxListeners(
|
|
1483
|
+
getEventListeners(abortController.signal, 'abort').length +
|
|
1484
|
+
this.constructor.ANY_TYPES.length,
|
|
1485
|
+
abortController.signal
|
|
1486
|
+
);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
this.#releaseAbortController(abortController);
|
|
1489
|
+
throw err;
|
|
1490
|
+
}
|
|
1413
1491
|
}
|
|
1414
1492
|
|
|
1415
1493
|
try {
|
|
@@ -1427,6 +1505,10 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1427
1505
|
err.syscall = 'queryAny';
|
|
1428
1506
|
err.message = `queryAny ${err.code} ${name}`;
|
|
1429
1507
|
throw err;
|
|
1508
|
+
} finally {
|
|
1509
|
+
if (mustReleaseAbortController) {
|
|
1510
|
+
this.#releaseAbortController(abortController);
|
|
1511
|
+
}
|
|
1430
1512
|
}
|
|
1431
1513
|
}
|
|
1432
1514
|
|
|
@@ -1553,11 +1635,80 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1553
1635
|
throw err;
|
|
1554
1636
|
}
|
|
1555
1637
|
|
|
1556
|
-
// edge case where c-ares detects "." as start of string
|
|
1638
|
+
// edge case where c-ares detects "." as start of string or malformed hostnames
|
|
1557
1639
|
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
|
|
1558
|
-
if (
|
|
1640
|
+
if (
|
|
1641
|
+
(name !== '.' && (name.startsWith('.') || name.includes('..'))) ||
|
|
1642
|
+
name.includes(',')
|
|
1643
|
+
)
|
|
1559
1644
|
throw this.constructor.createError(name, rrtype, dns.BADNAME);
|
|
1560
1645
|
|
|
1646
|
+
// IP addresses passed to resolve should return appropriate errors (matches c-ares behavior)
|
|
1647
|
+
// IPv6 addresses return EBADNAME for all record types
|
|
1648
|
+
// IPv4 addresses return ENOTFOUND for A records, ENODATA for AAAA records
|
|
1649
|
+
// <https://github.com/c-ares/c-ares/blob/main/src/lib/ares_getaddrinfo.c>
|
|
1650
|
+
if (isIPv6(name))
|
|
1651
|
+
throw this.constructor.createError(name, rrtype, dns.BADNAME);
|
|
1652
|
+
if (isIPv4(name)) {
|
|
1653
|
+
// For AAAA queries on IPv4 addresses, return ENODATA (no IPv6 data for IPv4 address)
|
|
1654
|
+
if (rrtype === 'AAAA')
|
|
1655
|
+
throw this.constructor.createError(name, rrtype, dns.NODATA);
|
|
1656
|
+
throw this.constructor.createError(name, rrtype, dns.NOTFOUND);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// Handle localhost and .localhost domains for A and AAAA records
|
|
1660
|
+
// This mirrors c-ares behavior which returns results from /etc/hosts
|
|
1661
|
+
// <https://www.rfc-editor.org/rfc/rfc6761.html#section-6.3>
|
|
1662
|
+
const lower = name.toLowerCase();
|
|
1663
|
+
if (
|
|
1664
|
+
(rrtype === 'A' || rrtype === 'AAAA') &&
|
|
1665
|
+
(lower === 'localhost' ||
|
|
1666
|
+
lower === 'localhost.' ||
|
|
1667
|
+
lower.endsWith('.localhost') ||
|
|
1668
|
+
lower.endsWith('.localhost.'))
|
|
1669
|
+
) {
|
|
1670
|
+
// Check /etc/hosts first
|
|
1671
|
+
let resolve4;
|
|
1672
|
+
let resolve6;
|
|
1673
|
+
for (const rule of this.constructor.HOSTS) {
|
|
1674
|
+
if (rule.hosts.every((h) => h.toLowerCase() !== lower)) continue;
|
|
1675
|
+
const type = isIP(rule.ip);
|
|
1676
|
+
if (!resolve4 && type === 4) {
|
|
1677
|
+
resolve4 = [rule.ip];
|
|
1678
|
+
} else if (!resolve6 && type === 6) {
|
|
1679
|
+
resolve6 = [rule.ip];
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Default fallback for localhost
|
|
1684
|
+
if (lower === 'localhost' || lower === 'localhost.') {
|
|
1685
|
+
resolve4 ||= ['127.0.0.1'];
|
|
1686
|
+
resolve6 ||= ['::1'];
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (rrtype === 'A' && resolve4) {
|
|
1690
|
+
if (options?.ttl)
|
|
1691
|
+
return resolve4.map((address) => ({ ttl: 0, address }));
|
|
1692
|
+
return resolve4;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (rrtype === 'AAAA' && resolve6) {
|
|
1696
|
+
if (options?.ttl)
|
|
1697
|
+
return resolve6.map((address) => ({ ttl: 0, address }));
|
|
1698
|
+
return resolve6;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// If no matching records found, throw appropriate error
|
|
1702
|
+
// For A records on subdomains of .localhost, throw ENOTFOUND
|
|
1703
|
+
// For AAAA records on subdomains of .localhost, throw ENODATA
|
|
1704
|
+
// This matches c-ares behavior
|
|
1705
|
+
if (rrtype === 'A') {
|
|
1706
|
+
throw this.constructor.createError(name, rrtype, dns.NOTFOUND);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
throw this.constructor.createError(name, rrtype, dns.NODATA);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1561
1712
|
// purge cache support
|
|
1562
1713
|
let purgeCache;
|
|
1563
1714
|
if (options?.purgeCache) {
|
|
@@ -1619,7 +1770,6 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1619
1770
|
const diff = data.ttl - ttl;
|
|
1620
1771
|
|
|
1621
1772
|
for (let i = 0; i < data.answers.length; i++) {
|
|
1622
|
-
// eslint-disable-next-line max-depth
|
|
1623
1773
|
if (typeof data.answers[i].ttl === 'number') {
|
|
1624
1774
|
// subtract ttl from answer
|
|
1625
1775
|
data.answers[i].ttl = Math.round(data.answers[i].ttl - diff);
|
|
@@ -1673,20 +1823,20 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1673
1823
|
if (this.options.cache && result) {
|
|
1674
1824
|
debug(`cached result found for "${key}"`);
|
|
1675
1825
|
} else {
|
|
1826
|
+
let mustReleaseAbortController = false;
|
|
1676
1827
|
if (!abortController) {
|
|
1677
|
-
abortController =
|
|
1678
|
-
|
|
1679
|
-
abortController.signal.addEventListener(
|
|
1680
|
-
'abort',
|
|
1681
|
-
() => {
|
|
1682
|
-
this.abortControllers.delete(abortController);
|
|
1683
|
-
},
|
|
1684
|
-
{ once: true }
|
|
1685
|
-
);
|
|
1828
|
+
abortController = this.#createAbortController();
|
|
1829
|
+
mustReleaseAbortController = true;
|
|
1686
1830
|
}
|
|
1687
1831
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1832
|
+
try {
|
|
1833
|
+
// setImmediate(() => this.cancel());
|
|
1834
|
+
result = await this.#query(name, rrtype, ecsSubnet, abortController);
|
|
1835
|
+
} finally {
|
|
1836
|
+
if (mustReleaseAbortController) {
|
|
1837
|
+
this.#releaseAbortController(abortController);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1690
1840
|
}
|
|
1691
1841
|
|
|
1692
1842
|
// <https://github.com/m13253/dns-over-https/blob/2e36b4ebcdb8a1a102ea86370d7f8b1f1e72380a/json-dns/response.go#L50-L74>
|
|
@@ -1810,10 +1960,24 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1810
1960
|
case 'CAA': {
|
|
1811
1961
|
// CA authorization records `dnsPromises.resolveCaa()`
|
|
1812
1962
|
// <https://www.rfc-editor.org/rfc/rfc6844#section-3>
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1963
|
+
// Node.js v24+ adds 'type' property to CAA records
|
|
1964
|
+
return result.answers.map((a) => {
|
|
1965
|
+
const record = {
|
|
1966
|
+
critical: a.data.flags,
|
|
1967
|
+
[a.data.tag]: a.data.value
|
|
1968
|
+
};
|
|
1969
|
+
// Add type property for Node.js v24+ compatibility
|
|
1970
|
+
if (NODE_MAJOR_VERSION >= 24) {
|
|
1971
|
+
// Insert type after critical to match native DNS order
|
|
1972
|
+
return {
|
|
1973
|
+
critical: record.critical,
|
|
1974
|
+
type: 'CAA',
|
|
1975
|
+
[a.data.tag]: a.data.value
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
return record;
|
|
1980
|
+
});
|
|
1817
1981
|
}
|
|
1818
1982
|
|
|
1819
1983
|
case 'CNAME': {
|
|
@@ -1823,15 +1987,34 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1823
1987
|
|
|
1824
1988
|
case 'MX': {
|
|
1825
1989
|
// mail exchange records `dnsPromises.resolveMx()`
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1990
|
+
// Node.js v24+ adds 'type' property to MX records
|
|
1991
|
+
return result.answers.map((a) => {
|
|
1992
|
+
const record = {
|
|
1993
|
+
exchange: a.data.exchange,
|
|
1994
|
+
priority: a.data.preference
|
|
1995
|
+
};
|
|
1996
|
+
// Add type property for Node.js v24+ compatibility
|
|
1997
|
+
if (NODE_MAJOR_VERSION >= 24) {
|
|
1998
|
+
record.type = 'MX';
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return record;
|
|
2002
|
+
});
|
|
1830
2003
|
}
|
|
1831
2004
|
|
|
1832
2005
|
case 'NAPTR': {
|
|
1833
2006
|
// name authority pointer records `dnsPromises.resolveNaptr()`
|
|
1834
|
-
|
|
2007
|
+
// Node.js v24+ adds 'type' property to NAPTR records (with undefined value)
|
|
2008
|
+
return result.answers.map((a) => {
|
|
2009
|
+
const record = { ...a.data };
|
|
2010
|
+
// Add type property for Node.js v24+ compatibility
|
|
2011
|
+
// Note: Node.js v24 sets type to undefined for NAPTR records
|
|
2012
|
+
if (NODE_MAJOR_VERSION >= 24) {
|
|
2013
|
+
record.type = undefined;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
return record;
|
|
2017
|
+
});
|
|
1835
2018
|
}
|
|
1836
2019
|
|
|
1837
2020
|
case 'NS': {
|
|
@@ -1846,15 +2029,25 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1846
2029
|
|
|
1847
2030
|
case 'SOA': {
|
|
1848
2031
|
// start of authority records `dnsPromises.resolveSoa()`
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2032
|
+
// Node.js v24+ adds 'type' property to SOA records (with undefined value)
|
|
2033
|
+
const answers = result.answers.map((a) => {
|
|
2034
|
+
const record = {
|
|
2035
|
+
nsname: a.data.mname,
|
|
2036
|
+
hostmaster: a.data.rname,
|
|
2037
|
+
serial: a.data.serial,
|
|
2038
|
+
refresh: a.data.refresh,
|
|
2039
|
+
retry: a.data.retry,
|
|
2040
|
+
expire: a.data.expire,
|
|
2041
|
+
minttl: a.data.minimum
|
|
2042
|
+
};
|
|
2043
|
+
// Add type property for Node.js v24+ compatibility
|
|
2044
|
+
// Note: Node.js v24 sets type to undefined for SOA records
|
|
2045
|
+
if (NODE_MAJOR_VERSION >= 24) {
|
|
2046
|
+
record.type = undefined;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
return record;
|
|
2050
|
+
});
|
|
1858
2051
|
//
|
|
1859
2052
|
// NOTE: probably should just return answers[0] for consistency (?)
|
|
1860
2053
|
//
|
|
@@ -1863,12 +2056,21 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1863
2056
|
|
|
1864
2057
|
case 'SRV': {
|
|
1865
2058
|
// service records `dnsPromises.resolveSrv()`
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2059
|
+
// Node.js v24+ adds 'type' property to SRV records
|
|
2060
|
+
return result.answers.map((a) => {
|
|
2061
|
+
const record = {
|
|
2062
|
+
name: a.data.target,
|
|
2063
|
+
port: a.data.port,
|
|
2064
|
+
priority: a.data.priority,
|
|
2065
|
+
weight: a.data.weight
|
|
2066
|
+
};
|
|
2067
|
+
// Add type property for Node.js v24+ compatibility
|
|
2068
|
+
if (NODE_MAJOR_VERSION >= 24) {
|
|
2069
|
+
record.type = 'SRV';
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
return record;
|
|
2073
|
+
});
|
|
1872
2074
|
}
|
|
1873
2075
|
|
|
1874
2076
|
case 'TXT': {
|
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": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"author": "Forward Email (https://forwardemail.net)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/forwardemail/nodejs-dns-over-https-tangerine/issues"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"Forward Email (https://forwardemail.net)"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@ungap/structured-clone": "^1.2.0",
|
|
14
13
|
"auto-bind": "4",
|
|
15
14
|
"dns-packet": "^5.6.1",
|
|
16
15
|
"dohdec": "^5.0.3",
|
|
@@ -57,7 +56,7 @@
|
|
|
57
56
|
"xo": "^0.58.0"
|
|
58
57
|
},
|
|
59
58
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
59
|
+
"node": ">=18"
|
|
61
60
|
},
|
|
62
61
|
"files": [
|
|
63
62
|
"index.js"
|