2026年6月14日

2026年6月14日

DoH (DNS over HTTPS) が動作しない場合のトラブルシューティング|自前の DoH サーバーで応答が返らないときに HTTP と DNS の層を分けて調べる

自前で立てた DoH(DNS over HTTPS)サーバーが動かないときは、「TLS と HTTP の層」「DoH のお作法(パス・メソッド・Content-Type)の層」「DNS の層」の3つに分けて、外側から順に確かめると原因に早くたどり着けます。この記事は、Unbound・BIND・dnsdist などで DoH を提供していて、前段に nginx などのリバースプロキシを置いている運用者向けです。curl と dig で症状を正確に読み取る方法と、起こりやすい原因を多い順に、確認方法と直し方を添えて説明します。ブラウザや Windows など利用者側の設定の問題は扱いません。

まず症状を正確に取る

「動かない」の中身を、HTTP のステータスコードと DNS の応答コードで言い表せる状態にするのが最初の作業です。

curl で DoH の GET を送る

RFC 8484 の DoH は、DNS のメッセージをそのまま(ワイヤ形式で)HTTP に載せます。GET では、メッセージを base64url で符号化して dns パラメーターに入れます。次の値は RFC 8484 の例と同じ「www.example.com の A レコード」の問い合わせです。

curl -sv --http2 \
  -H 'accept: application/dns-message' \
  'https://dns.example.com/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB' \
  -o /tmp/doh-answer.bin
ls -l /tmp/doh-answer.bin

-v の出力から次の3点を読み取ります。

  • **TLS のハンドシェイクが成功し、ALPN で h2 が選ばれたか。** ALPN: server accepted h2 のような行が目安です(表示は curl の版で異なります)。
  • HTTP のステータスコード。 正常なら 200 です。
  • **応答の content-type。** 正常なら application/dns-message で、本体は数十バイト以上のバイナリになります。

dig で DNS として読む

BIND 9.18 以降の dig は DoH で問い合わせられるので、応答の中身(DNS の応答コード)まで確認できます。

# 既定のパス /dns-query に POST で問い合わせる
dig @dns.example.com +https example.com A

# GET で問い合わせる
dig @dns.example.com +https-get example.com A

# パスが違う場合は指定する
dig @dns.example.com +https=/custom-path example.com A

ここまでで、症状は次のどれかに分類できます。

症状 問題のある層
接続できない・TLS のエラー TLS と HTTP(証明書・ポート・ALPN)
404・405・415・400 DoH のお作法(パス・メソッド・形式)
502・503・504 リバースプロキシとバックエンドの間
200 だが status: SERVFAIL や REFUSED DNS(リゾルバーの設定・上流・DNSSEC)

DoH では、DNS としての失敗(SERVFAIL など)も HTTP としては 200 で返るのが普通です。HTTP が 200 なのに名前が引けないときは、HTTP 側ではなく DNS 側を調べます。

原因と対処(起こりやすい順)

1. 証明書・SNI・ALPN の問題

TLS の段階で失敗している場合は、まず証明書を直接確認します。

openssl s_client -connect dns.example.com:443 -servername dns.example.com -alpn h2 </dev/null 2>/dev/null \
  | grep -E 'subject=|issuer=|Verify return code|ALPN'
  • Verify return code: 0 (ok) でなければ、証明書の期限切れ、ホスト名の不一致、中間証明書の欠落を疑います。同じ 443 番で WordPress のサイトも動かしている場合、SNI で別のサイトの証明書が返っていることがよくあります。nginx の server_name が DoH 用のホスト名と一致しているかを確認します。
  • ALPN protocol: h2 が出ない場合は、HTTP/2 が有効になっていません。RFC 8484 は HTTP/2 以上を推奨しており、クライアントによっては HTTP/2 でないと接続しません。nginx では 1.25.1 以降は http2 on;、それより前は listen 443 ssl http2; で有効にします。

2. パスの不一致(404)

クライアントに設定した URL のパスと、サーバー側のパスが一致していないケースです。一般的なパスは /dns-query で、Unbound の http-endpoint の既定値もこれです。nginx の location で末尾のスラッシュの有無が違う、proxy_pass の URL にパスを書いたためにパスが書き換わっている、といった食い違いがよく起こります。nginx のアクセスログで、実際にどのパスに来て何を返したかを確認します。

sudo tail -f /var/log/nginx/access.log | grep dns-query

3. メソッドや形式の誤り(405・415・400)

  • 405:GET か POST 以外で来ている、またはプロキシ側でメソッドを制限しています。
  • 415:POST で content-type: application/dns-message が付いていません。RFC 8484 では、受け付けない形式に 415 を返すことになっています。JSON 形式の API(application/dns-json)は一部の公開リゾルバー独自のもので、自前の DoH サーバーが対応しているとは限りません。
  • 400:dns パラメーターの値が壊れています。base64url では + と / の代わりに - と _ を使い、末尾の = を付けません。通常の base64 で符号化した値を渡すと、この失敗になります。

4. リバースプロキシとバックエンドのプロトコル不一致(502・503・504)

前段の nginx から見て、バックエンドの DoH サービスに接続できていない状態です。nginx のエラーログ(/var/log/nginx/error.log)に connect() failed や upstream prematurely closed などの理由が出ます。

多い原因は、プロキシとバックエンドが話すプロトコルの食い違いです。

  • Unbound の DoH は HTTP/2 のみに対応しています。 nginx の proxy_pass は長く上流へ HTTP/1.x で接続する仕様だったため、そのまま Unbound の DoH へ転送しても通りません。前段のプロキシが上流へ HTTP/2 で接続できるかを、使っている版のドキュメントで確認します。できない場合は、Unbound に直接 DoH を受けさせる、HTTP/2 で上流に接続できるプロキシ(HAProxy など)を使う、HTTP/1.1 で待ち受けられる DoH 実装を挟む、のいずれかにします。Unbound には、ローカルのプロキシから TLS なしで受けるための http-notls-downstream という設定もあります。
  • TLS の有無の食い違い。 バックエンドが TLS で待ち受けているのに proxy_pass http:// で平文接続している、またはその逆です。

HTTP/1.1 で待ち受ける DoH 実装の前に nginx を置く場合の、最小限の例を示します。

# server ブロック内(例。バックエンドのアドレスとパスは実装に合わせる)
location = /dns-query {
    proxy_pass http://127.0.0.1:8053;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Unbound で DoH を提供する場合は、Unbound が nghttp2 ライブラリ付きでビルドされている必要があります。ディストリビューションのパッケージによっては無効なことがあるため、unbound -V の出力でビルドオプションを確認します。

5. 443 番の取り合い

WordPress を動かしているサーバーで DoH も提供しようとすると、nginx や Apache がすでに 443 番を使っていて、DoH サービスが起動しない、または起動しても別のサーバーが応答している、ということが起こります。

sudo ss -lntp | grep ':443\b'

同じホストで両方を動かすなら、DoH 用に別の IP アドレスを割り当てるか、Web サーバーを前段にしてパスで振り分ける構成にします。

6. 200 なのに REFUSED や SERVFAIL

HTTP は正常で、DNS の応答コードが失敗している場合です。

  • REFUSED:リゾルバーのアクセス制御で拒否されています。リバースプロキシを挟むと、バックエンドから見た送信元はプロキシ(多くは 127.0.0.1)になります。Unbound なら access-control にプロキシのアドレスが含まれているか確認します。この構成では、利用者ごとの制限はリゾルバーではなくプロキシ側で行うことになります。
  • SERVFAIL:上流へ届いていない、または DNSSEC の検証に失敗しています。DoH を経由せず、サーバー上で平文の 53 番に dig @127.0.0.1 example.com と +cd 付きで問い合わせて比べると、どちらかが分かります。+cd で引けるなら DNSSEC の問題です。

7. キャッシュとタイムアウト

DoH の GET の応答は HTTP のキャッシュの対象になり得ます。プロキシや CDN を前段に置いていると、古い応答が返り続けることがあります。サーバーは DNS の TTL に合わせた Cache-Control を付けるのが一般的なので、プロキシ側で独自に長くキャッシュする設定を入れていないか確認します。また、上流の応答が遅いと、プロキシの読み取りタイムアウトに先に達して 504 になることがあります。

ログを見るときの注意

原因を調べるために Unbound の log-queries: yes や verbosity を上げると、問い合わせた名前と送信元がログに残ります。DoH は利用者のプライバシーを守るための仕組みなので、詳細なログは調査の間だけ有効にし、終わったら元に戻して、残したログの扱いも決めておきます。

設定を変更するときは、変更前の設定ファイルを控え、検証用の環境で試してから本番に反映してください。DoH を公開する場合は、利用者を限定する仕組みを設けないと、誰でも使えるリゾルバーとして悪用される恐れがあります。各ソフトウェアの設定項目や既定値は版によって変わるため、使っている版のドキュメントを確認してください(2026年10月現在)。

まとめ

  • まず curl の -v と dig +https で、HTTP のステータスコードと DNS の応答コードを取り、問題の層を決めます。
  • TLS の失敗は証明書・SNI・ALPN(HTTP/2)を openssl s_client で確認します。
  • 404・405・415・400 は、パス・メソッド・Content-Type・base64url の符号化を見直します。
  • 502〜504 はプロキシとバックエンドの問題です。Unbound の DoH は HTTP/2 のみなので、前段のプロキシとのプロトコルの組み合わせに注意します。
  • 200 で REFUSED ならアクセス制御、SERVFAIL なら上流か DNSSEC を、平文の 53 番での比較で切り分けます。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲