Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

sshuttleのssh.pyは、いかにしてssh先で起動したpythonとの双方向通信を確立するか

仕事ではテレワーク環境を作るためにsshuttleを使っているのだけれど、中身はあまり読み込んでいないので、勉強のためにちょっと中身を読んでみたところ勉強になる箇所を見付けたのでメモ。

ssh.py

sshuttleは、踏み台となるホストに対してsshセッションを張り、それを介してパケットを転送していくのだけれど、そのブートストラップにあたる箇所には以下のようなコードがある。

    (s1, s2) = socket.socketpair()

    def setup():
        # runs in the child process
        s2.close()
    s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
    s1.close()

    debug2('executing: %r' % argv)
    p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
                          close_fds=True, stderr=stderr)
    os.close(s1a)
    os.close(s1b)
    s2.sendall(content)
    s2.sendall(content2)
    return p, s2

github.com

この箇所が最終的に達成したいことは、リモートホスト上のSSHで起動したPythonスクリプトの標準入出力と接続されたsocketを作成することだ(なぜsocketかというと、一度接続が確立された後は、転送したいパケットをそのまま流していくためだ)。 これが如何に達成されるか、順を追って見ていく。

まず、(s1, s2) = socket.socketpair()によってソケットペアが作成される。ソケットペアとは、どちらかにデータを投入したらもう片方から出てくるように繋がったソケットのこと。つまり、s1にデータを入れるとs2から受信できて、逆もまた然り。s1s2のいずれも双方向である。

graph LR
subgraph python
s1
s2
s1 .-> s2
s2 .-> s1
end

次にsetup() が定義されているが、これは子プロセスから呼ばれるのでいったん飛ばす。

そして、s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())によって、s1のファイルディスクリプタは複製される。なぜ2つに複製するのかは、のちほど判明する(ネタバレ: 入力と出力のために分業させるためだ ---- ソケットは双方向通信可能でも、ファイルはそうではない)。複製しても、ソケットペアのつながりは維持される。

graph LR
subgraph python
s1
s2
s1a
s1b
s1 .-> s2
s2 .-> s1
s1a .-> s2
s2 .-> s1a
s1b .-> s2
s2 .-> s1b
end

次に、s1.close()によってs1が閉じられる。使うのはs1as1bだけであり、s1自体は使用せず、複製の雛形としてのみ使っているようだ。

graph LR
subgraph python
s2
s1a
s1b
s1a .-> s2
s2 .-> s1a
s1b .-> s2
s2 .-> s1b
end

そしてsshプロセスが起動される。

p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
                          close_fds=True, stderr=stderr)

プロセス起動の常として、まずforkが行われる。forkの際にstdinstdoutにそれぞれs1as1bとが結び付けられる。s1as1bは、sshした先のプロセスの入出力に繋ぐためだけのファイルディスクリプタなのである。

graph LR
subgraph python
s2
s1a
s1b
s1a .-> s2
s2 .-> s1a
s1b .-> s2
s2 .-> s1b
end

subgraph python2
s2f[s2]
s1af[STDIN]
s1bf[STDOUT]
s1af .-> s2f
s2f .-> s1af
s1bf .-> s2f
s2f .-> s1bf
end

s1a --- s1af
s1b --- s1bf
s2 --- s2f

次に、preexec_fn=setupの効力によって、fork先のpythonでsetupが呼ばれる。setupの定義は次の通り。

def setup():
        # runs in the child process
        s2.close()

fork先のpythonでは、入出力用にs1as1bのみを使用しており、それと接続されたs2は不要だからである。

graph LR
subgraph python
s2
s1a
s1b
s1a .-> s2
s2 .-> s1a
s1b .-> s2
s2 .-> s1b
end

subgraph python2
s1af[STDIN]
s1bf[STDOUT]
end

s1a --- s1af
s1b --- s1bf

preexec_fnの実行が終了すると、execが行われる。execする先のargvは、ここでは明記していないが、["ssh" "python" "-c" "ここに色々とpythonのコードが入っている"]だと考えてほしい。詳しくは、元コードを読んでほしい。

graph LR
subgraph python
s2
s1a
s1b
s1a .-> s2
s2 .-> s1a
s1b .-> s2
s2 .-> s1b
end

subgraph ssh[ssh python -c ...]
s1af[STDIN]
s1bf[STDOUT]
end

s1a --- s1af
s1b --- s1bf

fork元のpythonの側では、もはやs1a/s1bを使うことはないから、閉じてしまっている。

    os.close(s1a)
    os.close(s1b)

s1as1bとはそれぞれ入力と出力だけを行うので、矢印を一部省略して、以下のような状態になる。

graph TB
subgraph python
s2
end

subgraph ssh[ssh python -c ...]
s1af[STDIN]
s1bf[STDOUT]
end

s2 .-> s1af
s1bf .-> s2

そして、s2.sendallが行われる。これは、ssh先で起動したpythonスクリプトを初期化するための情報が格納されているデータで、内容は割愛する。要するに、ここで渡した内容はsshした先のpythonの標準入力に送られる。

graph TB
subgraph python
s2
end

subgraph ssh[ssh python -c ...]
s1af[STDIN]
s1bf[STDOUT]
end

s2 -. blah blah .-> s1af
s1bf .-> s2

最後にreturn p, s2が呼ばれ、プロセスオブジェクトとソケットs2が返されて終了。s2は双方向通信可能なソケットなので、データを流し込めばsshした先のpythonの標準入力に送られるし、読み込めばsshした先の標準出力からのデータが得られる。 このブートストラップが終了した後は、ローカルホストでファイアウォールの初期化が行われ、あらかじめ指定したIPレンジ宛てのパケットをこのソケットへと流し込んでいくという処理がはじまる。

読んだ感想

最初は一見して意味不明だったが、forkとexecの順序、そしてファイルディスクリプタの基本を抑えていたので読み解くことができた。ファイルディスクリプタに関する知識がないと難しいと思う。

www.coins.tsukuba.ac.jp

fdopen() を使う時に注意することは、socketpair() の結果が read() でも write() でも使えるのに対して、高水準入出力では、1つの FILE * では、入 力と出力のうち一方しかできないことである。よって、fdopen() の前に、 dup() により、入力と出力に別々のファイル記述子を割り当てておく必要があ る。

そして、s1s2との順序が悪いと思う。サブプロセスにはs2の側を渡したほうが分かりやすいと思うけれど、何か意図があるのだろうか・・・。

もう一つ気になったのが、forkするときのファイルディスクリプタの扱いである。pythonではforkするときはdupしたファイルディスクリプタは閉じられてしまう。

docs.python.org

でもstdin/stdoutに指定することができているので、標準入出力に割り当てた場合は違うのかな、と思う。s2はファイルディスクリプタではない(?)ので、手動で閉じる必要があるのだと思う。このあたりはよくわからなかった。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?