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）](https://qiita.com/bashaway/items/3e308cb217bbb225d11c)**

[![Screenshot from Gyazo](https://gyazo.com/1951188d1551e87ad3bbe20cc5255988/raw)](https://gyazo.com/1951188d1551e87ad3bbe20cc5255988)


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

|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` から 不要なものを除外してますが、あまり有用な情報でもないので、[補足](#必要なtableの確認)にまとめました。

### 外部端末からのリクエスト（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の中で２番目に優先順位が高いので、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の中で２番目に優先順位が高いので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を通過する際のアドレスが見えました。
１回の認証のやり取りなので、各chainで１パケットが見えていました。

```
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:input` と `hook: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