Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Implement SSL peers support #432

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

rcarpa
Copy link

@rcarpa rcarpa commented Sep 5, 2023

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.

@rcarpa
Copy link
Author

rcarpa commented Sep 5, 2023

Some core-functions added in this PR rely on a pending PR in libtorrent to expose the needed function in the python bindings: arvidn/libtorrent#7509 . So will be available in the next libtorrent release the earliest. Is there any convention on how to handle cases of requiring a specific libtorrent version for functions which are not required for normal deluge operation, but may be useful for some users?

Copy link
Member

@cas-- cas-- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a useful addition, I've added comments for changes and happy to hear your thoughts on how you expect the feature to be used.

If possible it would be quite nice to have some tests.

certificate_path, private_key_path, dh_params_path
)
except RuntimeError:
log.debug(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be log.error and if the exception message is useful then it should also be included.

@@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about having this as an exported method since it requires prior knowledge of the server setup and to me indicates this should be just an internal implementation.

We should also avoid exposing libtorrent implementation details and instead look at what the end-user needs for exported methods. This also applies to naming, so this method it should really be named set_ssl_torrent_cert.

I would consider the simplest option to have one core method accept cert, private key and DH param which are stored to the default SSL cert location for the daemon.

If really need the cert storage could be controlled by an optional parameter if the certs only want to be retained in memory but would that even be a use-case??

Comment on lines 51 to 59
'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'
),
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For naming of this feature we should use the more common term 'SSL Torrents' that end-users are more likely to know and use. I would also prefer that we keep with the existing layout for config, partially since our validator doesn't parse nested dicts.

For the random port option we could simplify it so that the SSL random port is listen_random_port + 1.

This narrows our set to just three config options:

Suggested change
'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'
),
},
'ssl_torrents': False,
'ssl_listen_ports': [6892, 6896],
'ssl_torrents_certs': os.path.join(
deluge.configmanager.get_config_dir(), 'ssl_torrents_certs'
),
},

certificate_path, private_key_path, dh_params_path, password
)
except RuntimeError as ex:
log.debug('Unable to set ssl certificate from file: %s', ex)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be log.error

@export
def get_ssl_listen_port(self) -> int:
"""Returns the active SSL listen port"""
return self.session.ssl_listen_port()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tricky one since we are both adding a new RPC method and relying on a new libtorrent exposed method...

Sometimes we check for specific libtorrent version but it likely is easier in this case to catch the AttributeError and log a warning. For the clients we could either re-raise the exception with a nice message or return an invalid port number, not sure which would be better long-term

Suggested change
return self.session.ssl_listen_port()
try:
return self.session.ssl_listen_port()
except AttributeError:
log.warning("libtorrent version >=2.x required to get active SSL listen port")
return -1

Comment on lines 1354 to 1369
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
Copy link
Member

@cas-- cas-- Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a consensus on file naming convention here? I notice that the cert file is missing an extension which doesn't seem quite right.

I feel that the files should end in .pem to indicate the encoding. The libtorrent tests use:

  • dhparams.pem
  • <torrent_id>.pem
  • <torrent_id>_key.pem

Also these default files will need documented.

I wonder if we can make the code cleaner with pathlib and for else:

Suggested change
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
certs_dir = Path(self.config['ssl_torrents_certs'])
dh_params_file = certs_dir / f'{torrent_id}.dh'
if not dh_params.is_file():
dh_params_file = certs_dir / 'dhparams.pem'
for filename in [torrent_id, 'default']:
cert_file = certs_dir / f'{filename}.pem'
priv_key_file = certs_dir / f'{filename}_key.pem'
if dh_params_file.is_file() and cert_file.is_file() and priv_key_file.is_file():
break
else:
log.error('<Certs not found...>')
return

In fact this feels like reusable code for a common helper function

@rcarpa
Copy link
Author

rcarpa commented Nov 2, 2023

Thank you for the review. I updated the pull request. I slightly deviated from the naming convention you proposed for the files on the disk, because it seemed strange to have a specific naming for dh_params.pem, while calling it <torrent_id>.dh.pem for per-torrent configuration. Files are now automatically saved on disk when set via RPC. I also added a test which creates a seeder and a leecher instance and performs an actual transfer between the 2 using SSL torrents. I did some minor adjustments to facilitate adding this test case. I also rebased on top of #430 which allows to slightly simplify the test case (create and return the torrent on the seeder in one call) + avoids merge conflicts because both PRs change the signature of create_torrent.

@rcarpa
Copy link
Author

rcarpa commented Nov 20, 2023

Just rebased. No functional changes in last force push.

@cas--
Copy link
Member

cas-- commented Nov 27, 2023

Thanks for all the changes and tests! I'll try to get to this PR next, oh and don't worry about rebasing

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. Add a new exported method to perform
this goal. By default, this method will persist certificates on
disk. Allowing them to be re-loaded automatically on restart.
Cleanup the certificates of a torrent when it is removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants