TL;DR
一段目だけ plink を使い、後は ssh すべし
経緯
業務上、踏み台経由でリモートホストのファイルを編集する必要に迫られ、Emacs なら TRAMP があるから簡単にできるだろうと思って着手したものの、案外手間取ったのでメモ。探し方が悪いのか、そのものズバリの記事を見つけられませんでした。
皆さんスイスイ接続できてるのか、はたまた Windows から多段 ssh のニーズが乏しいのか、はて。
必要なもの
以下の構成で動作確認してます。
- plink 0.65
- GNU Emacs 24.4.1 (x86_64-pc-mingw32) of 2014-11-10 on NTEMACS64
NTEmacs の TRAMP は PuTTY と連携するので、あらかじめ plink へのパスを通しておいてください。ssh そのものは、インストールしておかなくても大丈夫です(あっても害はありませんが)。
多段構成の想定
ここでは仮に、以下のような多段接続を想定します。
localhost(windows) -> proxy1 -> proxy2 -> target
さて、target のファイルを手元で編集するにはどうすればいいでしょうか?
ファイル名のルール(おさらい)
TRAMP 知ってる人には釈迦に説法ですが、ファイル名は以下の形式に従います。
/method:user@host#port:path
- method: 接続方法を指定する。ftp, ssh, scp, plink, pscp, su など。
- user: 接続ユーザーを指定する。
- host: 接続先ホストを指定する。
- port: 接続先ポート番号を指定する。のっぴきならない場合に。
- path: 編集するファイル名を指定する。
接続方法やユーザーなどを省略するとデフォルト値が使われたり、入力を促されたりします。
これで、ローカルファイルを開くのと同じように、C-x C-f /ssh:foo@bar:.zshrc
でリモートファイルを開くことができます。この透過性を一度でも体験すれば Emacs から離れられなくなること請け合いの魅力的な機能です。その昔は Ange-FTP で・・・なんて話はどうでもいいですね。
蛇足ながら、多段接続しない場合には、method
に scp や pscp などの外部メソッドを指定するとパフォーマンスが向上します。多段接続をサポートする ssh や plink などの内部メソッドはファイル転送時にエンコード処理がはさまるため、パフォーマンス面では不利です。
多段接続の設定例
以下の設定を追加しておくことで、C-x C-f /user@target:path
で target
のファイルを開けるようになります。
(with-eval-after-load 'tramp
(setq tramp-default-method "ssh")
(add-to-list 'tramp-default-proxies-alist
'("proxy2" nil "/plink:proxy1:"))
(add-to-list 'tramp-default-proxies-alist
'("target" nil "/proxy2:"))
nil)
tramp-default-method
はファイル名に method が指定されなかった場合のデフォルト値になります。tramp-default-proxies-alist
は、(HOST USER PROXY)
のリストです。これは、USER@HOST
に接続する場合は PROXY
を経由しろ、という指定になります。
ポイントは、一段目にだけ /plink:
を明示的に指定し、二段目以降は未指定にすることです。
わざわざ設定を書かずに多段したいものぐさな人は、C-x C-f /plink:proxy1|proxy2|user@target:path
でファイルを開けば OK です。この時にも、一段目にだけ /plink
を指定する必要があります。
多段接続の仕組み
TRAMP の多段接続は結局のところ、コマンドラインで
$ ssh -t user@host1 ssh -t user@host2
としているのと同じです。Windows では ssh のかわりに plink を使って
C:\>plink -t user@host1 ssh -t user@host2
とできればいいわけですね。踏み台に接続した後はそのホスト上で ssh
を普通に叩きます。Windows だからと言って安直に
(setq tramp-default-method "plink")
と設定してしまうと、いざ多段接続するときに
C:\>plink -t user@host1 plink -t user@host2
と残念なことになってしまいます。plink
が踏み台である host1
に存在することはまずないでしょうから、これでは繋がりません。
そこで、tramp-default-method
には ssh
を指定しておき、一段目の踏み台もしくは、直接 ssh する時だけ /plink:
を指定すれば良いのです。
あるいは、tramp-default-method
を plink
にしておき、接続方法として常に /ssh:
を指定する、という手もあります。この場合には直接接続時に /plink:
を省略できます。
多段が多いのか、はたまた直接が多いのかによって、どちらをデフォルトにするか決めれば良いでしょう。
はまりポイント - 多段で無限ループ
tramp-default-proxies-alist
で、HOST
と USER
に PROXY
と一致する正規表現を指定した場合には無限ループします。私はこれではまりました。例えば、(nil nil "/plink:foo@bar")
などは、無限ループ確定です。
その理由は、多段接続用のリストを作る tramp-compute-multi-hops
を見ればわかります。
;; Look for proxy hosts to be passed.
(setq choices tramp-default-proxies-alist)
(while choices
(setq item (pop choices)
proxy (eval (nth 2 item)))
(when (and
;; Host.
(string-match (or (eval (nth 0 item)) "")
(or (tramp-file-name-host (car target-alist)) ""))
;; User.
(string-match (or (eval (nth 1 item)) "")
(or (tramp-file-name-user (car target-alist)) "")))
(if (null proxy)
;; No more hops needed.
(setq choices nil)
;; Replace placeholders.
(setq proxy
(format-spec
proxy
(format-spec-make
?u (or (tramp-file-name-user (car target-alist)) "")
?h (or (tramp-file-name-host (car target-alist)) ""))))
(with-parsed-tramp-file-name proxy l
;; Add the hop.
(add-to-list 'target-alist l)
;; Start next search.
(setq choices tramp-default-proxies-alist)))))
処理はまず、接続先リストの先頭からホストとユーザーを取り出し、それらが経由すべき踏み台の条件と一致するかを調べます。もし一致した場合には、その踏み台を接続先リストの先頭に追加し、もう一度踏み台リストの先頭から調べ直します。
つまり、(nil nil "/plink:foo@bar")
のような条件はどんな PROXY
とも一致するので、自分で自分にヒットしてまた自分を追加する・・・という一人上手を延々と続けることになるわけです。わーお!
考察 - 汎用的な多段接続を実現するには
無限ループに関連して、現状の TRAMP では、どう頑張ってもできない設定があります。ドメイン名を利用した、汎用的な多段接続設定です(まあ、そんなニーズはそもそもないのかもしれませんが)。仮に、以下のような多段接続が必要だとします。
localhost -> *.local.domain
localhost -> proxy@gw.local.domain -> *.hop.domain
localhost -> proxy@gw.local.domain -> proxy@gw.hop.domain -> *.step.domain
localhost -> proxy@gw.local.domain -> proxy@gw.hop.domain -> proxy@gw.step.domain -> *.jump.domain
これを、以下のように設定できれば直観的です。
(setq tramp-default-method "ssh")
(add-to-list 'tramp-default-proxies-alist
'("local\\.domain" nil "/plink:%u@%h:"))
(add-to-list 'tramp-default-proxies-alist
'("hop\\.domain" nil "/proxy@gw.local.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("step\\.domain" nil "/proxy@gw.hop.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("jump\\.domain" nil "/proxy@gw.step.domain:"))
しかし、これは無限ループします。なぜなら、/plink:%u@%h:
が *.local.domain
と一致してしまうからです。せっかくなので(≒現実逃避)、どうすればこういった設定が可能になるかを考察してみます。
要件としては、一段目のルールに対して「これ以上多段生成しなくて良い」ということを指定できれば良いはずです。TRAMP では PROXY
に nil
を指定することで多段生成を抑制することができますが、残念ながら今回のようなケースには全く無力です。例えばこう書いてみたとしても:
(add-to-list 'tramp-default-proxies-alist
'("local\\.domain" nil "/plink:%u@%h:"))
(add-to-list 'tramp-default-proxies-alist
'("hop\\.domain" nil "/proxy@gw.local.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("step\\.domain" nil "/proxy@gw.hop.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("jump\\.domain" nil "/proxy@gw.step.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("local\\.domain" nil nil)) ;; prevent multi-hops
*.local.domain
への接続は PROXY
に nil
を指定したルールが最初にヒットしてしまうため、常に失敗することは明らかです。
そこでシンプルに考えて、tramp-default-proxies-alist
の各要素を (HOST USER PROXY NO-MORE-HOPS)
という風に拡張し、NO-MORE-HOPS
が t
なら PROXY
を接続先リストの先頭に追加して多段生成ループを抜ける、というロジックを考えてみます。
;; Look for proxy hosts to be passed.
(setq choices tramp-default-proxies-alist)
(while choices
(setq item (pop choices)
proxy (eval (nth 2 item))
no-more-hops (eval (nth 3 item)))
(when (and
;; Host.
(string-match (or (eval (nth 0 item)) "")
(or (tramp-file-name-host (car target-alist)) ""))
;; User.
(string-match (or (eval (nth 1 item)) "")
(or (tramp-file-name-user (car target-alist)) "")))
(if (null proxy)
;; No more hops needed.
(setq choices nil)
;; Replace placeholders.
(setq proxy
(format-spec
proxy
(format-spec-make
?u (or (tramp-file-name-user (car target-alist)) "")
?h (or (tramp-file-name-host (car target-alist)) ""))))
(with-parsed-tramp-file-name proxy l
;; Add the hop.
(add-to-list 'target-alist l)
;; Start next search or break if no more hops needed
(setq choices (if no-more-hops nil
tramp-default-proxies-alist))))))
これで設定は以下のように書けます。
(add-to-list 'tramp-default-proxies-alist
'("local\\.domain" nil "/plink:%u@%h:" t)) ;; prevent multi-hops any more
(add-to-list 'tramp-default-proxies-alist
'("hop\\.domain" nil "/proxy@gw.local.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("step\\.domain" nil "/proxy@gw.hop.domain:"))
(add-to-list 'tramp-default-proxies-alist
'("jump\\.domain" nil "/proxy@gw.step.domain:"))
既存の挙動と互換性を維持したまま、機能拡張できそうです。しかも、一段目に接続する時も /plink:
を指定する必要がなくなり、利便性が向上しています。
でも、tramp-default-proxies-alist
をこんなふうに拡張してしまうのは、バッドノウハウを増やすだけのような罪悪感があり、気が引けます。パッチを送るべきかどうか、悩むところです。
まとめ
NTEmacs で多段 ssh 接続する方法をまとめました。無限ループにはまって、ずいぶん寄り道してしまいましたが、モヤモヤを解消できてスッキリしました。Emacs との腐れ縁はまだまだ続きそうです。