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>