From d07859e8e6a8cae26ba11fc337cb5630bcfa4593 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 21 Feb 2024 00:06:47 +0000 Subject: [PATCH] fix a handful of tftp crashes: * if a nic was restarted mid-transfer, the server could crash * this workaround will probably fix a bunch of similar issues too * fix resource leak if dualstack fails the ipv4 bind --- copyparty/ftpd.py | 3 +++ copyparty/tftpd.py | 62 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 2bc99c38..4d72c4b1 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -20,6 +20,7 @@ from .authsrv import VFS from .bos import bos from .util import ( Daemon, + ODict, Pebkac, exclude_dotfiles, fsenc, @@ -545,6 +546,8 @@ class Ftpd(object): if self.args.ftp4: ips = [x for x in ips if ":" not in x] + ips = list(ODict.fromkeys(ips)) # dedup + ioloop = IOLoop() for ip in ips: for h, lp in hs: diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index ac1aaedf..0020e96a 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -36,7 +36,7 @@ from partftpy.TftpShared import TftpException from .__init__ import EXE, TYPE_CHECKING from .authsrv import VFS from .bos import bos -from .util import BytesIO, Daemon, exclude_dotfiles, min_ex, runhook, undot +from .util import BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot if True: # pylint: disable=using-constant-test from typing import Any, Union @@ -169,6 +169,8 @@ class Tftpd(object): if self.args.ftp4: ips = [x for x in ips if ":" not in x] + ips = list(ODict.fromkeys(ips)) # dedup + for ip in ips: name = "tftp_%s" % (ip,) Daemon(self._start, name, [ip, ports]) @@ -179,18 +181,54 @@ class Tftpd(object): def _start(self, ip, ports): fam = socket.AF_INET6 if ":" in ip else socket.AF_INET - srv = TftpServer.TftpServer("/", self._ls) - with self.mutex: - self.srv.append(srv) - self.ips.append(ip) - try: - srv.listen(ip, self.port, af_family=fam, ports=ports) - except OSError: + have_been_alive = False + while True: + srv = TftpServer.TftpServer("/", self._ls) with self.mutex: - self.srv.remove(srv) - self.ips.remove(ip) - if ip != "0.0.0.0" or "::" not in self.ips: - raise + self.srv.append(srv) + self.ips.append(ip) + + try: + # this is the listen loop; it should block forever + srv.listen(ip, self.port, af_family=fam, ports=ports) + except: + with self.mutex: + self.srv.remove(srv) + self.ips.remove(ip) + + try: + srv.sock.close() + except: + pass + + try: + bound = bool(srv.listenport) + except: + bound = False + + if bound: + # this instance has managed to bind at least once + have_been_alive = True + + if have_been_alive: + t = "tftp server [%s]:%d crashed; restarting in 3 sec:\n%s" + error(t, ip, self.port, min_ex()) + time.sleep(3) + continue + + # server failed to start; could be due to dualstack (ipv6 managed to bind and this is ipv4) + if ip != "0.0.0.0" or "::" not in self.ips: + # nope, it's fatal + t = "tftp server [%s]:%d failed to start:\n%s" + error(t, ip, self.port, min_ex()) + + # yep; ignore + # (TODO: move the "listening @ ..." infolog in partftpy to + # after the bind attempt so it doesn't print twice) + return + + info("tftp server [%s]:%d terminated", ip, self.port) + break def stop(self): with self.mutex: