Docker on WSL2 環境構築メモ (3/x) - ネットワーク設定

前提

導入対象とするマシンは下記の通り。

  • Hardware : CPU = i10900, GPU = GeForce RTX 2080Ti, Mem = 32GB, SSD 1 TB + HDD 2TB
  • ベース OS : Windows 10 Pro バージョン 20H2 (ビルド 19042.685)
  • 仮想 OS : Ubuntu 20.04 LTS on WSL2

ネットワーク設定の目的

Docker コンテナで何らかのサーバサービスを立ち上げる場合、Docker コンテナの外部からアクセスできるようにする必要がある。Docker on Ubuntu on WSL2 場合の外部とは

  1. Docker コンテナ外・ゲスト OS (Ubuntu 仮想端末) 内
  2. ゲスト OS (Ubuntu 仮想端末) 外・ホスト OS (Windows 物理端末) 内
  3. ホスト OS (Windows 物理端末) 外

の3種類ある。1つめの Docker コンテナ ⇔ Ubuntu 仮想端末の通信は Docker 構築の定番テーマである Port Relay (ubuntu -p オプション) の話。3つめの Windows 物理端末外から Ubuntu 仮想端末間の通信は OS 仮想化で定番テーマである Port Forwarding の話。

個人で環境構築している際には外部公開するほどのことはやらないし、かといって Ubuntu 仮想端末内で作業が完了するわけでもない。コンサーンとなるのは2つめの同一端末内の Windows 物理端末 (内) と Ubuntu 仮想端末の間でのみ相互通信したいということになる。

ここまでの構築メモの手順を踏むだけでも Windows 物理端末 (内) と Ubuntu 仮想端末の間の通信が阻まれることはないのだが、後述のとおり仮想端末起動ごとに仮想端末 IP アドレスが揺らぐため、名前解決が面倒くさい。単純に思いつく解決法は hosts ファイルを書き換えることだが、試行錯誤した結果、hosts を動的に書き換えるとセキュリティ・リスクを高め、運用しにくいことから断念することにした *1

結論として、スコープを小さく限定して利用する場合でも Port Forwarding が最も手っ取り早い。Windows 物理端末外からのアクセスまで考慮するか否かはホスト OS (Windows 物理端末) でパケットフィルタリング (Windows Defender) を使うかどうかの違いでしかない。

ネットワーク設定の手順

仮想 OS の IP アドレス

いろいろと調べたところ、ゲスト OS 側 Ubuntu 仮想端末としての IP アドレスはUbuntu 20.04 LTS on WSL2 環境構築メモ (2/4) - GUI 導入で調べたネットワークアダプタ関連情報には登場せず、Ubuntu 側からip a show dev eth0 とやると取得できるらしい。そこで IPv4 アドレスを下記のようにして取得する。

ip a show dev eth0 | awk '$1 == "inet" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ { sub(/\/[0-9]+$/, "", $2); print $2 }'

仮想端末の IP アドレスはよく調べたくなるため、whereami で調べられるように .bashrcエイリアスを追記しておく。bash ではクォーテーションのエスケープが少し特殊であることに注意。'\''エスケープされた ' だと思えばよい。

alias whereami='ip a show dev eth0 | awk '\''$1 == "inet" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ { sub(/\/[0-9]+$/, "", $2); print $2 }'\'''

Port Forwarding 設定の前提

Docker で apache web サービスのコンテナを port 80 → 8080 で Port Relay しつつ走らせており、これにホスト OS (Windows) のブラウザから接続するというシナリオを前提とする。実環境で Port を変える必要はないのだが、サーバサービスを Docker で本番/検証/開発環境並行運用するような場面を想定した仮想実験。構成図 (最終形) はこちら

docker run -p 8080:80 -d httpd

Port Forwarding 設定

WSL2 で実行するシェルスクリプトの作成

まず、Ubuntu 仮想端末において管理者権限で port-forward.sh というファイルを作成する。

sudo touch /opt/port-forward.sh

その中身の記述は次のとおり。xx.xx.xx.xxWindows 物理端末の実 IP アドレス。

#!/bin/bash

# obtain ipv4 on virtual os
IP=$(ip a show dev eth0 | awk '$1 == "inet" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ { sub(/\/[0-9]+$/, "", $2); print $2 }')

# reset and set port forwarding configuration on wsl2
netsh.exe interface portproxy delete v4tov4 listenport=80 listenaddress=localhost
netsh.exe interface portproxy delete v4tov4 listenport=80 listenaddress=xx.xx.xx.xx
netsh.exe interface portproxy add    v4tov4 listenport=80 listenaddress=localhost   connectport=8080 connectaddress=$IP
netsh.exe interface portproxy add    v4tov4 listenport=80 listenaddress=xx.xx.xx.xx connectport=8080 connectaddress=$IP
シェルスクリプトをキックするバッチの作成

Windows 物理端末において %UserProfile%\ あたりに port-forward.batというバッチファイルを作成する。その中身の記述は次のとおり。wsl -u オプションで root 指定必須。wsl -d オプションは仮想 OS ディストリビューションの指定。事前に wsl --list --verbose で正式な識別名を調べておく。

@echo off
wsl -d Ubuntu-20.04 -u root --exec /bin/bash /opt/port-forward.sh

rem コマンドラインから実行した場合に、設定状況を一覧表示する
netsh interface portproxy show v4tov4

このファイルは管理者権限で実行する必要があるため、ショートカットを作成し、その詳細プロパティで「管理者として実行」にチェックを入れる。

実行

このショートカットを実行しておけば、Port Forwarding は有効になっている。 (Ubuntu 仮想 OS をユーザが起動しなくても) WSL2 は Windows 起動時からバックグラウンドで動いているため、Windows 起動時に自動キックとなるようタスクスケジューラに仕込んでおけばよい。

Windows 物理端末のブラウザから http://localhost/ (Port 80) 指定で Docker コンテナの httpd (Port 8080) へアクセスできる。
Ubuntu 仮想端末のブラウザからは http://localhost:8080/ (Port 8080) 指定でなければアクセスできない。仮想端末内では Port Forwarding 設定 (Windows Port 80 → Ubuntu Port 8080) が効いておらず、一方で Port Relay 設定 (Ubuntu Port 8080 → Docker Port 80) が効いているため。

Windows 物理端末外からのアクセス許可

では Windows 物理端末外のブラウザから http://xx.xx.xx.xx/ 物理端末 IP & Port 80 指定でアクセスできるか、というとできない。これは Windows Defender のパケットフィルタリングに阻まれるためである。そこでローカルネットワークや VPN のセグメントからのみ、http (port 80 TCP) アクセスを受け入れるルールを Windows Defender に設定する。

http (port 80 TCP) inbound をローカルネットワークや VPN に許可するルールの作成方法
  1. "Windows Defender ファイアーウォール" を開く
  2. "詳細設定" を押す
  3. "受信の規則" を右クリックし、"新しい規則" を選択する
  4. "規則の種類" で "ポート" を選択し、"次へ" を押す
  5. TCP/UDP は "TCP" を選択、ローカルポートは "特定のローカルポート" を選択、ポート番号に "80" を指定し、"次へ" を押す
  6. "接続を許可する" を選択し、"次へ" を押す
  7. "ドメイン" と "プライベート" を選択し、"次へ" を押す
  8. 受信規則の名前を付け、"完了" を押す
  9. "受信の規則" からいま設定したルールを選択、右クリックでプロパティを選択、 ”スコープ" タブを開く
  10. リモート IP アドレス (Windows 物理端末外側) で "これらの IP アドレス" を選択し、"追加" を押し、"この IP アドレスまたはサブネット" で "xx.xx.xx.xx/xx" (ローカルネットワークや VPN のセグメント) を入力し、"次へ" を押す

構成図 (最終形)

f:id:Crayon:20210130112044j:plain

*1:hosts 格納フォルダ内は Windows 側の管理者権限がないと操作できず、かつ、hosts ファイル自身が Users グループ Readable/Executable でないと機能しない。アクセス権の緩いフォルダで制御して本来のフォルダからシンボリックリンクを設定することもできるがセキュリティ・リスクを高めてしまう。いずれにせよ NTFS アクセス権限を Ubuntu 側から制御するのはかなり面倒であった。