[home] [back]


NAT環境で放置したSSH接続が切れる問題への対処

NATで外部に接続しているプライベートネットワークなどで 外部にSSHコネクションを作った際に、 ログインしっぱなしで10分ほど放置しておくと接続が切れる環境があります。 例えば筆者の職場がそうです。 ネット上でも多くの人が同じ問題で困っているようで、 いろいろな解決方法が提案されています。 それぞれの方法の概略と問題点についてまとめてみます。


はじめに

まず、このような問題がなぜ起こるのか、どのような害があるのかを説明し、 対処方法をリストアップします。

この問題が発生すると、 見かけ上はSSHでログインした先のシェルが無反応になります。 しばらくして「Connection to *** closed.」などと言われて、 SSHクライアントが接続断を認識します。

これは、NATをしているルータが、 一定時間パケットの流れないコネクションを接続断と判断し、 NATテーブルをクリアしてしまうために起こると考えられます。 SSHクライアントがいざパケットを流そうとしたときに、 初めて接続断に気づくというわけです (NATテーブルをクリアする際に両側のソケットに対してハングアップを 知らせてくれればすぐに切れると思うのですが、 少なくとも職場のブロードバンドルータは何もしないようです)。 また、ファイヤーウォールの設定などで、 意図的に切断している環境もあるでしょう。

SSHに限らず、コネクションを作った後で 長期間何もパケットが流れないようなサービスであれば 同じように切れるはずですが、SSH以外はまず該当しないでしょう。

対策としては、SSHの設定で「KeepAlive」をオンにすれば万事解決… のような気がしますが、これは後述するように全く効果がありません。 有効な方法は以下のようにいくつかありますが、どれもメリットとデメリットがあります。

  1. SSH接続を放置しない
  2. ルータの設定をなんとかする
  3. ログイン先で定期的に何かを出力するプログラムを実行する
  4. KeepAliveが期待通り動くようにOSのTCP設定を変更する
  5. SSHサーバの設定で切れているかどうか確認するパケットを送る
  6. SSHクライアントの設定で無害なパケットを定期的に送る

また、SSH接続の際に接続先の端末(tty)が必要かどうかによっても対策は変わってきます。 つまり、ログインしてメンテナンスなどの目的でシェルを使う場合と、 ポートフォワーディングやcronによる定期実行の場合とでは 適用の可否が変わってきます。表にまとめると次のようになります。

方法ttyありttyなしオススメ度
1. 放置しない××
2. ルータの設定を変更
3. ログイン先で定期的に何かを出力
4. OSの設定変更×
5. SSHサーバで対処×
6. SSHクライアントで対処

以下、上記の方法の詳細と問題点・適用範囲について説明していきます。


SSH接続を放置しない

ログインして使う場合は、用が済んだらexitしましょう。 汎用的でかつセキュリティ的にも文句のない方法です。

筆者には絶対に無理です。


ルータの設定をなんとかする

ルータのタイムアウト設定を延ばすのが一番まともな解決方法です。 短時間でNATテーブルをクリアしてしまうために起こるトラブルなので、 クリアするまでの時間を非常に長くすればOKということになります。

もっとも、 設定変更ができない場合もあるでしょうし、 機器によってはNATテーブルがあふれてしまう可能性もあるなど、 適用できない可能性もあります。

また、15分で切れるのが2時間で切れるようになったところで 問題としては変わっていないともいえます。


ログイン先で定期的に何かを出力するプログラムを実行する

端末を取得する場合のみ有効な方法ですが、 一定時間おきに何かの文字列を出力するようなプロセスを バックグラウンド実行しておけば その文字列がコネクション上を流れますので、 接続を維持できます。

また、ポートフォワーディングが主目的の場合など、 ping -i 300などとリモートで実行する例がよく見られます。

問題点は、他のプロセスの出力と混ざってしまうことです。 リモートログインしてEmacsなどの端末制御を行うアプリケーションを実行すると、 このバックグラウンドプロセスの出力が画面を乱してしまいます。 ビープ(\a, 0x07)を出力すれば画面が乱れることはありませんが、 うるさいのでイマイチだと思います。


KeepAliveが期待通り動くようにOSのTCP設定を変更する

前述したように、ただssh_configでKeepAlive yesとしただけではこの問題は解決しません。 KeepAliveオプションはTCPソケットを作るときにSO_KEEPALIVEを有効にするものですが、 これは一定時間何もパケットの流れないコネクションに対し、 接続が生きているかどうかを確認するためのパケットを流すものです。 このkeepaliveメッセージを送るまでの時間は、手元のLinuxだと2時間の設定ですので、 とっくに切れているくらいの時間が経過してようやく接続断をOSが認識するわけです。 筆者にはこのあたりのレイヤーの話は全くわからないのですが、 多分そういうものなのでしょう。

Linuxの場合は /proc/sys/net/ipv4/tcp_keepalive_time などをいじってこの設定を5分などと変更すれば 問題は解決するようですが、心理的に抵抗があります。

「Programming UNIX Sockets in C - Frequently Asked Questions」の記述からしても、OSの設定は変更せずに アプリケーションレベルで対処すべき問題のように思います。


SSHサーバの設定で切れているかどうか確認するパケットを送る

SSHサーバの設定で、 切断しているかどうかを確認する目的で定期的にパケットを送ることができます。 これは比較的問題の少ない方法だと思います。

この設定を有効にすると、 本当は切れていないのにしばらく通信が途切れただけで コネクションが切れたと判定されるかもしれませんが、 問題になることはまず無いでしょう。

ClientAliveInterval 15
ClientAliveCountMax 3

この方法の欠点は、 サーバ・クライアントともOpenSSH同士でないと利用できず、 しかもプロトコルがSSH2でないと利用できないことです。 また、おそらく端末がないと使えないため、 ポートフォワーディング用途には使えません。

また、メンテナンスするサーバマシンが増えるたびにこの設定をするのも面倒です。


SSHクライアントの設定で無害なパケットを定期的に送る

Heartbeat/Watchdog Patch for OpenSSHを使って、 クライアントが一定期間ごとにSSH_MSG_IGNOREメッセージを出すようにする、 という方法です。個人的にはこれがオススメです。

ssh_config(または.ssh/config) に以下のように書くことで300秒に1回無害なパケットを流します。

Heartbeat 300

他にも、以下のクライアントにはこの機能が標準でついているようです。

残念ながらこの機能はOpenSSH標準では提供されていないので、 上記のHeartbeatパッチを拾ってきてコンパイルし直す必要があります。 ただし、Debianで配布されているOpenSSHには既に特別なパッチが当たっており、 Heartbeatと同じ機能がProtocolKeepAlivesという名前になっているようです。

Heartbeatパッチの内容を眺めてみましたが、 本質的な部分は10行程度のようです。 ですので、 自分の使っているクライアントソフトがこの機能に対応していない場合でも ソースさえあれば対応可能だと思います。

Heartbeatパッチの添付文書には、 一部の古い実装ではSSH_MSG_IGNOREを受け取ると 死ぬサーバがあるかもしれないなどと書いてありますが、 個人的には杞憂だろうと思っています。

hanawa<y@hnw.jp>

$Date: 2005-06-20 15:12:36 $


[home] [back]