├── htdocs ├── puddy.gif ├── puddy.jpg ├── puddy.css ├── resolvers └── index.cgi ├── CHANGES ├── Makefile ├── LICENSE ├── conf └── puddy.resolvers ├── doc ├── puddy.1.txt └── puddy.1 ├── README.md └── src └── puddy.pl /htdocs/puddy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschauma/puddy/HEAD/htdocs/puddy.gif -------------------------------------------------------------------------------- /htdocs/puddy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschauma/puddy/HEAD/htdocs/puddy.jpg -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Version 1.6 (2023-02-07) 2 | * add dns0.eu 3 | 4 | Version 1.5 (2022-02-12) 5 | * switch cidr-by-country source 6 | 7 | Version 1.4 (2021-09-30) 8 | * ensure the org/comment is printed 9 | 10 | Version 1.3 (2020-05-01) 11 | * remove doh.netweaver.uk and doh.securedns.eu, which 12 | disappeared 13 | * handle such failures more gracefully 14 | 15 | Version 1.2 (2019-11-19) 16 | * add '-e' to specify ECS explicitly 17 | 18 | Version 1.1 (2019-09-28) 19 | * use Parallel:ForkManager to speed up queries 20 | 21 | Version 1.0 (2019-09-24) 22 | * Hello World! 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=puddy 2 | PREFIX?=/usr/local 3 | PERL!=which perl 4 | 5 | help: 6 | @echo "The following targets are available:" 7 | @echo "clean remove temporary files" 8 | @echo "prep update the perl path in the source script" 9 | @echo "install install all files under ${PREFIX}" 10 | 11 | prep: src/${NAME} 12 | 13 | src/${NAME}: src/${NAME}.pl 14 | sed -e "s|/usr/local/bin/perl|${PERL}|" $? >$@ 15 | 16 | install: prep 17 | mkdir -p ${PREFIX}/bin ${PREFIX}/share/man/man1 18 | install -m 755 src/${NAME} ${PREFIX}/bin/${NAME} 19 | install -m 444 doc/${NAME}.1 ${PREFIX}/share/man/man1 20 | install -m 444 conf/${NAME}.resolvers ${PREFIX}/share/${NAME}.resolvers 21 | 22 | clean: 23 | rm -f src/${NAME} 24 | -------------------------------------------------------------------------------- /htdocs/puddy.css: -------------------------------------------------------------------------------- 1 | html, body, p { 2 | background-color: #FDFDFD; 3 | color: #111; 4 | font-size:16px; 5 | line-height:1.5em; 6 | font-family:"Myriad Pro", "Trebuchet MS", Helvetica, sans-serif; 7 | width: 40em; 8 | margin:4ex 0 12ex 5%; 9 | font-weight: normal; 10 | font-style: normal; 11 | font-variant: normal; 12 | letter-spacing: normal; 13 | } 14 | 15 | p, li { 16 | line-height: 1.555em; 17 | text-align: justify; 18 | margin: 0 0 0.754em 0; 19 | padding: 0 0 0 0; 20 | hyphens: auto; 21 | } 22 | 23 | p.narrow { 24 | width: 36em; 25 | } 26 | 27 | small { 28 | font-size:14px; 29 | } 30 | 31 | img { 32 | margin: 10px 35px; 33 | } 34 | 35 | img.framed { 36 | border:1px solid #000000; 37 | } 38 | 39 | IMG.displayed { 40 | display: block; 41 | margin-left: auto; 42 | margin-right: auto } 43 | 44 | A:link { color: #aa0000; } 45 | A:visited { color: #606060; } 46 | A:active { color: #ffffff; } 47 | 48 | .hidden { display: none; } 49 | .unhidden { display: block; } 50 | -------------------------------------------------------------------------------- /htdocs/resolvers: -------------------------------------------------------------------------------- 1 | # This file contains a list of public DNS servers 2 | # used by https://www.netmeister.org/puddy/ 3 | # 4 | # The format of the file is document in the manual 5 | # page, but in a nutshell, it's 6 | # ip-address [some-organization-identifier] 7 | 1.0.0.1 Cloudflare 8 | 1.1.1.1 Cloudflare 9 | 2606:4700:4700::1001 Cloudflare 10 | 2606:4700:4700::1111 Cloudflare 11 | 12 | 8.20.247.20 Comodo Secure DNS 13 | 8.26.56.26 Comodo Secure DNS 14 | 15 | 2001:1608:10:25::1c04:b12f DNS.Watch 16 | 84.200.69.80 DNS.Watch 17 | 84.200.70.40 DNS.Watch 18 | 19 | 216.146.35.35 Dyn 20 | 216.146.36.36 Dyn 21 | 22 | 2001:4860:4860::8844 Google Public DNS 23 | 2001:4860:4860::8888 Google Public DNS 24 | 8.8.4.4 Google Public DNS 25 | 8.8.8.8 Google Public DNS 26 | 27 | 2001:470:20::2 Hurricane Electric 28 | 74.82.42.42 Hurricane Electric 29 | 30 | 209.244.0.3 Level3 31 | 209.244.0.4 Level3 32 | 33 | 208.67.220.220 OpenDNS 34 | 208.67.222.222 OpenDNS 35 | 2620:0:ccc::2 OpenDNS 36 | 2620:0:ccd::2 OpenDNS 37 | 38 | 2620:fe::fe Quad9 39 | 9.9.9.9 Quad9 40 | 41 | 2a02:6b8:0:1::feed:bad Yandex.DNS 42 | 2a02:6b8::feed:bad Yandex.DNS 43 | 77.88.8.2 Yandex.DNS 44 | 77.88.8.88 Yandex.DNS 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019, Verizon Media 2 | 3 | Redistribution and use in source and binary forms, 4 | with or without modification, are permitted provided 5 | that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the 8 | above copyright notice, this list of conditions and 9 | the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the 12 | above copyright notice, this list of conditions and 13 | the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the 17 | names of its contributors may be used to endorse or 18 | promote products derived from this software without 19 | specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 22 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 23 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 26 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 27 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 28 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 29 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 30 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 32 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 33 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 34 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /conf/puddy.resolvers: -------------------------------------------------------------------------------- 1 | # This file contains a list of public DNS servers 2 | # suitable for being fed to puddy(1). 3 | # 4 | # The format of the file is document in the manual 5 | # page, but in a nutshell, it's 6 | # ip-address [some-organization-identifier] 7 | 198.101.242.72 Alternate DNS 8 | 23.253.163.53 Alternate DNS 9 | 10 | 178.21.23.150 BlockAid Public DNS (or PeerDNS) 11 | 205.204.88.60 BlockAid Public DNS (or PeerDNS) 12 | 13 | 2001:67c:28a4:: Censurfridns 14 | 2002:d596:2a92:1:71:53:: Censurfridns 15 | 89.233.43.71 Censurfridns 16 | 91.239.100.100 Censurfridns 17 | 18 | 212.82.225.7 ClaraNet 19 | 212.82.226.212 ClaraNet 20 | 21 | 1.0.0.1 Cloudflare 22 | 1.1.1.1 Cloudflare 23 | 2606:4700:4700::1001 Cloudflare 24 | 2606:4700:4700::1111 Cloudflare 25 | 26 | 8.20.247.20 Comodo Secure DNS 27 | 8.26.56.26 Comodo Secure DNS 28 | 29 | 193.110.81.0 dns0.eu 30 | 185.253.5.0 dns0.eu 31 | 2a0f:fc80:: dns0.eu 32 | 2a0f:fc81:: dns0.eu 33 | 34 | 2001:1608:10:25::1c04:b12f DNS.Watch 35 | 2001:1608:10:25::9249:d69b DNS.Watch 36 | 37 | 84.200.69.80 DNS.Watch 38 | 84.200.70.40 DNS.Watch 39 | 40 | 104.236.210.29 DNSReactor 41 | 45.55.155.25 DNSReactor 42 | 43 | 216.146.35.35 Dyn 44 | 216.146.36.36 Dyn 45 | 46 | 2001:910:800::12 FDN 47 | 80.67.169.12 FDN 48 | 49 | 85.214.73.63 FoeBud 50 | 51 | 213.187.11.62 FoolDNS 52 | 87.118.111.215 FoolDNS 53 | 54 | 37.235.1.174 FreeDNS 55 | 37.235.1.177 FreeDNS 56 | 57 | 80.80.80.80 Freenom World 58 | 80.80.81.81 Freenom World 59 | 60 | 62.141.58.13 German Privacy Foundation e.V. 61 | 85.25.251.254 German Privacy Foundation e.V. 62 | 87.118.100.175 German Privacy Foundation e.V. 63 | 94.75.228.29 German Privacy Foundation e.V. 64 | 65 | 2001:4860:4860::8844 Google Public DNS 66 | 2001:4860:4860::8888 Google Public DNS 67 | 8.8.4.4 Google Public DNS 68 | 8.8.8.8 Google Public DNS 69 | 70 | 209.88.198.133 GreenTeamDNS 71 | 81.218.119.11 GreenTeamDNS 72 | 73 | 2001:470:20::2 Hurricane Electric 74 | 74.82.42.42 Hurricane Electric 75 | 76 | 209.244.0.3 Level3 77 | 209.244.0.4 Level3 78 | 79 | 156.154.70.1 Neustar DNS Advantage 80 | 156.154.71.1 Neustar DNS Advantage 81 | 82 | 185.82.22.133 New Nations 83 | 5.45.96.220 New Nations 84 | 85 | 198.153.192.1 Norton DNS 86 | 198.153.194.1 Norton DNS 87 | 88 | 208.67.220.220 OpenDNS 89 | 208.67.222.222 OpenDNS 90 | 2620:0:ccc::2 OpenDNS 91 | 2620:0:ccd::2 OpenDNS 92 | 93 | 119.31.230.42 OpenNIC 94 | 200.252.98.162 OpenNIC 95 | 2001:470:1f07:38b::1 OpenNIC 96 | 2001:470:1f10:c6::2001 OpenNIC 97 | 2001:470:8388:2:20e:2eff:fe63:d4a9 OpenNIC 98 | 203.167.220.153 OpenNIC 99 | 207.192.69.155 OpenNIC 100 | 216.87.84.211 OpenNIC 101 | 217.79.186.148 OpenNIC 102 | 58.6.115.42 OpenNIC 103 | 58.6.115.43 OpenNIC 104 | 66.244.95.20 OpenNIC 105 | 72.14.189.120 OpenNIC 106 | 78.159.101.37 OpenNIC 107 | 81.89.98.6 OpenNIC 108 | 82.229.244.191 OpenNIC 109 | 110 | 194.145.226.26 PowerNS 111 | 77.220.232.44 PowerNS 112 | 113 | 2620:fe::fe Quad9 114 | 9.9.9.9 Quad9 115 | 116 | 195.46.39.39 SafeDNS 117 | 195.46.39.40 SafeDNS 118 | 119 | 193.58.251.251 SkyDNS 120 | 121 | 208.76.50.50 SmartViper Public DNS 122 | 208.76.51.51 SmartViper Public DNS 123 | 124 | 78.46.89.147 ValiDOM 125 | 88.198.75.145 ValiDOM 126 | 127 | 2620:74:1b::1:1 Verisign 128 | 2620:74:1c::2:2 Verisign 129 | 64.6.64.6 Verisign 130 | 64.6.65.6 Verisign 131 | 132 | 2001:1620:2078:136:: Xiala.net 133 | 2001:1620:2078:137:: Xiala.net 134 | 77.109.148.136 Xiala.net 135 | 77.109.148.137 Xiala.net 136 | 137 | 2a02:6b8:0:1::feed:bad Yandex.DNS 138 | 2a02:6b8::feed:bad Yandex.DNS 139 | 77.88.8.2 Yandex.DNS 140 | 77.88.8.88 Yandex.DNS 141 | 142 | 109.69.8.51 puntCAT 143 | 2a00:1508:0:4::9 puntCAT 144 | 145 | 4.2.2.2 CenturyLink/Level3 146 | -------------------------------------------------------------------------------- /doc/puddy.1.txt: -------------------------------------------------------------------------------- 1 | puddy(1) NetBSD General Commands Manual puddy(1) 2 | 3 | NAME 4 | puddy -- public DNS data yielder 5 | 6 | SYNOPSIS 7 | puddy [-146Vdhjpv] [-c country] [-e cidr] [-f file] [-n num] 8 | [-r resolver] query [type ...] 9 | 10 | DESCRIPTION 11 | The puddy tool allows you to query a number of different resolvers to 12 | compare whether results are uniform across the internet. 13 | 14 | OPTIONS 15 | The following options are supported by puddy: 16 | 17 | -1 Only use a single resolver from one and the same organiza- 18 | tion. This option cannot be combined with the -p flag. See 19 | Section PUBLIC RESOLVERS for details. 20 | 21 | -4 Only query IPv4 resolvers. 22 | 23 | -6 Only query IPv6 resolvers. 24 | 25 | -V Print version number and exit. 26 | 27 | -c country Try to use the EDNS Client Subnet (ECS) extension to elicit 28 | a response from the DNS server as if the client was located 29 | in the given country. 30 | 31 | The argument country can be a country code or the name of a 32 | country. 33 | 34 | See Section COUNTRY IMITATION for details. 35 | 36 | -d Use DNS-over-HTTPS. This option only works with a subset of 37 | the known public resolvers and thus conflicts with e.g., the 38 | -f and -p flags. 39 | 40 | -e cidr Try to use the given CIDR via the EDNS Client Subnet (ECS) 41 | extension. 42 | 43 | -f file Read the list of resolvers to query from this file. This 44 | option conflicts with the -p flag. See Section FILES for 45 | details. 46 | 47 | -h Display help and exit. 48 | 49 | -j Print output in json format. 50 | 51 | -n num Only query at most this many resolvers. 52 | 53 | -p Query all known public DNS resolvers. This option conflicts 54 | with the -f flag. See Section PUBLIC RESOLVERS for details. 55 | 56 | -r resolver Add the given resolver to the list of resolvers to query. 57 | Can be specified multiple times. 58 | 59 | -v Be verbose. Can be specified multiple times. 60 | 61 | DETAILS 62 | Sometimes it can be useful to check whether a given hostname or record 63 | resolves to the same value in different places on the internet, so you go 64 | and ask the magic 8.8.8.8 ball. But perhaps other resolvers elsewhere 65 | have different results? Manually querying multiple resolvers is labor- 66 | some, so instead puddy saves you some typing by performing these lookups 67 | in succession for you. 68 | 69 | By default, puddy will query resolvers for A, AAAA, and CNAME records, 70 | similar to the host(1) command. 71 | 72 | PUBLIC RESOLVERS 73 | If the -p flag is given, puddy will retrieve the list of known public DNS 74 | resolvers from https://public-dns.info/nameservers.txt and query each 75 | one. 76 | 77 | Otherwise, if the -f flag is given, puddy will read the list of resolvers 78 | to query from the specified file. 79 | 80 | Many organizations offer public DNS services on multiple IP addresses; if 81 | the list of resolvers to use contains multiple IP address for the same 82 | organization, and you only want one of those servers, specify the -1 83 | (numeric one) flag. puddy will then pick only one of the IPs for each 84 | organization from the input set. Note: If an organization's resolvers 85 | are reachable via both IPv4 and IPv6 addresses, then puddy will prefer 86 | the IPv6 address. 87 | 88 | If the -d flag is specified, puddy will query a short list of hardcoded 89 | public resolvers known to support DNS-over-HTTPS with results returned in 90 | JSON format: 91 | Cloudflare 92 | DNS.SB 93 | Google 94 | Quad9 95 | doh.li 96 | 97 | If neither the -d, -f, or -p flag is specified, puddy will only query a 98 | short list of hardocded, popular public resolvers, which consists of: 99 | Cloudflare 100 | Google 101 | Hurricane Electric 102 | OpenDNS 103 | Quad9 104 | 105 | In addition, puddy will also query the resolvers found in 106 | /etc/resolv.conf. 107 | 108 | Finally, if the -n flag is specified, then at most this many resolvers 109 | will be queried; the list is randomized before the selection of resolvers 110 | to query is made. 111 | 112 | FILES 113 | puddy may read a list of public resolvers to query from a file provided 114 | via the -f flag. Each line in the file is expected to consist of an IP 115 | address and an optional comment or identifier. Leading and trailing 116 | whitespace as well as empty lines and anything following the '#' charac- 117 | ter are ignored. 118 | 119 | An example puddy.resolvers file might look like so: 120 | 121 | # puddy.resolvers file 122 | 216.146.35.35 Dyn 123 | 2001:470:1f10:c6::2001 OpenNIC 124 | 10.0.0.1 My Private Resolver # not /etc/resolv.conf! 125 | 126 | puddy may have come with a list of public resolvers in 127 | /usr/local/share/puddy.resolvers. 128 | 129 | COUNTRY IMITATION 130 | Sometimes it is useful to be able to see if different resolvers might 131 | give different responses based on where the client is located. puddy 132 | supports the -c country option to attempt to elicit a response from the 133 | DNS server as if the client was in the given location. This is done 134 | through the use of the EDNS Client Subnet extension (ECS, see RFC7871). 135 | 136 | Note: at this time, puddy only supports this option when performing 137 | queries using DNS over HTTPS (i.e., in combination with the -d option). 138 | 139 | When specified, puddy will try to look up a suitable netblock for the 140 | given country via data from the site https://www.nirsoft.net/countryip/, 141 | then set the ECS option. 142 | 143 | If you wish to disable the use of the ECS extension altogether, then you 144 | can pass 'none' as an argument to the -c flag, yielding a netblock of 145 | 0.0.0.0/0. 146 | 147 | Note: not all DoH providers honor this option, but puddy has no way of 148 | telling the difference. In addition, even for those resolvers that do 149 | support it, there is no guarantee that the result returned does in fact 150 | reflect what would be returned to a client query actually originating 151 | from that netblock. 152 | 153 | EXAMPLES 154 | The following examples illustrate common usage of this tool. 155 | 156 | To look up the IP addresses for www.yahoo.com on the short list of public 157 | resolvers, limiting to one query per organization: 158 | 159 | $ puddy -1 www.yahoo.com 160 | 2001:470:20::2 (Hurricane Electric) 161 | A (20): 72.30.35.10 162 | A (20): 72.30.35.9 163 | AAAA (56): 2001:4998:58:1836::11 164 | AAAA (56): 2001:4998:58:1836::10 165 | CNAME (1759): atsv2-fp-shed.wg1.b.yahoo.com. 166 | 2001:4860:4860::8888 (Google) 167 | A (15): 98.138.219.231 168 | A (15): 72.30.35.9 169 | A (15): 72.30.35.10 170 | A (15): 98.138.219.232 171 | AAAA (45): 2001:4998:44:41d::4 172 | AAAA (45): 2001:4998:44:41d::3 173 | AAAA (45): 2001:4998:58:1836::11 174 | AAAA (45): 2001:4998:58:1836::10 175 | CNAME (77): atsv2-fp-shed.wg1.b.yahoo.com. 176 | 2606:4700:4700::1001 (Cloudflare) 177 | A (38): 72.30.35.10 178 | A (38): 72.30.35.9 179 | AAAA (33): 2001:4998:58:1836::10 180 | AAAA (33): 2001:4998:58:1836::11 181 | CNAME (15): atsv2-fp-shed.wg1.b.yahoo.com. 182 | 2620:0:ccc::2 (OpenDNS) 183 | A (18): 72.30.35.9 184 | A (18): 72.30.35.10 185 | AAAA (45): 2001:4998:58:1836::10 186 | AAAA (45): 2001:4998:58:1836::11 187 | CNAME (1531): atsv2-fp-shed.wg1.b.yahoo.com. 188 | 2620:fe::fe (Quad9) 189 | A (15): 72.30.35.9 190 | A (15): 72.30.35.10 191 | AAAA (56): 2001:4998:58:1836::11 192 | AAAA (56): 2001:4998:58:1836::10 193 | CNAME (772): atsv2-fp-shed.wg1.b.yahoo.com. 194 | 172.131.44.74 (/etc/resolv.conf) 195 | A (46): 74.6.143.8 196 | AAAA (51): 2001:4998:58:207::1000 197 | CNAME (1800): atsv2-fp-shed.wg1.b.yahoo.com. 198 | 199 | To only query at most 2 IPv6 resolvers from the public list of public 200 | resolvers for NS records, one of which does not respond in time: 201 | 202 | $ puddy -6 -n 2 -p netmeister.org NS 203 | 2610:a1:1019::31 204 | NS: timed out 205 | 2610:a1:1019::32 206 | NS (10799): ns-143-b.gandi.net. 207 | NS (10799): ns-179-c.gandi.net. 208 | NS (10799): ns-181-a.gandi.net. 209 | 210 | To query 3 DoH providers: 211 | 212 | $ puddy -n 3 -d _443._tcp.mta-sts.netmeister.org TLSA 213 | DNS.SB (https://doh.dns.sb/dns-query?) 214 | TLSA (3600): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 215 | Google (https://dns.google/resolve?) 216 | TLSA (3599): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 217 | Quad9 (https://9.9.9.9:5053/dns-query?) 218 | TLSA (3600): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 219 | 220 | To get DoH results with an EDNS Client Subnet set to a netblock from 221 | China: 222 | 223 | $ puddy -d -c cn www.google.cn 224 | Cloudflare (https://cloudflare-dns.com/dns-query?) 225 | A (105): 172.217.11.3 226 | AAAA (55): 2607:f8b0:4006:812::2003 227 | DNS.SB (https://doh.dns.sb/dns-query?) (ECS 45.126.116.0/22) 228 | A (300): 203.208.39.207 229 | A (300): 203.208.39.216 230 | A (300): 203.208.39.223 231 | A (300): 203.208.39.215 232 | Google (https://dns.google/resolve?) (ECS 45.126.116.0/22) 233 | A (299): 203.208.39.223 234 | A (299): 203.208.39.207 235 | A (299): 203.208.39.215 236 | A (299): 203.208.39.216 237 | Quad9 (https://9.9.9.9:5053/dns-query?) 238 | A (300): 172.217.7.3 239 | AAAA (300): 2607:f8b0:4006:801::2003 240 | doh.li (https://doh.li/dns-query?) (ECS 45.126.116.0/22) 241 | A (300): 203.208.39.216 242 | A (300): 203.208.39.215 243 | A (300): 203.208.39.207 244 | A (300): 203.208.39.223 245 | 246 | To get the results from the resolvers specified in the file 247 | /usr/local/share/puddy.resolvers and generate output in json format: 248 | 249 | $ puddy -j -f /usr/local/share/puddy.resolvers whocybered.me txt 250 | { 251 | "results" : { 252 | "209.244.0.3" : { 253 | "TXT" : { 254 | "status" : "NOERROR", 255 | "rrs" : [ 256 | { 257 | "value" : "\"Attribution is hard. Cyber doubly so. When in doubt, APT.\"", 258 | "ttl" : 10795 259 | } 260 | ] 261 | }, 262 | "comment" : "/tmp/f" 263 | }, 264 | "2620:74:1b::1:1" : { 265 | "TXT" : { 266 | "status" : "NOERROR", 267 | "rrs" : [ 268 | { 269 | "ttl" : 10794, 270 | "value" : "\"Attribution is hard. Cyber doubly so. When in doubt, APT.\"" 271 | } 272 | ] 273 | }, 274 | "comment" : "/tmp/f" 275 | } 276 | }, 277 | "query" : "whocybered.me" 278 | } 279 | 280 | EXIT STATUS 281 | The puddy utility exits 0 on success, and >0 if an error occurs. 282 | 283 | NOTES 284 | Feels like an Arby's night. 285 | 286 | SEE ALSO 287 | dig(1), host(1), nslookup(1) 288 | 289 | RFC7871 290 | 291 | HISTORY 292 | puddy was originally written by Jan Schaumann 293 | in September 2019. 294 | 295 | BUGS 296 | Please file bugs and feature requests by emailing the author. 297 | 298 | NetBSD 8.0 September 30, 2021 NetBSD 8.0 299 | -------------------------------------------------------------------------------- /doc/puddy.1: -------------------------------------------------------------------------------- 1 | .Dd September 30, 2021 2 | .Dt puddy 1 3 | .Os 4 | .Sh NAME 5 | .Nm puddy 6 | .Nd public DNS data yielder 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl 146Vdhjpv 10 | .Op Fl c Ar country 11 | .Op Fl e Ar cidr 12 | .Op Fl f Ar file 13 | .Op Fl n Ar num 14 | .Op Fl r Ar resolver 15 | .Ar query 16 | .Op Ar type Ar ... 17 | .Sh DESCRIPTION 18 | The 19 | .Nm 20 | tool allows you to query a number of different 21 | resolvers to compare whether results are uniform 22 | across the internet. 23 | .Sh OPTIONS 24 | The following options are supported by 25 | .Nm : 26 | .Bl -tag -width a_resolver_ 27 | .It Fl 1 28 | Only use a single resolver from one and the same 29 | organization. 30 | This option cannot be combined with the 31 | .Fl p 32 | flag. 33 | See Section PUBLIC RESOLVERS for details. 34 | .It Fl 4 35 | Only query IPv4 resolvers. 36 | .It Fl 6 37 | Only query IPv6 resolvers. 38 | .It Fl V 39 | Print version number and exit. 40 | .It Fl c Ar country 41 | Try to use the EDNS Client Subnet (ECS) extension to 42 | elicit a response from the DNS server as if the client 43 | was located in the given country. 44 | .Pp 45 | The argument 46 | .Ar country 47 | can be a country code or the name of a country. 48 | .Pp 49 | See Section COUNTRY IMITATION for details. 50 | .It Fl d 51 | Use DNS-over-HTTPS. 52 | This option only works with a subset of the known 53 | public resolvers and thus conflicts with e.g., the 54 | .Fl f 55 | and 56 | .Fl p 57 | flags. 58 | .It Fl e Ar cidr 59 | Try to use the given CIDR via the EDNS Client Subnet 60 | (ECS) extension. 61 | .It Fl f Ar file 62 | Read the list of resolvers to query from this file. 63 | This option conflicts with the 64 | .Fl p 65 | flag. 66 | See Section FILES for details. 67 | .It Fl h 68 | Display help and exit. 69 | .It Fl j 70 | Print output in json format. 71 | .It Fl n Ar num 72 | Only query at most this many resolvers. 73 | .It Fl p 74 | Query all known public DNS resolvers. 75 | This option conflicts with the 76 | .Fl f 77 | flag. 78 | See Section PUBLIC RESOLVERS for details. 79 | .It Fl r Ar resolver 80 | Add the given resolver to the list of resolvers to 81 | query. 82 | Can be specified multiple times. 83 | .It Fl v 84 | Be verbose. 85 | Can be specified multiple times. 86 | .El 87 | .Sh DETAILS 88 | Sometimes it can be useful to check whether a given 89 | hostname or record resolves to the same value in 90 | different places on the internet, so you go and ask 91 | the magic 8.8.8.8 ball. 92 | But perhaps other resolvers elsewhere have different 93 | results? 94 | Manually querying multiple resolvers is laborsome, so 95 | instead 96 | .Nm 97 | saves you some typing by performing these lookups in 98 | succession for you. 99 | .Pp 100 | By default, 101 | .Nm 102 | will query resolvers for A, AAAA, and CNAME records, 103 | similar to the 104 | .Xr host 1 105 | command. 106 | .Sh PUBLIC RESOLVERS 107 | If the 108 | .Fl p 109 | flag is given, 110 | .Nm 111 | will retrieve the list of known public DNS resolvers 112 | from https://public-dns.info/nameservers.txt and query 113 | each one. 114 | .Pp 115 | Otherwise, if the 116 | .Fl f 117 | flag is given, 118 | .Nm 119 | will read the list of resolvers to query from the 120 | specified file. 121 | .Pp 122 | Many organizations offer public DNS services on 123 | multiple IP addresses; if the list of resolvers to use 124 | contains multiple IP address for the same 125 | organization, and you only want one of those 126 | servers, specify the 127 | .Fl 1 128 | (numeric one) flag. 129 | .Nm 130 | will then pick only one of the IPs for each 131 | organization from the input set. 132 | Note: If an organization's resolvers are reachable via both 133 | IPv4 and IPv6 addresses, then 134 | .Nm 135 | will prefer the IPv6 address. 136 | .Pp 137 | If the 138 | .Fl d 139 | flag is specified, 140 | .Nm 141 | will query a short list of hardcoded public resolvers 142 | known to support DNS-over-HTTPS with results returned 143 | in JSON format: 144 | .Bl -tag -width 4n -offset indent -compact 145 | .It Cloudflare 146 | .It DNS.SB 147 | .It Google 148 | .It Quad9 149 | .It doh.li 150 | .El 151 | .Pp 152 | If neither the 153 | .Fl d , 154 | .Fl f , 155 | or 156 | .Fl p 157 | flag is specified, 158 | .Nm 159 | will only query a short list of hardocded, popular 160 | public resolvers, which consists of: 161 | .Bl -tag -width 4n -offset indent -compact 162 | .It Cloudflare 163 | .It Google 164 | .It Hurricane Electric 165 | .It OpenDNS 166 | .It Quad9 167 | .El 168 | .Pp 169 | In addition, 170 | .Nm 171 | will also query the resolvers found in 172 | /etc/resolv.conf. 173 | .Pp 174 | Finally, if the 175 | .Fl n 176 | flag is specified, then at most this many resolvers 177 | will be queried; the list is randomized before the 178 | selection of resolvers to query is made. 179 | .Sh FILES 180 | .Nm 181 | may read a list of public resolvers to query from a 182 | file provided via the 183 | .Fl f 184 | flag. 185 | Each line in the file is expected to consist of an IP 186 | address and an optional comment or identifier. 187 | Leading and trailing whitespace as well as empty lines 188 | and anything following the '#' character are ignored. 189 | .Pp 190 | An example puddy.resolvers file might look like so: 191 | .Bd -literal -offset indent 192 | # puddy.resolvers file 193 | 216.146.35.35 Dyn 194 | 2001:470:1f10:c6::2001 OpenNIC 195 | 10.0.0.1 My Private Resolver # not /etc/resolv.conf! 196 | .Ed 197 | .Pp 198 | .Nm 199 | may have come with a list of public resolvers in 200 | /usr/local/share/puddy.resolvers. 201 | .Sh COUNTRY IMITATION 202 | Sometimes it is useful to be able to see if different 203 | resolvers might give different responses based on 204 | where the client is located. 205 | .Nm 206 | supports the 207 | .Fl c Ar country 208 | option to attempt to elicit a response from the DNS 209 | server as if the client was in the given location. 210 | This is done through the use of the EDNS Client Subnet 211 | extension (ECS, see RFC7871). 212 | .Pp 213 | Note: at this time, 214 | .Nm 215 | only supports this option when performing queries 216 | using DNS over HTTPS (i.e., in combination with the 217 | .Fl d 218 | option). 219 | .Pp 220 | When specified, 221 | .Nm 222 | will try to look up a suitable netblock for the given 223 | country via data from the site 224 | https://www.nirsoft.net/countryip/, then set the ECS 225 | option. 226 | .Pp 227 | If you wish to disable the use of the ECS extension 228 | altogether, then you can pass 'none' as an argument to 229 | the 230 | .Fl c 231 | flag, yielding a netblock of 0.0.0.0/0. 232 | .Pp 233 | Note: not all DoH providers honor this option, but 234 | .Nm 235 | has no way of telling the difference. 236 | In addition, even for those resolvers that do support 237 | it, there is no guarantee that the result returned 238 | does in fact reflect what would be returned to a 239 | client query actually originating from that netblock. 240 | .Sh EXAMPLES 241 | The following examples illustrate common usage of this tool. 242 | .Pp 243 | To look up the IP addresses for www.yahoo.com on the 244 | short list of public resolvers, limiting to one query 245 | per organization: 246 | .Bd -literal -offset indent 247 | $ puddy -1 www.yahoo.com 248 | 2001:470:20::2 (Hurricane Electric) 249 | A (20): 72.30.35.10 250 | A (20): 72.30.35.9 251 | AAAA (56): 2001:4998:58:1836::11 252 | AAAA (56): 2001:4998:58:1836::10 253 | CNAME (1759): atsv2-fp-shed.wg1.b.yahoo.com. 254 | 2001:4860:4860::8888 (Google) 255 | A (15): 98.138.219.231 256 | A (15): 72.30.35.9 257 | A (15): 72.30.35.10 258 | A (15): 98.138.219.232 259 | AAAA (45): 2001:4998:44:41d::4 260 | AAAA (45): 2001:4998:44:41d::3 261 | AAAA (45): 2001:4998:58:1836::11 262 | AAAA (45): 2001:4998:58:1836::10 263 | CNAME (77): atsv2-fp-shed.wg1.b.yahoo.com. 264 | 2606:4700:4700::1001 (Cloudflare) 265 | A (38): 72.30.35.10 266 | A (38): 72.30.35.9 267 | AAAA (33): 2001:4998:58:1836::10 268 | AAAA (33): 2001:4998:58:1836::11 269 | CNAME (15): atsv2-fp-shed.wg1.b.yahoo.com. 270 | 2620:0:ccc::2 (OpenDNS) 271 | A (18): 72.30.35.9 272 | A (18): 72.30.35.10 273 | AAAA (45): 2001:4998:58:1836::10 274 | AAAA (45): 2001:4998:58:1836::11 275 | CNAME (1531): atsv2-fp-shed.wg1.b.yahoo.com. 276 | 2620:fe::fe (Quad9) 277 | A (15): 72.30.35.9 278 | A (15): 72.30.35.10 279 | AAAA (56): 2001:4998:58:1836::11 280 | AAAA (56): 2001:4998:58:1836::10 281 | CNAME (772): atsv2-fp-shed.wg1.b.yahoo.com. 282 | 172.131.44.74 (/etc/resolv.conf) 283 | A (46): 74.6.143.8 284 | AAAA (51): 2001:4998:58:207::1000 285 | CNAME (1800): atsv2-fp-shed.wg1.b.yahoo.com. 286 | .Ed 287 | .Pp 288 | To only query at most 2 IPv6 resolvers from the public list of 289 | public resolvers for NS records, one of which does not 290 | respond in time: 291 | .Bd -literal -offset indent 292 | $ puddy -6 -n 2 -p netmeister.org NS 293 | 2610:a1:1019::31 294 | NS: timed out 295 | 2610:a1:1019::32 296 | NS (10799): ns-143-b.gandi.net. 297 | NS (10799): ns-179-c.gandi.net. 298 | NS (10799): ns-181-a.gandi.net. 299 | .Ed 300 | .Pp 301 | To query 3 DoH providers: 302 | .Bd -literal -offset indent 303 | $ puddy -n 3 -d _443._tcp.mta-sts.netmeister.org TLSA 304 | DNS.SB (https://doh.dns.sb/dns-query?) 305 | TLSA (3600): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 306 | Google (https://dns.google/resolve?) 307 | TLSA (3599): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 308 | Quad9 (https://9.9.9.9:5053/dns-query?) 309 | TLSA (3600): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 310 | .Ed 311 | .Pp 312 | To get DoH results with an EDNS Client Subnet set to a 313 | netblock from China: 314 | .Bd -literal -offset indent 315 | $ puddy -d -c cn www.google.cn 316 | Cloudflare (https://cloudflare-dns.com/dns-query?) 317 | A (105): 172.217.11.3 318 | AAAA (55): 2607:f8b0:4006:812::2003 319 | DNS.SB (https://doh.dns.sb/dns-query?) (ECS 45.126.116.0/22) 320 | A (300): 203.208.39.207 321 | A (300): 203.208.39.216 322 | A (300): 203.208.39.223 323 | A (300): 203.208.39.215 324 | Google (https://dns.google/resolve?) (ECS 45.126.116.0/22) 325 | A (299): 203.208.39.223 326 | A (299): 203.208.39.207 327 | A (299): 203.208.39.215 328 | A (299): 203.208.39.216 329 | Quad9 (https://9.9.9.9:5053/dns-query?) 330 | A (300): 172.217.7.3 331 | AAAA (300): 2607:f8b0:4006:801::2003 332 | doh.li (https://doh.li/dns-query?) (ECS 45.126.116.0/22) 333 | A (300): 203.208.39.216 334 | A (300): 203.208.39.215 335 | A (300): 203.208.39.207 336 | A (300): 203.208.39.223 337 | .Ed 338 | .Pp 339 | To get the results from the resolvers specified in the 340 | file /usr/local/share/puddy.resolvers and generate 341 | output in json format: 342 | .Bd -literal -offset indent 343 | $ puddy -j -f /usr/local/share/puddy.resolvers whocybered.me txt 344 | { 345 | "results" : { 346 | "209.244.0.3" : { 347 | "TXT" : { 348 | "status" : "NOERROR", 349 | "rrs" : [ 350 | { 351 | "value" : "\\"Attribution is hard. Cyber doubly so. When in doubt, APT.\\"", 352 | "ttl" : 10795 353 | } 354 | ] 355 | }, 356 | "comment" : "/tmp/f" 357 | }, 358 | "2620:74:1b::1:1" : { 359 | "TXT" : { 360 | "status" : "NOERROR", 361 | "rrs" : [ 362 | { 363 | "ttl" : 10794, 364 | "value" : "\\"Attribution is hard. Cyber doubly so. When in doubt, APT.\\"" 365 | } 366 | ] 367 | }, 368 | "comment" : "/tmp/f" 369 | } 370 | }, 371 | "query" : "whocybered.me" 372 | } 373 | .Ed 374 | .Sh EXIT STATUS 375 | .Ex -std 376 | .Sh NOTES 377 | Feels like an Arby's night. 378 | .Sh SEE ALSO 379 | .Xr dig 1 , 380 | .Xr host 1 , 381 | .Xr nslookup 1 382 | .Pp 383 | RFC7871 384 | .Sh HISTORY 385 | .Nm 386 | was originally written by 387 | .An Jan Schaumann 388 | .Aq jschauma@netmeister.org 389 | in September 2019. 390 | .Sh BUGS 391 | Please file bugs and feature requests by emailing the author. 392 | -------------------------------------------------------------------------------- /htdocs/index.cgi: -------------------------------------------------------------------------------- 1 | #! /usr/pkg/bin/perl -Tw 2 | # 3 | # Originally written by Jan Schaumann 4 | # in September 2019. 5 | # 6 | # This CGI provides a web frontend to puddy(1): 7 | # https://github.com/jschauma/puddy 8 | 9 | use strict; 10 | use CGI qw(:standard); 11 | 12 | print "Content-Type: text/html; charset=utf-8\n\n"; 13 | 14 | ### 15 | ### Globals 16 | ### 17 | 18 | my $PUDDY = "/usr/local/bin/puddy"; 19 | my $RESOLVERS = "/htdocs/netmeister.org/puddy/resolvers"; 20 | my @CMD = ( $PUDDY, "-1" ); 21 | 22 | # per http://services.ce3c.be/ciprg/ 23 | my @COUNTRIES = ( 24 | "AFGHANISTAN", 25 | "ALAND ISLANDS", 26 | "ALBANIA", 27 | "ALGERIA", 28 | "AMERICAN SAMOA", 29 | "ANDORRA", 30 | "ANGOLA", 31 | "ANGUILLA", 32 | "ANTARCTICA", 33 | "ANTIGUA AND BARBUDA", 34 | "ARGENTINA", 35 | "ARMENIA", 36 | "ARUBA", 37 | "AUSTRALIA", 38 | "AUSTRIA", 39 | "AZERBAIJAN", 40 | "BAHAMAS", 41 | "BAHRAIN", 42 | "BANGLADESH", 43 | "BARBADOS", 44 | "BELARUS", 45 | "BELGIUM", 46 | "BELIZE", 47 | "BENIN", 48 | "BERMUDA", 49 | "BHUTAN", 50 | "BOLIVIA", 51 | "BONAIRE; SINT EUSTATIUS; SABA", 52 | "BOSNIA AND HERZEGOWINA", 53 | "BOTSWANA", 54 | "BRAZIL", 55 | "BRITISH INDIAN+OCEAN TERRITORY", 56 | "BRUNEI DARUSSALAM", 57 | "BULGARIA", 58 | "BURKINA FASO", 59 | "BURUNDI", 60 | "CAMBODIA", 61 | "CAMEROON", 62 | "CANADA", 63 | "CAPE VERDE", 64 | "CAYMAN ISLANDS", 65 | "CENTRAL AFRICAN REPUBLIC", 66 | "CHAD", 67 | "CHILE", 68 | "CHINA", 69 | "COLOMBIA", 70 | "COMOROS", 71 | "CONGO", 72 | "CONGO THE DEMOCRATIC REPUBLIC OF THE", 73 | "COOK ISLANDS", 74 | "COSTA RICA", 75 | "COTE D'IVOIRE", 76 | "CROATIA (LOCAL NAME: HRVATSKA)", 77 | "CUBA", 78 | "CURACAO", 79 | "CYPRUS", 80 | "CZECH REPUBLIC", 81 | "DENMARK", 82 | "DJIBOUTI", 83 | "DOMINICA", 84 | "DOMINICAN REPUBLIC", 85 | "ECUADOR", 86 | "EGYPT", 87 | "EL SALVADOR", 88 | "EQUATORIAL GUINEA", 89 | "ERITREA", 90 | "ESTONIA", 91 | "ETHIOPIA", 92 | "EUROPEAN UNION", 93 | "FALKLAND ISLANDS (MALVINAS)", 94 | "FAROE ISLANDS", 95 | "FIJI", 96 | "FINLAND", 97 | "FRANCE", 98 | "FRENCH GUIANA", 99 | "FRENCH POLYNESIA", 100 | "GABON", 101 | "GAMBIA", 102 | "GEORGIA", 103 | "GERMANY", 104 | "GHANA", 105 | "GIBRALTAR", 106 | "GREECE", 107 | "GREENLAND", 108 | "GRENADA", 109 | "GUADELOUPE", 110 | "GUAM", 111 | "GUATEMALA", 112 | "GUERNSEY", 113 | "GUINEA", 114 | "GUINEA-BISSAU", 115 | "GUYANA", 116 | "HAITI", 117 | "HOLY SEE (VATICAN CITY STATE)", 118 | "HONDURAS", 119 | "HONG KONG", 120 | "HUNGARY", 121 | "ICELAND", 122 | "INDIA", 123 | "INDONESIA", 124 | "IRAN (ISLAMIC REPUBLIC OF)", 125 | "IRAQ", 126 | "IRELAND", 127 | "ISLE OF MAN", 128 | "ISRAEL", 129 | "ITALY", 130 | "JAMAICA", 131 | "JAPAN", 132 | "JERSEY", 133 | "JORDAN", 134 | "KAZAKHSTAN", 135 | "KENYA", 136 | "KIRIBATI", 137 | "KOREA DEMOCRATIC PEOPLE's REPUBLIC OF", 138 | "KOREA REPUBLIC OF", 139 | "KUWAIT", 140 | "KYRGYZSTAN", 141 | "LAO PEOPLE'S DEMOCRATIC REPUBLIC", 142 | "LATVIA", 143 | "LEBANON", 144 | "LESOTHO", 145 | "LIBERIA", 146 | "LIBYAN ARAB JAMAHIRIYA", 147 | "LIECHTENSTEIN", 148 | "LITHUANIA", 149 | "LUXEMBOURG", 150 | "MACAU", 151 | "MACEDONIA", 152 | "MADAGASCAR", 153 | "MALAWI", 154 | "MALAYSIA", 155 | "MALDIVES", 156 | "MALI", 157 | "MALTA", 158 | "MARSHALL ISLANDS", 159 | "MARTINIQUE", 160 | "MAURITANIA", 161 | "MAURITIUS", 162 | "MAYOTTE", 163 | "MEXICO", 164 | "MICRONESIA FEDERATED STATES OF", 165 | "MOLDOVA REPUBLIC OF", 166 | "MONACO", 167 | "MONGOLIA", 168 | "MONTENEGRO", 169 | "MONTSERRAT", 170 | "MOROCCO", 171 | "MOZAMBIQUE", 172 | "MYANMAR", 173 | "NAMIBIA", 174 | "NAURU", 175 | "NEPAL", 176 | "NETHERLANDS", 177 | "NEW CALEDONIA", 178 | "NEW ZEALAND", 179 | "NICARAGUA", 180 | "NIGER", 181 | "NIGERIA", 182 | "NIUE", 183 | "NON-SPEC ASIA PAS LOCATION", 184 | "NORFOLK ISLAND", 185 | "NORTHERN MARIANA ISLANDS", 186 | "NORWAY", 187 | "OMAN", 188 | "PAKISTAN", 189 | "PALAU", 190 | "PALESTINIAN TERRITORY OCCUPIED", 191 | "PANAMA", 192 | "PAPUA NEW GUINEA", 193 | "PARAGUAY", 194 | "PERU", 195 | "PHILIPPINES", 196 | "POLAND", 197 | "PORTUGAL", 198 | "PUERTO RICO", 199 | "QATAR", 200 | "RESERVED", 201 | "REUNION", 202 | "ROMANIA", 203 | "RUSSIAN FEDERATION", 204 | "RWANDA", 205 | "SAINT KITTS AND NEVIS", 206 | "SAINT LUCIA", 207 | "SAINT MARTIN", 208 | "SAINT VINCENT AND THE GRENADINES", 209 | "SAMOA", 210 | "SAN MARINO", 211 | "SAO TOME AND PRINCIPE", 212 | "SAUDI ARABIA", 213 | "SENEGAL", 214 | "SERBIA", 215 | "SEYCHELLES", 216 | "SIERRA LEONE", 217 | "SINGAPORE", 218 | "SINT MAARTEN", 219 | "SLOVAKIA (SLOVAK REPUBLIC)", 220 | "SLOVENIA", 221 | "SOLOMON ISLANDS", 222 | "SOMALIA", 223 | "SOUTH AFRICA", 224 | "SOUTH SUDAN", 225 | "SPAIN", 226 | "SRI LANKA", 227 | "ST. PIERRE AND MIQUELON", 228 | "SUDAN", 229 | "SURINAME", 230 | "SWAZILAND", 231 | "SWEDEN", 232 | "SWITZERLAND", 233 | "SYRIAN ARAB REPUBLIC", 234 | "TAIWAN; REPUBLIC OF CHINA (ROC)", 235 | "TAJIKISTAN", 236 | "TANZANIA UNITED REPUBLIC OF", 237 | "THAILAND", 238 | "TIMOR-LESTE", 239 | "TOGO", 240 | "TOKELAU", 241 | "TONGA", 242 | "TRINIDAD AND TOBAGO", 243 | "TUNISIA", 244 | "TURKEY", 245 | "TURKMENISTAN", 246 | "TURKS AND CAICOS ISLANDS", 247 | "TUVALU", 248 | "UGANDA", 249 | "UKRAINE", 250 | "UNITED ARAB EMIRATES", 251 | "UNITED KINGDOM", 252 | "UNITED STATES", 253 | "URUGUAY", 254 | "UZBEKISTAN", 255 | "VANUATU", 256 | "VENEZUELA", 257 | "VIET NAM", 258 | "VIRGIN ISLANDS (BRITISH)", 259 | "VIRGIN ISLANDS (U.S.)", 260 | "WALLIS AND FUTUNA ISLANDS", 261 | "YEMEN", 262 | "ZAMBIA", 263 | "ZIMBABWE", 264 | 265 | ); 266 | 267 | $ENV{'PATH'} = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/pkg/bin"; 268 | 269 | my $CGI = new CGI; 270 | 271 | ### 272 | ### Functions 273 | ### 274 | 275 | sub printError($$) { 276 | my ($err, $format) = @_; 277 | 278 | if ($format eq "json") { 279 | print "{ \"Error\" : \"$err\" }"; 280 | } else { 281 | print "$err\n"; 282 | } 283 | } 284 | 285 | sub runPuddy() { 286 | 287 | my $error = 0; 288 | my $format = $CGI->param('format'); 289 | if ($format eq "json") { 290 | push(@CMD, "-j"); 291 | } 292 | 293 | if ($CGI->param('lookup') eq "doh") { 294 | push(@CMD, "-d"); 295 | my $c = $CGI->param('country'); 296 | if ($c && ($c ne "default") && ($c =~ m/^([a-z;:()']+)$/i)) { 297 | push(@CMD, "-c", $1); 298 | } 299 | } else { 300 | push(@CMD, "-f", $RESOLVERS); 301 | } 302 | 303 | my $name = $CGI->param('name'); 304 | if (!$name) { 305 | printError("Missing name", $format); 306 | $error = 1; 307 | } 308 | 309 | if ($name =~ m/^([a-z0-9.-]+)$/i) { 310 | $name = $1; 311 | } else { 312 | printError("Invalid name: $name", $format); 313 | $error = 1; 314 | } 315 | push(@CMD, $name); 316 | 317 | my @types; 318 | if ($CGI->param('type')) { 319 | foreach my $t (split(/,|\s/, $CGI->param('type'))) { 320 | if ($t =~ m/^([a-z]+)/i) { 321 | push(@types, $1); 322 | } else { 323 | printError("Invalid type: $t", $format); 324 | $error = 1; 325 | } 326 | } 327 | if (scalar(@types)) { 328 | push(@CMD, @types); 329 | } 330 | } else { 331 | push(@types, "A", "AAAA", "CNAME"); 332 | } 333 | 334 | if (!$error) { 335 | if ($format ne "json") { 336 | print "Name: $name
\n"; 337 | print "Type: " . join(", ", @types) . "
\n"; 338 | print "Lookup type: " . $CGI->param('lookup') . "
\n"; 339 | if ($CGI->param('country') && $CGI->param('country') ne "default") { 340 | print "ECS Country Netblock: " . $CGI->param('country') ."
\n"; 341 | } 342 | print "
Query sent, sit tight...
\n"; 343 | print "\"David
\n"; 344 | print "\n\n
\n";
345 | 		}
346 | 		system(@CMD);
347 | 		if ($format ne "json") {
348 | 			print "
\n"; 349 | } 350 | } 351 | } 352 | 353 | sub printHead() { 354 | print < 356 | 357 | puddy -- because sometimes you gotta ask the magic 8.8.8.8 ball 358 | 359 | 364 | 365 | 366 |

Compare public DNS resolver results

367 |
368 | EOD 369 | ; 370 | } 371 | 372 | sub printInstructions() { 373 | print < 375 | David Puddy pointing to his 8-ball jacket 376 | Sometimes it can be useful to check whether a given 377 | hostname or record resolves to the same value in 378 | different places on the internet, so you go and ask 379 | the magic 8.8.8.8 ball. But perhaps other resolvers 380 | elsewhere have different results? Manually querying 381 | multiple resolvers is bogus, man, so this service 382 | does the work for you. 383 |

384 |

385 | Enter a name and record type to look up and puddy 387 | will take care of the rest. 388 |

389 |

390 | By default, this service will query a bunch of public 391 | DNS resolvers via regular UDP port 53 DNS queries; 392 | if you choose DNS-over-HTTPS, 394 | several public DoH providers will be asked instead. 395 |

396 |

397 | If you select an ECS Country Subnet, we'll try to set 398 | the EDNS 400 | Client Subnet parameter to a subnet allocated to 401 | that country in the hopes that the DoH providers will 402 | honor it and return results for that given ECS. 403 |

404 |

405 | Finally, if HTML isn't your thing, you can also 406 | request JSON output, which then lends itself to 407 | querying this service from the command-line via e.g., 408 |

curl "https://www.netmeister.org/puddy/?name=wikipedia.org&type=A&format=json&lookup=doh&country=CHINA" | python -m json.tool
409 |

410 |
411 | EOD 412 | ; 413 | } 414 | 415 | sub printFoot() { 416 | print < 418 | [Made by \@jschauma | [about] | [Other Signs of Triviality] 419 | 420 | 421 | EOD 422 | ; 423 | } 424 | 425 | sub printForm() { 426 | print < 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 442 | 443 | 444 | 447 | 448 | 449 | 460 | 461 | 464 | 465 |
Name to look up:
Type:
Output format:HTML
440 | JSON
441 |
Lookup type:Normal
445 | DNS-over-HTTPS
446 |
ECS Country Netblock (implies DoH):
462 | 463 |
466 | 467 | EOD 468 | ; 469 | } 470 | 471 | ### 472 | ### Main 473 | ### 474 | 475 | my $format = $CGI->param('format'); 476 | if (!$format || $format eq "html") { 477 | printHead(); 478 | } 479 | 480 | if (!$format) { 481 | printInstructions(); 482 | printForm(); 483 | } else { 484 | runPuddy(); 485 | if ($format eq "html") { 486 | print '
'; 487 | printForm(); 488 | } 489 | } 490 | 491 | if (!$format || $format eq "html") { 492 | printFoot(); 493 | } 494 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puddy - because sometimes you gotta ask the magic 8.8.8.8 ball 2 | 3 | `puddy(1)` is a tool to successively query (public) 4 | DNS servers and present to you their results. 5 | 6 | Please see the [manual 7 | page](https://github.com/jschauma/puddy/blob/master/doc/puddy.1.txt) 8 | for details. 9 | 10 | `puddy(1)` has a web interface at 11 | https://www.netmeister.org/puddy/. That's right, 12 | puddy as a service. High-five! 13 | 14 | ## Requirements 15 | 16 | `puddy(1)` is old-school. You'll need to have Perl 17 | and the following modules installed: 18 | 19 | * IO::Socket::INET6 20 | * JSON 21 | * Net::DNS 22 | * Net::IP 23 | * Net::Netmask 24 | * Parallel::ForkManager 25 | 26 | ## Installation 27 | 28 | You can install `puddy(1)` by running `make install`. 29 | The Makefile defaults to '/usr/local' as the prefix, 30 | but you can change that, if you like: 31 | 32 | ``` 33 | $ make PREFIX=~ install 34 | ``` 35 | 36 | --- 37 | ``` 38 | NAME 39 | puddy -- public DNS data yielder 40 | 41 | SYNOPSIS 42 | puddy [-146Vdhjpv] [-c country] [-e cidr] [-f file] [-n num] 43 | [-r resolver] query [type ...] 44 | 45 | DESCRIPTION 46 | The puddy tool allows you to query a number of different resolvers to 47 | compare whether results are uniform across the internet. 48 | 49 | OPTIONS 50 | The following options are supported by puddy: 51 | 52 | -1 Only use a single resolver from one and the same organiza- 53 | tion. This option cannot be combined with the -p flag. See 54 | Section PUBLIC RESOLVERS for details. 55 | 56 | -4 Only query IPv4 resolvers. 57 | 58 | -6 Only query IPv6 resolvers. 59 | 60 | -V Print version number and exit. 61 | 62 | -c country Try to use the EDNS Client Subnet (ECS) extension to elicit 63 | a response from the DNS server as if the client was located 64 | in the given country. 65 | 66 | The argument country can be a country code or the name of a 67 | country. 68 | 69 | See Section COUNTRY IMITATION for details. 70 | 71 | -d Use DNS-over-HTTPS. This option only works with a subset of 72 | the known public resolvers and thus conflicts with e.g., the 73 | -f and -p flags. 74 | 75 | -e cidr Try to use the given CIDR via the EDNS Client Subnet (ECS) 76 | extension. 77 | 78 | -f file Read the list of resolvers to query from this file. This 79 | option conflicts with the -p flag. See Section FILES for 80 | details. 81 | 82 | -h Display help and exit. 83 | 84 | -j Print output in json format. 85 | 86 | -n num Only query at most this many resolvers. 87 | 88 | -p Query all known public DNS resolvers. This option conflicts 89 | with the -f flag. See Section PUBLIC RESOLVERS for details. 90 | 91 | -r resolver Add the given resolver to the list of resolvers to query. 92 | Can be specified multiple times. 93 | 94 | -v Be verbose. Can be specified multiple times. 95 | 96 | DETAILS 97 | Sometimes it can be useful to check whether a given hostname or record 98 | resolves to the same value in different places on the internet, so you go 99 | and ask the magic 8.8.8.8 ball. But perhaps other resolvers elsewhere 100 | have different results? Manually querying multiple resolvers is labor- 101 | some, so instead puddy saves you some typing by performing these lookups 102 | in succession for you. 103 | 104 | By default, puddy will query resolvers for A, AAAA, and CNAME records, 105 | similar to the host(1) command. 106 | 107 | PUBLIC RESOLVERS 108 | If the -p flag is given, puddy will retrieve the list of known public DNS 109 | resolvers from https://public-dns.info/nameservers.txt and query each 110 | one. 111 | 112 | Otherwise, if the -f flag is given, puddy will read the list of resolvers 113 | to query from the specified file. 114 | 115 | Many organizations offer public DNS services on multiple IP addresses; if 116 | the list of resolvers to use contains multiple IP address for the same 117 | organization, and you only want one of those servers, specify the -1 118 | (numeric one) flag. puddy will then pick only one of the IPs for each 119 | organization from the input set. Note: If an organization's resolvers 120 | are reachable via both IPv4 and IPv6 addresses, then puddy will prefer 121 | the IPv6 address. 122 | 123 | If the -d flag is specified, puddy will query a short list of hardcoded 124 | public resolvers known to support DNS-over-HTTPS with results returned in 125 | JSON format: 126 | Cloudflare 127 | DNS.SB 128 | Google 129 | Quad9 130 | doh.li 131 | 132 | If neither the -d, -f, or -p flag is specified, puddy will only query a 133 | short list of hardocded, popular public resolvers, which consists of: 134 | Cloudflare 135 | Google 136 | Hurricane Electric 137 | OpenDNS 138 | Quad9 139 | 140 | In addition, puddy will also query the resolvers found in 141 | /etc/resolv.conf. 142 | 143 | Finally, if the -n flag is specified, then at most this many resolvers 144 | will be queried; the list is randomized before the selection of resolvers 145 | to query is made. 146 | 147 | FILES 148 | puddy may read a list of public resolvers to query from a file provided 149 | via the -f flag. Each line in the file is expected to consist of an IP 150 | address and an optional comment or identifier. Leading and trailing 151 | whitespace as well as empty lines and anything following the '#' charac- 152 | ter are ignored. 153 | 154 | An example puddy.resolvers file might look like so: 155 | 156 | # puddy.resolvers file 157 | 216.146.35.35 Dyn 158 | 2001:470:1f10:c6::2001 OpenNIC 159 | 10.0.0.1 My Private Resolver # not /etc/resolv.conf! 160 | 161 | puddy may have come with a list of public resolvers in 162 | /usr/local/share/puddy.resolvers. 163 | 164 | COUNTRY IMITATION 165 | Sometimes it is useful to be able to see if different resolvers might 166 | give different responses based on where the client is located. puddy 167 | supports the -c country option to attempt to elicit a response from the 168 | DNS server as if the client was in the given location. This is done 169 | through the use of the EDNS Client Subnet extension (ECS, see RFC7871). 170 | 171 | Note: at this time, puddy only supports this option when performing 172 | queries using DNS over HTTPS (i.e., in combination with the -d option). 173 | 174 | When specified, puddy will try to look up a suitable netblock for the 175 | given country via data from the site https://www.nirsoft.net/countryip/, 176 | then set the ECS option. 177 | 178 | If you wish to disable the use of the ECS extension altogether, then you 179 | can pass 'none' as an argument to the -c flag, yielding a netblock of 180 | 0.0.0.0/0. 181 | 182 | Note: not all DoH providers honor this option, but puddy has no way of 183 | telling the difference. In addition, even for those resolvers that do 184 | support it, there is no guarantee that the result returned does in fact 185 | reflect what would be returned to a client query actually originating 186 | from that netblock. 187 | 188 | EXAMPLES 189 | The following examples illustrate common usage of this tool. 190 | 191 | To look up the IP addresses for www.yahoo.com on the short list of public 192 | resolvers, limiting to one query per organization: 193 | 194 | $ puddy -1 www.yahoo.com 195 | 2001:470:20::2 (Hurricane Electric) 196 | A (20): 72.30.35.10 197 | A (20): 72.30.35.9 198 | AAAA (56): 2001:4998:58:1836::11 199 | AAAA (56): 2001:4998:58:1836::10 200 | CNAME (1759): atsv2-fp-shed.wg1.b.yahoo.com. 201 | 2001:4860:4860::8888 (Google) 202 | A (15): 98.138.219.231 203 | A (15): 72.30.35.9 204 | A (15): 72.30.35.10 205 | A (15): 98.138.219.232 206 | AAAA (45): 2001:4998:44:41d::4 207 | AAAA (45): 2001:4998:44:41d::3 208 | AAAA (45): 2001:4998:58:1836::11 209 | AAAA (45): 2001:4998:58:1836::10 210 | CNAME (77): atsv2-fp-shed.wg1.b.yahoo.com. 211 | 2606:4700:4700::1001 (Cloudflare) 212 | A (38): 72.30.35.10 213 | A (38): 72.30.35.9 214 | AAAA (33): 2001:4998:58:1836::10 215 | AAAA (33): 2001:4998:58:1836::11 216 | CNAME (15): atsv2-fp-shed.wg1.b.yahoo.com. 217 | 2620:0:ccc::2 (OpenDNS) 218 | A (18): 72.30.35.9 219 | A (18): 72.30.35.10 220 | AAAA (45): 2001:4998:58:1836::10 221 | AAAA (45): 2001:4998:58:1836::11 222 | CNAME (1531): atsv2-fp-shed.wg1.b.yahoo.com. 223 | 2620:fe::fe (Quad9) 224 | A (15): 72.30.35.9 225 | A (15): 72.30.35.10 226 | AAAA (56): 2001:4998:58:1836::11 227 | AAAA (56): 2001:4998:58:1836::10 228 | CNAME (772): atsv2-fp-shed.wg1.b.yahoo.com. 229 | 172.131.44.74 (/etc/resolv.conf) 230 | A (46): 74.6.143.8 231 | AAAA (51): 2001:4998:58:207::1000 232 | CNAME (1800): atsv2-fp-shed.wg1.b.yahoo.com. 233 | 234 | To only query at most 2 IPv6 resolvers from the public list of public 235 | resolvers for NS records, one of which does not respond in time: 236 | 237 | $ puddy -6 -n 2 -p netmeister.org NS 238 | 2610:a1:1019::31 239 | NS: timed out 240 | 2610:a1:1019::32 241 | NS (10799): ns-143-b.gandi.net. 242 | NS (10799): ns-179-c.gandi.net. 243 | NS (10799): ns-181-a.gandi.net. 244 | 245 | To query 3 DoH providers: 246 | 247 | $ puddy -n 3 -d _443._tcp.mta-sts.netmeister.org TLSA 248 | DNS.SB (https://doh.dns.sb/dns-query?) 249 | TLSA (3600): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 250 | Google (https://dns.google/resolve?) 251 | TLSA (3599): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 252 | Quad9 (https://9.9.9.9:5053/dns-query?) 253 | TLSA (3600): 3 1 1 905254acd0785b76b76b42da2c419d065b2442427883f133c9305f2010ae6397 254 | 255 | To get DoH results with an EDNS Client Subnet set to a netblock from 256 | China: 257 | 258 | $ puddy -d -c cn www.google.cn 259 | Cloudflare (https://cloudflare-dns.com/dns-query?) 260 | A (105): 172.217.11.3 261 | AAAA (55): 2607:f8b0:4006:812::2003 262 | DNS.SB (https://doh.dns.sb/dns-query?) (ECS 45.126.116.0/22) 263 | A (300): 203.208.39.207 264 | A (300): 203.208.39.216 265 | A (300): 203.208.39.223 266 | A (300): 203.208.39.215 267 | Google (https://dns.google/resolve?) (ECS 45.126.116.0/22) 268 | A (299): 203.208.39.223 269 | A (299): 203.208.39.207 270 | A (299): 203.208.39.215 271 | A (299): 203.208.39.216 272 | Quad9 (https://9.9.9.9:5053/dns-query?) 273 | A (300): 172.217.7.3 274 | AAAA (300): 2607:f8b0:4006:801::2003 275 | doh.li (https://doh.li/dns-query?) (ECS 45.126.116.0/22) 276 | A (300): 203.208.39.216 277 | A (300): 203.208.39.215 278 | A (300): 203.208.39.207 279 | A (300): 203.208.39.223 280 | 281 | To get the results from the resolvers specified in the file 282 | /usr/local/share/puddy.resolvers and generate output in json format: 283 | 284 | $ puddy -j -f /usr/local/share/puddy.resolvers whocybered.me txt 285 | { 286 | "results" : { 287 | "209.244.0.3" : { 288 | "TXT" : { 289 | "status" : "NOERROR", 290 | "rrs" : [ 291 | { 292 | "value" : "\"Attribution is hard. Cyber doubly so. When in doubt, APT.\"", 293 | "ttl" : 10795 294 | } 295 | ] 296 | }, 297 | "comment" : "/tmp/f" 298 | }, 299 | "2620:74:1b::1:1" : { 300 | "TXT" : { 301 | "status" : "NOERROR", 302 | "rrs" : [ 303 | { 304 | "ttl" : 10794, 305 | "value" : "\"Attribution is hard. Cyber doubly so. When in doubt, APT.\"" 306 | } 307 | ] 308 | }, 309 | "comment" : "/tmp/f" 310 | } 311 | }, 312 | "query" : "whocybered.me" 313 | } 314 | 315 | EXIT STATUS 316 | The puddy utility exits 0 on success, and >0 if an error occurs. 317 | 318 | NOTES 319 | Feels like an Arby's night. 320 | 321 | SEE ALSO 322 | dig(1), host(1), nslookup(1) 323 | 324 | RFC7871 325 | 326 | HISTORY 327 | puddy was originally written by Jan Schaumann 328 | in September 2019. 329 | 330 | BUGS 331 | Please file bugs and feature requests by emailing the author. 332 | ``` 333 | -------------------------------------------------------------------------------- /src/puddy.pl: -------------------------------------------------------------------------------- 1 | #! /usr/local/bin/perl -Tw 2 | # 3 | # Copyright 2019, Verizon Media 4 | # 5 | # Licensed under the terms of the BSD 3-Clause License. 6 | # See LICENSE file for terms. 7 | # 8 | # This tool allows you to query a number of different 9 | # resolvers to compare whether results are uniform 10 | # across the internet. 11 | 12 | use 5.008; 13 | 14 | use strict; 15 | use File::Basename; 16 | use Getopt::Long; 17 | use IO::Socket::INET6; 18 | use JSON; 19 | use Locale::Country qw(code2country country2code); 20 | use List::Util 'shuffle'; 21 | use Net::DNS; 22 | use Net::DNS::Parameters qw(typebyval rcodebyval); 23 | use Net::IP; 24 | use Net::Netmask; 25 | use Parallel::ForkManager; 26 | use Socket qw(PF_INET PF_INET6 inet_ntoa inet_pton); 27 | use URI::Escape; 28 | 29 | Getopt::Long::Configure("bundling"); 30 | 31 | # We untaint the whole path, because we do allow the 32 | # user to change it to point to a curl(1) of their 33 | # preference. 34 | my $safepath = $ENV{PATH}; 35 | if ($safepath =~ m/(.*)/) { 36 | $ENV{PATH} = $1; 37 | } 38 | 39 | delete($ENV{CDPATH}); 40 | delete($ENV{ENV}); 41 | 42 | ### 43 | ### Constants 44 | ### 45 | 46 | use constant EXIT_FAILURE => 1; 47 | use constant EXIT_SUCCESS => 0; 48 | 49 | use constant PUBLIC_URL => "https://public-dns.info/nameservers.txt"; 50 | use constant COUNTRY_IP_URL => "https://www.nirsoft.net/countryip/"; 51 | 52 | use constant VERSION => 1.6; 53 | 54 | ### 55 | ### Globals 56 | ### 57 | 58 | # We start with a small default; we may bump this up 59 | # based on 'ulimit -n' below. 60 | my $CONCURRENCY = 8; 61 | my %OPTS; 62 | my $PROGNAME = basename($0); 63 | my $RETVAL = 0; 64 | 65 | my %RESULTS; 66 | 67 | my $EDNS_CLIENT_SUBNET; 68 | 69 | my %DOH = ( 70 | "https://cloudflare-dns.com/dns-query?" => "Cloudflare", 71 | "https://doh.li/dns-query?" => "doh.li", 72 | "https://doh.dns.sb/dns-query?" => "DNS.SB", 73 | "https://dns.google/resolve?" => "Google", 74 | "https://9.9.9.9:5053/dns-query?" => "Quad9", 75 | "https://dns0.eu/dns-query?" => "dns0.eu", 76 | ); 77 | 78 | my %RESOLVERS = ( 79 | "ipv4" => { 80 | "1.0.0.1" => "Cloudflare", 81 | "1.1.1.1" => "Cloudflare", 82 | "8.8.4.4" => "Google", 83 | "8.8.8.8" => "Google", 84 | "9.9.9.9" => "Quad9", 85 | "74.82.42.42" => "Hurricane Electric", 86 | "193.110.81.0" => "dns0.eu", 87 | "185.253.5.0" => "dns0.eu", 88 | "208.67.220.220" => "OpenDNS", 89 | "208.67.222.222" => "OpenDNS", 90 | }, 91 | "ipv6" => { 92 | "2001:470:20::2" => "Hurricane Electric", 93 | "2001:4860:4860::8844" => "Google", 94 | "2001:4860:4860::8888" => "Google", 95 | "2606:4700:4700::1001" => "Cloudflare", 96 | "2606:4700:4700::1111" => "Cloudflare", 97 | "2620:0:ccc::2" => "OpenDNS", 98 | "2620:0:ccd::2" => "OpenDNS", 99 | "2620:fe::fe" => "Quad9", 100 | "2a0f:fc80::" => "dns0.eu", 101 | "2a0f:fc81::" => "dns0.eu", 102 | }, 103 | ); 104 | 105 | ### 106 | ### Subroutines 107 | ### 108 | 109 | 110 | sub checkPTR() { 111 | if (inet_pton(PF_INET, $OPTS{'query'}) || inet_pton(PF_INET6, $OPTS{'query'})) { 112 | if (scalar(keys(%{$OPTS{'wanted'}}))) { 113 | if ((scalar(@{$OPTS{'types'}}) > 1) || !$OPTS{'wanted'}{'PTR'}) { 114 | error("Unable to lookup any RRs of types other than PTR for IP addresses.", EXIT_FAILURE); 115 | # NOTREACHED 116 | } 117 | } 118 | my $ip = new Net::IP($OPTS{'query'}); 119 | $OPTS{'query'} = $ip->reverse_ip(); 120 | my @types = ( "PTR" ); 121 | $OPTS{'types'} = \@types; 122 | } elsif (grep(/PTR/, @{$OPTS{'types'}})) { 123 | error("You can't lookup a PTR for something that's not an IP address.", EXIT_FAILURE); 124 | # NOTREACHED 125 | } 126 | } 127 | 128 | sub error($;$) { 129 | my ($msg, $err) = @_; 130 | 131 | if (!$OPTS{'q'}) { 132 | print STDERR "$PROGNAME: $msg\n"; 133 | } 134 | $RETVAL++; 135 | 136 | if ($err) { 137 | exit($err); 138 | # NOTREACHED 139 | } 140 | } 141 | 142 | sub getCountryNetblocks() { 143 | my $c = lc($OPTS{'country'}); 144 | # Catch a few common cases, even if some of those 145 | # are not correct or may infuriate people. 146 | if ($c eq "usa") { 147 | $c = "United States"; 148 | } elsif ($c eq "russia") { 149 | $c = "Russian Federation"; 150 | } elsif (($c eq "england") || ($c eq "uk")) { 151 | $c = "united kingdom"; 152 | } elsif ($c eq "syria") { 153 | $c = "syrian arab republic"; 154 | } 155 | 156 | if ($c eq "none") { 157 | $EDNS_CLIENT_SUBNET = "0.0.0.0/0"; 158 | return 159 | } 160 | 161 | # We try to look up the CC; if that works, 162 | # we're good. Otherwise, try to convert the 163 | # name into a CC. 164 | my $cc = $c; 165 | my $country = code2country($c); 166 | if (!$country) { 167 | $cc = country2code($c); 168 | } 169 | if (!$cc) { 170 | error("Unable to get valid country code for '$c'.", EXIT_FAILURE); 171 | } 172 | 173 | verbose("Looking up netblocks allocated to country code '$cc'..."); 174 | my $url = COUNTRY_IP_URL . uri_escape($cc) . ".html"; 175 | verbose("Fetching '$url'...", 2); 176 | 177 | my @country_netblocks; 178 | my @cmd = ( "curl", "-s", $url); 179 | open(my $out, "-|", @cmd) or error("Unable to open pipe from '". 180 | join(" ", @cmd) . "': $!", EXIT_FAILURE); 181 | 182 | my $next = 0; 183 | foreach my $line (<$out>) { 184 | if ($line =~ m/tr class="iptableheader"/) { 185 | $next = 1; 186 | next; 187 | } 188 | 189 | if ($next) { 190 | my @rows = split(/ /, $line); 191 | shift(@rows); # 192 | my $n = 0; 193 | 194 | my @pairs; 195 | my ($start, $end); 196 | foreach my $i (@rows) { 197 | $n++; 198 | if ($n == 1) { 199 | $start = $i; 200 | } elsif ($n == 2) { 201 | $end = $i; 202 | my @pair = ( $start, $end ); 203 | push(@pairs, \@pair); 204 | } elsif ($n == 5) { 205 | $n = 0; 206 | } 207 | } 208 | foreach my $i (@pairs) { 209 | my @pair = @{$i}; 210 | my @blocks = Net::Netmask::range2cidrlist($pair[0], $pair[1]); 211 | foreach my $j (@blocks) { 212 | push(@country_netblocks, "".$j); 213 | } 214 | } 215 | } 216 | } 217 | 218 | if (!scalar(@country_netblocks)) { 219 | error("Unable to determine a netblock for '" . $OPTS{'country'} ."'.", EXIT_FAILURE); 220 | } 221 | $EDNS_CLIENT_SUBNET = $country_netblocks[rand(@country_netblocks)]; 222 | } 223 | 224 | sub init() { 225 | my ($ok); 226 | 227 | if (!scalar(@ARGV)) { 228 | error("I have nothing to do. Try -h.", EXIT_FAILURE); 229 | # NOTREACHED 230 | } 231 | 232 | $ok = GetOptions( 233 | "one|1" => \$OPTS{'1'}, 234 | "ipv4|4" => \$OPTS{'4'}, 235 | "ipv6|6" => \$OPTS{'6'}, 236 | "version|V" => sub { print "$PROGNAME Version " . VERSION . "\n"; exit(EXIT_SUCCESS); }, 237 | "country|c=s" => \$OPTS{'country'}, 238 | "doh|d" => \$OPTS{'doh'}, 239 | "edns|e=s" => \$OPTS{'edns'}, 240 | "file|f=s" => \$OPTS{'file'}, 241 | "help|h" => \$OPTS{'h'}, 242 | "json|j" => \$OPTS{'json'}, 243 | "num|n=i" => \$OPTS{'num'}, 244 | "resolver|r=s@" => \@{$OPTS{'resolvers'}}, 245 | "public|p" => \$OPTS{'public'}, 246 | "verbose|v+" => sub { $OPTS{'v'}++; }, 247 | ); 248 | 249 | if ($OPTS{'h'} || !$ok) { 250 | usage($ok); 251 | exit(!$ok); 252 | # NOTREACHED 253 | } 254 | 255 | if (!scalar(@ARGV)) { 256 | error("Give me something to look up.", EXIT_FAILURE); 257 | # NOTREACHED 258 | } 259 | 260 | $OPTS{'query'} = shift(@ARGV); 261 | if (scalar(@ARGV)) { 262 | @{$OPTS{'types'}} = map { uc($_) } @ARGV; 263 | %{$OPTS{'wanted'}} = map { uc($_) => 1; } @ARGV; 264 | 265 | } else { 266 | my @types = ( "A", "AAAA", "CNAME" ); 267 | $OPTS{'types'} = \@types; 268 | } 269 | 270 | checkPTR(); 271 | 272 | if ($OPTS{'1'} && $OPTS{'public'}) { 273 | error("'-1' cannot be combined with '-p'.", EXIT_FAILURE); 274 | # NOTREACHED 275 | } 276 | 277 | if ($OPTS{'file'} && $OPTS{'public'}) { 278 | error("'-f' and '-p' cannot be specified at the same time.", EXIT_FAILURE); 279 | # NOTREACHED 280 | } 281 | 282 | if ($OPTS{'doh'} && ($OPTS{'file'} || $OPTS{'public'})) { 283 | error("'-f' and '-p' cannot be combined with '-d'.", EXIT_FAILURE); 284 | # NOTREACHED 285 | } 286 | 287 | if ($OPTS{'country'} && !$OPTS{'doh'}) { 288 | error("'-c' can only be used in combination with '-d'.", EXIT_FAILURE); 289 | # NOTREACHED 290 | } 291 | 292 | if ($OPTS{'edns'}) { 293 | if (!$OPTS{'doh'}) { 294 | error("'-e' can only be used in combination with '-d'.", EXIT_FAILURE); 295 | # NOTREACHED 296 | } 297 | $EDNS_CLIENT_SUBNET = $OPTS{'edns'}; 298 | } 299 | 300 | my $ulimit = `/bin/sh -c "ulimit -n"`; 301 | chomp($ulimit); 302 | if (($ulimit ne "unlimited") && ($ulimit > $CONCURRENCY)) { 303 | my $u = $ulimit / 2; 304 | if ($u > $CONCURRENCY) { 305 | $CONCURRENCY = $u; 306 | } 307 | } 308 | } 309 | 310 | sub parsePuddyFile() { 311 | verbose("Parsing input file $OPTS{'file'}...", 2); 312 | 313 | %RESOLVERS = (); 314 | my $n = 0; 315 | open(my $fh, "<", $OPTS{'file'}) or error("Unable to open $OPTS{'file'}: $!", EXIT_FAILURE); 316 | foreach my $line (<$fh>) { 317 | $line =~ s/#.*//; 318 | $line =~ s/^\s+//; 319 | $line =~ s/\s+$//; 320 | if ($line =~ m/^(\S+)(\s+(.*))?/) { 321 | my $ip = $1; 322 | my $comment = $OPTS{'file'}; 323 | if ($3) { 324 | $comment = $3; 325 | } 326 | if (inet_pton(PF_INET, $ip)) { 327 | $RESOLVERS{"ipv4"}{$ip} = $comment; 328 | } elsif (inet_pton(PF_INET6, $ip)) { 329 | $RESOLVERS{"ipv6"}{$ip} = $comment; 330 | } 331 | } 332 | } 333 | close($fh); 334 | } 335 | 336 | sub parseEtcResolv() { 337 | my $resolv = "/etc/resolv.conf"; 338 | verbose("Parsing $resolv...", 2); 339 | 340 | if (! -r $resolv) { 341 | return; 342 | } 343 | 344 | open(my $fh, "<", $resolv) or error("Unable to open $resolv: $!", EXIT_FAILURE); 345 | foreach my $line (<$fh>) { 346 | if ($line =~ m/^nameserver\s+(.*)/) { 347 | my $ip = $1; 348 | if ($ip =~ m/:/) { 349 | $RESOLVERS{"ipv6"}{$ip} = "/etc/resolv.conf"; 350 | } else { 351 | $RESOLVERS{"ipv4"}{$ip} = "/etc/resolv.conf"; 352 | } 353 | } 354 | } 355 | close($fh); 356 | } 357 | 358 | sub parseGivenResolvers() { 359 | my (@ipv4, @ipv6); 360 | 361 | foreach my $ip (@{$OPTS{'resolvers'}}) { 362 | if (inet_pton(PF_INET, $ip)) { 363 | push(@ipv4, $ip); 364 | } elsif (inet_pton(PF_INET6, $ip)) { 365 | push(@ipv6, $ip); 366 | } else { 367 | my $res = Net::DNS::Resolver->new; 368 | foreach my $type ( "A", "AAAA" ) { 369 | my $aref = \@ipv4; 370 | if ($type eq "AAAA") { 371 | $aref = \@ipv6; 372 | } 373 | 374 | my $query = $res->search($ip, $type); 375 | if (!$query) { 376 | error("Unable to resolve argument '$ip' to '-r'.", EXIT_FAILURE); 377 | # NOTREACHED 378 | } 379 | foreach my $rr ($query->answer) { 380 | if ($rr->type eq $type) { 381 | push(@{$aref}, $rr->address); 382 | } 383 | } 384 | } 385 | } 386 | } 387 | 388 | if ((scalar(@ipv4) > 0) && ($OPTS{'6'})) { 389 | error("You specified '-6', but the argument(s) to '-r' included (or resolved to) IPv4 addresses."); 390 | error("I'm going to ignore those."); 391 | @ipv4 = (); 392 | } 393 | 394 | if ((scalar(@ipv6) > 0) && ($OPTS{'4'})) { 395 | error("You specified '-4', but the argument(s) to '-r' included (or resolved to) IPv6 addresses."); 396 | error("I'm going to ignore those."); 397 | @ipv6 =(); 398 | } 399 | 400 | foreach my $ip (@ipv4) { 401 | $RESOLVERS{"ipv4"}{$ip} = "command-line"; 402 | } 403 | foreach my $ip (@ipv6) { 404 | $RESOLVERS{"ipv6"}{$ip} = "command-line"; 405 | } 406 | } 407 | 408 | 409 | sub parsePublicResolvers() { 410 | verbose("Preparing list of resolvers from " . PUBLIC_URL . "...", 2); 411 | 412 | %RESOLVERS = (); 413 | 414 | my @cmd = ( "curl", "-s", PUBLIC_URL ); 415 | open(my $out, "-|", @cmd) or error("Unable to open pipe to '". 416 | join(" ", @cmd) . "': $!", EXIT_FAILURE); 417 | foreach my $line (<$out>) { 418 | chomp($line); 419 | if (inet_pton(PF_INET, $line)) { 420 | $RESOLVERS{"ipv4"}{$line} = ""; 421 | } elsif (inet_pton(PF_INET6, $line)) { 422 | $RESOLVERS{"ipv6"}{$line} = ""; 423 | } 424 | } 425 | close($out); 426 | } 427 | 428 | 429 | sub ipv6Check() { 430 | verbose("Checking if we can even talk IPv6...", 2); 431 | 432 | my @ips = keys(%{$RESOLVERS{"ipv6"}}); 433 | 434 | my $s = IO::Socket::INET6->new( 435 | PeerAddr => $ips[0], 436 | PeerPort => "53", 437 | Proto => "udp" 438 | ); 439 | if (!$s) { 440 | error("Unable to open an IPv6 socket, ignoring all IPv6 resolvers."); 441 | $RESOLVERS{"ipv6"} = (); 442 | } 443 | } 444 | 445 | 446 | sub prepareListOfResolvers() { 447 | verbose("Preparing list of resolvers..."); 448 | 449 | if ($OPTS{'file'}) { 450 | parsePuddyFile(); 451 | } elsif ($OPTS{'public'}) { 452 | parsePublicResolvers(); 453 | } else { 454 | parseEtcResolv(); 455 | } 456 | 457 | if ($OPTS{'resolvers'}) { 458 | parseGivenResolvers(); 459 | } 460 | 461 | if ($OPTS{'4'}) { 462 | $RESOLVERS{"ipv6"} = (); 463 | } elsif ($OPTS{'6'}) { 464 | $RESOLVERS{"ipv4"} = (); 465 | } 466 | 467 | if (!$OPTS{'doh'}) { 468 | ipv6Check(); 469 | } else { 470 | # Ok, we're cheating a bit here. We don't know 471 | # (or care) which ones are IPv4, so let's just 472 | # pretend they all are. 473 | $RESOLVERS{"ipv6"} = (); 474 | $RESOLVERS{"ipv4"} = \%DOH; 475 | } 476 | 477 | 478 | if ($OPTS{'num'}) { 479 | my %resolvers; 480 | 481 | my @ipv4 = shuffle(keys(%{$RESOLVERS{"ipv4"}})); 482 | my @ipv6 = shuffle(keys(%{$RESOLVERS{"ipv6"}})); 483 | 484 | if ($OPTS{'num'} > (scalar(@ipv4) + scalar(@ipv6))) { 485 | error("List of resolvers is smaller than $OPTS{'num'}, so using all instead."); 486 | return; 487 | } 488 | 489 | my $numv4 = int(rand($OPTS{'num'})); 490 | 491 | while ($numv4 > scalar(@ipv4)) { 492 | $numv4--; 493 | } 494 | 495 | my $numv6 = $OPTS{'num'} - $numv4; 496 | while ($numv6 > scalar(@ipv6)) { 497 | $numv6--; 498 | $numv4++; 499 | } 500 | 501 | foreach my $n (0 .. ($numv4 - 1)) { 502 | my $ip = $ipv4[$n]; 503 | $resolvers{"ipv4"}{$ip} = $RESOLVERS{"ipv4"}{$ip}; 504 | } 505 | 506 | foreach my $n (0 .. ($numv6 - 1)) { 507 | my $ip = $ipv6[$n]; 508 | $resolvers{"ipv6"}{$ip} = $RESOLVERS{"ipv6"}{$ip}; 509 | } 510 | 511 | %RESOLVERS = %resolvers; 512 | } 513 | } 514 | 515 | 516 | sub printResults() { 517 | 518 | if ($OPTS{'json'}) { 519 | my $json = JSON->new; 520 | print $json->pretty->encode(\%RESULTS); 521 | } else { 522 | printResultsPlain(); 523 | } 524 | } 525 | 526 | sub printResultsPlain() { 527 | my @ips = keys(%{$RESULTS{"results"}}); 528 | my (@ipv4, @ipv6); 529 | 530 | if ($OPTS{'doh'}) { 531 | # Again overloading the "ipv4" array for DoH. 532 | @ipv4 = sort(@ips); 533 | } else { 534 | foreach my $ip (@ips) { 535 | if (inet_pton(PF_INET, $ip)) { 536 | push(@ipv4, $ip); 537 | } elsif (inet_pton(PF_INET6, $ip)) { 538 | push(@ipv6, $ip); 539 | } 540 | } 541 | 542 | @ipv4 = sortIPv4(@ipv4); 543 | @ipv6 = sort(@ipv6); 544 | } 545 | 546 | foreach my $ip (@ipv6, @ipv4) { 547 | if (!$RESULTS{"results"}{$ip}) { 548 | next; 549 | } 550 | my %oneResult = %{$RESULTS{"results"}{$ip}}; 551 | print $ip; 552 | if ($oneResult{"comment"}) { 553 | print " (" . $oneResult{"comment"} . ")"; 554 | } 555 | 556 | # Some resolvers always return an ECS. 557 | if ($EDNS_CLIENT_SUBNET || $oneResult{"edns_client_subnet"}) { 558 | print " (ECS "; 559 | } 560 | if ($EDNS_CLIENT_SUBNET) { 561 | print "sent: $EDNS_CLIENT_SUBNET; "; 562 | if (!$oneResult{"edns_client_subnet"}) { 563 | print "ignored by server"; 564 | } 565 | } 566 | if ($oneResult{"edns_client_subnet"}) { 567 | if ($EDNS_CLIENT_SUBNET) { 568 | print "ECS "; 569 | } 570 | print "returned: " . $oneResult{"edns_client_subnet"}; 571 | } 572 | if ($EDNS_CLIENT_SUBNET || $oneResult{"edns_client_subnet"}) { 573 | print ")"; 574 | } 575 | print "\n"; 576 | 577 | my $n = 0; 578 | foreach my $k (sort(keys(%oneResult))) { 579 | if (($k eq "comment") || ($k eq "edns_client_subnet")) { 580 | $n = 1; 581 | next; 582 | } 583 | if ($oneResult{$k}{"status"} && $oneResult{$k}{"status"} ne "NOERROR") { 584 | $n = 1; 585 | print "\t$k: " . $oneResult{$k}{"status"} . "\n"; 586 | } elsif (!($oneResult{$k}{"status"} && $oneResult{$k}{"rrs"}) && $OPTS{'wanted'}{$k}) { 587 | # even if we didn't find data 588 | # we still got a result, so set $n 589 | $n = 1; 590 | print "\t$k: [NO RECORD FOUND]\n"; 591 | } 592 | 593 | my $sortFunc; 594 | if ($k eq "A") { 595 | $sortFunc = sub { 596 | my %a = %{$a}; 597 | my %b = %{$b}; 598 | my $v1 = $a{"value"}; 599 | my $v2 = $b{"value"}; 600 | if ($v1 eq $v2) { 601 | return 0; 602 | } 603 | my $i1 = new Net::IP ($v1); 604 | my $i2 = new Net::IP ($v2); 605 | if ($i1->bincomp('lt', $i2)) { 606 | return -1; 607 | } else { 608 | return 1; 609 | } 610 | }; 611 | } else { 612 | $sortFunc = sub { my %a = %{$a}; my %b = %{$b}; 613 | return $a{"value"} cmp $b{"value"}; 614 | }; 615 | } 616 | 617 | if ($oneResult{$k}{"rrs"}) { 618 | my @data = @{$oneResult{$k}{"rrs"}}; 619 | foreach my $ref (sort $sortFunc @{$oneResult{$k}{"rrs"}}) { 620 | $n++; 621 | my %data= %{$ref}; 622 | print "\t$k (" . $data{"ttl"} ."): " . $data{"value"} . "\n"; 623 | } 624 | } 625 | } 626 | 627 | if (!$n) { 628 | print "\tNO RESULTS\n"; 629 | } 630 | } 631 | } 632 | 633 | 634 | sub queryDOH($$$) { 635 | my ($url, $query, $type) = @_; 636 | my %dohResults; 637 | 638 | my $provider = $DOH{$url}; 639 | 640 | verbose("Querying $provider for '$query' ($type) via DoH...", 4); 641 | $url .= "name=" . uri_escape($query) . "&type=" . uri_escape($type); 642 | 643 | if ( ($OPTS{'country'} || $OPTS{'edns'}) && $EDNS_CLIENT_SUBNET) { 644 | $url .= "&edns_client_subnet=$EDNS_CLIENT_SUBNET"; 645 | } 646 | 647 | verbose("Full URL: $url", 5); 648 | 649 | # We call out to curl(1) because it turns out 650 | # that the various ways to fetch https resources 651 | # in Perl across platforms are less uniform or 652 | # predictable with regards to the presence of 653 | # a proper CA bundle and support for modern ciphers 654 | # (Google DNS, for example, requires TLS >= 1.2 and 655 | # only supports ECDHE) than curl(1). 656 | my @cmd = ( "curl", "-s", 657 | "-H", "Accept: application/dns-json", 658 | "$url"); 659 | 660 | verbose("Running '" . join(" ", @cmd) . "'...", 5); 661 | open(my $out, "-|", @cmd) or error("Unable to open pipe from '". 662 | join(" ", @cmd) . "': $!", EXIT_FAILURE); 663 | my $json = JSON->new->allow_nonref; 664 | my $data = <$out>; 665 | close($out); 666 | 667 | my $result; 668 | if ($data) { 669 | eval { $result = $json->decode($data); }; 670 | if ($@) { 671 | # E.g., Google DNS returns a 500 error on invalid query type. 672 | $dohResults{"status"} = "Unable to query '$url'."; 673 | return; 674 | } 675 | } 676 | 677 | if (!$result) { 678 | $RESULTS{"results"}{$provider}{$type}{"status"} = "Unable to parse json from '$url'."; 679 | return; 680 | } 681 | 682 | my @answers; 683 | if ($result->{"Answer"}) { 684 | @answers = @{$result->{"Answer"}}; 685 | if (!scalar(@answers)) { 686 | return; 687 | } 688 | } 689 | 690 | foreach my $href (@answers) { 691 | my %a = %{$href}; 692 | my $t = $a{type}; 693 | my $foundType = typebyval($t); 694 | if (!$foundType) { 695 | $dohResults{"status"} = "Unknown RR type '$t'."; 696 | next; 697 | } else { 698 | my $status = $result->{"Status"}; 699 | $dohResults{"status"} = rcodebyval($status); 700 | 701 | if ($result->{"edns_client_subnet"}) { 702 | $dohResults{"edns_client_subnet"} = $result->{"edns_client_subnet"}; 703 | } 704 | } 705 | 706 | if ($foundType ne $type) { 707 | # e.g., an A lookup against a CNAME 708 | next; 709 | } 710 | 711 | my %tuple = ( 712 | "value" => $a{"data"}, 713 | "ttl" => $a{"TTL"}, 714 | ); 715 | if ($dohResults{"rrs"}) { 716 | push(@{$dohResults{"rrs"}}, \%tuple); 717 | } else { 718 | @{$dohResults{"rrs"}} = ( \%tuple ); 719 | } 720 | } 721 | return \%dohResults; 722 | } 723 | 724 | 725 | sub queryOneResolver($$$) { 726 | my ($r, $org, $query) = @_; 727 | 728 | my $msg = "Querying $r "; 729 | if ($org) { 730 | $msg .= "($org)"; 731 | $RESULTS{"results"}{$r}{"comment"} = $org; 732 | } 733 | $msg .= "..."; 734 | 735 | verbose($msg, 2); 736 | my %resolverResults; 737 | 738 | foreach my $type (@{$OPTS{'types'}}) { 739 | $resolverResults{$type} = queryOneResolverForRR($r, $query, $type); 740 | # We're cheating a bit here: ECS is returned for each type 741 | # but we assume that the same ECS will be used for each. 742 | # This isn't necessarily but at least apparently so, 743 | # so for presentation purposes, this makes things a bit easier. 744 | if ($resolverResults{$type}{"edns_client_subnet"}) { 745 | $resolverResults{"edns_client_subnet"} = $resolverResults{$type}{"edns_client_subnet"}; 746 | delete($resolverResults{$type}{"edns_client_subnet"}); 747 | } 748 | } 749 | 750 | return \%resolverResults; 751 | } 752 | 753 | sub queryOneResolverForRR($$$) { 754 | my ($r, $query, $type) = @_; 755 | 756 | verbose("Looking up RR $type...", 3); 757 | 758 | my %result; 759 | 760 | if ($OPTS{'doh'}) { 761 | return queryDOH($r, $query, $type); 762 | } 763 | 764 | my ($res, $q); 765 | my $timeout = 1; 766 | 767 | eval { 768 | local $SIG{ALRM} = sub { die "alarm\n"; }; 769 | alarm($timeout); 770 | $res = Net::DNS::Resolver->new( 771 | nameservers => [$r], 772 | udp_timeout => 1, 773 | tcp_timeout => 1, 774 | debug => 0, 775 | ); 776 | $q = $res->send($query, $type); 777 | alarm(0); 778 | }; 779 | if ($@) { 780 | $result{"status"} = "timed out"; 781 | return \%result; 782 | } 783 | 784 | $result{"status"} = $res->errorstring; 785 | if ($q) { 786 | foreach my $rr ($q->answer) { 787 | if ($rr->type ne $type) { 788 | # e.g., an A lookup against a CNAME 789 | next; 790 | } 791 | my %tuple = ( 792 | "value" => $rr->rdstring, 793 | "ttl" => $rr->ttl, 794 | ); 795 | if ($result{"rrs"}) { 796 | push(@{$result{"rrs"}}, \%tuple); 797 | } else { 798 | @{$result{"rrs"}} = ( \%tuple ); 799 | } 800 | } 801 | } 802 | return \%result; 803 | } 804 | 805 | sub queryResolvers() { 806 | my $n = scalar(keys(%{$RESOLVERS{"ipv4"}})) + scalar(keys(%{$RESOLVERS{"ipv6"}})); 807 | verbose("Querying $n resolvers..."); 808 | 809 | my %seen; 810 | my @classes = ( "ipv6", "ipv4" ); 811 | 812 | my $query = $OPTS{'query'}; 813 | 814 | $RESULTS{"query"} = $query; 815 | $RESULTS{"type"} = \@{$OPTS{'types'}}; 816 | 817 | my $pm = Parallel::ForkManager->new($CONCURRENCY); 818 | $pm->run_on_finish(sub { 819 | my (undef, undef, undef, undef, undef, $ref) = @_; 820 | my @values = @{$ref}; 821 | my $key = $values[0]; 822 | if ($OPTS{'doh'}) { 823 | $key = $DOH{$key}; 824 | } 825 | $RESULTS{"results"}{$key} = $values[1]; 826 | if ($OPTS{'doh'}) { 827 | $RESULTS{"results"}{$key}{"comment"} = $values[0]; 828 | } 829 | }); 830 | 831 | foreach my $c (@classes) { 832 | LOOP: 833 | foreach my $r (keys(%{$RESOLVERS{$c}})) { 834 | my $org = $RESOLVERS{$c}{$r}; 835 | 836 | if ($OPTS{'1'} && $seen{$org} && $org ne "command-line") { 837 | verbose("Skipping $r ($org already queried)...", 2); 838 | next; 839 | } 840 | 841 | $seen{$org} = 1; 842 | 843 | $pm->start() and next LOOP; 844 | my $hr = queryOneResolver($r, $org, $query); 845 | my %oneResult = %{$hr}; 846 | if ($OPTS{'doh'}) { 847 | $oneResult{"comment"} = $query; 848 | } 849 | if (!$oneResult{"comment"}) { 850 | $oneResult{"comment"} = $org; 851 | } 852 | my @values = ($r, \%oneResult); 853 | 854 | $pm->finish(0, \@values); 855 | } 856 | $pm->wait_all_children(); 857 | } 858 | } 859 | 860 | 861 | sub sortIPv4(@) { 862 | 863 | my (@ipv4) = @_; 864 | 865 | # map address to 32bit number representation, 866 | # sort keys of numbers, then remove the 32bit number representation again 867 | my %numeric = map { inet_pton(PF_INET, $_) => $_ } @ipv4; 868 | @ipv4 = (); 869 | foreach my $ip (sort(keys(%numeric))) { 870 | push(@ipv4, $numeric{$ip}); 871 | } 872 | return @ipv4; 873 | } 874 | 875 | sub usage($) { 876 | my ($err) = @_; 877 | 878 | my $FH = $err ? \*STDERR : \*STDOUT; 879 | 880 | print $FH < $msg\n"; 910 | } 911 | } 912 | 913 | 914 | ### 915 | ### Main 916 | ### 917 | 918 | init(); 919 | prepareListOfResolvers(); 920 | if ($OPTS{'country'}) { 921 | getCountryNetblocks(); 922 | } 923 | queryResolvers(); 924 | printResults(); 925 | 926 | exit($RETVAL); 927 | --------------------------------------------------------------------------------