ネットの海の片隅で

技術ネタの放流、あるいは不法投棄。

EC2からactive modeでFTP通信ができない件

問題

Local

require 'net/ftp'

ftp = Net::FTP.new("example.com")
ftp.login("username", "password")

ftp.list # => Array[String]

期待通りに動きます。

EC2

require 'net/ftp'

ftp = Net::FTP.new("example.com")
ftp.login("username", "password")

ftp.list # => Errno::EPIPE: Broken pipe

このコードは正常に動作せず、例外を投げます。

Note: PASVモード
require 'net/ftp'

ftp = Net::FTP.new("exmample.com")
ftp.passive = true # Use passive mode
ftp.login("username", "password")

ftp.list # => Array[String]

EC2でもコレは動くので、接続先のFTPサーバがPASVモードを受け入れてくれるならPASVで繋げばOK。

何が起こっているのか

コントロールコネクション

FTPクライアントがサーバとの間にコントロールコネクションを確立するまでには、概ねこんなことが起こっています。図がテキトーなのはご容赦ください*1

f:id:s_osa:20150224125918p:plain

  1. EC2上のクライアントがサーバに接続要求を送ろうとする。
  2. AWSのNATが送信元アドレスをプライベートアドレスからパブリックアドレスに書き換えてサーバに送る。
  3. サーバが返答をパブリックアドレス宛てに送り返す。
  4. NATが送信先アドレスをEC2のプライベートアドレスに書き換えてクライアントに届ける。

NATが正常に仕事をしているため、コントロールコネクションが正常に確立されます*2

データ転送コネクション

Activeモードでデータ転送コネクションの確立を試みるときは、概ねこんなことが起こっているはずです。

f:id:s_osa:20150224131933p:plain

  1. EC2上のクライアントがサーバにFTPのPORTコマンドを送ろうとする。
  2. AWSのNATが送信元アドレスをプライベートアドレスからパブリックアドレスに書き換えてサーバに送る。ただし、PORTコマンドで指定した接続先アドレスとポートはTCPセグメントのヘッダ部ではなくデータ部に入っているため、書き換えられない。
  3. サーバがPORTコマンドで指定されたアドレス・ポートに接続しようとするが、プライベートアドレスであるため接続に失敗する。

つまり、PORTコマンドでプライベートアドレスをサーバに送ってしまっているのが原因であって、EC2固有の問題ではなく一般的なNAT越えの問題です。

解決のための方針

PORTコマンドでパブリックアドレスを送る

正攻法です。

EC2の場合、アドレスにEIPを指定して、ポートにSecurity Groupで許可してあるポートを指定すれば良いと思います。

ちなみに、RubyのNet::FTPクラスには任意のPORTコマンドを設定するメソッドはありません。

NATがPORTコマンドのIPアドレスを書き換える

市販のルータ製品にはFTPのPORTコマンド中に含まれているIPアドレスに対してNATと同様の処理を行なってくれる製品が結構あります*3

しかし、EC2を使っている場合にはそういった機能は使えません。

仮に無理矢理実現するとなると、VPC内にプロキシを立ててEC2のデフォルトゲートウェイをそのプロキシに設定、プロキシにPORTコマンドを書き換える機能を実装するといった流れになると思います。辛い。

まとめ

いにしえのプロトコルFTPは現代においては少し辛いものがある。

解決したらまたエントリ書きます。

追記

Rubyだけですが、解決策を書きました。

*1:描くのが楽そうだったのでシーケンス図を使いましたが、シーケンス図として見ないでください。

*2:PASVモードのときはデータ転送コネクションもこの形式で確立されるため、正常にデータ伝送ができます。

*3:みんなだいすきRTX1210とか