librats 2.3.2 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +1 -0
- package/native-src/CMakeLists.txt +15 -0
- package/native-src/src/librats/bindings/rats.cpp +62 -4
- package/native-src/src/librats/bindings/rats.h +32 -1
- package/native-src/src/librats/bittorrent/client.cpp +221 -26
- package/native-src/src/librats/bittorrent/client.h +73 -6
- package/native-src/src/librats/bittorrent/mse.cpp +626 -0
- package/native-src/src/librats/bittorrent/mse.h +218 -0
- package/native-src/src/librats/bittorrent/peer_connection.cpp +187 -59
- package/native-src/src/librats/bittorrent/peer_connection.h +109 -17
- package/native-src/src/librats/bittorrent/peer_link.cpp +134 -0
- package/native-src/src/librats/bittorrent/peer_link.h +145 -0
- package/native-src/src/librats/bittorrent/peer_list.cpp +75 -12
- package/native-src/src/librats/bittorrent/peer_list.h +130 -18
- package/native-src/src/librats/bittorrent/torrent.cpp +54 -11
- package/native-src/src/librats/bittorrent/torrent.h +19 -3
- package/native-src/src/librats/bittorrent/types.h +27 -0
- package/native-src/src/librats/bittorrent/utp_manager.cpp +234 -0
- package/native-src/src/librats/bittorrent/utp_manager.h +131 -0
- package/native-src/src/librats/bittorrent/utp_packet.h +216 -0
- package/native-src/src/librats/bittorrent/utp_stream.cpp +903 -0
- package/native-src/src/librats/bittorrent/utp_stream.h +400 -0
- package/native-src/src/librats/core/socket.cpp +44 -8
- package/native-src/src/librats/core/socket.h +23 -1
- package/native-src/src/librats/subsystems/file_transfer.cpp +54 -20
- package/native-src/src/librats/subsystems/file_transfer.h +21 -3
- package/package.json +1 -1
- package/src/librats_node.cpp +6 -3
package/lib/index.d.ts
CHANGED
|
@@ -379,6 +379,15 @@ if(RATS_SEARCH_FEATURES)
|
|
|
379
379
|
src/librats/bittorrent/piece_picker.cpp
|
|
380
380
|
src/librats/bittorrent/reactor.h
|
|
381
381
|
src/librats/bittorrent/reactor.cpp
|
|
382
|
+
src/librats/bittorrent/mse.h
|
|
383
|
+
src/librats/bittorrent/mse.cpp
|
|
384
|
+
src/librats/bittorrent/utp_packet.h
|
|
385
|
+
src/librats/bittorrent/utp_stream.h
|
|
386
|
+
src/librats/bittorrent/utp_stream.cpp
|
|
387
|
+
src/librats/bittorrent/utp_manager.h
|
|
388
|
+
src/librats/bittorrent/utp_manager.cpp
|
|
389
|
+
src/librats/bittorrent/peer_link.h
|
|
390
|
+
src/librats/bittorrent/peer_link.cpp
|
|
382
391
|
src/librats/bittorrent/peer_connection.h
|
|
383
392
|
src/librats/bittorrent/peer_connection.cpp
|
|
384
393
|
src/librats/bittorrent/choker.h
|
|
@@ -736,14 +745,20 @@ if(RATS_BUILD_TESTS)
|
|
|
736
745
|
tests/test_bt_piece_picker.cpp
|
|
737
746
|
# Phase 4 — reactor + peer wire protocol
|
|
738
747
|
tests/test_bt_reactor.cpp
|
|
748
|
+
tests/test_bt_mse.cpp
|
|
749
|
+
tests/test_bt_utp.cpp
|
|
739
750
|
tests/test_bt_peer_connection.cpp
|
|
740
751
|
# Phase 5 — torrent + client + choker
|
|
741
752
|
tests/test_bt_choker.cpp
|
|
742
753
|
tests/test_bt_download.cpp
|
|
754
|
+
tests/test_bt_mse_wire.cpp
|
|
755
|
+
tests/test_bt_utp_wire.cpp
|
|
743
756
|
tests/test_bt_stall_metadata.cpp
|
|
757
|
+
tests/test_bt_connect_timeout.cpp
|
|
744
758
|
# Phase 6 — extension protocol + metadata exchange + discovery
|
|
745
759
|
tests/test_bt_extensions.cpp
|
|
746
760
|
tests/test_bt_magnet_fetch.cpp
|
|
761
|
+
tests/test_bt_dht_peer_sources.cpp
|
|
747
762
|
tests/test_bt_peer_list.cpp
|
|
748
763
|
tests/test_bt_pex.cpp
|
|
749
764
|
tests/test_bt_tracker.cpp
|
|
@@ -46,6 +46,12 @@ struct RatsHandle {
|
|
|
46
46
|
Relay* relay = nullptr;
|
|
47
47
|
#ifdef RATS_SEARCH_FEATURES
|
|
48
48
|
Bittorrent* bittorrent = nullptr;
|
|
49
|
+
/// MSE/PE policy for the BitTorrent session, applied when it is enabled.
|
|
50
|
+
/// Defaults match the C++ side: obfuscate outgoing dials first, accept both.
|
|
51
|
+
bittorrent::EncPolicy bt_out_enc = bittorrent::EncPolicy::Enabled;
|
|
52
|
+
bittorrent::EncPolicy bt_in_enc = bittorrent::EncPolicy::Enabled;
|
|
53
|
+
bool bt_out_utp = true;
|
|
54
|
+
bool bt_in_utp = true;
|
|
49
55
|
#endif
|
|
50
56
|
std::string data_dir; ///< copied from NodeConfig so subsystems can co-locate state
|
|
51
57
|
bool dht_enabled = false;
|
|
@@ -574,8 +580,9 @@ rats_error_t rats_on_file_complete(rats_t node, rats_file_complete_cb cb, void*
|
|
|
574
580
|
if (!cb) return RATS_ERR_INVALID_ARG;
|
|
575
581
|
auto* h = as_handle(node);
|
|
576
582
|
if (!h->files) return RATS_ERR_NOT_ENABLED;
|
|
577
|
-
h->files->on_complete([cb, user](uint64_t id, bool success, const std::string& path) {
|
|
578
|
-
|
|
583
|
+
h->files->on_complete([cb, user](const PeerId& peer, uint64_t id, bool success, const std::string& path) {
|
|
584
|
+
const std::string hex = peer.to_hex();
|
|
585
|
+
cb(user, id, hex.c_str(), success ? 1 : 0, path.c_str());
|
|
579
586
|
});
|
|
580
587
|
return RATS_OK;
|
|
581
588
|
}
|
|
@@ -686,8 +693,12 @@ rats_error_t rats_enable_bittorrent(rats_t node, uint16_t listen_port, const cha
|
|
|
686
693
|
if (h->started) return RATS_ERR_ALREADY_STARTED;
|
|
687
694
|
if (!h->bittorrent) {
|
|
688
695
|
Bittorrent::Config cfg;
|
|
689
|
-
cfg.client.listen_port
|
|
690
|
-
cfg.client.download_path
|
|
696
|
+
cfg.client.listen_port = listen_port;
|
|
697
|
+
cfg.client.download_path = download_path ? download_path : ".";
|
|
698
|
+
cfg.client.out_enc_policy = h->bt_out_enc;
|
|
699
|
+
cfg.client.in_enc_policy = h->bt_in_enc;
|
|
700
|
+
cfg.client.enable_outgoing_utp = h->bt_out_utp;
|
|
701
|
+
cfg.client.enable_incoming_utp = h->bt_in_utp;
|
|
691
702
|
h->bittorrent = h->node->add_subsystem(std::make_unique<Bittorrent>(cfg));
|
|
692
703
|
}
|
|
693
704
|
return RATS_OK;
|
|
@@ -697,6 +708,53 @@ rats_error_t rats_enable_bittorrent(rats_t node, uint16_t listen_port, const cha
|
|
|
697
708
|
#endif
|
|
698
709
|
}
|
|
699
710
|
|
|
711
|
+
rats_error_t rats_bt_set_encryption(rats_t node, rats_bt_enc_policy_t out_policy,
|
|
712
|
+
rats_bt_enc_policy_t in_policy) {
|
|
713
|
+
#ifdef RATS_SEARCH_FEATURES
|
|
714
|
+
auto to_policy = [](rats_bt_enc_policy_t p, bool& ok) {
|
|
715
|
+
switch (p) {
|
|
716
|
+
case RATS_BT_ENC_FORCED: return bittorrent::EncPolicy::Forced;
|
|
717
|
+
case RATS_BT_ENC_ENABLED: return bittorrent::EncPolicy::Enabled;
|
|
718
|
+
case RATS_BT_ENC_DISABLED: return bittorrent::EncPolicy::Disabled;
|
|
719
|
+
}
|
|
720
|
+
ok = false;
|
|
721
|
+
return bittorrent::EncPolicy::Enabled;
|
|
722
|
+
};
|
|
723
|
+
bool ok = true;
|
|
724
|
+
auto* h = as_handle(node);
|
|
725
|
+
if (h->started) return RATS_ERR_ALREADY_STARTED;
|
|
726
|
+
// The policy is baked into the session's config when it is created, so asking
|
|
727
|
+
// for it afterwards would silently do nothing — say so instead.
|
|
728
|
+
if (h->bittorrent) return RATS_ERR_ALREADY_STARTED;
|
|
729
|
+
const auto out = to_policy(out_policy, ok);
|
|
730
|
+
const auto in = to_policy(in_policy, ok);
|
|
731
|
+
if (!ok) return RATS_ERR_INVALID_ARG;
|
|
732
|
+
h->bt_out_enc = out;
|
|
733
|
+
h->bt_in_enc = in;
|
|
734
|
+
return RATS_OK;
|
|
735
|
+
#else
|
|
736
|
+
(void)node; (void)out_policy; (void)in_policy;
|
|
737
|
+
return RATS_ERR_NOT_ENABLED;
|
|
738
|
+
#endif
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
rats_error_t rats_bt_set_utp(rats_t node, int enable_outgoing, int enable_incoming) {
|
|
742
|
+
#ifdef RATS_SEARCH_FEATURES
|
|
743
|
+
auto* h = as_handle(node);
|
|
744
|
+
if (h->started) return RATS_ERR_ALREADY_STARTED;
|
|
745
|
+
// Same reasoning as rats_bt_set_encryption: the setting is baked into the
|
|
746
|
+
// session's config when it is created, so accepting it afterwards would
|
|
747
|
+
// silently do nothing.
|
|
748
|
+
if (h->bittorrent) return RATS_ERR_ALREADY_STARTED;
|
|
749
|
+
h->bt_out_utp = enable_outgoing != 0;
|
|
750
|
+
h->bt_in_utp = enable_incoming != 0;
|
|
751
|
+
return RATS_OK;
|
|
752
|
+
#else
|
|
753
|
+
(void)node; (void)enable_outgoing; (void)enable_incoming;
|
|
754
|
+
return RATS_ERR_NOT_ENABLED;
|
|
755
|
+
#endif
|
|
756
|
+
}
|
|
757
|
+
|
|
700
758
|
rats_error_t rats_bt_add_magnet(rats_t node, const char* magnet_uri, const char* save_path) {
|
|
701
759
|
#ifdef RATS_SEARCH_FEATURES
|
|
702
760
|
if (!magnet_uri) return RATS_ERR_INVALID_ARG;
|
|
@@ -345,7 +345,8 @@ typedef void (*rats_file_offer_cb)(void* user, const char* peer_id_hex, uint64_t
|
|
|
345
345
|
const char* name, uint64_t size, int is_directory);
|
|
346
346
|
typedef void (*rats_file_progress_cb)(void* user, uint64_t transfer_id, const char* peer_id_hex,
|
|
347
347
|
uint64_t bytes_transferred, uint64_t total_bytes, int status);
|
|
348
|
-
typedef void (*rats_file_complete_cb)(void* user, uint64_t transfer_id,
|
|
348
|
+
typedef void (*rats_file_complete_cb)(void* user, uint64_t transfer_id, const char* peer_id_hex,
|
|
349
|
+
int success, const char* path);
|
|
349
350
|
|
|
350
351
|
/** Enable the file-transfer subsystem. `temp_dir` holds in-progress downloads
|
|
351
352
|
* (NULL → current directory). Call before start(). */
|
|
@@ -400,6 +401,36 @@ RATS_API rats_error_t rats_remove_reconnect(rats_t node, const char* host, uint1
|
|
|
400
401
|
/** Enable BitTorrent. listen_port 0 picks an ephemeral port; download_path is the
|
|
401
402
|
* default save directory (NULL = "."). Enable DHT first to share the node's DHT. */
|
|
402
403
|
RATS_API rats_error_t rats_enable_bittorrent(rats_t node, uint16_t listen_port, const char* download_path);
|
|
404
|
+
|
|
405
|
+
/** MSE/PE connection obfuscation policy (see librats/bittorrent/mse.h). */
|
|
406
|
+
typedef enum {
|
|
407
|
+
RATS_BT_ENC_FORCED = 0, /**< obfuscated connections only, both directions */
|
|
408
|
+
RATS_BT_ENC_ENABLED = 1, /**< default: obfuscate first, fall back to plaintext */
|
|
409
|
+
RATS_BT_ENC_DISABLED = 2, /**< plaintext only */
|
|
410
|
+
} rats_bt_enc_policy_t;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Set the BitTorrent encryption policy. Optional — the default (ENABLED for both)
|
|
414
|
+
* already dials obfuscated first, which is what reaches the large part of the swarm
|
|
415
|
+
* that refuses plaintext. Must be called BEFORE rats_enable_bittorrent, since the
|
|
416
|
+
* policy is fixed when the session is created.
|
|
417
|
+
*/
|
|
418
|
+
RATS_API rats_error_t rats_bt_set_encryption(rats_t node, rats_bt_enc_policy_t out_policy,
|
|
419
|
+
rats_bt_enc_policy_t in_policy);
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Turn uTP (BEP 29) on or off, per direction. Optional — both are on by default,
|
|
423
|
+
* which is what every modern client ships: uTP's delay-based congestion control
|
|
424
|
+
* yields to other traffic instead of saturating the user's uplink, and much of the
|
|
425
|
+
* swarm answers UDP more readily than TCP. A peer with no uTP costs one connect
|
|
426
|
+
* timeout before the dial falls back to TCP, and is remembered.
|
|
427
|
+
*
|
|
428
|
+
* Pass non-zero to enable each direction. Turning outgoing uTP off makes every dial TCP. Turning incoming off still dials
|
|
429
|
+
* out over uTP but never answers, which is what a blocked inbound UDP port looks
|
|
430
|
+
* like anyway. Must be called BEFORE rats_enable_bittorrent, since the setting is
|
|
431
|
+
* fixed when the session is created.
|
|
432
|
+
*/
|
|
433
|
+
RATS_API rats_error_t rats_bt_set_utp(rats_t node, int enable_outgoing, int enable_incoming);
|
|
403
434
|
/** Start downloading a magnet link (metadata is fetched from peers). */
|
|
404
435
|
RATS_API rats_error_t rats_bt_add_magnet(rats_t node, const char* magnet_uri, const char* save_path);
|
|
405
436
|
/** Start a torrent from a .torrent file on disk. */
|
|
@@ -12,6 +12,7 @@ Client::Client() : Client(Config{}) {}
|
|
|
12
12
|
|
|
13
13
|
Client::Client(Config config)
|
|
14
14
|
: config_(std::move(config))
|
|
15
|
+
, utp_(reactor_)
|
|
15
16
|
, peer_id_(generate_peer_id(config_.peer_id_prefix)) {}
|
|
16
17
|
|
|
17
18
|
Client::~Client() {
|
|
@@ -52,9 +53,15 @@ void Client::stop() {
|
|
|
52
53
|
connections_.clear();
|
|
53
54
|
// Reclaim any outbound sockets still mid-connect (their completion lambda will
|
|
54
55
|
// never run now that the reactor is stopped).
|
|
55
|
-
for (
|
|
56
|
+
for (const auto& [s, deadline] : pending_connects_) {
|
|
57
|
+
reactor_.cancel(deadline);
|
|
58
|
+
reactor_.remove(s);
|
|
59
|
+
close_socket(s);
|
|
60
|
+
}
|
|
56
61
|
pending_connects_.clear();
|
|
57
62
|
if (is_valid_socket(listener_)) { reactor_.remove(listener_); close_socket(listener_); listener_ = RATS_INVALID_SOCKET; }
|
|
63
|
+
// After connections_, so every UtpPeerLink has already handed its stream back.
|
|
64
|
+
utp_.close();
|
|
58
65
|
// Fresh token, so a Client that is start()ed again gets DHT peers delivered.
|
|
59
66
|
// Safe to swap unsynchronised: the reactor thread is joined and the old token is
|
|
60
67
|
// kept alive by whatever callbacks still hold it.
|
|
@@ -62,16 +69,73 @@ void Client::stop() {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
void Client::open_listener() {
|
|
65
|
-
|
|
72
|
+
const bool want_utp = config_.enable_outgoing_utp || config_.enable_incoming_utp;
|
|
73
|
+
|
|
74
|
+
// TCP and uTP must answer on the *same* port: a peer learns one number for us
|
|
75
|
+
// (from the tracker, the DHT or PEX) and has to be able to reach us with it
|
|
76
|
+
// over either wire. When the port is ephemeral the TCP bind picks it and the
|
|
77
|
+
// UDP bind may then find that number already taken by something else, so the
|
|
78
|
+
// pair is acquired as a pair and retried as one.
|
|
79
|
+
constexpr int kPairAttempts = 8;
|
|
80
|
+
for (int attempt = 0; attempt < kPairAttempts; ++attempt) {
|
|
81
|
+
const socket_t tcp = create_tcp_server(config_.listen_port, 16, "", AddressFamily::IPv4);
|
|
82
|
+
if (!is_valid_socket(tcp)) break;
|
|
83
|
+
const std::uint16_t port = std::uint16_t(get_bound_port(tcp));
|
|
84
|
+
// Keep this TCP socket when the pair came up, when a fixed port leaves us
|
|
85
|
+
// nowhere to move, or when this was the last go — running out of attempts
|
|
86
|
+
// must not cost us a listener we already hold. Only a retry that is really
|
|
87
|
+
// going to happen may throw one away.
|
|
88
|
+
if (!want_utp || utp_.open(port) || config_.listen_port != 0
|
|
89
|
+
|| attempt + 1 == kPairAttempts) {
|
|
90
|
+
listener_ = tcp;
|
|
91
|
+
actual_port_ = port;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
close_socket(tcp);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The matching UDP port was taken — most often by our own DHT, which by mainline
|
|
98
|
+
// convention serves the torrent port. An ephemeral one is still worth having:
|
|
99
|
+
// an outgoing dial is answered on the source port of its own SYN, so it does not
|
|
100
|
+
// care what that number is. Only inbound uTP needs the advertised port, and it is
|
|
101
|
+
// the half we give up here. Nothing is lost by trying — with outgoing uTP off
|
|
102
|
+
// there is nothing left for a mux to do, so we do not open one.
|
|
103
|
+
if (want_utp && !utp_.is_open() && config_.enable_outgoing_utp) {
|
|
104
|
+
if (!utp_.open(0)) {
|
|
105
|
+
LOG_WARN("bt.client", "no UDP port available — running without uTP");
|
|
106
|
+
} else if (actual_port_ != 0) {
|
|
107
|
+
LOG_WARN("bt.client", "UDP port " << actual_port_ << " is held by another socket — "
|
|
108
|
+
"uTP on port " << utp_.port() << " instead: dialling out works, "
|
|
109
|
+
"inbound uTP does not");
|
|
110
|
+
} else {
|
|
111
|
+
// No listener at all, so there is no advertised number to have missed.
|
|
112
|
+
LOG_WARN("bt.client", "uTP on port " << utp_.port() << ", outgoing only");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Inbound uTP only means anything on the port peers are told about. On any other
|
|
117
|
+
// number nothing would ever arrive, so say so rather than pretend to listen.
|
|
118
|
+
const bool utp_inbound = utp_.is_open() && utp_.port() == actual_port_
|
|
119
|
+
&& config_.enable_incoming_utp;
|
|
120
|
+
if (utp_.is_open()) {
|
|
121
|
+
utp_.set_accept_incoming(utp_inbound);
|
|
122
|
+
utp_.set_accept_handler([this](utp::Stream& s) { on_utp_accept(s); });
|
|
123
|
+
}
|
|
124
|
+
|
|
66
125
|
if (!is_valid_socket(listener_)) {
|
|
126
|
+
// Outgoing uTP may well be up, so this is not the end of the session — but
|
|
127
|
+
// nobody can reach us, which is worth an error either way.
|
|
67
128
|
LOG_ERROR("bt.client", "failed to bind listen port " << config_.listen_port
|
|
68
129
|
<< " — inbound peers disabled");
|
|
69
130
|
return;
|
|
70
131
|
}
|
|
71
132
|
set_socket_nonblocking(listener_);
|
|
72
|
-
actual_port_ = std::uint16_t(get_bound_port(listener_));
|
|
73
133
|
reactor_.add(listener_, PollIn, [this](std::uint32_t) { on_accept(); });
|
|
74
|
-
|
|
134
|
+
|
|
135
|
+
LOG_INFO("bt.client", "listening on port " << actual_port_
|
|
136
|
+
<< (utp_inbound ? " (TCP + uTP)"
|
|
137
|
+
: utp_.is_open() ? " (TCP, uTP outgoing only)"
|
|
138
|
+
: " (TCP)"));
|
|
75
139
|
}
|
|
76
140
|
|
|
77
141
|
void Client::on_accept() {
|
|
@@ -102,24 +166,122 @@ void Client::on_accept() {
|
|
|
102
166
|
}
|
|
103
167
|
}
|
|
104
168
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
169
|
+
adopt_inbound(std::make_unique<TcpPeerLink>(reactor_, s), std::move(ip), port);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void Client::adopt_inbound(std::unique_ptr<PeerLink> link, std::string ip, std::uint16_t port) {
|
|
174
|
+
auto resolver = [this](const InfoHash& ih, PeerConnection::Binding& out) -> bool {
|
|
175
|
+
auto it = torrents_.find(ih);
|
|
176
|
+
if (it == torrents_.end() || !it->second) return false;
|
|
177
|
+
out.observer = it->second.get();
|
|
178
|
+
out.num_pieces = it->second->num_pieces();
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
181
|
+
// An inbound peer may open either protocol; the connection sniffs which and
|
|
182
|
+
// enforces in_enc_policy. The stream-key resolver is what lets an obfuscated
|
|
183
|
+
// peer name its torrent without ever putting the info-hash on the wire.
|
|
184
|
+
mse::Handshake::SkeyResolver skey =
|
|
185
|
+
[this](const std::uint8_t* obfuscated, const std::uint8_t* req3, InfoHash& out) {
|
|
186
|
+
return resolve_mse_skey(obfuscated, req3, out);
|
|
111
187
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
188
|
+
LOG_DEBUG("bt.client", "inbound connection from " << ip << ':' << port);
|
|
189
|
+
auto pc = std::make_unique<PeerConnection>(reactor_, std::move(link), peer_id_,
|
|
190
|
+
std::move(resolver), std::move(ip), port,
|
|
191
|
+
config_.in_enc_policy, std::move(skey));
|
|
192
|
+
PeerConnection* raw = pc.get();
|
|
193
|
+
connections_.push_back(std::move(pc));
|
|
194
|
+
raw->start();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void Client::on_utp_accept(utp::Stream& stream) {
|
|
198
|
+
// The stream cap is the manager's; this is the session-wide connection cap. A
|
|
199
|
+
// stream we refuse here is simply left without an observer, which is exactly
|
|
200
|
+
// what tells the manager to reap it on its next pass.
|
|
201
|
+
if (connections_.size() >= kMaxConnections) {
|
|
202
|
+
LOG_DEBUG("bt.client", "connection cap " << kMaxConnections
|
|
203
|
+
<< " reached, dropping inbound uTP");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const Address& from = stream.remote();
|
|
207
|
+
adopt_inbound(std::make_unique<UtpPeerLink>(utp_, stream), from.ip.to_string(), from.port);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
bool Client::dial_encrypted(bool prefer_encrypted) const noexcept {
|
|
211
|
+
switch (config_.out_enc_policy) {
|
|
212
|
+
case EncPolicy::Forced: return true;
|
|
213
|
+
case EncPolicy::Disabled: return false;
|
|
214
|
+
case EncPolicy::Enabled: break;
|
|
118
215
|
}
|
|
216
|
+
return prefer_encrypted;
|
|
119
217
|
}
|
|
120
218
|
|
|
121
|
-
|
|
122
|
-
|
|
219
|
+
bool Client::resolve_mse_skey(const std::uint8_t* obfuscated, const std::uint8_t* req3_hash,
|
|
220
|
+
InfoHash& out) const {
|
|
221
|
+
// Linear over the session's torrents: the stream key is deliberately not
|
|
222
|
+
// reversible, so guess-and-check is the only way, and a node holds few enough
|
|
223
|
+
// torrents for one SHA-1 each to be nothing next to the DH that just ran.
|
|
224
|
+
for (const auto& [ih, t] : torrents_) {
|
|
225
|
+
if (mse::skey_matches(obfuscated, req3_hash, ih)) { out = ih; return true; }
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
void Client::connect_peer(Torrent& torrent, const PeerList::Endpoint& peer) {
|
|
231
|
+
if (connections_.size() >= kMaxConnections) {
|
|
232
|
+
torrent.on_connect_failed(peer.ip, peer.port);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
DialOptions opts;
|
|
237
|
+
opts.obfuscate = dial_encrypted(peer.prefer_encrypted);
|
|
238
|
+
// Only an alternating policy has a second encryption form to fall back to. Under
|
|
239
|
+
// Forced or Disabled there is nothing else to try there, so a refusal is a plain
|
|
240
|
+
// failure and the peer should serve its usual backoff.
|
|
241
|
+
const bool enc_alternates = config_.out_enc_policy == EncPolicy::Enabled;
|
|
242
|
+
|
|
243
|
+
if (config_.enable_outgoing_utp && peer.prefer_utp && utp_.is_open()) {
|
|
244
|
+
// A uTP dial that never reaches a handshake is worth retrying immediately —
|
|
245
|
+
// over TCP if we have it, otherwise with the other encryption form.
|
|
246
|
+
opts.retry_other_form_on_failure = enc_alternates || config_.enable_outgoing_tcp;
|
|
247
|
+
if (connect_peer_utp(torrent, peer, opts)) return;
|
|
248
|
+
}
|
|
249
|
+
if (!config_.enable_outgoing_tcp) {
|
|
250
|
+
torrent.on_connect_failed(peer.ip, peer.port);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
opts.retry_other_form_on_failure = enc_alternates;
|
|
254
|
+
connect_peer_tcp(torrent, peer, opts);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
bool Client::connect_peer_utp(Torrent& torrent, const PeerList::Endpoint& peer, DialOptions opts) {
|
|
258
|
+
// IpAddress::parse rather than the Address(string, port) constructor: that one
|
|
259
|
+
// asserts on anything non-numeric, and while every peer source we have hands us
|
|
260
|
+
// numeric addresses, a dial is not the place to discover otherwise.
|
|
261
|
+
const auto ip = IpAddress::parse(peer.ip);
|
|
262
|
+
if (!ip) return false;
|
|
263
|
+
utp::Stream* stream = utp_.connect(Address(*ip, peer.port));
|
|
264
|
+
if (stream == nullptr) return false;
|
|
265
|
+
|
|
266
|
+
// No pending-connect bookkeeping, unlike TCP: the stream accepts the handshake
|
|
267
|
+
// bytes immediately and holds them until its SYN is answered, so there is no
|
|
268
|
+
// half-open state for the Client to track. The stream's own connect timeout and
|
|
269
|
+
// the connection's handshake deadline both still apply.
|
|
270
|
+
auto pc = std::make_unique<PeerConnection>(reactor_,
|
|
271
|
+
std::make_unique<UtpPeerLink>(utp_, *stream),
|
|
272
|
+
/*outgoing=*/true, torrent.info_hash(), peer_id_,
|
|
273
|
+
torrent.num_pieces(), &torrent,
|
|
274
|
+
peer.ip, peer.port, opts);
|
|
275
|
+
LOG_DEBUG("bt.client", "dialing " << peer.ip << ':' << peer.port << " over uTP");
|
|
276
|
+
PeerConnection* raw = pc.get();
|
|
277
|
+
connections_.push_back(std::move(pc));
|
|
278
|
+
raw->start();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
void Client::connect_peer_tcp(Torrent& torrent, const PeerList::Endpoint& endpoint, DialOptions enc) {
|
|
283
|
+
const std::string ip = endpoint.ip;
|
|
284
|
+
const std::uint16_t port = endpoint.port;
|
|
123
285
|
socket_t s = tcp_connect_start(ip, int(port));
|
|
124
286
|
if (!is_valid_socket(s)) { torrent.on_connect_failed(ip, port); return; }
|
|
125
287
|
|
|
@@ -128,10 +290,27 @@ void Client::connect_peer(Torrent& torrent, const std::string& ip, std::uint16_t
|
|
|
128
290
|
// than dereference a dangling pointer (H10). The socket is tracked so a
|
|
129
291
|
// mid-connect stop() can reclaim it.
|
|
130
292
|
const InfoHash ih = torrent.info_hash();
|
|
131
|
-
|
|
132
|
-
|
|
293
|
+
|
|
294
|
+
// The connect deadline. Whichever of the two fires first takes the socket out
|
|
295
|
+
// of pending_connects_; the other then finds it gone and does nothing, so the
|
|
296
|
+
// fd is closed exactly once and the peer is reported to the torrent once.
|
|
297
|
+
const TimerId deadline = reactor_.schedule(config_.connect_timeout, [this, s, ih, ip, port] {
|
|
298
|
+
if (pending_connects_.erase(s) == 0) return; // the connect already completed
|
|
299
|
+
reactor_.remove(s);
|
|
300
|
+
close_socket(s);
|
|
301
|
+
LOG_DEBUG("bt.client", "connect to " << ip << ':' << port << " timed out after "
|
|
302
|
+
<< config_.connect_timeout.count() << " ms");
|
|
303
|
+
auto it = torrents_.find(ih);
|
|
304
|
+
if (it != torrents_.end()) it->second->on_connect_failed(ip, port);
|
|
305
|
+
});
|
|
306
|
+
pending_connects_.emplace(s, deadline);
|
|
307
|
+
|
|
308
|
+
reactor_.add(s, PollOut, [this, ih, s, ip, port, enc](std::uint32_t) {
|
|
309
|
+
auto pending = pending_connects_.find(s);
|
|
310
|
+
if (pending == pending_connects_.end()) return; // the deadline already reclaimed it
|
|
311
|
+
reactor_.cancel(pending->second);
|
|
312
|
+
pending_connects_.erase(pending);
|
|
133
313
|
reactor_.remove(s); // done watching for connect completion
|
|
134
|
-
pending_connects_.erase(s);
|
|
135
314
|
auto it = torrents_.find(ih);
|
|
136
315
|
Torrent* t = (it != torrents_.end()) ? it->second.get() : nullptr;
|
|
137
316
|
if (tcp_connect_result(s) != 0 || !t) {
|
|
@@ -139,9 +318,10 @@ void Client::connect_peer(Torrent& torrent, const std::string& ip, std::uint16_t
|
|
|
139
318
|
if (t) t->on_connect_failed(ip, port);
|
|
140
319
|
return;
|
|
141
320
|
}
|
|
142
|
-
auto pc = std::make_unique<PeerConnection>(reactor_,
|
|
143
|
-
|
|
144
|
-
|
|
321
|
+
auto pc = std::make_unique<PeerConnection>(reactor_,
|
|
322
|
+
std::make_unique<TcpPeerLink>(reactor_, s),
|
|
323
|
+
/*outgoing=*/true, t->info_hash(), peer_id_,
|
|
324
|
+
t->num_pieces(), t, ip, port, enc);
|
|
145
325
|
PeerConnection* raw = pc.get();
|
|
146
326
|
connections_.push_back(std::move(pc));
|
|
147
327
|
raw->start();
|
|
@@ -172,10 +352,25 @@ void Client::find_peers_via_dht(const InfoHash& info_hash,
|
|
|
172
352
|
});
|
|
173
353
|
}
|
|
174
354
|
|
|
175
|
-
void Client::announce_to_dht(const InfoHash& info_hash, std::uint16_t port
|
|
355
|
+
void Client::announce_to_dht(const InfoHash& info_hash, std::uint16_t port,
|
|
356
|
+
std::function<void(const std::string&, std::uint16_t)> on_peer) {
|
|
176
357
|
// Publish ourselves to the info-hash's DHT nodes so other clients' get_peers
|
|
177
358
|
// find us (BEP 5). DhtClient is the node's shared, thread-safe instance.
|
|
178
|
-
if (dht_
|
|
359
|
+
if (!dht_ || !dht_->is_running()) return;
|
|
360
|
+
if (!on_peer) { dht_->announce_peer(info_hash, port); return; }
|
|
361
|
+
// An announce runs a get_peers traversal of its own, so it discovers the same
|
|
362
|
+
// peers a find_peers would. Deliver them through the identical guarded marshal
|
|
363
|
+
// as find_peers_via_dht (see the reasoning there) instead of discarding them.
|
|
364
|
+
dht_->announce_peer(info_hash, port,
|
|
365
|
+
[this, guard = dht_guard_, info_hash, on_peer](const std::vector<Address>& peers,
|
|
366
|
+
const InfoHash&) {
|
|
367
|
+
std::lock_guard<std::mutex> lock(guard->mutex);
|
|
368
|
+
if (!guard->alive) return; // Client stopped/destroyed — its reactor is gone
|
|
369
|
+
reactor_.post([this, info_hash, peers, on_peer] {
|
|
370
|
+
if (torrents_.find(info_hash) == torrents_.end()) return; // torrent gone
|
|
371
|
+
for (const Address& a : peers) on_peer(a.ip.to_string(), a.port);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
179
374
|
}
|
|
180
375
|
|
|
181
376
|
Torrent* Client::add_torrent(const TorrentInfo& info, const std::string& save_path) {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
#include "librats/util/rats_export.h"
|
|
19
19
|
#include "librats/bittorrent/peer_connection.h"
|
|
20
20
|
#include "librats/bittorrent/reactor.h"
|
|
21
|
+
#include "librats/bittorrent/utp_manager.h"
|
|
21
22
|
#include "librats/bittorrent/torrent.h"
|
|
22
23
|
#include "librats/bittorrent/torrent_info.h"
|
|
23
24
|
#include "librats/bittorrent/types.h"
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
#include <mutex>
|
|
36
37
|
#include <string>
|
|
37
38
|
#include <type_traits>
|
|
39
|
+
#include <unordered_map>
|
|
38
40
|
#include <unordered_set>
|
|
39
41
|
#include <vector>
|
|
40
42
|
|
|
@@ -65,6 +67,39 @@ public:
|
|
|
65
67
|
std::uint16_t listen_port = 6881; ///< 0 = ephemeral
|
|
66
68
|
std::string download_path; ///< default save directory
|
|
67
69
|
std::string peer_id_prefix = "-LR0001-";
|
|
70
|
+
/// How long an outbound TCP connect may stay pending before we give up on
|
|
71
|
+
/// the address. Without a deadline of our own, the only thing that ever
|
|
72
|
+
/// abandons an unreachable peer is the OS SYN retry — ~21 s on Windows,
|
|
73
|
+
/// longer still on Linux — and for all that time the peer sits `connecting`
|
|
74
|
+
/// in the PeerList: not retried, not penalised, holding a socket. Most DHT
|
|
75
|
+
/// peers are behind a NAT with no forwarded TCP port, so a magnet has to
|
|
76
|
+
/// work through a lot of dead addresses before it reaches one that answers;
|
|
77
|
+
/// this bound is what decides how many it gets through. Matches
|
|
78
|
+
/// libtorrent's peer_connect_timeout default.
|
|
79
|
+
std::chrono::milliseconds connect_timeout{std::chrono::seconds(15)};
|
|
80
|
+
|
|
81
|
+
/// How we dial. Enabled (the default, as in libtorrent) alternates per
|
|
82
|
+
/// attempt starting with MSE, so a peer that refuses obfuscation is still
|
|
83
|
+
/// reached on its next turn and one that refuses plaintext — a large and
|
|
84
|
+
/// growing part of the swarm — is reached at all.
|
|
85
|
+
EncPolicy out_enc_policy = EncPolicy::Enabled;
|
|
86
|
+
/// What we accept. Enabled takes both; Forced turns away plaintext peers;
|
|
87
|
+
/// Disabled turns away obfuscated ones.
|
|
88
|
+
EncPolicy in_enc_policy = EncPolicy::Enabled;
|
|
89
|
+
|
|
90
|
+
/// Dial peers over uTP (BEP 29) when they look like they support it. On by
|
|
91
|
+
/// default, as in every modern client: it is what keeps a saturated swarm
|
|
92
|
+
/// from making the rest of the user's connection unusable, and most of the
|
|
93
|
+
/// swarm now answers UDP more readily than TCP. A peer that does not answer
|
|
94
|
+
/// costs exactly one round trip before we fall back (see
|
|
95
|
+
/// PeerList::note_utp_dial_failed).
|
|
96
|
+
bool enable_outgoing_utp = true;
|
|
97
|
+
/// Answer inbound uTP connections. Off means we still dial out over uTP but
|
|
98
|
+
/// never accept — the equivalent of a firewalled UDP port.
|
|
99
|
+
bool enable_incoming_utp = true;
|
|
100
|
+
/// Dial peers over TCP. Turning it off makes uTP the only outgoing
|
|
101
|
+
/// transport, which is only sensible on a link where TCP is the problem.
|
|
102
|
+
bool enable_outgoing_tcp = true;
|
|
68
103
|
};
|
|
69
104
|
|
|
70
105
|
Client();
|
|
@@ -84,6 +119,10 @@ public:
|
|
|
84
119
|
|
|
85
120
|
bool is_running() const noexcept { return opened_; }
|
|
86
121
|
std::uint16_t listen_port() const noexcept { return actual_port_; }
|
|
122
|
+
/// The port the uTP mux actually got, or 0 if there is none. Normally equal to
|
|
123
|
+
/// listen_port(); it differs when that number's UDP half was already taken, in
|
|
124
|
+
/// which case uTP can dial out but nothing can dial in over it.
|
|
125
|
+
std::uint16_t utp_port() const noexcept { return utp_.port(); }
|
|
87
126
|
Reactor& reactor() noexcept { return reactor_; }
|
|
88
127
|
|
|
89
128
|
Torrent* add_torrent(const TorrentInfo& info, const std::string& save_path = "");
|
|
@@ -130,6 +169,10 @@ public:
|
|
|
130
169
|
|
|
131
170
|
// ---- aggregate stats (for status lines / UI) ----
|
|
132
171
|
std::size_t num_torrents() const noexcept { return torrents_.size(); }
|
|
172
|
+
/// Outbound connects still waiting on the TCP handshake. Reactor-thread only;
|
|
173
|
+
/// exposed so a caller (and the tests for the connect deadline) can see that a
|
|
174
|
+
/// dead address is actually being reclaimed rather than held forever.
|
|
175
|
+
std::size_t num_pending_connects() const noexcept { return pending_connects_.size(); }
|
|
133
176
|
std::size_t total_peers() const;
|
|
134
177
|
/// Swarm-wide transfer rates in bytes/sec, sampled once per second by the
|
|
135
178
|
/// housekeeping timer. Atomic so they can be read from another thread.
|
|
@@ -142,11 +185,12 @@ public:
|
|
|
142
185
|
DhtClient* get_dht_client() const noexcept { return dht_; }
|
|
143
186
|
|
|
144
187
|
// ---- TorrentHost ----
|
|
145
|
-
void connect_peer(Torrent& torrent, const
|
|
188
|
+
void connect_peer(Torrent& torrent, const PeerList::Endpoint& peer) override;
|
|
146
189
|
const PeerId& peer_id() const override { return peer_id_; }
|
|
147
190
|
void find_peers_via_dht(const InfoHash& info_hash,
|
|
148
191
|
std::function<void(const std::string& ip, std::uint16_t port)> on_peer) override;
|
|
149
|
-
void announce_to_dht(const InfoHash& info_hash, std::uint16_t port
|
|
192
|
+
void announce_to_dht(const InfoHash& info_hash, std::uint16_t port,
|
|
193
|
+
std::function<void(const std::string& ip, std::uint16_t port)> on_peer = {}) override;
|
|
150
194
|
|
|
151
195
|
/// Largest number of peer connections (in + out) the session will hold at once.
|
|
152
196
|
/// Beyond this, inbound sockets are accepted and immediately closed so a flood
|
|
@@ -156,6 +200,23 @@ public:
|
|
|
156
200
|
private:
|
|
157
201
|
void open_listener();
|
|
158
202
|
void on_accept();
|
|
203
|
+
/// Build the resolver + stream-key pair every inbound connection needs, and
|
|
204
|
+
/// wrap @p link in a PeerConnection. Shared by the TCP and uTP accept paths,
|
|
205
|
+
/// which differ in nothing but how the bytes arrive.
|
|
206
|
+
void adopt_inbound(std::unique_ptr<PeerLink> link, std::string ip, std::uint16_t port);
|
|
207
|
+
/// An inbound uTP stream completed its handshake; give it a PeerConnection.
|
|
208
|
+
void on_utp_accept(utp::Stream& stream);
|
|
209
|
+
/// Dial @p peer over uTP. Returns false if uTP is unavailable, so the caller
|
|
210
|
+
/// falls back to TCP.
|
|
211
|
+
bool connect_peer_utp(Torrent& torrent, const PeerList::Endpoint& peer, DialOptions opts);
|
|
212
|
+
void connect_peer_tcp(Torrent& torrent, const PeerList::Endpoint& peer, DialOptions opts);
|
|
213
|
+
/// Should this dial be obfuscated? Policy decides outright unless it is
|
|
214
|
+
/// Enabled, in which case the peer's own alternation does.
|
|
215
|
+
bool dial_encrypted(bool prefer_encrypted) const noexcept;
|
|
216
|
+
/// MSE stream-key resolver handed to every inbound connection: try each
|
|
217
|
+
/// torrent we hold against the obfuscated hash the peer sent.
|
|
218
|
+
bool resolve_mse_skey(const std::uint8_t* obfuscated, const std::uint8_t* req3_hash,
|
|
219
|
+
InfoHash& out) const;
|
|
159
220
|
void schedule_reap();
|
|
160
221
|
void reap_closed();
|
|
161
222
|
void sample_rates(); ///< recompute down_rate_/up_rate_ from per-torrent byte counters
|
|
@@ -187,6 +248,10 @@ private:
|
|
|
187
248
|
|
|
188
249
|
Reactor reactor_;
|
|
189
250
|
Config config_;
|
|
251
|
+
/// The one shared UDP socket every uTP peer rides, bound to the same port as
|
|
252
|
+
/// the TCP listener (see open_listener). Empty of streams — and cheap — when
|
|
253
|
+
/// uTP is switched off on both sides.
|
|
254
|
+
utp::Manager utp_;
|
|
190
255
|
PeerId peer_id_;
|
|
191
256
|
socket_t listener_ = RATS_INVALID_SOCKET;
|
|
192
257
|
std::uint16_t actual_port_ = 0;
|
|
@@ -211,10 +276,12 @@ private:
|
|
|
211
276
|
|
|
212
277
|
std::map<InfoHash, std::unique_ptr<Torrent>> torrents_;
|
|
213
278
|
std::vector<std::unique_ptr<PeerConnection>> connections_;
|
|
214
|
-
/// Outbound sockets still waiting for connect() to complete
|
|
215
|
-
///
|
|
216
|
-
///
|
|
217
|
-
|
|
279
|
+
/// Outbound sockets still waiting for connect() to complete, each mapped to the
|
|
280
|
+
/// timer that abandons it at Config::connect_timeout. Tracked so a mid-connect
|
|
281
|
+
/// stop() can close them, so the completion looks the torrent up by info-hash
|
|
282
|
+
/// rather than holding a raw Torrent* that may have been removed, and so the
|
|
283
|
+
/// deadline and the completion can each tell whether the other got there first.
|
|
284
|
+
std::unordered_map<socket_t, TimerId> pending_connects_;
|
|
218
285
|
|
|
219
286
|
// Rate sampling (updated on the reactor thread once per second).
|
|
220
287
|
std::atomic<std::uint64_t> down_rate_{0};
|