某天中午,我打算把一些文件从电脑通过 FTP 上传到 NAS 上。上传时一切顺利,然而之后我发现文件的时间戳出现异常,比原本的时间早了8小时。这个偏差往往意味着时区配置出了问题,然而该过程涉及到的所有系统的时区均设置为 CST(中国标准时间,下文中提到的所有设备的时区均设置为 CST)。尝试找了一下,WinSCP 并没有任何关于时区有关的配置。并且时间在 WinSCP 客户端上是能正确显示的,但在 NAS 上存储的却是偏移了8小时的时间戳,因此这一定是 FTP 传输的某个环节出了问题。
如果你嫌太长不想看,可以直接跳转到结论部分,解决方法非常简单。
过程再现
接下来我们重现一下这个问题。测试的客户端环境是 Windows 10,安装了 WinSCP 作为 FTP 客户端。服务端环境为 Debian GNU/Linux 12,安装了 vsftpd 作为服务端。下文中所有操作系统都已经将时区设置为中国标准时间(CST)。
首先我们在本地创建一个测试文件,其最后修改时间为 21:31。

打开 WinSCP,连接到服务器并上传文件,文件的修改时间正确显示。

然而我们远程登录到服务器查看文件属性时,会发现所谓“正确”只是表象,实际存储的时间有八小时的偏差。
user@****:/path/to/ftp/folder$ stat ftp-upload-test.txt
File: ftp-upload-test.txt
Size: 20 Blocks: 8 IO Block: 4096 regular file
Device: **** Inode: **** Links: 1
Access: (0660/-rw-rw----) Uid: ( 1005/ ****) Gid: ( 1003/ ****)
Access: 2024-11-05 13:31:23.000000000 +0800
Modify: 2024-11-05 13:31:23.000000000 +0800
Change: 2024-11-05 21:38:26.287957168 +0800
Birth: 2024-11-05 21:38:26.287957168 +0800
因此,除了 FTP 客户端之外,从其他地方查看文件属性都会得到这个错误的时间,例如与 FTP 服务共享相同分区的 Nextcloud:

调试与分析
FTP 默认是不加密的明文传输,且报文对人类可读,甚至自带很友好的交互式提示,因此抓包分析显得异常方便。我们通过 Wireshark 抓取关键过程报文,首先是文件上传:

然后是查看文件属性:

我们可以看由此看出,MDTM 命令便是涉及到文件修改时间的关键。
MDTM 命令是 RFC 3659 中规定的 FTP 协议扩展之一,它用于查询文件的最后更改时间(MoDification TiMe)。它的用法大概是这样的:
客户端命令:MDTM <file-name>
服务端响应:213 <time-value>
其中时间戳的表示格式是 YYYYMMDDHHMMSS.sss,毫秒位可以省略。例如图中的 20241105133123 便是 2024年11月5日13时31分23秒。注意到了吗?这个时间戳偏移了8小时,但这不是问题,因为协议中明确规定了,这个时间戳的时区是世界协调时(UTC),也就是中国标准时间减去八小时。原文如下:
Time values are always represented in UTC (GMT), and in the Gregorian calendar regardless of what calendar may have been in use at the date and time indicated at the location of the server-PI.
此外顺便一提,从报文中可以看到,WinSCP 不仅使用该命令查询了文件的最后编辑时间,还在上传完后修改了这个时间。协议中只规定了查询用途,而没有定义如何修改。从文档中的举例来看,设计者甚至明确指出不能将其用于修改。不过 vsftpd 倒也支持这个协议之外的功能,我们也暂且睁一只眼闭一只眼。
我也测试了 FileZilla 客户端,与 WinSCP 反应出的情况类似,不过前者仅支持通过 MFMT 命令修改时间,该命令并未受到广泛支持,FTP 协议及其扩展事实上也没有正式对编辑文件修改时间的方式进行规定。
总之,WinSCP 在传输时将时间戳看作是 UTC 时间,并且能正确地与本地时间转换。那么问题就在服务端 vsftpd 上了。经过一番查阅资料,我发现 vsftpd 有一个配置项 use_localtime。顾名思义,如果启用此配置项,服务端会将时间戳当成是服务器的本地时间,而不按照协议转换至 UTC 时间。我猜测,该配置也许是因为为了兼容某些软件的历史遗留特性而保留的。但最大的问题是,在默认的配置文件中,该配置项处于启用状态,因此才导致了时间戳偏移的问题。
解决方法
那么解决方法也很简单了,编辑 vsftpd 的配置文件(默认是 /etc/vsftpd.conf),找到其中 use_localtime=YES 的行,改为 use_localtime=NO,或者删除该行(若未指定则不启用该特性),重新启动服务,问题迎刃而解。
后记
Q: 为什么还使用 FTP?SFTP 等其他协议不好吗?
FTP 是一个略显古老的协议。除非使用了 FTPS(带有 SSL 的 FTP),否则它的传输默认不加密,抓包的过程便会看见用户名和密码都是完全以明文形式传输的,这一点不像 HTTP Basic Auth 和 SMTP,都有“自欺欺人”的“加密”——把密码编码成 Base64。另外,FTP 的控制和传输工作在不同的端口,还有主动被动等模式的区别,如果想把 FTP 服务部署在公网上,相比于其他单端口的协议会稍微麻烦一些。SFTP 没有上述的两个问题,拥有和 SSH 同样的安全性,并且只用一个端口,非常适合部署在公网上。
不过这并不影响我使用 FTP,因为我把它的用途限制于在局域网内上传与访问 NAS 里的文件。如此便无需考虑加密的问题,因为该服务也不会暴露在公网上,很安全;此时明文传输成了一个优点,因为我的 NAS 处理器性能很一般,这样可以顺便省去加解密的开销,尽量跑满局域网的峰值传输速度。