Skip to content

Commit

Permalink
[Core] Implement SSL peers support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rcarpa committed Sep 5, 2023
1 parent a459e78 commit 9acee05
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 10 deletions.
42 changes: 42 additions & 0 deletions deluge/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
46 changes: 36 additions & 10 deletions deluge/core/preferencesmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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'],
Expand Down
50 changes: 50 additions & 0 deletions deluge/core/torrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions deluge/core/torrentmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def __init__(self):
'torrent_finished',
'torrent_paused',
'torrent_checked',
'torrent_need_cert',
'torrent_resumed',
'tracker_reply',
'tracker_announce',
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 9acee05

Please sign in to comment.