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.
@@ -5,18 +5,29 @@
5
5
  * @brief One peer link: the BitTorrent wire handshake, message codec and the
6
6
  * choke/interest state machine.
7
7
  *
8
- * A PeerConnection owns a non-blocking TCP socket registered with a Reactor and
9
- * lives entirely on that reactor's thread. It turns the byte stream into
10
- * protocol events delivered to an Observer, and offers send_* methods to emit
11
- * messages. It deliberately knows nothing about pieces-to-request strategy or
12
- * disk — the owning Torrent (a later phase) drives those through this surface.
8
+ * A PeerConnection owns a non-blocking byte stream a PeerLink, which is either
9
+ * a TCP socket or a uTP stream (BEP 29) and lives entirely on the reactor's
10
+ * thread. It turns that stream into protocol events delivered to an Observer, and
11
+ * offers send_* methods to emit messages. It deliberately knows nothing about
12
+ * pieces-to-request strategy or disk — the owning Torrent drives those through
13
+ * this surface — and nothing about which transport carries it: everything below
14
+ * read/write is the link's business.
13
15
  *
14
16
  * Wire format: a 68-byte handshake, then length-prefixed messages
15
17
  * `[u32 length][u8 id][payload]` (length 0 = keep-alive). All integers are
16
18
  * big-endian.
19
+ *
20
+ * That stream may be wrapped in MSE/PE obfuscation (see mse.h). When it is, an
21
+ * MSE handshake runs first and the bytes above are then RC4'd in both directions
22
+ * — but only between here and the socket: everything from parse() and the send_*
23
+ * methods inward sees the same plaintext protocol either way. An inbound
24
+ * connection does not announce which it is, so the first 20 bytes are sniffed —
25
+ * a literal "\x13BitTorrent protocol" is plaintext, anything else is a DH key.
17
26
  */
18
27
 
19
28
  #include "librats/bittorrent/bitfield.h"
29
+ #include "librats/bittorrent/mse.h"
30
+ #include "librats/bittorrent/peer_link.h"
20
31
  #include "librats/bittorrent/reactor.h"
21
32
  #include "librats/bittorrent/types.h"
22
33
  #include "librats/core/bytes.h"
@@ -26,6 +37,7 @@
26
37
  #include <chrono>
27
38
  #include <cstdint>
28
39
  #include <functional>
40
+ #include <memory>
29
41
  #include <string>
30
42
  #include <vector>
31
43
 
@@ -45,7 +57,20 @@ enum class MessageId : std::uint8_t {
45
57
  Extended = 20,
46
58
  };
47
59
 
48
- class PeerConnection {
60
+ /// How an outgoing connection opens, and what a failure to open it means for the
61
+ /// peer's next attempt. Decided by the caller from the session policy plus what
62
+ /// worked for this peer last time; the connection just carries it.
63
+ struct DialOptions {
64
+ /// Run an MSE handshake instead of writing a plaintext one.
65
+ bool obfuscate = false;
66
+ /// There is another way to reach this peer that we have not tried yet — the
67
+ /// other encryption form (EncPolicy::Enabled), or TCP after a uTP dial. A peer
68
+ /// that refuses this attempt has very likely refused the *form*, not us, so the
69
+ /// alternative is worth trying at once rather than after the reconnect backoff.
70
+ bool retry_other_form_on_failure = false;
71
+ };
72
+
73
+ class PeerConnection : private PeerLink::Observer {
49
74
  public:
50
75
  /// Protocol events. All fire on the reactor thread; ByteView arguments are
51
76
  /// only valid for the duration of the call (copy if you need to keep them).
@@ -74,14 +99,23 @@ public:
74
99
  using Resolver = std::function<bool(const InfoHash& their_info_hash, Binding& out)>;
75
100
 
76
101
  /// Outgoing connection: we know the torrent up front.
102
+ /// @param link the byte stream, TCP or uTP; owned from here on.
77
103
  /// @param num_pieces sizes the peer's bitfield; 0 if metadata isn't known yet.
78
- PeerConnection(Reactor& reactor, socket_t sock, bool outgoing,
104
+ /// @param opts whether to open with an MSE handshake, and whether a
105
+ /// failure to open should be retried another way.
106
+ PeerConnection(Reactor& reactor, std::unique_ptr<PeerLink> link, bool outgoing,
79
107
  const InfoHash& info_hash, const PeerId& our_peer_id,
80
108
  std::uint32_t num_pieces, Observer* observer,
81
- std::string remote_ip = "", std::uint16_t remote_port = 0);
109
+ std::string remote_ip = "", std::uint16_t remote_port = 0,
110
+ DialOptions opts = DialOptions{});
82
111
  /// Incoming connection: the torrent is resolved from the peer's handshake.
83
- PeerConnection(Reactor& reactor, socket_t sock, const PeerId& our_peer_id,
84
- Resolver resolver, std::string remote_ip = "", std::uint16_t remote_port = 0);
112
+ /// @param enc_policy what we accept — plaintext, MSE, or either.
113
+ /// @param skey resolves an obfuscated MSE stream key to a torrent;
114
+ /// required whenever @p enc_policy permits MSE.
115
+ PeerConnection(Reactor& reactor, std::unique_ptr<PeerLink> link, const PeerId& our_peer_id,
116
+ Resolver resolver, std::string remote_ip = "", std::uint16_t remote_port = 0,
117
+ EncPolicy enc_policy = EncPolicy::Disabled,
118
+ mse::Handshake::SkeyResolver skey = {});
85
119
  ~PeerConnection();
86
120
 
87
121
  PeerConnection(const PeerConnection&) = delete;
@@ -96,6 +130,17 @@ public:
96
130
  bool closed() const noexcept { return closed_; }
97
131
  bool handshake_done() const noexcept { return handshake_sent_ && handshake_received_; }
98
132
  bool outgoing() const noexcept { return outgoing_; }
133
+ /// Which wire carries this connection. Only the reconnect policy cares — the
134
+ /// protocol above is identical either way.
135
+ PeerTransport transport() const noexcept { return link_->transport(); }
136
+ /// True once an MSE handshake has completed. Note this says the *connection*
137
+ /// was obfuscated, not that the payload is still encrypted — crypto_select may
138
+ /// have settled on plaintext after the obfuscated header.
139
+ bool encrypted() const noexcept { return encrypted_; }
140
+ /// Should a failure to reach the handshake on this connection skip the peer's
141
+ /// reconnect backoff, so the other encryption form can be tried at once?
142
+ /// Meaningless once handshake_done() — by then the form is known to work.
143
+ bool fast_reconnect() const noexcept { return fast_reconnect_; }
99
144
  bool am_choking() const noexcept { return am_choking_; }
100
145
  bool am_interested() const noexcept { return am_interested_; }
101
146
  bool peer_choking() const noexcept { return peer_choking_; }
@@ -124,20 +169,47 @@ public:
124
169
  void send_extended(std::uint8_t ext_id, ByteView payload);
125
170
 
126
171
  private:
127
- void on_io(std::uint32_t events);
172
+ // ---- PeerLink::Observer ----
173
+ void on_link_readable() override;
174
+ void on_link_writable() override;
175
+ void on_link_error(const std::string& reason) override;
176
+
128
177
  void do_read();
129
178
  std::size_t read_size() const; ///< bytes to offer the next recv() (see rx_need_)
130
179
  void parse();
131
180
  bool parse_handshake();
132
181
  void send_handshake();
182
+ /// The 68 bytes of the BitTorrent handshake, built but not queued — MSE needs
183
+ /// them as its initial payload rather than as something to write directly.
184
+ Bytes build_handshake();
133
185
  void dispatch(MessageId id, const std::uint8_t* payload, std::uint32_t len);
134
186
 
187
+ // ---- MSE ----
188
+ /// Decide from the first 20 bytes of an inbound stream whether the peer is
189
+ /// speaking plaintext or MSE, and start the obfuscated handshake if it is.
190
+ /// Returns false if it closed the connection (a policy refusal) or is still
191
+ /// waiting for bytes.
192
+ bool detect_inbound_encryption();
193
+ /// Write whatever the handshake produced, then act on @p status.
194
+ void pump_mse(mse::Handshake::Status status);
195
+ /// Adopt the negotiated ciphers and hand the handshake's leftovers to parse().
196
+ void finish_mse();
197
+
135
198
  void send_message(MessageId id, const std::uint8_t* payload, std::uint32_t len);
136
- /// Append to the send queue. No syscall: the caller flushes once the whole
137
- /// message is queued, so a message never costs more than one send().
138
- void queue(ByteView bytes) { if (!closed_) tx_.append(bytes); }
139
- /// Queue a buffer the caller already owns — moved in, never copied.
140
- void queue(Bytes bytes) { if (!closed_) tx_.append(std::move(bytes)); }
199
+ /// Append to the send queue, encrypting first if the payload stream is RC4'd.
200
+ /// No syscall: the caller flushes once the whole message is queued, so a
201
+ /// message never costs more than one send().
202
+ ///
203
+ /// This is the single funnel every protocol message goes through, and that is
204
+ /// what makes the cipher correct: RC4 is one keystream, so bytes have to be
205
+ /// encrypted in exactly the order they enter the queue, exactly once each.
206
+ void queue(ByteView bytes);
207
+ /// Queue a buffer the caller already owns — moved in, and encrypted in place if
208
+ /// need be, so a block read from disk is never copied.
209
+ void queue(Bytes bytes);
210
+ /// Queue bytes that must bypass the payload cipher: the MSE handshake, which
211
+ /// carries its own encryption and establishes the cipher everything else uses.
212
+ void queue_raw(Bytes bytes) { if (!closed_) tx_.append(std::move(bytes)); }
141
213
  /// Push the queue to the socket with one gather-send, (dis)arm write interest,
142
214
  /// and enforce the send high-water mark. May close the connection.
143
215
  void flush();
@@ -147,7 +219,7 @@ private:
147
219
  void tick();
148
220
 
149
221
  Reactor& reactor_;
150
- socket_t sock_;
222
+ std::unique_ptr<PeerLink> link_;
151
223
  bool outgoing_;
152
224
  InfoHash info_hash_;
153
225
  PeerId our_peer_id_;
@@ -159,6 +231,26 @@ private:
159
231
  std::string remote_ip_; ///< peer's address (source for incoming, dialed for outgoing)
160
232
  std::uint16_t remote_port_ = 0;
161
233
 
234
+ // ---- MSE / PE ----
235
+ EncPolicy enc_policy_ = EncPolicy::Disabled; ///< inbound only
236
+ mse::Handshake::SkeyResolver skey_; ///< inbound only
237
+ /// Runs the obfuscated handshake and owns the byte stream while it does, so
238
+ /// nothing reaches rx_ until it has finished and been destroyed.
239
+ std::unique_ptr<mse::Handshake> mse_;
240
+ mse::Rc4Cipher rc4_send_;
241
+ mse::Rc4Cipher rc4_recv_;
242
+ bool rc4_active_ = false; ///< payload stream is RC4'd
243
+ bool encrypted_ = false; ///< an MSE handshake completed
244
+ bool want_mse_ = false; ///< outbound: dial obfuscated
245
+ /// Outbound: this dial was one of an alternating pair, so failing to reach the
246
+ /// handshake earns the peer an immediate retry with the other form.
247
+ bool fast_reconnect_ = false;
248
+ bool detecting_ = false; ///< inbound: still sniffing
249
+ InfoHash mse_skey_{}; ///< torrent the stream key named
250
+ /// Scratch for encrypting copy-appends, kept around so a steady stream of small
251
+ /// messages does not allocate a buffer each.
252
+ Bytes enc_scratch_;
253
+
162
254
  ReceiveBuffer rx_;
163
255
  /// Wire size of the message rx_ is mid-way through (4-byte prefix included), once
164
256
  /// that prefix has been read; 0 when no message is in flight. Lets the next recv()
@@ -0,0 +1,134 @@
1
+ #include "librats/bittorrent/peer_link.h"
2
+
3
+ #include "librats/bittorrent/utp_manager.h"
4
+
5
+ #include <cerrno>
6
+
7
+ namespace librats::bittorrent {
8
+
9
+ namespace {
10
+
11
+ #ifdef _WIN32
12
+ inline bool would_block() { return WSAGetLastError() == WSAEWOULDBLOCK; }
13
+ #else
14
+ inline bool would_block() { return errno == EAGAIN || errno == EWOULDBLOCK; }
15
+ #endif
16
+
17
+ } // namespace
18
+
19
+ // ---- TcpPeerLink -------------------------------------------------------------
20
+
21
+ TcpPeerLink::TcpPeerLink(Reactor& reactor, socket_t sock) : reactor_(reactor), sock_(sock) {}
22
+
23
+ TcpPeerLink::~TcpPeerLink() {
24
+ close();
25
+ }
26
+
27
+ void TcpPeerLink::start(Observer* obs) {
28
+ obs_ = obs;
29
+ set_socket_nonblocking(sock_);
30
+ reactor_.add(sock_, PollIn, [this](std::uint32_t ev) { on_io(ev); });
31
+ }
32
+
33
+ void TcpPeerLink::on_io(std::uint32_t events) {
34
+ // The observer may close us from inside any of these, which clears obs_ — so
35
+ // each step re-checks rather than assuming the link is still alive.
36
+ if ((events & PollOut) && obs_ != nullptr) obs_->on_link_writable();
37
+ if ((events & PollIn) && obs_ != nullptr) obs_->on_link_readable();
38
+ if ((events & (PollErr | PollHup)) && obs_ != nullptr) obs_->on_link_error("socket error");
39
+ }
40
+
41
+ PeerLink::IoResult TcpPeerLink::read(ByteSpan into) {
42
+ if (!is_valid_socket(sock_)) return {0, Status::Error};
43
+ const int n = ::recv(sock_, reinterpret_cast<char*>(into.data()),
44
+ static_cast<int>(into.size()), 0);
45
+ if (n == 0) return {0, Status::Closed};
46
+ if (n < 0) return {0, would_block() ? Status::WouldBlock : Status::Error};
47
+ return {std::size_t(n), Status::Ok};
48
+ }
49
+
50
+ PeerLink::IoResult TcpPeerLink::write(const ByteView* slices, std::size_t count) {
51
+ if (!is_valid_socket(sock_)) return {0, Status::Error};
52
+ const std::ptrdiff_t n = send_vectored(sock_, slices, count);
53
+ if (n > 0) return {std::size_t(n), Status::Ok};
54
+ // Zero accepted is not an error: the socket buffer is full right now. Treated
55
+ // the same as would-block, i.e. retry when the poller says we may.
56
+ if (n == 0) return {0, Status::WouldBlock};
57
+ return {0, would_block() ? Status::WouldBlock : Status::Error};
58
+ }
59
+
60
+ void TcpPeerLink::want_write(bool on) {
61
+ if (on == want_write_ || !is_valid_socket(sock_)) return;
62
+ want_write_ = on;
63
+ reactor_.modify(sock_, PollIn | (on ? PollOut : PollNone));
64
+ }
65
+
66
+ void TcpPeerLink::close() {
67
+ obs_ = nullptr;
68
+ if (!is_valid_socket(sock_)) return;
69
+ reactor_.remove(sock_);
70
+ close_socket(sock_);
71
+ sock_ = RATS_INVALID_SOCKET;
72
+ }
73
+
74
+ // ---- UtpPeerLink -------------------------------------------------------------
75
+
76
+ UtpPeerLink::UtpPeerLink(utp::Manager& manager, utp::Stream& stream)
77
+ : manager_(manager), stream_(&stream) {}
78
+
79
+ UtpPeerLink::~UtpPeerLink() {
80
+ close();
81
+ }
82
+
83
+ void UtpPeerLink::start(PeerLink::Observer* obs) {
84
+ obs_ = obs;
85
+ if (stream_ != nullptr) stream_->set_observer(this);
86
+ }
87
+
88
+ PeerLink::IoResult UtpPeerLink::read(ByteSpan into) {
89
+ if (stream_ == nullptr) return {0, Status::Error};
90
+ const auto r = stream_->read(into);
91
+ switch (r.status) {
92
+ case utp::Stream::Status::Ok: return {r.bytes, Status::Ok};
93
+ case utp::Stream::Status::WouldBlock: return {0, Status::WouldBlock};
94
+ case utp::Stream::Status::Eof: return {0, Status::Closed};
95
+ case utp::Stream::Status::Error: break;
96
+ }
97
+ return {0, Status::Error};
98
+ }
99
+
100
+ PeerLink::IoResult UtpPeerLink::write(const ByteView* slices, std::size_t count) {
101
+ if (stream_ == nullptr) return {0, Status::Error};
102
+ const auto r = stream_->write(slices, count, utp::Stream::Clock::now());
103
+ switch (r.status) {
104
+ case utp::Stream::Status::Ok: return {r.bytes, Status::Ok};
105
+ case utp::Stream::Status::WouldBlock: return {0, Status::WouldBlock};
106
+ case utp::Stream::Status::Eof:
107
+ case utp::Stream::Status::Error: break;
108
+ }
109
+ return {0, Status::Error};
110
+ }
111
+
112
+ void UtpPeerLink::close() {
113
+ obs_ = nullptr;
114
+ if (stream_ == nullptr) return;
115
+ // Hand it back rather than delete it: the stream still owes the peer a FIN, and
116
+ // the manager keeps it alive just long enough to deliver one.
117
+ utp::Stream* s = stream_;
118
+ stream_ = nullptr;
119
+ manager_.release(*s);
120
+ }
121
+
122
+ void UtpPeerLink::on_utp_readable() {
123
+ if (obs_ != nullptr) obs_->on_link_readable();
124
+ }
125
+
126
+ void UtpPeerLink::on_utp_writable() {
127
+ if (obs_ != nullptr && want_write_) obs_->on_link_writable();
128
+ }
129
+
130
+ void UtpPeerLink::on_utp_error(const std::string& why) {
131
+ if (obs_ != nullptr) obs_->on_link_error(why);
132
+ }
133
+
134
+ } // namespace librats::bittorrent
@@ -0,0 +1,145 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file peer_link.h
5
+ * @brief The byte stream a PeerConnection runs on, independent of how it is carried.
6
+ *
7
+ * Everything above this line — the BitTorrent handshake, the message codec, MSE
8
+ * obfuscation, the choke machine, backpressure, the idle deadlines — only ever
9
+ * needs an ordered, reliable, non-blocking byte stream. A PeerLink is exactly that
10
+ * and nothing more, so the same PeerConnection code drives a kernel TCP socket and
11
+ * the library's own uTP implementation. Neither is a special case of the other: a
12
+ * peer reached over either is indistinguishable to every layer above.
13
+ *
14
+ * This mirrors `transport/link.h` on the node side of the library, and libtorrent's
15
+ * `aux::socket_type` variant on the BitTorrent side, for the same reason all three
16
+ * exist: without it, "which transport is this?" leaks into every function that
17
+ * touches the wire.
18
+ *
19
+ * The read/write calls deliberately mirror non-blocking socket semantics, because
20
+ * that is what a reactor loop is built around:
21
+ * - read() fills the caller's span and reports WouldBlock once nothing more is
22
+ * ready, or Closed when the peer has finished sending.
23
+ * - write() takes what it can and reports WouldBlock when it can take no more;
24
+ * the caller keeps the remainder queued and calls want_write(true) so
25
+ * it is told when to retry.
26
+ *
27
+ * Ownership and threading: a link belongs to exactly one PeerConnection, lives on
28
+ * the BitTorrent reactor thread, and holds no locks. Events arrive through the
29
+ * Observer the connection installs in start().
30
+ */
31
+
32
+ #include "librats/bittorrent/reactor.h"
33
+ #include "librats/bittorrent/types.h"
34
+ #include "librats/bittorrent/utp_stream.h"
35
+ #include "librats/core/bytes.h"
36
+ #include "librats/core/socket.h"
37
+
38
+ #include <cstddef>
39
+ #include <string>
40
+
41
+ namespace librats::bittorrent {
42
+
43
+ namespace utp { class Manager; }
44
+
45
+ class PeerLink {
46
+ public:
47
+ /// Outcome of a read/write attempt. `Closed` is orderly (the peer is done
48
+ /// sending); `Error` means the link is broken and must be torn down.
49
+ enum class Status : std::uint8_t { Ok, WouldBlock, Closed, Error };
50
+
51
+ struct IoResult {
52
+ std::size_t bytes = 0;
53
+ Status status = Status::WouldBlock;
54
+ };
55
+
56
+ /// How the link tells its connection that something happened. Every callback
57
+ /// runs on the reactor thread, and the connection may close the link from
58
+ /// inside one — so a link must not touch its own state after calling out.
59
+ struct Observer {
60
+ virtual ~Observer() = default;
61
+ virtual void on_link_readable() = 0;
62
+ virtual void on_link_writable() = 0;
63
+ virtual void on_link_error(const std::string& reason) = 0;
64
+ };
65
+
66
+ virtual ~PeerLink() = default;
67
+
68
+ virtual PeerTransport transport() const noexcept = 0;
69
+
70
+ /// Begin delivering events to @p obs.
71
+ virtual void start(Observer* obs) = 0;
72
+
73
+ /// Read up to `into.size()` bytes. A Closed or Error result always carries zero
74
+ /// bytes — data and end-of-stream are never reported together, so the caller
75
+ /// drains first and only then sees the close.
76
+ virtual IoResult read(ByteSpan into) = 0;
77
+
78
+ /// Hand `count` contiguous slices to the link in order, writing as many bytes
79
+ /// as it will take (a partial write is normal and expected).
80
+ virtual IoResult write(const ByteView* slices, std::size_t count) = 0;
81
+
82
+ /// Ask to be woken (via on_link_writable) when more can be written. Called only
83
+ /// when the state actually changes, so an implementation may treat it as an
84
+ /// unconditional set.
85
+ virtual void want_write(bool on) = 0;
86
+
87
+ /// Tear down. Idempotent, and safe to call from inside an Observer callback.
88
+ virtual void close() = 0;
89
+ };
90
+
91
+ /// A plain TCP socket, registered with the reactor. The kernel does the work.
92
+ class TcpPeerLink final : public PeerLink {
93
+ public:
94
+ TcpPeerLink(Reactor& reactor, socket_t sock);
95
+ ~TcpPeerLink() override;
96
+
97
+ PeerTransport transport() const noexcept override { return PeerTransport::Tcp; }
98
+ void start(PeerLink::Observer* obs) override;
99
+ IoResult read(ByteSpan into) override;
100
+ IoResult write(const ByteView* slices, std::size_t count) override;
101
+ void want_write(bool on) override;
102
+ void close() override;
103
+
104
+ private:
105
+ void on_io(std::uint32_t events);
106
+
107
+ Reactor& reactor_;
108
+ socket_t sock_;
109
+ Observer* obs_ = nullptr;
110
+ bool want_write_ = false;
111
+ };
112
+
113
+ /**
114
+ * The same guarantee obtained from a uTP stream instead of a socket.
115
+ *
116
+ * The stream is owned by the utp::Manager (it may have to outlive this object to
117
+ * finish flushing a FIN), so the link holds a pointer and hands it back on close().
118
+ * Readiness is pushed rather than polled: the manager's socket is the only thing
119
+ * registered with the reactor, and it drives the stream, which drives us.
120
+ */
121
+ class UtpPeerLink final : public PeerLink, private utp::Stream::Observer {
122
+ public:
123
+ UtpPeerLink(utp::Manager& manager, utp::Stream& stream);
124
+ ~UtpPeerLink() override;
125
+
126
+ PeerTransport transport() const noexcept override { return PeerTransport::Utp; }
127
+ void start(PeerLink::Observer* obs) override;
128
+ IoResult read(ByteSpan into) override;
129
+ IoResult write(const ByteView* slices, std::size_t count) override;
130
+ void want_write(bool on) override { want_write_ = on; }
131
+ void close() override;
132
+
133
+ private:
134
+ // ---- utp::Stream::Observer ----
135
+ void on_utp_readable() override;
136
+ void on_utp_writable() override;
137
+ void on_utp_error(const std::string& why) override;
138
+
139
+ utp::Manager& manager_;
140
+ utp::Stream* stream_;
141
+ PeerLink::Observer* obs_ = nullptr;
142
+ bool want_write_ = false;
143
+ };
144
+
145
+ } // namespace librats::bittorrent
@@ -13,10 +13,10 @@ bool PeerList::add(const std::string& ip, std::uint16_t port, PeerSource source)
13
13
  return inserted;
14
14
  }
15
15
 
16
- std::vector<PeerList::Endpoint> PeerList::connect_candidates(std::size_t max) {
16
+ std::vector<PeerList::Endpoint> PeerList::connect_candidates(std::size_t max, Clock::time_point now) {
17
17
  std::vector<Peer*> eligible_peers;
18
18
  for (auto& [k, p] : peers_)
19
- if (eligible(p)) eligible_peers.push_back(&p);
19
+ if (ready(p, now)) eligible_peers.push_back(&p);
20
20
 
21
21
  // Fewest past failures first; ties broken by richer source provenance so a
22
22
  // tracker/DHT-vouched peer outranks one only seen via PEX.
@@ -28,25 +28,88 @@ std::vector<PeerList::Endpoint> PeerList::connect_candidates(std::size_t max) {
28
28
  std::vector<Endpoint> out;
29
29
  const std::size_t take = (std::min)(max, eligible_peers.size());
30
30
  out.reserve(take);
31
+ // Only `connecting` is set here — the backoff clock is started when the attempt
32
+ // *ends*, not when it begins (libtorrent stamps last_connected in
33
+ // connection_closed for the same reason). Stamping on hand-out instead would
34
+ // make Disconnect::Release meaningless: a released peer would still be sitting
35
+ // out a backoff started by the dial we ourselves just tore down. `connecting`
36
+ // is what keeps an in-flight dial from being handed out twice, and every dial
37
+ // now ends in on_connect_failed or on_disconnected — the connect deadline in
38
+ // Client guarantees it — so nothing can leave a peer stamp-less forever.
31
39
  for (std::size_t i = 0; i < take; ++i) {
32
- eligible_peers[i]->connecting = true;
33
- out.push_back(Endpoint{eligible_peers[i]->ip, eligible_peers[i]->port});
40
+ Peer& p = *eligible_peers[i];
41
+ p.connecting = true;
42
+ // uTP unless this peer has already shown it has none. Unlike the encryption
43
+ // form this is a latch, not an alternation: one failed uTP dial settles the
44
+ // question, and re-asking it every other attempt would spend a wasted round
45
+ // trip on a peer we already know the answer for.
46
+ out.push_back(Endpoint{p.ip, p.port, p.prefer_encrypted,
47
+ p.supports_utp || p.confirmed_supports_utp});
48
+ // Flip now, not on failure: this attempt has claimed the current mode, so
49
+ // whatever happens to it the *next* one should try the other. A success
50
+ // pins the working mode back in set_connected().
51
+ p.prefer_encrypted = !p.prefer_encrypted;
34
52
  }
35
53
  return out;
36
54
  }
37
55
 
38
- void PeerList::set_connected(const std::string& ip, std::uint16_t port, bool connected) {
56
+ void PeerList::set_connected(const std::string& ip, std::uint16_t port, bool encrypted,
57
+ PeerTransport transport) {
39
58
  auto it = peers_.find(key(ip, port));
40
59
  if (it == peers_.end()) return;
41
- it->second.connected = connected;
42
- it->second.connecting = false;
43
- if (connected) it->second.fail_count = 0; // a successful connect clears the penalty
60
+ Peer& p = it->second;
61
+ p.connected = true;
62
+ p.connecting = false;
63
+ p.fail_count = 0; // reaching a working session clears the penalty
64
+ p.prefer_encrypted = encrypted;
65
+ if (transport == PeerTransport::Utp) {
66
+ // Proof rather than assumption. Clearing the optimistic flag alongside is
67
+ // libtorrent's bookkeeping: from here on the confirmation is what keeps us
68
+ // dialing uTP, so a stale guess can never contradict it.
69
+ p.confirmed_supports_utp = true;
70
+ p.supports_utp = false;
71
+ }
72
+ }
73
+
74
+ void PeerList::note_utp_dial_failed(const std::string& ip, std::uint16_t port) {
75
+ auto it = peers_.find(key(ip, port));
76
+ if (it == peers_.end()) return;
77
+ it->second.supports_utp = false;
78
+ }
79
+
80
+ void PeerList::on_disconnected(const std::string& ip, std::uint16_t port, Disconnect how,
81
+ Clock::time_point now) {
82
+ auto it = peers_.find(key(ip, port));
83
+ if (it == peers_.end()) return;
84
+ Peer& p = it->second;
85
+ p.connected = false;
86
+ p.connecting = false;
87
+ if (how == Disconnect::Release) return; // our own doing — leave the peer untouched
88
+
89
+ const bool failed = how == Disconnect::Failed || how == Disconnect::FailedRetryNow;
90
+ if (failed && p.fail_count < kMaxFails) ++p.fail_count;
91
+
92
+ // A waiver clears the clock rather than setting it, so the peer reads as never
93
+ // dialed and comes back on the very next pass — the whole point being to learn
94
+ // whether it wanted the other form of handshake, which costs one round trip and
95
+ // should not cost a minute. Still counted against fail_count above, and rationed
96
+ // by kMaxFastReconnects, so this cannot become a loop.
97
+ if (how == Disconnect::FailedRetryNow && p.fast_reconnects < kMaxFastReconnects) {
98
+ ++p.fast_reconnects;
99
+ p.last_attempt = Clock::time_point{};
100
+ return;
101
+ }
102
+
103
+ // Stamp the attempt even for a clean disconnect: the backoff is what keeps the
104
+ // dial loop from immediately re-opening a connection the peer just closed.
105
+ p.last_attempt = now;
44
106
  }
45
107
 
46
- void PeerList::on_connect_failed(const std::string& ip, std::uint16_t port) {
108
+ void PeerList::on_connect_failed(const std::string& ip, std::uint16_t port, Clock::time_point now) {
47
109
  auto it = peers_.find(key(ip, port));
48
110
  if (it == peers_.end()) return;
49
- it->second.connecting = false;
111
+ it->second.connecting = false;
112
+ it->second.last_attempt = now;
50
113
  ++it->second.fail_count;
51
114
  }
52
115
 
@@ -55,9 +118,9 @@ void PeerList::ban(const std::string& ip, std::uint16_t port) {
55
118
  if (it != peers_.end()) it->second.banned = true;
56
119
  }
57
120
 
58
- std::size_t PeerList::num_candidates() const {
121
+ std::size_t PeerList::num_candidates(Clock::time_point now) const {
59
122
  std::size_t n = 0;
60
- for (const auto& [k, p] : peers_) if (eligible(p)) ++n;
123
+ for (const auto& [k, p] : peers_) if (ready(p, now)) ++n;
61
124
  return n;
62
125
  }
63
126