はじめに
centOS8にてDocker動かそうと思ったら下記エラーが出て名前解決ができず、カスタムイメージが作れなかった問題を解決できたのでメモ
$ 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以外のパケットがドロップされないようにする。
nft add rule inet firewalld filter_FWDI_public_allow counter udp dport 53 accept
もしくはもっと簡潔に--net=host
オプションをつける方法やfirewall-cmd
を使う方法(CentOS8にDockerをインストール。名前解決できなかったのが解消した。)、バックエンドをiptablesに置き換える方法(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の処理順序)、POSTROUTING targetに到達する直前にはfilter tableのFORWORD targetにてACCEPTされているハズなのにそこでどこかに消えている。。。
解決
centos8になったことでnetfileterに対するインターフェースがiptablesからnftables, firewalldになっているということだったので試しにnftコマンドによってフィルタリングルールを見てみるとrejectしている奴がいました。
$ 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じゃないと参照できない)。
$ 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を参考にすると、priorityの値が小さい順にBase Chain(typeとhookが定められているChain)で評価されていって、dropされない限りは全てのChainを通過するようです。
つまりは上記のchainのなかでrejectされる前のどこかでacceptしてしまえばDNS用のパケットは通過できるようになるハズ(その他の部分ではacceptされるようになっていることを確認済)。
ルールを定めるため、jumpなどからそれっぽいところを辿っていくとfilter_FWDI_public_allowというchainを見つけました。
$ 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
}
}
$ 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だけ通るようになっていた部分の設定
}
}
$ 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するようにすると、無事コンテナ内部から名前解決できるようになりました
$ 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 #<-追加されたもの
}
}
$ 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を許可するとコンテナ内部でパッケージマネージャも動作するようになります。
$ 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に纏わるチェーンは以下のように設定されていました。
$ 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
の順で通過するハズである。
テストのため、下記のようにログを取得するルールを追加する。
$ 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
ログ機能が追加されたことを確認。
$ 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のログが出力されることになる。
### in container
$ ping 192.168.13.1
### 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になっているので、こちらに従い各種設定が必要。
バージョンの違いかデフォルトのデバイス(docker0)にipv6のアドレスプールが割り当てられなかったが、直接プールを指定して作成したらなんとかできた。
### 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
### 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の関係の記事がわかりやすくまとめてくださっていたので、ここまで来てくださった方がいたら是非ご覧ください。