如何调试为什么 PHP FTP 在 PASV 模式下无法工作,而控制台 FTP 似乎工作正常?

How to debug why PHP FTP won't work in PASV mode, when console FTP seems to work fine?

我有一个 Docker Compose 系统用于测试,我在其中对单页 Web 应用程序进行端到端测试。网站中的几个按钮将导致在一个容器 (missive-transmitter) 中启动 FTP 连接,然后转到另一个容器 (missive-testbox) 中的测试 FTP 服务器。

我在 PHP 中的 FTP 逻辑总是使用“被动”模式,我认为这是导致问题的原因。我在 missive-transmitter 中创建了一个 运行 的脚本,这是真实事物的简化版本。如下,直接从控制台运行:

<?php
# ftptest.php

error_reporting(-1);
ini_set('display_errors', true);

$conn = ftp_connect('missive-testbox', 21);

$ok1 = ftp_login($conn, 'missive_test', 'password');
if (!$ok1)
{
    die("Cannot log in\n");
}

// *** Start problem section
$ok2 = ftp_pasv($conn, true);
if (!$ok2)
{
    die("Cannot switch to passive mode\n");
}
// *** End problem section

$info = ftp_systype($conn);
echo "Info: $info\n";

$ok3 = ftp_put($conn, 'ftptest.php', 'ftptest.php', FTP_ASCII);
if (!$ok3)
{
    die("Cannot send a file\n");
}

现在,如果我删除 *** 部分(启用被动模式),脚本就会运行。如果我把它留在里面,我会得到这个:

Info: UNIX

Warning: ftp_put(): php_connect_nonb() failed: Operation in progress (115) in /root/src/ftptest.php on line 23

Warning: ftp_put(): TYPE is now ASCII in /root/src/ftptest.php on line 23

Cannot send a file

我希望我的 FTP 操作在 PASV 模式下工作。

奇怪的是,如果我安装一个 FTP 客户端,那么它似乎可以在主动或被动模式下工作,这是我不明白的。 missive-transmitter 方:

~/src $ # This is the `sh` shell in `missive-transmitter`
~/src $ #
~/src $ # Install LFTP in Alpine environment
~/src $ apk add lftp
~/src $ lftp missive_test@missive-testbox
Password: 
lftp missive_test@missive-testbox:~> set ftp:passive-mode off         
lftp missive_test@missive-testbox:~> put ftptest.php       
457 bytes transferred                            
lftp missive_test@missive-testbox:/> set ftp:passive-mode on 
lftp missive_test@missive-testbox:/> put ftptest.php        
457 bytes transferred
lftp missive_test@missive-testbox:/> 

PHP 是在做一些不同的事情,还是我实际上没有在控制台客户端中使用 PASV 模式?

我已经确认两个容器可以从各自的 sh 控制台 ping 彼此。他们在同一个(自定义)Docker 网络上。

missive-testboxDocker容器是基于gists/pure-ftpd的,所以据我所知应该配置正确。

更新

下面答案中的一个有用点是关于 NAT 如何使一方使用错误的 IP 地址建立连接。然而,IP 地址似乎在同一个子网上,尽管我不是网络专家。

来自missive-transmitter

~ # ping missive-testbox
PING missive-testbox (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.076 ms

并且来自 missive-testbox

~ # ping missive-transmitter
PING missive-transmitter (172.19.0.4): 56 data bytes
64 bytes from 172.19.0.4: seq=0 ttl=64 time=0.119 ms

认为 他们都是 172.19.0.x 地址的事实意味着他们应该能够完全看到对方,尽管我愿意根据这个假设进行更正。

更新 2

有人建议获取一些 FTP 客户端或服务器日志是调试此问题的好方法。客户端很简单。这是与上面相同的操作,但是在 LFTP 的调试模式下。

主动模式优先:

~/src # lftp -d missive_test@missive-testbox
Password: 
---- Resolving host address...
---- 1 address found: 172.19.0.2
lftp missive_test@missive-testbox:~> set ftp:passive-mode off
lftp missive_test@missive-testbox:~> put ftptest.php
---- Connecting to missive-testbox (172.19.0.2) port 21
<--- 220-Welcome to Pure-FTPd.
<--- 220-You are user number 1 of 5 allowed.
<--- 220-Local time is now 17:54. Server port: 21.
<--- 220-This is a private system - No anonymous login
<--- 220-IPv6 connections are also welcome on this server.
<--- 220 You will be disconnected after 15 minutes of inactivity.
---> FEAT
<--- 530 You aren't logged in
---> AUTH TLS
<--- 500 This security scheme is not implemented
---> USER missive_test
<--- 331 User missive_test OK. Password required
---> PASS XXXX
<--- 230 OK. Current directory is /              
---> FEAT
<--- 500 Unknown command
---> PWD
<--- 257 "/" is your current location
---> TYPE I
<--- 200 TYPE is now 8-bit binary
---> PORT 172,19,0,4,159,62
<--- 200 PORT command successful
---> ALLO 457
<--- 500 Unknown command
---> STOR ftptest.php
---- Accepted data connection from (172.19.0.2) port 20
<--- 150 Connecting to port 40766
---- Closing data socket
<--- 226-File successfully transferred
<--- 226 0.000 seconds (measured here), 3.16 Mbytes per second
---> SITE UTIME 20171030154823 ftptest.php
<--- 500 Unknown command
---> SITE UTIME ftptest.php 20171030154823 20171030154823 20171030154823 UTC
<--- 500 Unknown command
457 bytes transferred

OK,成功了。这里是LFTP中的被动版本,再次成功。

我注意到开头的警告,关于地址需要修复 - 这可能相关吗?如果任何一方将自己宣传为“本地主机”,那可能是个问题 :-):

lftp missive_test@missive-testbox:/> set ftp:passive-mode on 
lftp missive_test@missive-testbox:/> put ftptest.php        
---> PASV
<--- 227 Entering Passive Mode (127,0,0,1,117,54)
---- Address returned by PASV seemed to be incorrect and has been fixed
---- Connecting data socket to (172.19.0.2) port 30006
---- Data connection established
---> STOR ftptest.php
<--- 150 Accepted data connection
---- Closing data socket
<--- 226-File successfully transferred
<--- 226 0.000 seconds (measured here), 1.79 Mbytes per second
457 bytes transferred

很难说这里进行了哪些FTP操作。但可能是 PHP 使用 PASV 而 lftp 使用 EPSV 设置被动模式。

PASV 的情况下,服务器会发送 IP 地址和端口号,等待连接。使用 EPSV 服务器仅提供端口号,目标 IP 地址是来自当前 FTP 控制连接的 IP 地址。如果涉及 NAT(网络地址转换)(这在 Docker 设置中并非不可能),与从 FTP 客户端外部可见的 IP 地址相比,服务器可能会看到一个不同的内部 IP 地址作为自己的 IP 地址,这意味着客户端无法连接到 PASV 命令的响应中给出的(错误的)IP 地址。对于 EPSV,此问题不存在,因为客户端不使用服务器提供的 IP 地址作为目标。

结合有用的答案和评论,加上来自 LFTP 的警告,我找到了解决方案。正如 Steffen 推测的那样,该问题与 PASV 模式下的 IP 可见性有关。 LFTP 客户端已帮助检测到我的服务器配置错误,因此透明地对其进行了修复,这就是引起混淆的地方。

值得注意的是 PHP 不是那么聪明或善良 - 它没有进行此类修复(当然这在技术上是正确的)。

该配置的原因是 default Dockerfile settings:

ENV PUBLIC_HOST localhost

所以,我现在所做的是用 LAN IP 替换我自己的 Dockerfile 中的这个:

ENV PUBLIC_HOST 172.19.0.2

稍后重新启动,修复消息从 LFTP 消失,我的 PHP 脚本工作。

更新

由于不知道是否可以依赖上述IP地址的静态性,我已经将其重新设置为容器的名称:

ENV PUBLIC_HOST missive-testbox

这似乎工作正常。