Docker+DNS入門　その2：Unboundを用いたキャッシュDNSサーバ構築

本記事は[Docker+DNS入門　その1：CoreDNSを用いた権威DNSサーバ構築](https://qiita.com/miwamoto/items/2a8befac7135bd21e08a)の続きの記事になります．

 https://qiita.com/miwamoto/items/2a8befac7135bd21e08a

今回はローカルのdocker環境にUnboundによるキャッシュDNSサーバを構築し，前回の記事で構築した2つの権威DNSサーバへの反復問い合わせを実現します．

## 対象者
- DNSの基礎的な知識はあるが，実装したことがない方

## 実行環境
- Windows10
- WSL2 (Ubuntu 22.04)
- Docker v2.21.0(注: Docker desktop ではありません)
    - 環境構築は[WindowsでもサクサクDocker (Docker on WSL2 without Docker Desktop)](https://zenn.dev/rhene/articles/docker-on-wsl2-without-docker-desktop)が参考になります．
- Unbound 1.13.1
- CoreDNS 1.11.1

## システムの全体像
Docker環境に3つのDNSサーバを構築します．
2つの権威DNSサーバがそれぞれ管理するドメインに対して，自前のキャッシュDNSサーバが名前解決を行います．
権威DNSサーバ1は`example.org`のドメインを管理し，そのサブドメイン`"home".example.org`の管理を権威DNSサーバ2が委譲します．
前回の記事 [Docker+DNS入門　その1：CoreDNSを用いた権威DNSサーバ構築](https://qiita.com/miwamoto/items/2a8befac7135bd21e08a)でCoreDNSを用いて権威DNSサーバを構築しました．
今回はキャッシュDNSサーバを構築します．

![](https://i.imgur.com/UBJinaD.png)
## 用意するファイル
本記事では[前回の記事](https://qiita.com/miwamoto/items/2a8befac7135bd21e08a)で用意したファイルに，`full_resolver`配下を追加します．
```
$ tree -a dns_test/
dns_test/
├── compose.yml
├── .env
├── example_auth
│   ├── Corefile
│   └── zone
│       └── db.example.org
├── home_example_auth
│   ├── Corefile
│   └── zone
│       └── db.home.example.org
└── full_resolver
    ├── Dockerfile
    └── server.conf
```

## Unboundの設定
1. Dockerfileを作成
    ```Dockerfile: ./full_resolver/Dockerfile

    FROM ubuntu:22.04

    # Install unbound
    RUN set -x \
       && apt-get update && apt-get install -y --no-install-recommends \
       unbound=1.13.1-1ubuntu5.3 \
       curl \
       ca-certificates \
       && apt-get clean \
       && rm -r /var/lib/apt/lists/*

    RUN curl -o /etc/unbound/root.hints -LO https://www.internic.net/domain/named.cache
    RUN chown -R unbound:unbound /etc/unbound
    
    # unbound-anchor: DNSSEC検証のためのルートトラストアンカーの設置と更新
    RUN unbound-anchor -4 -a "/var/lib/unbound/root.key" ; ls -al /var/lib/unbound/root.key

    # unbound-checkconf: unboundの設定のエラーを検査
    RUN unbound-checkconf
    ```
    - `set -x`によってその後に実行したコマンド自体を標準出力します．
        - これによって，`&&`で複数コマンドをまとめて実行したときの状況が見やすくなります．
    - `RUN curl ~`によってルートヒント (ルートDNSサーバのIPアドレスが記載されているファイル) をダウンロードし，docker内に配置します．
    - `RUN chown -R unbound:unbound /etc/unbound`によって，所有者をunboundに変更し，ルートヒントを読み込めるようにします．
    - `unbound-anchor`によってDNSSEC検証に必要なルートトラストアンカー (root.key) を取得します．
        - 本記事ではDNSSECを利用しないため，機能的には不要ですが，`unbound-checkconf`でroot.keyがないと怒られるので取得しておきます．(DNSSECは次回の記事で実装します．)
        - オプション
            - `-4`: トラストアンカーを提供するサーバにアクセスする際に，ipv4を利用します．dockerはデフォルトだとipv6通信ができないため．
            - `-a "/var/lib/unbound/root.key"`: root.keyを生成するパスを指定します．
    - `unbound-checkconf`によって，unboundの設定に誤りがないか検査します．
    #### unbound-anchorの注意点
    ただし，このままでは`unbound-anchor`によってroot.keyが生成されているにも関わらず，Dockerfile上ではエラー扱いになります．(Unbound 1.16.2)
    そこで，`unbound-anchor`の後に`; ls -al /var/lib/unbound/root.key`を加えます．
    これにより，`unbound-anchor`の実行結果にかかわらずlsコマンドを呼び出し，`root.key`が存在すれば正常とみなして，Dockerfileをエラー終了させないようにします．

1. unboundの設定ファイル (server.conf) を作成
    - 設定ファイルの書き方は[unbound.conf(5) – 日本Unboundユーザー会](https://unbound.jp/unbound/unbound-conf/)が参考になります．
    ```conf: ./full_resolver/server.conf
    server:
        verbosity: 4
        do-daemonize: no
        do-ip6: no
        do-ip4: yes
        root-hints: root.hints

        # module-config: "validator" or "iterator"
        module-config: "iterator" 

        # trust-anchor
        auto-trust-anchor-file: "/var/lib/unbound/root.key"

        # access-control
        access-control: 0.0.0.0/0 refuse
        access-control: 127.0.0.0/8 allow_snoop
        access-control: 172.19.0.0/16 allow_snoop

        # interface
        interface: 0.0.0.0

        # stub-zone
        stub-zone:
            name: "example.org"
            stub-addr: 172.19.0.21
        stub-zone:
            name: "0.19.172.in-addr.arpa."
            stub-addr: 172.19.0.21
    ```
    - `root.hints:`でDockerfileでダウンロードしたルートヒントを指定します．
    - `module-config:`で"iterator"を指定することで，DNSSECを無効にします．
    - `auto-trust-anchor-file:`でDockerfileでダウンロードしたトラストアンカーを指定します．
    - `access-contorl:`でこのサーバにアクセス可能なクライアントのIPアドレスの範囲(ここではプライベートIP)を指定します．
    - `interface:`でunboundがリクエストを受け付ける(リッスンする)インターフェースのIPアドレスを指定します．
        - デフォルトではlocalhost (172.0.0.1) ですが，外部と通信するので，任意のインターフェース (`0.0.0.0`) で受け付けます．
    - `stub-zone:`によって，あるゾーンに対応する権威DNSサーバを設定します．
        - ゾーン名を`name:`，その権威DNSサーバのIPアドレスを`stub-addr:`で指定します．

## dockerの設定
- compose.ymlを作成
    - [前回の記事](https://qiita.com/miwamoto/items/2a8befac7135bd21e08a)から`full_resolver:`の部分を追加しています．
    - これを実行することで，[前回の記事](https://qiita.com/miwamoto/items/2a8befac7135bd21e08a)で構築した2つの権威DNSサーバと，今回構築するキャッシュDNSサーバが起動します．
    ```yml:./compose.yml
    services:
        example_auth:
            image: coredns/coredns:1.11.1
            container_name: example_auth
            restart: on-failure
            volumes:
                - './example_auth:/etc/coredns'
            networks:
                dns_test:
                    ipv4_address: $ipv4_example_auth
            command: -conf /etc/coredns/Corefile -dns.port $dns_port
        home_example_auth:
            image: coredns/coredns:1.11.1
            container_name: home_example_auth
            restart: on-failure
            volumes:
                - './home_example_auth:/etc/coredns'
            networks:
                dns_test:
                    ipv4_address: $ipv4_home_example_auth
            command: -conf /etc/coredns/Corefile -dns.port $dns_port
        full_resolver:
            container_name: full_resolver
            build:
                context: './full_resolver'
                dockerfile: Dockerfile
            restart: on-failure
            volumes:
                - '/etc/unbound'
                - './full_resolver:/etc/unbound/unbound.conf.d'
            networks:
                dns_test:
                    ipv4_address: $ipv4_full_resolver
            command: /usr/sbin/unbound
    networks:
        dns_test:
            external: true
    ```
- compose.yml内の変数は`./.env`に記述
    ```conf: ./.env
    dns_port=53
    ipv4_full_resolver=172.19.0.11
    ipv4_example_auth=172.19.0.21
    ipv4_home_example_auth=172.19.0.31
    ```

## 動作確認
### 権威DNSサーバの起動
```compose.yml```があるディレクトリで```docker compose up```を実行します．
```:Ubuntu-22.04 (WSL2)
~/dns_test$ docker compose up
[+] Running 3/0
 ✔ Container home_example_auth  Cre...                               0.0s 
 ✔ Container full_resolver      Created                              0.0s 
 ✔ Container example_auth       Created                              0.0s 
Attaching to example_auth, full_resolver, home_example_auth
full_resolver      | [1704280672] unbound[1:0] debug: creating udp4 socket 0.0.0.0 53
full_resolver      | [1704280672] unbound[1:0] debug: creating tcp4 socket 0.0.0.0 53
full_resolver      | [1704280672] unbound[1:0] debug: creating tcp4 socket 127.0.0.1 8953
full_resolver      | [1704280672] unbound[1:0] debug: setup SSL certificates
full_resolver      | [1704280672] unbound[1:0] debug: switching log to syslog
example_auth       | example.org.:53
example_auth       | [INFO] plugin/reload: Running configuration SHA512 = ~~~
example_auth       | CoreDNS-1.11.1
example_auth       | linux/amd64, go1.20.7, ae2bbc2
home_example_auth  | home.example.org.:53
home_example_auth  | [INFO] plugin/reload: Running configuration SHA512 = ~~~
home_example_auth  | CoreDNS-1.11.1
home_example_auth  | linux/amd64, go1.20.7, ae2bbc2
```

### キャッシュDNSサーバによる名前解決
- 構築したキャッシュDNSサーバに対して，WSL2から再帰問い合わせを行います．
- まず権威DNSサーバ1 (example.orgサーバ) の内容を問い合わせ，名前解決できていることを確認します．
    権威DNSサーバの内容は[前回の記事](https://qiita.com/miwamoto/items/2a8befac7135bd21e08a)と同じです．
    ```:Ubuntu-22.04 (WSL2)
    $ dig +nocookie @172.19.0.11 ns1.example.org
    
    ; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> +nocookie @172.19.0.11 ns1.example.org
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34321
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
    
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 1232
    ;; QUESTION SECTION:
    ;ns1.example.org.               IN      A
    
    ;; ANSWER SECTION:
    ns1.example.org.        86400   IN      A       172.19.0.21
    
    ;; Query time: 0 msec
    ;; SERVER: 172.19.0.11#53(172.19.0.11) (UDP)
    ;; WHEN: ~~~
    ;; MSG SIZE  rcvd: 60
    ```
- 同様に権威DNSサーバ2 (home.example.orgサーバ) の内容を問い合わせ，名前解決できていることを確認します．
    ```:Ubuntu-22.04 (WSL2)
    $ dig +nocookie @172.19.0.11 www.home.example.org

    ; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> +nocookie @172.19.0.11 www.home.example.org
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18139
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
    
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 1232
    ;; QUESTION SECTION:
    ;www.home.example.org.          IN      A
    
    ;; ANSWER SECTION:
    www.home.example.org.   86400   IN      A       172.19.0.33
    
    ;; Query time: 0 msec
    ;; SERVER: 172.19.0.11#53(172.19.0.11) (UDP)
    ;; WHEN: ~~~
    ;; MSG SIZE  rcvd: 65
    ```
    この結果から，再帰問い合わせが機能していることが分かります．
    Unboundの設定(server.conf)で，example.orgドメインのstub-zoneを権威DNSサーバ1としているため，`www.home.example.org`の問い合わせはまず権威DNSサーバ1へ送られます．    そのあと，権威DNSサーバ1から権威DNSサーバ2への委譲され，名前解決されています．

## 次回の話
これまでの権威DNSサーバ，キャッシュDNSサーバの構築では，簡単のためDNSSEC機能を無効化していました．
しかし，実際にはDNSSEC機能を無効にすると，キャッシュDNSサーバの問い合わせに対して，偽装された回答が送られてもそれを受け入れてしまいます (DNSキャッシュポイズニング)．
次回の記事では，これまで構築してきたDNSサーバにDNSSEC機能を実装します．