LoginSignup
1
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-22

はじめに

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の関係の記事がわかりやすくまとめてくださっていたので、ここまで来てくださった方がいたら是非ご覧ください。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2