Chapter 04無料公開

Dnsmasqの構築とOSへの登録

ArkBig
ArkBig
2022.05.14に更新

ワイルドカードDNSを使ってワイルドな環境を構築しましょう!

この後説明するTraefikで使うため、簡易DNSをコンテナで設置します。
ホスト名でアクセス先を切り替えると覚えやすくポート番号がほかと被らずに運用できます。そのためにワイルドカードDNSが必要なのでDnsmasqを使います。
もし、コンテナからのアクセスが不要かつブラウザにChromeを使うなら、DNSを使わずとも"http://.localhost/"が使えます。
ここではコンテナ内からホスト経由でほかコンテナにアクセスする"
.dev.test"を名前解決してもらいます。もちろんホストからコンテナにもアクセスできます。

https://thekelleys.org.uk/dnsmasq/doc.html

DnsmasqはDNSとDHCPを提供していますが、本書で使うのはDNSだけです。今回は触れていないDHCPも使うとコンテナ間通信が楽にできる可能性があります。
Netgearのルーター内部で動作しているという情報もあり、一般的なDNS/DHCPサーバーなんでしょう。

こちらのサイトを参考にさせていただきました。

https://nextat.co.jp/staff/archives/248

https://do-gugan.com/~furuta/archives/2017/05/macosresolver.html

本章はarkbig/devbaseリポジトリのdnsmasqフォルダの説明になります。

https://github.com/arkbig/devbase

本章で説明するファイル
📂 devbase/
├── 📄compose.override.yaml(必要なら作る)
├── 📄compose.yaml
├── 📂dnsmasq/
│   ├── 📄Dockerfile        # Dnsmasqを実行するコンテナ作成指示書
│   └── 📄entrypoint.sh     # コンテナ起動時に実行するヘルパースクリプト
└── 📂wsl2/
    ├── 📄wsl_assign_ip.bat  # WSL2に固定IPを付与します
    └── 📄:

🍎Macの関連図

このうち「socat_host」〜「socatコンテナ」までは次章のsocatを使用したUDPトンネルで説明します。

🪟Windowsの関連図

こちらはシンプルです。WSL2に固定IPを付与するのがポイントです。

Dockerfile

ではDnsmasqコンテナの説明に入ります。まずはコンテナのためのイメージを作るDockerfileです。

dnsmasq/Dockerfile ※Shift+マウスホイールで横スクロール
FROM ubuntu

#---------------(1)--------------#
RUN set -eux \
 && apt-get update -y \
 && apt-get install -y --no-install-recommends \
        dnsmasq \
 && apt-get -y clean \
 && rm -rf /var/lib/apt/lists/*

#---------------(2)--------------#
# 権限の問題があり、rootで実行するので省略
# ENV CONTAINER_UID=${CONTAINER_UID:-1000}
# ENV CONTAINER_GID=${CONTAINER_GID:-1000}
# RUN groupadd -g ${CONTAINER_GID} -o dnsstaff \
#  && useradd -g dnsstaff -m -o -u ${CONTAINER_UID} dnsstaff
# WORKDIR /home/dnsstaff/

#---------------(3)--------------#
COPY entrypoint.sh ./
RUN chmod +x ./entrypoint.sh

# デフォルトの起動引数
# 長くならないように省略形を使う
# -A, --address=/<domain>/<ipaddr>                       Return ipaddr for all hosts in specified domains.
# -h, --no-hosts                                         Do NOT load /etc/hosts file.
# -k, --keep-in-foreground                               Do NOT fork into the background, do NOT run in debug mode.
# -n, --no-poll                                          Do NOT poll /etc/resolv.conf file, reload only on SIGHUP.
# -R, --no-resolv                                        Do NOT read resolv.conf.
# -S, --server=/<domain>/<ipaddr>                        Specify address(es) of upstream servers with optional domains.
# -u, --user=<username>                                  Change to this user after startup. (defaults to nobody).
# -8, --log-facility=<facility>|<file>                   Log to this syslog facility or file. (defaults to DAEMON)
# これらの後に-Aと-Sを動的に追加する
ENV DNSMASQ_ARGS=${DNSMASQ_ARGS:-"-h -k -n -R -u root -8 -"}
# entrypoint.shで-A "/${DNSMASQ_DOMAIN}/${DNSMASQ_ADDR}"を追加する
# DNSMASQ_{DOMAIN,ADDR}_1〜DNSMASQ_{DOMAIN,ADDR}_Nまで連番で対応(抜け番があれば停止だが、"-"はスキップ)
ENV DNSMASQ_DOMAIN=${DNSMASQ_DOMAIN:-.test}
ENV DNSMASQ_ADDR=${DNSMASQ_ADDR:-192.168.100.100}
# entrypoint.shで-S "${DNSMASQ_SERVER}"を追加する
# DNSMASQ_SERVER_1〜DNSMASQ_SERVER_Nまで連番で対応(抜け番があれば停止だが、"-"はスキップ)
ENV DNSMASQ_SERVER=1.1.1.1

EXPOSE 53/tcp
EXPOSE 53/udp
ENTRYPOINT ["./entrypoint.sh"]
CMD ["dnsmasq"]

大きく3つのパートに分かれています。

  1. OSのパッケージマネージャーを使って必要なソフトdnsmasqをインストール
  2. コマンド実行用のアカウント作成(コメントアウト)
  3. 実行するコマンドをコンテナへコピー&設定

2番目のアカウント作成はDnsmasqではコメントアウトしています。aptでインストールするとdnsmasqユーザーが作られます。

3番目の環境変数はcompose.override.yamlで個人ごとに設定するため定義しています。

entrypoint.sh

次にコンテナ実行のヘルパースクリプト(ENTRYPOINT)です。

dnsmasq/entrypoint.sh ※Shift+マウスホイールで横スクロール
#!/usr/bin/env sh
#====================================================================
# begin of 定型文
# このスクリプトを厳格に実行
set -eu
# set -eux
# 環境に影響を受けないようにしておく
umask 0022
# PATH='/usr/bin:/bin'
IFS=$(printf ' \t\n_')
IFS=${IFS%_}
export IFS LC_ALL=C LANG=C PATH
# end of 定型文
#--------------------------------------------------------------------

#---------------(1)--------------#
# 権限の問題があり、rootで実行するので省略
# UID,GIDを合わせる
# uid=$(stat -c "%u" .)
# gid=$(stat -c "%g" .)
# ug_name=dnsstaff
# if [ "${CONTAINER_GID}" != "${gid}" ]; then
#     groupmod -g "${CONTAINER_GID}" -o "${ug_name}"
#     chgrp -R "${CONTAINER_GID}" .
# fi
# if [ "${CONTAINER_UID}" != "${uid}" ]; then
#     usermod -g "${ug_name}" -o -u "${CONTAINER_UID}" "${ug_name}"
# fi

#---------------(2)--------------#
# dnsmasqコマンドなら引数を追加する
if [ "${1}" = "dnsmasq" ]; then
    # オプションを付与したいのでコマンドは消しておく
    shift
    command_is_dnsmasq=true
elif [ "${1}" = "${1#-}" ]; then
    command_is_dnsmasq=false
else
    command_is_dnsmasq=true
fi
if ${command_is_dnsmasq}; then
    skip_address=false
    if [ -z "${DNSMASQ_DOMAIN}" ]; then
        echo "DNSMASQ_DOMAIN environment variable is empty so address not added."
        skip_address=true
    fi
    if [ -z "${DNSMASQ_ADDR}" ]; then
        echo "DNSMASQ_ADDR environment variable is empty so address not added."
        skip_address=true
    fi
    if ! ${skip_address}; then
        address_args="-A /${DNSMASQ_DOMAIN}/${DNSMASQ_ADDR}"
        sequence_number=1
        while true; do
            sequence_domain=$(eval "printf $(printf "%s" "\${DNSMASQ_DOMAIN_${sequence_number}-\"\"}")")
            sequence_addr=$(eval "printf $(printf "%s" "\${DNSMASQ_ADDR_${sequence_number}-\"\"}")")
            if [ -z "${sequence_domain}" ] || [ -z "${sequence_addr}" ]; then
                # 未定義なので終了
                break
            fi
            if [ "${sequence_domain}" = "-" ] || [ "${sequence_addr}" = "-" ]; then
                echo "skip sequence number ${sequence_number} because DNSMASQ_DOMAIN_${sequence_number} or DNSMASQ_ADDR_${sequence_number} is '-'."
            else
                address_args="${address_args} -A /${sequence_domain}/${sequence_addr}"
            fi
            sequence_number=$((sequence_number + 1))
        done
        DNSMASQ_ARGS="${DNSMASQ_ARGS} ${address_args}"
    fi
    skip_server=false
    if [ -z "${DNSMASQ_SERVER}" ]; then
        echo "DNSMASQ_SERVER environment variable is empty so server not added."
        skip_server=true
    fi
    if ! ${skip_server}; then
        server_args="-S ${DNSMASQ_SERVER}"
        sequence_number=1
        while true; do
            sequence_server=$(eval "printf $(printf "%s" "\${DNSMASQ_SERVER_${sequence_number}-\"\"}")")
            if [ -z "${sequence_server}" ]; then
                # 未定義なので終了
                break
            fi
            if [ "${sequence_server}" = "-" ]; then
                echo "skip sequence number ${sequence_number} because DNSMASQ_SERVER_${sequence_number} is '-'."
            else
                server_args="${server_args} -S ${sequence_server}"
            fi
            sequence_number=$((sequence_number + 1))
        done
        DNSMASQ_ARGS="${DNSMASQ_ARGS} ${server_args}"
    fi
    # shellcheck disable=SC2086
    set -- dnsmasq ${DNSMASQ_ARGS} "$@"
    echo "$@"
fi

#---------------(3)--------------#
# 権限の問題があり、rootで実行するので省略
# if [ "$(id -u)" = "${CONTAINER_UID}" ]; then
#     exec "$@"
# else
#     # ユーザー変更してコマンド実行
#     exec /usr/sbin/gosu "${ug_name}" "$@"
# fi
exec "$@"

大きく3つのパートに分かれています。

  1. UID,GIDをホストに合わせる(コメントアウト)
  2. dnsmasqコマンドの引数設定
  3. コマンド実行

1番目のUID,GID設定ですが、不要なのでコメントアウトしています。

2番目のdnsmasqコマンドの引数設定がここでのメイン処理です。
実行するコマンドがdnsmasqの場合、各種設定をコマンド引数で行っています。DNSMASQ_{DOMAIN,ADDR}DNSMASQ_SERVER_1_2と連番で複数指定できるようにしています。
この複数指定がなければシンプルに$DNSMASQ_ARGS -A /$DNSMASQ_DOMAIN/$DNSMASQ_ADDR -S $DNSMASQ_SERVERとつなげているだけです。
Dnsmasqは/etc/dnsmasq.confで設定可能ですが、コンテナ起動時に変更できるようすべてコマンドライン引数としています。

3番目のコマンド実行ですが、権限の問題があるためrootのまま実行しています。
dnsmasqの参照先をいい感じに設定すればdnsmasqユーザーで実行も可能ですが深くみていません。

compose.yaml(抜粋)

そして、dockerで使用するcomposeファイルです。(最終形はTraefik経由だが、ここでは直接ポートバインドする)

compose.yaml
services:
  dnsmasq:
    image: arkbig/dnsmasq
    init: true
    build:
      context: ./dnsmasq
      args:
        no_proxy: ${no_proxy-}
        http_proxy: ${http_proxy-}
        https_proxy: ${https_proxy-}
    restart: unless-stopped
    ports:
      - 53:53
      - 53:53/udp

init: trueを指定しコンテナ内でinitプロセス(PID 1)を実行しています。これによりサービス起動しているdnsmasqがシグナルを正しく受け取れるようになりコンテナの停止が正しくできます。
もし指定しなかった場合、compose downで終了するとき10秒のタイムアウト待ち後に強制終了されます。

また、必要に応じてenvironment:で動作を変更できます。個人環境の設定ですので、Git管理外のcompose.override.yamlで指定します。
下記がデフォルト値での設定例です。

compose.override.yaml
services:
  dnsmasq:
    environment:
      # Dnsmasqの起動引数
      # これらの後に-Aと-Sを動的に追加する
      DNSMASQ_ARGS: -h -k -n -R -u root -8 -
      # メインの変換ドメイン
      DNSMASQ_DOMAIN: .test
      # メインの変換IPアドレス(コンテナから使用するため、ホストIPアドレスにすべき)
      DNSMASQ_ADDR: 192.168.100.100
      # 以降も連番で指定可能
      # 空文字れつもしくは未定義に遭遇するとそこで終了
      # "-"ハイフンだけならその番号はスキップして、次の番号を処理
      DNSMASQ_DOMAIN_1:
      DNSMASQ_ADDR_1:
      # 通常使うDNSサーバー
      DNSMASQ_SERVER: 1.1.1.1
      # 以降も連番で指定可能
      # 空文字れつもしくは未定義に遭遇するとそこで終了
      # "-"ハイフンだけならその番号はスキップして、次の番号を処理
      DNSMASQ_SERVER_1:

特に重要なのが、DNSMASQ_ADDRDNSMASQ_SERVERです。

DNSMASQ_ADDRDNSMASQ_DOMAINから正引きするIPアドレスを指定します。これはコンテナからホストにアクセスできるものが望ましいです。
私は、Macだとホストマシンに固定IPアドレスを設定、WindowsではWSL2用のブリッジに固定IPアドレスを設定し、それぞれ指定します。
デフォルト値の192.168.100.100WindowsのDNSサーバー設定で付与するIPアドレスです。

DNSMASQ_SERVERはdnsmasqがDNSMASQ_DOMAIN以外を名前解決するために問い合わせるDNSサーバーです。通常はホストに設定しているものを指定します。
なおMacではDNSMASQ_DOMAIN以外は最初からホストで名前解決するので使用されません。

Dnsmasqを起動する

これまでに説明したファイルたちを利用してDnsmasqを起動するには次のコマンドを実行します。

docker compose up -d dnsmasq

DNSサーバーをOSへ登録

Dnsmasqを起動したので、DNSサーバーとして利用するようOSに登録しましょう。

🍎MacのDNSサーバー設定

【クリックで展開】Macの/etc/resolver/への登録手順。

Macの場合は/etc/resolver/でドメインごとにDNSサーバーを設定できます。ここでは「test」ドメインを登録します。(ドメインがファイル名になる)

sudo mkdir /etc/resolver
sudo vi /etc/resolver/test
/etc/resolver/test
options timeout:1
options attempts:2
options use-vc
nameserver 127.0.0.1

optionsで次の指定をします。

option 説明
timeout 名前解決1回あたりのタイムアウト時間
attempts 名前解決の試行回数
use-vc UDPでなくTCPを使った通信(効いてなさそう)

optionsの指定はMan pageに書いています。

use-vcが効いていればUDPは使わずにTCPで通信してくれそうなんですが、ダメでした。
TCPにしたかったのはLimaがUDPトンネルに対応していないためです。これはsudo lsof -i:53を実行すると確認できます。

sudo lsof -i:53
COMMAND PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     719  big   18u  IPv4 0x764ac823d019058d      0t0  TCP *:domain (LISTEN)

このようにsshコマンドがNODEのTCPを使用しているのがわかりますが、UDPがありません。sshなのはLimaがホストとVM間をsshトンネルで通信してくれているためです。
しかたないので、UDPは自前でトンネルします。それが次の章 socatを使用したUDPトンネルです。

🪟WindowsのDNSサーバー設定

【クリックで展開】WSL2に固定IPアドレスを付与して、ネットワークアダプターへ登録する手順。

Windowsの場合はネットワークアダプターでDNSサーバーを指定します。Macと違ってドメインごとの設定はできないようです。
またWindowsからWSL2へ通信するためのブリッジがありますが、IPアドレスが固定ではないようです。そのため、先にWSL2へ固定IPアドレスを付与します。

wsl2/wsl_assign_ip.bat ※Shift+マウスホイールで横スクロール
@echo off
setlocal
call "%~dp0wsl_env.bat"

@echo on
rem ---------------(1)--------------#
wsl -u root ip addr change %DNSMASQ_ADDR%%WSL2_ADDR_SUBNET% broadcast %WSL2_BROADCAST% dev eth0 label eth0:100
@if errorlevel 1 pause

rem ---------------(2)--------------#
netsh interface ip add address "vEthernet (WSL)" %WSL2_GATEWAY% %WSL2_GATEWAY_SUBNET%
@if errorlevel 1 netsh interface ip show address "vEthernet (WSL)" | find "%WSL2_GATEWAY%"
@if errorlevel 1 pause

rem ---------------(3)--------------#
netsh interface ipv4 set dns "vEthernet (WSL)" static %DNSMASQ_ADDR% none no
@if errorlevel 1 pause
netsh interface ipv4 add dns "vEthernet (WSL)" %DNSMASQ_SERVER% 2 no
@if errorlevel 1 pause

このバッチを実行するとWSL2に固定IPアドレスが付与されます。内容は次のとおりです。

  1. WSL2上の「eth0」ネットワークに%DNSMASQ_ADDR%のIPアドレスを付与しています(デフォルトは192.168.100.100)
  2. Windows上の「vEthenet (WSL)」ブリッジに%WSL2_GATEWAY%のIPアドレスを付与しています(デフォルトは192.168.100.1)
  3. Windows上の「vEthenet (WSL)」ブリッジにDNSサーバーを2つ設定しています
    1. 優先DNSサーバーに「%DNSMASQ_ADDR%」(Dnsmasqへ通信できるIPアドレス)
    2. 代替DNSサーバーに「%DNSMASQ_SERVER%」これは、各自の環境で変わります。(デフォルトはパブリックDNSの1.1.1.1)
      • Windows上のipconfig /allで確認できる「DNSサーバー」のIPドレスです
      • 2つしか設定できないので、本来のセカンダリはここでは指定しません

次にWindows側で使用するネットワークアダプターのDNS設定をします。上記バッチの「vEthenet (WSL)」にDNSサーバーを設定している部分と同じ方法でも可能ですが名前が各自違うのでGUI操作を示します。

  1. ネットワーク接続から使用しているアダプターのプロパティを開く
  2. インターネットプロトコルバージョン4(TCP/IPv4)のプロパティを設定
  3. 「次のDNSサーバーのアドレスを使う」で
    1. 優先DNSサーバーに「192.168.100.100」(上記%DNSMASQ_ADDR%)
    2. 代替DNSサーバーに「%DNSMASQ_SERVER%」これは自分のDNSサーバー(どこでもいいならパブリックDNSの1.1.1.1や8.8.8.8とか)

ネットワーク接続

これで、名前解決にDnsmasqが使われます。もしDnsmasqが起動していなければ、代替DNSサーバーに問い合わせをします。

そしてWSL2側のDNSサーバーの設定もしましょう。WSL2上では/etc/resolv.confを利用しますが、これは自動生成されるものです。
まずは/etc/wsl.confに自動生成しないように制限します。

/etc/wsl.conf
[network]
generateResolvConf = false

次に/etc/resolv.confを書き換えますが、まずは自動生成されたファイルを削除してリンクを解除します。

sudo cp /etc/resolv.conf /etc/resolv.conf.bak
sudo rm /etc/resolv.conf
sudo mv /etc/resolv.conf.bak /etc/resolv.conf

そして/etc/resolv.confを次のように書きます。

/etc/resolv.conf
options timeout:1
options attempts:2
nameserver 127.0.0.1
# 以下は自分の環境に合わせて書き換える
nameserver 1.1.1.1

optionsで次の指定をします。

option 説明
timeout 名前解決1回あたりのタイムアウト時間
attempts 名前解決の試行回数

optionsの指定はMan pageに書いています。

optionsで変更しなければnameserverの上から順番に名前解決を試みます。
そのため、1つ目に127.0.0.1(VMローカル)でDnsmasqに問い合わせします。2つ目、3つ目は通常使うDNSサーバー(%DNSMASQ_SERVER%、%DNSMASQ_SERVER_1%)を登録します。

あとここでは関係ないですが、ついでに書くとsearch your.company.comのように指定すると、ドメイン未指定時に自動付与されるドメインを設定できます。ホストPCに合わせておくと便利です。
options ndots:Nでserch listの使用閾値を変えられます)

これでDNS設定は完了です。

53番ポートがどうなっているかWSL2上でsudo lsof -i:53コマンドを使って確認してみます。

sudo lsof -i:53
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
docker-pr 3076 root    4u  IPv4  22232      0t0  TCP *:domain (LISTEN)
docker-pr 3098 root    4u  IPv4  23870      0t0  UDP *:domain
#(IPv6は省略)

このようにdocker-pr(フルネームはdocker-proxy)コマンドがNODEのTCPとUDPを使用しているのがわかります。

🐧LinuxのDNSサーバー設定

【クリックで展開】Linuxの/etc/resolv.confへ登録手順。

動作確認したWSL2上のUbuntuの例です。

/etc/resolv.confを次のように書きます。

/etc/resolv.conf
options timeout:1
options attempts:2
nameserver 127.0.0.1
# 以下は自分の環境に合わせて書き換える(.envの$DNSMASQ_SERVER)
nameserver 1.1.1.1

optionsで次の指定をします。

option 説明
timeout 名前解決1回あたりのタイムアウト時間
attempts 名前解決の試行回数

optionsの指定はMan pageに書いています。

Windowsでの動作確認

MacのClima環境などudptunnelが必要なら次の章socatを使用したUDPトンネルの対応が必要です。
LinuxならWSL2側相当の処理になります。
実際に名前解決できているか確認してみましょう。下記コマンドをコマンドプロンプトとWSL2の両方で実行してみます。

nslookup a.dev.test

ping a.dev.test

nslookupはServerが$DNSMASQ_SERVERで指定したIPアドレスが表示されていれば成功です。サーバー自体はないのでSERVFAILエラーになります。

ping$DNSMASQ_SERVERで指定したIPアドレスに通信していたらOKです。(Linux側で実行した場合はCtrl-Cで終了させましょう)

うまくいかなかったときの確認手順です。

  1. docker compose logs dnsmasqでDnsmasqコンテナがエラーを出していないか確認します
  2. WSL2側でnslookup a.dev.test 127.0.0.1とローカルのDNSサーバーの使用を明示してみましょう
    • これで動かないなら、Dnsmasqコンテナの起動を疑います
  3. WSL2側でうまく動かないなら、/etc/resolv.confの設定を疑います
  4. Dnsmasqの動作確認をします
    1. docker compose run --rm dnsmasq /bin/bashで新しいコンテナを実行
    2. sudo apt update; sudo apt install dnsutilsでnslookupコマンドをインストール
    3. nslookup a.dev.test devbase-dnsmasq-1(コンテナ名はdevbaseフォルダで実行していないなら読み替える)
    4. Serverが$DNSMASQ_SERVERで指定したIPアドレスならOK、表示されないならDnsmasqが正しく起動できていません
  5. WSL2側でping -c1 192.168.100.1($WSL2_GATEWAYのIPアドレス)を実行
    • これがダメならWSL2への固定IPアドレス付与の処理で、「vEthenet (WSL)」ブリッジへのIPアドレス追加を疑います
  6. Windows側でping 192.168、100.100($DNSMASQ_ADDRのIPアドレス)を実行
    • これがダメならWSL2への固定IPアドレス付与の処理で、WSL2側の「eth0」へのIPアドレス追加を疑います
  7. 最後にネットワークアダプターへのDNS登録を疑います

また、Dnsmasqを使わない名前解決も試してみてください。こちらが動かなかった場合はDNS登録の代替サーバー指定を疑います。

✔️本章の作業チェックリスト