IPv6 only 環境で Docker / Alpine Linux を動かす

2022年5月、IPv4アドレスが枯渇してきていることもあって、IPv6 onlyな環境がVPSの一般サービスとして出てきました。 OSレベルではIPv4/IPv6のデュアルスタックで両方のネットワーク環境にサーバもクライアントも対応しつつあるのですが、 世の中にはまだAAAAレコードを持たないHTTP/HTTPSサーバも結構あって、DockerをIPv6 only環境で動かすのに一苦労しました。

今回、私自身が Rocky Linux 8.5 の IPv6 only 環境で Docker / Alpine Linux を動かした記録をメモで残しておきます。

Dockerのインストール

まず最初にdnfコマンドを使ってDockerをインストールします。

sudo dnf update -y
sudo dnf install -y dnf-utils device-mapper-persistent-data lvm2
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf makecache
sudo dnf install -y docker-ce
sudo systemctl start docker
sudo systemctl enable docker

(最近のCentOSLinuxではyumは推奨されていないので、dnfコマンドを使用しました。)

Dockerを動かすユーザの権限設定

root以外でdockerを動かす場合、自分自身のユーザ($USER)をdockerグループに所属させておきます。

sudo groupadd docker
# groupadd: group 'docker' already exists
sudo usermod -aG docker $USER

IPv6対応のdaemon.jsonを作成

dockerデーモンの起動オプション/etc/docker/daemon.jsonIPv6対応の設定を追加します。

sudo vi /etc/docker/daemon.json

作成するファイルの中身は以下の通りです。

{
  "ipv6": true,
  "fixed-cidr-v6": "fd11:2233:4455:6677::/64",
  "registry-mirrors": ["https://mirror.gcr.io"]
}

Dockerのデフォルトのレジストリregistry-1.docker.ioIPv4のアドレスしか返さないのでIPv6 only環境だと通信できません。この場合、IPv6に対応しているGoogleレジストリミラー https://mirror.gcr.io を設定しておきます。 fixed-cidr-v6で設定しているfd11:2233:4455:6677::/64は、自分で設定するユニークローカルアドレス (ULA: unique local address) で、IPv6のプライベートアドレスに相当するものです。自分で使う場合は、fdから始まるアドレスで任意のアドレスを指定してください。

dockerの再起動

これらの設定を有効にするにはdockerを再起動します。

sudo systemctl restart docker

もしもエラーが出た場合はdaemon.jsonの設定ファイルを見直して修正します。

ip6tablesでNATを追加

daemon.jsonのfixed-cidr-v6で設定した内部ネットワークfd11:2233:4455:6677::/64から外に通信ができるようにNATを設定します。

sudo ip6tables -t nat -A POSTROUTING -s fd11:2233:4455:6677::/64 ! -o docker0 -j MASQUERADE

引数に-t natを指定してNATテーブルが正しく追加されているかどうか確認します。

sudo ip6tables -t nat -L

以下の出力にMASQUERADE all fd11:2233:4455:6677::/64 anywhereの行があれば大丈夫です。

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all      fd11:2233:4455:6677::/64  anywhere

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

再起動時のrc.localの設定

再起動してもNATの設定が有効となるように、rc.localの設定をします。

sudo vi /etc/rc.d/rc.local

rc.localは過去の互換性のために残されているファイルですが、最後に以下の一行を追加します。

#!/bin/bash
# THIS FILE IS ADDED FOR COMPATIBILITY PURPOSES
#
# It is highly advisable to create own systemd services or udev rules
# to run scripts during boot instead of using this file.
#
# In contrast to previous versions due to parallel execution during boot
# this script will NOT be run after all other services.
#
# Please note that you must run 'chmod +x /etc/rc.d/rc.local' to ensure
# that this script will be executed during boot.

touch /var/lock/subsys/local

ip6tables -t nat -A POSTROUTING -s fd11:2233:4455:6677::/64 ! -o docker0 -j MASQUERADE

rc.localの設定が初めてであれば、以下のコマンドを実行して再起動時に実行されるようにします。

sudo chmod +x /etc/rc.d/rc.local
sudo systemctl start rc-local

docker infoで現在の設定を確認

docker infoを実行して現在の設定を確認します。

docker info

こんな感じの出力がされるはずです。

Client:
 Context:    default
 Debug Mode: false
 Plugins:
  app: Docker App (Docker Inc., v0.9.1-beta3)
  buildx: Docker Buildx (Docker Inc., v0.8.1-docker)
  scan: Docker Scan (Docker Inc., v0.17.0)

Server:
~中略~
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:
  https://mirror.gcr.io/
 Live Restore Enabled: false

Registry Mirrors:にhttps://mirror.gcr.io/があればokです。

Alpine Linux の Dockerfile を作成

適当なディレクトリでDockerfileを作成します。

vi Dockerfile

Dockerfileの中身は以下の通りです。

FROM alpine:3.15.4
RUN apk update && apk add bind-tools curl
CMD ["/bin/sh"]

alpine:3.15.4のイメージをpullした後、digコマンドとcurlコマンドが使いたいのでbind-toolsとcurlのapkパッケージをインストールして、デフォルトでシェルを起動しています。

docker build

タグ名は何でも良いのですが、ipv6/alpineでdocker buildします。

docker build -t ipv6/alpine .

ビルド時に以下のような感じでIPv6のサーバからapkのパッケージがダウンロードできれば成功です。

Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM alpine:3.15.4
3.15.4: Pulling from library/alpine
df9b9388f04a: Pull complete
Digest: sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454
Status: Downloaded newer image for alpine:3.15.4
 ---> 0ac33e5f5afa
Step 2/3 : RUN apk update && apk add bind-tools curl
 ---> Running in a1ebb762c17e
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/community/x86_64/APKINDEX.tar.gz
v3.15.4-78-ge239b26b75 [https://dl-cdn.alpinelinux.org/alpine/v3.15/main]
v3.15.4-79-gaf675e239c [https://dl-cdn.alpinelinux.org/alpine/v3.15/community]
OK: 15855 distinct packages available
(1/18) Installing fstrm (0.6.1-r0)
(2/18) Installing krb5-conf (1.0-r2)
(3/18) Installing libcom_err (1.46.4-r0)
(4/18) Installing keyutils-libs (1.6.3-r0)
(5/18) Installing libverto (0.3.2-r0)
(6/18) Installing krb5-libs (1.19.3-r0)
(7/18) Installing json-c (0.15-r1)
(8/18) Installing protobuf-c (1.4.0-r0)
(9/18) Installing libuv (1.42.0-r0)
(10/18) Installing xz-libs (5.2.5-r1)
(11/18) Installing libxml2 (2.9.13-r0)
(12/18) Installing bind-libs (9.16.27-r0)
(13/18) Installing bind-tools (9.16.27-r0)
(14/18) Installing ca-certificates (20211220-r0)
(15/18) Installing brotli-libs (1.0.9-r5)
(16/18) Installing nghttp2-libs (1.46.0-r0)
(17/18) Installing libcurl (7.80.0-r1)
(18/18) Installing curl (7.80.0-r1)
Executing busybox-1.34.1-r5.trigger
Executing ca-certificates-20211220-r0.trigger
OK: 15 MiB in 32 packages
Removing intermediate container a1ebb762c17e
 ---> 18d7e9a3b51b
Step 3/3 : CMD ["/bin/sh"]
 ---> Running in ccda19d240b3
Removing intermediate container ccda19d240b3
 ---> ff8bbe4b67c8
Successfully built ff8bbe4b67c8
Successfully tagged ipv6/alpine:latest

エラーが出なければ成功です。おめでとうございます。

※ registry-1.docker.io でエラーが出る場合

もしもdocker build時に以下のエラーが出る場合は、registry-1.docker.io がIPv6のAAAAレコードを持っていないため、IPv4アドレスで通信しようとして失敗しています。

Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM alpine:3.15.4
Get "https://registry-1.docker.io/v2/": dial tcp 34.203.135.183:443: connect: network is unreachable

この場合は、前述のdaemon.jsonで"registry-mirrors": ["https://mirror.gcr.io"]を指定して、IPv6で通信が可能なミラーサーバを指定する必要があります。 一度、失敗するとdocker内で変にダウンロードURLがキャッシュされてしまうようで、設定を変更したら、FROM alpine:3.15.4をFROM alpine:3.15.2のように別のバージョンをダウンロードして試してみてください。

※ dl-cdn.alpinelinux.org でエラーが出る場合

docker build時に以下のエラーが出る場合は、docker内からIPv6 only環境での外への通信ができないで失敗しています。

fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/main/x86_64/APKINDEX.tar.gz
ERROR: https://dl-cdn.alpinelinux.org/alpine/v3.15/main: temporary error (try again later)
WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/v3.15/main: No such file or directory
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/community/x86_64/APKINDEX.tar.gz
ERROR: https://dl-cdn.alpinelinux.org/alpine/v3.15/community: temporary error (try again later)
WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/v3.15/community: No such file or directory

前述のdaemon.jsonで設定した"fixed-cidr-v6": "fd11:2233:4455:6677::/64"で指定した内部ネットワークから外に通信ができないといけないため、ip6tablesのNATの設定が有効になっているかどうかを確認する必要があります。

また、2022年5月3日現在、dl-cdn.alpinelinux.orgがデュアルスタックになってIPv6のAAAAレコードを持っているため、IPv6 only環境でも通信ができるはずです。

$ dig dl-cdn.alpinelinux.org. A
; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> dl-cdn.alpinelinux.org. A
~中略~
;; QUESTION SECTION:
;dl-cdn.alpinelinux.org.                IN      A

;; ANSWER SECTION:
dl-cdn.alpinelinux.org. 1420    IN      CNAME   dualstack.d.sni.global.fastly.net.
dualstack.d.sni.global.fastly.net. 30 IN A      151.101.110.133
$ dig dl-cdn.alpinelinux.org. AAAA

; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> dl-cdn.alpinelinux.org. AAAA
~中略~
;; QUESTION SECTION:
;dl-cdn.alpinelinux.org.                IN      AAAA

;; ANSWER SECTION:
dl-cdn.alpinelinux.org. 1484    IN      CNAME   dualstack.d.sni.global.fastly.net.
dualstack.d.sni.global.fastly.net. 30 IN AAAA   2a04:4e42:1a::645

もしも、AAAAレコードを返さないサーバだった場合、IPv4アドレスで通信しようとして失敗してしまいますので、この場合は、Dockerfile で RUN sed -r "s;//dl-cdn.alpinelinux.org/alpine;//ftp.udx.icscoe.jp/Linux/alpine;g" -i /etc/apk/repositories を実行するなどして、IPv6で通信が可能なミラーサーバを指定してください。ちなみにAlpine Linuxのオフィシャルのミラーサーバの一覧は https://mirrors.alpinelinux.org/ で確認できます。

docker run

ビルド時に指定したタグ名ipv6/alpineでdocker runします。-iオプションでインタラクティブに対話しながらシェルを実行できます。

docker container run -it ipv6/alpine

無事にシェルに入れたら、curlコマンドでIPv6のサーバとHTTP通信できるかどうか確認します。

/ # cd /tmp
/tmp # curl http://google.com/
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
/tmp #

301のレスポンスが表示されればIPv6 onlyで疎通できたので成功です。おめでとうございます。

busyboxwgetIPv6対応が不完全

ちなみに、busyboxwgetIPv6対応が完全ではないため、apk add wgetしてwgetコマンドを置き換えておくか、curlコマンドを使うようにしておくと良いでしょう。

takesako (y0sh1) on Twitter: "IPv6 only環境でbusybox wgetで名前解決する際にIPv4アドレスを優先してしまってダウンロードできない問題:ラッパー関数xhost2sockaddrの中でstr2sockaddrに(sa_family_t)AF_UNSPECを指定してしまっていて、getaddrinfoで複数のIPアドレス(v4/v6)が返ってきたときに最初のIPをそのまま使ってしまう罠."

IPv6 only環境でインターネットがもっと使えるようになると嬉しいですね。

参考文献