ワイルドカードDNSを使ってワイルドな環境を構築しましょう!
この後説明するTraefikで使うため、簡易DNSをコンテナで設置します。
ホスト名でアクセス先を切り替えると覚えやすくポート番号がほかと被らずに運用できます。そのためにワイルドカードDNSが必要なのでDnsmasqを使います。
もし、コンテナからのアクセスが不要かつブラウザにChromeを使うなら、DNSを使わずとも"http://.localhost/"が使えます。
ここではコンテナ内からホスト経由でほかコンテナにアクセスする".dev.test"を名前解決してもらいます。もちろんホストからコンテナにもアクセスできます。
DnsmasqはDNSとDHCPを提供していますが、本書で使うのはDNSだけです。今回は触れていないDHCPも使うとコンテナ間通信が楽にできる可能性があります。
Netgearのルーター内部で動作しているという情報もあり、一般的なDNS/DHCPサーバーなんでしょう。
こちらのサイトを参考にさせていただきました。
本章はarkbig/devbaseリポジトリのdnsmasqフォルダの説明になります。
📂 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です。
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つのパートに分かれています。
- OSのパッケージマネージャーを使って必要なソフト
dnsmasq
をインストール - コマンド実行用のアカウント作成(コメントアウト)
- 実行するコマンドをコンテナへコピー&設定
2番目のアカウント作成はDnsmasqではコメントアウトしています。apt
でインストールするとdnsmasqユーザーが作られます。
3番目の環境変数はcompose.override.yamlで個人ごとに設定するため定義しています。
entrypoint.sh
次にコンテナ実行のヘルパースクリプト(ENTRYPOINT)です。
#!/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つのパートに分かれています。
- UID,GIDをホストに合わせる(コメントアウト)
- dnsmasqコマンドの引数設定
- コマンド実行
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経由だが、ここでは直接ポートバインドする)
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で指定します。
下記がデフォルト値での設定例です。
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_ADDR
とDNSMASQ_SERVER
です。
DNSMASQ_ADDR
はDNSMASQ_DOMAIN
から正引きするIPアドレスを指定します。これはコンテナからホストにアクセスできるものが望ましいです。
私は、Macだとホストマシンに固定IPアドレスを設定、WindowsではWSL2用のブリッジに固定IPアドレスを設定し、それぞれ指定します。
デフォルト値の192.168.100.100
はWindowsの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
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
を実行すると確認できます。
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アドレスを付与します。
@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アドレスが付与されます。内容は次のとおりです。
- WSL2上の「eth0」ネットワークに
%DNSMASQ_ADDR%
のIPアドレスを付与しています(デフォルトは192.168.100.100) - Windows上の「vEthenet (WSL)」ブリッジに
%WSL2_GATEWAY%
のIPアドレスを付与しています(デフォルトは192.168.100.1) - Windows上の「vEthenet (WSL)」ブリッジにDNSサーバーを2つ設定しています
- 優先DNSサーバーに「%DNSMASQ_ADDR%」(Dnsmasqへ通信できるIPアドレス)
- 代替DNSサーバーに「%DNSMASQ_SERVER%」これは、各自の環境で変わります。(デフォルトはパブリックDNSの1.1.1.1)
- Windows上の
ipconfig /all
で確認できる「DNSサーバー」のIPドレスです - 2つしか設定できないので、本来のセカンダリはここでは指定しません
- Windows上の
次にWindows側で使用するネットワークアダプターのDNS設定をします。上記バッチの「vEthenet (WSL)」にDNSサーバーを設定している部分と同じ方法でも可能ですが名前が各自違うのでGUI操作を示します。
- ネットワーク接続から使用しているアダプターのプロパティを開く
- インターネットプロトコルバージョン4(TCP/IPv4)のプロパティを設定
- 「次のDNSサーバーのアドレスを使う」で
- 優先DNSサーバーに「192.168.100.100」(上記%DNSMASQ_ADDR%)
- 代替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に自動生成しないように制限します。
[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を次のように書きます。
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
コマンドを使って確認してみます。
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を次のように書きます。
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で終了させましょう)
うまくいかなかったときの確認手順です。
-
docker compose logs dnsmasq
でDnsmasqコンテナがエラーを出していないか確認します - WSL2側で
nslookup a.dev.test 127.0.0.1
とローカルのDNSサーバーの使用を明示してみましょう- これで動かないなら、Dnsmasqコンテナの起動を疑います
- WSL2側でうまく動かないなら、/etc/resolv.confの設定を疑います
- Dnsmasqの動作確認をします
-
docker compose run --rm dnsmasq /bin/bash
で新しいコンテナを実行 -
sudo apt update; sudo apt install dnsutils
でnslookupコマンドをインストール -
nslookup a.dev.test devbase-dnsmasq-1
(コンテナ名はdevbaseフォルダで実行していないなら読み替える) - Serverが
$DNSMASQ_SERVER
で指定したIPアドレスならOK、表示されないならDnsmasqが正しく起動できていません
-
- WSL2側で
ping -c1 192.168.100.1
($WSL2_GATEWAYのIPアドレス)を実行- これがダメならWSL2への固定IPアドレス付与の処理で、「vEthenet (WSL)」ブリッジへのIPアドレス追加を疑います
- Windows側で
ping 192.168、100.100
($DNSMASQ_ADDRのIPアドレス)を実行- これがダメならWSL2への固定IPアドレス付与の処理で、WSL2側の「eth0」へのIPアドレス追加を疑います
- 最後にネットワークアダプターへのDNS登録を疑います
また、Dnsmasqを使わない名前解決も試してみてください。こちらが動かなかった場合はDNS登録の代替サーバー指定を疑います。
✔️本章の作業チェックリスト
- ✔️Dnsmasqを起動する
- DNSサーバーをOSへ登録
- 🍎Mac
- 🪟Windows
- 🐧Linux