コンテナを増やしたいのに

1次予選でも出題したコンテナへのポートフォワード問題です。本問題のコンテナはLXCからDockerへ変わりました。コンテナ型の仮想化には基本的にiptablesのラッパーが搭載されています。構成ファイルやコマンドに引数を指定するだけでポートフォワードできるのはこのためです。 今回は特殊な環境でトラブルシューティングを行う想定だったため、素のiptablesとebtablesを触る問題になりました。

問題文  

Dockerコンテナでウェブサーバを動かしているVMに、新しくコンテナを追加することになった。 docker run -d -p 8080:80 --name web2 nginx を実行してコンテナを立ち上げたいのだが、コンテナに疎通が取れないという。 原因を究明して新しく追加されたコンテナに疎通が取れるようにしてほしい。

初期状態  

  • 手元のPCから curl 192.168.13.1 を実行すると正常に応答する
  • VMで docker start web2 を実行したあと手元のPCから curl 192.168.13.1:8080 を実行しても正常に応答しない

終了状態  

  • VMを再起動しても以下が成り立つ
    • 手元のPCから curl 192.168.13.1 を実行すると正常に応答する
    • VMで docker start web2 を実行するとVMの8080番ポートが web2 コンテナの80番ポートへフォワードされ、手元のPCから curl 192.168.13.1:8080 を実行すると正常に応答する

アクセス情報  

VM名ホスト名ユーザパスワード
VM192.168.13.1userPlzComeToICTSC2020

解説  

docker start web2 をすると特にエラーは返ってこず、正常にコンテナが起動していることはわかるかと思います。

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS
  NAMES
cb9075111a78        nginx               "nginx -g 'daemon of…"   About an hour ago   Up 1 second         0.0.0.0:8080->80/tcp
  web2
d732a7925959        nginx               "nginx -g 'daemon of…"   3 hours ago         Up 3 minutes        0.0.0.0:80->80/tcp
  web1

そこで iptables を見ると DOCKER-USER チェインに以下のルールが入っていることがわかります。

Chain DOCKER-USER (1 references)
target     prot opt source               destination
ACCEPT     all  --  anywhere             vm
ACCEPT     tcp  --  anywhere             10.213.213.213       tcp dpt:http
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DROP       all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere

Dockerはiptablesを使ってポートフォワード等を実行しますが DOCKER-USER はDockerの作ったルールの前に適用されるチェインで、ユーザが利用するためのものです(参考: https://docs.docker.com/network/iptables/)。

これを見ると 10.213.213.213 へのパケット転送以外は基本的に許可されていないことがわかります。 このIPアドレスは web1 コンテナのものです。つまり web1 以外へのパケット転送を許可していないということです。 そこで web2 コンテナにもパケットの転送が許可されるようにルールを編集します。 例えば、以下のようにすればいかなる場合でもパケットの転送が許可されます。

sudo iptables -I DOCKER_USER 1 -j ACCEPT

しかし、これだけでは問題が解決しません。実はebtablesというパケットフィルタがARPパケットを落としているからです。 sudo ebtables -L とすると以下のルールが見えます。

Bridge table: filter

Bridge chain: INPUT, entries: 4, policy: ACCEPT
-i eth0 -j ACCEPT
-p ARP --arp-ip-src 10.213.213.213 --arp-ip-dst 10.213.213.1 -j ACCEPT
-p ARP --arp-ip-src 10.213.213.1 --arp-ip-dst 10.213.213.213 -j ACCEPT
-p ARP -j DROP

Bridge chain: FORWARD, entries: 0, policy: ACCEPT

Bridge chain: OUTPUT, entries: 4, policy: ACCEPT
-o eth0 -j ACCEPT
-p ARP --arp-ip-src 10.213.213.213 --arp-ip-dst 10.213.213.1 -j ACCEPT
-p ARP --arp-ip-src 10.213.213.1 --arp-ip-dst 10.213.213.213 -j ACCEPT
-p ARP -j DROP

これによると 10.213.213.1 と 10.213.213.213 の間でしかARPのやり取りができないことがわかります。 これは 10.213.213.1 は web1 コンテナが繋がっているブリッジです。

最も手っ取り早い方法としては sudo ebtables -F としてebtablesのルールを消し去ってしまうことです。

問題の終了条件としては、再起動しても web2 へ疎通が取れる必要がありました。 iptablesとebtablesのルールは通常再起動すれば消えますが、この問題ではそうではありません。

/etc/systemd/system/secure-firewall.service というサービスがVMの起動時に自動でルールを書き込むようになっています。 このサービスは /opt/secure_firewall.sh を実行するようになっています。 したがって secure-firewall.service を無効化するか secure_firewall.sh を書き換えて、パケットフィルタへの変更が永続化されるようにします。

本来は、既存のiptables/ebtablesの設定にならって、必要最小限の条件を許可するように設定するべきだと考えられますが、この問題では終了状態が達成されるようにルールを編集していればよしとしました。

環境  

Dockerコンテナ起動  

docker network create -d bridge my-net --gateway 10.213.213.1 --subnet 10.213.213.0/24
docker run --restart=always -p 80:80 --network=my-net  --ip=10.213.213.213 --mac-address=02:42:ac:11:00:02 --name web1 nginx

設定永続化のためサービスを定義  

/etc/systemd/system/secure-firewall.service

[Unit]
Description=secure firewall
After=docker.service
[Service]
ExecStart=/opt/secure_firewall.sh
[Install]
WantedBy=multi-user.target
sudo systemctl enable secure-firewall

設定スクリプト  

/opt/secure_firewall.sh

#!/bin/bash
iptables -I DOCKER-USER 1 -j DROP
iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -I DOCKER-USER 1 -p tcp --dport 80  -d 10.213.213.213  -j ACCEPT
iptables -I DOCKER-USER 1 -d 10.213.213.1  -j ACCEPT
ebtables -A INPUT -i eth0 -j ACCEPT
ebtables -A INPUT -p ARP --arp-ip-src 10.213.213.213 --arp-ip-dst 10.213.213.1 -j ACCEPT
ebtables -A INPUT -p ARP --arp-ip-src 10.213.213.1 --arp-ip-dst 10.213.213.213 -j ACCEPT
ebtables -A INPUT -p ARP -j DROP
ebtables -A OUTPUT -o eth0 -j ACCEPT
ebtables -A OUTPUT -p ARP --arp-ip-src 10.213.213.213 --arp-ip-dst 10.213.213.1 -j ACCEPT
ebtables -A OUTPUT -p ARP --arp-ip-src 10.213.213.1 --arp-ip-dst 10.213.213.213 -j ACCEPT
ebtables -A OUTPUT -p ARP -j DROP

終わりに  

皆さんiptablesについては報告してくださっていましたが、ebtablesについては2チームしか発見できていなかったようです。 あまり素で触ることのない技術ですが、楽しんでいただければ幸いです。