From 8c4e931300c3e60ee5ecfbaef4227abfcb18a616 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 5 Jul 2026 18:59:09 +0000 Subject: [PATCH] sftp: filter spam from portscanners (closes #1519); also use a threadpool for accepting clients, to avoid spinning up a new thread for each hit from a portscanner --- copyparty/__main__.py | 2 ++ copyparty/sftpd.py | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 97a2b178..baf49e21 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1507,6 +1507,8 @@ def add_sftp(ap): ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated") ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath") ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") + ap2.add_argument("--sftp-hs-t", metavar="SEC", type=int, default=15, help="connection handshake timeout in seconds") + ap2.add_argument("--sftp-hs-n", metavar="NUM", type=int, default=4, help="max num ongoing/incomplete handshakes") def add_ftp(ap): diff --git a/copyparty/sftpd.py b/copyparty/sftpd.py index aff5c358..d4138fae 100644 --- a/copyparty/sftpd.py +++ b/copyparty/sftpd.py @@ -21,6 +21,7 @@ from paramiko.sftp import ( SFTP_OP_UNSUPPORTED, SFTP_PERMISSION_DENIED, ) +from queue import Queue from .__init__ import ANYWIN, TYPE_CHECKING from .authsrv import LEELOO_DALLAS, VFS, AuthSrv @@ -736,6 +737,22 @@ class SFTP_Srv(paramiko.SFTPServerInterface): return "/%s" % (undot(path),) +class Transport2(paramiko.Transport): + def __init__(self, cli, addr): + self.addr = addr + super(Transport2, self).__init__(cli) + + def _check_banner(self): + try: + return super()._check_banner() + except Exception as ex: + if "Error reading SSH protocol bann" in str(ex): + self.logger.warning("%s rejected: %s" % (self.addr, ex)) + self.addr = None + else: + raise + + class Sftpd(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub @@ -743,6 +760,7 @@ class Sftpd(object): self.log_func = hub.log self.srv: list[socket.socket] = [] self.bound: list[str] = [] + self.hs_q = Queue() self.sessions = {} ips = args.sftp_i @@ -793,6 +811,9 @@ class Sftpd(object): self.log("listening @ %s port %s" % (self.bound, args.sftp)) + for n in range(self.args.sftp_hs_n): + Daemon(self.acceptor, "sftp-hs-%d" % (n,)) + def log(self, msg: str, c: Union[int, str] = 0) -> None: self.hub.log("sftp", msg, c) @@ -828,12 +849,18 @@ class Sftpd(object): cli, addr = srv.accept() # cli.settimeout(0) # == srv.setblocking(False) self.log("%r is connecting" % (addr,)) - zs = "sftp-%s" % (addr[0],) - # Daemon(self._accept2, zs, (cli, addr)) - self._accept2(cli, addr) + self.hs_q.put((cli, addr)) + + def acceptor(self) -> None: + while True: + try: + cli, addr = self.hs_q.get() + self._accept2(cli, addr) + except: + time.sleep(1) def _accept2(self, cli, addr) -> None: - tra = paramiko.Transport(cli) + tra = Transport2(cli, addr) for hkey in self.hostkeys: tra.add_server_key(hkey) tra.set_subsystem_handler("sftp", paramiko.SFTPServer, SFTP_Srv) @@ -841,11 +868,12 @@ class Sftpd(object): try: tra.start_server(server=psrv) except Exception as ex: - self.log("%r could not establish connection: %r" % (addr, ex), 3) + if tra.addr: + self.log("%r could not establish connection: %r" % (addr, ex), 3) cli.close() return - chan = tra.accept() + chan = tra.accept(timeout=self.args.sftp_hs_t) if chan is None: self.log("%r did not open an sftp channel" % (addr,), 3) cli.close()