I am trying to download a file to a device over FTPS. I am using curl (7.88.1, OpenSSL/1.1.1u) as the client on the remote machine and pyftpdlib (1.5.7) to implement the FTP server on the local machine. While attempting to download my_file.txt, curl outputs: curl: (13) Bad PASV/EPSV response: 200
. I have run the same setup against OpenSSL 1.1.1q & 3.1.2, LibreSSL 3.3.6, where I did not encounter this issue.
In Wireshark I can see that when the client attempts to establish a TLS connection on the passive port, the pyftpdlib server responds to the "client hello" with a RST, ACK packet.
Here is the basic implementation of my FTP server:
import os
import threading
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import TLS_FTPHandler
from pyftpdlib.servers import ThreadedFTPServer
CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__),
"certificate.pem"))
KEYFILE = os.path.abspath(os.path.join(os.path.dirname(__file__),
"private_key.pem"))
class FtpSrv:
def __init__(self):
self.server = None
self.server_thread = None
def start_ftp_server(self, port=2121):
server_address = ('0.0.0.0', port)
authorizer = DummyAuthorizer()
authorizer.add_anonymous(os.getcwd())
handler = TLS_FTPHandler
handler.authorizer = authorizer
handler.banner = "pyftpdlib based ftpd ready."
handler.passive_ports = [1280]
handler.certfile = CERTFILE
handler.keyfile = KEYFILE
self.server = ThreadedFTPServer(server_address, handler)
self.server.max_cons = 256
self.server.max_cons_per_ip = 5
self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.server_thread.start()
server = FtpSrv()
server.start_ftp_server(port=2128)
On the remote Linux machine I then run this curl command:
curl --ssl-reqd ftp://xxx.xxx.4.233:2128/myfile.txt -o my_file.txt -k -v
... and see this output returned: curl: (13) Bad PASV/EPSV response: 200
The verbose curl logging on the remote machine logs the following:
curl --ssl-reqd ftp://xxx.xxx.4.233:2128/myfile.txt -o my_file.txt -k -v
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying xxx.xxx.4.233:2128...
* Connected to xxx.xxx.4.233 (xxx.xxx.4.233) port 2128 (#0)
< 220 pyftpdlib based ftpd ready.
> AUTH SSL
< 234 AUTH SSL successful.
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [6 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [930 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [264 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate:
* subject: C=XX; ST=Some-State; L=xxx; O=xxx; CN=xxx.xxx.4.233
* start date: Aug 1 23:59:00 2023 GMT
* expire date: Jul 31 23:59:00 2024 GMT
* issuer: C=xx; ST=Some-State; L=xxx; O=xxx; CN=xxx.xxx.4.233
* SSL certificate verify result: self-signed certificate (18), continuing anyway.
} [5 bytes data]
> USER anonymous
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [217 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [217 bytes data]
* old SSL session ID is stale, removing
{ [5 bytes data]
< 331 Username ok, send password.
} [5 bytes data]
> PASS ftp@example.com
{ [5 bytes data]
< 230 Login successful.
} [5 bytes data]
> PBSZ 0
{ [5 bytes data]
< 200 PBSZ=0 successful.
} [5 bytes data]
> PROT P
{ [5 bytes data]
< 200 Protection set to Private
} [5 bytes data]
> PWD
{ [5 bytes data]
< 257 "/" is the current directory.
* Entry path is '/'
* Request has same path as previous transfer
} [5 bytes data]
> EPSV
* Connect data stream passively
* ftp_perform ends with SECONDARY: 0
{ [5 bytes data]
< 229 Entering extended passive mode (|||1280|).
* Connecting to xxx.xxx.4.233 (xxx.xxx.4.233) port 1280
* Trying xxx.xxx.4.233:1280...
* Connected to xxx.xxx.4.233 (xxx.xxx.4.233) port 2128 (#0)
* SSL re-using session ID
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [673 bytes data]
> TYPE I
* Recv failure: Connection reset by peer
* OpenSSL SSL_connect: Connection reset by peer in connection to xxx.xxx.4.233:2128
* Failed EPSV attempt. Disabling EPSV
} [5 bytes data]
> PASV
{ [5 bytes data]
< 200 Type set to: Binary.
* Bad PASV/EPSV response: 200
* Remembering we are in dir ""
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
* Connection #0 to host xxx.xxx.4.233 left intact
curl: (13) Bad PASV/EPSV response: 200
And debug logging on the pyftpdlib server outputs the following:
concurrency model: multi-thread
masquerade (NAT) address: None
passive ports: 1280->1280
poller: 'pyftpdlib.ioloop.Select'
authorizer: 'pyftpdlib.authorizers.DummyAuthorizer'
handler: 'pyftpdlib.handlers.type'
max connections: 256
max connections per ip: 5
timeout: 300
banner: 'pyftpdlib based ftpd ready.'
max login attempts: 3
SSL certfile: '.\\certificate.pem'
SSL keyfile: '.\\private_key.pem'
xxx.xxx.4.233:10700-[] FTP session opened (connect)
xxx.xxx.4.233:10700-[] -> 220 pyftpdlib based ftpd ready.
xxx.xxx.4.233:10700-[] <- AUTH SSL
xxx.xxx.4.233:10700-[] -> 234 AUTH SSL successful.
[debug] securing SSL connection (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700')>)
[debug] call: _do_ssl_handshake, err: ssl-want-read (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True)>)
[debug] SSL connection established (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True)>)
xxx.xxx.4.233:10700-[] <- USER anonymous
xxx.xxx.4.233:10700-[] -> 331 Username ok, send password.
xxx.xxx.4.233:10700-[anonymous] <- PASS ******
xxx.xxx.4.233:10700-[anonymous] -> 230 Login successful.
xxx.xxx.4.233:10700-[anonymous] USER 'anonymous' logged in.
xxx.xxx.4.233:10700-[anonymous] <- PBSZ 0
xxx.xxx.4.233:10700-[anonymous] -> 200 PBSZ=0 successful.
xxx.xxx.4.233:10700-[anonymous] <- PROT P
xxx.xxx.4.233:10700-[anonymous] -> 200 Protection set to Private
xxx.xxx.4.233:10700-[anonymous] <- PWD
xxx.xxx.4.233:10700-[anonymous] -> 257 "/" is the current directory.
xxx.xxx.4.233:10700-[anonymous] <- EPSV
xxx.xxx.4.233:10700-[anonymous] -> 229 Entering extended passive mode (|||1280|).
[debug] call: close() (<pyftpdlib.handlers.PassiveDTP listening xxx.xxx.4.233:1280 at 0x1efe97835d0>)
[debug] securing SSL connection (<TLS_DTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous')>)
[debug] call: close() (<pyftpdlib.handlers.PassiveDTP xxx.xxx.4.233:1280 at 0x1efe97835d0>)
xxx.xxx.4.233:10700-[anonymous] <- TYPE I
xxx.xxx.4.233:10700-[anonymous] -> 200 Type set to: Binary.
[debug] call: close() (<TLS_DTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous', ssl-data=True)>)
xxx.xxx.4.233:10700-[anonymous] <- PASV
xxx.xxx.4.233:10700-[anonymous] -> 227 Entering passive mode (xxx,xxx,4,233,5,0).
xxx.xxx.4.233:10700-[anonymous] <- QUIT
xxx.xxx.4.233:10700-[anonymous] -> 221 Goodbye.
[debug] call: close() (<pyftpdlib.handlers.PassiveDTP listening xxx.xxx.4.233:1280 at 0x1efe9783710>)
[debug] call: _do_ssl_shutdown() -> shutdown(), err: zero-return (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous')>)
[debug] call: close() (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous')>)
xxx.xxx.4.233:10700-[anonymous] FTP session closed (disconnect).
[debug] closing IOLoop (<pyftpdlib.ioloop.Select (fds=0, tasks=0) at 0x1efe9783050>)
NOTE: Configuring a Filezilla FTP server to 'Require explicit FTP over TLS' using the same certificate and key as well as passive port (1280). The file transfer just works.