Reed Bertolotti created NET-739:
-----------------------------------

             Summary: 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


*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:*
 * *Fails:* Commons Net {{storeFile()}} + TLS 1.3 + low latency -> Returns 
{{false}} (451).

 * *Works:* Commons Net {{storeFile()}} + TLS 1.2 -> Returns {{true}} (226).

 * *Works:* Commons Net {{storeFile()}} + TLS 1.3 + high latency -> Returns 
{{true}} (226).

 * *Works:* FileZilla FTP Client + TLS 1.3 
 * *Works:* Manual implementation using {{storeFileStream()}}

 * {{}}

|*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.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to