当第一次接触FTP的时候,肯定会为FTP的不同连接模式而苦恼,Active Mode
和Passive Mode
到底有什么样的不同?具体到网络中的防火墙又要怎么配置才能保证网络安全的同时获取正确的FTP访问?本篇文章给出了详细的解释。
FTP服务仅仅基于TCP协议,实现不包括UDP协议部分。FTP工作时同时使用了两个端口:数据(data)
端口和命令(command)
端口(又称为control port)。传统的FTP实现(Active Mode)用20
作为数据端口,21
作为命令端口;而被动模式(Passive Mode)会随机启用本机的一个端口作为数据监听端口。当使用不同的Mode(Active或者Passive)连接FTP时,数据端口总是会发生改变,这就是令人迷惑的地方。
控制端口和数据端口都可以指定。
主动模式(Active Mode)连接FTP时,客户端会在本地启用一个非预留端口N
(N > 1023),客户端从这个端口连接FTP Server端口21
并发送FTP command PORT N+1
到FTP Server,同时客户端会监听端口N+1
。FTP Server将通过本地的20
端口连接回客户端指定的监听端口N+1
。
从服务端防火墙的角度,为了支持主动模式的FTP访问,如下策略是必须是打开的:
这里的
N
和N+1
端口是示意端口,实际上并不一定满足+1
的关系。
主动模式的网络图:
第一步:客户端的命令端口连接服务端的命令端口,并且发送命令
Port 1027
。
第二步:服务发回ACK到客户端命令端口。
第三步:服务端数据端口(默认为20端口)初始化数据连接到客户端数据端口(监听端口,端口号第一步中已经通过命令端口发送给服务端)。
第四步:客户端数据端口发回ACK到服务端数据端口。
主动模式存在的问题:假设公网的网段为210.33.55.xxx
,局域网共享了210.33.55.2
这个IP,公网中有公共的ftp,当局域网中192.168.200.2
用户访问公共ftp下载时,如果连接方式为主动模式就会出现问题。因为公网中的ftp需要向210.33.55.2
这个路由的192.168.200.2
中开启的数据端口发送数据,很显然路由210.33.55.2
IP中找不到该数据端口。
那可能有的同学会有这样的疑问:我在家中用局域网(192.168.1.101)也能访问互联网啊,那说明公网IP的数据至少是能到达我的局域网IP(192.168.1.101)端口的,为什么这里就说公网IP找不到局域网中的数据端口?实际上,这里的关键是谁来初始化连接,我们在内网中访问互联网是本地(局域网)的客户端主动初始化互联网中某IP端口连接,一旦建立连接就可以进行双向通信。但是互联网不能主动初始化连接到局域网中某个监听端口,这也就是为什么主动模式访问部署在公网IP中的ftp不能连接成功,因为这种模式需要公网主动初始化连接到局域网数据监听端口。就像某个企业HR用内部电话打电话说让你去面试,电话通之后你们就能通讯了(局域网访问互联网通了之后,就能够互相通讯了)。但是,电话挂断之后你再打过去就是前台了(互联网访问局域网,只能访问到局域网共享的互联网出口),找不到之前给你打电话的人(因为是互联网服务主动初始化连接到局域网)。这里你的电话就是公网,企业的内部电话就是局域网,企业内部电话能主动联系到你,而你不能主动联系到企业内部的某个座机(当然了找前台报分机号,前台会帮你找,但是对于局域网-互联网模型,公网IP是无法初始化连接到内网的)。
使用ftp客户端工具设置主动连接到FTP服务,这里FTP服务的地址是ftp://xx.20.146.247:2121
,设置主动连接模式服务端数据端口为2020
(这里ftp使用的是Apache FTP,平时经常用的还有Vsftpd,相关主动、被动连接配置参考这里),FTP建立连接后服务端tcp连接如图:
客户端tcp连接如图:
这里我们可以看到服务端2120
端口和客户端60089
端口建立tcp连接,60089
作为客户端数据监听端口,由服务端2120
端口主动连接(这就是为啥称之为主动active模式)。
测试的时候可以传输一个大文件或者设置一下传输速率,这样能够有机会看到服务端和客户端tcp连接情况。Linux服务使用命令:
netstat -nalp |grep 2120
,Windows客户端使用命令:netstat -nao |findstr 2120
为了解决服务端要向客户端初始化数据连接的问题,被动模式应运而生。客户端可以通过PASSIVE ON
告诉服务端使用的连接方式是被动模式。
在被动模式中,命令端口和数据端口的初始化连接都是由客户端主动发起的,这样就解决了客户端防火墙可能过滤掉服务端数据端口发来的连接。当打开FTP连接时,客户端在本地启用了两个端口(N>1023和N+1)。第一个端口连向服务端命令端口21
,然后向服务端发出Pasv
命令而不是像主动模式那样发送PORT N+1
,这个PSAV
命令可以让服务端随机开启一个非预留端口(P>1023)并且将这个端口号返回给客户端回应PASV
命令。客户端就可以从端口N+1
向端口P
进行初始化连接来传输数据。
从服务端防火墙的角度,为了支持主动模式的FTP访问,如下策略是必须是打开的:
被动模式的网络图:
第一步:客户端连接服务端
21
并且发出PASV
命令。
第二步:服务端开启2024端口并监听来自客户端的数据连接,同时回应第一步PORT 2024
。
第三步:客户端从本地数据端口初始化数据连接到服务端指定数据端口2024。
第四部:服务端回应客户端ACK
(和数据)。
虽然被动模式解决了很多服务端的问题,但是服务端也产生了一些问题。最大的问题就是要求客户端能够连接到服务端任意大于1023的端口,不过幸运的是服务端的任意数据端口可以设定在一个范围内。
相关主动、被动连接配置参考这里
注意:
- 如果被动模式端口范围最大最小设置为一个固定的值,这个固定端口处于监听状态时,新的客户端不能用被动模式连接到ftp服务,原因还没有弄清楚。如果把端口设置为一个区间,就不会出现此问题。在更新这篇文章(2021-12-01)的时候,这个问题有了答案:假如你设置pasv port为一个固定的值,那么只能建立一个连接,因为客户端使用pasv模式连接服务端并传输数据的时候,设定的pasv port处于ESTABLISH状态,新的连接会尝试重新建立tcp连接(这里其实不太明白为什么不公用同一个pasv端口建立tcp数据连接,可能是数据传输tcp实现上的要求),这时候就会失败。也就是说pasv模式下,不同的连接用的是不同的服务端pasv端口。这也是pasv port要设定一个范围的原因吧。
- 当使用web浏览器或者Windows网络共享作为客户端访问FTP时,经常会输入
ftp://ip:port
,大部分浏览器只支持被动模式。- Windows系统资源管理器输入ftp地址默认是被动模式,可以在IE中设置为主动模式。
使用ftp客户端工具设置被动连接到FTP服务,这里FTP服务的地址是ftp://xx.xx.19.68:2121
,被动模式pasv端口没有做限制。
下图展示一个客户端用pasv模式连接ftp并传输数据时,服务端查看tcp连接情况:
新增一个客户端pasv模式连接并传输数据,服务端查看tcp连接如图
上二图我们可以看出,2个pasv模式连接到ftp服务客户端传输数据时,服务端用了2个不同的pasv端口,分别是连接1的36369
和连接2的44219
主动与被动ftp工作连接方式:
Active FTP:
command: client >N --> server 21
data : client >N+1 <-- server 20
Passive FTP:
command: client >N --> server 21
data : client >N+1 --> server P(P>1023)
通俗的语言将主动模式与被动模式的区别:
主动模式:客户端对服务端说,我给你发一个端口,这个端口在我本地监听,我需要数据的时候会通过命令端口发送命令,然后你把数据主动推送到我这里!
被动模式:客户端对服务端说,我给你发一个PASV
命令,这样我就约定好了用被动模式连接了,你启动一个监听端口吧,我需要数据的时候会去你那里取的,你需要被动传数据到我这里!
注意问题:
现场银河麒麟FT2000+ SM服务器,装有Apache FTP,相关配置为:
<active enable="true" local-address="0.0.0.0" local-port="9999" ip-check="true" />
<passive ports="19000-20000" address="0.0.0.0" external-address="0.0.0.0" />
客户端连接代码为:
FTPClient ftpClient = new FTPClient();
ftpClient.setControlEncoding(config.getEncoding());
ftpClient.setConnectTimeout(config.getConnectTimeout());
try {
//连接ftp服务器
ftpClient.connect(config.getHost(), config.getPort());
//获取返回代码
int replyCode = ftpClient.getReplyCode();
//如果没连上
if (!FTPReply.isPositiveCompletion(replyCode)) {
ftpClient.disconnect();
log.warn(FTPSERVER_REFUSED_CONNECTION, replyCode);
return null;
}
//如果用户名密码不对
if (!ftpClient.login(config.getUsername(), config.getPassword())) {
log.warn(FTP_CLIENT_LOGIN_FAILED, config.getUsername(), config.getPassword());
}
//设置一些参数
ftpClient.setBufferSize(config.getBufferSize());
ftpClient.setFileType(config.getTransferFileType());
if (config.isPassiveMode()) {
ftpClient.enterLocalPassiveMode();
}
} catch (IOException e) {
log.error(CREATE_FTP_CONNECTION_FAILED, e);
}
//下载文件
InputStream inputStream = null;
try {
// 验证FTP服务器是否登录成功
if (!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) {
log.error(FTP_CONNECTED_FAILED);
throw new IllegalArgumentException(FTP_CONNECTED_FAILED);
}
String ftpIsPassive = ArteryConfigUtil.getProperty("ftp.isPassive");
ftpClient.enterLocalPassiveMode();//使用passive模式连接
log.info(MSG_START_DOWNLOADING, remotePath);
inputStream = ftpClient.retrieveFileStream(remotePath);
log.info(MSG_END_DOWNLOADING, remotePath);
} finally {
if (null != ftpClient && ftpClient.isConnected()) {
try {
log.info("退出ftp");
ftpClient.logout();
log.info("断开ftp");
ftpClient.disconnect();
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
使用上面的代码下载文件流,报错:
Exception in thread "main" java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
...
at java.net.SocketsSocketImpl.connetct(SocketsSocketImpl.java:394)
at java.net.Socket.connetct(Socket.java:606)
at org.apache.commons.net.ftp.FTPClient._openDataConnection_(FTPClient.java:924)
at org.apache.commons.net.ftp.FTPClient._retriveFileStream(FTPClient.java:1984)
at org.apache.commons.net.ftp.FTPClient.retriveFileStream(FTPClient.java:1971)
现场的Apache FTP所在服务器端口19000-20000
确认没有防火墙策略,是可达的。代码执行到开始下载文件时,用netstat
命令查看Apache FTP进程已经监听了19000-20000
的端口,但报错似乎提示客户端初始化连接该端口就被“拒绝连接”。
使用WinSCP
工具和Apache FTPClient在被动连接模式下效果一样,但是用curl
命令行工具被动模式下能够下载成功:curl -o a.obj ftp://xxx:xxx@xxx:xxx/xxx
。
网上查了很多资料,没有验证成功。现场抓包工具不可用,以后需要时再用Fiddler
、Wireshark
、tcpdump
等抓包工具进一步排查tcp
初始化连接情况。
上述代码注释掉ftpClient.enterLocalPassiveMode()
使用主动模式连接下载文件流没有问题。
上面“6.1”如果问题是端口可达性问题的话,那么可以把ftp服务端和客户端放在一起,这样肯定就不会出现端口不可达的情况了。测试后发现报错:
Writing File failed with: File operation failed... Host attempting data connection 127.0.0.1 is not the same as server 141.151.1.62
可以有两种解决办法:
127.0.0.1
,这样就不会不一致了,能正常工作。FTPClient.setRemoteVerificationEnabled(false)
。参考:https://stackoverflow.com/questions/57164983/how-to-handle-host-attempting-data-connection-x-x-x-x-is-not-the-same-as-serverReference