我们出现新的需求,要把以前的 FTP 服务器迁移到 NAT 之后的一台机器上。但是,FTP 不仅用到 20 21 端口, PASV 还会用到高端口,这给端口转发带来了一些麻烦。我们一开始测试,直接在 Router 上转发 20 和 21 端口到 Server 上。但是很快发现, Filezilla 通过 PASV 获取到地址为 (内网地址,端口高8位,端口低8位),然后,Filezilla 检测出这个地址是内网地址,于是转而向 router_ip:port 发包,这自然是不会得到结果的。

此时我们去网上找了找资料,找到了一个很粗暴的方法:

iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 20 -j DNAT --to-destination internal_ip:20
iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 21 -j DNAT --to-destination internal_ip:21
iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 1024:65535 -j DNAT --to-destination internal_ip:1024-65535

有趣地是, macOS 自带的 ftp 命令(High Sierra似乎已经删去)可以正常使用。研究发现,它用 EPSV(Extended Passive Mode) 代替 PASV ,这里并没有写内网地址,因而可以正常使用。

这么做, Filezilla 可以成功访问了。但是,用其它客户端的时候,它会直连那个内网地址而不是 Router 的地址,于是还是连不上。而且,使用了 1024-65535 的所有端口,这个太浪费而且会影响我们其它的服务。

我们开始研究我们 FTP 服务器(pyftpdlib)的配置。果然,找到了适用于 FTP behind NAT 的相关配置:

     - (str) masquerade_address:
        the "masqueraded" IP address to provide along PASV reply when
        pyftpdlib is running behind a NAT or other types of gateways.
        When configured pyftpdlib will hide its local address and
        instead use the public address of your NAT (default None).
     - (dict) masquerade_address_map:
        in case the server has multiple IP addresses which are all
        behind a NAT router, you may wish to specify individual
        masquerade_addresses for each of them. The map expects a
        dictionary containing private IP addresses as keys, and their
        corresponding public (masquerade) addresses as values.
     - (list) passive_ports:
        what ports the ftpd will use for its passive data transfers.
        Value expected is a list of integers (e.g. range(60000, 65535)).
        When configured pyftpdlib will no longer use kernel-assigned
        random ports (default None).

于是,我们配置了 masquerade_address 使得 FTP 服务器会在 PASV 中返回 Router 的地址,并且在 passive_ports 中缩小了 pyftpdlib 使用的端口范围。

进行配置以后,我们在前述的 iptables 命令中相应修改了端口范围,现在工作一切正常。