librats 2.3.3 → 2.3.5
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/native-src/CMakeLists.txt +15 -0
- package/native-src/src/librats/bindings/rats.cpp +59 -2
- package/native-src/src/librats/bindings/rats.h +30 -0
- 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/package.json +1 -1
|
@@ -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
|
|
9
|
-
*
|
|
10
|
-
* protocol events delivered to an Observer, and
|
|
11
|
-
* messages. It deliberately knows nothing about
|
|
12
|
-
* disk — the owning Torrent
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
///
|
|
138
|
-
|
|
139
|
-
///
|
|
140
|
-
|
|
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
|
-
|
|
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 (
|
|
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]
|
|
33
|
-
|
|
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
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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 (
|
|
123
|
+
for (const auto& [k, p] : peers_) if (ready(p, now)) ++n;
|
|
61
124
|
return n;
|
|
62
125
|
}
|
|
63
126
|
|