Docker のネットワークまわりの理解を深める一環として iptables について整理します。 全体として Iptables Tutorial がとても参考になりました。

  1. iptables とは
  2. iptables の構成要素
  3. Docker デーモン起動時の iptables
  4. Docker コンテナ起動時の iptables

環境:

# OS
$ cat /proc/version 
Linux version 4.14.13-1-ARCH (builduser@heftig-32336) (gcc version 7.2.1 20171224 (GCC)) #1 SMP PREEMPT Wed Jan 10 11:14:50 UTC 2018

# iptables
$ iptables --version
iptables v1.6.1

# Docker
$ docker version
Client:
 Version:	18.01.0-ce
 API version:	1.35
 Go version:	go1.9.2
 Git commit:	03596f51b1
 Built:	Sun Jan 14 23:10:39 2018
 OS/Arch:	linux/amd64
 Experimental:	false
 Orchestrator:	swarm

Server:
 Engine:
  Version:	18.01.0-ce
  API version:	1.35 (minimum version 1.12)
  Go version:	go1.9.2
  Git commit:	03596f51b1
  Built:	Sun Jan 14 23:11:14 2018
  OS/Arch:	linux/amd64
  Experimental:	false

iptables とは

iptables は Linux でファイアウォールやルータ設定を行うために使用するツールであり、Netfilter というパケット処理用モジュールのフロントエンドとして働くものです。

iptables に変わるものとして nftables や CentOS 7 以降では firewalld が使用されたりもしますが、まだ一般的には iptables が主流なのかなと思います (例えば Docker もまだ nftables サポートは feature request 状態みたいですし)。

iptables の構成要素

iptables の理解に重要な概念としてルール、チェイン、テーブルがあります。

ルールとは iptables におけるファイアウォールの具体的な設定です。例えば「ループバックインタフェースから送信されたパケットならば全て許可する」といったものです。ルールは

  • プロトコル (prot)
  • 受信元インタフェース (in)
  • 送信先インタフェース (out)
  • 受信元 IP (の範囲) (source)
  • 送信先 IP (の範囲) (destination)
  • パケットに対するアクション (e.g. 許可する、許可しない) (target)

といったもので構成されます。上で例に挙げたルールの場合はそれぞれ

  • prot: all
  • in: lo
  • out: *
  • source: 0.0.0.0/0
  • destination: 0.0.0.0/0
  • target: ACCEPT

のようになります。言い換えればルールはパケットに対するアクション (target) とそれを適用するための条件をセットにしたものです。

ルール単体でも簡単なパケットの制御はできそうですが、iptables ではテーブルとチェインという概念により更に複雑な設定が可能になります。テーブルは iptables を構成する最も大きな括りであり、filter、nat、raw、mangle、security の 5 つが存在します。よく使用されるのは filter と nat で、それぞれパケットフィルタリング、NAT (Network Address Translation) ルールを定義するためのものです。テーブルは複数のチェインを持ち、チェインによりどのタイミングでパケットにルールを適用するかが決定されます。テーブル、チェイン、ルールの構造は以下のようにまとめられます。

iptables structure

実際に各チェイン、ルールがどのように適用されるかは以下のフローチャートでまとめられます。ここでは ArchWiki の iptables に倣って filter, nat テーブルに関連するチェインのみを取り上げています。

flow chart of iptables

パケットはこのフローチャートにそって各チェインを通過し、チェインで定義されたルールが順番に評価、適用されます。具体的にはあるマシン上で考えられるパケットの送受信 3 パターンについてはそれぞれ以下のようになります。

  • (1) 自分から外部にパケットを送信する (下図 blue)
  • (2) 外部からパケットを受信しかつそれが自分宛て (下図 red)
  • (3) 外部からパケットを受信したが自分宛てではなく外部へフォワーディングする (下図 green)

flow chart of iptables specifying three way of packets

ルールを設定する際には、この図を見ることでどのチェインに追加するべきかがわかります。例えば自分宛てのパケットに対してフィルタリングをかけたい際には filter テーブルの INPUT にルールを設定します。

iptables に関しては以下の資料を参考にしました。

Docker デーモン起動時の iptables

Docker ではコンテナ間、コンテナホスト間、コンテナから外部ネットワーク、といった通信の制御を iptables で行っています。Docker デーモン起動前後に iptables を確認すると filter, nat テーブルにいくつか新規にルールが定義されることがわかります。

filter テーブル

Docker デーモン起動後の filter テーブルでは主に FORWARD チェインへルールが定義されます。

# Show filter table after starting Docker daemon.
$ iptables -nvL --line-number
Chain INPUT (policy ACCEPT 309 packets, 49021 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain FORWARD (policy DROP 0 packets, 0 bytes)
num pkts bytes target     prot opt in     out     source               destination         
1      0     0 DOCKER-USER  all  --  *      *       0.0.0.0/0            0.0.0.0/0           
2      0     0 DOCKER-ISOLATION  all  --  *      *       0.0.0.0/0            0.0.0.0/0           
3      0     0 ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
4      0     0 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0           
5      0     0 ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0           
6      0     0 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0           

Chain OUTPUT (policy ACCEPT 277 packets, 21141 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination         

Chain DOCKER-ISOLATION (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0           

Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0           

FORWARD チェインに定義されたルールを上から順番に見ていきます。ホスト上の FORWARD チェインに定義されるようなルールなので、主に管理するコンテナにどのような通信を許可するか、を定めたものになります。

  • num 1: まずは必ず DOCKER-USER チェインにいく
    • ターゲットで Docker が追加したチェインが指定されているので、そちらでルールの評価が行われる
    • 今回の場合 DOCKER-USER チェインはすぐに元のチェインに戻る RETURN ルールのみが定義されている
      • つまり何もしない
    • もし Docker が追加したルールを上書きするような挙動を追加したい場合、ここに定義するのがよい
  • num 2: DOCKER-ISOLATION チェインにいく
    • Docker ネットワーク間の通信を制限するためのルールがここに定義される
    • いまは docker0 ブリッジネットワークしか存在しないので何もしない
  • num 3: docker0 インタフェース行きで ctstate RELATED, ESTABLISHED なパケットは許可
    • ctstate については後述
  • num 4: それ以外の docker0 インタフェース行きは DOCKER チェインで判断
    • いまはここでも何もしない
  • num 5: docker0 インタフェースから来てそれ以外のインタフェースに行くパケットは許可
    • 例えばコンテナから外部ネットワークに行くようなもの
    • ただし異なる Docker ネットワークに行くようなものは DOCKER-ISOLATION チェインで制限される
  • num 6: docker0 インタフェースから来て docker0 インタフェースに行くパケットは許可する
    • いわばコンテナ間通信
  • 上記のいずれにも当てはまらない場合、チェインで定義したデフォルトポリシーが適用される
    • ここでは DROP

もう少しルール 3 の定義に含まれる ctstate を見ていきます。ctstate は iptables 拡張の一つ conntrack により提供される (というより Linux カーネルの一部?) 機能で、通信状態による条件付けを提供します。ctstate が管理する状態には NEW, ESTABLISHED, RELATED, INVALID といったものがあります。

iptables がどのように接続状態を判断するのかですが、どうやら主要な通信プロトコル (e.g. TCP, UDP, and ICMP) についてはこの状態なら ctstate は NEW, ESTABLISHED, あるいは RELATED であるという定義をそれぞれで与えているようです。それ以外のプロコルに対しても初回の通信なのか両方向で送受信したものかで ctstate を設定するような挙動になるとされています。

ここでいう接続状態については /proc/net/nf_conntrack で確認できます (Linux OS のバージョンによって多少パスが異なりそう)。iptables は nat テーブルの PREROUTING, OUTPUT チェインで適宜この情報の更新を行います。

# To see ICMP connetion in conntrack
$ ping -c 3 google.co.jp
PING google.co.jp (216.58.220.227) 56(84) bytes of data.
64 bytes from nrt13s37-in-f3.1e100.net (216.58.220.227): icmp_seq=1 ttl=54 time=27.4 ms
...

$ cat /proc/net/nf_conntrack | grep icmp
ipv4     2 icmp     1 27 src=192.168.11.10 dst=216.58.220.227 type=8 code=0 id=2090 src=216.58.220.227 dst=192.168.11.10 type=0 code=0 id=2090 mark=0 zone=0 use=2

上のルールでは ctstate が ESTABLISHED, RELATED のときに ACCEPT するとしていますが、これは既に自分が知っている接続に関するものなので許可してもいいよ、ということを定義するのだと考えれば良さそうです。

ctstate については以下の内容を参考にしました。

nat テーブル

続いて Docker が nat テーブルに定義するルールについて見ていきます。

# Show nat table
$ iptables -nvL -t nat
Chain PREROUTING (policy ACCEPT 34 packets, 3186 bytes)
 pkts bytes target     prot opt in     out     source               destination         
   17  1007 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 84 packets, 5316 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 84 packets, 5316 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0           

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0 
  • PREROUTING チェイン
    • ADDRTYPE match dst-type LOCAL のときに DOCKER チェインへいく
      • addrtype については後述
  • OUTPUT チェイン
    • PREROUTING チェインと同様
    • ただしパケットの行き先がループバックアドレスならば該当しない
  • POSTROUTING チェイン
    • 172.17.0.0/16 は docker0 ブリッジネットワークのセグメント
    • このネットワークから外部のネットワークに行くものに対し MASQUERADE する
      • MASQUERADE については後述

ADDRTYPE についてですが、これはカーネルがパケットの分類に使用する情報であり、例えば LOCAL, UNICAST, BLOADCAST といったタイプがあるようです。調べたり触っている感じ LOCAL というのはループバックアドレスのような自身から自身への通信のようなものなのかなと思うのですがちょっと自信がないです。また各タイプの意味はレイヤー 3 プロトコルによって異なるらしいのでそこも注意が必要になります。

ADDRTYPE の概要は以下が参考になりました。

MASQUERADE (IP マスカレード) ですがこれは Linux で実装された場合の名称で、一般的には NAPT (Network Address and Port Translation) と呼ばれます。NAT には送信元アドレスを変換する SNAT, 送信先を返還する DNAT がありますが、マスカレードは SNAT の特殊なケースになります。

iptables 上では SNAT (MASQUERADE) は nat テーブルの POSTROUTING チェイン、DNAT は PREROUTING チェインで定義します。

NAT, NAPT については以下を参考にしました。

Docker コンテナ起動時の iptables

次にコンテナを起動した際の iptables への変化を見ていきます。ただ単純にコンテナを起動するだけだと特に変わりがないので、 -p オプション設定時の挙動を見ることにします。

# Run container with -p option
$ docker container run -d -p 80:80 dockersamples/static-site
$ docker container ls -a
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                         NAMES
4be67466de54        dockersamples/static-site   "/bin/sh -c 'cd /usr…"   17 seconds ago      Up 16 seconds       0.0.0.0:80->80/tcp, 443/tcp   mystifying_poincare

これによりホスト上のポート 80 にアクセスすることでコンテナ上ポート 80 で公開している Web サーバにアクセスできます。

このようにコンテナを起動すると、filter テーブルの DOCKER チェインに新規ルールが追加されます。

$ iptables -nvL
...
Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     tcp  --  !docker0 docker0  0.0.0.0/0            172.17.0.2           tcp dpt:80
...

このルールでは「TCP で docker0 以外のインタフェースから docker0 インタフェースへ行くパケットで、172.17.0.2:80 が送信先のもの」が許可されています。

filter テーブルの DOCKER チェインは FORWARD チェイン内

 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0   

のルールで参照されるチェインです。

コンテナ起動前は docker0 ブリッジネットワーク外部から docker0 行きのパケットは ctstate RELATED, ESTABLISHED なものしか許可していなかったので、コンテナ 80 番ポートへのアクセスを許可するにはこのようなルールが必要になることがわかります。

また nat テーブルにも POSTROUTING, DOCKER チェインに 2 つのルールが追加されます。

$ iptables -nvL -t nat
Chain PREROUTING (policy ACCEPT 30 packets, 1761 bytes)
 pkts bytes target     prot opt in     out     source               destination         
 1265 74405 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 226 packets, 14411 bytes)
 pkts bytes target     prot opt in     out     source               destination         
 2023  121K DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 253 packets, 15551 bytes)
 pkts bytes target     prot opt in     out     source               destination         
 1021 61260 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0           
    0     0 MASQUERADE  tcp  --  *      *       172.17.0.2           172.17.0.2           tcp dpt:80 <- 増えた

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0           
    3   180 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:80 ← 増えた

このうち DOCKER チェインに追加された DNAT ルールが実際にコンテナ 80 番ポートにポートフォワーディングするルールになります。ルール自体を見ると TCP で送信先ポートが 80 というけっこうざっくりしたルールに見えるのですが、これは DOCKER チェインを参照するルール側で条件やタイミングを絞っているからなのかなと思います。

POSTROUTING チェインの方に追加された方は具体的にどの場合に対応するのかよくわかりません。見た感じ Web サーバを動かすコンテナ (IP 172.17.0.2 が割り当てられているもの) 自身からホストのポート 80 を叩きに来た場合なんかが当てはまるのかなと思ったのですが、実際試してみると違いそうでした。