DNS を直接 JavaScript で受け取る

[[English Version](https://qiita.com/martinheidegger/items/0a6ecd63b9bfb7f639f7)]

[2021年の「ID について」のポスト](https://qiita.com/martinheidegger/items/425112e84f7c12148d90)の後、今年がもう少し低レベルのになっています。

誰でも一人一人は毎日[グローバルの DNS](https://ja.wikipedia.org/wiki/Domain_Name_System) を使っています。インターネットや日常生活とあまりにも根本的に結びついているため、ほとんど考えず、ただうまく機能することを願うだけになってしまっています。

> リマインダー: DNS (domain=ドメイン,name=名前,system0システム) を使って、我々のパソコンはグローバルの名前（例「qiita.com」または 「google.com」）と連携しているリソース(例：メールサーバ、ウエッブサーバ)を調べることができます。

通常、ドメインの検索はOSの低レベルの処理に任せています。ブラウザで`https://qiita.com`にナビゲートするとき、ブラウザがどのサーバにデータがあるのか、どうやって知ることができるのか、考えたことがありますか？

普段、意識することはありませんが...実は、DNSエントリにはちょっとしたデータが保存されていることをご存知ですか？ そのデータは大したものではありませんが、デジタル署名、名刺、デジタルウォレットのアドレスとしては十分なものです。このデータは、実用的なものにも非実用的なものにも利用できます。 :wink:

個人的には、[→ dnslink](https://dnslink.dev)の実験をしたときに、このことを知る必要がありました。

今日の話題はかなりマニアックで、おそらく皆さんには必要のないものであることを知っておいてください。私にとっては、考えるのが楽しくて、実験するのが面白いだけなのです。

### Node.js を使っての基本的なこと

Node.js で DNS 情報にアクセスする通常の方法は、比較的退屈です。

```javascript:test_01_nodejs_lookup.mjs
import { lookup, resolveTxt } from 'node:dns/promises'

console.log(await lookup('qiita.com'))
console.log(await resolveTxt('qiita.com'))
```

結果は以下のようなものです。

```js
{ address: '54.65.85.19', family: 4 }
[
  ['google-site-verification=OCwPip5vYMz3KMtOVXGDy8MVETsPPKp8zcR5gxcP0mk'],
  ['pardot859913=459e91b4646f79a9dc3bb58970676deab274ab5fb48dbb4a2ae1f01950a469f5'],
  ['pardot859913=4e67330c615e435955fd1e1973811b368a84de7712bebe791483228997d47650'],
  ['v=spf1 include:_spf.google.com include:amazonses.com include:servers.mcsv.net include:mail.zendesk.com include:aspmx.pardot.com a:qiita.com ~all']
]
```

この2つの関数は、他のプログラミング言語でも非常によく使われています。`lookup` と `resolveTxt` は他のプログラミング言語でも非常によく使われる関数です。

その理由は簡単です。Node.js は他の多くのものと同様に、DNS にアクセスするための低レベルのC言語のライブラリである「[c-ares](https://c-ares.org/)」を使用しています。

C言語で書いているから、その API はブラウザーでは利用できまないね。

### DNS をブラウザーで使う

まず、ちょっと歴史について: DNS はプライバシーの問題あります。そのままで使うと `http://` と同じく、インタネットの皆さんが DNS リクエストを読めます。例え、「[https://jsbeginners.com](https://jsbeginners.com)」のドメインをブラウザに入力すれば、自動的に `jsbeginners.com` の DNS リクエストが投げているので、理論的にはパケットデータを読む誰でもあなたが初心者のことをわかるかも。:sweat:

データを隠すためには VPN を使えますが、それでも VPN社は相変わらずそのパケットを読めますね。これを避けるために、プライバシーを向上させるための取り組みが行われています。最初は DoT (DNS over TLS) が2018年に出てきました。スペック通りだとプロクシサーバが DNS API を TLS(secure) なコネクションを使っています。

私にさらに面白そうなのは DoH (DNS over HTTPS)です。DoT と同時に出てきたんですが、DNS APIを普通の HTTPS リクエストを使って呼ぶようになっています。DoH サーバを使って DNS をブラウザーで利用できます！

```mermaid
sequenceDiagram
    ブラウザー-->>DoH サーバ: DNS-リクエスト
    DoH サーバ-->>DNS: DNS-リクエスト
    DNS-->>DoH サーバ: DNS-リスポンス
    DoH サーバ-->>ブラウザー: DNS-リスポンス
```

### 使い方

ウエッブ開発者としては `JSON` または `XML` データが似合っていますが、`DNS-リクエスト`も`DNS-リスポンス`も両方ともがバイナリパケット (`Uint8Array`) としてスペックされています。そのためは [→ mafintosh](https://github.com/mafintosh)様が結構前に [→ dns-packet](https://www.npmjs.com/package/dns-packet) を公開しました。そのライブラリーは DNS スペックのうちに意外とたくさん対応しています。そのままで使えますが、ちょっと古臭くて commonjs で開発しています。だから、私は頑張って `esm` で書き換えてフォークして公開しました。[→ @leichtgewicht/dns-packet](https://npmjs.com/@leichtgewicht/dns-packet)。ブラウザーでも、Node.jsでも React-nativeなどでもそのまま使えます。

```javascript:test_02_dns-packet.mjs
import { encode, decode } from '@leichtgewicht/dns-packet'

console.log(
  encode({
    type: 'query',
    id: 1,
    questions: [
      { type: 'A', name: 'qiita.com' } 
    ]
  }),
  decode(new Uint8Array([
      0,  1, 0,  0,   0,   1,   0,   0,  0,  0,  0,  0,   5, 113, 105, 105,
    116, 97, 3, 99, 111, 109,   0,   0,  1,  0,  1
  ]))
)
```

それなら `https://` とちゃんと利用できる DNS パケットが実現できました。:eyeglasses:
安全なサーバ 「dns.cloudflare.com」に fetch の API を使って POST リクエストを投げて、DNSの返信を手入ることは可能になりました。

```javascript:test_03_send_dns.mjs
import { encode, decode, RECURSION_DESIRED } from '@leichtgewicht/dns-packet'

const res = await fetch('https://dns.cloudflare.com/dns-query', {
  method: 'POST',
  headers: { 'content-type': 'application/dns-message' },
  body: encode({
    type: 'query',
    flags: RECURSION_DESIRED,
    questions: [
      { type: 'A', name: 'qiita.com' }
    ]
  })
})
const bytes = new Uint8Array(await res.arrayBuffer())
console.log(decode(bytes).answers)
```

結果を見て、すぐ気づくと思いますが、もうNode.jsと比べて他のデータが出てきます。

```
{
  name: 'qiita.com',
  type: 'A',
  ttl: 60,
  class: 'IN',
  flush: false,
  data: '3.115.205.169'
}
```

特に `ttl` が結構大切になります。Node.js が全ての API にちゃんと対応しないからです。

## 頭痛の減らすように

なお、それを使ってリクエストやリスポンスをいくらでもハックできます。それでも不便やめんどくさいところはいくつかあります。プラットフォーム対オプ、エラーハンドリング、などは結構めんどくさいです。

作業が簡単になるために [→ dns-query](https://npmjs.com/package/dns-query) を公開しました。

```javascript:test_04_dns-query.mjs
import { query, wellknown } from 'dns-query'

console.log(
  (await query(
    { question: { type: 'A', name: 'qiita.com' } },
    { endpoints: wellknown.endpoints() }
  )).answers
)
```

`dns-query` のパッケージはいくつかの細かいことを対応しています。すぐにでも役に立つかもしれません。例えば： `wellknown.entpoints()` では `https://dnscrypt.info` があつめている最新版の新しいサーバ一覧がダウンロードされています。

## これから

この簡単な API でどのエンバイロンメントでも DNS リクエストを投げることができるようになりました。例としては [→ dnslink.dev](https://dnslink.dev#try-it-out)の「Try it out」コンポーネントを考えいています。その時には JavaScript を使って DNS のエントリーをブラウザ上でテストできます。

<kbd>

[![Screenshot of dnslink.dev](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20010/697ef325-66e9-9bd6-68ef-5943044932ff.png)](https://dnslink.dev#try-it-out)

</kbd>

今からはあなたもフロント、バック、どこでも特別な設定なくてもすぐ使えると思います。

未来に [DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions) を対応できると良いと思いますので、ちょっとしたお願いあります。[webcrypto-secure-curves](https://github.com/WICG/webcrypto-secure-curves) に :+1: (vote) をするとこれから実現しやすくなります。ありがとうございます。

---


`dns-query` でアイディアがうめれたら、コメントがあったらまた `dns-query`を使って何かを実現できたら、ぜひ以下にコメントしてください。

個人的なアップデートなんですが、今年からはまた日本企業のフリーランスになりました。最近結構忙しいので今年の記事はちょっと短くなりましたし、OSS の公開したものも少なくなりました。来年はもうちょっと実用的にしようと思って、ログインシステムについた書くかとおもいっています。

- :bird: Twitter → [@leichtgewicht](https://twitter.com/leichtgewicht)
- :speech_left: Discord → [OWDDM Osaka Community](https://owddm.com/discord)

ありがとうございました。メリークリスマス、そして来年もよろしくお願いします。
:heart: :xmas-tree: