Help us understand the problem. What is going on with this article?

Dockerのbridge接続の通信フローをnftablesで追う

はじめに

いまさらながらCentOS8でDockerを勉強しはじめました。で、外部ホスト<--->コンテナのパケットの流れを追ってみたくていろいろ調べました。

けど、リクエストは流れが追えたのですが、レスポンスが追えていません。。。
ひとまずリクエストの経路だけまとめます。

いろいろ前提条件

検証環境

以下の環境でトレースしています。

  • CentOS8(8.1.1911)
  • Docker(19.03.5)
  • firewalld(0.7.0)
  • iptables(1.8.2:nf_tables)
  • nftables(0.9.0)

Docker動作環境

userland proxy(docker-proxy)

iptables / nftables によるdockerネットワークの基本的な動作をみたかったので、docker-pxoryを利用していません。(ヘアピンNAT)
以下のファイルを配置して検証しています。

/etc/docker/daemon.json
{
    "userland-proxy": false
}

参考:https://github.com/nigelpoulton/docker/blob/master/docs/userguide/networking/default_network/binding.md

docker network and containers

検証環境として、以下のエントリで生成されたものを利用します。
CentOS8で構成したdockerホスト 10.254.10.252 、説明はradiusを対象にしていきます。

Docker Composeでネットワークサービス群を5分で作れるようにした(dhcp/radius/proxy/tftp/syslog)

Screenshot from Gyazo

以下のコンテナが生成されます。

server app address listen
proxy squid 172.20.0.2 8080/tcp
syslog rsyslog 172.20.0.3 514/udp
radius freeRADIUS 172.20.0.4 1812/udp
dhcp ISC-Kea 172.20.0.5 67/udp
tftp tftp-server - 69/udp
# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
b11308767849        infraserv:proxy     "/usr/sbin/init"    3 minutes ago       Up 3 minutes        0.0.0.0:8080->8080/tcp   proxy
33054f8b7d58        infraserv:tftp      "/usr/sbin/init"    35 hours ago        Up 2 hours                                   tftp
851ea861d04e        infraserv:syslog    "/usr/sbin/init"    35 hours ago        Up 2 hours          0.0.0.0:514->514/udp     syslog
dd3a657cfda2        infraserv:dhcp      "/usr/sbin/init"    35 hours ago        Up 2 hours          0.0.0.0:67->67/udp       dhcp
7249b9c4f11d        infraserv:radius    "/usr/sbin/init"    35 hours ago        Up 2 hours          0.0.0.0:1812->1812/udp   radius

以下のパラメータのネットワークが生成されます。

key value
name infraserv_infranet
subnet 172.20.0.0/24
interface docker1

tftpは --net=host な環境で動作しているため、 docker network は以下のような状態です。

# docker network inspect infraserv_infranet
[
    {
        "Name": "infraserv_infranet",
        "Id": "7ed8face2e4fec3110384fa3366512f8c78db6e10be6e7271b3d92452aefd254",
        "Created": "2020-02-15T05:37:59.248249755-05:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.20.0.0/24",
                    "Gateway": "172.20.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "7249b9c4f11de1f986892965671086d20957a6021269a5f5bc6dd85263bc0d70": {
                "Name": "radius",
                "EndpointID": "03ae6a9b9ff7817eea101955d2d6ff016982beb65c7dd6631c75c7299682c2dd",
                "MacAddress": "02:42:ac:14:00:04",
                "IPv4Address": "172.20.0.4/24",
                "IPv6Address": ""
            },
            "851ea861d04edeb5f5c2498cc60f58532c87a44592db1f6c51280a8ce27940bd": {
                "Name": "syslog",
                "EndpointID": "d18e466d27def913ac74b7555acc9ef79c88c62e62085b50172636546d2e72bb",
                "MacAddress": "02:42:ac:14:00:03",
                "IPv4Address": "172.20.0.3/24",
                "IPv6Address": ""
            },
            "b11308767849c7227fbde53234c1b1816859c8e871fcc98c4fcaacdf7818e89e": {
                "Name": "proxy",
                "EndpointID": "ffa6479b4f28c9c1d106970ffa43bd149461b4728b64290541643eb895a02892",
                "MacAddress": "02:42:ac:14:00:02",
                "IPv4Address": "172.20.0.2/24",
                "IPv6Address": ""
            },
            "dd3a657cfda211c08b7c5c2166f10d189986e4779f1dfea227b3afe284cbafec": {
                "Name": "dhcp",
                "EndpointID": "7371f4cf652d8b1bdbf2dc1e5e8ae97013a9a70b890c2caa36c2a7cc93b165df",
                "MacAddress": "02:42:ac:14:00:05",
                "IPv4Address": "172.20.0.5/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker1"
        },
        "Labels": {
            "com.docker.compose.network": "infranet",
            "com.docker.compose.project": "infraserv",
            "com.docker.compose.version": "1.25.3"
        }
    }
]

アドレスファミリ

説明を簡略するために、IPv4に絞っています。

Dockerでのパケットの流れについて

通信を追ってみる(radiusの場合)

今回は外部端末(10.254.10.105)から、Dockerホスト(10.254.10.252)あてにradiusのRequestを送付することを例にします。
自ホストに着信した後に転送されるので、注目するchainのhookは prerouting --> forward --> postrouting となります。
そのため、chainのtypeは、filterとnatだけ、に絞って説明します。

ルールは nft list ruleset から 不要なものを除外してますが、あまり有用な情報でもないので、補足にまとめました。

外部端末からのリクエスト(prerouting)

nft list ruleset からhookがpreroutingのものを抽出すると、以下となります。

         table ip nat {
           chain PREROUTING {
(1)          type nat hook prerouting priority -100; policy accept;
(2)->        fib daddr type local COUNTER jump DOCKER
           }
   ->(2)   chain DOCKER {
      ↓      meta l4proto udp udp dport 514 COUNTER dnat to 172.20.0.3:514
      ↓      meta l4proto udp udp dport 67 COUNTER dnat to 172.20.0.5:67
      ↓      meta l4proto tcp tcp dport 8080 COUNTER dnat to 172.20.0.2:8080
     (3)     meta l4proto udp udp dport 1812 COUNTER dnat to 172.20.0.4:1812
           }
         }

現時点での通信は 10.254.10.105:random --> 10.254.10.252:1812 となります。
(1) preroutingをhookしてnatを行うPREROUTINGというchainが選択される
(2) DstAddrはlocalなので、DOCKERというchainに飛ぶ
  addr type localは自ホスト(この場合はDockerホスト)が持つアドレスのことで、
  今回ならlo:127.0.0.1 ens192:10.254.10.252 docker1:172.20.0.1 のことです。
(3) DstPortは1812なので、DstAddrを172.20.0.4:1812にDNATする
引き続きの処理がないため、policy適用 -> accept

この時点の通信は 10.254.10.105:random --> 172.20.0.4:1812 となります。
宛先が172.20.0.4に変更されたため、 routing decision により forward の hook へ進むことになります。

外部端末からのリクエスト(forward)

nft list ruleset からhookがforwardのものを抽出すると、以下となります。

                                table ip filter {
                                  chain FORWARD {
(1)                                 type filter hook forward priority 0; policy drop;
(2)->                               COUNTER jump DOCKER-USER
        ->(3)(4)->                  COUNTER jump DOCKER-ISOLATION-STAGE-1
                    ->(5)           oifname "docker1" ct state related,established COUNTER accept
                      (6)->         oifname "docker1" COUNTER jump DOCKER
                                    iifname "docker1" oifname != "docker1" COUNTER accept
                                    iifname "docker1" oifname "docker1" COUNTER accept
                                  }
               ->(4)              chain DOCKER-ISOLATION-STAGE-1 {
                 (5)->              COUNTER return
                                  }
   ->(2)                          chain DOCKER-USER {
     (3)->                          COUNTER return
                                  }
                         ->(6)    chain DOCKER {
                            ↓       iifname != "docker1" oifname "docker1" meta l4proto udp ip daddr 172.20.0.3 udp dport 514 COUNTER accept
                            ↓       iifname != "docker1" oifname "docker1" meta l4proto udp ip daddr 172.20.0.5 udp dport 67 COUNTER accept
                            ↓       iifname != "docker1" oifname "docker1" meta l4proto tcp ip daddr 172.20.0.2 tcp dport 8080 COUNTER accept
                           (7)      iifname != "docker1" oifname "docker1" meta l4proto udp ip daddr 172.20.0.4 udp dport 1812 COUNTER accept
                                  }                          
                                }
                                table inet firewalld {
                                  chain filter_FORWARD {
                           (8)      type filter hook forward priority 10; policy accept;
                            ↓       ct state established,related accept
                           (9)      ct status dnat accept
                                    iifname "lo" accept
                                    jump filter_FORWARD_IN_ZONES
                                    jump filter_FORWARD_OUT_ZONES
                                    ct state invalid drop
                                    reject with icmpx type admin-prohibited
                                  }
                                  chain filter_FORWARD_IN_ZONES {
                                    iifname "ens192" goto filter_FWDI_public
                                    goto filter_FWDI_public
                                  }
                                  chain filter_FORWARD_OUT_ZONES {
                                    oifname "ens192" goto filter_FWDO_public
                                    goto filter_FWDO_public
                                  }
                                  chain filter_FWDI_public { meta l4proto { icmp, ipv6-icmp } accept }
                                  chain filter_FWDO_public { jump filter_FWDO_public_allow }
                                  chain filter_FWDO_public_allow { ct state new,untracked accept }
                                }

現時点での通信は 10.254.10.105:random --> 172.20.0.4:1812 となります。
(1)forwardのhookの中で最も優先順位が高いので、filterを行うFORWARDというchainが選択される(pri:0)
(2)無条件にDOCKER-USERに飛ぶ
(3)なにもせず戻る
(4)無条件にDOCKER-ISOLATION-STAGE-1に飛ぶ
(5)なにもせず戻る
(6)出力IFはdocker1なので、DOCKERに飛ぶ
(7)入力IFはens192、出力IFはdocker1、DstAddrは172.20.0.4:1812なので、accept
  regular chain のDOCKERはbase chainのFORWARDから呼び出されている。
  DOCKER でacceptした時点で呼び出し元のFORWARDの評価がされ、このchainは終了する。
(8)forwardのhookの中で2番目に優先順位が高いので、filterを行うfilter_FORWARDというchainが選択される(pri:10)
(9)パケットはDNATされているので、accept
この時点の通信は最初と変わらず 10.254.10.105:random --> 172.20.0.4:1812 となります。

外部端末からのリクエスト(postrouting)

nft list ruleset からhookがpostroutingのものを抽出すると、以下となります。

                   table ip nat {
                     chain POSTROUTING {
(1)                    type nat hook postrouting priority 100; policy accept;
 ↓                     oifname "docker1" fib saddr type local COUNTER masquerade
 ↓                     oifname != "docker1" ip saddr 172.20.0.0/24 COUNTER masquerade
 ↓                     meta l4proto udp ip saddr 172.20.0.3 ip daddr 172.20.0.3 udp dport 514 COUNTER masquerade
 ↓                     meta l4proto udp ip saddr 172.20.0.5 ip daddr 172.20.0.5 udp dport 67 COUNTER masquerade
 ↓                     meta l4proto tcp ip saddr 172.20.0.2 ip daddr 172.20.0.2 tcp dport 8080 COUNTER masquerade
 ↓                     meta l4proto udp ip saddr 172.20.0.4 ip daddr 172.20.0.4 udp dport 1812 COUNTER masquerade
                     }
                     table ip firewalld {
                       chain nat_POSTROUTING {
(2)                    type nat hook postrouting priority 110; policy accept;
(3)->                    jump nat_POSTROUTING_ZONES
                       }
   ->(3)               chain nat_POSTROUTING_ZONES {
      ↓                  oifname "ens192" goto nat_POST_public
     (4)->               goto nat_POST_public
                       }
        ->(4)          chain nat_POST_public {
          (5)->          jump nat_POST_public_allow
                       }
             ->(5)     chain nat_POST_public_allow {
               (6)       oifname != "lo" masquerade
                       }
                     }
                   }

現時点での通信は 10.254.10.105:random --> 172.20.0.4:1812 となります。
(1) postroutingのhookの中で最も優先順位が高いのでnatを行うPOSTROUTINGというchainが選択される(pri:100)
  引き続きの処理がないため、policy適用 -> accept
(2) postroutingのhookの中で2番目に優先順位が高いのでnatを行うnat_POSTROUTINGというchainが選択される(pri:110)
(3) 無条件にnat_POSTROUTING_ZONESに飛ぶ
(4) 無条件にnat_POST_publicに飛ぶ
(5) 無条件にnat_POST_public_allowに飛ぶ
(6) 出力IFはdocker1なので、masquerade
  gotoで呼び出された先でchainが終了するため、policy適用 -> accept
  regular chain のnat_POST_public_allowはregular chain のnat_POST_publicから呼び出されている。
  regular chain のnat_POST_publicはregular chain のnat_POSTROUTING_ZONESからgoto命令で呼び出されている。
  goto命令で呼び出されたnat_POST_publicの処理が終了した時点で、呼び出したnat_POSTROUTING_ZONESが終了し
  それを呼び出したnat_POSTROUTINGも終了しpolicy accept が適用される。

masqueradeの処理を受け、最終的には 172.20.0.1:random --> 172.20.0.4:1812 となります。
(docker1から送出されるため、masqueradeで処理されると、送信元アドレスがdocker1になります)

radiusによる認証可否

radiusコンテナが受け取るリクエスト
172.20.0.1:random --> 172.20.0.4:1812

radiusサーバはその可否をチェックし、radiusクライアントに返答を返します。

radiusコンテナが返答するレスポンス
172.20.0.4:1812 --> 172.20.0.1:random

外部端末へのレスポンス

力尽きました。。。
nftablesでカウンタを仕掛けてみると、以下のchainを通過する際のアドレスが見えました。
1回の認証のやり取りなので、各chainで1パケットが見えていました。

type filter hook prerouting  : 172.20.0.4:1812 --> 172.20.0.1:random
type filter hook input       : 172.20.0.4:1812 --> 10.254.10.105:random
type filter hook forward     : 172.20.0.4:1812 --> 10.254.10.105:random
type filter hook postrouting : 172.20.0.4:1812 --> 10.254.10.105:random

radiusコンテナからの返答は、172.20.0.4:1812 --> 172.20.0.1:random であり、
着信時は自分宛の通信に見えるから、hook:inputを通過しているのは分かります。
その後、LocalProcessを通ってforwardに行く、のでしょうか?このあたりからよくわからなくなってしまいました。。。

中途半端になってしまった。。。

radiusからの応答パケットの経路がいまひとつわからない。
なぜどのchainの type:nat も通らないんだろう。。。
なぜ hook:inputhook:forward を同時に通っているんだろう。。。
table bridge filter の type:filter hook:input pri:-200 に入ってるのに
table ip filter の type:filter hook:input pri:0 には入って行ってないんだよなぁ。
L2のブリッジとL3のIPで違う処理をしているとか?

出典

https://knowledge.sakura.ad.jp/22636/
https://ja.wikipedia.org/wiki/Iptables
https://ja.wikipedia.org/wiki/Nftables
https://wiki.archlinux.jp/index.php/Nftables
https://wiki.archlinux.jp/index.php/Iptables
https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks
https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#TRAVERSINGOFTABLES
https://wiki.archlinux.jp/index.php/Nftables
https://knowledge.sakura.ad.jp/22636/
https://www.codeflow.site/ja/article/a-deep-dive-into-iptables-and-netfilter-architecture

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした