概要

SSH (Secure Shell) は、ネットワーク上の別のコンピュータにリモートでアクセスするための暗号化されたプロトコルである。

主に以下の3つの機能がある。

  • リモートログイン
    別のコンピュータにログインして、コマンドラインからファイル操作やプログラムの実行等ができる。
  • ファイル転送
    SCPまたはSFTPというプロトコルを使用してて、安全にファイルを転送できる。
  • X11フォワーディング
    X Window Systemのグラフィカルな出力をリモートで表示することができる。


SSHは従来のTelnetやFTP等の非暗号化プロトコルと比較して、通信内容が盗聴から守られるため、セキュリティが高いのが大きな利点である。
また、公開鍵暗号方式を用いた認証では、パスワードを明示的に送る必要が無い。

UNIXシステムでは標準でSSHサーバが含まれており、WindowsでもサードパーティーのSSHクライアント / サーバが利用できる。
多くのクラウドサービスやネットワーク機器でもSSHによるリモート管理を行うことができる。

C#において使用できるSSHライブラリとして、SSH.NET等がある。



処理の流れ

  1. SshClientクラスのインスタンスを生成して、ホスト、ユーザ名、パスワードを指定する。
  2. Connectメソッドを使用して、リモートPCへの接続を確立する。
  3. RunCommandメソッドを使用して、コマンドを実行する。
    Resultプロパティで出力を取得することができる。
  4. Executeメソッドを使用してコマンドを実行することにより、終了コードを取得することができる。


SSH.NETには他にも様々な機能があり、ファイル転送、SFTPサポート、ポートフォワーディング等ができる。

SshClientクラスのインスタンス生成

  • 認証方法
 using Renci.SshNet;
 
 // パスワード認証の場合
 var client = new SshClient("192.168.1.100", "<ユーザ名>", "<パスワード>");
 
 // ポート番号を指定する場合 (デフォルトは22)
 var client = new SshClient("192.168.1.100", 2222, "<ユーザ名>", "<パスワード>");
 
 // 公開鍵認証を使用する場合
 var keyFile = new PrivateKeyFile(@"/path/to/private_key");
 var client = new SshClient("192.168.1.100", "<ユーザ名>", keyFile);
 
 // 公開鍵認証を使用する場合 (パスフレーズ付き秘密鍵の場合)
 var keyFile = new PrivateKeyFile(@"C:\path\to\private_key", "passphrase");
 var client = new SshClient("192.168.1.100", "<ユーザ名>", keyFile);


  • 接続情報をまとめて管理する方法
 using Renci.SshNet;
 
 var connectionInfo = new ConnectionInfo(
    "192.168.1.100",
    "username",
    new PasswordAuthenticationMethod("<ユーザ名>", "<パスワード>")
 );
 
 var client = new SshClient(connectionInfo);


  • タイムアウト設定
 csharpvar connectionInfo = new ConnectionInfo(
    "192.168.1.100",
    "username",
    new PasswordAuthenticationMethod("<ユーザ名>", "<パスワード>")
 )
 {
    Timeout = TimeSpan.FromSeconds(30)  // 接続タイムアウト
 };
 
 var client = new SshClient(connectionInfo);


Connectメソッドによる接続確立

  • 接続方法
 using Renci.SshNet;
 
 var client = new SshClient("192.168.1.100", "<ユーザ名>", "<パスワード>");
 
 try
 {
    client.Connect();
    Console.WriteLine("接続成功");
    Console.WriteLine($"接続状態: {client.IsConnected}");
 }
 catch (SshException ex)
 {
    Console.WriteLine($"SSH接続エラー: {ex.Message}");
 }
 catch (SocketException ex)
 {
    Console.WriteLine($"ネットワークエラー: {ex.Message}");
 }
 finally
 {
    if (client.IsConnected)
    {
       client.Disconnect();
    }
    client.Dispose();
 }


  • 接続状態の確認
 // 接続前
 Console.WriteLine($"接続前: {client.IsConnected}"); // False
 
 client.Connect();
 
 // 接続後
 Console.WriteLine($"接続後: {client.IsConnected}"); // True
 Console.WriteLine($"サーバー情報: {client.ConnectionInfo.ServerVersion}");


  • 自動再接続の方法
 public bool ConnectWithRetry(SshClient client, int maxRetries = 3, int delaySeconds = 5)
 {
    for (int i = 0; i < maxRetries; i++)
    {
       try
       {
          client.Connect();
          Console.WriteLine("接続成功");
          return true;
       }
       catch (Exception ex)
       {
          Console.WriteLine($"接続失敗 ({i + 1}/{maxRetries}): {ex.Message}");
          if (i < maxRetries - 1)
          {
             Thread.Sleep(delaySeconds * 1000);
          }
       }
    }
    return false;
 }


RunCommandメソッドとResultプロパティ

  • コマンドの実行
 using Renci.SshNet;
 
 using (var client = new SshClient("192.168.1.100", "username", "password"))
 {
    client.Connect();
 
    // コマンド実行
    var cmd = client.RunCommand("ls -la /home");
 
    // 結果の取得
    Console.WriteLine("=== 標準出力 ===");
    Console.WriteLine(cmd.Result);
 
    // エラー出力の取得
    if (!string.IsNullOrEmpty(cmd.Error))
    {
       Console.WriteLine("=== エラー出力 ===");
       Console.WriteLine(cmd.Error);
    }
 
    // 終了コードの取得
    Console.WriteLine($"終了コード: {cmd.ExitStatus}");
 }


  • 複数のコマンドの実行
 // セミコロンで区切って複数コマンドを実行
 var cmd = client.RunCommand("cd /tmp; pwd; ls -l");
 Console.WriteLine(cmd.Result);
 
 // &&を使用して前のコマンドが成功した場合のみ次を実行
 var cmd2 = client.RunCommand("mkdir testdir && cd testdir && touch testfile.txt");
 Console.WriteLine(cmd2.Result);


  • 長時間実行されるコマンド
 // タイムアウトを設定
 var cmd = client.RunCommand("sleep 10; echo 'Done'");
 cmd.CommandTimeout = TimeSpan.FromSeconds(15);
 
 Console.WriteLine(cmd.Result);


  • 標準出力と標準エラー出力を区別する
 var cmd = client.RunCommand("ls /existing_dir /non_existing_dir");
 
 // 標準出力 (正常な結果)
 if (!string.IsNullOrEmpty(cmd.Result))
 {
    Console.WriteLine("成功した出力:");
    Console.WriteLine(cmd.Result);
 }
 
 // 標準エラー(エラーメッセージ)
 if (!string.IsNullOrEmpty(cmd.Error))
 {
    Console.WriteLine("エラー出力:");
    Console.WriteLine(cmd.Error);
 }
 
 // 終了コードで判定
 if (cmd.ExitStatus == 0)
 {
    Console.WriteLine("コマンドは正常終了しました");
 }
 else
 {
    Console.WriteLine($"コマンドは異常終了しました (コード: {cmd.ExitStatus})");
 }


Executeメソッドによる終了コード取得

  • CreateCommandとExecuteの使用方法
 using Renci.SshNet;
 
 using (var client = new SshClient("192.168.1.100", "<ユーザ名>", "<パスワード>"))
 {
    client.Connect();
 
    // CreateCommandでコマンドオブジェクトを作成
    using (var cmd = client.CreateCommand("ls -l /home"))
    {
       // Executeメソッドで実行し、標準出力を取得
       string output = cmd.Execute();
 
       Console.WriteLine("=== コマンド出力 ===");
       Console.WriteLine(output);
       Console.WriteLine($"終了コード: {cmd.ExitStatus}");
 
       if (!string.IsNullOrEmpty(cmd.Error))
       {
          Console.WriteLine($"エラー: {cmd.Error}");
       }
    }
 }


  • RunCommand および CreateCommand + Executeの違い
 // 方法1 : RunCommand (簡単であるが詳細制御が少ない)
 var result1 = client.RunCommand("ls -l");
 Console.WriteLine(result1.Result);
 
 // 方法2 : CreateCommand + Execute (詳細な制御が可能)
 using (var cmd = client.CreateCommand("ls -l"))
 {
    string output = cmd.Execute();
    Console.WriteLine(output);
 
    // より詳細な情報が取得可能
    Console.WriteLine($"実行時間: {cmd.CommandTimeout}");
 }


  • 終了コードによる分岐処理
 using (var cmd = client.CreateCommand("test -f /path/to/file.txt"))
 {
    cmd.Execute();
 
    switch (cmd.ExitStatus)
    {
       case 0:
          Console.WriteLine("ファイルが存在します");
          break;
       case 1:
          Console.WriteLine("ファイルが存在しません");
          break;
       default:
          Console.WriteLine($"エラーが発生しました (コード: {cmd.ExitStatus})");
          break;
    }
 }


  • 非同期実行とストリーム処理
 using (var cmd = client.CreateCommand("find / -name '*.log' 2>/dev/null"))
 {
    var asyncResult = cmd.BeginExecute();
 
    // 出力をリアルタイムで読み取る
    var outputStream = cmd.OutputStream;
    using (var reader = new StreamReader(outputStream))
    {
       string line;
       while ((line = reader.ReadLine()) != null)
       {
          Console.WriteLine($"見つかったファイル: {line}");
       }
    }
 
    cmd.EndExecute(asyncResult);
    Console.WriteLine($"検索完了(終了コード: {cmd.ExitStatus})");
 }


エラーハンドリング

 public int ExecuteCommandSafely(SshClient client, string command)
 {
    try
    {
       using (var cmd = client.CreateCommand(command))
       {
          string output = cmd.Execute();
 
          if (cmd.ExitStatus == 0)
          {
             Console.WriteLine("成功:");
             Console.WriteLine(output);
          }
          else
          {
             Console.WriteLine($"コマンド実行エラー (コード: {cmd.ExitStatus})");
             if (!string.IsNullOrEmpty(cmd.Error))
             {
                Console.WriteLine($"エラー詳細: {cmd.Error}");
             }
          }
 
          return cmd.ExitStatus;
       }
    }
    catch (SshException ex)
    {
       Console.WriteLine($"SSH例外: {ex.Message}");
       return -1;
    }
    catch (Exception ex)
    {
       Console.WriteLine($"予期しないエラー: {ex.Message}");
       return -1;
    }
 }



リモートPCでコマンドを実行

 using Renci.SshNet;

 class Program
 {
    static void Main(string[] args)
    {
       using (var client = new SshClient("<リモートPCのIPアドレス または ホスト名>",
                                         "<リモートPCのユーザ名>",
                                         "<リモートPCのユーザ名のパスワード>"))
       {
          client.Connect();
 
          // リモートPC上で"ls -l"コマンドを実行する
          var cmd = client.RunCommand("ls -l");
          Console.WriteLine(cmd.Result);
 
          // リモートPC上で"mkdir tmpdir"コマンドを実行する
          var result = client.RunCommand("mkdir tmpdir").Execute();
          Console.WriteLine("Exit code: {0}", result);
       }
    }
 }



ポートフォワーディング

SSH.NETには、ポートフォワーディングの機能が組み込まれており、
ForwardedPortLocalクラスを使用してローカルポートフォワーディングを実装することができる。

ポートフォワーディングが使用される理由としては、リモートPCにSSHで直接接続できない場合に別のホストを経由して接続するためである。

以下の例では、ローカルPCの50022番ポートからリモートPCの22番ポートへの接続を転送している。

  • portForwarded.Startメソッドを使用して、ポートフォワーディングを開始する。
  • portForwarded.Stopメソッドを使用して、ポートフォワーディングを停止する。


ただし、リモートPCの設定によっては、ポートフォワーディングができない場合もあるので注意が必要となる。

 using Renci.SshNet;
 using Renci.SshNet.Common;
 
 class Program
 {
    static void Main(string[] args)
    {
       using (var client = new SshClient("<リモートPCのIPアドレス または ホスト名>",
                                         "<リモートPCのユーザ名>",
                                         "<リモートPCのユーザ名のパスワード>"))
       {
          client.Connect();
 
          var portForwarded = new ForwardedPortLocal("<ホストPCのIPアドレス または ホスト名>", 50022,
                                                     "<リモートPCのIPアドレス または ホスト名>", 22);
          client.AddForwardedPort(portForwarded);
          portForwarded.Exception += PortForwarded_Exception;
          portForwarded.Start();
 
          using (var cmd = client.CreateCommand("ls -l"))
          {
             var result = cmd.Execute();
             Console.WriteLine("Command output: {0}", result);
          }
 
          portForwarded.Stop();
       }
    }
 
    private static void PortForwarded_Exception(object sender, ExceptionEventArgs e)
    {
       Console.WriteLine($"Port forwarded exception: {e.Exception.Message}");
    }
 }