DockerとIPv6



IPv6環境でDockerを使うときの使い方と実装の（一部の）説明です。
環境はCentOS7付属の1.8、IPv4での設定、操作が出来ることを前提としています。
現行バージョン(1.10)ではいくつか実装が異なりますので後述します。



# 公式ドキュメント
ドキュメントが公開されています
https://docs.docker.com/engine/userguide/networking/default_network/ipv6/

日本語はこちら
http://docs.docker.jp/engine/userguide/networking/default_network/ipv6.html





# 設計
## アドレス空間
ドキュメントの記述では/80以上の空間を推奨しています。
これはLink local address同様の仕組みでMACアドレスからIPv6アドレスを生成しているためです。
※128(IPv6 len) - 48(MAC len) = 80

```text:コンテナ
# ip addr show
・・・
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:01 brd ff:ff:ff:ff:ff:ff
    inet6 2001:db8:ffff::242:ac11:1/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:1/64 scope link 
       valid_lft forever preferred_lft forever

```
MACアドレスとIPv6（2001:db8:ffff::/64）の下位が一致しています。

link localのアドレスバッティングを避けるために/80以上にするようですが、よほど広大なL2セグメントを作らない限りこれより小さいセグメントでも問題はありません。実際より/124などの小さいセグメントでも設定可能です。

またMACから生成されたlink localとは別にfe80::1/64がdocker0にアサインされます。

```text:ホスト
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:1d:5d:12:0b brd ff:ff:ff:ff:ff:ff
    inet 172.17.42.1/16 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:1dff:fe5d:120b/64 scope link 
       valid_lft forever preferred_lft forever
    inet6 fe80::1/64 scope link 
```



## ルーティング
ドキュメントに記述されているコンテナへのルーティングは

* routed（IPv6 nativeなルーティング）
* NDP Proxy（IPv4のproxy arp）

の二つです。
IPv6は全てのコンテナにグローバルアドレスを割り当ててもおつりがくるくらいのアドレス空間を持つことから、クライアントとコンテナはダイレクトに通信することを想定しているようです。
ポートフォワードも設定自体は出来ますがIPv4とは挙動が異なりますので後述します。
docker engine起動時のホスト側ルーティングテーブルは以下のようになります。

```text:ホスト
# ip -6 route show
2001:db8:1::/64 dev eno16780032  proto kernel  metric 256 
2001:db8:ffff::/64 dev docker0  metric 1024 
fe80::/64 dev eno16780032  proto kernel  metric 256 
fe80::/64 dev docker0  proto kernel  metric 256 
```


コンテナ側はdefault-gateway-v6若しくは指定しない場合はfe80::1に向けられます。

```text:コンテナ
# ip -6 route show
2001:db8:ffff::/64 dev eth0  proto kernel  metric 256 
fe80::/64 dev eth0  proto kernel  metric 256 
default via 2001:db8:ffff::1 dev eth0  metric 1024 
```




# パラメータ、設定内容など
##ホスト
アドレスアサイン、フォワーディング、ルーティング、ゲートウェイ設定など、IPv6の一般的な設定が必要ですがOSとして設定することはアドレスくらいです。
docker engineがいろいろと設定してくれたりします。


## Docker engine
コンテナに払いだすIPv6セグメント、ゲートウェイなどの設定を行います。

関連するパラメータ

* --ipv6
　IPv6の有効化。ホスト側のフォワーディングも設定されます。
　※sysctl net.ipv6.conf.all.forwarding=1 相当

* --fixed-cidr-v6
　コンテナにアサインするIPv6アドレスブロック

* --default-gateway-v6
　コンテナに設定するdefault gateway


## コンテナ
IPv6のプロトコルスタック自体はホスト側の実装をそのまま使いますのでイメージ側のアプリケーションがIPv6に対応するだけです。コンテナ側にネットワーク系のコマンド（ip, ifconfig, netstatなど）が入っていると確認が楽です。
docker hubのイメージならばIPv6関連コマンドが入っているubuntuが楽かもしれません。




# 設定
実際に設定してみます。

##構成
以下のようなネットワークを想定してみます。

![dock.png](https://qiita-image-store.s3.amazonaws.com/0/19360/8ce78f5b-7a96-5bc7-dd42-2c06eb7c2499.png)


## Docker engine
ホスト側の設定ファイル

```text:/etc/sysconfig/docker
OPTIONS='--selinux-enabled --ipv6 --fixed-cidr-v6="2001:db8:ffff::/64" --default-gateway-v6="2001:db8:ffff::1"'
```

CentOS7は標準でselinux-enabledが指定されていますが、必要に応じて変更して下さい。



##ホスト

```text:IF設定
# ip -6 addr add 2001:db8:1::1/64 dev eth0
```



検証時にはfirewalldは止めておいた方がいいかと思います。

```
# systemctl stop firewalld
```




##ルータ
IPv6アドレスは2001:db8:1::2/64とします。
2001:db8:ffff::/64へののルーティングの追加を忘れずに。



##起動

ホストで以下のコマンドを投入

```
# systemctl start docker
# ip -6 addr add 2001:db8:ffff::1/64 dev docker0
# docker run -it ubuntu /bin/bash
```

それぞれ

* docker engineの立ち上げ
* docker0にIPv6アドレスをアサイン
* コンテナ起動

です。
docker engine起動時にdocker0の作成及びIPv4アドレスがアサインされますが、IPv6アドレスはアサインされないため手動で設定する必要があります。


疎通確認は立ち上げたコンテナのIPv6アドレスに対して、対向からping6などのIPv6用のコマンドで確認します。

```text:コンテナ
# ip addr show
・・・
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:01 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:ffff::242:ac11:1/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:1/64 scope link 
       valid_lft forever preferred_lft forever
```


```text:ルータ
> ping6 2001:db8:ffff::1
```

疎通できないときはfirewalld(iptables)、selinux、ルーティング、セグメント長、docker0へのアドレス付与忘れあたりを確認すればいいかと思います。


# ポートフォワード
ドキュメントには記述がありませんがIPv6でポートフォワードも可能です。IPv6の場合はクライアント⇔コンテナがIPv6⇔IPv4のtranslateになります。
L3で変換されるためWebサーバなどではX-Forwardなどのヘッダは一切付加されません。
クライアントのIPv6アドレスが必要ならばホスト側でnginxなどのアプリケーションプロキシを立てる必要があります。

変換するのはホスト側のdocker-proxyになります。
ex. -p 8080:8080でコンテナを立ち上げた場合のホスト側のポートlisten状況

```text
# netstat -anp|grep 8080
tcp6   0    0 :::8080        :::*      LISTEN     12671/docker-proxy    
```



外部からのリクエストはホスト側でIPv6として終端しコンテナ側にIPv4として渡されます。その際のソースアドレスはホスト側ブリッジ（docker0)のアドレスが使われます。

ex. コンテナで立ち上げたapacheのログ

```
192.168.0.2 - - [28/Jan/2016:00:20:53 +0000] "GET / HTTP/1.1" 200 12 "-" "curl/7.29.0"
172.17.42.1 - - [28/Jan/2016:00:21:32 +0000] "GET / HTTP/1.1" 200 12 "-" "curl/7.29.0"
```

上がIPv4クライアント(192.168.0.2)、下がIPv6でのログになります。
IPv6でアクセスするとdocker0(172.17.42.1)のアドレスを始点としたIPv4アドレスにtranslateされることが確認できます。



またIPv4の場合はホスト側のiptablesでコンテナへのnatが設定されますが、IPv6の場合はdocker-proxyプロセスがIPv6で受けコンテナ側IPv4へ転送されます。ip6tablesの設定は行われません。

```text
# iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  anywhere            !loopback/8           ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
MASQUERADE  all  --  172.17.0.0/16        anywhere            
MASQUERADE  tcp  --  172.17.0.2           172.17.0.2           tcp dpt:webcache

Chain DOCKER (2 references)
target     prot opt source               destination         
DNAT       tcp  --  anywhere             anywhere             tcp dpt:webcache to:172.17.0.2:8080
```

```text
# ip6tables -L -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination       
```






# docker network
docker 1.9以降でネットワークまわりの機能が大幅に拡張されています。
しかしIPv6の対応状況はこれからという状況なので、現時点(2016/2, docker1.10）では

* IPv4ネットワークはdocker networkで一元管理
* IPv6ネットワークは自前管理、dockerで管理するのはコンテナだけ

というのが現実的な解になります。
またdocker engineもネットワークまわりで手が入ったためかパラメータ設定次第で動いたり動かなかったりと挙動が1.8以前と異なる点があり、ホスト側はそのままでは移行できない場合があります。

1.8以前と1.9以降との差分で確認している範囲では以下のものがあります

## default-gateway-v6を指定するとdocker engineが起動しない

fixed-cidr-v6を設定すると、docker0に若番をアサイン、コンテナ側のdefault gatewayも指定されます。

ex. --fixed-cidr-v6=2001:db8:cccc::/80

```text:ホスト側docker0
# ip addr show
11: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN 
    link/ether 02:42:5a:b2:44:1d brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.16/16 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:cccc::1/80 scope global 
```

```text:コンテナ側ルーティングテーブル
# ip -6 route show
2001:db8:cccc::/80 dev eth0  proto kernel  metric 256 
fe80::/64 dev eth0  proto kernel  metric 256 
default via 2001:db8:cccc::1 dev eth0  metric 1024 
```


ここでオプションに--default-gateway-v6に2001:db8:cccc::1を追加するとすでにアドレスがアサインされているとのメッセージが出力されdocker engineが起動しません。
docker0へのアドレスアサインが強制的に行われるため回避策としては以下のものが挙げられます

* default-gateway-v6を指定しなければfe80::1（docker0）が利用されるのでこちらを使う。
* aliasとして2001:db8:ffff::2などの別のIPv6を付与しこちらをgatewayに指定する(xxx::1でなければ起動可能）
* docker側ではなくホスト側のDHCPv6やRAなどからIPv6設定を行う

別途指定しなくてもルーティングは行われるので、1番目の「指定しない」というのが良いかもしれません。


# その他
## 接続できないとき
アドレスやルーティングの追加はdocker engineがうまくやってくれますが削除に関しては原則行われないため、検証などでアドレスやセグメント長などを変更する場合、ホスト側のブリッジやルーティングテーブルが汚れてくるのでうまく動かないときはホストの再起動であっさりうまくいくことが多々あります。


## 設定コマンド
原則dockerコマンドのみで済ませるように設計すべきです。
ネットワークに限らず各リソース(name space)をdockerが独自に管理しているものがほとんどなので、docker管理外のコマンドで操作した場合、不整合が発生することが多々あります。

残念ながらIPv6に関しては未実装のものが多く外部コマンドの力を借りなければなりませんが、dockerが設定してしまうモノ・自前で設定するモノを把握したうえで各操作をする必要があります。