From 9acee05fed9001b2ed3e4563a78d2fb262026774 Mon Sep 17 00:00:00 2001 From: Radu Carpa Date: Tue, 5 Sep 2023 14:27:35 +0200 Subject: [PATCH] [Core] Implement SSL peers support This feature is interesting when multiple deluge instances are managed by the same administrator who uses it to transfer private data across a non-secure network. A separate port has to be allocated for incoming SSL connections from peers. Libtorrent already supports this. It's enough to add the suffix 's' when configuring libtorrent's listen_interfaces. Implement a way to activate listening on an SSL port via the configuration. To actually allow SSL connection between peers, one has to also configure a x509 certificate, private_key and diffie-hellman for each affected torrent. This is achieved by calling libtorrent's handle->set_ssl_certificate. The certificates are only kept in-memory, so they have to be explicitly re-added after each restart. Implement two ways to set these certificates: - either by putting them in a directory with predefined names and letting deluge set them when receiving the corresponding alert; - or by using a core api call. --- deluge/core/core.py | 42 ++++++++++++++++++++++++++ deluge/core/preferencesmanager.py | 46 +++++++++++++++++++++------- deluge/core/torrent.py | 50 +++++++++++++++++++++++++++++++ deluge/core/torrentmanager.py | 43 ++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 10 deletions(-) diff --git a/deluge/core/core.py b/deluge/core/core.py index 198410e318..be3260cb30 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -675,6 +675,43 @@ def connect_peer(self, torrent_id: str, ip: str, port: int): if not self.torrentmanager[torrent_id].connect_peer(ip, port): log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id) + @export + def set_ssl_certificate( + self, + torrent_id: str, + certificate_path: str, + private_key_path: str, + dh_params_path: str, + password: str = '', + ): + """ + Set the SSL certificates used to connect to SSL peers of the given torrent from files. + """ + log.debug('adding ssl certificate %s to %s', certificate_path, torrent_id) + if not self.torrentmanager[torrent_id].set_ssl_certificate( + certificate_path, private_key_path, dh_params_path, password + ): + log.warning( + 'Error adding certificate %s to %s', certificate_path, torrent_id + ) + + @export + def set_ssl_certificate_buffer( + self, + torrent_id: str, + certificate: str, + private_key: str, + dh_params: str, + ): + """ + Set the SSL certificates used to connect to SSL peers of the given torrent. + """ + log.debug('adding ssl certificate to %s', torrent_id) + if not self.torrentmanager[torrent_id].set_ssl_certificate_buffer( + certificate, private_key, dh_params + ): + log.warning('Error adding certificate to %s', torrent_id) + @export def move_storage(self, torrent_ids: List[str], dest: str): log.debug('Moving storage %s to %s', torrent_ids, dest) @@ -822,6 +859,11 @@ def get_listen_port(self) -> int: """Returns the active listen port""" return self.session.listen_port() + @export + def get_ssl_listen_port(self) -> int: + """Returns the active SSL listen port""" + return self.session.ssl_listen_port() + @export def get_proxy(self) -> Dict[str, Any]: """Returns the proxy settings diff --git a/deluge/core/preferencesmanager.py b/deluge/core/preferencesmanager.py index 7e5c207a1f..e01b49942d 100644 --- a/deluge/core/preferencesmanager.py +++ b/deluge/core/preferencesmanager.py @@ -48,6 +48,15 @@ 'listen_random_port': None, 'listen_use_sys_port': False, 'listen_reuse_port': True, + 'ssl_peers': { + 'enabled': False, + 'random_port': True, + 'listen_ports': [6892, 6896], + 'listen_random_port': None, + 'certificate_location': os.path.join( + deluge.configmanager.get_config_dir(), 'ssl_peers_certificates' + ), + }, 'outgoing_ports': [0, 0], 'random_outgoing_ports': True, 'copy_torrent_file': False, @@ -197,18 +206,20 @@ def _on_set_outgoing_interface(self, key, value): def _on_set_random_port(self, key, value): self.__set_listen_on() - def __set_listen_on(self): - """Set the ports and interface address to listen for incoming connections on.""" - if self.config['random_port']: - if not self.config['listen_random_port']: - self.config['listen_random_port'] = random.randrange(49152, 65525) - listen_ports = [ - self.config['listen_random_port'] - ] * 2 # use single port range + @staticmethod + def __pick_ports(config): + if config['random_port']: + if not config['listen_random_port']: + config['listen_random_port'] = random.randrange(49152, 65525) + listen_ports = [config['listen_random_port']] * 2 # use single port range else: - self.config['listen_random_port'] = None - listen_ports = self.config['listen_ports'] + config['listen_random_port'] = None + listen_ports = config['listen_ports'] + return listen_ports + def __set_listen_on(self): + """Set the ports and interface address to listen for incoming connections on.""" + listen_ports = self.__pick_ports(self.config) if self.config['listen_interface']: interface = self.config['listen_interface'].strip() else: @@ -224,6 +235,21 @@ def __set_listen_on(self): f'{interface}:{port}' for port in range(listen_ports[0], listen_ports[1] + 1) ] + + if self.config['ssl_peers']['enabled']: + ssl_listen_ports = self.__pick_ports(self.config['ssl_peers']) + interfaces.extend( + [ + f'{interface}:{port}s' + for port in range(ssl_listen_ports[0], ssl_listen_ports[1] + 1) + ] + ) + log.debug( + 'SSL listen Interface: %s, Ports: %s', + interface, + listen_ports, + ) + self.core.apply_session_settings( { 'listen_system_port_fallback': self.config['listen_use_sys_port'], diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index 57ec26f37a..9afe09ac3d 100644 --- a/deluge/core/torrent.py +++ b/deluge/core/torrent.py @@ -1276,6 +1276,56 @@ def connect_peer(self, peer_ip, peer_port): return False return True + def set_ssl_certificate( + self, + certificate_path: str, + private_key_path: str, + dh_params_path: str, + password: str = '', + ): + """add a peer to the torrent + + Args: + certificate_path(str) : Path to the PEM-encoded x509 certificate + private_key_path(str) : Path to the PEM-encoded private key + dh_params_path(str) : Path to the PEM-encoded Diffie-Hellman parameter + password(str) : (Optional) password used to decrypt the private key + + Returns: + bool: True is successful, otherwise False + """ + try: + self.handle.set_ssl_certificate( + certificate_path, private_key_path, dh_params_path, password + ) + except RuntimeError as ex: + log.debug('Unable to set ssl certificate from file: %s', ex) + return False + return True + + def set_ssl_certificate_buffer( + self, + certificate: str, + private_key: str, + dh_params: str, + ): + """add a peer to the torrent + + Args: + certificate(str) : PEM-encoded content of the x509 certificate + private_key(str) : PEM-encoded content of the private key + dh_params(str) : PEM-encoded content of the Diffie-Hellman parameters + + Returns: + bool: True is successful, otherwise False + """ + try: + self.handle.set_ssl_certificate_buffer(certificate, private_key, dh_params) + except RuntimeError as ex: + log.debug('Unable to set ssl certificate from buffer: %s', ex) + return False + return True + def move_storage(self, dest): """Move a torrent's storage location diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index a758d5c62f..b631d27a1a 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -209,6 +209,7 @@ def __init__(self): 'torrent_finished', 'torrent_paused', 'torrent_checked', + 'torrent_need_cert', 'torrent_resumed', 'tracker_reply', 'tracker_announce', @@ -1339,6 +1340,48 @@ def on_alert_torrent_checked(self, alert): torrent.update_state() + def on_alert_torrent_need_cert(self, alert): + """Alert handler for libtorrent torrent_need_cert_alert""" + + if not self.config['ssl_peers']['enabled']: + return + + torrent_id = str(alert.handle.info_hash()) + base_path = self.config['ssl_peers']['certificate_location'] + if not os.path.isdir(base_path): + return + + certificate_path = None + private_key_path = None + dh_params_path = None + for file_name in [torrent_id + '.dh', 'default.dh']: + params_path = os.path.join(base_path, file_name) + if os.path.isfile(params_path): + dh_params_path = params_path + break + if dh_params_path: + for file_name in [torrent_id, 'default']: + crt_path = os.path.join(base_path, file_name) + key_path = crt_path + '.key' + if os.path.isfile(crt_path) and os.path.isfile(key_path): + certificate_path = crt_path + private_key_path = private_key_path + break + + if certificate_path and private_key_path and dh_params_path: + try: + # Cannot use the handle via self.torrents. + # torrent_need_cert_alert is raised before add_torrent_alert + alert.handle.set_ssl_certificate( + certificate_path, private_key_path, dh_params_path + ) + except RuntimeError: + log.debug( + 'Unable to set ssl certificate for %s from file %s', + torrent_id, + certificate_path, + ) + def on_alert_tracker_reply(self, alert): """Alert handler for libtorrent tracker_reply_alert""" try: