dockerのポートフォワードをIPv6にも対応させる

# 更新履歴
2020/08/02
いつのまにかスクリプト内で発行する`nft`コマンドのオプション`-a`が先頭にないとエラーになっていたので、修正しました。

# 概要
 **[squidでActiveDirectory連携とSSLインターセプトするProxyをdockerで手軽につくる](https://qiita.com/bashaway/items/6adc75265973de9b5fcc)**
**[Docker Composeでネットワークサービス群を5分で作れるようにした（dhcp/radius/proxy/tftp/syslog/dns）](https://qiita.com/bashaway/items/3e308cb217bbb225d11c)**
上記のエントリでつくったProxyにULAのIPv6 Onlyな環境からアクセスしようとしたら、できなかった。

以下、Windowsの状況ですが、iOSとAndroidも混在しているので、DHCPv6が使えず（AndroidがDHCPv6つかえない）、ゲートウェイでRAとRDNSS（ドメインサフィックスも付与）を動作させている状態です。

```
Wireless LAN adapter Wi-Fi:

   接続固有の DNS サフィックス . . . . .: prosper2.net
   説明. . . . . . . . . . . . . . . . .: Intel(R) WiFi Link 5300 AGN
   物理アドレス. . . . . . . . . . . . .: 00-21-6A-CA-4A-A2
   DHCP 有効 . . . . . . . . . . . . . .: はい
   自動構成有効. . . . . . . . . . . . .: はい
   IPv6 アドレス . . . . . . . . . . . .: fd5a:ceb9:ed8d:fe10:70ff:9d38:95b6:9682(優先)
   一時 IPv6 アドレス. . . . . . . . . .: fd5a:ceb9:ed8d:fe10:4ca8:def3:2da5:a3b6(優先)
   リンクローカル IPv6 アドレス. . . . .: fe80::70ff:9d38:95b6:9682%13(優先)
   デフォルト ゲートウェイ . . . . . . .: fe80::b60c:25ff:fe0f:df11%13
   DHCPv6 IAID . . . . . . . . . . . . .: 117449066
   DHCPv6 クライアント DUID. . . . . . .: 00-01-00-01-24-DA-AE-0D-5C-26-0A-2D-95-59
   DNS サーバー. . . . . . . . . . . . .: fd5a:ceb9:ed8d:fe0a:10:254:10:251
   NetBIOS over TCP/IP . . . . . . . . .: 無効
   接続固有の DNS サフィックス検索の一覧:
                                          prosper2.net
```

# なぜ通信できなかったのか
Dockerネットワークは以下のコマンドで作成したものを利用していました。

```
# docker network create --ipv6  --driver=bridge  --subnet=fd5a:ceb9:ed8d:dead::/64 br_infra_net
```

IPv4のDNATは設定されていた。（ほかのコンテナのものも入ってます）

```
# nft list chain ip nat PREROUTING ; nft list chain ip nat DOCKER
table ip nat {
        chain PREROUTING {
                type nat hook prerouting priority -100; policy accept;
                fib daddr type local counter packets 42 bytes 6049 jump DOCKER
        }
}
table ip nat {
        chain DOCKER {
                meta l4proto tcp tcp dport 8081 counter packets 2 bytes 104 dnat to 172.18.0.2:8081
                meta l4proto tcp tcp dport 53 counter packets 0 bytes 0 dnat to 172.18.0.3:53
                meta l4proto udp udp dport 53 counter packets 0 bytes 0 dnat to 172.18.0.3:53
                meta l4proto udp udp dport 514 counter packets 10 bytes 3332 dnat to 172.18.0.4:514
                meta l4proto tcp tcp dport 8080 counter packets 0 bytes 0 dnat to 172.18.0.5:8080
                meta l4proto udp udp dport 1813 counter packets 0 bytes 0 dnat to 172.18.0.6:1813
                meta l4proto udp udp dport 1812 counter packets 0 bytes 0 dnat to 172.18.0.6:1812
        }
}
```

IPv6はされてなかった。悲しい。

```
# nft list chain ip6 nat PREROUTING
table ip6 nat {
        chain PREROUTING {
                type nat hook prerouting priority -100; policy accept;
        }
}
```

どうやらコンテナにIPv6ふってるんだからNDPで直接通信してね。
みたいなニュアンスらしい。（違ってたらごめんなさい）

やだ、めんどくさい。
IPv4とおんなじ考え方で通信させたいんだ。


# スクリプトで対応した
ちゃんとdockerのマニュアルとか読めば解決策があるような気もするのですが、面倒なのでnftでDNATすることにしました。
IPv4のnftを参考にして、IPv6もおなじようなDNATするスクリプトを作成。

スクリプトは `jq` を利用しています。
`dnf -y install jq` などでインストールしておく必要があります。


```perl:ipv6_port_bind.pl
#!/usr/bin/perl
use strict;

my ($buf,$addr,$cnt,$port,$prot,$handle,$cmd);

my $CHAIN = "DOCKER-USER-ipv6-dnat";

if($ARGV[0] !~ /flush|^$/){
  print "usage : $0 [ flush ]\n";
  exit;
}


####################
#  FLUSH RULES
####################


print "---------- FLUSH exist rules and chains ----------\n";
foreach(split(/\n/,`nft -a list chain ip6 nat PREROUTING | grep "jump $CHAIN" 2> /dev/null`)){
  chomp($_);
  $handle = (split(/\ /,$_))[-1];
  $cmd = "nft delete rule ip6 nat PREROUTING handle $handle";
  print $cmd."\n";
  system("$cmd 2> /dev/null");
}

$cmd = "nft delete chain ip6 nat $CHAIN";
print $cmd."\n";
system("$cmd 2> /dev/null");

if($ARGV[0] eq "flush"){
  exit;
}

print "\n";

####################
#  NEW RULES
####################

$buf  = "";
$buf .= "nft create chain ip6 nat $CHAIN\n";

foreach $cnt (split(/\n/,`docker-compose ps --service`)){
  chomp($cnt);

  $addr = `docker inspect $cnt | jq  '.[].NetworkSettings.Networks[].GlobalIPv6Address'`;
  chomp($addr);
  $addr =~ s/\"//g;

  foreach(split(/\n/,`docker inspect $cnt | jq  '.[].HostConfig.PortBindings|keys' | egrep "tcp|udp" `)){
    chomp($_);
    $_ =~ s/\"|\,|\ //g;
    $port = (split(/\//,$_))[0];
    $prot = (split(/\//,$_))[1];

    #print "$addr : $port $prot \n";
    $buf .= "nft add rule ip6 nat $CHAIN meta l4proto $prot $prot dport $port counter dnat to [$addr]:$port\n";
  }

}

$buf .= "nft add rule ip6 nat PREROUTING  fib daddr type local counter jump $CHAIN\n";

print "---------- CREATE chain and ADD rules ----------\n";
system($buf);
print $buf."\n";

print "---------- CHECK rules and chain ----------\n";
print `nft list chain ip6 nat PREROUTING ; nft list chain ip6 nat $CHAIN`;

exit;
```

内容は、IPv4のDNATの処理を同じものを、 `docker-compose.yml` で生成したコンテナに対して実施しています。

`docker-compose.yml` と同じディレクトリにおいて、スクリプトを実行すると、起動しているコンテナに対して `docker inspect コンテナ` を実行します。
得られた出力からポートバインドの設定を抜き出して、 `nft` の処理を生成しています。

実際に動作させると、以下のようになります。

```
# ./ipv6_port_bind.pl

---------- FLUSH exist rules and chains ----------
nft delete rule ip6 nat PREROUTING handle 250
nft delete chain ip6 nat DOCKER-USER-ipv6-dnat

---------- CREATE chain and ADD rules ----------
nft create chain ip6 nat DOCKER-USER-ipv6-dnat
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 514 counter dnat to [fd5a:ceb9:ed8d:dead::4]:514
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto tcp tcp dport 8081 counter dnat to [fd5a:ceb9:ed8d:dead::2]:8081
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto tcp tcp dport 8080 counter dnat to [fd5a:ceb9:ed8d:dead::5]:8080
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 1812 counter dnat to [fd5a:ceb9:ed8d:dead::6]:1812
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 1813 counter dnat to [fd5a:ceb9:ed8d:dead::6]:1813
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto tcp tcp dport 53 counter dnat to [fd5a:ceb9:ed8d:dead::3]:53
nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 53 counter dnat to [fd5a:ceb9:ed8d:dead::3]:53
nft add rule ip6 nat PREROUTING  fib daddr type local counter jump DOCKER-USER-ipv6-dnat

---------- CHECK rules and chain ----------
table ip6 nat {
        chain PREROUTING {
                type nat hook prerouting priority -100; policy accept;
                fib daddr type local counter packets 0 bytes 0 jump DOCKER-USER-ipv6-dnat
        }
}
table ip6 nat {
        chain DOCKER-USER-ipv6-dnat {
                udp dport shell counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::4]:shell
                tcp dport tproxy counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::2]:tproxy
                tcp dport http-alt counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::5]:http-alt
                udp dport radius counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::6]:radius
                udp dport radius-acct counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::6]:radius-acct
                tcp dport domain counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::3]:domain
                udp dport domain counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::3]:domain
        }
}
```

複数回起動しても問題ないように、処理の際には、既存ルールをフラッシュしてから、再作成しています。
`nft ～` で始まる行が、実際に発行されるnftコマンドです。
最後に新たに作成したルールとCHAINを表示しています。

これで、ULAなIPv6 Only環境からもDockerコンテナにアクセスできるようになりました。
よかった。