tangerine 1.6.0 → 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 +210 -119
- package/index.js +372 -171
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -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
|
@@ -49,6 +49,14 @@ for (const line of hosts) {
|
|
|
49
49
|
HOSTS.push({ ip, hosts });
|
|
50
50
|
}
|
|
51
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
|
+
|
|
52
60
|
// <https://github.com/szmarczak/cacheable-lookup/pull/76>
|
|
53
61
|
class Tangerine extends dns.promises.Resolver {
|
|
54
62
|
static HOSTFILE = HOSTFILE;
|
|
@@ -632,8 +640,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
632
640
|
// remap and perform syscall
|
|
633
641
|
err.syscall = 'getaddrinfo';
|
|
634
642
|
err.message = err.message.replace('query', 'getaddrinfo');
|
|
635
|
-
|
|
636
|
-
|
|
643
|
+
// errno -3007 is for invalid hostnames (like ".")
|
|
644
|
+
err.errno = -3007;
|
|
637
645
|
throw err;
|
|
638
646
|
}
|
|
639
647
|
|
|
@@ -734,19 +742,27 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
734
742
|
}
|
|
735
743
|
}
|
|
736
744
|
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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);
|
|
747
762
|
// remap and perform syscall
|
|
748
763
|
err.syscall = 'getaddrinfo';
|
|
749
764
|
err.message = err.message.replace('query', 'getaddrinfo');
|
|
765
|
+
// errno -3008 is the standard ENOTFOUND errno
|
|
750
766
|
err.errno = -3008;
|
|
751
767
|
throw err;
|
|
752
768
|
}
|
|
@@ -935,7 +951,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
935
951
|
for (const rule of this.constructor.HOSTS) {
|
|
936
952
|
if (rule.ip === ip) {
|
|
937
953
|
match = true;
|
|
938
|
-
|
|
954
|
+
// Include all hosts (c-ares includes all hosts from /etc/hosts)
|
|
955
|
+
for (const host of rule.hosts) {
|
|
939
956
|
answers.add(host);
|
|
940
957
|
}
|
|
941
958
|
}
|
|
@@ -1029,7 +1046,32 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1029
1046
|
// (this means it's a drop-in replacement for `dns`)
|
|
1030
1047
|
// <https://github.com/nodejs/node/blob/9bbde3d7baef584f14569ef79f116e9d288c7aaa/lib/internal/dns/utils.js#L87-L95>
|
|
1031
1048
|
getServers() {
|
|
1032
|
-
|
|
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
|
+
});
|
|
1033
1075
|
}
|
|
1034
1076
|
|
|
1035
1077
|
//
|
|
@@ -1072,7 +1114,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1072
1114
|
|
|
1073
1115
|
debug('request', { url, options });
|
|
1074
1116
|
const t = setTimeout(() => {
|
|
1075
|
-
if (!abortController?.signal?.aborted) abortController.abort();
|
|
1117
|
+
if (!abortController?.signal?.aborted) abortController.abort('ETIMEOUT');
|
|
1076
1118
|
}, timeout);
|
|
1077
1119
|
const response = await this.request(url, options);
|
|
1078
1120
|
clearTimeout(t);
|
|
@@ -1217,7 +1259,12 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1217
1259
|
if (errors.length > 0) throw this.constructor.combineErrors(errors);
|
|
1218
1260
|
// if no errors and no response
|
|
1219
1261
|
// that must indicate that it was aborted
|
|
1220
|
-
|
|
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);
|
|
1221
1268
|
}
|
|
1222
1269
|
|
|
1223
1270
|
// without logging an error here, one might not know
|
|
@@ -1234,10 +1281,20 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1234
1281
|
} catch (_err) {
|
|
1235
1282
|
debug(_err, { name, rrtype, ecsSubnet });
|
|
1236
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
|
+
|
|
1237
1294
|
const err = this.constructor.createError(
|
|
1238
1295
|
name,
|
|
1239
1296
|
rrtype,
|
|
1240
|
-
|
|
1297
|
+
errorCode,
|
|
1241
1298
|
_err.errno
|
|
1242
1299
|
);
|
|
1243
1300
|
// then map it to dns.CONNREFUSED
|
|
@@ -1248,6 +1305,23 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1248
1305
|
}
|
|
1249
1306
|
}
|
|
1250
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
|
+
|
|
1251
1325
|
// Cancel all outstanding DNS queries made by this resolver
|
|
1252
1326
|
// NOTE: callbacks not currently called with ECANCELLED (prob need to alter got options)
|
|
1253
1327
|
// (instead they are called with "ABORT_ERR"; see ABORT_ERROR_CODES)
|
|
@@ -1265,120 +1339,124 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1265
1339
|
|
|
1266
1340
|
#resolveByType(name, options = {}, parentAbortController) {
|
|
1267
1341
|
return async (type) => {
|
|
1268
|
-
const abortController =
|
|
1269
|
-
this.abortControllers.add(abortController);
|
|
1270
|
-
abortController.signal.addEventListener(
|
|
1271
|
-
'abort',
|
|
1272
|
-
() => {
|
|
1273
|
-
this.abortControllers.delete(abortController);
|
|
1274
|
-
},
|
|
1275
|
-
{ once: true }
|
|
1276
|
-
);
|
|
1277
|
-
parentAbortController.signal.addEventListener(
|
|
1278
|
-
'abort',
|
|
1279
|
-
() => {
|
|
1280
|
-
try {
|
|
1281
|
-
abortController.abort('Parent abort controller aborted');
|
|
1282
|
-
} catch (err) {
|
|
1283
|
-
this.options.logger.debug(err);
|
|
1284
|
-
}
|
|
1285
|
-
},
|
|
1286
|
-
{ once: true }
|
|
1287
|
-
);
|
|
1288
|
-
// wrap with try/catch because ENODATA shouldn't cause errors
|
|
1342
|
+
const abortController = this.#createAbortController();
|
|
1289
1343
|
try {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
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
|
+
}
|
|
1299
1366
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
+
}
|
|
1308
1375
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1376
|
+
case 'CNAME': {
|
|
1377
|
+
const result = await this.resolveCname(
|
|
1378
|
+
name,
|
|
1379
|
+
options,
|
|
1380
|
+
abortController
|
|
1381
|
+
);
|
|
1382
|
+
return result.map((value) => ({ type, value }));
|
|
1383
|
+
}
|
|
1317
1384
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1385
|
+
case 'MX': {
|
|
1386
|
+
const result = await this.resolveMx(
|
|
1387
|
+
name,
|
|
1388
|
+
options,
|
|
1389
|
+
abortController
|
|
1390
|
+
);
|
|
1391
|
+
return result.map((r) => ({ type, ...r }));
|
|
1392
|
+
}
|
|
1322
1393
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1394
|
+
case 'NAPTR': {
|
|
1395
|
+
const result = await this.resolveNaptr(
|
|
1396
|
+
name,
|
|
1397
|
+
options,
|
|
1398
|
+
abortController
|
|
1399
|
+
);
|
|
1400
|
+
return result.map((value) => ({ type, value }));
|
|
1401
|
+
}
|
|
1331
1402
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1403
|
+
case 'NS': {
|
|
1404
|
+
const result = await this.resolveNs(
|
|
1405
|
+
name,
|
|
1406
|
+
options,
|
|
1407
|
+
abortController
|
|
1408
|
+
);
|
|
1409
|
+
return result.map((value) => ({ type, value }));
|
|
1410
|
+
}
|
|
1336
1411
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1412
|
+
case 'PTR': {
|
|
1413
|
+
const result = await this.resolvePtr(
|
|
1414
|
+
name,
|
|
1415
|
+
options,
|
|
1416
|
+
abortController
|
|
1417
|
+
);
|
|
1418
|
+
return result.map((value) => ({ type, value }));
|
|
1419
|
+
}
|
|
1345
1420
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1421
|
+
case 'SOA': {
|
|
1422
|
+
const result = await this.resolveSoa(
|
|
1423
|
+
name,
|
|
1424
|
+
options,
|
|
1425
|
+
abortController
|
|
1426
|
+
);
|
|
1427
|
+
return { type, ...result };
|
|
1428
|
+
}
|
|
1354
1429
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1430
|
+
case 'SRV': {
|
|
1431
|
+
const result = await this.resolveSrv(
|
|
1432
|
+
name,
|
|
1433
|
+
options,
|
|
1434
|
+
abortController
|
|
1435
|
+
);
|
|
1436
|
+
return result.map((value) => ({ type, value }));
|
|
1437
|
+
}
|
|
1363
1438
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1439
|
+
case 'TXT': {
|
|
1440
|
+
const result = await this.resolveTxt(
|
|
1441
|
+
name,
|
|
1442
|
+
options,
|
|
1443
|
+
abortController
|
|
1444
|
+
);
|
|
1445
|
+
return result.map((entries) => ({ type, entries }));
|
|
1446
|
+
}
|
|
1372
1447
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1448
|
+
default: {
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1375
1451
|
}
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
debug(err);
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
debug(err);
|
|
1379
1454
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1455
|
+
if (err.code === dns.NODATA) return;
|
|
1456
|
+
throw err;
|
|
1457
|
+
}
|
|
1458
|
+
} finally {
|
|
1459
|
+
this.#releaseAbortController(abortController);
|
|
1382
1460
|
}
|
|
1383
1461
|
};
|
|
1384
1462
|
}
|
|
@@ -1394,23 +1472,22 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1394
1472
|
// <https://gist.github.com/andrewcourtice/ef1b8f14935b409cfe94901558ba5594#file-task-ts-L37>
|
|
1395
1473
|
// <https://github.com/nodejs/undici/blob/0badd390ad5aa531a66aacee54da664468aa1577/lib/api/api-fetch/request.js#L280-L295>
|
|
1396
1474
|
// <https://github.com/nodejs/node/issues/40849>
|
|
1475
|
+
let mustReleaseAbortController = false;
|
|
1397
1476
|
if (!abortController) {
|
|
1398
|
-
abortController =
|
|
1399
|
-
|
|
1400
|
-
abortController.signal.addEventListener(
|
|
1401
|
-
'abort',
|
|
1402
|
-
() => {
|
|
1403
|
-
this.abortControllers.delete(abortController);
|
|
1404
|
-
},
|
|
1405
|
-
{ once: true }
|
|
1406
|
-
);
|
|
1477
|
+
abortController = this.#createAbortController();
|
|
1478
|
+
mustReleaseAbortController = true;
|
|
1407
1479
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
+
}
|
|
1414
1491
|
}
|
|
1415
1492
|
|
|
1416
1493
|
try {
|
|
@@ -1428,6 +1505,10 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1428
1505
|
err.syscall = 'queryAny';
|
|
1429
1506
|
err.message = `queryAny ${err.code} ${name}`;
|
|
1430
1507
|
throw err;
|
|
1508
|
+
} finally {
|
|
1509
|
+
if (mustReleaseAbortController) {
|
|
1510
|
+
this.#releaseAbortController(abortController);
|
|
1511
|
+
}
|
|
1431
1512
|
}
|
|
1432
1513
|
}
|
|
1433
1514
|
|
|
@@ -1554,11 +1635,80 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1554
1635
|
throw err;
|
|
1555
1636
|
}
|
|
1556
1637
|
|
|
1557
|
-
// edge case where c-ares detects "." as start of string
|
|
1638
|
+
// edge case where c-ares detects "." as start of string or malformed hostnames
|
|
1558
1639
|
// <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
|
|
1559
|
-
if (
|
|
1640
|
+
if (
|
|
1641
|
+
(name !== '.' && (name.startsWith('.') || name.includes('..'))) ||
|
|
1642
|
+
name.includes(',')
|
|
1643
|
+
)
|
|
1560
1644
|
throw this.constructor.createError(name, rrtype, dns.BADNAME);
|
|
1561
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
|
+
|
|
1562
1712
|
// purge cache support
|
|
1563
1713
|
let purgeCache;
|
|
1564
1714
|
if (options?.purgeCache) {
|
|
@@ -1620,7 +1770,6 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1620
1770
|
const diff = data.ttl - ttl;
|
|
1621
1771
|
|
|
1622
1772
|
for (let i = 0; i < data.answers.length; i++) {
|
|
1623
|
-
// eslint-disable-next-line max-depth
|
|
1624
1773
|
if (typeof data.answers[i].ttl === 'number') {
|
|
1625
1774
|
// subtract ttl from answer
|
|
1626
1775
|
data.answers[i].ttl = Math.round(data.answers[i].ttl - diff);
|
|
@@ -1674,20 +1823,20 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1674
1823
|
if (this.options.cache && result) {
|
|
1675
1824
|
debug(`cached result found for "${key}"`);
|
|
1676
1825
|
} else {
|
|
1826
|
+
let mustReleaseAbortController = false;
|
|
1677
1827
|
if (!abortController) {
|
|
1678
|
-
abortController =
|
|
1679
|
-
|
|
1680
|
-
abortController.signal.addEventListener(
|
|
1681
|
-
'abort',
|
|
1682
|
-
() => {
|
|
1683
|
-
this.abortControllers.delete(abortController);
|
|
1684
|
-
},
|
|
1685
|
-
{ once: true }
|
|
1686
|
-
);
|
|
1828
|
+
abortController = this.#createAbortController();
|
|
1829
|
+
mustReleaseAbortController = true;
|
|
1687
1830
|
}
|
|
1688
1831
|
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
+
}
|
|
1691
1840
|
}
|
|
1692
1841
|
|
|
1693
1842
|
// <https://github.com/m13253/dns-over-https/blob/2e36b4ebcdb8a1a102ea86370d7f8b1f1e72380a/json-dns/response.go#L50-L74>
|
|
@@ -1811,10 +1960,24 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1811
1960
|
case 'CAA': {
|
|
1812
1961
|
// CA authorization records `dnsPromises.resolveCaa()`
|
|
1813
1962
|
// <https://www.rfc-editor.org/rfc/rfc6844#section-3>
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
+
});
|
|
1818
1981
|
}
|
|
1819
1982
|
|
|
1820
1983
|
case 'CNAME': {
|
|
@@ -1824,15 +1987,34 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1824
1987
|
|
|
1825
1988
|
case 'MX': {
|
|
1826
1989
|
// mail exchange records `dnsPromises.resolveMx()`
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
+
});
|
|
1831
2003
|
}
|
|
1832
2004
|
|
|
1833
2005
|
case 'NAPTR': {
|
|
1834
2006
|
// name authority pointer records `dnsPromises.resolveNaptr()`
|
|
1835
|
-
|
|
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
|
+
});
|
|
1836
2018
|
}
|
|
1837
2019
|
|
|
1838
2020
|
case 'NS': {
|
|
@@ -1847,15 +2029,25 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1847
2029
|
|
|
1848
2030
|
case 'SOA': {
|
|
1849
2031
|
// start of authority records `dnsPromises.resolveSoa()`
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
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
|
+
});
|
|
1859
2051
|
//
|
|
1860
2052
|
// NOTE: probably should just return answers[0] for consistency (?)
|
|
1861
2053
|
//
|
|
@@ -1864,12 +2056,21 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1864
2056
|
|
|
1865
2057
|
case 'SRV': {
|
|
1866
2058
|
// service records `dnsPromises.resolveSrv()`
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
+
});
|
|
1873
2074
|
}
|
|
1874
2075
|
|
|
1875
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"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"xo": "^0.58.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
|
-
"node": ">=
|
|
59
|
+
"node": ">=18"
|
|
60
60
|
},
|
|
61
61
|
"files": [
|
|
62
62
|
"index.js"
|