Skip to content

Commit 9acee05

Browse files
author
Radu Carpa
committed
[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.
1 parent a459e78 commit 9acee05

File tree

4 files changed

+171
-10
lines changed

4 files changed

+171
-10
lines changed

deluge/core/core.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,43 @@ def connect_peer(self, torrent_id: str, ip: str, port: int):
675675
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
676676
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)
677677

678+
@export
679+
def set_ssl_certificate(
680+
self,
681+
torrent_id: str,
682+
certificate_path: str,
683+
private_key_path: str,
684+
dh_params_path: str,
685+
password: str = '',
686+
):
687+
"""
688+
Set the SSL certificates used to connect to SSL peers of the given torrent from files.
689+
"""
690+
log.debug('adding ssl certificate %s to %s', certificate_path, torrent_id)
691+
if not self.torrentmanager[torrent_id].set_ssl_certificate(
692+
certificate_path, private_key_path, dh_params_path, password
693+
):
694+
log.warning(
695+
'Error adding certificate %s to %s', certificate_path, torrent_id
696+
)
697+
698+
@export
699+
def set_ssl_certificate_buffer(
700+
self,
701+
torrent_id: str,
702+
certificate: str,
703+
private_key: str,
704+
dh_params: str,
705+
):
706+
"""
707+
Set the SSL certificates used to connect to SSL peers of the given torrent.
708+
"""
709+
log.debug('adding ssl certificate to %s', torrent_id)
710+
if not self.torrentmanager[torrent_id].set_ssl_certificate_buffer(
711+
certificate, private_key, dh_params
712+
):
713+
log.warning('Error adding certificate to %s', torrent_id)
714+
678715
@export
679716
def move_storage(self, torrent_ids: List[str], dest: str):
680717
log.debug('Moving storage %s to %s', torrent_ids, dest)
@@ -822,6 +859,11 @@ def get_listen_port(self) -> int:
822859
"""Returns the active listen port"""
823860
return self.session.listen_port()
824861

862+
@export
863+
def get_ssl_listen_port(self) -> int:
864+
"""Returns the active SSL listen port"""
865+
return self.session.ssl_listen_port()
866+
825867
@export
826868
def get_proxy(self) -> Dict[str, Any]:
827869
"""Returns the proxy settings

deluge/core/preferencesmanager.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@
4848
'listen_random_port': None,
4949
'listen_use_sys_port': False,
5050
'listen_reuse_port': True,
51+
'ssl_peers': {
52+
'enabled': False,
53+
'random_port': True,
54+
'listen_ports': [6892, 6896],
55+
'listen_random_port': None,
56+
'certificate_location': os.path.join(
57+
deluge.configmanager.get_config_dir(), 'ssl_peers_certificates'
58+
),
59+
},
5160
'outgoing_ports': [0, 0],
5261
'random_outgoing_ports': True,
5362
'copy_torrent_file': False,
@@ -197,18 +206,20 @@ def _on_set_outgoing_interface(self, key, value):
197206
def _on_set_random_port(self, key, value):
198207
self.__set_listen_on()
199208

200-
def __set_listen_on(self):
201-
"""Set the ports and interface address to listen for incoming connections on."""
202-
if self.config['random_port']:
203-
if not self.config['listen_random_port']:
204-
self.config['listen_random_port'] = random.randrange(49152, 65525)
205-
listen_ports = [
206-
self.config['listen_random_port']
207-
] * 2 # use single port range
209+
@staticmethod
210+
def __pick_ports(config):
211+
if config['random_port']:
212+
if not config['listen_random_port']:
213+
config['listen_random_port'] = random.randrange(49152, 65525)
214+
listen_ports = [config['listen_random_port']] * 2 # use single port range
208215
else:
209-
self.config['listen_random_port'] = None
210-
listen_ports = self.config['listen_ports']
216+
config['listen_random_port'] = None
217+
listen_ports = config['listen_ports']
218+
return listen_ports
211219

220+
def __set_listen_on(self):
221+
"""Set the ports and interface address to listen for incoming connections on."""
222+
listen_ports = self.__pick_ports(self.config)
212223
if self.config['listen_interface']:
213224
interface = self.config['listen_interface'].strip()
214225
else:
@@ -224,6 +235,21 @@ def __set_listen_on(self):
224235
f'{interface}:{port}'
225236
for port in range(listen_ports[0], listen_ports[1] + 1)
226237
]
238+
239+
if self.config['ssl_peers']['enabled']:
240+
ssl_listen_ports = self.__pick_ports(self.config['ssl_peers'])
241+
interfaces.extend(
242+
[
243+
f'{interface}:{port}s'
244+
for port in range(ssl_listen_ports[0], ssl_listen_ports[1] + 1)
245+
]
246+
)
247+
log.debug(
248+
'SSL listen Interface: %s, Ports: %s',
249+
interface,
250+
listen_ports,
251+
)
252+
227253
self.core.apply_session_settings(
228254
{
229255
'listen_system_port_fallback': self.config['listen_use_sys_port'],

deluge/core/torrent.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,56 @@ def connect_peer(self, peer_ip, peer_port):
12761276
return False
12771277
return True
12781278

1279+
def set_ssl_certificate(
1280+
self,
1281+
certificate_path: str,
1282+
private_key_path: str,
1283+
dh_params_path: str,
1284+
password: str = '',
1285+
):
1286+
"""add a peer to the torrent
1287+
1288+
Args:
1289+
certificate_path(str) : Path to the PEM-encoded x509 certificate
1290+
private_key_path(str) : Path to the PEM-encoded private key
1291+
dh_params_path(str) : Path to the PEM-encoded Diffie-Hellman parameter
1292+
password(str) : (Optional) password used to decrypt the private key
1293+
1294+
Returns:
1295+
bool: True is successful, otherwise False
1296+
"""
1297+
try:
1298+
self.handle.set_ssl_certificate(
1299+
certificate_path, private_key_path, dh_params_path, password
1300+
)
1301+
except RuntimeError as ex:
1302+
log.debug('Unable to set ssl certificate from file: %s', ex)
1303+
return False
1304+
return True
1305+
1306+
def set_ssl_certificate_buffer(
1307+
self,
1308+
certificate: str,
1309+
private_key: str,
1310+
dh_params: str,
1311+
):
1312+
"""add a peer to the torrent
1313+
1314+
Args:
1315+
certificate(str) : PEM-encoded content of the x509 certificate
1316+
private_key(str) : PEM-encoded content of the private key
1317+
dh_params(str) : PEM-encoded content of the Diffie-Hellman parameters
1318+
1319+
Returns:
1320+
bool: True is successful, otherwise False
1321+
"""
1322+
try:
1323+
self.handle.set_ssl_certificate_buffer(certificate, private_key, dh_params)
1324+
except RuntimeError as ex:
1325+
log.debug('Unable to set ssl certificate from buffer: %s', ex)
1326+
return False
1327+
return True
1328+
12791329
def move_storage(self, dest):
12801330
"""Move a torrent's storage location
12811331

deluge/core/torrentmanager.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def __init__(self):
209209
'torrent_finished',
210210
'torrent_paused',
211211
'torrent_checked',
212+
'torrent_need_cert',
212213
'torrent_resumed',
213214
'tracker_reply',
214215
'tracker_announce',
@@ -1339,6 +1340,48 @@ def on_alert_torrent_checked(self, alert):
13391340

13401341
torrent.update_state()
13411342

1343+
def on_alert_torrent_need_cert(self, alert):
1344+
"""Alert handler for libtorrent torrent_need_cert_alert"""
1345+
1346+
if not self.config['ssl_peers']['enabled']:
1347+
return
1348+
1349+
torrent_id = str(alert.handle.info_hash())
1350+
base_path = self.config['ssl_peers']['certificate_location']
1351+
if not os.path.isdir(base_path):
1352+
return
1353+
1354+
certificate_path = None
1355+
private_key_path = None
1356+
dh_params_path = None
1357+
for file_name in [torrent_id + '.dh', 'default.dh']:
1358+
params_path = os.path.join(base_path, file_name)
1359+
if os.path.isfile(params_path):
1360+
dh_params_path = params_path
1361+
break
1362+
if dh_params_path:
1363+
for file_name in [torrent_id, 'default']:
1364+
crt_path = os.path.join(base_path, file_name)
1365+
key_path = crt_path + '.key'
1366+
if os.path.isfile(crt_path) and os.path.isfile(key_path):
1367+
certificate_path = crt_path
1368+
private_key_path = private_key_path
1369+
break
1370+
1371+
if certificate_path and private_key_path and dh_params_path:
1372+
try:
1373+
# Cannot use the handle via self.torrents.
1374+
# torrent_need_cert_alert is raised before add_torrent_alert
1375+
alert.handle.set_ssl_certificate(
1376+
certificate_path, private_key_path, dh_params_path
1377+
)
1378+
except RuntimeError:
1379+
log.debug(
1380+
'Unable to set ssl certificate for %s from file %s',
1381+
torrent_id,
1382+
certificate_path,
1383+
)
1384+
13421385
def on_alert_tracker_reply(self, alert):
13431386
"""Alert handler for libtorrent tracker_reply_alert"""
13441387
try:

0 commit comments

Comments
 (0)