IPv6 と Docker と NAT

IPv6 のアドレスが1つしかない状況で、ネットワークが分離されたコンテナから IPv6 で通信しようとすると IPv6 だろうと NAT が必要になる。 このへん Docker がどう扱うのかよくわかってなくて、結局 Docker の外側で docker0 とか ip6tables を管理することで動いた……

まず docker0 に使うレンジを決める。ここでは fdbb:3f26:ceda::/4810.11.0.0/16 とする。 docker0 を systemd-networkd で作る。

% cat /etc/systemd/network/docker0.netdev
[NetDev]
Name=docker0
Kind=bridge
% cat /etc/systemd/network/docker0.network
[Match]
Name=docker0

[Network]
Address=10.11.0.1/16
Address=fdbb:3f26:ceda::1/48

dockerd を起動する。このときネットワーク系のオプションを切っておく。

% cat /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=fdbb:3f26:ceda::/48 --bridge=docker0 --ip-forward=false --ip-masq=false --iptables=false -H fd://
# systemctl start docker.service

デフォルトだと dockerd が IPv4 でやっていたことを、IPv4IPv6 の両方について自分でやる。

% cat /etc/sysctl.d/30-ip-forward.conf
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.forwarding = 1
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
# iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# iptables -A FORWARD -s 10.11.0.0/16 -i docker0 ! -o docker0 -j ACCEPT
# iptables -A POSTROUTING -s 10.11.0.0/16 ! -o docker0 -j MASQUERADE
# ip6tables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# ip6tables -A FORWARD -s fdbb:3f26:ceda::/48 -i docker0 ! -o docker0 -j ACCEPT
# ip6tables -A POSTROUTING -s fdbb:3f26:ceda::/48 ! -o docker0 -j MASQUERADE

これで Docker コンテナ内から IPv6 で通信できるようになる。

% cat Dockerfile
FROM ubuntu:16.04
RUN apt update && apt install -y curl
% docker build -t ubuntu-curl .
% docker run --rm ubuntu-curl curl -sv -o /dev/null https://ipv6.google.com/
*   Trying 2404:6800:4004:816::200e...
* Connected to ipv6.google.com (2404:6800:4004:816::200e) port 443 (#0)
(snip)

ただ、ULA なアドレスを使っているせいで A も AAAA も持っているようなドメインの場合、デフォルトだと IPv4 が優先されてしまう。

% docker run --rm ubuntu-curl curl -sv -o /dev/null https://google.com/
*   Trying 172.217.25.78...
* Connected to google.com (172.217.25.78) port 443 (#0)
(snip)

この優先度は /etc/gai.conf で決められているので、とりあえずそこを変えれば IPv6 が優先されるようになる *1

% cat gai.conf
label ::1/128       0
label ::/0          1
label 2002::/16     2
label ::/96         3
label ::ffff:0:0/96 4
% docker run -v $PWD/gai.conf:/etc/gai.conf:ro --rm ubuntu-curl curl -sv -o /dev/null https://google.com/
*   Trying 2404:6800:4004:819::200e...
* Connected to google.com (2404:6800:4004:819::200e) port 443 (#0)
(snip)

今回は Docker でやったけど、systemd-nspawn (machinectl) を使うときもやることは同じ。 docker0 と同じ要領で br0 を作って systemd-nspawn --network-bridge=br0 で起動する。 systemd-nspawn の場合は勝手にアドレスを割り当てたりしてくれないので、それはコンテナ内の systemd-netword でやるようにする。 このときデフォルトだと host0 に対して余計な設定があるので、コンテナ内で ln -s /dev/null /etc/systemd/network/80-container-host0.network で切っておく。

*1:curl の場合は -6 で IPv6 を強制できるけど