はじめに
いまさらながら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)
以下のファイルを配置して検証しています。
{
"userland-proxy": false
}
docker network and containers
検証環境として、以下のエントリで生成されたものを利用します。
CentOS8で構成したdockerホスト 10.254.10.252
、説明はradiusを対象にしていきます。
Docker Composeでネットワークサービス群を5分で作れるようにした(dhcp/radius/proxy/tftp/syslog)
以下のコンテナが生成されます。
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: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