TCPでDockerの標準入出力を提供したい

追記2

続き → [SSHのたびにDockerコンテナを立ち上げる \- Qiita](https://qiita.com/kusano_k/items/173876c8b6c24246d156)

追記

`docker`のプロセスを殺せれば、コンテナも死ぬと思っていたけど、勘違いだった。これは`--rm -d`が同時に指定できるようになったという話だった。そもそも`docker`を殺してもコンテナが停止しないのでダメ。

https://github.com/moby/moby/issues/30341

<hr>

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">そういえば、いつかのGoogle CTFでncで繋ぐとsshで繋いだときのように好きにコマンドが実行できて、しかも他の参加者はいないという問題があった。あれ、どうやっていたんだろう。xinetdでdocker～を実行すれば良いのか？</p>&mdash; kusanoさん@がんばらない (@kusano_k) <a href="https://twitter.com/kusano_k/status/1303374373273784320?ref_src=twsrc%5Etfw">September 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これをやりたい。何をされても、Docker内なので終了すれば綺麗に消える。xinetdは標準入出力を読み書きするプログラムをTCPサーバーとして公開してくれるデーモン。

Google CTF 2018予選の「Filter Env」だったかな。

[Google CTF \(2018\): Beginners Quest \- PWN Solutions \(2/2\) \- Jack Hacks](https://jhalon.github.io/2018-google-ctf-beginners-pwn-solutions-2/#filter-env)

ソースコードは公開されているけれど、肝心の動かす部分は無し。

https://github.com/google/google-ctf/tree/master/2018/beginners/pwn-env

この問題ではプロンプト（`[root@54ed35399771 /]#`）が無いけど、分かりづらいのでできれば欲しい。こうしたい。

```shell-session
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`グループに所属させておく。

```shell-session
useradd docker_xinetd
gpasswd -a docker_xinetd docker
```

xinetdをインストールして有効にしておく。設定ファイルは次の通り。

```/etc/xinetd.d/docker
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
}
```

実行しているシェルスクリプトは、

```/home/docker_xinetd/docker.sh
socat - 'system:timeout -s KILL --foreground 60 docker run --rm -it centos\:8 /bin/sh -c \"stty -echo; bash\",pty'
kill -KILL 0
```

以降は、なぜこんなに面倒なことになっているのかという話。

## 基本

素直に書くとこうなる。

```/etc/xinetd.d/docker
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
}
```

結果はこう。

```shell-session
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`の警告は警告なので無視しても良いし、設定で環境変数を追加すれば消える。

```/etc/xinetd.d/docker
    env = HOME=/home/docker_xinetd
```

## TTY

> the input device is not a TTY

TTYが何かを知らなかったのでググる。この記事が分かりやすかった。

[TTY/PTYに関するクイズ \- Qiita](https://qiita.com/angel_p_57/items/ff1c0d054714b7982ca5)

「`-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かのように動かせる。なので、とりあえず動かすなら、

```/etc/xinetd.d/docker
    server_args = run --rm -i centos:8 bash -i
```

で良い。警告と入力したコマンドが二重に表示されるのが気になるけれど、一応動く。

```shell-session
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を使えばできるらしい（他にもっと楽な方法は無いだろうか……）。

```shell-session
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'`と認識されているっぽい。たぶん、`'`を特別扱いするような処理は無い。

https://github.com/xinetd-org/xinetd/blob/e7c1ba41f4f86b436fb82b0d55cd5d387bd4ecc4/libs/src/str/strparse.c#L119

ということで、コマンドをシェルスクリプトに書いて、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は、新たにプロセスグループを作り、タイムアウト時はそのプロセスグループに属するプロセス全部にシグナルを送るという挙動をする。

https://github.com/coreutils/coreutils/blob/master/src/timeout.c

`timeout hoge | fuga | piyo`みたいな使い方をしたときや、`hoge`が処理中に`fuga`や`piyo`を立ち上げたときに、`hoge`だけではなく`fuga`や`piyo`も終了させるためらしい。副作用として、timeoutと起動したコマンドが、起動元のシェルとは別プロセスグループになってしまう。シェルスクリプト中で`kill -KILL 0`したときに止まらなくなって困るので、`--foreground`を付ける。

プロセスグループの上位概念にセッションというものがある。xinetdはプロセスグループもセッションも新しく作るが、timeoutはセッションはそのまま。自分と同じセッションのプロセスを殺すという手もありそうだけど、面倒。