WinError 10060 ftplib.FTP_TLS

Any help would be appreciated. I have googled for two days now and tried everything I could find. Thinking it might be PASV issue with internal IP, Reuse session ID and things I dont even know what they did but I tried.

Ultimately want to upload a file to an external site I do not control. Was trying to use storbinary and got the same error, so I simplified to a dir listing and still get the error. Not sure what else to try. Running Python 3.10.7

Exception has occurred: TimeoutError

[WinError 10060] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond
  File "", line 96, in <module>

Very Basic Code:

with ftplib.FTP_TLS('XXXXX') as ftp:
    ftp.login('XXXXXX', 'XXXXXXX')

Debug shows:

*cmd* 'AUTH TLS'
*resp* '234 AUTH TLS successful'
*cmd* 'PBSZ 0'
*resp* '200 PBSZ 0 successful'
*cmd* 'PROT P'
*resp* '200 Protection set to Private'
*cmd* 'TYPE A'
*resp* '200 Type set to A'
*cmd* 'PASV'
*resp* '227 Entering Passive Mode (3,87,37,152,196,95).'
*cmd* 'QUIT'
*resp* '221 Goodbye.'

If I conenct via WinSCP it works fine:

2022-09-07 09:33:36.427 OPTS UTF8 ON
< 2022-09-07 09:33:36.461 200 UTF8 set to on
> 2022-09-07 09:33:36.461 PBSZ 0
< 2022-09-07 09:33:36.495 200 PBSZ 0 successful
> 2022-09-07 09:33:36.495 PROT P
< 2022-09-07 09:33:36.530 200 Protection set to Private
. 2022-09-07 09:33:36.530 Session upkeep
. 2022-09-07 09:33:36.563 Connected
. 2022-09-07 09:33:36.563 Got reply 1 to the command 1
. 2022-09-07 09:33:36.563 --------------------------------------------------------------------------
. 2022-09-07 09:33:36.563 Using FTP protocol.
. 2022-09-07 09:33:36.563 Doing startup conversation with host.
> 2022-09-07 09:33:36.579 PWD
< 2022-09-07 09:33:36.613 257 "/" is the current directory
. 2022-09-07 09:33:36.613 Got reply 1 to the command 16
. 2022-09-07 09:33:36.613 Changing directory to "/".
> 2022-09-07 09:33:36.613 CWD /
< 2022-09-07 09:33:36.648 250 CWD command successful
. 2022-09-07 09:33:36.648 Got reply 1 to the command 16
. 2022-09-07 09:33:36.648 Getting current directory name.
> 2022-09-07 09:33:36.648 PWD
< 2022-09-07 09:33:36.682 257 "/" is the current directory
. 2022-09-07 09:33:36.682 Got reply 1 to the command 16
. 2022-09-07 09:33:36.734 Retrieving directory listing...
> 2022-09-07 09:33:36.734 TYPE A
< 2022-09-07 09:33:36.769 200 Type set to A
> 2022-09-07 09:33:36.770 PASV
< 2022-09-07 09:33:36.804 227 Entering Passive Mode (52,91,110,136,197,134).
> 2022-09-07 09:33:36.804 MLSD
. 2022-09-07 09:33:36.804 Connecting to ...
< 2022-09-07 09:33:36.876 150 Opening ASCII mode data connection for MLSD
. 2022-09-07 09:33:36.877 Session ID reused
. 2022-09-07 09:33:36.877 Using TLSv1.2, cipher TLSv1.2: ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA, ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(256) Mac=AEAD
. 2022-09-07 09:33:36.877 TLS connection established
. 2022-09-07 09:33:36.935 modify=20220906233223;perm=flcdmpe;type=cdir;unique=27UE026C66DFCF056E6;;UNIX.mode=0777;UNIX.owner=99; .
. 2022-09-07 09:33:36.935 modify=20220906233223;perm=flcdmpe;type=pdir;unique=27UE026C66DFCF056E6;;UNIX.mode=0777;UNIX.owner=99; ..
. 2022-09-07 09:33:36.935 modify=20220905013650;perm=flcdmpe;type=dir;unique=27UCC6B22EAEE6D4E2E;;UNIX.mode=0777;UNIX.owner=99; release
. 2022-09-07 09:33:36.935 Session upkeep
. 2022-09-07 09:33:36.986 Data connection closed
< 2022-09-07 09:33:36.986 226 Transfer complete
. 2022-09-07 09:33:36.986 Directory listing successful
  1. The passive mode IP address differs in the two cases. Does it correspond to the FTP server IP address in both the cases?
  2. You are using only ftp.set_debuglevel(1) maybe higher levels would show more information about the failure (or about the moment when it happens).
  3. Between the lines *resp* '227 Entering Passive Mode… and *cmd* 'QUIT' is there any noticeable delay (like waiting for a response which does not come)? I.e. is it a real timeout as the error message suggests?
  4. If not I would guess that the failure could be in the TLS setup - unmatched ciphersuites, failing certificate check etc.
  5. If the increased debug level does not provide enough information then you can capture the communication (Wireshark) in both cases and compare them.

Other notes:

  • ftp.set_pasv(True) should be on by default.
  • You are using ftp.dir(). Although it should not matter to be closer to WinSCP you could use ftp.mlsd().

See response below, but one thing to note, I can run makepasv and that causes no error when it run PASV. The only differance I see between that and DIR is DIR send a Type A ahead of it??

  1. My guess is its behind a load balencer or something. Everytime I run it I get a differnt PASV IP address, regardless of WinSCP or Python Code
  2. Bumpped it up below (2)
  3. Yes, it sits there for a bit, gets the WinError, then submits the QUIT once the error occurs.
  4. See #3
  5. See below

Other Notes:
Bullet 1 - Yea, I know I put it in there just to make sure as part of testing
Bullet 2 - Oddly enough if I send mlsd, nothing returns even though there is a file in the target location, but at the same time it doesn’t send the PASV command either, so no error, but nothing done either.

    print('MLSD About to Run')
    print('MLSD Done')
    print('makepasv Starting')
    print('makepasv Done')
    print('DIR About to Run')
    print('DIR Done')
*cmd* 'PROT P'*put* 'PROT P\r\n'*get* '200 Protection set to Private\n'
*resp* '200 Protection set to Private'
MLSD About to Run
makepasv Starting
*cmd* 'PASV'
*put* 'PASV\r\n'
*get* '227 Entering Passive Mode (54,210,177,3,199,1).\n'
*resp* '227 Entering Passive Mode (54,210,177,3,199,1).'
makepasv Done
DIR About to Run
*cmd* 'TYPE A'
*put* 'TYPE A\r\n'
*get* '200 Type set to A\n'
*resp* '200 Type set to A'
*cmd* 'PASV'
*put* 'PASV\r\n'
*get* '227 Entering Passive Mode (54,210,177,3,195,151).\n'
*resp* '227 Entering Passive Mode (54,210,177,3,195,151).'
*cmd* 'QUIT'
*put* 'QUIT\r\n'
*get* '221 Goodbye.\n'
*resp* '221 Goodbye.'

And if I try storbinary, same error, but Type I as you would expect. Hangs there, before error and sending QUIT command.

*cmd* 'TYPE I'
*put* 'TYPE I\r\n'
*get* '200 Type set to I\n'
*resp* '200 Type set to I'
*cmd* 'PASV'
*put* 'PASV\r\n'
*get* '227 Entering Passive Mode (3,87,37,152,197,102).\n'
*resp* '227 Entering Passive Mode (3,87,37,152,197,102).'

Ok, it looks like on the data connection no response from the server comes back to the client. I would capture the communication using Wireshark or tcpdump to see what is really going on.

Alternatively you can try to run the program in a debugger to see in which phase of the connection the lack of response comes but I think it is easier to capture the packets first.

So I ran wireshrark. Admittedly not that familier with it, but this is what I got. I put a breakpoint on the Dir command, ran the code to that point, then turned it on and had it run dri() until the Win 10060 error came up. Odd there was no traffic from based on the Python debug, otherwise not sure what some of this other stuff means.

In terms of the output from python:

*cmd* 'TYPE A'
*put* 'TYPE A\r\n'
*get* '200 Type set to A\n'
*resp* '200 Type set to A'
*cmd* 'PASV'
*put* 'PASV\r\n'
*get* '227 Entering Passive Mode (35,171,182,226,195,198).\n'
*resp* '227 Entering Passive Mode (35,171,182,226,195,198).'
*cmd* 'QUIT'
*put* 'QUIT\r\n'
*get* '221 Goodbye.\n'
*resp* '221 Goodbye.'

I think this last one is where is failes and sents the QUIT

So is the IP address of the SFTP server, right?

Frames 366 - 380 look like the control connection with encrypted communication. Is it right? Unfortunately we do not see the connection from the beginning. Was it cleartext at the start? Unfortunately I do not know anything about how this encryption should work.

381, 720, 1228 look like SYN packet for the data connection (with retransmission) for which an answer never comes. So the problem does not seem to be in the TLS handshake because no TLS handshake could even start…

Yes, I am almost sure that in the frame 1969 is the QUIT command.

Are you sure the packet capture is of the same connection as the debug log? How is it possible that the IP addresses (packet capture) vs (debug log) differ?

Are you sure you connected using WinSCP from the same machine (including virtual), using the same internet connection, without using any proxy?

Additional info (I looked into the control connection encryption):

The TLS handshake for the control connection should be initiated from the client using AUTH TLS. Did you see cleartext communication at the beginning and this command initiating the TLS?

Does WinSCP initiate TLS using AUTH TLS too? …and in the same place of the communication?

Yup, same computer and everything and works on WinSCP.

Ran again with Waveshark starting at the begining.
get ‘227 Entering Passive Mode (18,205,244,215,197,112).\n’
resp ‘227 Entering Passive Mode (18,205,244,215,197,112).’

The only one I am sure of is that I am hitting, I’ve included other external IPs but those could just be other traffic on the network

My filter is:
ip.addr == or ip.addr == or ip.addr == or ip.addr == or ip.addr == or ip.addr ==

All other traffic is internal to internal. Nothing hits the 18.205 address that comes back in Python, not sure about the others, but 107.21 address is defintaley part of the endpoint I am hitting and you can see the AUTH TLS part.

And the latest WinSCP log related to the dir

It just feels like Python is not using the IP that comes back from teh PASV command or something, or the PASV never completes, but point being it never seems to use the address it lists, Wheeras WinSCP clearly shows

. 2022-09-08 10:54:39.693 Connecting to …
. 2022-09-08 10:54:39.756 Session ID reused

. 2022-09-08 10:54:39.626 Retrieving directory listing...
> 2022-09-08 10:54:39.626 TYPE A
< 2022-09-08 10:54:39.659 200 Type set to A
> 2022-09-08 10:54:39.660 PASV
< 2022-09-08 10:54:39.693 227 Entering Passive Mode (54,210,177,3,197,252).
> 2022-09-08 10:54:39.693 MLSD
. 2022-09-08 10:54:39.693 Connecting to ...
. 2022-09-08 10:54:39.756 Session ID reused
. 2022-09-08 10:54:39.756 Using TLSv1.2, cipher TLSv1.2: ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA, ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(256) Mac=AEAD
. 2022-09-08 10:54:39.756 TLS connection established
< 2022-09-08 10:54:39.757 150 Opening ASCII mode data connection for MLSD
. 2022-09-08 10:54:39.805 modify=20220908141616;perm=flcdmpe;type=cdir;unique=27UE026C66DFCF056E6;;UNIX.mode=0777;UNIX.owner=99; .
. 2022-09-08 10:54:39.805 modify=20220908141616;perm=flcdmpe;type=pdir;unique=27UE026C66DFCF056E6;;UNIX.mode=0777;UNIX.owner=99; ..
. 2022-09-08 10:54:39.805 modify=20220908141616;perm=adfrw;size=0;type=file;unique=27U789898EF5F76DA16;;UNIX.mode=0666;UNIX.owner=99; 20220613_085733.jpg
. 2022-09-08 10:54:39.805 modify=20220905013650;perm=flcdmpe;type=dir;unique=27UCC6B22EAEE6D4E2E;;UNIX.mode=0777;UNIX.owner=99; release
. 2022-09-08 10:54:39.805 Session upkeep
. 2022-09-08 10:54:39.815 Data connection closed
< 2022-09-08 10:54:39.837 226 Transfer complete
. 2022-09-08 10:54:39.837 Directory listing successful

OK, it looks like a bug in the ftplib module. In passive mode it seems to ignore the IP address sent by the FTP server and it tries to connect to the FTP server’s IP address.

It seems to obey the port number though. In your previous attempt it was:
50118 == (195 << 8) + 198

If you do not have the latest version of Python installed, install it and test the problem there.

If the problem persists it is time to create an issue:

I have tested this code to make sure you are using the library correctly:

import ftplib

with ftplib.FTP_TLS('') as ftp:

You can test it too. Unfortunately the FTP server returns the same IP address as the one used for the control connection so the problem cannot be tested with this FTP server.

I am using 3.10.7 which I belive is the latest version for windows.

I did run your test code just fine.

Which model does it use? Maybe I can edit it for now to to get it to work until its fixed in another release.

Well this made Dir work, now to see if I can get the transfer to work

In ftplib I changed

def makepasv(self):
        """Internal: Does the PASV or EPSV handshake -> (address, port)"""
        # if == socket.AF_INET:
        #     untrusted_host, port = parse227(self.sendcmd('PASV'))
        #     if self.trust_server_pasv_ipv4_address:
        #         host = untrusted_host
        #     else:
        #         host = self.sock.getpeername()[0]
        # else:
        #     host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername())

        host, port = parse227(self.sendcmd('PASV'))

        return host, port
1 Like

Wahoo… Uploading files now. Thanks for the help…

I think that orginal code was suposed to check for a non-routable (private) IP and use the public one if so, but seems to be break when a different public IP is sent back it doesn; use it.

Great discovery! I did not expect that the bug is missing documentation! The behaviour was changed intentionally to not obey the RFC (for perfectly valid security reasons) but it did not make it into the ftplib documentation.

The explanation is in the commit:

@nad is it intentional that the nonstandard behaviour is not described in the ftplib documentation? I think it is really wrong as you can see in this topic.

@BillyBob please test this code with your FTP server (and revert the ftplib modification):

import ftplib

with ftplib.FTP_TLS('') as ftp:
    ftp.trust_server_pasv_ipv4_address = True

It appears that the original issue and discussion is here (migrated from the old bug tracker). The changes were applied to all the then-active branches not just the 3.7 link you show and the discussion here is about 3.10.x. If you think a change in documentation is warranted, you should open a new issue to discuss it.

Spot on! Adding the new line

ftp.trust_server_pasv_ipv4_address = True

Also fixes it. Can’t say I found anything online when I was troublshooting this that referance this attribute.

2 Days - one line of code…

Thanks again, I have reverted the change in the module and just added this line and still works.