Chienomi

rsyncのprotocol version mismatchとSSHマルチセッション

Live With Linux::trouble shooting

rsyncは運用環境により、protocol version mismatchというエラーを発生させることがある。

この問題について検索すると、.bashrcに出力を伴うものを書いているからだ、という内容のページが数多くヒットする。

確かにその場合も問題は起きるが、実際はこれに限った話ではない。

今回私は、sftpとSSHマルチセッションに絡む問題によりこの問題を生じ、原因を特定するのに少々時間がかかった。

本記事では問題特定のための時間を短縮するためのリマインド記事としての位置づけとともに、この問題について理解を深める内容を記載する。

問題の解説

rsyncを実行したときに次のようなエラーを発生する場合がある。

protocol version mismatch -- is your shell clean?
(see the rsync manpage for an explanation)
rsync error: protocol incompatibility (code 2) at compat.c(622) [sender=3.3.0]

この問題を理解するには、まずrsyncの動作を知る必要がある。 ここではSSH越しにrsyncすることを前提として話を進める。

SSHの接続はストリームであり、通信をひとつの流れに収める必要がある。 つまり、rsyncはSSH越しにファイルを転送する場合、双方向パイプでやりとりできるような方法でファイルを交換している。

rsyncでリモートホストにファイルを送ろうとすると、

ssh host rsync --server #...

というようなコマンドを実行することで、リモートホストに受け取り用のプロセスを用意する。 そして、このコマンドと2本のパイプを介してやりとりを行う。

なお、この仕組みのため、リモートサーバー側にもrsyncが必要である。

双方向接続になっている、というのがひとつのポイントで、つまりリモートホストの応答をrsyncは受け取っている。 これは、差分転送を行うためにも必要なことだ。

protocol version mismatchは実際にプロトコルのバージョンが疎通できないということを意味していない。 つまり、リモートホストから受け取ったバージョン情報に基づいているエラーではない。 もっとシンプルに、ローカルのrsyncがリモートから理解できない応答を受け取った、ということを意味する。

このため、接続したときになんらかの出力を行うようになっていると、rsyncはその出力を受け取って「理解できない応答が返ってきた」とみなしエラーを発生させる。

ただ、.bashrcに書かれているから、というのは「本当にそうか?」と思ってしまう。 rcファイルは対話的シェルに起動時に読まれるはずで、sshでコマンドを実行したときにはrcファイルは読まれないはずだ。 少なくともZshはそうだし、私が試した限りBashでもそうだった。

こういった仕組みのため、SSH公開鍵に対してcommand=...のようにキーコマンドが設定されている場合も、そのコマンドがrsyncに対応したもの(例えばrrsync)でない限りこのエラーを生じる。 もちろん、internal-sftpにバインドされてる場合もだ。

限定された鍵とSSHマルチセッション

さて、ここで問題になるのがSSHマルチセッションとの組み合わせだ。

一般的には

ControlPath ~/.local/state/ssh/ssh-session-%r_%h_%p

というセッションパスを用いる。 ここで使われるのは

  • %r - The remote username
  • %h - The remote hostname
  • %p - The remote port

である。

つまり、ホストへの接続の認証に使った鍵が異なっていたとしても、同じ接続のセッションを使う。

結果として、先にSFTP限定の鍵でログインした状態でrsyncしようとすると、SFTP限定である旨の応答メッセージが返るためにprotocol version mismatchが発生する。

今回生じたケースのようにNAS相手だと、SSHFSでマウントするためにSFTP限定鍵を使うというようなケースがあるために発生しやすい。

また、ControlPathに含められるTOKENSに認証鍵がないため、これを解決する汎用的な方法はない。

部分的なSSHマルチセッション

OpenSSHは「先に見つけた設定を優先する」方式である。

グローバルな設定はHostディレクティブが登場した後に書けないために~/.ssh/configの先頭のほうに書くことになるが、そこにControlPathを書いてしまうとHostディレクティブ内にControlPathを書いても上書きはできない。

最も無難なのは、グローバル設定をせずに各HostディレクティブにControlPathを書き、限定された鍵を使うHostディレクティブには固有のControlPathを設定するか、またはControlPath noneとすることである。

もうひとつ考えられる方法は、設定ファイルの最後に

Host *
  ControlPath ~/.local/state/ssh/ssh-session-%r_%h_%p

と書くことだ。

これで適用順が最後になるため、各Hostディレクティブに書いたControlPathが優先され、ControlPathが書かれたディレクティブが適用されない場合のみHost *のものが適用される。