One particular cause of the following error is finally known:

sockRecv(47ms):
    WindowsError: An existing connection was forcibly closed by the remote host.
    WindowsErrorCode: 0x2746
    numBytesRequested: 5
    Failed to receive data on the TCP socket
--sockRecv
sockRecv failed.
sockRecvN_buf: Did not receive the exact number of bytes desired.
numBytesToReceive: 5

This error was caused by the Windows Firewall doing stateful FTP filtering. The solution is to disable stateful FTP filtering so that the firewall does not block any FTP traffic. (This is typically prescribed for the server-side of FTP, but it also applies to the client-side.)

Note: The Windows Firewall’s stateful FTP filtering only causes problems when the connection is SSL/TLS. Clear (non-secured) connections are not affected.

To disable stateful FTP filtering, open an administrative command prompt, and type the following:

netsh advfirewall set global StatefulFTP disable

Also Note: The above error could also be a result of the connection truly being closed by the remote host, or something on the server-side. This solution only applies to the case where Windows Firewall (on the client-side computer) is interfering with a SSL/TLS FTP connection.

How to Reproduce the Problem

To reproduce the problem, write a program that establishes a TLS connection with an FTP server.   After authenticating, write a simple loop that calls ftp.Noop 1500 times.   If Windows Firewall is interfering, it is likely to fail in the same place every time.  For example, in my case the error occurs on iteration 699 every time.

Also, the error may not be easily reproducible even if Windows Firewall is a problem.  For example, the problem seemed to be not reproducible if the FTP server was not a Windows-hosted FTP server.   One might suspect the server-side to be the problem, but the connection reset ([RST, ACK] in a WireShark trace) definitely originated from the client-side.

Disabling the firewall’s Stateful Packet Inspection (SPI) feature solved the problem every time. (It should be the case that stateful packet inspection should NEVER try to inspect the packets of a TLS encrypted channel for the simple fact that it’s impossible to inspect.  The firewall does not have the ability to decrypt the packets in the channel, and therefore it cannot inspect anything. The firewall’s only choices SHOULD be to simply allow or disallow the connection.  I don’t understand why stateful packet inspection should ever be “monkeying around” with encrypted channels..)