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
This commit is contained in:
ed 2026-07-05 18:59:09 +00:00
parent 32d074ffc5
commit 8c4e931300
2 changed files with 36 additions and 6 deletions

View file

@ -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):

View file

@ -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()