追記2
続き → SSHのたびにDockerコンテナを立ち上げる - Qiita
追記
docker
のプロセスを殺せれば、コンテナも死ぬと思っていたけど、勘違いだった。これは--rm -d
が同時に指定できるようになったという話だった。そもそもdocker
を殺してもコンテナが停止しないのでダメ。
そういえば、いつかのGoogle CTFでncで繋ぐとsshで繋いだときのように好きにコマンドが実行できて、しかも他の参加者はいないという問題があった。あれ、どうやっていたんだろう。xinetdでdocker~を実行すれば良いのか?
— kusanoさん@がんばらない (@kusano_k) September 8, 2020
これをやりたい。何をされても、Docker内なので終了すれば綺麗に消える。xinetdは標準入出力を読み書きするプログラムをTCPサーバーとして公開してくれるデーモン。
Google CTF 2018予選の「Filter Env」だったかな。
Google CTF (2018): Beginners Quest - PWN Solutions (2/2) - Jack Hacks
ソースコードは公開されているけれど、肝心の動かす部分は無し。
この問題ではプロンプト([root@54ed35399771 /]#
)が無いけど、分かりづらいのでできれば欲しい。こうしたい。
kusano@RIO:~$ nc docker.example.com 10000
[root@54ed35399771 /]# id
uid=0(root) gid=0(root) groups=0(root)
[root@54ed35399771 /]# ls -al /
total 56
drwxr-xr-x 1 root root 4096 Feb 23 11:01 .
drwxr-xr-x 1 root root 4096 Feb 23 11:01 ..
-rwxr-xr-x 1 root root 0 Feb 23 11:01 .dockerenv
:
他の要望として、
-
exit
で終了せずにCtrl+Cでnc
を終了した場合に、Dockerのプロセスが残っていては困る - 一定の時間が経過したら強制終了してほしい
意外と面倒だったのでやりかたをメモしておく。
どの程度意味があるのかは分からないが、念のため、Dockerを立ち上げるユーザー(docker_xinetd
)を作って、docker
グループに所属させておく。
useradd docker_xinetd
gpasswd -a docker_xinetd docker
xinetdをインストールして有効にしておく。設定ファイルは次の通り。
service docker
{
type = UNLISTED
socket_type = stream
protocol = tcp
port = 10000
user = docker_xinetd
env = HOME=/home/docker_xinetd
server = /bin/sh
server_args = /home/docker_xinetd/docker.sh
wait = no
}
実行しているシェルスクリプトは、
socat - 'system:timeout -s KILL --foreground 60 docker run --rm -it centos\:8 /bin/sh -c \"stty -echo; bash\",pty'
kill -KILL 0
以降は、なぜこんなに面倒なことになっているのかという話。
基本
素直に書くとこうなる。
service docker
{
type = UNLISTED
socket_type = stream
protocol = tcp
port = 10000
user = docker_xinetd
server = /bin/docker
server_args = run --rm -it centos:8
wait = no
}
結果はこう。
kusano@RIO:~$ nc docker.example.com 10000
WARNING: Error loading config file: .dockercfg: $HOME is not defined
the input device is not a TTY
$HOME
の警告は警告なので無視しても良いし、設定で環境変数を追加すれば消える。
env = HOME=/home/docker_xinetd
TTY
the input device is not a TTY
TTYが何かを知らなかったのでググる。この記事が分かりやすかった。
「-i
なら入力を受け付ける。-t
なら出力する」と思っていたけど、違うらしい。-i
はあっている。-t
が違う。そういえばサンプルで良く見るdocker run --rm hello-world
には-t
が無い。docker help run
によると、
-t, --tty Allocate a pseudo-TTY
確認してみると、-t
を指定すると入力も出力もTTYになる。docker
の入出力がTTYでない場合に、-t
は通るのに-it
が通らないのが不思議。出力はDockerが何とかするけど、入力はダメということなのだろうか。
今回やろうとしていることでTTYが無いと困るのは、bashがプロンプトを出さないことと、docker
がシグナルを受け付けないこと(これは後述)。
bashについて、-i
オプションを付ければ、無理矢理TTYかのように動かせる。なので、とりあえず動かすなら、
server_args = run --rm -i centos:8 bash -i
で良い。警告と入力したコマンドが二重に表示されるのが気になるけれど、一応動く。
kusano@RIO:~$ nc docker.example.com 10000
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
[root@8d4c76b698da /]# id
id
uid=0(root) gid=0(root) groups=0(root)
[root@8d4c76b698da /]# exit
exit
exit
まあ、この状態か、プロンプトを出すのを諦めるのが楽だと思う。
socatでTTYを割り当てる。
たぶんTTYでないものを無理にTTYとして扱おうとして警告が出ているのだろう。また、二重になっているもののうち1個は(たぶん)nc
が出しているのでどうしようもない。Dockerが出しているほうを消すしかない。stty -echo
で消せる。ただしTTYなら。
ということで、何とかしてTTYを割り当てたい。socatを使えばできるらしい(他にもっと楽な方法は無いだろうか……)。
socat - 'system:docker run --rm -it centos\:8,pty'
これで、標準入出力(-
)とDockerの入出力(system:docker...
)を繋いでくれる。末尾の,pty
がポイントで、これでsocatがTTYを作ってくれる。socatでは:
が特殊文字なので\
でエスケープ。
このコマンドをxinetdの設定ファイルに直接書けないという問題がある。'system:...,pty
が1個の引数なのに、これをそのままserver_args
に書くと、'system:docker
, run
, …, centos\:8,pty'
と認識されているっぽい。たぶん、'
を特別扱いするような処理は無い。
ということで、コマンドをシェルスクリプトに書いて、xinetdにはそのスクリプトを指定する。
socatはTCPとコマンドの標準入出力を繋ぐことができるので、もはやxinetdは要らない気がするが……デーモンのほうが何となく安心。
入力したコマンドが二重になるのを消す
stty -echo
してからbash
を立ち上げれば良い。エスケープしているのはsocatのため。"
はsocatの特殊文字ではない気がするけど、エスケープいるのか。
/bin/sh -c \"stty -echo; bash\"
接続終了時にDockerが終了しなくなる
socatを通すとncをCtrl+Cで止めたときにDockerのプロセスが残る。てっきり接続終了時はxinetdが起動しているプログラムを終了してくれると思っていたのだけど、そういうことはしていないらしい。標準入出力に割り当てられているソケットが閉じられるので、それを見てプログラムが自分で終了する。試しに、sleep 9999
とかをxinetdで起動してみると、これも同じようにプロセスが残る。
xinetdでDockerを起動したときや、socatにpty
を付けなかった場合は問題無い。疑似TTY周りで何かあるのだろうか。
socat自身は終了するので、シェルスクリプトでsocatの直後にkill -KILL 0
を追加。-KILL
なのはSIGTERM
だと-t
を付けていない場合にDockerが無視して終了しないから。今回は付けているけれど、それでも中でsleep 9999
とかしていたりすると終了しない。良く分からん。まあ、-KILL
しておいて悪いことはないだろう。0で自分自身の属するプロセスグループ全体にシグナルを送る。xinetdから起動された時点で新規のプロセスグループが作られ、以降の子プロセスは同じグループになるので、これでDockerも殺せる。
以前はdocker
のプロセスを殺してもDockerのインスタンス自体は動き続けることがあったけど、インスタンスを止めるのがDockerデーモンの仕事になったから今は無理矢理止めても大丈夫……みたいな話をどこかで見たような気がしたけど、見失ってしまった。
タイムアウト
timeout -s KILL --foreground 60 ...
timeout 60 command
で、command
を実行して60秒後もまだ動いていたらtimeoutがシグナルを送ってくれる。1h
とか30m
みたいな指定も可。ここもデフォルトのTERM
だと効かないときがあるので、KILL
。
--foreground
。これが無い場合、timeoutは、新たにプロセスグループを作り、タイムアウト時はそのプロセスグループに属するプロセス全部にシグナルを送るという挙動をする。
timeout hoge | fuga | piyo
みたいな使い方をしたときや、hoge
が処理中にfuga
やpiyo
を立ち上げたときに、hoge
だけではなくfuga
やpiyo
も終了させるためらしい。副作用として、timeoutと起動したコマンドが、起動元のシェルとは別プロセスグループになってしまう。シェルスクリプト中でkill -KILL 0
したときに止まらなくなって困るので、--foreground
を付ける。
プロセスグループの上位概念にセッションというものがある。xinetdはプロセスグループもセッションも新しく作るが、timeoutはセッションはそのまま。自分と同じセッションのプロセスを殺すという手もありそうだけど、面倒。