docker composeプロジェクト内のコンテナでtcpdumpを実行するスクリプトを書く

はじめに

network programming - How to capture packets for single docker container - Stack Overflowで紹介されている2つの方法を試してみたのでメモです。

1つ目の方法:nsenterを使う

対象のコンテナのpidを取得します。

docker inspect --format "{{ .State.Pid }}" "$CONTAINER_ID_OR_NAME"

nsenter-tでターゲットの指定し、-nでnetwork namespace`に入ってtcpdumpコマンドを実行します。

sudo nsenter -n -t "$PID" tcpdump -i any -U -w "$OUTPUT_FILE" "$FILTER"

nsenterの実行にroot権限が必要なため、sudoを使っています。

2つ目の方法:dockerの--netオプションを使う

docker run -it --rm container:$CONTAINER_ID_OR_NAME utils/tcpdump -i any -U -w "$OUTPUT_FILE" "$FILTER"

--net container:$CONTAINER_ID_OR_NAMEについてはNetworking | Docker DocsContainer networksに記載がありました。

また、関連する話として、docker composeのRelease notes1.2.0に以下の記載がありました(今回は試してないですが、docker composeのYAMLファイルを書き換えてtcpdumpを実行するコンテナーを追加するというのもできそう)。

A service can now share another service’s network namespace with net: container:<service>.

tcpdumpのDockerイメージはutils/tcpdumpにしました。 docker-utilities/tcpdump: Docker image with tcpdumpにDockerfileがあり、内容もalpineでtcpdumpパッケージを追加するだけとミニマムです。

実際の例:coraza-caddyのテスト

corazawaf/coraza-caddy: OWASP Coraza middleware for Caddy. It provides Web Application Firewall capabilitiesftwディレクトリにdocker composeを使ってテストを実行するためのファイル群があります。

テスト実行中にftw、caddy、backendと3つのコンテナ内の通信に対してtcpdumpを実行し、テスト終了時にkillするシェルスクリプトを書いてみました。

nsenterでtcpdumpを実行するスクリプト

#!/bin/sh
set -eu

# Cache sudo credential before running nsenter and tcpdump in the background.
sudo -p "[sudo] password for $USER to run nsenter and tcpdump: " -s :

bg_pids=""

capture() {
  service="$1"
  filter="$2"

  container="$(docker compose ps --format '{{.Name}}' $service)"
  c_pid=$(docker inspect --format "{{ .State.Pid }}" "$container")
  sudo nsenter -n -t $c_pid tcpdump -i any -U -w "${service}.pcap" "$filter" 2>/dev/null &
  bg_pids="$bg_pids $!"
}

stop_capture() {
  kill $bg_pids
}

trap stop_capture EXIT

crs_ver=$(go list -m -f '{{.Version}}' github.com/corazawaf/coraza-coreruleset/v4)
docker compose build --pull --build-arg CRS_VERSION=$crs_ver

docker compose up -d
docker compose pause

capture ftw 'tcp port 8080'
capture caddy 'tcp port (8080 or 8081)'
capture backend 'tcp port (8080 or 8081)'

docker compose unpause

# note this process terminates after running docker compose down.
(docker compose logs -f --no-log-prefix ftw 2>&1 | tee ../build/run-ftw.log > /dev/null) &

set +e
docker compose wait --down-project ftw
rc=$?
echo service "ftw" exited with status code $rc
exit $rc

以下補足説明。

dockerでtcpdumpを実行するスクリプト

dockerコマンドはsudo無しで実行できるので、sudoのパスワード入力が不要な分こちらのほうが使いやすいです。

#!/bin/sh
set -eu

bg_pids=""

capture() {
  service="$1"
  filter="$2"

  container="$(docker compose ps --format '{{.Name}}' $service)"
  docker run --rm -v ../build:/home --net container:${container} utils/tcpdump -i any -U -w /home/${service}.pcap "$filter" 2>/dev/null &
  bg_pids="$bg_pids $!"
}

stop_capture() {
  echo $bg_pids | xargs -r kill
}

trap stop_capture EXIT

crs_ver=$(go list -m -f '{{.Version}}' github.com/corazawaf/coraza-coreruleset/v4)
docker compose build --pull --build-arg CRS_VERSION=$crs_ver

docker compose up -d
docker compose pause

capture ftw 'tcp port 8080'
capture caddy 'tcp port (8080 or 8081)'
capture backend 'tcp port (8080 or 8081)'

docker compose unpause

# note this process terminates after running docker compose down.
(docker compose logs -f --no-log-prefix ftw 2>&1 | tee ../build/run-ftw.log > /dev/null) &

set +e
docker compose wait --down-project ftw
rc=$?
echo service "ftw" exited with status code $rc
exit $rc

汎用化はあえてしてないです

スクリプトの引数をサービス1 フィルター1 サービス2 ファイルター2 ...のような形にして汎用化できるかもと一瞬思いましたが、呼び出すコマンドラインが長くなって結局ラッパースクリプトを書きたくなるので、それだったらdocker composeのプロジェクト毎に上記のようなスクリプトを書き換えて使うほうが良いなと思ったので。