├── LICENSE ├── README.md ├── base64url-decode ├── dns2doh ├── docs ├── _config.yml ├── dns2doh.md ├── doh.md ├── index.md └── server.md └── doh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Stenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dns2doh 2 | 3 | dns2doh - resolves with DNS and generates a DoH response. 4 | 5 | doh - resolves a host name over DoH. 6 | 7 | Docs: https://bagder.github.io/dns2doh/ 8 | 9 | # FAQ 10 | 11 | ## I can't base64 decode it! 12 | 13 | It uses the [base64url](https://tools.ietf.org/html/rfc4648#section-5) format, 14 | which is like normal base64 but with two letters changed. 15 | 16 | ## Where's the spec? 17 | 18 | DoH: [RFC 8484](https://tools.ietf.org/html/rfc8484) 19 | 20 | DNS: [RFC 1035](https://www.ietf.org/rfc/rfc1035.txt) 21 | -------------------------------------------------------------------------------- /base64url-decode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use MIME::Base64 qw(decode_base64url); 4 | 5 | while() { 6 | my $dec = decode_base64url($_); 7 | print $dec; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /dns2doh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use Encode qw(encode); 4 | use MIME::Base64 qw(encode_base64url decode_base64url); 5 | use Socket qw(inet_pton AF_INET6 AF_INET); 6 | 7 | sub help { 8 | print STDERR "Usage: dns2doh [options] [input]\n", 9 | "Options:\n", 10 | " --A encode a type A request\n", 11 | " --AAAA encode a type AAAA request\n", 12 | " --CNAME encode a type CNAME request\n", 13 | " --decode decode DOH input to type + host name\n", 14 | " --encode encode host name input to DOH\n", 15 | " --hex show extra hex output\n", 16 | " --hosts=[file] data for custom responses\n", 17 | " --NS encode a type NS request\n", 18 | " --onlyq encode only the question\n", 19 | " --raw output without encoding\n"; 20 | exit; 21 | } 22 | 23 | my %dnstype = (1 => "A", 24 | 2 => "NS", 25 | 5 => "CNAME", 26 | 28 => "AAAA"); 27 | 28 | my $h; 29 | my $type = 1; # default type is A 30 | my $onlyq = 0; # set to 1 if only asking, not getting the answer 31 | my $encode = 1; # default is base64 encode 32 | while($ARGV[0]) { 33 | 34 | if($ARGV[0] eq "--hex") { 35 | $showhex=1; 36 | shift @ARGV; 37 | } 38 | elsif($ARGV[0] eq "--decode") { 39 | $decode=1; 40 | shift @ARGV; 41 | } 42 | elsif($ARGV[0] eq "--encode") { 43 | $encode=1; 44 | shift @ARGV; 45 | } 46 | elsif($ARGV[0] eq "--onlyq") { 47 | $onlyq=1; 48 | shift @ARGV; 49 | } 50 | elsif($ARGV[0] eq "--raw") { 51 | # do not base64 encode output, pass it out "raw" 52 | $encode=0; 53 | shift @ARGV; 54 | } 55 | elsif($ARGV[0] eq "--A") { 56 | $type=1; 57 | shift @ARGV; 58 | } 59 | elsif($ARGV[0] eq "--AAAA") { 60 | $type=28; 61 | shift @ARGV; 62 | } 63 | elsif($ARGV[0] eq "--NS") { 64 | $type=2; 65 | shift @ARGV; 66 | } 67 | elsif($ARGV[0] eq "--CNAME") { 68 | $type=5; 69 | shift @ARGV; 70 | } 71 | elsif(($ARGV[0] eq "--help") || 72 | ($ARGV[0] eq "-h")) { 73 | help(); 74 | } 75 | elsif($ARGV[0] =~ /--hosts=(.*)/) { 76 | $hosts = $1; 77 | shift @ARGV; 78 | } 79 | else { 80 | $h = $ARGV[0]; 81 | last; 82 | } 83 | } 84 | 85 | if(!$h) { 86 | while() { 87 | $h .= $_; 88 | } 89 | } 90 | 91 | if($hosts) { 92 | # host type address 93 | # www.example.com A 127.0.0.1 94 | # www.example.com AAAA 2606:2800:220:1:248:1893:25c8:1946 95 | 96 | open(H, "<$hosts"); 97 | while ( ) { 98 | chomp; 99 | if($_ =~ /^ *\#/) { 100 | # comment 101 | next; 102 | } 103 | elsif($_ =~ /([^ ]*) *([^ ]*) *([^ ]*)/) { 104 | my ($name, $type, $addr) = ($1, $2, $3); 105 | $faketype{$name}{$type} .= "$addr "; 106 | } 107 | } 108 | close(H); 109 | } 110 | 111 | if($decode) { 112 | # decode base64url encoded DNS packet and extract host name 113 | my $r = decode_base64url($h); 114 | hexdump($r, "Incoming") if($showhex); 115 | my @arr = split(//, $r); 116 | my $i=12; 117 | my $len = ord($arr[$i++]); 118 | my $name; 119 | while($len) { 120 | while($len) { 121 | $name .= $arr[$i++]; 122 | $len--; 123 | } 124 | $len = ord($arr[$i++]); 125 | if($len) { 126 | $name .= "."; 127 | } 128 | } 129 | # TYPE is 16 bits following the name 130 | my $DTYPE = ord($arr[$i++]) << 8; 131 | $DTYPE += ord($arr[$i++]); 132 | printf "%s $name\n", $dnstype{$DTYPE}; 133 | exit; 134 | } 135 | 136 | my $answers; 137 | my @rdata; 138 | if(!$onlyq && ($type != 2) && ($type != 5)) { 139 | my $digtype = $dnstype{$type}; 140 | my @dig; 141 | if($faketype{$h}{$digtype}) { 142 | foreach my $a (split(/ /, $faketype{$h}{$digtype})) { 143 | push @dig, "$a\n"; 144 | } 145 | } 146 | else { 147 | @dig=`dig +short -t $digtype $h`; 148 | } 149 | 150 | if(!$dig[0]) { 151 | # blank return: not found 152 | exit; 153 | } 154 | 155 | # dig[0] starts with names, then follows IP addresses 156 | foreach my $ipstr (@dig) { 157 | chomp $ipstr; 158 | my $address; 159 | 160 | $address = inet_pton( ($type == 28)?AF_INET6:AF_INET, $ipstr); 161 | if(!$address) { 162 | # not a valid address 163 | next; 164 | } 165 | push @rdata, $address; 166 | $answers++; 167 | } 168 | 169 | if(!$answers) { 170 | # no addresses 171 | exit; 172 | } 173 | } 174 | elsif(!$onlyq && (($type == 2) || ($type == 5))) { 175 | my @ns; 176 | my $digtype = $dnstype{$type}; 177 | # NS or CNAME record lookup 178 | if($faketype{$h}{$digtype}) { 179 | foreach my $a (split(/ /, $faketype{$h}{$digtype})) { 180 | push @ns, "$a"; 181 | } 182 | } 183 | else { 184 | @ns=`dig +short -t $digtype $h`; 185 | } 186 | 187 | if(!$ns[0]) { 188 | # blank return: no NS found 189 | exit; 190 | } 191 | foreach my $n (@ns) { 192 | push @rdata, QNAME($n); 193 | $answers++; 194 | } 195 | } 196 | 197 | my $seconds = 55; # TODO: get the real 198 | 199 | sub hexdump { 200 | my ($raw, $title) = @_; 201 | my $i=0; 202 | 203 | if($title) { 204 | print "= $title\n"; 205 | } 206 | my $s; 207 | for my $c (split(//, $raw)) { 208 | if(!($i%16)) { 209 | my $txt="\n"; 210 | if($i) { 211 | $txt=" |$s|\n"; 212 | $s=""; 213 | } 214 | printf "%s%02x: ", $i?$txt:"", $i; 215 | } 216 | printf ("%02x ", ord($c)); 217 | if((ord($c)>=32) && (ord($c)<127)) { 218 | $s .= $c; 219 | } 220 | else { 221 | $s .= '.'; 222 | } 223 | $i++; 224 | } 225 | if($s) { 226 | my $left; 227 | if($i%16) { 228 | $left = ' ' x ((16-$i%16)*3); 229 | } 230 | print "$left |$s|\n"; 231 | } 232 | else { 233 | print "\n"; 234 | } 235 | } 236 | 237 | sub QNAME { 238 | my ($h) = @_; 239 | my $raw; 240 | 241 | my @labels=split(/\./, $h); 242 | foreach my $l (@labels) { 243 | #print STDERR "Label: $l\n"; 244 | $raw .= sprintf("%c", length($l)); 245 | $raw .= $l; 246 | } 247 | $raw .= "\x00"; # end with a zero labeel 248 | return $raw; 249 | } 250 | 251 | my $qname = QNAME($h); 252 | my $qnameptr = sprintf("\xc0\x0c", $answers); 253 | my $ancount = sprintf("\x00%c", $answers); 254 | my $qdcount = sprintf("\x00%c", 1); # questions 255 | my $qtype = sprintf("\x00%c", $type); # for now 256 | my $ttl = pack 'N', $seconds; 257 | 258 | my $header; 259 | my $answer_header = sprintf("\x00\x00". # ID 260 | "\x01\x00". # |QR| Opcode |AA|TC|RD|RA| Z | RCODE | 261 | $qdcount. # QDCOUNT 262 | $ancount. # ANCOUNT 263 | "\x00\x00". # NSCOUNT 264 | "\x00\x00"); # ARCOUNT 265 | my $query_header = sprintf("\x00\x00". # ID 266 | "\x01\x00". # |QR| Opcode |AA|TC|RD|RA| Z | RCODE | 267 | $qdcount. # QDCOUNT 268 | "\x00\x00". # ANCOUNT 269 | "\x00\x00". # NSCOUNT 270 | "\x00\x00"); # ARCOUNT 271 | my $question = sprintf("$qname". # QNAME 272 | "$qtype". # QTYPE 273 | "\x00\x01"); # QCLASS 274 | 275 | foreach my $rd (@rdata) { 276 | my $len = length($rd); 277 | my $rdlen = pack 'n', $len; 278 | my $one .= sprintf("$qnameptr". # QNAME (pointer) 279 | "$qtype". # QTYPE 280 | "\x00\x01". # QCLASS 281 | "$ttl". # TTL 282 | "$rdlen". # RDLENGTH 283 | "$rd"); # RDATA 284 | $resource .= $one; 285 | } 286 | 287 | my $msg; 288 | if($onlyq) { 289 | $header = $query_header; 290 | $msg = "$header$question"; 291 | } 292 | else { 293 | $header = $answer_header; 294 | $msg = "$header$question$resource"; 295 | } 296 | 297 | my $output = encode("iso-8859-1", "$msg"); 298 | 299 | if($showhex) { 300 | hexdump($output, "ALL"); 301 | hexdump($header, "Header"); 302 | hexdump($question, "Question"); 303 | hexdump($resource, "Resources") unless($onlyq); 304 | } 305 | elsif($encode) { 306 | my $encoded = encode_base64url($output, ""); 307 | $encoded =~ s/[=]+\z//; 308 | 309 | print "$encoded\n"; 310 | } 311 | else { 312 | # raw 313 | print "$output"; 314 | } 315 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /docs/dns2doh.md: -------------------------------------------------------------------------------- 1 | # dns2doh 2 | 3 | Resolve with DNS and generate DOH response 4 | 5 | ## Usage 6 | 7 | Usage: dns2doh [options] [input] 8 | 9 | ### --A 10 | 11 | Ask for a type A resource. (default) 12 | 13 | ### --AAAA 14 | 15 | Ask for a type AAAA resource. 16 | 17 | ### --decode 18 | 19 | Decode the DOH input and output the type field and host name from the question 20 | part of the DNS packet. The type is output as `A` or `AAAA` depending on the 21 | field being 1 or 28. 22 | 23 | ### --encode 24 | 25 | (default) Encode the host name input to proper DOH message and output it. This 26 | is the default action if no option is given. Use `--A` (default) or `--AAAA` 27 | to control which TYPE to use. 28 | 29 | ### --hex 30 | 31 | Show hexdump of the input/output. For debugging the process. 32 | 33 | ### --hosts=FILE 34 | 35 | Gives dns2doh a list of host names with type and addresses for which dns2doh 36 | will not use dig to resolve but instead use this set of custom handicrafted 37 | addresses. Add multiple addresses for a host name by adding multiple lines for 38 | it. 39 | 40 | Each line in the hosts file should use the format 41 | 42 | host type address 43 | 44 | It could look like 45 | 46 | www.example.com A 127.0.0.1 47 | www.example.com AAAA 2606:2800:220:1:248:1893:25c8:1946 48 | 49 | ### --NS 50 | 51 | Ask for a type NS resource. 52 | 53 | ### --onlyq 54 | 55 | When encoding a host name, only put the question part in the package. Skip the answer. 56 | 57 | ### --raw 58 | 59 | Pass the output without doing base64url encoding. 60 | 61 | ### --help 62 | 63 | Show usage message 64 | 65 | # Examples 66 | 67 | $ ./dns2doh daniel.haxx.se 68 | AAAAAQAAAAQAAAAABmRhbmllbARoYXh4AnNlAAABAAHADAABAAEAAAA3AASXZQIxwAwAAQABAAAANwAEl2VCMcAMAAEAAQAAADcABJdlgjHADAABAAEAAAA3AASXZcIx 69 | 70 | $ ./dns2doh daniel.haxx.se | ./base64url-decode | hd 71 | 00000000 00 00 00 01 00 00 00 04 00 00 00 00 06 64 61 6e |.............dan| 72 | 00000010 69 65 6c 04 68 61 78 78 02 73 65 00 00 01 00 01 |iel.haxx.se.....| 73 | 00000020 c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 02 31 |.........7...e.1| 74 | 00000030 c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 42 31 |.........7...eB1| 75 | 00000040 c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 82 31 |.........7...e.1| 76 | 00000050 c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 c2 31 |.........7...e.1| 77 | 00000060 78 | 79 | $ ./dns2doh --AAAA daniel.haxx.se | ./dns2doh --decode --hex 80 | = Incoming 81 | 00: 00 00 00 01 00 00 00 04 00 00 00 00 06 64 61 6e |.............dan| 82 | 10: 69 65 6c 04 68 61 78 78 02 73 65 00 00 1c 00 01 |iel.haxx.se.....| 83 | 20: c0 0c 00 1c 00 01 00 00 00 37 00 10 2a 04 4e 42 |.........7..*.NB| 84 | 30: 00 00 00 00 00 00 00 00 00 00 05 61 c0 0c 00 1c |...........a....| 85 | 40: 00 01 00 00 00 37 00 10 2a 04 4e 42 02 00 00 00 |.....7..*.NB....| 86 | 50: 00 00 00 00 00 00 05 61 c0 0c 00 1c 00 01 00 00 |.......a........| 87 | 60: 00 37 00 10 2a 04 4e 42 04 00 00 00 00 00 00 00 |.7..*.NB........| 88 | 70: 00 00 05 61 c0 0c 00 1c 00 01 00 00 00 37 00 10 |...a.........7..| 89 | 80: 2a 04 4e 42 06 00 00 00 00 00 00 00 00 00 05 61 |*.NB...........a| 90 | AAAA daniel.haxx.se 91 | 92 | $ ./dns2doh --hex daniel.haxx.se 93 | = ALL 94 | 00: 00 00 00 01 00 00 00 04 00 00 00 00 06 64 61 6e |.............dan| 95 | 10: 69 65 6c 04 68 61 78 78 02 73 65 00 00 01 00 01 |iel.haxx.se.....| 96 | 20: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 02 31 |.........7...e.1| 97 | 30: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 42 31 |.........7...eB1| 98 | 40: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 82 31 |.........7...e.1| 99 | 50: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 c2 31 |.........7...e.1| 100 | = Header 101 | 00: 00 00 00 01 00 00 00 04 00 00 00 00 |............| 102 | = Question 103 | 00: 06 64 61 6e 69 65 6c 04 68 61 78 78 02 73 65 00 |.daniel.haxx.se.| 104 | 10: 00 01 00 01 |....| 105 | = Resources 106 | 00: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 02 31 |.........7...e.1| 107 | 10: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 42 31 |.........7...eB1| 108 | 20: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 82 31 |.........7...e.1| 109 | 30: c0 0c 00 01 00 01 00 00 00 37 00 04 97 65 c2 31 |.........7...e.1| 110 | -------------------------------------------------------------------------------- /docs/doh.md: -------------------------------------------------------------------------------- 1 | # doh 2 | 3 | Resolve a host with a given DOH server 4 | 5 | ## Usage 6 | 7 | Usage: doh [options] [host name] [URL] 8 | 9 | ### --A 10 | 11 | Ask for a set of type A resource records. (default) 12 | 13 | ### --AAAA 14 | 15 | Ask for a set of type AAAA resource records. 16 | 17 | ### --NS 18 | 19 | Ask for a set of type NS resource records. 20 | 21 | ### --TXT 22 | 23 | Ask for a set of type TXT resource records. 24 | 25 | ### --TYPEnum 26 | 27 | Ask for a set of resource records of arbitrary numeric type **num**, 28 | in range [1..65535]. 29 | 30 | ### --help 31 | 32 | Show usage message 33 | 34 | # Examples 35 | 36 | $ ./doh daniel.haxx.se https://dns.google.com/experimental | hd 37 | 00000000 00 00 81 80 00 01 00 05 00 00 00 00 06 64 61 6e |.............dan| 38 | 00000010 69 65 6c 04 68 61 78 78 02 73 65 00 00 01 00 01 |iel.haxx.se.....| 39 | 00000020 c0 0c 00 05 00 01 00 00 02 e5 00 27 09 64 75 61 |...........'.dua| 40 | 00000030 6c 73 74 61 63 6b 02 6a 32 06 73 68 61 72 65 64 |lstack.j2.shared| 41 | 00000040 06 67 6c 6f 62 61 6c 06 66 61 73 74 6c 79 03 6e |.global.fastly.n| 42 | 00000050 65 74 00 c0 2c 00 01 00 01 00 00 00 1d 00 04 97 |et..,...........| 43 | 00000060 65 02 31 c0 2c 00 01 00 01 00 00 00 1d 00 04 97 |e.1.,...........| 44 | 00000070 65 42 31 c0 2c 00 01 00 01 00 00 00 1d 00 04 97 |eB1.,...........| 45 | 00000080 65 82 31 c0 2c 00 01 00 01 00 00 00 1d 00 04 97 |e.1.,...........| 46 | 00000090 65 c2 31 |e.1| 47 | 00000093 48 | 49 | $ ./doh --AAAA example.com https://dns.cloudflare.com/.well-known/dns-query | hd 50 | 00000000 00 00 81 80 00 01 00 01 00 00 00 00 07 65 78 61 |.............exa| 51 | 00000010 6d 70 6c 65 03 63 6f 6d 00 00 1c 00 01 c0 0c 00 |mple.com........| 52 | 00000020 1c 00 01 00 00 00 c4 00 10 26 06 28 00 02 20 00 |.........&.(.. .| 53 | 00000030 01 02 48 18 93 25 c8 19 46 |..H..%..F| 54 | 55 | $ ./doh --TXT _esni.only.esni.defo.ie \ 56 | https://dns.cloudflare.com/.well-known/dns-query | hd 57 | 00000000 00 00 81 a0 00 01 00 01 00 00 00 01 05 5f 65 73 |............._es| 58 | 00000010 6e 69 04 6f 6e 6c 79 04 65 73 6e 69 04 64 65 66 |ni.only.esni.def| 59 | 00000020 6f 02 69 65 00 00 10 00 01 c0 0c 00 10 00 01 00 |o.ie............| 60 | 00000030 00 06 f7 00 5d 5c 2f 77 46 38 4e 30 37 37 41 43 |....]\/wF8N077AC| 61 | 00000040 51 41 48 51 41 67 4e 67 79 67 51 63 46 72 77 47 |QAHQAgNgygQcFrwG| 62 | 00000050 6a 65 61 32 37 68 34 6c 49 38 54 77 4b 58 68 42 |jea27h4lI8TwKXhB| 63 | 00000060 49 69 44 55 33 59 75 68 32 6a 62 75 55 46 32 30 |IiDU3Yuh2jbuUF20| 64 | 00000070 63 41 41 68 4d 42 41 51 51 41 41 41 41 41 58 56 |cAAhMBAQQAAAAAXV| 65 | 00000080 73 65 4f 41 41 41 41 41 42 64 57 7a 4e 51 41 41 |seOAAAAABdWzNQAA| 66 | 00000090 41 3d 00 00 29 05 ac 00 00 00 00 00 63 00 0c 00 |A=..).......c...| 67 | 000000a0 5f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |_...............| 68 | 000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 69 | 70 | $ ./doh --TYPE16 _esni.only.esni.defo.ie \ 71 | https://dns.cloudflare.com/.well-known/dns-query | hd 72 | 00000000 00 00 81 a0 00 01 00 01 00 00 00 01 05 5f 65 73 |............._es| 73 | 00000010 6e 69 04 6f 6e 6c 79 04 65 73 6e 69 04 64 65 66 |ni.only.esni.def| 74 | 00000020 6f 02 69 65 00 00 10 00 01 c0 0c 00 10 00 01 00 |o.ie............| 75 | 00000030 00 07 08 00 5d 5c 2f 77 46 38 4e 30 37 37 41 43 |....]\/wF8N077AC| 76 | 00000040 51 41 48 51 41 67 4e 67 79 67 51 63 46 72 77 47 |QAHQAgNgygQcFrwG| 77 | 00000050 6a 65 61 32 37 68 34 6c 49 38 54 77 4b 58 68 42 |jea27h4lI8TwKXhB| 78 | 00000060 49 69 44 55 33 59 75 68 32 6a 62 75 55 46 32 30 |IiDU3Yuh2jbuUF20| 79 | 00000070 63 41 41 68 4d 42 41 51 51 41 41 41 41 41 58 56 |cAAhMBAQQAAAAAXV| 80 | 00000080 73 65 4f 41 41 41 41 41 42 64 57 7a 4e 51 41 41 |seOAAAAABdWzNQAA| 81 | 00000090 41 3d 00 00 29 05 ac 00 00 00 00 00 63 00 0c 00 |A=..).......c...| 82 | 000000a0 5f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |_...............| 83 | 000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 84 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # DOH 2 | 3 | - The [doh tool](doh.md). Resolve a host name using DOH. 4 | - The [dns2doh tool](dns2doh.md). Create DOH queries and answers. 5 | 6 | -------------------------------------------------------------------------------- /docs/server.md: -------------------------------------------------------------------------------- 1 | # Server endpoint 2 | 3 | I run a **toy server** end point at https://daniel.haxx.se. It is HTTPS only 4 | and supports HTTP/2. Issue real-looking DOH requests to this URI: 5 | `https://daniel.haxx.se/dns/?dns=[base64url]`. 6 | 7 | For example, ask for the AAAA resources for the host `daniel.haxx.se`: 8 | 9 | `https://daniel.haxx.se/dns/?dns=AAAAAQAAAAAAAAAABmRhbmllbARoYXh4AnNlAAAcAAE` 10 | 11 | Or the A resources for the host `www.mozilla.org`: 12 | 13 | `https://daniel.haxx.se/dns/?dns=AAAAAQAAAAAAAAAAA3d3dwdtb3ppbGxhA29yZwAAAQAB` 14 | 15 | ## Response 16 | 17 | The server will respond with a "200 OK" and a base64url encoded reply 18 | according to spec. The HTTP answer to a query for the AAAA resources for 19 | `daniel.haxx.se` could look something like: 20 | 21 | HTTP/2 200 22 | date: Wed, 15 Nov 2017 12:33:41 GMT 23 | content-type: application/dns-udpwireformat 24 | 25 | AAAAAQAAAAEAAAAABmRhbmllbARoYXh4AnNlAAABAAHADAABAAEAAAA3AASXZVYx 26 | 27 | A failure to lookup the requested entry will result in a 404 response. 28 | 29 | ## Limitations 30 | 31 | - It requires the query as a GET 32 | 33 | - The server doesn't yet send back the *real* TTL values in the 34 | responses. Only a made up number 35 | 36 | - The server doesn't set a proper cache lifetime as a HTTP response header 37 | 38 | ## Debug helpers 39 | 40 | The server also supports reading 'host' and 'type' as plain ascii strings 41 | *instead* of the base64url encoded 'body'. This allows easy tests without 42 | having to encode the request as a DNS packet: 43 | 44 | `https://daniel.haxx.se/dns/?host=daniel.haxx.se&type=AAAA` 45 | -------------------------------------------------------------------------------- /doh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use Encode qw(encode); 4 | use MIME::Base64 qw(encode_base64url decode_base64url); 5 | use Socket qw(inet_pton AF_INET6 AF_INET); 6 | 7 | sub help { 8 | print STDERR "Usage: doh [options] [host] [uri]\n", 9 | "Options:\n", 10 | " --A encode a type A request (default)\n", 11 | " --AAAA encode a type AAAA request\n", 12 | " --CNAME encode a type CNAME request\n", 13 | " --NS encode a type NS request\n", 14 | " --TXT encode a type TXT request\n", 15 | " --TYPEnum (or --type=num)\n", 16 | " encode a type request (num in [1..65535])\n", 17 | "\n", 18 | "Notes:\n", 19 | " 1. ESNI data may appear as TXT records using prefix '_esni.'\n", 20 | " or as TYPE65439 records using the host name without prefix.\n", 21 | " 2. Not all DOH servers accept queries which specify TYPE65439.\n", 22 | "\n"; 23 | exit; 24 | } 25 | 26 | my %dnstype = (1 => "A", 27 | 2 => "NS", 28 | 5 => "CNAME", 29 | 16 => "TXT", 30 | 28 => "AAAA"); 31 | 32 | my $host; 33 | my $url; 34 | my $type = 1; # default type is A 35 | my $onlyq = 0; # set to 1 if only asking, not getting the answer 36 | my $encode = 1; # default is base64 encode 37 | while($ARGV[0]) { 38 | 39 | if($ARGV[0] eq "--A") { 40 | $type=1; 41 | shift @ARGV; 42 | } 43 | elsif($ARGV[0] eq "--AAAA") { 44 | $type=28; 45 | shift @ARGV; 46 | } 47 | elsif($ARGV[0] eq "--NS") { 48 | $type=2; 49 | shift @ARGV; 50 | } 51 | elsif($ARGV[0] eq "--CNAME") { 52 | $type=5; 53 | shift @ARGV; 54 | } 55 | elsif($ARGV[0] eq "--TXT") { 56 | $type=16; 57 | shift @ARGV; 58 | } 59 | elsif((($ARGV[0] =~ /^--TYPE([0-9]+)$/) || 60 | ($ARGV[0] =~ /^--type=([0-9]+)$/)) && 61 | (int($1) > 0) && 62 | (int($1) < 2**16)) { 63 | $type=int($1); 64 | shift @ARGV; 65 | } 66 | elsif(($ARGV[0] eq "--help") || 67 | ($ARGV[0] eq "-h")) { 68 | help(); 69 | } 70 | elsif(!$host) { 71 | $host = $ARGV[0]; 72 | shift @ARGV; 73 | } 74 | elsif(!$url) { 75 | $url = $ARGV[0]; 76 | shift @ARGV; 77 | } 78 | else { 79 | help(); 80 | } 81 | } 82 | 83 | if(!$host || !$url) { 84 | help(); 85 | } 86 | 87 | sub hexdump { 88 | my ($raw, $title) = @_; 89 | my $i=0; 90 | 91 | if($title) { 92 | print "= $title\n"; 93 | } 94 | my $s; 95 | for my $c (split(//, $raw)) { 96 | if(!($i%16)) { 97 | my $txt="\n"; 98 | if($i) { 99 | $txt=" |$s|\n"; 100 | $s=""; 101 | } 102 | printf "%s%02x: ", $i?$txt:"", $i; 103 | } 104 | printf ("%02x ", ord($c)); 105 | if((ord($c)>=32) && (ord($c)<127)) { 106 | $s .= $c; 107 | } 108 | else { 109 | $s .= '.'; 110 | } 111 | $i++; 112 | } 113 | if($s) { 114 | my $left; 115 | if($i%16) { 116 | $left = ' ' x ((16-$i%16)*3); 117 | } 118 | print "$left |$s|\n"; 119 | } 120 | else { 121 | print "\n"; 122 | } 123 | } 124 | 125 | sub QNAME { 126 | my ($h) = @_; 127 | my $raw; 128 | 129 | my @labels=split(/\./, $h); 130 | foreach my $l (@labels) { 131 | #print STDERR "Label: $l\n"; 132 | $raw .= sprintf("%c", length($l)); 133 | $raw .= $l; 134 | } 135 | $raw .= "\x00"; # end with a zero labeel 136 | return $raw; 137 | } 138 | 139 | my $qname = QNAME($host); 140 | my $qnameptr = sprintf("\xc0\x0c", $answers); 141 | my $ancount = sprintf("\x00%c", $answers); 142 | my $qdcount = sprintf("\x00%c", 1); # questions 143 | my $qtype = sprintf("%c%c", $type >> 8, $type & 255); 144 | my $ttl = pack 'N', $seconds; 145 | 146 | my $header; 147 | 148 | my $query_header = sprintf("\x00\x00". # ID 149 | "\x01\x00". # |QR| Opcode |AA|TC|RD|RA| Z | RCODE | 150 | $qdcount. # QDCOUNT 151 | "\x00\x00". # ANCOUNT 152 | "\x00\x00". # NSCOUNT 153 | "\x00\x00"); # ARCOUNT 154 | my $question = sprintf("$qname". # QNAME 155 | "$qtype". # QTYPE 156 | "\x00\x01"); # QCLASS 157 | 158 | my $msg; 159 | 160 | $header = $query_header; 161 | $msg = "$header$question"; 162 | 163 | my $output = encode("iso-8859-1", "$msg"); 164 | 165 | open(CURL, "|curl -s --data-binary \@- -H 'Content-Type: application/dns-message' $url -o-"); 166 | 167 | print CURL $output; 168 | close(CURL); 169 | 170 | --------------------------------------------------------------------------------