FTP 被动模式为何导致防火墙拦截数据连接
FTP 可正常登录,但目录列表卡在 150 响应后超时。被动模式还需放行独立数据端口,本文说明如何声明固定端口范围并配置防火墙规则。
FTP 可以登录,但目录列表一直卡住的原因
FTP 被防火墙拦截时,被动模式会失效,因为 FTP 使用两条 TCP 连接,而不是一条。连接到 21 端口的连接承载登录操作和命令,因此用户名和密码可以通过,防火墙看起来也配置正确。第一次 ls 随后需要在另一个端口上建立第二条连接。防火墙没有放行这条连接,因此客户端会一直等待,直到超时。
解决方法是为这些数据连接指定固定端口范围,并添加允许同一端口范围的防火墙规则。位于 NAT(网络地址转换)之后的服务器还需要额外设置,以便通告正确的地址。过去,连接跟踪辅助程序会自动完成这项工作。现在已不再如此。在照搬旧指南之前,值得先了解原因。
控制通道和数据通道
FTP(文件传输协议)在 RFC 959 中定义,早于 NAT 和有状态防火墙。一次会话会与 TCP 端口 21 建立一条控制连接,并在整个会话期间保持连接。命令以明文发送。响应由三位数字代码和一行文本组成。这条连接不会传输文件内容。
每项数据都使用独立的 TCP 连接:目录列表使用一条连接(LIST),每次下载使用一条连接(RETR),每次上传使用一条连接(STOR)。连接建立后只使用一次,然后关闭。身份验证完全在控制通道上进行,因此数据路径中断时,表现始终相同:登录成功,随后连接卡住。如果客户端输出 230 响应后,在获取目录列表时停止,说明问题出在数据通道,而不是凭据。
主动模式:服务器回连客户端
在主动模式下,客户端选择一个端口,在该端口上监听,然后告诉服务器连接目标:
PORT 192,168,1,50,195,80前四个数字是客户端的 IP 地址。后两个数字表示端口,并编码为两个字节:195 * 256 + 80 = 50000。随后,服务器从自己的 20 端口向客户端的 50000 端口建立数据连接。
从客户端的角度看,这是一个入站且未经请求的连接,因此客户端防火墙会将其丢弃。如果客户端位于家用路由器后,PORT 命令中的地址是服务器完全无法访问的私有地址。FTP 之所以长期被认为无法正常工作,主动模式就是主要原因。
被动模式:客户端同时打开两条连接
被动模式反转了数据连接的方向。客户端发送 PASV,服务器返回自己的地址和端口:
227 Entering Passive Mode (203,0,113,10,195,80)编码方式相同,因此客户端会连接到 203.0.113.10 的 50000 端口。此时两条连接都由客户端打开,这就是被动模式可以穿过客户端 NAT 的原因,也是当前所有客户端都会优先请求被动模式的原因。
问题只是转移了,并没有消失。现在,未经请求的入站连接会到达您的服务器,并使用每次传输都会变化的高位端口。防火墙由您管理,因此问题也由您负责。
EPSV(扩展被动模式,RFC 2428)采用相同的思路,但响应格式更简洁:
229 Entering Extended Passive Mode (|||50000|)响应中没有地址。客户端会重复使用控制连接所使用的地址,因此可以在 IPv6 上正常工作,并消除一整类 NAT 错误。curl 手册指出,curl 通常会先尝试 EPSV,再尝试 PASV。端口仍在运行时选择,因此 EPSV 不会改变您的防火墙规则。
为什么普通防火墙规则无法允许数据通道
因为编写规则时,端口号还不存在。服务器会为每次传输动态选择端口。vsftpd 的默认配置将 pasv_min_port 和 pasv_max_port 记为 0,含义是“使用任意端口”,因此数据连接可能从任何大于 1023 的端口到达。sudo ufw allow 21/tcp 只允许控制通道,不允许其他连接;这正是登录成功但目录列表无法显示的原因。如果您仍不清楚服务监听固定端口的含义,请参阅Linux 中端口和监听套接字的工作原理,了解相关背景。
有状态防火墙确实会跟踪连接,内核也可以将新连接识别为现有连接的 RELATED。但对 FTP 而言,必须有程序读取控制流,并从 227 或 PORT 行中提取端口号。默认情况下没有任何程序会执行此操作。
为什么 FTP 连接跟踪辅助模块不再是解决方案
旧版指南通常会让你使用内核模块 nf_conntrack_ftp。它读取明文控制通道,找到其中通告的端口并注册一个期望项,因此无需为该端口单独编写规则,数据连接也能通过。自这些指南发布以来,情况发生了 4 项变化。
自动分配辅助模块的功能已关闭,在当前内核中也无法重新启用。内核在 2016 年默认禁用了该功能。Pablo Neira Ayuso 随后于 2022 年 8 月提交了 “netfilter: remove nf_conntrack_helper sysctl and modparam toggles”,彻底移除了该开关,并说明“用户现在必须通过规则集显式配置连接跟踪辅助模块”。nf_conntrack_helper sysctl 在 Linux 5.19 中存在,从 Linux 6.0 起已被移除,内核的连接跟踪 sysctl 文档也不再列出它。仅加载该模块不会产生任何效果。
你可以检查当前使用的内核类型。运行 sysctl net.netfilter.nf_conntrack_helper。如果返回 sysctl: cannot stat /proc/sys/net/netfilter/nf_conntrack_helper: No such file or directory,表示该内核已不再提供可启用的自动辅助模块分配功能。如果返回一个数字,表示你使用的是早于 6.0 的内核,该开关仍然存在,其默认值为 0。
防火墙前端也已弃用此功能。Ubuntu 24.04 中的 man ufw-framework 在 /etc/default/ufw 的 IPT_MODULES 行说明:“以这种方式无条件加载连接跟踪模块(nf_conntrack_*)已弃用”,并补充说明辅助模块规则“必须在 RULES FILES 中管理”。firewalld 在 firewalld.conf 中将 AutomaticHelpers 标记为:“已弃用。此选项会被忽略,不再使用。”现在附加辅助模块意味着手动编写带有 CT target 的显式规则。这比下面的修复方法更繁琐,并且启用 TLS 后会立即失效。Ubuntu 上的 iptables 和 nftables介绍了这些规则实际存放的位置。
TLS 会使这一机制失效。辅助模块依靠读取文本形式的控制通道工作。控制通道加密后,辅助模块看到的只是密文,因此无法找到端口。这个问题无法修复,也不应修复:能够读取控制通道的中间设备,也就读取了你的密码。
在服务器上声明被动端口范围
所有 FTP 服务器都可以配置为从指定范围内选择被动端口。选项名称各不相同,因此请查阅实际运行的服务器文档。
vsftpd 配置位于 /etc/vsftpd.conf:
pasv_enable=YES
pasv_min_port=30000
pasv_max_port=30099pasv_enable 已默认为 YES。两个端口选项的默认值为 0,也就是上文所述的“使用任意端口”行为。使用 sudo systemctl restart vsftpd 应用配置,然后通过 systemctl status vsftpd 确认服务已恢复运行。vsftpd 无法解析配置行时会拒绝启动,而不是忽略该行。因此,如果重启失败,请查看 journalctl -u vsftpd -n 20,找到标出刚输入选项的 500 OOPS: 行。
ProFTPD 配置位于 proftpd.conf:
PassivePorts 30000 30099ProFTPD 未在此处说明默认值:未设置该指令时,由内核选择端口。ProFTPD 文档还说明,如果指定范围内没有可用端口,服务器会回退到由内核分配的端口,并记录一条消息。因此,范围过小会导致传输偶尔失败,而不是明确失败,这更难诊断。请使用非特权端口,即 1024 及以上的端口。
Pure-FTPd 使用选项 -p first:last。在 man pure-ftpd 中,该选项说明为“被动模式下载仅使用从起始端口到结束端口(含)的范围”,并且“可使 pure-ftpd 更好地兼容数据包过滤器”。打包版本通常会在配置文件中封装该选项,因此请查阅发行版自身的文档确定文件名,不要猜测。
需要多少个端口?每个并发数据连接需要一个端口。关闭的 TCP 端口会在 TIME_WAIT 中停留几分钟,之后才能重新使用,因此应按预期峰值的数倍预留端口。100 个端口足以应对少量用户,繁忙的公网服务器则需要更多。vsftpd 在这方面不如 ProFTPD 宽容:其源代码会在范围内尝试少量随机端口;如果所有端口都已占用,它会以 500 OOPS: vsf_sysutil_bind 结束会话,而不是借用范围外的端口。对于任一服务器,端口范围过小都会在最繁忙时刻中断传输。
端口范围应放在哪里?先运行 sysctl net.ipv4.ip_local_port_range。在标准 Ubuntu 系统上,该命令会显示 32768 60999,即内核为出站连接分配的端口范围。被动端口范围如果位于该窗口内,可能与已占用该端口的出站连接冲突,因此应将范围置于该窗口以下。默认系统中的 30000 到 30099 没有冲突。请检查自己的系统,不要直接相信这个数值。
在防火墙中开放相同的端口范围
ufw 使用冒号表示范围,其手册指出,范围或列表“也可用于指定多个端口;在这种情况下必须指定协议”:
sudo ufw allow 21/tcp
sudo ufw allow 30000:30099/tcp
sudo ufw status verbose现在,ufw status verbose 应列出两个条目。不带 /tcp 执行相同命令会被拒绝,并显示要求指定 tcp 或 udp 的错误,因为 ufw 不会自行猜测。VPS 上的 ufw 规则语法介绍了其余内容。
firewalld 使用连字符表示范围,并且需要重新加载:
sudo firewall-cmd --permanent --add-service=ftp
sudo firewall-cmd --permanent --add-port=30000-30099/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-all--add-service=ftp 会开放 21/tcp,并请求 ftp helper;随附的服务定义使用的正是这个名称。它不会开放您的被动端口范围,因此单独使用时,结果与最初完全相同。VPS 上的 firewalld 区域和服务介绍了更完整的内容。
直接使用 nftables,在 input 链中:
tcp dport { 21, 30000-30099 } accept还要注意另一层防火墙:大多数服务商会在控制面板中运行网络防火墙,位于操作系统之外。如果服务器上的规则看起来正确,但数据包仍然完全无法到达,请同时在那里开放相同的端口范围。
服务器位于 NAT 后时,指定其公网地址
在服务器上运行 ip -4 addr show。如果网卡上的地址就是客户端连接使用的地址,请跳过本节。如果网卡使用私有地址(10.x、172.16 到 172.31.x、192.168.x),而平台将公网地址映射到该地址,服务器就不知道自己的公网地址。vsftpd 将 pasv_address 的默认行为记录为“地址取自传入的已连接套接字”,因此 227 响应会携带私有地址,客户端也会被发送到无法访问的位置。
FileZilla 对此问题有明确的名称:
Server sent passive reply with unroutable address. Using server address instead.FileZilla 会修正该地址并继续传输。许多其他客户端不会这样处理。它们会连接 10.0.0.5,然后一直等待。
curl 也会隐藏这个问题。如果您使用 curl 进行测试,这一点很重要。其手册说明,--ftp-skip-pasv-ip“默认启用(在 7.74.0 中加入)”,因此 curl 会忽略 227 响应中的地址,并重新使用控制连接的地址。传输即使能在 curl 中成功,也可能仅因这个原因在图形客户端中失败。
请显式设置该地址。vsftpd 使用 pasv_address=203.0.113.10;如果您希望填写主机名,还可以使用 pasv_addr_resolve=YES(默认值为 NO)。ProFTPD 使用 MasqueradeAddress,它接受地址、DNS 名称或网卡名称。Pure-FTPd 使用 -P,其文档说明该选项适用于“服务器位于伪装(NAT)设备之后”的情况。EPSV 的响应不包含地址字段,因此可以避免整个问题;但不能依赖这一点,因为要发送哪个命令由客户端决定。
TLS 的变化
FTPS 是通过 TLS(传输层安全协议)运行的 FTP。客户端照常连接 21 端口,发送 AUTH TLS 以保护控制通道,然后发送 PROT P,同时加密数据通道。普通 FTP 会以可读文本在网络上传输密码,因此如果必须继续使用 FTP,应运行 FTPS。vsftpd 发布时,ssl_enable 的默认值为 NO。
这会带来两点影响。连接跟踪辅助程序将无法工作,这正是上一节从另一侧看到的结果。此外,sudo tcpdump -nAi any 'tcp port 21' 将不再显示 227 响应。因此,需要确认服务器通告的地址和端口时,应读取服务器自身的日志,而不是查看网络传输内容。
从外部测试更改
在另一台机器上运行这些命令。从服务器本身测试会绕过您正在修复的防火墙。
sudo ss -ltnp | grep :21
curl -v --disable-epsv --user ftpuser:secret ftp://example.com/
nc -vz example.com 30000ss 应显示 FTP 守护进程正在监听端口 21。服务器空闲时,被动端口范围内没有进程监听,因为这些套接字会在传输开始时创建,并在传输结束后关闭。
--disable-epsv 强制 curl 使用 PASV 路径。该路径会暴露地址问题。跟踪输出会先显示服务器的响应,然后显示 curl 要连接的地址和端口:
< 227 Entering Passive Mode (203,0,113,10,117,52)117 * 256 + 52 = 30004,位于声明的范围内。如果该行显示的是私有地址,说明未设置 pasv_address。如果端口超出您的范围,说明服务器从未读取配置更改。请确认您编辑的是正在运行的服务所使用的文件。
nc 可以单独回答防火墙问题。立即出现 Connection refused 表示数据包已到达服务器,但没有进程监听该端口。对于空闲的被动端口,这是正确结果,说明您的规则有效。如果连接一直挂起,直到 nc 放弃,说明某个组件静默丢弃了数据包。这是防火墙问题,可能是服务器上的防火墙,也可能是服务提供商控制面板中的防火墙。SSH 中的连接被拒绝与连接超时介绍了相同的区别,该区别适用于所有端口。
是否仍应运行 FTP?
对于新项目,不应运行。SFTP(SSH 文件传输协议)在 22 端口上的单个 SSH 连接内运行。它不需要第二个通道、被动端口范围、NAT 设置或额外的守护进程来加固,因为 OpenSSH 已经提供这些功能。sftp user@example.com 可在从未配置文件传输服务的系统上运行。如果只允许某人获取文件而不能执行其他操作,sshd_config 会将 ForceCommand internal-sftp 与 ChrootDirectory 一起使用。该目录必须由 root 所有,并且不能由用户写入,否则 sshd 会拒绝会话,并记录一条 bad ownership or modes for chroot directory 行。
当对端无法更换协议时,FTP 仍有使用价值。扫描仪和多功能打印机出厂固件通常只支持 FTP。实验室和工业设备通常运行固定镜像,没有人会重新进行认证。业务合作伙伴可能会发布 FTPS 投递点,并且不会为了一个供应商增加其他协议。在这些情况下,被动端口范围及匹配的防火墙规则就是全部配置工作,而且应运行 FTPS,而不是明文 FTP。双通道设计源自 1980 年的决策;它早在 RFC 959 延续该设计前五年就已写入 RFC 765。如今它运行在一个当初完全没有预料到的环境中,详见文件传输协议的历史。
FAQ
为什么 FTP 可以登录,但目录列表会卡住?
登录只使用 21 端口上的控制连接,而防火墙允许该连接。目录列表需要通过另一个端口建立第二个 TCP 连接,但该连接被阻断。请在 FTP 服务器上声明被动端口范围,并在防火墙中开放相同范围,目录列表即可正常完成。目录列表卡住是数据通道问题,不是密码问题。
FTP 被动模式需要开放哪些端口?
控制通道使用 21 端口,此外还需要开放为被动数据连接配置的端口范围。不存在标准范围,因为该范围由您自行选择。例如,30000 到 30099 可正常工作:应根据同时传输的峰值数量确定范围大小,并确保它不与内核的出站端口范围重叠;您可以使用 sysctl net.ipv4.ip_local_port_range 查看该范围。如果您的服务商在控制面板中提供网络防火墙,也应在那里开放相同范围。
还需要 nf_conntrack_ftp 吗?
不需要。在当前内核上也不能依赖它。自动分配辅助模块默认处于禁用状态;从 Linux 6.0 开始,net.netfilter.nf_conntrack_helper 开关已被移除,因此 sysctl 会报告文件不存在。ufw 手册将无条件加载这些模块标记为已弃用,firewalld 则完全忽略 AutomaticHelpers。辅助模块还必须以纯文本读取控制通道,因此启用 FTPS 后会立即停止工作。请改为声明被动端口范围。
为什么 FTP 客户端提示被动模式回复包含不可路由的地址?
服务器使用自身接口看到的地址回复了 PASV,但该地址是私有地址。当平台将公网地址映射到私有地址时,就会发生这种情况。请显式设置公网地址:在 vsftpd 中设置 pasv_address,在 ProFTPD 中设置 MasqueradeAddress,或在 Pure-FTPd 中设置 -P。FileZilla 会通过重新使用已连接的地址来规避该问题,并记录“使用服务器地址”,因此某些客户端可以应对这种配置错误,而其他客户端会卡住。
应使用 FTPS 还是 SFTP?
对于两端都由您控制的场景,应使用 SFTP:它通过 SSH 在 22 端口上使用单个连接,不需要开放数据通道,而且通常已经在运行。FTPS 是通过 TLS 运行的 FTP,因此保留了双通道设计及其带来的所有防火墙问题。当对端不支持其他协议时,才选择 FTPS。不要在互联网上使用纯 FTP,因为密码会以可读文本形式通过网络传输。