[
https://issues.apache.org/jira/browse/NET-739?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Reed Bertolotti updated NET-739:
--------------------------------
Description:
*Summary:*
{{FTPSClient.storeFile()}} intermittently fails to complete file uploads when
using *TLSv1.3* against a strict FTP server (Pure-FTPd). The file data is
successfully transmitted, but the transfer ends with a {{451 Transfer aborted}}
error from the server instead of the expected {{{}226 Transfer complete{}}}.
This appears to be a timing race condition in the data channel closure
sequence. The issue is highly sensitive to network latency: it is reproducible
in *low-latency environments* (e.g. FTP client and FTP server in same AWS Same
Region), but disappears if network latency is increased (e.g. different AWS
regions or adding latency with tc command)
----
*Environment:*
* *Library:* Apache Commons Net 3.9.0 (also reproduced on 3.10.0, 3.11.0,
3.12.0)
* *Java:* JDK 17
* *OS:* Amazon Linux 2023
* *Server:* Pure-FTPd 1.0.52 (TLS 1.3 enabled)
*
** Relevant Build Args:
*
**
*** {{--with-tls}} (Standard TLS support)
*
** Relevant Runtime Args:
*
**
*** {{-Y 3}} (Enforce TLS for Control & Data)
*
**
*** {{--tlsciphersuite=HIGH:MEDIUM:!TLSv1:!TLSv1.1:!aNULL}}
*Comparison:*
|*Scenario*|*Client/Server Location*|*Protocol*|*Latency*|*Result*|
|AWS Intra-Region|Same Region (e.g. us-west-2)|TLSv1.3|Low
|{color:#de350b}FAILS (451){color}|
|AWS Inter-Region|Different Regions|TLSv1.3|Not low|WORKS (226)|
|Artificial Delay|Same Region + {{tc}} delay|TLSv1.3|Not low (artificial)|WORKS
(226)|
|TLS 1.2 Downgrade|Same Region|TLSv1.2|Any (high or low)|WORKS (226)|
{_}Note: Attempts to reproduce this using {{localhost}} failed. Reproduction
seems to require a public IP / two servers{_}{{{{}}{}}}
*Reproduction Code (Simplified):*
{code:java}
FTPSClient ftpClient = new FTPSClient(false);
ftpClient.setEnabledProtocols(new String[]{"TLSv1.3"}); // Issue specific to
TLS 1.3
ftpClient.addProtocolCommandListener(new PrintCommandListener(new
PrintWriter(System.out), true));
// Setup
ftpClient.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX));
ftpClient.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
// IMPORTANT: Reproduction requires a real network path (cannot be localhost)
String serverPublicIp = "18.98.23.15";
ftpClient.connect(serverPublicIp, 21);
ftpClient.login(user, pass);
ftpClient.execPROT("P");
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// Upload
// Use a dummy 10KB payload in memory so this is runnable without external files
try (InputStream input = new ByteArrayInputStream(new byte[10240])) {
String remoteFilePath = "privateFileUpload_10KB.txt";
// Returns false. Server replies with 451.
boolean success = ftpClient.storeFile(remoteFilePath, input);
System.out.println("Success status: " + success);
}{code}
*Workaround / Additional Context:*
We found that we could avoid the error by avoiding {{storeFile()}} and manually
implementing the transfer using {{{}storeFileStream(){}}}. Specifically, using
{{input.transferTo(output)}} inside a try-with-resources block (to ensure
{{{}close(){}}}), followed by {{{}completePendingCommand(){}}}, results in a
successful transfer (226) in the same TLSv1.3/low latency environment where
{{storeFile()}} fails.
----
*Logs:*
*1. Client Protocol Log (Commons Net):* The client sends the data, but receives
a 451 failure immediately after the transfer.
{code:java}
...
PASV
227 Entering Passive Mode (18,98,23,15,218,245)
STOR //privateFileUpload_10KB.txt
150 Accepted data connection
451-Transfer aborted
451 0.084 seconds (measured here), 119.71 Kbytes per second
...{code}
*2. Server Log (Pure-FTPd):* The server acknowledges the upload size (10240
bytes) but flags the transfer as aborted, likely due to a socket closure race
condition.
{code:java}
[INFO] TLS: Enabled TLSv1.3 with TLS_AES_256_GCM_SHA384, 256 secret bits cipher
[DEBUG] 150 Accepted data connection
[NOTICE] privateFileUpload_10KB.txt uploaded (10240 bytes, 119.71KB/sec)
[DEBUG] 451-Transfer aborted {code}
----
It appears {{commons-net}} is closing the underlying socket or data stream too
aggressively for TLS 1.3 strictness. If {{commons-net}} closes the TCP socket
before the server has processed the TLS closure, strict servers like
{{pure-ftpd}} interpret this as a premature disconnection ("Transfer aborted").
Since this works on TLS 1.2 and high-latency connections, it suggests a timing
race between the Java client's TCP FIN and the TLS protocol shutdown.
was:
*Summary:*
{{FTPSClient.storeFile()}} intermittently fails to complete file uploads when
using *TLSv1.3* against a strict FTP server (Pure-FTPd). The file data is
successfully transmitted, but the transfer ends with a {{451 Transfer aborted}}
error from the server instead of the expected {{{}226 Transfer complete{}}}.
This appears to be a timing race condition in the data channel closure
sequence. The issue is highly sensitive to network latency: it is reproducible
in *low-latency environments* (e.g. FTP client and FTP server in same AWS Same
Region), but disappears if network latency is increased (e.g. different AWS
regions or adding latency with tc command)
----
*Environment:*
* *Library:* Apache Commons Net 3.9.0 (also reproduced on 3.10.0, 3.11.0,
3.12.0)
* *Java:* JDK 17
* *OS:* Amazon Linux 2023
* *Server:* Pure-FTPd 1.0.52 (TLS 1.3 enabled)
*
** Relevant Build Args:
*
**
*** {{--with-tls}} (Standard TLS support)
*
** Relevant Runtime Args:
*
**
*** {{-Y 3}} (Enforce TLS for Control & Data)
*
**
*** {{--tlsciphersuite=HIGH:MEDIUM:!TLSv1:!TLSv1.1:!aNULL}}
*Comparison:*
|*Scenario*|*Client/Server Location*|*Protocol*|*Latency*|*Result*|
|*AWS Intra-Region*|*Same Region (e.g. us-west-2)*|*TLSv1.3*|*Low* |*FAILS
(451)*|
|AWS Inter-Region|Different Regions|TLSv1.3|Not low|WORKS (226)|
|Artificial Delay|Same Region + {{tc}} delay|TLSv1.3|Not low (artificial)|WORKS
(226)|
|TLS 1.2 Downgrade|Same Region|TLSv1.2|Any (high or low)|WORKS (226)|
{_}Note: Attempts to reproduce this using {{localhost}} failed. Reproduction
seems to require a public IP / two servers{_}{{{{}}{}}}
*Reproduction Code (Simplified):*
{code:java}
FTPSClient ftpClient = new FTPSClient(false);
ftpClient.setEnabledProtocols(new String[]{"TLSv1.3"}); // Issue specific to
TLS 1.3
ftpClient.addProtocolCommandListener(new PrintCommandListener(new
PrintWriter(System.out), true));
// Setup
ftpClient.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX));
ftpClient.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
// IMPORTANT: Reproduction requires a real network path (cannot be localhost)
String serverPublicIp = "18.98.23.15";
ftpClient.connect(serverPublicIp, 21);
ftpClient.login(user, pass);
ftpClient.execPROT("P");
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// Upload
// Use a dummy 10KB payload in memory so this is runnable without external files
try (InputStream input = new ByteArrayInputStream(new byte[10240])) {
String remoteFilePath = "privateFileUpload_10KB.txt";
// Returns false. Server replies with 451.
boolean success = ftpClient.storeFile(remoteFilePath, input);
System.out.println("Success status: " + success);
}{code}
*Workaround / Additional Context:*
We found that we could avoid the error by avoiding {{storeFile()}} and manually
implementing the transfer using {{{}storeFileStream(){}}}. Specifically, using
{{input.transferTo(output)}} inside a try-with-resources block (to ensure
{{{}close(){}}}), followed by {{{}completePendingCommand(){}}}, results in a
successful transfer (226) in the same TLSv1.3/low latency environment where
{{storeFile()}} fails.
----
*Logs:*
*1. Client Protocol Log (Commons Net):* The client sends the data, but receives
a 451 failure immediately after the transfer.
{code:java}
...
PASV
227 Entering Passive Mode (18,98,23,15,218,245)
STOR //privateFileUpload_10KB.txt
150 Accepted data connection
451-Transfer aborted
451 0.084 seconds (measured here), 119.71 Kbytes per second
...{code}
*2. Server Log (Pure-FTPd):* The server acknowledges the upload size (10240
bytes) but flags the transfer as aborted, likely due to a socket closure race
condition.
{code:java}
[INFO] TLS: Enabled TLSv1.3 with TLS_AES_256_GCM_SHA384, 256 secret bits cipher
[DEBUG] 150 Accepted data connection
[NOTICE] privateFileUpload_10KB.txt uploaded (10240 bytes, 119.71KB/sec)
[DEBUG] 451-Transfer aborted {code}
----
It appears {{commons-net}} is closing the underlying socket or data stream too
aggressively for TLS 1.3 strictness. If {{commons-net}} closes the TCP socket
before the server has processed the TLS closure, strict servers like
{{pure-ftpd}} interpret this as a premature disconnection ("Transfer aborted").
Since this works on TLS 1.2 and high-latency connections, it suggests a timing
race between the Java client's TCP FIN and the TLS protocol shutdown.
> FTPSClient storeFile() fails with "451 Transfer aborted" on TLS 1.3 (with low
> latency and pure-ftpd)
> ----------------------------------------------------------------------------------------------------
>
> Key: NET-739
> URL: https://issues.apache.org/jira/browse/NET-739
> Project: Commons Net
> Issue Type: Bug
> Reporter: Reed Bertolotti
> Priority: Major
>
> *Summary:*
> {{FTPSClient.storeFile()}} intermittently fails to complete file uploads when
> using *TLSv1.3* against a strict FTP server (Pure-FTPd). The file data is
> successfully transmitted, but the transfer ends with a {{451 Transfer
> aborted}} error from the server instead of the expected {{{}226 Transfer
> complete{}}}.
> This appears to be a timing race condition in the data channel closure
> sequence. The issue is highly sensitive to network latency: it is
> reproducible in *low-latency environments* (e.g. FTP client and FTP server in
> same AWS Same Region), but disappears if network latency is increased (e.g.
> different AWS regions or adding latency with tc command)
> ----
>
> *Environment:*
> * *Library:* Apache Commons Net 3.9.0 (also reproduced on 3.10.0, 3.11.0,
> 3.12.0)
> * *Java:* JDK 17
> * *OS:* Amazon Linux 2023
> * *Server:* Pure-FTPd 1.0.52 (TLS 1.3 enabled)
> *
> ** Relevant Build Args:
> *
> **
> *** {{--with-tls}} (Standard TLS support)
> *
> ** Relevant Runtime Args:
> *
> **
> *** {{-Y 3}} (Enforce TLS for Control & Data)
> *
> **
> *** {{--tlsciphersuite=HIGH:MEDIUM:!TLSv1:!TLSv1.1:!aNULL}}
> *Comparison:*
> |*Scenario*|*Client/Server Location*|*Protocol*|*Latency*|*Result*|
> |AWS Intra-Region|Same Region (e.g. us-west-2)|TLSv1.3|Low
> |{color:#de350b}FAILS (451){color}|
> |AWS Inter-Region|Different Regions|TLSv1.3|Not low|WORKS (226)|
> |Artificial Delay|Same Region + {{tc}} delay|TLSv1.3|Not low
> (artificial)|WORKS (226)|
> |TLS 1.2 Downgrade|Same Region|TLSv1.2|Any (high or low)|WORKS (226)|
> {_}Note: Attempts to reproduce this using {{localhost}} failed. Reproduction
> seems to require a public IP / two servers{_}{{{{}}{}}}
> *Reproduction Code (Simplified):*
> {code:java}
> FTPSClient ftpClient = new FTPSClient(false);
> ftpClient.setEnabledProtocols(new String[]{"TLSv1.3"}); // Issue specific to
> TLS 1.3
> ftpClient.addProtocolCommandListener(new PrintCommandListener(new
> PrintWriter(System.out), true));
> // Setup
> ftpClient.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX));
> ftpClient.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
> // IMPORTANT: Reproduction requires a real network path (cannot be localhost)
> String serverPublicIp = "18.98.23.15";
> ftpClient.connect(serverPublicIp, 21);
> ftpClient.login(user, pass);
> ftpClient.execPROT("P");
> ftpClient.enterLocalPassiveMode();
> ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
> // Upload
> // Use a dummy 10KB payload in memory so this is runnable without external
> files
> try (InputStream input = new ByteArrayInputStream(new byte[10240])) {
> String remoteFilePath = "privateFileUpload_10KB.txt";
>
> // Returns false. Server replies with 451.
> boolean success = ftpClient.storeFile(remoteFilePath, input);
> System.out.println("Success status: " + success);
> }{code}
> *Workaround / Additional Context:*
> We found that we could avoid the error by avoiding {{storeFile()}} and
> manually implementing the transfer using {{{}storeFileStream(){}}}.
> Specifically, using {{input.transferTo(output)}} inside a try-with-resources
> block (to ensure {{{}close(){}}}), followed by
> {{{}completePendingCommand(){}}}, results in a successful transfer (226) in
> the same TLSv1.3/low latency environment where {{storeFile()}} fails.
> ----
>
> *Logs:*
> *1. Client Protocol Log (Commons Net):* The client sends the data, but
> receives a 451 failure immediately after the transfer.
> {code:java}
> ...
> PASV
> 227 Entering Passive Mode (18,98,23,15,218,245)
> STOR //privateFileUpload_10KB.txt
> 150 Accepted data connection
> 451-Transfer aborted
> 451 0.084 seconds (measured here), 119.71 Kbytes per second
> ...{code}
> *2. Server Log (Pure-FTPd):* The server acknowledges the upload size (10240
> bytes) but flags the transfer as aborted, likely due to a socket closure race
> condition.
> {code:java}
> [INFO] TLS: Enabled TLSv1.3 with TLS_AES_256_GCM_SHA384, 256 secret bits
> cipher
> [DEBUG] 150 Accepted data connection
> [NOTICE] privateFileUpload_10KB.txt uploaded (10240 bytes, 119.71KB/sec)
> [DEBUG] 451-Transfer aborted {code}
> ----
>
> It appears {{commons-net}} is closing the underlying socket or data stream
> too aggressively for TLS 1.3 strictness. If {{commons-net}} closes the TCP
> socket before the server has processed the TLS closure, strict servers like
> {{pure-ftpd}} interpret this as a premature disconnection ("Transfer
> aborted").
> Since this works on TLS 1.2 and high-latency connections, it suggests a
> timing race between the Java client's TCP FIN and the TLS protocol shutdown.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)