LoginSignup
0
0

DNS over HTTPS を使う

Posted at

DNS over HTTPS を使ってみる

ホスト名から IP アドレスを取得する方法が RFC 8484 で標準化されているのでテストするものを作ってみました。

DoH サーバーはデフォルトで https://dns.google/dns-query としています。
他にも https://cloudflare-dns.com/dns-query などがあるようです。

※ Punycode 化は行っていないので、いわゆる日本語ドメイン名などには対応していません
※ IPv4,IPv6 アドレスの短縮表現化も行っていません
※ 不足は他にもあると思います

See the Pen DNS over HTTPS テスト by Ikiuo (@ikiuo) on CodePen.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>DNS over HTTPS テスト</title>
  </head>
  <body>
    <script>

     /* Base64URL 符号化に使用する文字 */
     const BASE64_CHAR = ([...
         'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
         'abcdefghijklmnopqrstuvwxyz' +
         '0123456789' + '-.'
     ]);

     /* ドメイン名で使用可能な文字 */
     const DNS_CHAR = new Set(
         'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
         'abcdefghijklmnopqrstuvwxyz' +
         '0123456789' + '.-'
     );

     /* バイト列を Base64URL に符号化する */
     function encodeBase64URL(binary) {
         const table = BASE64_CHAR;
         const blen = binary.length;
         const brem = blen % 3;
         const bcnt = blen - brem;
         let text = '';
         let i = 0;
         while (i < bcnt) {
             const d0 = binary[i++];
             const d1 = binary[i++];
             const d2 = binary[i++];
             const d = (d0 << 16) | (d1 << 8) | d2;
             text += table[(d >> 18) & 0x3f];
             text += table[(d >> 12) & 0x3f];
             text += table[(d >>  6) & 0x3f];
             text += table[d & 0x3f];
         }
         if (brem) {
             const b2 = (brem == 2);
             const d0 = binary[i++];
             const d1 = (b2 ? binary[i++] : 0);
             const d = (d0 << 16) | (d1 << 8);
             text += table[(d >> 18) & 0x3f];
             text += table[(d >> 12) & 0x3f];
             if (b2) text += table[(d >> 6) & 0x3f];
         }
         return text;
     }

     /* DNS 質問を作る */
     function createDNSQuery(id, qtype, hostname) {
         const binary = [
             (id >> 8) & 0xff, (id >> 0) & 0xff,  // ID
             0x01, 0x00,  // RD=1
             0x00, 0x01,  // QDCOUNT=1
             0x00, 0x00,  // ANCOUNT=0
             0x00, 0x00,  // NSCOUNT=0
             0x00, 0x00,  // ARCOUNT=0
         ];

         const qhostname = (hostname[hostname.length - 1] == '.')
                         ? hostname.slice(0, -1) : hostname;

         (new Set(qhostname)).forEach(c => {
             if (!DNS_CHAR.has(c))
                 throw TypeError(`invalid hostname: "${hostname}"`);
         });

         // QNAME
         qhostname.split('.').forEach(label => {
             if (label.length == 0 || label.length > 63)
                 throw TypeError(`invalid hostname: "${hostname}"`)
             binary.push(label.length);
             for (const c of label)
                 binary.push(c.charCodeAt(0));
         });
         binary.push(0);

         binary.push(
             // QTYPE
             (qtype >> 8) & 0xff,
             (qtype >> 0) & 0xff,

             // QCLASS
             0x00, 0x01 // IN
         );

         return new Uint8Array(binary);
     }

     /*
      * DNS 応答を分析する
      * (A, AAAA, CNAME のみ)
      */
     const textDecoder = new TextDecoder();
     function parseDNSResponse(packet) {
         const decoder = textDecoder;

         let pos = 0;
         const readByte = () => packet[pos++];
         const readWord = () => (readByte() << 8) | readByte();
         function readTextN(len) {
             const [beg, end] = [pos, pos += len];
             return decoder.decode(packet.slice(beg, end));
         }
         const readText = () => readTextN(readByte());
         function readLabel() {
             const label = [];
             let stack = null;
             for (;;) {
                 const len = readByte();
                 if (len == 0) {
                     if (stack != null)
                         pos = stack;
                     break;
                 }
                 if (len < 64) {
                     label.push(readTextN(len));
                     continue;
                 }
                 if (len < 192)
                     throw new RangeError();

                 const newpos = ((len & 0x3f) << 8) | readByte();
                 if (stack == null)
                     stack = pos;
                 pos = newpos;
             }
             return label.join('.');
         }
         function readRR() {
             const name = readLabel();
             const rType = readWord();
             const rClass = readWord();
             const ttl = (readWord() << 16) | readWord();
             const rdlength = readWord();
             const rd_beg = pos;
             const rd_end = pos + rdlength;

             const RR = {
                 name: name,
                 resourceType: rType,
                 resourceClass: rClass,
                 ttl: ttl,
                 rdlength: rdlength,
                 rdata: packet.slice(rd_beg, rd_end),
             }

             switch (rType) {
                 case 1: // A
                     {
                         const ipv4 = [];
                         while ((rd_end - pos) >= 4) {
                             const addr = [...Array(4)].map(() => readByte());
                             ipv4.push(addr.join('.'));
                         }
                         RR.ipv4 = ipv4;
                     }
                     break;
                 case 5: // CNAME
                     RR.cname = readLabel();
                     break;
                 case 28: // AAAA
                     {
                         const ipv6 = [];
                         while ((rd_end - pos) >= 16) {
                             const addr = [...Array(8)].map(() => readWord());
                             ipv6.push(addr.map(w => w.toString(16)).join(':'));
                         }
                         RR.ipv6 = ipv6;
                     }
                     break;
             }

             pos = rd_end;
             return RR;
         }

         const header = {}
         header.id = readWord();
         {
             const flag = readByte();
             header.qr = !!(flag & 0x80);
             header.opcode = (flag >> 3) & 0x0f;
             header.aa = !!(flag & 0x04);
             header.tc = !!(flag & 0x02);
             header.rd = !!(flag & 0x01);
         }
         {
             const flag = readByte();
             header.ra = !!(flag & 0x80);
             header.z = (flag >> 4) & 0x07;
             header.rcode = flag & 0x0f;
         }
         header.qdcount = readWord();
         header.ancount = readWord();
         header.nscount = readWord();
         header.arcount = readWord();

         const question = [...Array(header.qdcount)].map(() => ({
             qname: readLabel(),
             qtype: readWord(),
             qclass: readWord(),
         }));
         const answer = [...Array(header.ancount)].map(() => readRR());
         const authority = [...Array(header.nscount)].map(() => readRR());
         const additional = [...Array(header.arcount)].map(() => readRR());
         return { header, question, answer, authority, additional }
     }

     /*
      * ホスト名から IP アドレスを取得する
      * (RFC 8484 : DNS over HTTP)
      */

     const debug = false;
     let dnsQueryID = 0;
     async function doQuery(urlquery, hostname, qtype) {
         try {
             const query_packet = createDNSQuery(++dnsQueryID, qtype, hostname);
             if (debug)
                 console.log('Query:', parseDNSResponse(query_packet));
             const query_base64 = encodeBase64URL(query_packet);
             const res = await fetch(`${urlquery}?dns=${query_base64}`, {
                 method: 'GET',
                 headers: new Headers({
                     'Accept': 'application/dns-message',
                 }),
             });

             const blob = await res.blob();
             const binary = await blob.arrayBuffer();

             if (!res.ok)
                 return {
                     status: `ERROR(${res.status})`,
                     error: textDecoder.decode(binary),
                 }

             const dnsres = parseDNSResponse(new Uint8Array(binary));
             if (debug)
                 console.log('Response:', dnsres);
             const respose = {
                 status: `HTTP=${res.status}, RCODE=${dnsres.header.rcode}`,
                 rcode: dnsres.header.rcode,
                 cname: [
                     dnsres.answer.map(a => a.cname),
                     dnsres.authority.map(a => a.cname),
                     dnsres.additional.map(a => a.cname),
                 ].flat().filter(s => !!s),
                 address: [
                     dnsres.answer.map(a => a.ipv4 ?? a.ipv6),
                     dnsres.authority.map(a => a.ipv4 ?? a.ipv6),
                     dnsres.additional.map(a => a.ipv4 ?? a.ipv6),
                 ].flat().filter(s => s && s.length > 0),
             }
             respose.cname.sort();
             respose.address.sort();
             return respose;
         } catch (e) {
             console.error(e);
             return {status:`ERROR(${e})`}
         }
     }

     async function getIPAddr(urlquery, hostname) {
         const v4 = await doQuery(urlquery, hostname, 1);   // 'A' = 1
         const v6 = await doQuery(urlquery, hostname, 28);  // 'AAAA' = 28
         const status = {
             v4: v4.status,
             v6: v6.status,
         }
         const error = {
             v4: v4.error,
             v6: v6.error,
         }
         const cname = [... new Set([
             v4.cname ?? [],
             v6.cname ?? [],
         ].flat().filter(s => s && s.length > 0))].sort();
         const ipv4 = !v4 ? [] : v4.address ?? [];
         const ipv6 = !v6 ? [] : v6.address ?? [];
         return {status, cname, ipv4, ipv6, error}
     }

     /*
      * UI
      */

     function onClick() {
         const dns = tagDNS.value;
         const host = tagHost.value;

         if (host.length == 0) {
             tagOut.innerText = 'ホスト名がありません';
             tagErrV4.innerHTML = '';
             tagErrV6.innerHTML = '';
             return;
         }

         const indent = '\xa0\xa0\xa0\xa0';
         getIPAddr(dns, host, true).then(res => {
             tagOut.innerText = [
                 `IPv4: ${res.status.v4}\n${indent}${res.ipv4.join('\n' + indent)}`, '',
                 `IPv6: ${res.status.v6}\n${indent}${res.ipv6.join('\n' + indent)}`, '',
                 `CNAME:\n${indent}${res.cname.join('\n' + indent)}`,
             ].join('\n');
             tagErrV4.innerHTML = res.error.v4 ?? '';
             tagErrV6.innerHTML = res.error.v6 ?? '';
         });
     }

     // CodePen 対策
     window.onload = function() {
         document.body.innerHTML = [
             '<table border="1">',
             '<tr>',
             '<th>DoH(URL)</th>',
             '<td><input id="tagDNS" type="text" style="width: 40em;" value="https://dns.google/dns-query"></td>',
             '</tr>',
             '<tr>',
             '<th>ホスト名</th>',
             '<td><input id="tagHost" type="text" style="width: 40em;"></td>',
             '</tr>',
             '<tr>',
             '<th><button onclick="onClick()">取得</button></th>',
             '<td id="tagOut"></td>',
             '</tr>',
             '<tr><th>IPv4: Error</th><td id="tagErrV4"></td></tr>',
             '<tr><th>IPv6: Error</th><td id="tagErrV6"></td></tr>',
             '</table>'
         ].join('');
     }

    </script>
  </body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0