Add handshake/stream encryption RPC commands

Replace protocol.encryption.set bitmask flags with
protocol.encryption.handshake.set and protocol.encryption.stream.set.
Add encryption_config to map commands to libtorrent EncryptionPolicy.
Startup default matches the old preset (allow_incoming, enable_retry,
prefer_plaintext → handshake=allow, stream=allow).

Update docs and examples. Peer list distinguishes RC4 (R/L),
handshake-only (H/h), and plain (r/l). Fix ui encrypted column
conditional. Add encryption config tests; remove OPTION_ENCRYPTION
parse tests. Drop the encryption command redirect.
This commit is contained in:
Jesse Miller
2026-07-08 09:32:30 -06:00
parent fa351c017d
commit 743363b405
18 changed files with 271 additions and 77 deletions
+9 -15
View File
@@ -214,21 +214,15 @@ Add a preferred filename encoding to the list. The encodings are
attempted in the order they are inserted, if none match the torrent
default is used.
.TP
\fBencryption = \fIoption\fB,\fI\&...\fB\fR
Set how rtorrent should deal with encrypted Bittorrent connections. By
default, encryption is disabled, equivalent to specifying the option
\fBnone\fR\&. Alternatively, any number of the following
options may be specified:
\fBallow_incoming\fR (allow incoming encrypted connections),
\fBtry_outgoing\fR (use encryption for outgoing connections),
\fBrequire\fR (disable unencrypted handshakes),
\fBrequire_RC4\fR (also disable plaintext transmission after the
initial encrypted handshake),
\fBenable_retry\fR (if the initial outgoing connection fails, retry
with encryption turned on if it was off or off if it was on),
\fBprefer_plaintext\fR (choose plaintext when peer offers a choice
between plaintext transmission and RC4 encryption, otherwise RC4 will be used).
\fBprotocol.encryption.handshake.set\fR (deny, allow, prefer, require)
and \fBprotocol.encryption.stream.set\fR (deny, allow, prefer,
require) control how rtorrent deals with encrypted Bittorrent
connections. Handshake \fBallow\fR accepts plain or PE inbound and
tries plain first outbound with one PE retry on failure.
\fBprefer\fR tries PE first with one plain retry. Stream \fBallow\fR
and \fBprefer\fR offer both handshake-only and RC4 on outgoing PE
connections. The default is handshake=allow, stream=allow. Use
\fBprotocol.encryption\fR to inspect the effective policy.
.TP
\fBpeer_exchange = \fIyes | no\fB\fR
Enable/disable peer exchange for torrents that aren't marked private. Disabled by default.
+11 -17
View File
@@ -484,25 +484,19 @@ default is used.
</varlistentry>
<varlistentry>
<term>encryption = <replaceable>option</replaceable>,<replaceable>...</replaceable></term>
<term>protocol.encryption.handshake.set = <replaceable>policy</replaceable></term>
<listitem><para>
Set how rtorrent should deal with encrypted Bittorrent connections. By
default, encryption is disabled, equivalent to specifying the option
<emphasis>none</emphasis>. Alternatively, any number of the following
options may be specified:
</para><para>
<emphasis>allow_incoming</emphasis> (allow incoming encrypted connections),
<emphasis>try_outgoing</emphasis> (use encryption for outgoing connections),
<emphasis>require</emphasis> (disable unencrypted handshakes),
<emphasis>require_RC4</emphasis> (also disable plaintext transmission after the
initial encrypted handshake),
<emphasis>enable_retry</emphasis> (if the initial outgoing connection fails, retry
with encryption turned on if it was off or off if it was on),
<emphasis>prefer_plaintext</emphasis> (choose plaintext when peer offers a choice
between plaintext transmission and RC4 encryption, otherwise RC4 will be used).
Control how rtorrent deals with encrypted Bittorrent connections.
<emphasis>protocol.encryption.handshake.set</emphasis> (deny, allow, prefer,
require) and <emphasis>protocol.encryption.stream.set</emphasis> (deny, allow,
prefer, require). Handshake <emphasis>allow</emphasis> accepts plain or PE
inbound and tries plain first outbound with one PE retry on failure.
<emphasis>prefer</emphasis> tries PE first with one plain retry. Stream
<emphasis>allow</emphasis> and <emphasis>prefer</emphasis> offer both
handshake-only and RC4 on outgoing PE connections. The default is
handshake=allow, stream=allow. Use <emphasis>protocol.encryption</emphasis> to
inspect the effective policy.
</para></listitem>
</varlistentry>
+32 -6
View File
@@ -95,14 +95,40 @@
#schedule2 = ip_tick,0,1800,ip=rakshasa
#schedule2 = bind_tick,0,1800,bind=rakshasa
# Encryption options, set to none (default) or any combination of the following:
# allow_incoming, try_outgoing, require, require_RC4, enable_retry, prefer_plaintext
# --- Protocol encryption (PE handshake / optional RC4 stream) ---
# Default at startup: handshake=allow stream=allow
#
# The example value allows incoming encrypted connections, starts unencrypted
# outgoing connections but retries with encryption if they fail, preferring
# plain-text to RC4 encryption after the encrypted handshake.
# Handshake (PE handshake; incoming accepts plain or PE for allow/prefer):
# deny - plain BT handshake only both ways; no retry
# allow - accept plain or PE inbound; plain first outbound with one PE retry
# prefer - accept plain or PE inbound; PE first outbound with one plain retry
# require - PE handshake required both ways; no retry
#
# protocol.encryption.set = allow_incoming,enable_retry,prefer_plaintext
# protocol.encryption.handshake.set = allow
#
# Stream (cipher negotiation after PE handshake succeeds):
# deny - offer handshake-only; require handshake-only (no RC4)
# allow - incoming: prefer handshake-only if offered, else RC4
# outgoing: offer both handshake-only and RC4
# prefer - incoming: pick RC4 if offered, else handshake-only
# outgoing: offer both handshake-only and RC4
# require - offer RC4 only; require RC4
#
# Outgoing retry summary with handshake=allow (plain first):
# stream=allow/prefer -> plain BT -> PE (both)
# stream=deny -> plain BT -> PE (handshake-only)
# stream=require -> plain BT -> PE (RC4-only)
#
# Outgoing retry summary with handshake=prefer (PE first):
# stream=allow/prefer -> PE (both) -> plain BT
# stream=deny -> PE (handshake-only) -> plain BT
# stream=require -> PE (RC4-only) -> plain BT
#
# Negotiated stream cipher is logged at connection_handshake level.
#
# protocol.encryption.stream.set = allow
#
# Use protocol.encryption to inspect the effective policy.
# Enable DHT support for trackerless torrents or when all trackers are down.
# May be set to "disable" (completely disable DHT), "off" (do not start DHT),
+2 -1
View File
@@ -42,7 +42,8 @@ throttle.min_peers.seed.set = 30
throttle.max_peers.seed.set = 80
trackers.numwant.set = 80
protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
protocol.encryption.handshake.set = allow
protocol.encryption.stream.set = prefer
# Limits for file handle resources, this is optimized for
# an `ulimit` of 1024 (a common default). You MUST leave
+2 -1
View File
@@ -72,7 +72,8 @@ rc.throttle.min_peers.seed = 30
rc.throttle.max_peers.seed = 80
rc.trackers.numwant = 80
rc.protocol.encryption.set('allow_incoming', 'try_outgoing', 'enable_retry')
rc.protocol.encryption.handshake.set('allow')
rc.protocol.encryption.stream.set('prefer')
-- Limits for file handle resources, this is optimized for
-- an `ulimit` of 1024 (a common default). You MUST leave
+1 -1
View File
@@ -12,7 +12,7 @@ echo "Client version: " `xmlrpc2scgi.py -p scgi://127.0.0.1:${PORT_NUMBER} syste
echo
echo "Generated by 'rtorrent/doc/scripts/print_option_string.sh' on `date -u`."
for i in strings.choke_heuristics strings.choke_heuristics.upload strings.choke_heuristics.download strings.connection_type strings.encryption strings.ip_filter strings.ip_tos strings.log_group strings.tracker_event strings.tracker_mode; do
for i in strings.choke_heuristics strings.choke_heuristics.upload strings.choke_heuristics.download strings.connection_type strings.encryption.handshake strings.encryption.stream strings.ip_filter strings.ip_tos strings.log_group strings.tracker_event strings.tracker_mode; do
echo
echo $i
echo `echo $i | tr 'a-z_.' '-'`
+2
View File
@@ -188,6 +188,8 @@ libsub_root_a_SOURCES = \
command_local.cc \
command_logging.cc \
command_network.cc \
encryption_config.cc \
encryption_config.h \
command_peer.cc \
command_throttle.cc \
command_tracker.cc \
+4 -2
View File
@@ -448,7 +448,8 @@ initialize_command_dynamic() {
CMD2_ANY ("strings.choke_heuristics.upload", std::bind(&torrent::option_list_strings, torrent::OPTION_CHOKE_HEURISTICS_UPLOAD));
CMD2_ANY ("strings.choke_heuristics.download", std::bind(&torrent::option_list_strings, torrent::OPTION_CHOKE_HEURISTICS_DOWNLOAD));
CMD2_ANY ("strings.connection_type", std::bind(&torrent::option_list_strings, torrent::OPTION_CONNECTION_TYPE));
CMD2_ANY ("strings.encryption", std::bind(&torrent::option_list_strings, torrent::OPTION_ENCRYPTION));
CMD2_ANY ("strings.encryption.handshake", std::bind(&torrent::option_list_strings, torrent::OPTION_ENCRYPTION_MODE));
CMD2_ANY ("strings.encryption.stream", std::bind(&torrent::option_list_strings, torrent::OPTION_ENCRYPTION_MODE));
CMD2_ANY ("strings.ip_filter", std::bind(&torrent::option_list_strings, torrent::OPTION_IP_FILTER));
CMD2_ANY ("strings.ip_tos", std::bind(&torrent::option_list_strings, torrent::OPTION_IP_TOS));
CMD2_ANY ("strings.log_group", std::bind(&torrent::option_list_strings, torrent::OPTION_LOG_GROUP));
@@ -472,7 +473,8 @@ initialize_command_dynamic() {
rpc::rpc.mark_safe("strings.choke_heuristics.upload");
rpc::rpc.mark_safe("strings.choke_heuristics.download");
rpc::rpc.mark_safe("strings.connection_type");
rpc::rpc.mark_safe("strings.encryption");
rpc::rpc.mark_safe("strings.encryption.handshake");
rpc::rpc.mark_safe("strings.encryption.stream");
rpc::rpc.mark_safe("strings.ip_filter");
rpc::rpc.mark_safe("strings.ip_tos");
rpc::rpc.mark_safe("strings.log_group");
+3 -19
View File
@@ -17,6 +17,7 @@
#include <torrent/utils/log.h>
#include <torrent/utils/option_strings.h>
#include "encryption_config.h"
#include "globals.h"
#include "control.h"
#include "command_helpers.h"
@@ -32,24 +33,6 @@
#include <systemd/sd-daemon.h>
#endif
torrent::Object
apply_encryption(const torrent::Object::list_type& args) {
uint32_t options_mask = torrent::runtime::NetworkConfig::encryption_none;
for (const auto& arg : args) {
uint32_t opt = torrent::option_find_string(torrent::OPTION_ENCRYPTION, arg.as_string().c_str());
if (opt == torrent::runtime::NetworkConfig::encryption_none)
options_mask = torrent::runtime::NetworkConfig::encryption_none;
else
options_mask |= opt;
}
torrent::runtime::network_config()->set_encryption_options(options_mask);
return torrent::Object();
}
torrent::Object
apply_tos(const torrent::Object::string_type& arg) {
rpc::command_base::value_type value;
@@ -230,7 +213,8 @@ initialize_command_network() {
CMD_ANY_VALUE_V ("network.listen.backlog.set", [nw_config](auto, auto& value) { return nw_config->set_listen_backlog(value); });
CMD_VAR_BOOL ("protocol.pex", true);
CMD_ANY_LIST ("protocol.encryption.set", [](auto, auto& args) { return apply_encryption(args); });
encryption_config::initialize_commands();
CMD_VAR_STRING ("protocol.connection.leech", "leech");
CMD_VAR_STRING ("protocol.connection.seed", "seed");
+17 -1
View File
@@ -16,6 +16,21 @@
namespace display {
namespace {
char
connection_type_char(const torrent::Peer* p) {
if (p->is_encrypted())
return p->is_incoming() ? 'R' : 'L';
if (p->is_obfuscated())
return p->is_incoming() ? 'H' : 'h';
return p->is_incoming() ? 'r' : 'l';
}
} // namespace
WindowPeerList::WindowPeerList(core::Download* d, PList* l, PList::iterator* f) :
Window(new Canvas, 0, 0, 0, extent_full, extent_full),
m_download(d),
@@ -35,6 +50,7 @@ WindowPeerList::redraw() {
m_canvas->print(x, y, "UP"); x += 7;
m_canvas->print(x, y, "DOWN"); x += 7;
m_canvas->print(x, y, "PEER"); x += 7;
// CT: R/H/r or L/h/l (RC4 / handshake-only / plain) + peer type (u/p/ )
m_canvas->print(x, y, "CT/RE/LO"); x += 10;
m_canvas->print(x, y, "QS"); x += 6;
m_canvas->print(x, y, "DONE"); x += 6;
@@ -95,7 +111,7 @@ WindowPeerList::redraw() {
peerType = ' ';
m_canvas->print(x, y, "%c%c/%c%c/%c%c",
p->is_encrypted() ? (p->is_incoming() ? 'R' : 'L') : (p->is_incoming() ? 'r' : 'l'),
connection_type_char(p),
peerType,
p->is_down_choked() ? std::tolower(remoteChoked) : remoteChoked,
+88
View File
@@ -0,0 +1,88 @@
#include "config.h"
#include "encryption_config.h"
#include <torrent/runtime/network_config.h>
#include <torrent/runtime/runtime.h>
#include <torrent/utils/option_strings.h>
#include "command_helpers.h"
#include "globals.h"
namespace encryption_config {
namespace {
Policy
current_policy() {
return torrent::runtime::network_config()->encryption_policy();
}
} // namespace
Policy
default_policy() {
Policy policy;
policy.handshake = Policy::Mode::allow;
policy.stream = Policy::Mode::allow;
return policy;
}
std::string
mode_to_string(Policy::Mode value) {
return torrent::option_to_str_or_throw(torrent::OPTION_ENCRYPTION_MODE,
static_cast<unsigned int>(value));
}
void
apply_mode_value(Policy& policy, Policy::Mode Policy::*field, const std::string& value) {
policy.*field = static_cast<Policy::Mode>(
torrent::option_find_string_str(torrent::OPTION_ENCRYPTION_MODE, value));
}
std::string
summary_string(const Policy& policy) {
return "handshake=" + mode_to_string(policy.handshake)
+ " stream=" + mode_to_string(policy.stream);
}
void
apply_policy(const Policy& policy) {
torrent::runtime::network_config()->set_encryption_policy(policy);
}
torrent::Object
apply_handshake_set(const std::string& value) {
Policy policy = current_policy();
apply_mode_value(policy, &Policy::handshake, value);
apply_policy(policy);
return torrent::Object();
}
torrent::Object
apply_stream_set(const std::string& value) {
Policy policy = current_policy();
apply_mode_value(policy, &Policy::stream, value);
apply_policy(policy);
return torrent::Object();
}
void
initialize_commands() {
apply_policy(default_policy());
CMD2_ANY("protocol.encryption", [](auto, auto) { return summary_string(current_policy()); });
CMD2_ANY("protocol.encryption.handshake", [](auto, auto) { return mode_to_string(current_policy().handshake); });
CMD2_ANY_STRING_V("protocol.encryption.handshake.set", [](auto, auto& str) { return apply_handshake_set(str); });
CMD2_ANY("protocol.encryption.stream", [](auto, auto) { return mode_to_string(current_policy().stream); });
CMD2_ANY_STRING_V("protocol.encryption.stream.set", [](auto, auto& str) { return apply_stream_set(str); });
rpc::rpc.mark_safe("protocol.encryption");
rpc::rpc.mark_safe("protocol.encryption.handshake");
rpc::rpc.mark_safe("protocol.encryption.handshake.set");
rpc::rpc.mark_safe("protocol.encryption.stream");
rpc::rpc.mark_safe("protocol.encryption.stream.set");
}
} // namespace encryption_config
+29
View File
@@ -0,0 +1,29 @@
#ifndef RTORRENT_ENCRYPTION_CONFIG_H
#define RTORRENT_ENCRYPTION_CONFIG_H
#include <cstdint>
#include <string>
#include <torrent/runtime/encryption_policy.h>
#include <torrent/object.h>
namespace encryption_config {
using Policy = torrent::EncryptionPolicy;
Policy default_policy();
std::string mode_to_string(Policy::Mode value);
void apply_mode_value(Policy& policy, Policy::Mode Policy::*field, const std::string& value);
std::string summary_string(const Policy& policy);
void apply_policy(const Policy& policy);
torrent::Object apply_handshake_set(const std::string& value);
torrent::Object apply_stream_set(const std::string& value);
void initialize_commands();
} // namespace encryption_config
#endif
+2 -3
View File
@@ -298,7 +298,8 @@ main(int argc, char** argv) {
"schedule = low_diskspace,5,60,((close_low_diskspace,500M))\n"
"schedule = prune_file_status,3600,86400,((system.file_status_cache.prune))\n"
"protocol.encryption.set=allow_incoming,prefer_plaintext,enable_retry\n"
"protocol.encryption.handshake.set=allow\n"
"protocol.encryption.stream.set=allow\n"
"ui.color.focus.set=reverse\n"
);
@@ -318,8 +319,6 @@ main(int argc, char** argv) {
CMD_REDIRECT("ratio.max.set", "group.seeding.ratio.max.set");
CMD_REDIRECT("ratio.upload.set", "group.seeding.ratio.upload.set");
CMD_REDIRECT("encryption", "protocol.encryption.set");
CMD_REDIRECT("check_hash", "pieces.hash.on_completion.set");
CMD_REDIRECT("connection_leech", "protocol.connection.leech.set");
+1 -1
View File
@@ -111,7 +111,7 @@ ElementPeerList::create_info() {
element->push_column("Client:", te_command("p.client_version="));
element->push_column("Options:", te_command("p.options_str="));
element->push_column("Connected:", te_command("if=$p.is_incoming=,incoming,outgoing"));
element->push_column("Encrypted:", te_command("if=$p.is_encrypted=,yes,$p.is_obfuscated=,handshake,no"));
element->push_column("Encrypted:", te_command("if=$p.is_encrypted=,yes,$if=$p.is_obfuscated=\\,handshake\\,no"));
element->push_back("");
element->push_column("Snubbed:", te_command("if=$p.is_snubbed=,yes,no"));
+2
View File
@@ -49,6 +49,8 @@ rtorrent_Test_Rpc_SOURCES = $(rtorrent_Test_Common) \
rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \
src/test_command_dynamic.cc \
src/test_command_dynamic.h \
src/test_encryption_config.cc \
src/test_encryption_config.h \
src/test_watch_ready_queue.cc \
src/test_watch_ready_queue.h
+9 -10
View File
@@ -170,19 +170,18 @@ TestParseOptions::test_flag_libtorrent() {
FLAG_LT_LOG_ASSERT_ERROR("resume_data|rpc_dump");
}
#define FLAGS_LT_ENCRYPTION_ASSERT(flags, result) \
CPPUNIT_ASSERT(rpc::parse_option_flags(flags, std::bind(&torrent::option_find_string_str, torrent::OPTION_ENCRYPTION, std::placeholders::_1)) == (result))
#define FLAGS_LT_IP_TOS_ASSERT(flags, result) \
CPPUNIT_ASSERT(rpc::parse_option_flags(flags, std::bind(&torrent::option_find_string_str, torrent::OPTION_IP_TOS, std::placeholders::_1)) == (result))
#define FLAGS_LT_ENCRYPTION_ASSERT_ERROR(flags) \
ASSERT_CATCH_INPUT_ERROR(rpc::parse_option_flags(flags, std::bind(&torrent::option_find_string_str, torrent::OPTION_ENCRYPTION, std::placeholders::_1)))
#define FLAGS_LT_IP_TOS_ASSERT_ERROR(flags) \
ASSERT_CATCH_INPUT_ERROR(rpc::parse_option_flags(flags, std::bind(&torrent::option_find_string_str, torrent::OPTION_IP_TOS, std::placeholders::_1)))
void
TestParseOptions::test_flags_libtorrent() {
FLAGS_LT_ENCRYPTION_ASSERT("", torrent::runtime::NetworkConfig::encryption_none);
FLAGS_LT_ENCRYPTION_ASSERT("none", torrent::runtime::NetworkConfig::encryption_none);
FLAGS_LT_ENCRYPTION_ASSERT("require_rc4", torrent::runtime::NetworkConfig::encryption_require_RC4);
FLAGS_LT_ENCRYPTION_ASSERT("require_RC4", torrent::runtime::NetworkConfig::encryption_require_RC4);
FLAGS_LT_ENCRYPTION_ASSERT("require_RC4 | enable_retry", torrent::runtime::NetworkConfig::encryption_require_RC4 | torrent::runtime::NetworkConfig::encryption_enable_retry);
FLAGS_LT_IP_TOS_ASSERT("throughput", torrent::option_find_string(torrent::OPTION_IP_TOS, "throughput"));
FLAGS_LT_IP_TOS_ASSERT("lowdelay | throughput",
torrent::option_find_string(torrent::OPTION_IP_TOS, "lowdelay")
| torrent::option_find_string(torrent::OPTION_IP_TOS, "throughput"));
FLAGS_LT_ENCRYPTION_ASSERT_ERROR("require_");
FLAGS_LT_IP_TOS_ASSERT_ERROR("throughput_");
}
+45
View File
@@ -0,0 +1,45 @@
#include "config.h"
#include "test/src/test_encryption_config.h"
#include <torrent/runtime/encryption_policy.h>
#include "encryption_config.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestEncryptionConfig);
void
TestEncryptionConfig::test_default_policy() {
const auto policy = encryption_config::default_policy();
CPPUNIT_ASSERT(policy.handshake == encryption_config::Policy::Mode::allow);
CPPUNIT_ASSERT(policy.stream == encryption_config::Policy::Mode::allow);
CPPUNIT_ASSERT_EQUAL(std::string("handshake=allow stream=allow"),
encryption_config::summary_string(policy));
}
void
TestEncryptionConfig::test_granular_round_trip() {
auto round_trip_handshake = [](const std::string& value) {
encryption_config::Policy policy;
encryption_config::apply_mode_value(policy, &encryption_config::Policy::handshake, value);
return encryption_config::mode_to_string(policy.handshake);
};
CPPUNIT_ASSERT_EQUAL(std::string("deny"), round_trip_handshake("deny"));
CPPUNIT_ASSERT_EQUAL(std::string("allow"), round_trip_handshake("allow"));
CPPUNIT_ASSERT_EQUAL(std::string("prefer"), round_trip_handshake("prefer"));
CPPUNIT_ASSERT_EQUAL(std::string("require"), round_trip_handshake("require"));
auto round_trip_stream = [](const std::string& value) {
encryption_config::Policy policy;
encryption_config::apply_mode_value(policy, &encryption_config::Policy::stream, value);
return encryption_config::mode_to_string(policy.stream);
};
CPPUNIT_ASSERT_EQUAL(std::string("deny"), round_trip_stream("deny"));
CPPUNIT_ASSERT_EQUAL(std::string("allow"), round_trip_stream("allow"));
CPPUNIT_ASSERT_EQUAL(std::string("prefer"), round_trip_stream("prefer"));
CPPUNIT_ASSERT_EQUAL(std::string("require"), round_trip_stream("require"));
}
+12
View File
@@ -0,0 +1,12 @@
#include "test/helpers/test_fixture.h"
class TestEncryptionConfig : public test_fixture {
CPPUNIT_TEST_SUITE(TestEncryptionConfig);
CPPUNIT_TEST(test_default_policy);
CPPUNIT_TEST(test_granular_round_trip);
CPPUNIT_TEST_SUITE_END();
public:
void test_default_policy();
void test_granular_round_trip();
};