Dockerで運用しているMastodonのIPv6対応

Docker-composeで作成されるネットワークはデフォルトではIPv4アドレスしか割り当てられないため、Mastodonインスタンス間の通信ではIPv6を使えません。
とりあえずDockerコンテナにIPv6 ULA([RFC4193](https://tools.ietf.org/html/rfc4193))を割り当ててNAT66(IP masquerade)を使用してコンテナから外部へIPv6で喋れるようにしました。

# 環境

```console
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.0 (stretch)
Release:        9.0
Codename:       stretch

$ uname -r
4.9.0-3-amd64

$ docker -v
Docker version 17.06.0-ce, build 02c1d87

$ docker-compose -v
docker-compose version 1.14.0, build c7bdf9e
```

# FORWARDとNATのルールを追加
ULAは例でfd91:7981:3415::/64としますが生成したものと全て置き換えてください。

```console
# ip6tables -I FORWARD -i br+ -s fd91:7981:3415::/64 -m conntrack --ctstate NEW -j ACCEPT
# ip6tables -I FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# ip6tables -t nat -I POSTROUTING -s fd91:7981:3415::/64 -j MASQUERADE
```

# docker-compose.yml

[Compose file version 3 reference | Docker Documentation](https://docs.docker.com/compose/compose-file/#ipv4_address-ipv6_address)のIPv6の部分にはCompose file version 2の設定例しかなくversion 3ではenable_ipv6が使えなかったので、予め[docker network create](http://docs.docker.jp/engine/reference/commandline/network_create.html)でネットワークを作成してからコンテナに接続することにしました。

```console
$ docker network create --attachable --ipv6 --subnet=fd91:7981:3415::/64 mastodon_ipv6

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
becf15cc01a8        bridge              bridge              local
32bad14627e6        host                host                local
4e3ea6b2b0ba        mastodon_ipv6       bridge              local
cd479a9cd90a        none                null                local
```

--ipv6を指定してネットワークを作成するとsysctlのnet.ipv6.conf.all.forwardingが有効になるためRAが無効になります。
RAを使用するインターフェースがある場合は[accept_ra](https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt)を2に設定してください。

```text:/etc/sysctl.conf
net.ipv6.conf.eth0.accept_ra=2
```

```diff:docker-compose.yml
@@ -7,6 +7,8 @@
 ### Uncomment to enable DB persistance
     volumes:
       - ./postgres:/var/lib/postgresql/data
+    networks:
+      - default

   redis:
     restart: always
@@ -14,6 +16,8 @@
 ### Uncomment to enable REDIS persistance
     volumes:
       - ./redis:/data
+    networks:
+      - default

   web:
     build: .
@@ -30,6 +34,9 @@
       - ./public/assets:/mastodon/public/assets
       - ./public/packs:/mastodon/public/packs
       - ./public/system:/mastodon/public/system
+    networks:
+      - default
+      - mastodon_ipv6

   streaming:
     build: .
@@ -42,6 +49,8 @@
     depends_on:
       - db
       - redis
+    networks:
+      - default

   sidekiq:
     build: .
@@ -54,3 +63,10 @@
       - redis
     volumes:
       - ./public/system:/mastodon/public/system
+    networks:
+      - default
+      - mastodon_ipv6
+
+networks:
+  mastodon_ipv6:
+    external: true
```

# コンテナから外部へIPv6で喋れるか確認

```console
$ docker network inspect mastodon_ipv6
[
    {
        "Name": "mastodon_ipv6",
        "Id": "af17c9b4af1732769e88165c988bba473c10d8a3af89cce6c1d2ca10884995d0",
        "Created": "2017-05-28T08:06:18.985725893+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": true,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                },
                {
                    "Subnet": "fd91:7981:3415::/64"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "Containers": {
            "290105e8d2d8f851f2691cb48de1451f66165f0afa06991e3cb047e649693e6c": {
                "Name": "mastodon_web_1",
                "EndpointID": "c95a7cd9be5f4bfc58ffe7f76b28425b677728bbeb2f292f862a5db8f6a41e22",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": "fd91:7981:3415::2/64"
            },
            "fa12db023c789d5967d43731b4b9894d4a3056b9fa4dc27391309eec0f7b0b23": {
                "Name": "mastodon_sidekiq_1",
                "EndpointID": "5e86e412fbccf73ecee590facc252afca4e8e2e890566821a0d8bf7b6ea577a8",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": "fd91:7981:3415::3/64"
            }
        },
        "Options": {},
        "Labels": {}
    }
]

$ docker exec -it mastodon_sidekiq_1 sh

/mastodon # ping6 google.co.jp
PING google.co.jp (2404:6800:4004:807::2003): 56 data bytes
64 bytes from 2404:6800:4004:807::2003: seq=0 ttl=55 time=0.938 ms
64 bytes from 2404:6800:4004:807::2003: seq=1 ttl=55 time=0.871 ms
64 bytes from 2404:6800:4004:807::2003: seq=2 ttl=55 time=0.932 ms

/mastodon # traceroute6 google.co.jp
traceroute to google.co.jp (2404:6800:4004:807::2003), 30 hops max, 24 byte packets
 1  fd91:7981:3415::1 (fd91:7981:3415::1)  0.011 ms  0.005 ms  0.002 ms
 2  2400:8500:1301:746::1 (2400:8500:1301:746::1)  28.441 ms  21.597 ms  2.113 ms
 3  2400:8500:1300:1028::2 (2400:8500:1300:1028::2)  0.884 ms  0.818 ms  0.803 ms
 4  2400:8500:1300:1027::1 (2400:8500:1300:1027::1)  2.424 ms  1.909 ms  1.954 ms
 5  2400:8500:1300:1077::1 (2400:8500:1300:1077::1)  0.814 ms  1.059 ms  1.072 ms
 6  ae-25.r00.tokyjp05.jp.bb.gin.ntt.net (2001:218:2000:5000::3c5)  1.475 ms  1.371 ms  1.374 ms
 7  ae-6.r31.tokyjp05.jp.bb.gin.ntt.net (2001:218:0:2000::351)  1.982 ms  0.935 ms  0.907 ms
 8  ae-6.r02.tokyjp03.jp.bb.gin.ntt.net (2001:218:0:2000::192)  1.714 ms  1.724 ms  1.575 ms
 9  2001:218:2000:5000::47e (2001:218:2000:5000::47e)  1.314 ms  1.325 ms  1.332 ms
10  2001:4860:0:1000::1 (2001:4860:0:1000::1)  1.774 ms  1.715 ms  1.539 ms
11  2001:4860:0:1::1b9b (2001:4860:0:1::1b9b)  1.598 ms  1.530 ms  1.495 ms
12  nrt12s15-in-x03.1e100.net (2404:6800:4004:807::2003)  1.576 ms  1.601 ms  1.551 ms
```

とりあえずこれでIPv6 onlyのMastodonインスタンスとの通信は出来ているのですが、ULA(fc00::/7)を使っているため[RFC6724](https://tools.ietf.org/html/rfc6724)で定義されているpolicy tableに従って、IPv4/IPv6 dual stackのインスタンスとの通信ではIPv4が優先されるようでした。
~~Dockerfileを書いてコンテナ内に/etc/gai.confを置いてみたりしたんですが、相変わらずIPv4が優先されてしまいます。~~
Alpine Linuxはglibcではなくmusl libcを使っているので、そもそも/etc/gai.confを参照していませんでした。

# コンテナにGUAを割り当てる場合

例えばホスト側のネットワークを2001:db8:1111:aaaa::/64と想定してコンテナに2001:db8:1111:aaaa:1::/80を割り当てる場合は次のように設定します。

```console
# ip6tables -I FORWARD -i br+ -s 2001:db8:1111:aaaa:1::/80 -m conntrack --ctstate NEW -j ACCEPT
# ip6tables -I FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# ip6tables -L FORWARD
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all      anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     all      2001:db8:1111:aaaa:1::/80  anywhere             ctstate NEW
REJECT     all      anywhere             anywhere             reject-with icmp6-adm-prohibited
```

```console
$ docker network create --attachable --ipv6 --subnet=2001:db8:1111:aaaa:1::/80 mastodon_ipv6

$ ip -6 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,ALLMULTI,UP,LOWER_UP> mtu 1500 qlen 1000
    inet6 2001:db8:1111:aaaa::4/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::9ea3:baff:fe31:e19e/64 scope link
       valid_lft forever preferred_lft forever
4: br-d797fe9ca63e: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500
    inet6 2001:db8:1111:aaaa:1::1/80 scope global tentative
       valid_lft forever preferred_lft forever
    inet6 fe80::1/64 scope link tentative
       valid_lft forever preferred_lft forever
```

ホストとコンテナのルーティングテーブルは次のように設定されます。

```console
$ ip -6 route
2001:db8:1111:aaaa:1::/80 dev br-d797fe9ca63e  proto kernel  metric 256
2001:db8:1111:aaaa:1::/80 dev br-d797fe9ca63e  metric 1024
2001:db8:1111:aaaa::/64 dev eth0  proto kernel  metric 256
fe80::/64 dev eth0  proto kernel  metric 256
fe80::/64 dev br-d797fe9ca63e  proto kernel  metric 256
default via fe80::1 dev eth0  metric 1024

$ docker exec -it mastodon_sidekiq_1 sh
/mastodon # ip -6 route
2001:db8:1111:aaaa:1::/80 dev eth0  metric 256
fe80::/64 dev eth0  metric 256
default via 2001:db8:1111:aaaa:1::1 dev eth0  metric 1024
unreachable default dev lo  metric -1  error -101
ff00::/8 dev eth0  metric 256
unreachable default dev lo  metric -1  error -101
```



### NDP Proxyの使用

次の例では上流のルータからコンテナのIPv6アドレスに対してパケットを送信する際、マルチキャストアドレス宛てにMACアドレスを解決するためのNeighbor Solicitation(NS)が送信されていますが、このIPv6アドレスはホスト側のネットワークにいないためNeighbor Advertisement(NA)を応答せず通信に失敗しています。

```text
14:44:37.764074 IP6 2001:db8:1111:aaaa:1::2 > nrt12s14-in-x0e.1e100.net: ICMP6, echo request, seq 0, length 64
14:44:37.771600 IP6 fe80::fac2:88ff:fe70:aebf > ff02::1:ff00:2: ICMP6, neighbor solicitation, who has 2001:db8:1111:aaaa:1::2, length 32
14:44:38.764905 IP6 2001:db8:1111:aaaa:1::2 > nrt12s14-in-x0e.1e100.net: ICMP6, echo request, seq 1, length 64
14:44:38.817944 IP6 fe80::fac2:88ff:fe70:aebf > ff02::1:ff00:2: ICMP6, neighbor solicitation, who has 2001:db8:1111:aaaa:1::2, length 32
14:44:39.765253 IP6 2001:db8:1111:aaaa:1::2 > nrt12s14-in-x0e.1e100.net: ICMP6, echo request, seq 2, length 64
14:44:39.905765 IP6 fe80::fac2:88ff:fe70:aebf > ff02::1:ff00:2: ICMP6, neighbor solicitation, who has 2001:db8:1111:aaaa:1::2, length 32
```

この場合は次のようにNDP Proxyを使用します。

```text:/etc/sysctl.conf
net.ipv6.conf.eth0.proxy_ndp=1
```

```console
$ docker network inspect mastodon_ipv6
        "Containers": {
            "085a4c201b795629e7447ac4fa019e2e36dbfd89a6166370b3ffba8e92f6f172": {
                "Name": "mastodon_web_1",
                "EndpointID": "6128ab4997af8ff3506ebe40f07fc44e86c19a30ad6ce32f24918ce3c2d1c681",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": "2001:db8:1111:aaaa:1::2/80"
            },
            "1f6e68fde888ef6b30be41667d9b4117227b466b5d31ba1f2a9d412698f1d803": {
                "Name": "mastodon_sidekiq_1",
                "EndpointID": "8d3a29b4c643d9841c4a6bbbb8f6bb259f8bb478a39d21b8f4f84135db9fdfee",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": "2001:db8:1111:aaaa:1::3/80"
            }
        },

# ip -6 neigh add proxy 2001:db8:1111:aaaa:1::2 dev eth0
# ip -6 neigh add proxy 2001:db8:1111:aaaa:1::3 dev eth0
# ip -6 neigh show proxy
2001:db8:1111:aaaa:1::2 dev eth0  proxy
2001:db8:1111:aaaa:1::3 dev eth0  proxy
```

2001:db8:1111:aaaa:1::2と2001:db8:1111:aaaa:1::3のNSに対する応答として、ホストのeth0のMACアドレスを格納したNAを送信し、このMACアドレスに対してパケットが送信されるようになります。

```text
15:03:53.033531 IP6 2001:db8:1111:aaaa:1::2 > nrt12s14-in-x0e.1e100.net: ICMP6, echo request, seq 0, length 64
15:03:53.037057 IP6 fe80::fac2:88ff:fe70:aebf > ff02::1:ff00:2: ICMP6, neighbor solicitation, who has 2001:db8:1111:aaaa:1::2, length 32
15:03:53.542840 IP6 fe80::9ea3:baff:fe31:e19e > fe80::fac2:88ff:fe70:aebf: ICMP6, neighbor advertisement, tgt is 2001:db8:1111:aaaa:1::2, length 32
15:03:53.543872 IP6 nrt12s14-in-x0e.1e100.net > 2001:db8:1111:aaaa:1::2: ICMP6, echo reply, seq 0, length 64
```

```text
15:03:53.542840 IP6 fe80::9ea3:baff:fe31:e19e > fe80::fac2:88ff:fe70:aebf: ICMP6, neighbor advertisement, tgt is 2001:db8:1111:aaaa:1::2, length 32
        0x0000:  6000 0000 0020 3aff fe80 0000 0000 0000  `.....:.........
        0x0010:  9ea3 baff fe31 e19e fe80 0000 0000 0000  .....1..........
        0x0020:  fac2 88ff fe70 aebf 8800 5644 4000 0000  .....p....VD@...
        0x0030:  2001 0db8 1111 aaaa 0001 0000 0000 0002  ................
        0x0040:  0201 9ca3 ba31 e19e                      .....1..
```

```console
# ip link show dev eth0
2: eth0: <BROADCAST,MULTICAST,ALLMULTI,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 9c:a3:ba:31:e1:9e brd ff:ff:ff:ff:ff:ff
```

コンテナに割り当てている全てのアドレスでip -6 neigh add proxyコマンドを実行しましたが、docker-compose scaleを使う時に面倒なので[ndppd](https://github.com/DanielAdolfsson/ndppd)を使った方がいいかもしれません。

```console
# apt update
# apt install ndppd
# cat /etc/ndppd.conf
proxy eth0 {
   rule 2001:db8:1111:aaaa:1::/80 {
      static
   }
}
```