Debian 10(buster) + ufw + Docker: Dockerコンテナが外部と通信できない問題の対処

## いきなり対処法

`update-alternatives --config iptables`で，`iptables-legacy`を使うよう変更する．

```bash
$ sudo update-alternatives --config iptables
alternative iptables (/usr/sbin/iptables を提供) には 2 個の選択肢があります。

  選択肢    パス                     優先度  状態
------------------------------------------------------------
* 0            /usr/sbin/iptables-nft      20        自動モード
  1            /usr/sbin/iptables-legacy   10        手動モード
  2            /usr/sbin/iptables-nft      20        手動モード

現在の選択 [*] を保持するには <Enter>、さもなければ選択肢の番号のキーを押してください: 1
update-alternatives: /usr/sbin/iptables (iptables) を提供するためにマニュアルモードで /usr/sbin/iptables-legacy を使います

$ sudo reboot
```

## いきなり結論

- Debian 10はiptables v1.8系を採用．nftables APIを使う`iptables-nft`と旧来の`iptables-legacy`が用意されている．
- デフォルトは`iptables-nft`だが，Dockerは`iptables-legacy`を使う．
- この状態で，`ufw`などを使って`iptables-nft`で定義を設定すると，`iptables-legacy`の定義は無視される（ようだ）．その結果，外部との通信ができない．
- そこで，`iptables-legacy`をデフォルトに変更し，定義をlegacy側に寄せて対処する．

## 詳細

### ufwとiptables

[ufw - Uncomplicated Firewall](https://help.ubuntu.com/community/UFW) は， `ufw allow 22/tcp` などと書くだけでポート開放ができる，お手軽なファイアウォール管理ツールです．

内部的には `iptables`(Netfilter) のラッパーのような位置づけで， `iptables` の忘れやすいコマンド体系を忘れたままにしておけます．もともとUbuntu向けのツールだったように思いますが，かなり前からDebianでも利用が可能です．

### iptables v1.8 - nftables APIサポート

現時点で testing である Debian GNU/Linux 10 (buster) では，iptablesが1.6から1.8にバージョンアップされています．

- [iptables 1.8.0 release [LWN.net]](https://lwn.net/Articles/759184/)

iptables 1.8では，新たに `nftables` Kernel API[^1]を用いた`iptables-nft`ツールが提供され，旧来のツールは`iptables-legacy`で使用することが出来るようになっています．

`iptables`はというと，Debian 10はデフォルトで`iptables-nft`へのシンボリックリンクが張られています．`ufw`等はこのデフォルトを使用します．

```bash
$ ls -l /usr/sbin/iptables
lrwxrwxrwx 1 root root 26  2月 17 11:44 /usr/sbin/iptables -> /etc/alternatives/iptables
$ ls -l /etc/alternatives/iptables
lrwxrwxrwx 1 root root 22  2月 18 22:53 /etc/alternatives/iptables -> /usr/sbin/iptables-nft
```

### Dockerは `iptables-legacy` を使う

一方，Dockerは`iptables-nft`がうまく扱えず，デフォルトがどうあれ必ず`iptables-legacy`を使うように修正が加えられています[^2] [^3]．

- [Docker doesn't work with iptables v1.8.1 · Issue #38099 · moby/moby](https://github.com/moby/moby/issues/38099)
- [debian has iptables-legacy and iptables-nft now by myobie · Pull Request #2285 · docker/libnetwork](https://github.com/docker/libnetwork/pull/2285)

### ufw + Docker = iptables-nft + iptables-legacy

ここで，デフォルトのまま`ufw`とDockerを使うと，`iptables-nft`と`iptables-legacy`の両方の定義が存在することになります．

```bash
$ sudo iptables-nft -L
Chain INPUT (policy DROP)
target     prot opt source               destination
ufw-before-logging-input  all  --  anywhere             anywhere
ufw-before-input  all  --  anywhere             anywhere
ufw-after-input  all  --  anywhere             anywhere
ufw-after-logging-input  all  --  anywhere             anywhere
ufw-reject-input  all  --  anywhere             anywhere
ufw-track-input  all  --  anywhere             anywhere
（省略）

$ sudo iptables-legacy -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy DROP)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-1  all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
（省略）
```

ここから先は推測が入りますが，この状態になると `iptables-nft` の定義のみが有効となるようです．そのため，Dockerがせっかく書き換えてくれた定義が効かず，コンテナが外部と通信できなくなってしまいます．

`ping`でIPアドレスを指定すると通るが，ホスト名を指定するとDNSが引けない，という状況です．

```bash
$ sudo docker run -it --rm busybox
/ # ping github.com
ping: bad address 'github.com'
/ # ping 192.30.255.112
PING 192.30.255.112 (192.30.255.112): 56 data bytes
64 bytes from 192.30.255.112: seq=0 ttl=49 time=133.961 ms
64 bytes from 192.30.255.112: seq=1 ttl=49 time=134.677 ms
```

### 対処: `iptables-legacy`をデフォルトにする

さて，対処法ですが，`ufw`も含めてみんな`iptables-legacy`を使うようにします．

[Docker doesn't work with iptables v1.8.1 · Issue #38099 · moby/moby](https://github.com/moby/moby/issues/38099#issuecomment-434894468)のIssue commentにもあるように，デフォルトの`iptables`は選べるようになっています．そこで，`iptables-legacy`をデフォルトに変更します．

```bash
$ sudo update-alternatives --config iptables
alternative iptables (/usr/sbin/iptables を提供) には 2 個の選択肢があります。

  選択肢    パス                     優先度  状態
------------------------------------------------------------
* 0            /usr/sbin/iptables-nft      20        自動モード
  1            /usr/sbin/iptables-legacy   10        手動モード
  2            /usr/sbin/iptables-nft      20        手動モード

現在の選択 [*] を保持するには <Enter>、さもなければ選択肢の番号のキーを押してください: 1
update-alternatives: /usr/sbin/iptables (iptables) を提供するためにマニュアルモードで /usr/sbin/iptables-legacy を使います
```

あとは再起動すれば完了です．

`iptables-nft`での定義が残っていても，`iptables-legacy`をデフォルトとしていると，legacyが優先されるようです．

## 参考

- [Debian -- buster の iptables パッケージに関する詳細](https://packages.debian.org/ja/buster/iptables)
- [iptables 1.8.0 release [LWN.net]](https://lwn.net/Articles/759184/)
- [Docker doesn't work with iptables v1.8.1 · Issue #38099 · moby/moby](https://github.com/moby/moby/issues/38099)
- [debian has iptables-legacy and iptables-nft now by myobie · Pull Request #2285 · docker/libnetwork](https://github.com/docker/libnetwork/pull/2285)

[^1]: API自体は2014年1月にリリースされたLinux Kernel 3.13から提供されている
[^2]: 本来`iptables-nft`でも同じコマンドで通るはずなのですが，謎．Pull requestには『[トランスレータのバグかもね？ 追っかけきれてないけど😅](https://github.com/docker/libnetwork/pull/2285#issuecomment-435171584)』みたいなコメントが見られる．
[^3]: nftablesサポートについてはかなり前からIssueが上がっている様子([[feature request] nftables support · Issue #26824 · moby/moby](https://github.com/moby/moby/issues/26824))．