DockerでAdGuard HomeとUnboundを構築:HomeLab向けの再帰DNSリゾルバーと広告ブロッカーを作成

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

半年前、私はGoogleやCloudflareなどの商用アップストリームDNSサーバーにDNSクエリが送信され続ける状況を見直すことにしました。一般的な広告ブロッカーはブラウザ上のバナーを非表示にできますが、暗号化されていないドメインの問い合わせ自体は依然として外部へ送信されてしまいます。AdGuard HomeとUnboundを組み合わせることで、この問題を根本から解決できます。

AdGuard Homeは最前線のフィルターとして機能し、LAN全体の広告、テレメトリ、追跡ドメインへのアクセスをシンクホール(破棄)します。その直背後に配置されたUnboundはDNSSEC検証を行う再帰リゾルバーとして動作し、権威ルートネームサーバーと直接通信します。40台以上のデバイスから1日あたり12万件を超えるクエリを処理する環境で運用していますが、この2コンテナ構成は外部プロファイリングを完全に排除しながら、キャッシュヒット時1msという極めて安定した応答速度を実現しています。

クイックスタート(5分)

DockerおよびDocker ComposeがインストールされているLinuxマシン(Debian、Ubuntu、Raspberry Pi OSなど)であれば、すぐにこのスタックを立ち上げることができます。

1. ホストディレクトリ構造の準備

mkdir -p ~/homelab-dns/{adguard/work,adguard/conf,unbound}
cd ~/homelab-dns

2. Unboundの設定

unbound/unbound.confにUnboundの設定ファイルを作成します。この設定では、ルートサーバーへの反復問い合わせ、厳格なDNSSEC検証を有効化し、内部リッスンポートを5335にバインドします:

cat <<'EOF' > unbound/unbound.conf
server:
    verbosity: 1
    interface: 0.0.0.0
    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no
    prefer-ip6: no

    # アクセス制御
    access-control: 127.0.0.1/32 allow
    access-control: 192.168.0.0/16 allow
    access-control: 172.16.0.0/12 allow
    access-control: 10.0.0.0/8 allow

    # セキュリティとパフォーマンス
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no
    edns-buffer-size: 1232
    prefetch: yes
    prefetch-key: yes
    num-threads: 1

    # キャッシュ設定
    msg-cache-size: 64m
    rrset-cache-size: 128m
    infra-cache-numhosts: 10000
EOF

3. Docker Composeファイルの作成

作業ディレクトリ内に以下の構成をdocker-compose.ymlとして保存します:

version: "3.8"

networks:
  dns_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24

services:
  unbound:
    image: mvance/unbound:latest
    container_name: unbound-resolver
    restart: unless-stopped
    networks:
      dns_net:
        ipv4_address: 172.20.0.2
    volumes:
      - ./unbound/unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro

  adguardhome:
    image: adguard/adguardhome:latest
    container_name: adguard-home
    restart: unless-stopped
    depends_on:
      - unbound
    networks:
      dns_net:
        ipv4_address: 172.20.0.3
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "3000:3000/tcp"  # 初期Webセットアップウィザード
      - "8080:80/tcp"    # Webダッシュボード
    volumes:
      - ./adguard/work:/opt/adguardhome/work
      - ./adguard/conf:/opt/adguardhome/conf

4. ポート53の解放とコンテナの起動

UbuntuやDebianでは、デフォルトでsystemd-resolvedが127.0.0.53:53でリッスンしています。このポート競合があるとAdGuard Homeが起動できません。コンテナを起動する前にスタブリスナーを無効化します:

sudo sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved

# スタックの起動
docker compose up -d

ブラウザでhttp://<YOUR_SERVER_IP>:3000を開き、AdGuard Homeの初期ウィザードを完了します。Web管理インターフェースをポート80(ホスト側からは8080でアクセス可能)に、DNSを全インターフェースのポート53でリッスンするように設定します。

詳細解説

この2層アーキテクチャ内でクエリがどのように処理されるかを理解しておくと、名前解決のトラブルシューティングがスムーズになります。

クエリ解決の仕組み

ネットワーク内の端末からのすべてのDNSリクエストは、以下の明確な5段階のパイプラインで処理されます:

  1. クライアントの問い合わせ: LAN上のデバイスが、サーバーIPのポート53(UDP/TCP)へ標準的なDNSリクエストを送信します。
  2. AdGuardによるフィルタリング: AdGuardが有効なブロックリストと照合してドメインを評価します。トラッカーや広告サーバーに一致した場合、AdGuardは即座に0.0.0.0を返し、1ms未満でブロックします。
  3. Unboundでの反復解決: 許可されたクエリは内部ネットワーク経由で172.20.0.2:5335のUnboundに渡されます。サードパーティの再帰DNSに頼る代わりに、Unboundはルートゾーンサーバー(.)に問い合わせ、TLDネームサーバー(.comなど)へ下り、最終的に権威ネームサーバーから正確なレコードを取得します。
  4. DNSSEC検証: Unboundがトラストチェーンに沿って暗号署名を認証します。署名が偽造または削除されている場合、Unboundはレコードを破棄してSERVFAILを返し、キャッシュポイズニング攻撃を防止します。
  5. キャッシュと応答: AdGuardがローカルメモリに応答を保存し、最終的なIPアドレスをクライアントデバイスに返します。

ルートヒントチェーンを経由する初回の再帰問い合わせには45〜90msほどかかりますが、強力なキャッシュ機構により体感レイテンシは極めて低く保たれます。日々の実運用では、クエリの85%〜92%がローカルRAMのキャッシュにヒットし、WAN側にアクセスすることなく2ms未満で応答が返されます。

AdGuardとUnboundの連携設定

AdGuard HomeのWeb管理画面で設定 > DNS設定を開き、以下の項目を設定します:

  • アップストリームDNSサーバー: 172.20.0.2:5335
  • ブートストラップDNSサーバー: 1.1.1.1:53(コンテナ起動時にブロックリストURLを初回解決するためだけに使用されます)
  • アップストリームモード: 同時リクエスト または 負荷分散

アップストリームをテストをクリックします。緑色の検証バッジが表示されれば、Unboundによる名前解決が正常に動作しています。

高度な活用法

基本的な名前解決が完了したら、ローカルルーティングや自動メンテナンスの設定を追加してみましょう。

1. 内部ドメイン向けのスプリットホライズンDNS

TraefikやNginx Proxy Managerの配下でローカルサービスを運用している場合は、プライベートドメインをリバースプロキシ(例: 192.168.1.50)へ直接ルーティングすることでHairpin NATを回避できます。AdGuard Homeのフィルター > カスタムフィルタリングルールに移動し、以下を追加します:

# 内部ドメインをローカルリバースプロキシにルーティング
||home.lab^$dnsrewrite=192.168.1.50
||*.home.lab^$dnsrewrite=192.168.1.50

2. ブロックリストの選定とチューニング

膨大で重複したルールを読み込みすぎると、防御効果が高まらないばかりかパフォーマンスが低下します。約35万ルール前後でバランスの取れたホームネットワークを構築するには、フィルター > DNSブロックリストで以下を有効にすることをお勧めします:

  • AdGuard DNS filter: Web広告や一般的なトラッカーに対する優れた基本保護を提供します。
  • OISD (Basic / Full): 誤検知(False Positive)を極力抑えつつ、インターネット全体を幅広くカバーします。
  • HaGeZi Multi Pro: 強力なテレメトリ、アフィリエイトトラッキング、マルウェアドメインを徹底的に防御します。

3. ルートヒント更新の自動化

UnboundはIANAのRoot Hintsファイルを参照してルートサーバーのIPを特定します。これらのアドレスが変更される頻度は低いものの、年に2回程度更新しておくのが運用のベストプラクティスです。ホスト側に以下のシンプルな月次cronジョブを追加します:

# 毎月1日に最新のルートヒントを取得してUnboundを再起動
0 3 1 * * curl -s -o ~/homelab-dns/unbound/root.hints https://www.internic.net/domain/named.root && docker restart unbound-resolver

運用の実践的なヒント

自前で再帰リゾルバーを運用する際には、いくつかの注意点があります。ネットワークの高速性と安定性を維持するための実践的なアドバイスを4つ紹介します:

  • 楽観的キャッシュ(Optimistic Caching)の有効化: AdGuardのDNS設定でキャッシュサイズを67108864(64MB)に設定し、楽観的キャッシングを有効にします。期限切れのキャッシュレコードを使ってクライアントへ即座に応答しつつ、バックグラウンドで静かにキャッシュを更新するため、Webブラウジングが常に快適になります。
  • use-caps-for-idは無効のままにする: 0x20ビットのランダム化は大文字・小文字をランダムに変更(例: eXaMpLe.CoM)してスプーフィングを防ぐ技術です。しかし、一部の大手CDNや権威ネームサーバーでは大文字小文字が混在したリクエストを正しく処理できず、間欠的な名前解決タイムアウトの原因となる場合があります。
  • クライアントごとの個別フィルタリング: AdGuardのクライアント設定を利用してIPサブネットごとにグループ化します。IoT機器やスマートTVには厳格な広告・マルウェアブロックを適用し、開発用マシンはアグレッシブな分析ブロックから除外するといった柔軟な運用が可能です。
  • ハードウェア再起動対策: カーネル更新などでDockerホストが再起動すると、家全体のDNSが一時的に停止します。ルーターのDHCP設定でセカンダリDNSフォールバックIPを指定するか、2台目のRaspberry Piなどで冗長構成を組んでおくと、不意のネットワーク遮断を防げます。
Share: