centOS8でDockerコンテナから名前解決ができない話をnftコマンドで解決する

# はじめに
centOS8にてDocker動かそうと思ったら下記エラーが出て名前解決ができず、カスタムイメージが作れなかった問題を解決できたのでメモ

```bash
$ docker run -ti --rm busybox nslookup google.com
nslookup: write to '<ホスト指定のDNS>': No route to host
;; connection timed out; no servers could be reached
```
# TL:DR
結論から言えばCentOS8からデフォルトとなったNFTablesにおいて下記コマンドを実施し、コンテナ内部からのICMP以外のパケットがドロップされないようにする。

``` bash
nft add rule inet firewalld filter_FWDI_public_allow counter udp dport 53 accept
```

もしくはもっと簡潔に`--net=host`オプションをつける方法や`firewall-cmd`を使う方法[(CentOS8にDockerをインストール。名前解決できなかったのが解消した。)](https://qiita.com/bashaway/items/4001c0ad5a6f21957986)、バックエンドをiptablesに置き換える方法[(CentOS8でDocker CEを使うのは(現状は)やめとけという話)](https://blog.potproject.net/2020/03/25/centos8-docker-ce)などがあるようです。いろいろなアプローチ方法があります。


# 前提条件
筆者の環境は、centos8をVirtualBOXにminimalインストールした下記の通りです。

``` 
$ cat /etc/os-release 
NAME="CentOS Linux"
VERSION="8 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="8"
PLATFORM_ID="platform:el8"
PRETTY_NAME="CentOS Linux 8 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:8"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-8"
CENTOS_MANTISBT_PROJECT_VERSION="8"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="8"
```

```
$ nft --version
nftables v0.9.0 (Fearless Fosdick)
```

事の成り行きから話すと、まずcentos8にDockerをインストール後(そもそも違うの使えというのが上で挙げた物ですが）、自作イメージをビルドしようとしたら名前解決ができずにエラー。
Dockerコンテナ内からpingはホストのGatewayに通るが、名前解決しようとするとicmp type3 (Destination Unreachable)が帰ってくる。
iptablesでUDPパケットの到達点を地道に調べてみても[(参考：Netfilterの処理順序)](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg)、POSTROUTING targetに到達する直前にはfilter tableのFORWORD targetにてACCEPTされているハズなのにそこでどこかに消えている。。。


# 解決
centos8になったことでnetfileterに対するインターフェースがiptablesからnftables, firewalldになっているということだったので試しにnftコマンドによってフィルタリングルールを見てみるとrejectしている奴がいました。

```bash
$ nft list ruleset | grep -n reject
244:		reject with icmpx type admin-prohibited
252:		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable
258:		reject with icmpx type admin-prohibited
264:		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable
```
今回hook forwardしているchainを知りたいので、上記のrejectを行うルールを含むchainを見てみると
ADDRESS FAMILY: inet
TABLE         : firewalld
CHAIN         : filter_FORWARD
のものがどうやらdocker0からのパケットをrejectしてしまっているようです(iptablesで定義されているTABLEとは異なるため、nftじゃないと参照できない)。

```bash
$ nft list chain inet firewalld filter_FORWARD
table inet firewalld {
	chain filter_FORWARD {
		type filter hook forward priority 10; policy accept;
		ct state established,related accept
		ct status dnat accept
		iifname "lo" accept
		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable
		jump filter_FORWARD_IN_ZONES_SOURCE
		jump filter_FORWARD_IN_ZONES
		jump filter_FORWARD_OUT_ZONES_SOURCE
		jump filter_FORWARD_OUT_ZONES
		ct state invalid drop
		reject with icmpx type admin-prohibited # ここまでにacceptされなければrejectされてしまう
	}
}
```
ちなみにフィルタリングの評価順序についてはこちらの[開発元のWiki](https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_priority)を参考にすると、priorityの値が小さい順にBase Chain(typeとhookが定められているChain)で評価されていって、dropされない限りは全てのChainを通過するようです。
つまりは上記のchainのなかでrejectされる前のどこかでacceptしてしまえばDNS用のパケットは通過できるようになるハズ（その他の部分ではacceptされるようになっていることを確認済）。
ルールを定めるため、jumpなどからそれっぽいところを辿っていくとfilter_FWDI_public_allowというchainを見つけました。

```bash
$ nft list chain inet firewalld filter_FORWARD_IN_ZONES
table inet firewalld {
	chain filter_FORWARD_IN_ZONES {
		iifname "enp0s3" goto filter_FWDI_public
		goto filter_FWDI_public
	}
}
```

```bash
$ nft list chain inet firewalld filter_FWDI_public
table inet firewalld {
	chain filter_FWDI_public {
		jump filter_FWDI_public_pre
		jump filter_FWDI_public_log
		jump filter_FWDI_public_deny
		jump filter_FWDI_public_allow
		jump filter_FWDI_public_post
		meta l4proto { icmp, ipv6-icmp } accept # pingだけ通るようになっていた部分の設定
	}
}
```

```bash
$ nft list chain inet firewalld filter_FWDI_public_allow
table inet firewalld {
	chain filter_FWDI_public_allow {
	}
}
```

二つ目の、filter_FWDI_publicでicmpはacceptしているからpingだけ通るのだなと納得。
そういうわけで冒頭のコマンドを持ってしてDNS用のパケットをここでacceptするようにすると、無事コンテナ内部から名前解決できるようになりました

```bash
$ nft add rule inet firewalld filter_FWDI_public_allow counter udp dport 53 accept
$ nft list chain inet firewalld filter_FWDI_public_allow
table inet firewalld {
	chain filter_FWDI_public_allow {
		counter packets 0 bytes 0 udp dport domain accept #<-追加されたもの
	}
}
```

```bash
$ docker run -ti --rm busybox nslookup google.com
Server:		<ホスト指定のDNS>
Address:	<ホスト指定のDNS>:53

Non-authoritative answer:
Name:	google.com
Address: 172.217.25.238

$ nft list chain inet firewalld filter_FWDI_public_allow
table inet firewalld {
	chain filter_FWDI_public_allow {
		counter packets 2 bytes 112 udp dport domain accept # counterをつけているため、パケットが通過したことが分かる
	}
}
```
ちなみに同様の手順でhttpを許可するとコンテナ内部でパッケージマネージャも動作するようになります。

```bash
$ nft add rule inet firewalld filter_FWDI_public_allow tcp dport { 80,443 } accept
$ nft list chain inet firewalld filter_FWDI_public_allow
table inet firewalld {
	chain filter_FWDI_public_allow {
		counter packets 0 bytes 0 udp dport domain accept
		tcp dport { http, https } accept #<-追加されたもの
	}
}

$ docker run -ti --rm alpine apk add openssl
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
(1/1) Installing openssl (1.1.1g-r0)
Executing busybox-1.31.1-r9.trigger
OK: 6 MiB in 15 packages
```
めでたしめでたし

## 追加調査：Netfilterの評価順について
上記で問題は解決しましたが、nftについて少し学んだので合わせてメモ
centos8インストールした状態で、hook forwardに纏わるチェーンは以下のように設定されていました。


```bash
$ nft list chains | sed 's/^}$/}\n/' | awk 'BEGIN{RS="";FS="\n"} { for(i=0;i<NF;i++){ if( match($i, "chain") && match($(i+1), "forward") )  {print $1, $i, $(i+1)} } }'
table ip filter { 	chain FORWARD { 		type filter hook forward priority 0; policy drop;
table ip6 filter { 	chain FORWARD { 		type filter hook forward priority 0; policy accept;
table bridge filter { 	chain FORWARD { 		type filter hook forward priority -200; policy accept;
table ip security { 	chain FORWARD { 		type filter hook forward priority 150; policy accept;
table ip mangle { 	chain FORWARD { 		type filter hook forward priority -150; policy accept;
table ip6 security { 	chain FORWARD { 		type filter hook forward priority 150; policy accept;
table ip6 mangle { 	chain FORWARD { 		type filter hook forward priority -150; policy accept;
table inet firewalld { 	chain filter_FORWARD { 		type filter hook forward priority 10; policy accept;
```
本稿の上部で触れたPriorityの話と照らし合わせると、forwardされるパケットは
bridge filter -> ip(ip6) mangle -> ip(ip6) filter -> inet firewalld -> ip(ip6) security
の順で通過するハズである。
テストのため、下記のようにログを取得するルールを追加する。

```bash
$ while read AF T C ORD;do
> nft insert rule $AF $T $C counter log prefix \""$ORD FORWARD>> "\"
> done<<EOF
> bridge         filter         FORWARD        1st
> ip             mangle         FORWARD        2nd
> ip6            mangle         FORWARD        3rd
> ip             filter         FORWARD        4th
> ip6            filter         FORWARD        5th
> inet           firewalld      filter_FORWARD 6th
> ip             security       FORWARD        7th
> ip6            security       FORWARD        8th
> EOF
```
ログ機能が追加されたことを確認。

```bash
$ nft list ruleset | grep -v '^$' | sed 's/^}$/}\n/' | awk 'BEGIN{RS="";FS="\n"} { for(i=0;i<NF;i++){ if( match($i, "chain") && match($(i+1), "forward") && match($(i+2), "log"))  {print $1, $i, $(i+1),$(i+2)} } }'
table ip filter {       chain FORWARD {                 type filter hook forward priority 0; policy drop;               counter packets 0 bytes 0 log prefix "4th FORWARD>> "
table ip6 filter {      chain FORWARD {                 type filter hook forward priority 0; policy accept;             counter packets 0 bytes 0 log prefix "5th FORWARD>> "
table bridge filter {   chain FORWARD {                 type filter hook forward priority -200; policy accept;          counter packets 0 bytes 0 log prefix "1st FORWARD>> "
table ip security {     chain FORWARD {                 type filter hook forward priority 150; policy accept;           counter packets 0 bytes 0 log prefix "7th FORWARD>> "
table ip mangle {       chain FORWARD {                 type filter hook forward priority -150; policy accept;          counter packets 0 bytes 0 log prefix "2nd FORWARD>> "
table ip6 security {    chain FORWARD {                 type filter hook forward priority 150; policy accept;           counter packets 0 bytes 0 log prefix "8th FORWARD>> "
table ip6 mangle {      chain FORWARD {                 type filter hook forward priority -150; policy accept;          counter packets 0 bytes 0 log prefix "3rd FORWARD>> "
table inet firewalld {  chain filter_FORWARD {          type filter hook forward priority 10; policy accept;            counter packets 0 bytes 0 log prefix "6th FORWARD>> "
```

hook forwardさせるため、コンテナ内部から各種通信を行う。
まずはコンテナからホストネットワークへの疎通確認(ping)。ここでは上記の中からアドレスファミリーとしてip, inetが選択されるため、その中のテーブルを通過することになる。そのためsyslogには2nd, 4th, 6th, 7thのログが出力されることになる。

```bash
### in container
$ ping 192.168.13.1
```

```bash
### in host
$ tail -n0 -f /var/log/messages | sed 's/... .. ..:..:../MON DD hh:mm:ss/'
MON DD hh:mm:ss centos8 kernel: 2nd FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0 
MON DD hh:mm:ss centos8 kernel: 4th FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0 
MON DD hh:mm:ss centos8 kernel: 6th FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0 
MON DD hh:mm:ss centos8 kernel: 7th FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0
```

次にipv6でのコンテナ間疎通確認。
ここではアドレスファミリーとしてbridge（dockerによって自動生成されたブリッジデバイス(docker0)をvethデバイス(vethxxxxx)で通過し、forwardするため）, ip6, inetが選択されるハズなので、本稿で設定した中では 1st, 3rd, 5th, 6th, 8thのhook forwardを経由する。
dockerではipv6はデフォルトでdisableになっているので、[こちら](https://docs.docker.com/network/bridge/#use-ipv6)に従い各種設定が必要。
バージョンの違いかデフォルトのデバイス（docker0）にipv6のアドレスプールが割り当てられなかったが、直接プールを指定して作成したらなんとかできた。

```bash
### in host
# daemonの設定
$ vim /etc/docker/daemon.json
#　下記の設定を書き加える、もしくはデーモン起動オプションに指定
{
 "ipv6": true
}

# 設定のリロード
$ systemctl reload docker

# docker networkの作成
$ docker network create  --ipv6 --subnet fe80:0:0:42:ff::/80 mynetv6

# 上記ネット内でコンテナを二台立ち上げる
$ docker run --rm -ti --net mynetv6 --name C1 busybox sh 
$ docker run --rm -ti --net mynetv6 --name C2 busybox sh 

### in container C1
$ ping -6 C2

```

```bash
### in host
$  tail -n0 -f /var/log/messages | sed 's/... .. ..:..:../MON DD hh:mm:ss/'
MON DD hh:mm:ss centos8 kernel: 1st FORWARD>> IN=veth08a234c OUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000:0000:0002 DST=ff02:0000:0000:0000:0000:0000:000
0:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0                                                                                                                                      
MON DD hh:mm:ss centos8 kernel: 3rd FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000:
0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0                                                                                       
MON DD hh:mm:ss centos8 kernel: 5th FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000:
0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0                                                                                       
MON DD hh:mm:ss centos8 kernel: 6th FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000:
0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0                                                                                       
MON DD hh:mm:ss centos8 kernel: 8th FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000:
0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0 
```

というわけで、無事全ての設定したhook forwardを通過し、想定通りの動作をしていることが確かめられました。

# 終わりに
元の問題はnftablesとdockerの不整合でしたが、どうせならnftables扱えるようになろうという事で取り組み始めました。ネットワークはやはり奥が深いですね。
記事書くに当たりiptables, nftables, firewalldの関係性が曖昧でしたが、[netfilterとfirewalldとiptablesとnftablesの関係](https://qiita.com/bashaway/items/e405d59d92670fbc5341)の記事がわかりやすくまとめてくださっていたので、ここまで来てくださった方がいたら是非ご覧ください。