Zenn
🚚

C# - FTP/FTPS/SFTPクライアント(+ Ubuntu 24.04 でサーバ構築)

に公開

はじめに

昔は、ファイル転送として、大変お世話になった FTP ですが、WEB API で事足りるので、しばらく利用する機会がありませんでした。
しかし、数年前、案件対応として、FTPS でのファイル転送を C# で実装するケースがありました。
今回は、FTP / FTPS / SFTP クライアントについて記載したいと思います。

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8
  • Windows Forms - .NET 8
  • WPF - .NET Framework 4.8
  • WPF - .NET 8

FTP / FTPS / SFTP サーバは、Ubuntu 24.04 上に構築しました。

FTP/FTPS/SFTP

FTP、FTPS、SFTP、SCP:ファイル転送プロトコルを解説

まずは、FTP / FTPS / SFTP の概要説明をします。

  • FTP
    • FTP(File Transfer Protocol)は、1970年代から利用されている、ファイル転送プロトコルで、制御チャネルとデータ転送チャネルの 2つのチャネル(デュアルチャネル)を利用します。
    • FTP には、Active(アクティブ:能動的)モードと Passive(パッシブ:受動的)モードが存在します。双方ともに、最初は、クライアントからサーバのポート番号 21 に接続して制御チャネルを確立します。
      • Active モードでは、クライアントからサーバに接続ポート番号を伝えて、サーバがポート番号 20 からクライアントの対象ポートに接続して、データチャネルを確立します。
      • Passive モードでは、サーバからクライアントに、サーバで割り当てらた範囲内のポート番号を伝えて、クライアントからサーバの対象ポートに接続して、データ転送チャネルを確立します。
    • メリット
      • 迅速、かつ、シンプルです。
    • デメリット
      • 暗号化されないため、セキュリティリスクがあります。
  • FTPS
    • FTPS(FTP Secure、もしくは、FTP over SSL/TLS)は、FTP で送受信するデータを、SSL/TLS で暗号化する通信プロトコルで、FTP と同様に、制御チャネルとデータ転送チャネルの 2つのチャネルを利用します。(Active モードと Passive モードが存在)
    • FTPS には、暗号化モードとして、Explicit(エクスプリシット:明示的)モード と Implicit(インプリシット:暗黙的)モードが存在します。
      • Explicit モードは、クライアントからサーバのポート番号 21 に接続して制御チャネルを確立します。通信開始時は暗号化せず、クライアントが AUTH コマンドを実行することで、途中から通信内容を暗号化します。データ転送チャネルの利用ポート番号については、Active モード / Passive モードともに FTP と同様です。
      • Implicit モードは、クライアントからサーバのポート番号 990 に接続して制御チャネルを確立して、初めから暗号化通信を行います。データ転送チャネルの利用ポート番号については、Active モードの場合、サーバはポート番号 20 ではなく 989 を利用します。Passive モードの場合、FTP と同様です。
    • メリット
      • 既存の FTP インフラを活用しながらセキュリティを強化できるため、システムの大幅な変更が不要です。
      • SSL/TLS で、認証情報、および、転送データの暗号化を行うことで、セキュリティリスクが軽減されます。
    • デメリット
      • ファイアウォールに妨げられることがあるので、ポート番号、もしくは、FTPS サーバ / クライアントソフトの例外登録が必要となります。
  • SFTP
    • SFTP(SSH File Transfer Protocol)は、SSH(Secure Shell)を利用したファイル転送プロトコルです。SSH の暗号化を利用することで、高いセキュリティを確保します。また、FTP とは異なり、シングルチャネル(制御とデータ転送を同じチャネルで実施)実装となり、ポート番号 22 を利用します。
    • メリット
      • すべての通信が暗号化され、認証も SSH の強力な仕組みに基づいて行われます。
      • シングルチャネルのため、設定やファイアウォールの構成が容易です。
    • デメリット
      • SSH が標準利用できる環境でない場合には、各種ソフトの導入が必要となります。

事前準備

Ubuntu

下記目的で ftpusers というグループを作成します。

  • FTP / FTPS で vsftpd.conf - umask を 002 として、対象グループで更新可とする
  • SFTP 接続を対象グループで許可して umask を 002 として、対象グループで更新可とする
$ sudo groupadd ftpusers

ftpuser1, ftpuser2 というユーザを ftpusers グループで作成して、パスワードを設定します。

$ sudo useradd -m -g ftpusers ftpuser1
$ sudo passwd sftpuser1
新しい パスワード: 
新しい パスワードを再入力してください: 
passwd: パスワードは正しく更新されました
$ sudo useradd -m -g ftpusers ftpuser2
$ sudo passwd sftpuser2
新しい パスワード: 
新しい パスワードを再入力してください: 
passwd: パスワードは正しく更新されました

共用公開ディレクトリとして /var/ftproot を作成して、サンプルとして hoge1.txt を配置します。

$ sudo -i
# mkdir /var/ftproot
# chown root:ftpusers /var/ftproot
# chmod 0775 /var/ftproot
# echo "HogeHoge" > /var/ftproot/hoge1.txt
# chown ftpuser2:ftpusers /var/ftproot/hoge1.txt
# chmod 0664 /var/ftproot/hoge1.txt
$

C# クライアント

FTP / FTPS は、FtpWebRequest を用いた実装が可能でしたが、FtpWebRequest 基底クラス WebRequest が .NET 6 で非推奨となり、FtpWebRequest マニュアルに「新しい開発に FtpWebRequest を使用することはお勧めしません」との記載がされました。
このため、NuGet Gallery | FluentFTP を利用することとします。

SFTP については、NuGet Gallery | SSH.NET を利用します。

PM> NuGet\Install-Package FluentFTP
PM> NuGet\Install-Package SSH.NET

FTP

FTP - Passive をサンプルとします。

Ubuntu

FTPサーバ vsftpdの設定(vsftpd.conf)

vsftpd(3.0.5)をインストールして、起動されていることを確認します。

$ sudo apt update
$ sudo apt install vsftpd

$ sudo systemctl status vsftpd

vsftpd の設定ファイルで、接続時のルートディレクトリ、Passive モードなどを設定します。

$ sudo vi /etc/vsftpd.conf
# vsftpd.conf 既存項目
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=002
use_localtime=YES
connect_from_port_20=NO
ascii_upload_enable=YES
ascii_donwload_enable=YES
chroot_local_user=YES

# 接続可能ユーザ制限
userlist_enable=YES
userlist_deny=NO
userlist_file=/etc/vsftpd.user_list

# 接続時のルートディレクトリ
local_root=/var/ftproot

# 書き込み権限があるとchroot出来ない機能を無効
allow_writeable_chroot=YES

# Passive モード関係
pasv_enable=YES
pasv_min_port=50000
pasv_max_port=50050

接続可能ユーザを vsftpd.user_list に記載します。

$ sudo vi /etc/vsftpd.user_list
ftpuser1
ftpuser2

FireWall の状態を確認、非アクティブでしたので FireWall 設定はスキップします。
※本記事は Ubuntu サーバ構築ではなく、FTP/FTPS/SFTP クライアントが主題のため。

$ sudo ufw status
状態:非アクティブ

vsftpd を再起動します。

$ sudo systemctl restart vsftpd

C# クライアント

FluentFTP は GitHub で公開されていて、CSharpExamples もあります。

https://github.com/robinrodricks/FluentFTP

以前、利用した時は 37.0 でしたが、RELEASES.md を見ると、40.0 で大幅な改修がされて、設定情報が Config に集約されたようです。

FtpClient(同期)、AsyncFtpClient(非同期)が用意されています。
FtpClient での 接続 / 一覧取得 / ダウンロード / アップロード / 切断 サンプルコードを記載します。

using FluentFTP;
// FtpClient は、IDisposable なので、using もしくは Dispose() が必要
// IP_OR_HOST:接続先、ftpuser1:ユーザ名、PASSWORD:パスワード、21:接続ポート
using (var client = new FluentFTP.FtpClient("IP_OR_HOST", "ftpuser1", "PASSWORD", 21))
{
  // FTP - Passive
  client.Config.EncryptionMode = FtpEncryptionMode.None;
  client.Config.DataConnectionType = FtpDataConnectionType.AutoPassive;

  // タイムアウト値 (msec)
  client.Config.ConnectTimeout = 5000;
  // 必要に応じて下記も設定
  // client.Config.DataConnectionConnectTimeout
  // client.Config.DataConnectionReadTimeout

  // 接続
  try
  {
    client.Connect();
  }
  catch (System.TimeoutException)
  {
    // タイムアウト - TODO
  }
  catch (FluentFTP.Exceptions.FtpAuthenticationException)
  {
    // 認証情報不正、および、FTP接続許可されていないユーザ - TODO
  }

  // 公開フォルダの一覧取得
  FtpListItem[] items = client.GetListing();
  foreach (var item in items)
  {
    var target = item.Name;
    // TODO
  }

  // ダウンロード
  string localPath = System.IO.Path.GetTempPath() + "hoge1.txt";
  string remotePath = "hoge1.txt";
  FtpStatus sts = client.DownloadFile(localPath, remotePath, FtpLocalExists.Overwrite);
  if (sts != FtpStatus.Success)
  {
    // ERROR - TODO
  }

  // アップロード
  remotePath = "hoge2.txt";
  sts = client.UploadFile(localPath, remotePath, FtpRemoteExists.Overwrite);
  if (sts != FtpStatus.Success)
  {
    // ERROR - TODO
  }

  // 切断
  client.Disconnect();
}

FluentFTP.FtpClient、上記以外の主な機能を記載します。

メソッド 内容
FileExists リモートファイル存在確認
DeleteFile リモートファイル削除
DirectoryExists リモートディレクトリ存在確認
CreateDirectory リモートディレクトリ作成
Rename リモートファイル名称変更

FTPS

FTPS - Implicit Passive をサンプルとします。

Ubuntu

【vsftpd】FTPをSSL/TLSで暗号化する設定 #ftps - Qiita
vsftpdでchrootを使って複数ユーザでFTPS接続させる
vsftpd.conf(5) — Arch manual pages

前述、FTP サーバとして構築した環境を FTPS - Implicit に再構築します。
まず、自己署名証明書を作成します。
vsftpd.key 作成で、画面表示に従いパスワードを設定します。

$ sudo -i
# cd /etc/ssl/private
# openssl genrsa -aes256 4096 > vsftpd.key
Generating RSA private key, 4096 bit long modulus (2 primes)
........++++
.......................++++
e is 65537 (0x010001)
Enter pass phrase:
Verifying - Enter pass phrase:

vsftpd.conf では、rsa_private_key_file に対するパスワード設定項目がないので、パスワードを削除した後、vsftpd.pem を作成して、ファイルモードを変更します。

# openssl rsa -in vsftpd.key -out vsftpd.key
Enter pass phrase for vsftpd.key:
writing RSA key
# openssl req -new -key vsftpd.key -x509 -days 365 -out vsftpd.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Tokyo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:hoge
Organizational Unit Name (eg, section) []:hoge
Common Name (e.g. server FQDN or YOUR name) []:hoge
Email Address []:hoge@example.com
# chmod 0666 vsftpd.key vsftpd.pem
# exit
$

vsftpd の設定ファイルに下記内容を更新します。

$ sudo vi /etc/vsftpd.conf
# vsftpd.conf 既存項目をコメントアウト
#rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
#rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
#ssl_enable=NO

# SSL/TLSを有効化
ssl_enable=YES

# 使用プロトコル
ssl_sslv2=NO
ssl_sslv3=NO
ssl_tlsv1=NO
ssl_tlsv11=NO
ssl_tlsv12=YES
ssl_tlsv13=YES

# 暗号化方式
ssl_ciphers=HIGH

# サーバ証明書、秘密鍵
rsa_cert_file=/etc/ssl/private/vsftpd.pem
rsa_private_key_file=/etc/ssl/private/vsftpd.key

# Implicit モード設定
implicit_ssl=YES
listen_port=990

vsftpd を再起動します。

$ sudo systemctl restart vsftpd

C# クライアント

FTP サンプルコードの一部を更新して、FTPS - Implicit Passive とします。

  • ポート番号 990 利用
  • FtpEncryptionMode.Implicit 指定
  • ValidateCertificate コールバック
// FtpClient は、IDisposable なので、using もしくは Dispose() が必要
// IP_OR_HOST:接続先、ftpuser1:ユーザ名、PASSWORD:パスワード、990:接続ポート
using (var client = new FluentFTP.FtpClient("IP_OR_HOST", "ftpuser1", "PASSWORD", 990))
{
  // FTPS - Implicit Passive
  client.Config.EncryptionMode = FtpEncryptionMode.Implicit;
  client.Config.DataConnectionType = FtpDataConnectionType.AutoPassive;

  // 証明書の内容を確認しない(自己署名証明書なので)
  client.ValidateCertificate += (control, e) => { e.Accept = true; };

  // タイムアウト値 (msec)
  client.Config.ConnectTimeout = 5000;
  
<以降は FTP と同一>

正規の認証局が発行する証明書がある場合には、下記手順で証明書を利用します。

// 証明書
string pfxPath = "PATH_TO_PFX";                              // TODO - pfxファイルのパス
var certificate = new X509Certificate2(pfxPath, "PASSWORD"); // TODO - pfxのパスワード
client.Config.ClientCertificates.Add(certificate);
client.ValidateCertificate += (control, e) => {
  if (e.PolicyErrors == System.Net.Security.SslPolicyErrors.None)
  {
    e.Accept = true;
  }
  else
  {
    // TODO 
    e.Accept = false;
  }
};

SFTP

パスワード認証と公開鍵認証がありますが、今回は、パスワード認証とします。

Ubuntu

【Ubuntu】Linuxでsftpサーバー構築し、sftpユーザーのディレクトリ制限を行う方法

SSHサーバとしてOpen SSH Serverをインストールして、起動されていることを確認します。

$ sudo apt update
$ sudo apt install openssh-server

$ sudo systemctl status ssh

sshd_config を修正します。

$ sudo vi /etc/ssh/sshd_config
# ssh 無効、sftp のみ有効
#Subsystem      sftp    /usr/lib/openssh/sftp-server
Subsystem       sftp    internal-sftp

# パスワード認証無効、公開鍵認証有効
PasswordAuthentication no
PubkeyAuthentication yes

# ftpusers グループはパスワード認証有効、/var/ftproot を初期ディレクトリ
Match Group ftpusers
        X11Forwarding no
        AllowTcpForwarding no
        PasswordAuthentication yes
        ForceCommand internal-sftp -d /var/ftproot -u 002

FireWall の状態を確認、非アクティブでしたので FireWall 設定はスキップします。
※本記事は Ubuntu サーバ構築ではなく、FTP/FTPS/SFTP クライアントが主題のため。

$ sudo ufw status
状態:非アクティブ

SSHサーバを再起動します。

$ sudo systemctl restart ssh

C# クライアント

GitHub - sshnet/SSH.NET: SSH.NET is a Secure Shel
C#でSFTPでファイル転送やってみる

SftpClient での 接続 / 一覧取得 / ダウンロード / アップロード / 切断 サンプルコードを記載します。

// SftpClient は、IDisposable なので、using もしくは Dispose() が必要
// IP_OR_HOST:接続先、22:接続ポート、ftpuser1:ユーザ名、PASSWORD:パスワード
using (var client = new SftpClient("IP_OR_HOST", 22, "ftpuser1", "PASSWORD"))
{
  // タイムアウト値 (TimeSpan)
  client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(5);
  // 必要に応じて下記も設定
  // client.OperationTimeout

  // 接続
  try
  {
    client.Connect();
  }
  catch (Renci.SshNet.Common.SshOperationTimeoutException)
  {
    // タイムアウト - TODO
  }
  catch(Renci.SshNet.Common.SshAuthenticationException)
  {
    // 認証情報不正、および、対象ユーザはパスワード認証無効 - TODO
  }

  // 公開フォルダの一覧取得
  foreach (ISftpFile file in client.ListDirectory("."))
  {
    var name = file.Name;
    if (name.StartsWith("."))
    {
      // "."、".." などをスキップ
      continue;
    }
    // TODO
    var size = file.Length;
  }

  // ダウンロード
  string localPath = System.IO.Path.GetTempPath() + "hoge1.txt";
  string remotePath = "hoge1.txt";
  using (var fs = System.IO.File.OpenWrite(localPath))
  {
    client.DownloadFile(remotePath, fs);
  }

  // アップロード
  remotePath = "hoge2.txt";
  using (var fs = System.IO.File.OpenRead(localPath))
  {
    client.UploadFile(fs, remotePath, true);  // true:上書き
  }

  // 切断
  client.Disconnect();
}

SftpClient、および、ISftpFile、上記以外の主な機能を記載します。

メソッド 内容
SftpClient.CreateDirectory ディレクトリ作成
SftpClient.Exist 存在確認
ISftpFile.Delete 削除
ISftpFile.MoveTo リネーム

対象がディレクトリかの確認は、プロパティで可能です。

プロパティ 内容
ISftpFile.IsDirectory ディレクトリ確認

出典

本記事は、2025/03/17 Qiita 投稿記事の転載です。

C# - FTP/FTPS/SFTPクライアント(+ Ubuntu 24.04 でサーバ構築)

Discussion

ログインするとコメントできます