FTPのアクティブモードが機能しない理由

# FTP接続をおこなう
WSL(Ubuntu)からクラウド上 (AWS) にある EC2インスタンスにFTPで接続する。
FTPクライアントは`ftp`あるいは`lftp` ( [公式ダウンロードページ](https://launchpad.net/ubuntu/noble/arm64/lftp/4.9.2-2ubuntu1))を使う。
`lftp`のリンクはパッケージ ( `deb`ファイル）をダウンロードするページです。
#### Ubuntu (wsl)へのインストール方法 (lftp)
WindowsスタートメニューからPowershellのターミナルを起動し、ターミナルで`wsl`を開く

``` .bash
# WSLのディストリビューションを選択して起動
wsl -d Ubuntu-24.04
```

``` .bash
# パッケージをダウンロードしたディレクトリ (/mnt/c/以下がC:\以降のパスに相当）に移動する
cd /mnt/c/path/to/deb

# パッケージを解凍
sudo dpkg -i lftp4.9.2-2ubuntu1.deb

# aptパッケージ一覧の更新
sudo apt update

# lftpをインストール
sudo apt install lftp4.9.2-2ubuntu1.deb
```

#### サーバーの環境
OS: Amazon Linux 2023（AWS EC2インスタンスでクラウド環境にて起動）
サーバーには`vsftpd`を使う。
## 準備
### EC2のセキュリティルールでポートの開放をおこなう
![スクリーンショット (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1003831/0f9369fa-c9c0-4f83-ade2-e0b87c49df5b.png)

### SSL証明書をACMで設定する（設定済みとする）※FTPS（FTP over SSL/TLS）を行う場合
![スクリーンショット (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1003831/ff3f8684-dff2-411a-98db-12912d54d0c4.png)

## ftpクライアント（CLI）での疎通確認ができない
ftpコマンド等を使い、ローカルからの FTP接続を確認する。

``` .bash
# サーバーのユーザー名、ドメイン名、FTPのコントロールコネクションに使用するポートを指定して接続（アクティブモードで接続）
$ ftp -A <your_username>@ec2-12-34-567-890.ap-northeast-1.compute.amazonaws.com 21
Connected to ec2-35-79-153-134.ap-northeast-1.compute.amazonaws.com.
220 (vsFTPd 3.0.5)
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>

# 単一行の簡単な文字列を送信してみる
$ echo "Hello World" > ~/hello.txt
$ ftp -A <your_username>@ec2-12-34-567-890.ap-northeast-1.compute.amazonaws.com 21

# ローカルファイルをリモートマシン上のファイルに追加
$ ftp> append /home/local_username/hello.txt /home/your_username/hello.txt
local: /home/local_username/hello.txt remote: /home/your_username/hello.txt
```

### 失敗例 1
デフォルトのパッシブモードで接続すると、データコネクションの接続先のサーバー側のポートがランダムに指定されるため、EC2のセキュリティルールではじかれる。（下の例の場合、58099番ポートが指定されている）
```
229 Entering Extended Passive Mode (|||58099|)
ftp: Can't connect to `12.34.567.890:58099': Connection timed out
500 Illegal PORT command.
ftp: Can't bind for data connection: Address already in use
```

#### 対応
アクティブモードで接続。

``` .bash
# アクティブモードで接続
ftp -A <your_username>@ec2-12-34-567-890.ap-northeast-1.compute.amazonaws.com 21
```

### 失敗例 2
アクティブモードではデータコネクションに20番ポートが使われるが、コントロールコネクションの21番ポートしかEC2のセキュリティルールで開放していないため、はじかれる。<br>

※データコネクションとコントロールコネクションについては参考サイトを参照

```
500 Illegal PORT command.
ftp: Can't bind for data connection: Address already in use
```

#### 対応
セキュリティルールで20番ポートを開ける。

### 失敗例 3

```
421 Timeout.
```

#### 原因　クライアント側のファイアウォールがデータコネクションを拒否している
アクティブモードでwell-known port のポート番号（0～1023）をサーバーが指定した場合、クライアント側のファイアウォールによってブロックされる。

#### 参考

https://community.hpe.com/t5/operating-system-hp-ux/ftp-problem-421-service-no-available/td-p/3126898


> Once the TCP port (even higher than 1024) used by ftp client for ftp data connection is defined as a well-known port for specific network service, the ftp session will be reject by firewall.
(クライアントでFTPデータコネクションのために使用するTCPポート（たとえ1024番より大きくても）が特定のネットワークサービスのためのwell-knownポートとして一度定義されると、ftpのセッションはファイアウォールによって拒否されます。）


#### 対応　サーバーが指定するポート番号を変更する
デフォルトでは、クライアント側（ローカル）のポートが固定できないようになっているので、固定できるようにこの設定を変更する。具体的には、`vsftpd.conf`で`connect_from_port_20=YES`にする。<br>
その後、固定したいローカルのポート番号を`vsftpd.conf`の`ftp_data_port`で具体的に指定する。この値をwell-known port 以外のポート番号に設定する。（デフォルトでは、アクティブモードでサーバー側のデータコネクションが開始されるポートと同じ20に設定されてしまうため、ローカル側でwell-known portである20番ポートではデータを受け取ることができない）

```/etc/vsftpd/vsftpd.conf
# アクティブモードでデータコネクションを待ち受けるポートをサーバーが指定するようにする
connect_from_port_20=YES

# ローカル側でデータコネクションを待ち受けるポートの番号を変更する
# この場合、デフォルトの20番から20000番ポートに変更する
ftp_data_port=20000
```

#### vsftpd.confのconnect_from_port_20について
このオプションは、サーバー側のデータコネクションのポートと同じ20番のローカルで使うかどうかを設定する。これを`YES`にしないと、20番以外のものには変更できない。なので、これを`YES`にしたうえで、`ftp_data_port`の値を、データコネクションに使用したい好きなポート番号に指定する必要がある。

#### 参考

https://stackoverflow.com/questions/20785403/ftp-server-ports-in-active-mode-and-passive-mode

> For active mode, you can disable the connect_from_port_20 option in vsftpd.conf to cause the server to use ephemeral local ports for active (PORT) outgoing data connections. The default for this option is NO but most distributions' stock configuration files set it to YES. If this is set to YES you can also set the ftp_data_port option to use a fixed local data port other than 20.
(アクティブモードでは、（サーバー側で）アクティブな（PORTコマンドが）通信口から出ていくデータコネクションに対して、一時的なローカルのポートをサーバーが使用するようにvsftpd.confでconnect_from_port_20というオプションを無効にすることができます。このオプションのデフォルト値はNOですが、ほとんどのディストリビューションで保管されている設定ファイルではこれがYESになっています。この値がYESになっていれば、ローカルでデータを待ち受けるポートに、20番以外の固定値を使用するようにftp_data_port オプションをセットすることも可能です。)

### 失敗例 4

```
500 Illegal PORT command.
ftp: Can't bind for data connection: Address already in use
```

#### 原因 1　データコネクションのポートがすでに使用されている（`"Address already in use"`）
#### 確認すること 1：FTPのデータコネクションに使用しているサーバーのポートを使用しているサービスを確認する。
* サーバー側でデータコネクションの20番ポートを使用しているのがvsftpdであることを確認する。他のサービスが20番ポートを使用している、あるいは、vsftpdを含めてどのサービスも使用していないなら、そもそもサーバー側でvsftpdによってデータコネクションが確立されていない。
* 必要に応じて、そのプロセス（サービス）を解放する。

``` .bash
# サーバー側でFTPデータコネクションが使用する20番ポートをどのサービスも占有していない
$ netstat -nao | grep ":20"
$ 
```

#### 確認すること 2：FTPのデータコネクションに使用しているローカルのポートを使用しているサービスを確認する。
* ローカル側でデータコネクションの20000番ポート（vsftpd.confファイルの設定で変更したポート番号）を使用しているのがftpであることを確認する。
* 必要に応じて、そのプロセス（サービス）を解放する。

``` .bash
# ローカル側でFTPデータコネクションが使用する20000番ポートをどのサービスも占有していない
$ netstat -nao | grep ":20000"
$ 
```

以上のことから、次のことがわかる。
* サーバー側でvsftpdによってデータコネクションが確立されていない、あるいはサーバー側ではデータコネクションが確立されたが、その後にローカル側の20000番ポートで通信が失敗したため、サーバー側で自動的に20番ポートが解放されたのかもしれない。（ネットワーク分野に明るくない為、これ以上は追及しない）
* 少なくとも「データコネクションのアドレス（&ポート）がすでに使用されている (`"Address already in use"`)」のではない。

#### 原因 2　PORTコマンドが不正 (`"500 Illegal PORT command."`)
#### 【PORTコマンドとは？】

> The PORT command is issued by the client to initiate a data connection required to transfer data (such as directory listings or files) between the client and server. This command is used during “active” mode transfers.
(PORTコマンドはクライアントとサーバーの間でデータの転送要求（ディレクトリやファイルのリストを取得するなど）が行われたデータコネクションを初期化するためにクライアントが発行します。このコマンドは「アクティブ」モードの転送の間に使用されるものです。)

https://www.solarwinds.com/serv-u/tutorials/pasv-response-epsv-port-pbsz-rein-ftp-command

ということなので、そもそもデータコネクションが確立される前の、データコネクション初期化を行うためのPORTコマンドが失敗している、ということが分かります。

#### TCPの通信を確認

``` .bash
# 接続先へのパケットを確認
$ sudo tcpdump -A -p host 35.73.xx.xx
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes


^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
$ 
```

"`0 packets captured`" とあるので、FTPもパケットが出て行っていないことが分かります。たしかにエラーメッセージ通り、ローカルからアクティブモードのPORTコマンドが出ていっていないことになります。ファイアウォール（Windows/WSL）の問題か、後述のローカル側のグローバルIPアドレスに変換されないことが原因みたいです。（この問題は当投稿で修正可能。）

#### 色々調べていくと...
PORTコマンドが出ていくときのローカルのプライベートIPアドレスからグローバルIPアドレスへの変換をよしなにはかってくれない、というのが原因かもしれません。<br>


> EC2インスタンスに割り当てられたプライベートIPを送っているのですが、AWSがこの辺りをよしなに変換してくれるということはなく、相手側は送られたプライベートIPに接続しようとします。
その結果、当然接続できないので通信が正常に行えないという事になるのでした。

ソースは以下のリンクからです。こちらは別サーバーをFTPサーバーとして、クライアントのEC2からアクティブモードで接続している例ですが、私のWSLのUbuntuとEC2インスタンスのディストリビューションがおそらく違うだけで、リンク先の情報を信じればNATゲートウェイが存在する/しないにかかわらず ftp（あるいはftpに準ずる一般的なFTPクライアント）は、アクティブモードのデータコネクションにおいて、ローカルから出ていくIPアドレスを変更することができないようです。
<br>

https://qiita.com/ryuseino/items/a0442ec2ff5f04738b86

#### 対応案　別のFTPクライアントでローカルのグローバルIPアドレスを使用する
上記リンクにある「`lftp`」というFTPクライアントツールを利用します。`yum`や`apt`でインストール可能なようです。

#### lftpをインストールする

``` .bash
# yum でlftpをインストールする
$ sudo yum update
$ sudo yum install lftp


# aptでlftpを検索する

$ sudo apt search lftp
Sorting... Done
Full Text Search... Done
lftp/noble 4.9.2-2ubuntu1 amd64
  Sophisticated command-line FTP/HTTP/BitTorrent client programs


# または、

# aptでlftpをインストールする
$ sudo apt update
$ sudo apt install lftp
```

#### lftpでの接続と設定をする
`lftp`をインストールしたら、グローバルIPアドレスを指定してアクティブモードで接続します。以下の使い方はリンク元から参照しました。

``` .bash
# まずは接続先とユーザーを指定する
# -u オプションでユーザーを指定（接続先のユーザー）
lftp -u <your_username> example.jp

# パスワードを聞かれるので入力する
Password:
# パッシブモードをオフにする
set ftp:passive-mode off
# 相手に送るグローバルIPを指定する
set ftp:port-ipv4 xx.xx.xx.xx
```

lftpでのファイルアップロードなど、他の使い方は以下を参照します。（apt版）

https://www.server-world.info/query?os=Ubuntu_24.04&p=ftp&f=3#google_vignette

#### データコネクションを確認する

#### ケース 1："`Waiting for data connection...`" と表示される
以下のように、データコネクションが確立できていない場合、先述した失敗例 3 や後述する原因 3 のように、ローカルのデータコネクションを待ち受けるポートで通信が拒否されている可能性があります。その場合、各節に書いている対応をおこないます。（`vsftpd.conf`やファイアウォールの設定など）

``` .bash
# リモートホストのカレントディレクトリ表示
lftp <your_username>@ec2-35-73-xx-x.ap-northeast-1.compute.amazonaws.com:~> pwd
ftp://your_username@ec2-35-73-xx-x.ap-northeast-1.compute.amazonaws.com


# リモートホストのカレントディレクトリのファイル一覧表示

# データコネクションを確立できていない場合、以下のように "Waiting for data connection..." と表示されたままとなる（Ctrl+ C で中断）
lftp <your_username>@ec2-35-73-xx-x.ap-northeast-1.compute.amazonaws.com:~> ls
`ls' at 0 [Waiting for data connection...]
Interrupt
lftp <your_username>@ec2-35-73-xx-x.ap-northeast-1.compute.amazonaws.com:~>
```

ケース1のあと、ファイアウォールでポートを許可していても、このメッセージが出ます。ファイアウォールがちゃんと設定できていれば、ローカル側は問題ないと考えていいはずですが。<br>

#### 再びサーバー側を調べる
今度はサーバーの問題を考えてみます。手順は省略しますが、`lftp`でパッシブモードをオフにしてローカルのグローバルIPを指定し、データコネクションを待ち受けているときにnetstatを使ってポートを調べると、サーバー側でアクティブモードのコントロールコネクションとデータコネクションに使うポートである21番（LISTEN）と20番（SYNC_SENT）は正常に表示されました。

``` .bash
# サーバー側 Before（lftpでローカル側のポートを指定する前）

$ netstat -nao | grep ":21"
tcp6       0      0 :::21                   :::*                    LISTEN      off (0.00/0/0)
$ netstat -nao | grep ":20"
tcp6       0      1 172.31.3y.yyy:20        123.45.67.890:55441    SYN_SENT    on (1.00/2/0)
```

ここで注目すべきは、宛先のポート（クライアント側）が`55441`という、あらぬ値になっています。`lftp`でクライアント側のグローバルIPアドレスを指定しましたが、ここでポートの指定も必要なのかもしれません。`vsftpd.conf`でアクティブモードでデータコネクションを待ち受けるポートをデフォルトの20番から20000番に変更したので、`lftp`でも指定しないといけないのかもしれません。<br>
ちなみにクライアント側の`netstat`は以下のように、20番も20000番も何も表示されませんでした。

``` .bash
# アクティブモードのデータコネクションに使用するポートの状態を確認。

# vsftpd.confで設定した20000番ポートを確認
$ netstat -nao | grep ":20000"
$

# デフォルトの20番ポートを確認
$ netstat -nao | grep ":20"
$
```

#### `lftp`でローカルのポートを指定
本来は、「`1024-2000`」のようにポートの範囲を指定するために使う設定だと思いますが、とりあえず20000番ポートだけで確認できればいいので、「`20000-20000`」と指定します。`lftp`の設定コマンド（`set`）で設定できる内容は、[ここ](https://lftp.yar.ru/lftp-man.html)を見たら載っていました。

``` .bash
# ローカルのポートの範囲を指定（`<from-to>`の形式で指定する）
lftp <your_username>@ec2-35-73-xx-x.ap-northeast-1.compute.amazonaws.com:~> set ftp:port-range 20000-20000
```

#### すると...?

``` .bash
# アクティブモードのデータコネクションに使用するポートの状態を確認。

# vsftpd.confで設定した20000番ポートを確認
$ netstat -nao | grep ":20000"
$
$ netstat -nao | grep ":20"
tcp        0      0 172.30.xx.xxx:20000    0.0.0.0:*               LISTEN      off (0.00/0/0)
```

おお、`20000`ポートが開いていますね。（なぜか`20`で`20000`番ポートがヒットしましたが...）
ちなみにサーバー側もちゃんとローカルのグローバルIPアドレス（本記事ではマスクしていますが、`123.45.67.890`のIPがそれです）を指していました。

``` .bash
# サーバー側 After（lftpでローカル側のポートを20000番に指定した後）
$ netstat -nao | grep ":20"
tcp6       0      1 172.31.3y.yyy:20        123.45.67.890:20000    SYN_SENT    on (0.67/2/0)
```

#### だがしかし "Waiting for data connection..." と表示される
ここまでやってみましたが状況が変わらないので、クライアント側の `lftp`の設定は今のところ、ひとまずここまでとします。そのほかの原因の勘所でいうと、次は`vsftpd.conf`とかのサーバー側の設定ですかね。

#### lftpを使う方法の注意点
ただし、この方法では自宅のWi-FiのルーターがDHCPでIPアドレスを振っているような場合、いちいちそのIPアドレスを確認しないといけません。不具合などでルーターを再起動した際などは、値が変わってしまうので管理もめんどうです。IPアドレスを確認できるサイトはありますが。<br>

https://www.whatismyip.com/

そして何より、CLIベースのFTPクライアントツールでしかないため、あくまで簡単なデータの転送に利用を制限するほうがいいかもしれません。Linuxどうしであれば、ASCIIモードとかを気にしなくていいので複数行あるようなテキストファイルも送信することはできるかもしれません。私のようにWSLでやっている場合、WindowsのテキストエディタでCRLFで改行して保存したものをLinuxに送信してしまうと内容が崩れたり（ファイルが壊れたり）することもあり得ます。

#### 原因 3　指定したローカルのデータコネクションに使用するポートがwell-known ポートではないがファイアウォールでブロックされている
#### 対応案 1　特定の範囲内のポート番号はファイアウォールで許可する
* 以下では、ファイやウォールの設定をそのままかくわけにいかないため、ポートの範囲を例でかいていますが、本記事に則って書いた場合、20000番ポートを含むポート範囲を適宜、指定します。
* なお、接続元が`Anywhere`となっていますが、20000番ポートでデータコネクションが確立できることを確認したあとは、アクティブモードのFTPサーバーであるEC2の固定IP（Elastic IP）アドレスのみに許可することがセキュリティ上は望ましいと思います。

``` .bash
# 例）1024-2000の間のポート番号は許可するようにファイアウォールで設定する場合（Ubuntu）
$ sudo ufw status
Status: inactive

$ sudo ufw enable
Firewall is active and enabled on system startup

$ sudo ufw status
Status: active

$ sudo ufw allow 1024:2000/tcp
Rule added
Rule added (v6)

$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
1024:2000/tcp            ALLOW       Anywhere
1024:2000/tcp (v6)       ALLOW       Anywhere (v6)

```

#### 対応案 2　特定のポートをファイアウォールで許可する

* 対応案 1 をおこなってもうまくいかない場合、こちらの対応も行う。

* 先述の失敗例に対する対応のところで書いたように、データコネクションを待ち受けるポートとして、`vsftpd.conf`でデフォルトの20番から変更した20000番のポートのみに通信を許可するようにファイアウォールの設定を変更する。

#### lftpでローカルのファイルをアップロードする（上記「ケース 1：Waiting for data connection..." と表示される」が解決した場合）
例として、`hello.txt`というローカルのホームディレクトリにあるテキストファイルを転送します。テストしたいだけなので、ASCIIモードで単一行のテキストを送ります。`hello.txt`の中身は省略しますが、適宜ファイルを作成 &編集します。

``` .bash
# ASCIIモードでリモートユーザー (remote_user) のホームディレクトリにローカルのhello.txtというテキストファイルを転送する
lftp root@ec2-xx-xx-xxx-xxx.ap-northeast-1.compute.amazonaws.com:~> put -a -O /home/remote_user /home/username/hello.txt -o
```

## 補足
### ftpのバイナリモードについて
複数行をもつテキストファイルは、ローカルのOS（MicrosoftのWindowsなど）によって行末を表す仕方が違うので、別のオペレーティングシステム（UNIX系のLinuxなど）では検出することができないことがあります。なので大抵の場合は複数行をもつテキストファイルをFTPで転送する場合、バイナリモードではなくASCIIモードを使用するべきです。<br>
今回は、単一行のテキストを試験的に`ftp`コマンドでデータ転送できるかを確かめるため、単一行のファイル（文字列）をバイナリモードのFTPを使用して転送しても問題ないのですが、デフォルトで設定されているASCIIモードを使用します。

> FTP binary and ASCII transfer modes ensure file integrity during transfers. Misuse can corrupt files, affecting their functionality. Binary mode suits non-text files (e.g., images), while ASCII mode suits text files. Set default modes for efficiency and discover why some text files, like those using UTF-8 character encoding, may contain characters not supported by ASCII.
(FTPバイナリモードとASCII転送モードはファイルの正当性を保証するものです。これらを取り違えて使用すると、ファイルが破損することがあり、ファイル転送の機能に影響します。バイナリモードは非テキストファイル (例えば、画像ファイル)に適しています。一方ASCIIモードはテキストファイルに適しています。効率のいいデフォルトのモードをセットし、UTF-8の文字エンコーディングを使用したファイルのように、テキストファイルによってはASCIIによってサポートされない文字を含むことがあるのはなぜなのかを知っておきましょう。)

https://www.jscape.com/blog/ftp-binary-and-ascii-transfer-types-and-the-case-of-corrupt-files


### 参考サイト
#### パッシブモードについて
https://dev.classmethod.jp/articles/survey-ftp-clients-passive-mode-behavior/

http://cos.linux-dvr.biz/archives/131#google_vignette

#### FTPのバイナリモードについて
[Binary mode FTP and unexpected line endings -FTP binary and ASCII transfer types and the case of corrupt files](https://www.jscape.com/blog/ftp-binary-and-ascii-transfer-types-and-the-case-of-corrupt-files)

#### lftp
アクティブモードでデータコネクションを待ち受けるローカル側のグローバルIPアドレスを指定することができる希少な `lftp`というFTPクライアントツールは以下の記事で知りました。（海外の質問投稿サイトより）

https://serverfault.com/questions/1097305/cant-ftp-on-active-mode-from-ec2-instance-as-a-client