librats 2.3.3 → 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.
@@ -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;
@@ -687,8 +693,12 @@ rats_error_t rats_enable_bittorrent(rats_t node, uint16_t listen_port, const cha
687
693
  if (h->started) return RATS_ERR_ALREADY_STARTED;
688
694
  if (!h->bittorrent) {
689
695
  Bittorrent::Config cfg;
690
- cfg.client.listen_port = listen_port;
691
- cfg.client.download_path = download_path ? 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;
692
702
  h->bittorrent = h->node->add_subsystem(std::make_unique<Bittorrent>(cfg));
693
703
  }
694
704
  return RATS_OK;
@@ -698,6 +708,53 @@ rats_error_t rats_enable_bittorrent(rats_t node, uint16_t listen_port, const cha
698
708
  #endif
699
709
  }
700
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
+
701
758
  rats_error_t rats_bt_add_magnet(rats_t node, const char* magnet_uri, const char* save_path) {
702
759
  #ifdef RATS_SEARCH_FEATURES
703
760
  if (!magnet_uri) return RATS_ERR_INVALID_ARG;
@@ -401,6 +401,36 @@ RATS_API rats_error_t rats_remove_reconnect(rats_t node, const char* host, uint1
401
401
  /** Enable BitTorrent. listen_port 0 picks an ephemeral port; download_path is the
402
402
  * default save directory (NULL = "."). Enable DHT first to share the node's DHT. */
403
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);
404
434
  /** Start downloading a magnet link (metadata is fetched from peers). */
405
435
  RATS_API rats_error_t rats_bt_add_magnet(rats_t node, const char* magnet_uri, const char* save_path);
406
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 (socket_t s : pending_connects_) { reactor_.remove(s); close_socket(s); }
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
- listener_ = create_tcp_server(config_.listen_port, 16, "", AddressFamily::IPv4);
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
- LOG_INFO("bt.client", "listening on port " << actual_port_);
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
- auto resolver = [this](const InfoHash& ih, PeerConnection::Binding& out) -> bool {
106
- auto it = torrents_.find(ih);
107
- if (it == torrents_.end() || !it->second) return false;
108
- out.observer = it->second.get();
109
- out.num_pieces = it->second->num_pieces();
110
- return true;
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
- auto pc = std::make_unique<PeerConnection>(reactor_, s, peer_id_, std::move(resolver),
113
- std::move(ip), port);
114
- LOG_DEBUG("bt.client", "inbound connection from " << ip << ':' << port);
115
- PeerConnection* raw = pc.get();
116
- connections_.push_back(std::move(pc));
117
- raw->start();
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
- void Client::connect_peer(Torrent& torrent, const std::string& ip, std::uint16_t port) {
122
- if (connections_.size() >= kMaxConnections) { torrent.on_connect_failed(ip, port); return; }
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
- pending_connects_.insert(s);
132
- reactor_.add(s, PollOut, [this, ih, s, ip, port](std::uint32_t) {
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_, s, /*outgoing=*/true,
143
- t->info_hash(), peer_id_, t->num_pieces(), t,
144
- ip, port);
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_ && dht_->is_running()) dht_->announce_peer(info_hash, port);
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 std::string& ip, std::uint16_t port) override;
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) override;
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. Tracked so a
215
- /// mid-connect stop() can close them, and so the completion looks the torrent
216
- /// up by info-hash rather than holding a raw Torrent* that may have been removed.
217
- std::unordered_set<socket_t> pending_connects_;
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};