librats 2.3.2 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +1 -0
- package/native-src/CMakeLists.txt +15 -0
- package/native-src/src/librats/bindings/rats.cpp +62 -4
- package/native-src/src/librats/bindings/rats.h +32 -1
- package/native-src/src/librats/bittorrent/client.cpp +221 -26
- package/native-src/src/librats/bittorrent/client.h +73 -6
- package/native-src/src/librats/bittorrent/mse.cpp +626 -0
- package/native-src/src/librats/bittorrent/mse.h +218 -0
- package/native-src/src/librats/bittorrent/peer_connection.cpp +187 -59
- package/native-src/src/librats/bittorrent/peer_connection.h +109 -17
- package/native-src/src/librats/bittorrent/peer_link.cpp +134 -0
- package/native-src/src/librats/bittorrent/peer_link.h +145 -0
- package/native-src/src/librats/bittorrent/peer_list.cpp +75 -12
- package/native-src/src/librats/bittorrent/peer_list.h +130 -18
- package/native-src/src/librats/bittorrent/torrent.cpp +54 -11
- package/native-src/src/librats/bittorrent/torrent.h +19 -3
- package/native-src/src/librats/bittorrent/types.h +27 -0
- package/native-src/src/librats/bittorrent/utp_manager.cpp +234 -0
- package/native-src/src/librats/bittorrent/utp_manager.h +131 -0
- package/native-src/src/librats/bittorrent/utp_packet.h +216 -0
- package/native-src/src/librats/bittorrent/utp_stream.cpp +903 -0
- package/native-src/src/librats/bittorrent/utp_stream.h +400 -0
- package/native-src/src/librats/core/socket.cpp +44 -8
- package/native-src/src/librats/core/socket.h +23 -1
- package/native-src/src/librats/subsystems/file_transfer.cpp +54 -20
- package/native-src/src/librats/subsystems/file_transfer.h +21 -3
- package/package.json +1 -1
- package/src/librats_node.cpp +6 -3
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
* Every discovery source — tracker, DHT, PEX, LSD, incoming — funnels addresses
|
|
9
9
|
* here; the Torrent then asks for connect_candidates() to dial. The list
|
|
10
10
|
* deduplicates, remembers which sources vouched for a peer, counts connection
|
|
11
|
-
* failures (so hopeless peers drift to the back and eventually drop out),
|
|
12
|
-
*
|
|
11
|
+
* failures (so hopeless peers drift to the back and eventually drop out), applies
|
|
12
|
+
* a reconnect backoff to peers that have just failed, and supports banning. Owned
|
|
13
|
+
* by one torrent on the reactor thread — not thread-safe.
|
|
14
|
+
*
|
|
15
|
+
* Time is passed in rather than read from the clock so the backoff is testable;
|
|
16
|
+
* the Torrent supplies Clock::now().
|
|
13
17
|
*/
|
|
14
18
|
|
|
19
|
+
#include "librats/bittorrent/types.h"
|
|
20
|
+
|
|
21
|
+
#include <chrono>
|
|
15
22
|
#include <cstddef>
|
|
16
23
|
#include <cstdint>
|
|
17
24
|
#include <string>
|
|
@@ -31,43 +38,148 @@ enum class PeerSource : std::uint8_t {
|
|
|
31
38
|
|
|
32
39
|
class PeerList {
|
|
33
40
|
public:
|
|
34
|
-
|
|
41
|
+
using Clock = std::chrono::steady_clock;
|
|
35
42
|
|
|
36
|
-
struct
|
|
43
|
+
struct Endpoint {
|
|
37
44
|
std::string ip;
|
|
38
|
-
std::uint16_t port
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
bool
|
|
43
|
-
|
|
45
|
+
std::uint16_t port;
|
|
46
|
+
/// Whether to open this attempt with an MSE handshake. Only consulted when
|
|
47
|
+
/// the session's policy is EncPolicy::Enabled, where it alternates per
|
|
48
|
+
/// attempt so a peer that refuses one form is reached with the other.
|
|
49
|
+
bool prefer_encrypted = true;
|
|
50
|
+
/// Whether to dial over uTP rather than TCP. Only consulted when the
|
|
51
|
+
/// session has outgoing uTP enabled.
|
|
52
|
+
bool prefer_utp = true;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
struct Peer {
|
|
56
|
+
std::string ip;
|
|
57
|
+
std::uint16_t port = 0;
|
|
58
|
+
std::uint8_t sources = 0;
|
|
59
|
+
bool connected = false;
|
|
60
|
+
bool connecting = false;
|
|
61
|
+
bool banned = false;
|
|
62
|
+
std::uint32_t fail_count = 0;
|
|
63
|
+
/// When our last connection to this peer *ended*. Zero until one does,
|
|
64
|
+
/// which is what lets a freshly discovered peer be dialed immediately.
|
|
65
|
+
Clock::time_point last_attempt{};
|
|
66
|
+
/// Whether the next dial should be obfuscated. Starts true — nearly every
|
|
67
|
+
/// client in the swarm speaks MSE, and a good part of it accepts nothing
|
|
68
|
+
/// else — and flips on each attempt so a peer is eventually tried both
|
|
69
|
+
/// ways. A completed handshake pins it to whatever actually worked.
|
|
70
|
+
bool prefer_encrypted = true;
|
|
71
|
+
/// How many backoff waivers this peer has already been granted; see
|
|
72
|
+
/// kMaxFastReconnects.
|
|
73
|
+
std::uint8_t fast_reconnects = 0;
|
|
74
|
+
/// Optimism: assume every peer speaks uTP until one proves otherwise, since
|
|
75
|
+
/// most of the swarm does and a peer that does not costs exactly one wasted
|
|
76
|
+
/// dial to discover (which the fast-reconnect waiver then makes free).
|
|
77
|
+
/// Cleared by the first uTP dial that fails to reach a handshake.
|
|
78
|
+
bool supports_utp = true;
|
|
79
|
+
/// A uTP connection to this peer has actually worked. Outranks the guess
|
|
80
|
+
/// above and is never withdrawn: a peer that answered uTP once will answer
|
|
81
|
+
/// it again, and a later failure is far more likely to be the peer being
|
|
82
|
+
/// gone than the transport being wrong.
|
|
83
|
+
bool confirmed_supports_utp = false;
|
|
44
84
|
};
|
|
45
85
|
|
|
46
86
|
static constexpr std::uint32_t kMaxFails = 5;
|
|
47
87
|
|
|
88
|
+
/// Base reconnect delay, scaled by the failure count: a peer is not re-dialed
|
|
89
|
+
/// until (fail_count + 1) * kMinReconnectInterval has passed since the last
|
|
90
|
+
/// attempt. Without it a peer that accepts TCP and then drops us — a client
|
|
91
|
+
/// that requires encryption does exactly this — is re-dialed on every torrent
|
|
92
|
+
/// tick, once a second, forever, while genuinely useful addresses wait behind
|
|
93
|
+
/// it. Mirrors libtorrent's min_reconnect_time (60 s) and its
|
|
94
|
+
/// `session_time - last_connected < (failcount + 1) * min_reconnect_time` gate.
|
|
95
|
+
static constexpr std::chrono::seconds kMinReconnectInterval{60};
|
|
96
|
+
|
|
97
|
+
/// How many times a peer may skip that wait to try the *other* encryption
|
|
98
|
+
/// form (see Disconnect::FailedRetryNow). Two covers both alternations; past
|
|
99
|
+
/// that a peer that keeps refusing us waits its turn like any other, which is
|
|
100
|
+
/// what stops an endless one-second ping-pong with a peer that rejects both.
|
|
101
|
+
/// Mirrors libtorrent's cap on torrent_peer::fast_reconnects.
|
|
102
|
+
static constexpr std::uint8_t kMaxFastReconnects = 2;
|
|
103
|
+
|
|
48
104
|
/// Add or merge a candidate. Returns true if it was newly created.
|
|
49
105
|
bool add(const std::string& ip, std::uint16_t port, PeerSource source);
|
|
50
106
|
|
|
51
|
-
/// Up to @p max eligible peers to dial (not connected/connecting/banned,
|
|
52
|
-
///
|
|
53
|
-
/// `connecting` so they aren't handed out again
|
|
54
|
-
|
|
107
|
+
/// Up to @p max eligible peers to dial (not connected/connecting/banned, under
|
|
108
|
+
/// the failure limit, and past their reconnect backoff at @p now), best first.
|
|
109
|
+
/// The returned peers are marked `connecting` so they aren't handed out again
|
|
110
|
+
/// until the attempt resolves.
|
|
111
|
+
std::vector<Endpoint> connect_candidates(std::size_t max, Clock::time_point now);
|
|
112
|
+
|
|
113
|
+
/// The peer completed its handshake: it is a live connection, and whatever
|
|
114
|
+
/// failures it accumulated getting here no longer count against it.
|
|
115
|
+
/// @p encrypted records whether that took MSE, so the next dial starts there
|
|
116
|
+
/// instead of paying for the alternation again; @p transport does the same for
|
|
117
|
+
/// the wire it arrived on.
|
|
118
|
+
void set_connected(const std::string& ip, std::uint16_t port, bool encrypted,
|
|
119
|
+
PeerTransport transport = PeerTransport::Tcp);
|
|
120
|
+
|
|
121
|
+
/// An outgoing uTP dial to this peer never reached a handshake. Take it as
|
|
122
|
+
/// evidence the peer has no uTP — the overwhelmingly likely cause, since a peer
|
|
123
|
+
/// whose TCP port is reachable usually has the UDP one blocked rather than the
|
|
124
|
+
/// other way round — and dial it over TCP from here on. Paired with
|
|
125
|
+
/// Disconnect::FailedRetryNow, so the TCP attempt happens immediately rather
|
|
126
|
+
/// than after the backoff: the whole point is that one wasted dial costs
|
|
127
|
+
/// nothing but a round trip.
|
|
128
|
+
void note_utp_dial_failed(const std::string& ip, std::uint16_t port);
|
|
129
|
+
|
|
130
|
+
/// Why a connection to this peer ended — the three cases earn different
|
|
131
|
+
/// treatment on the way back in.
|
|
132
|
+
enum class Disconnect {
|
|
133
|
+
/// It never became usable: refused mid-handshake, protocol error, dropped
|
|
134
|
+
/// before the handshake completed. Penalised and backed off.
|
|
135
|
+
Failed,
|
|
136
|
+
/// Failed as above, but we dialed one of two alternating encryption forms
|
|
137
|
+
/// and the other is worth trying at once — the peer very likely refused
|
|
138
|
+
/// the handshake we opened with, not us. Penalised like Failed (so a peer
|
|
139
|
+
/// that rejects both still runs out of chances) but made eligible
|
|
140
|
+
/// immediately instead of waiting out a minute to learn a one-bit answer.
|
|
141
|
+
/// Granted at most kMaxFastReconnects times per peer. libtorrent's
|
|
142
|
+
/// fast_reconnect, which rewinds last_connected for the same reason.
|
|
143
|
+
FailedRetryNow,
|
|
144
|
+
/// It carried a real session and then ended. No penalty, but still backed
|
|
145
|
+
/// off — re-opening a connection the peer just closed helps nobody.
|
|
146
|
+
Clean,
|
|
147
|
+
/// *We* dropped it for our own reasons (pausing the torrent), and nothing
|
|
148
|
+
/// about the peer changed. Neither penalised nor backed off, so a resume
|
|
149
|
+
/// dials straight back. This is libtorrent's fast_reconnect: it is the one
|
|
150
|
+
/// case where last_connected is deliberately left unstamped.
|
|
151
|
+
Release,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/// A connection to this peer ended; see Disconnect for how @p how is treated.
|
|
155
|
+
void on_disconnected(const std::string& ip, std::uint16_t port, Disconnect how,
|
|
156
|
+
Clock::time_point now);
|
|
157
|
+
|
|
158
|
+
/// The outbound connect itself never completed (refused, unreachable, timed out).
|
|
159
|
+
void on_connect_failed(const std::string& ip, std::uint16_t port, Clock::time_point now);
|
|
55
160
|
|
|
56
|
-
void set_connected(const std::string& ip, std::uint16_t port, bool connected);
|
|
57
|
-
void on_connect_failed(const std::string& ip, std::uint16_t port);
|
|
58
161
|
void ban(const std::string& ip, std::uint16_t port);
|
|
59
162
|
|
|
60
|
-
std::size_t size()
|
|
61
|
-
|
|
163
|
+
std::size_t size() const noexcept { return peers_.size(); }
|
|
164
|
+
/// Count of peers currently eligible to dial at @p now (backoff included).
|
|
165
|
+
std::size_t num_candidates(Clock::time_point now) const;
|
|
62
166
|
bool contains(const std::string& ip, std::uint16_t port) const;
|
|
63
167
|
|
|
64
168
|
private:
|
|
65
169
|
static std::string key(const std::string& ip, std::uint16_t port) {
|
|
66
170
|
return ip + ":" + std::to_string(port);
|
|
67
171
|
}
|
|
172
|
+
/// Eligible ignoring time: not already in play, not banned, chances left.
|
|
68
173
|
bool eligible(const Peer& p) const noexcept {
|
|
69
174
|
return !p.connected && !p.connecting && !p.banned && p.fail_count < kMaxFails;
|
|
70
175
|
}
|
|
176
|
+
/// Eligible *and* past its reconnect backoff. A peer never dialed
|
|
177
|
+
/// (last_attempt == {}) is always ready.
|
|
178
|
+
bool ready(const Peer& p, Clock::time_point now) const noexcept {
|
|
179
|
+
if (!eligible(p)) return false;
|
|
180
|
+
if (p.last_attempt == Clock::time_point{}) return true;
|
|
181
|
+
return now - p.last_attempt >= (p.fail_count + 1) * kMinReconnectInterval;
|
|
182
|
+
}
|
|
71
183
|
|
|
72
184
|
std::unordered_map<std::string, Peer> peers_;
|
|
73
185
|
};
|
|
@@ -77,7 +77,12 @@ void Torrent::stop() {
|
|
|
77
77
|
trackers_->stop(); // drains the in-flight Stopped announce
|
|
78
78
|
trackers_.reset();
|
|
79
79
|
}
|
|
80
|
-
for
|
|
80
|
+
// Snapshot + release, for the same two reasons as pause(): close() runs
|
|
81
|
+
// on_closed inline and erases from peers_ mid-loop, and a peer we drop on our
|
|
82
|
+
// own way out has not failed us.
|
|
83
|
+
releasing_peers_ = true;
|
|
84
|
+
for (PeerConnection* pc : std::vector<PeerConnection*>(peers_)) pc->close("torrent stopped");
|
|
85
|
+
releasing_peers_ = false;
|
|
81
86
|
peers_.clear();
|
|
82
87
|
outstanding_.clear();
|
|
83
88
|
recent_down_.clear();
|
|
@@ -145,7 +150,14 @@ void Torrent::pause() {
|
|
|
145
150
|
LOG_INFO("bt.torrent", short_hash(info_hash()) << " paused (" << peers_.size() << " peers dropped)");
|
|
146
151
|
// Drop all peers and their per-peer request state, but keep picker_/disk_ so a
|
|
147
152
|
// later resume() does not have to re-hash what is already on disk.
|
|
148
|
-
|
|
153
|
+
//
|
|
154
|
+
// These peers did nothing wrong, so release them without the reconnect penalty
|
|
155
|
+
// — otherwise resume() would sit out the backoff before it could dial the very
|
|
156
|
+
// peers it just dropped. Iterate a SNAPSHOT: close() runs on_closed inline,
|
|
157
|
+
// which erases from peers_ (see the same hazard in on_check_complete).
|
|
158
|
+
releasing_peers_ = true;
|
|
159
|
+
for (PeerConnection* pc : std::vector<PeerConnection*>(peers_)) pc->close("torrent paused");
|
|
160
|
+
releasing_peers_ = false;
|
|
149
161
|
peers_.clear();
|
|
150
162
|
outstanding_.clear();
|
|
151
163
|
request_time_.clear();
|
|
@@ -176,14 +188,24 @@ void Torrent::add_peer(const std::string& ip, std::uint16_t port) {
|
|
|
176
188
|
post([this] { try_connect(); });
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
std::function<void(const std::string&, std::uint16_t)> Torrent::dht_peer_sink() {
|
|
192
|
+
// Safe to capture `this` bare: the host re-resolves the torrent by info-hash on
|
|
193
|
+
// the reactor thread before invoking this, and removal happens only there, so a
|
|
194
|
+
// torrent that is still registered when the sink runs is still alive.
|
|
195
|
+
return [this](const std::string& ip, std::uint16_t port) {
|
|
196
|
+
if (peer_list_.add(ip, port, PeerSource::Dht)) try_connect();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
void Torrent::try_connect() {
|
|
180
201
|
if (!running_ || paused_ || peers_.size() >= kMaxPeers) return;
|
|
181
|
-
auto candidates = peer_list_.connect_candidates(kMaxPeers - peers_.size()
|
|
182
|
-
|
|
202
|
+
auto candidates = peer_list_.connect_candidates(kMaxPeers - peers_.size(),
|
|
203
|
+
PeerList::Clock::now());
|
|
204
|
+
for (const auto& c : candidates) host_.connect_peer(*this, c);
|
|
183
205
|
}
|
|
184
206
|
|
|
185
207
|
void Torrent::on_connect_failed(const std::string& ip, std::uint16_t port) {
|
|
186
|
-
peer_list_.on_connect_failed(ip, port);
|
|
208
|
+
peer_list_.on_connect_failed(ip, port, PeerList::Clock::now());
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
// ---- scheduling ----
|
|
@@ -207,16 +229,19 @@ void Torrent::tick() {
|
|
|
207
229
|
// Ask the DHT for fresh peers periodically (every ~30 s).
|
|
208
230
|
if (tick_count_ % 30 == 1) {
|
|
209
231
|
LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → DHT get_peers");
|
|
210
|
-
host_.find_peers_via_dht(info_hash(),
|
|
211
|
-
if (peer_list_.add(ip, port, PeerSource::Dht)) try_connect();
|
|
212
|
-
});
|
|
232
|
+
host_.find_peers_via_dht(info_hash(), dht_peer_sink());
|
|
213
233
|
}
|
|
214
234
|
// Announce ourselves to the DHT so others can find us — promptly on startup,
|
|
215
235
|
// then every ~15 min per BEP 5 (H15). Was previously never done → undiscoverable.
|
|
236
|
+
//
|
|
237
|
+
// The announce is a get_peers traversal with an announce_peer at the end, so it
|
|
238
|
+
// sees the very peers we are looking for. Dropping them meant a magnet waited
|
|
239
|
+
// for the *next* find_peers round (~30 s) to be told about addresses the node
|
|
240
|
+
// already had in hand — half the wait before the first live peer, for nothing.
|
|
216
241
|
if (tick_count_ % 900 == 5) {
|
|
217
242
|
LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → DHT announce_peer port "
|
|
218
243
|
<< host_.listen_port());
|
|
219
|
-
host_.announce_to_dht(info_hash(), host_.listen_port());
|
|
244
|
+
host_.announce_to_dht(info_hash(), host_.listen_port(), dht_peer_sink());
|
|
220
245
|
}
|
|
221
246
|
// Re-announce when the tracker's requested interval has elapsed (H13).
|
|
222
247
|
if (tick_count_ >= next_announce_tick_) {
|
|
@@ -253,7 +278,7 @@ void Torrent::on_handshake(PeerConnection& pc, const InfoHash&, const PeerId&) {
|
|
|
253
278
|
peers_.push_back(&pc);
|
|
254
279
|
outstanding_[&pc] = 0;
|
|
255
280
|
recent_down_[&pc] = 0;
|
|
256
|
-
peer_list_.set_connected(pc.remote_ip(), pc.remote_port(),
|
|
281
|
+
peer_list_.set_connected(pc.remote_ip(), pc.remote_port(), pc.encrypted(), pc.transport());
|
|
257
282
|
// Milestone: the first peer on a torrent is worth an INFO line; the rest are
|
|
258
283
|
// routine (each peer's handshake is already logged at DEBUG in bt.peer).
|
|
259
284
|
if (peers_.size() == 1)
|
|
@@ -512,7 +537,25 @@ void Torrent::on_request(PeerConnection& pc, std::uint32_t piece, std::uint32_t
|
|
|
512
537
|
}
|
|
513
538
|
|
|
514
539
|
void Torrent::on_closed(PeerConnection& pc, const std::string&) {
|
|
515
|
-
|
|
540
|
+
// A connection that died before the handshake completed never gave us anything;
|
|
541
|
+
// count it against the peer so it backs off instead of being re-dialed on the
|
|
542
|
+
// next tick — without the penalty we would hammer it once a second for as long
|
|
543
|
+
// as the torrent lives. Two exceptions: a peer we dropped ourselves (pause) is
|
|
544
|
+
// neither penalised nor delayed, and a dial that may simply have opened with the
|
|
545
|
+
// wrong form of handshake is penalised but not delayed, so the other form can be
|
|
546
|
+
// tried at once rather than a minute later. See PeerList::Disconnect.
|
|
547
|
+
const auto how = releasing_peers_ ? PeerList::Disconnect::Release
|
|
548
|
+
: pc.handshake_done() ? PeerList::Disconnect::Clean
|
|
549
|
+
: pc.fast_reconnect() ? PeerList::Disconnect::FailedRetryNow
|
|
550
|
+
: PeerList::Disconnect::Failed;
|
|
551
|
+
// A dial we opened over uTP that never reached a handshake says something the
|
|
552
|
+
// failure count alone does not: this peer probably has no uTP at all. Record it
|
|
553
|
+
// so the immediate retry above goes out over TCP instead of repeating itself.
|
|
554
|
+
// Only for outgoing connections — an inbound uTP peer that drops us proves the
|
|
555
|
+
// transport works, whatever else went wrong.
|
|
556
|
+
if (pc.outgoing() && pc.transport() == PeerTransport::Utp && !pc.handshake_done())
|
|
557
|
+
peer_list_.note_utp_dial_failed(pc.remote_ip(), pc.remote_port());
|
|
558
|
+
peer_list_.on_disconnected(pc.remote_ip(), pc.remote_port(), how, PeerList::Clock::now());
|
|
516
559
|
pex_sent_.erase(&pc);
|
|
517
560
|
remove_peer(&pc);
|
|
518
561
|
}
|
|
@@ -53,7 +53,10 @@ class Torrent;
|
|
|
53
53
|
class RATS_API TorrentHost {
|
|
54
54
|
public:
|
|
55
55
|
virtual ~TorrentHost() = default;
|
|
56
|
-
|
|
56
|
+
/// Dial a peer for @p torrent. The endpoint carries the PeerList's per-peer
|
|
57
|
+
/// preferences (MSE alternation, uTP-or-TCP); the host combines them with the
|
|
58
|
+
/// session's own policy, which may override either.
|
|
59
|
+
virtual void connect_peer(Torrent& torrent, const PeerList::Endpoint& peer) = 0;
|
|
57
60
|
virtual const PeerId& peer_id() const = 0;
|
|
58
61
|
virtual std::uint16_t listen_port() const = 0;
|
|
59
62
|
/// Discover peers for @p info_hash via the DHT (if the host has one); each
|
|
@@ -61,8 +64,12 @@ public:
|
|
|
61
64
|
virtual void find_peers_via_dht(const InfoHash& /*info_hash*/,
|
|
62
65
|
std::function<void(const std::string& ip, std::uint16_t port)> /*on_peer*/) {}
|
|
63
66
|
/// Announce ourselves to the DHT for @p info_hash on @p port so other clients
|
|
64
|
-
/// doing get_peers can find us (BEP 5).
|
|
65
|
-
|
|
67
|
+
/// doing get_peers can find us (BEP 5). An announce *is* a get_peers traversal
|
|
68
|
+
/// with a write at the end, so it discovers peers as a side effect; they are
|
|
69
|
+
/// delivered to @p on_peer on the reactor thread exactly like find_peers_via_dht's
|
|
70
|
+
/// are, rather than being thrown away. Default no-op (no DHT attached).
|
|
71
|
+
virtual void announce_to_dht(const InfoHash& /*info_hash*/, std::uint16_t /*port*/,
|
|
72
|
+
std::function<void(const std::string& ip, std::uint16_t port)> /*on_peer*/ = {}) {}
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
class RATS_API Torrent final : public PeerConnection::Observer {
|
|
@@ -143,6 +150,15 @@ public:
|
|
|
143
150
|
void on_closed(PeerConnection&, const std::string& reason) override;
|
|
144
151
|
|
|
145
152
|
private:
|
|
153
|
+
/// Set while pause() is tearing down its peers, so on_closed can tell "we let
|
|
154
|
+
/// this peer go" from "this peer failed on us" and skip the reconnect penalty.
|
|
155
|
+
bool releasing_peers_ = false;
|
|
156
|
+
|
|
157
|
+
/// The callback both DHT paths (find_peers and announce) hand to the host:
|
|
158
|
+
/// merge the address into peer_list_ and dial if it is new. Runs on the
|
|
159
|
+
/// reactor thread — the host marshals it there.
|
|
160
|
+
std::function<void(const std::string&, std::uint16_t)> dht_peer_sink();
|
|
161
|
+
|
|
146
162
|
/// Post @p fn to the reactor, guarded by alive_ so it is dropped if this
|
|
147
163
|
/// torrent has since been destroyed. Disk and tracker completions run on their
|
|
148
164
|
/// own worker threads and marshal back through here; a completion that a worker
|
|
@@ -32,6 +32,33 @@ using PeerId = std::array<std::uint8_t, kPeerIdSize>;
|
|
|
32
32
|
/// The 8 reserved bytes exchanged in the BitTorrent handshake.
|
|
33
33
|
using ReservedBytes = std::array<std::uint8_t, 8>;
|
|
34
34
|
|
|
35
|
+
/// How willing we are to speak MSE/PE (see bittorrent/mse.h). Named and valued
|
|
36
|
+
/// after libtorrent's pe_forced / pe_enabled / pe_disabled so the behaviour is
|
|
37
|
+
/// recognisable to anyone who has configured a BitTorrent client before.
|
|
38
|
+
enum class EncPolicy {
|
|
39
|
+
/// Obfuscated connections only. Plaintext is refused in both directions.
|
|
40
|
+
Forced,
|
|
41
|
+
/// Both are acceptable. Outgoing dials alternate — MSE first, plaintext on the
|
|
42
|
+
/// peer's next attempt — so a peer that rejects one is reached with the other.
|
|
43
|
+
Enabled,
|
|
44
|
+
/// Never speak MSE: dial in plaintext, refuse obfuscated inbound connections.
|
|
45
|
+
Disabled,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/// Which wire a peer connection is carried on. Both are first-class: the
|
|
49
|
+
/// BitTorrent protocol above them is byte-for-byte identical, and a peer reached
|
|
50
|
+
/// either way is treated the same everywhere except when deciding how to dial it
|
|
51
|
+
/// again (see PeerList::Peer::supports_utp).
|
|
52
|
+
enum class PeerTransport : std::uint8_t {
|
|
53
|
+
/// A kernel TCP socket. Always available, universally reachable, and rude to
|
|
54
|
+
/// everything else sharing the uplink.
|
|
55
|
+
Tcp,
|
|
56
|
+
/// uTP (BEP 29) over the session's one shared UDP socket: same guarantees,
|
|
57
|
+
/// delay-based congestion control that yields to TCP instead of competing with
|
|
58
|
+
/// it, and one NAT mapping for the whole swarm.
|
|
59
|
+
Utp,
|
|
60
|
+
};
|
|
61
|
+
|
|
35
62
|
// ---- Protocol constants ----
|
|
36
63
|
constexpr char kProtocolString[] = "BitTorrent protocol"; // 19 chars (+NUL)
|
|
37
64
|
constexpr std::size_t kProtocolStringLen = 19;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#include "librats/bittorrent/utp_manager.h"
|
|
2
|
+
|
|
3
|
+
#include "librats/bittorrent/log.h"
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <random>
|
|
7
|
+
|
|
8
|
+
namespace librats::bittorrent::utp {
|
|
9
|
+
|
|
10
|
+
namespace {
|
|
11
|
+
|
|
12
|
+
std::uint16_t random_id() {
|
|
13
|
+
static thread_local std::mt19937 rng{std::random_device{}()};
|
|
14
|
+
// Leave room for the +1: the pair must not straddle the wrap, or the two ids
|
|
15
|
+
// would not be adjacent in the arithmetic every peer uses to derive them.
|
|
16
|
+
return std::uint16_t(std::uniform_int_distribution<std::uint32_t>(1, 0xfffd)(rng));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Datagrams read per readable event before we go back to the poller. Without a
|
|
20
|
+
/// bound, a peer able to fill the socket faster than we drain it would keep the
|
|
21
|
+
/// loop here indefinitely and starve every other connection and timer.
|
|
22
|
+
constexpr int kMaxRecvPerWakeup = 256;
|
|
23
|
+
|
|
24
|
+
} // namespace
|
|
25
|
+
|
|
26
|
+
Manager::Manager(Reactor& reactor) : reactor_(reactor) {}
|
|
27
|
+
|
|
28
|
+
Manager::~Manager() {
|
|
29
|
+
close();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bool Manager::open(std::uint16_t port, const std::string& bind_address) {
|
|
33
|
+
if (is_open()) return true;
|
|
34
|
+
// Exclusive, and this is the one place in the library where that matters. The
|
|
35
|
+
// port we want by protocol is the torrent port, which the DHT is very often
|
|
36
|
+
// already serving on UDP — mainline BitTorrent puts them on one number. A
|
|
37
|
+
// Shared bind succeeds there and then the two sockets split the datagrams
|
|
38
|
+
// between them: every uTP answer that lands on the DHT socket is a dial that
|
|
39
|
+
// times out for no visible reason, and every KRPC reply that lands here is a
|
|
40
|
+
// DHT query that goes unanswered. Failing instead lets the caller take a port
|
|
41
|
+
// that is really ours.
|
|
42
|
+
socket_ = create_udp_socket(int(port), bind_address, AddressFamily::IPv4,
|
|
43
|
+
UdpPortMode::Exclusive);
|
|
44
|
+
if (!is_valid_socket(socket_)) return false;
|
|
45
|
+
set_socket_nonblocking(socket_);
|
|
46
|
+
port_ = std::uint16_t(get_bound_port(socket_));
|
|
47
|
+
if (!reactor_.add(socket_, PollIn, [this](std::uint32_t) { on_readable(); })) {
|
|
48
|
+
close_socket(socket_);
|
|
49
|
+
socket_ = RATS_INVALID_SOCKET;
|
|
50
|
+
port_ = 0;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
tick_timer_ = reactor_.schedule(kTickInterval, [this] { tick(); });
|
|
54
|
+
LOG_INFO("bt.utp", "uTP listening on UDP port " << port_);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void Manager::close() {
|
|
59
|
+
if (tick_timer_ != kInvalidTimerId) {
|
|
60
|
+
reactor_.cancel(tick_timer_);
|
|
61
|
+
tick_timer_ = kInvalidTimerId;
|
|
62
|
+
}
|
|
63
|
+
if (is_valid_socket(socket_)) {
|
|
64
|
+
reactor_.remove(socket_);
|
|
65
|
+
close_socket(socket_);
|
|
66
|
+
socket_ = RATS_INVALID_SOCKET;
|
|
67
|
+
}
|
|
68
|
+
port_ = 0;
|
|
69
|
+
deferred_.clear();
|
|
70
|
+
streams_.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- Host --------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
void Manager::utp_send(const Address& to, const std::uint8_t* data, std::size_t len) {
|
|
76
|
+
if (!is_valid_socket(socket_)) return;
|
|
77
|
+
// A datagram that will not fit in the socket's send buffer is simply dropped:
|
|
78
|
+
// that is exactly what a congested link does to it a hop later, and the stream's
|
|
79
|
+
// retransmission timer is already the recovery path for it.
|
|
80
|
+
send_udp_to(socket_, data, len, to, AddressFamily::IPv4);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void Manager::utp_defer_ack(Stream& s) {
|
|
84
|
+
deferred_.push_back(&s);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- Streams -----------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
Stream* Manager::find(const Address& from, std::uint16_t id) {
|
|
90
|
+
auto range = streams_.equal_range(id);
|
|
91
|
+
for (auto it = range.first; it != range.second; ++it) {
|
|
92
|
+
if (it->second->matches(from, id)) return it->second.get();
|
|
93
|
+
}
|
|
94
|
+
return nullptr;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Stream* Manager::connect(const Address& to) {
|
|
98
|
+
if (!is_open() || streams_.size() >= kMaxStreams) return nullptr;
|
|
99
|
+
|
|
100
|
+
// The initiator picks the id it will *receive* on and derives the send id from
|
|
101
|
+
// it. Retry a few times so two live connections to the same peer cannot collide
|
|
102
|
+
// on an id — the pair (id, endpoint) is what identifies a stream.
|
|
103
|
+
std::uint16_t recv_id = 0;
|
|
104
|
+
for (int attempt = 0; attempt < 8; ++attempt) {
|
|
105
|
+
recv_id = random_id();
|
|
106
|
+
if (find(to, recv_id) == nullptr) break;
|
|
107
|
+
recv_id = 0;
|
|
108
|
+
}
|
|
109
|
+
if (recv_id == 0) return nullptr;
|
|
110
|
+
|
|
111
|
+
auto s = std::make_unique<Stream>(*this, recv_id, std::uint16_t(recv_id + 1));
|
|
112
|
+
Stream* raw = s.get();
|
|
113
|
+
streams_.emplace(recv_id, std::move(s));
|
|
114
|
+
raw->connect(to, Stream::Clock::now());
|
|
115
|
+
return raw;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void Manager::release(Stream& s) {
|
|
119
|
+
s.detach();
|
|
120
|
+
s.close(Stream::Clock::now());
|
|
121
|
+
// Not erased here: close() may still owe the peer a FIN, and we may be inside
|
|
122
|
+
// an iteration over streams_ right now. reap() collects it once it is done.
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- I/O ---------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
void Manager::on_readable() {
|
|
128
|
+
std::uint8_t buf[kMaxRecvDatagram];
|
|
129
|
+
const auto now = Stream::Clock::now();
|
|
130
|
+
|
|
131
|
+
for (int i = 0; i < kMaxRecvPerWakeup; ++i) {
|
|
132
|
+
Address from;
|
|
133
|
+
const std::ptrdiff_t n = recv_udp_from(socket_, buf, sizeof(buf), from);
|
|
134
|
+
if (n == kUdpRecvWouldBlock) break;
|
|
135
|
+
// kUdpRecvError covers an ICMP unreachable for some *other* destination,
|
|
136
|
+
// reported against the shared socket. It says nothing about this socket, so
|
|
137
|
+
// keep draining rather than dropping every peer over one dead address.
|
|
138
|
+
if (n == kUdpRecvError) continue;
|
|
139
|
+
if (n < std::ptrdiff_t(kHeaderSize)) continue;
|
|
140
|
+
handle_datagram(buf, std::size_t(n), from, now);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
flush_deferred_acks(now);
|
|
144
|
+
reap(now);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void Manager::handle_datagram(const std::uint8_t* data, std::size_t len, const Address& from,
|
|
148
|
+
Stream::Clock::time_point now) {
|
|
149
|
+
Header h;
|
|
150
|
+
if (!parse_header(data, len, h)) return;
|
|
151
|
+
|
|
152
|
+
if (Stream* s = find(from, h.connection_id); s != nullptr) {
|
|
153
|
+
s->on_packet(data, len, from, now);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Nothing is registered under that id. Only a SYN may create a stream; anything
|
|
158
|
+
// else names a connection that no longer exists. We answer with silence rather
|
|
159
|
+
// than a reset: the sender is unauthenticated, and a reset would make this
|
|
160
|
+
// socket a reflector that answers every spoofed datagram with one of its own.
|
|
161
|
+
if (h.type != PacketType::Syn) return;
|
|
162
|
+
|
|
163
|
+
// A retransmitted SYN — the answer to the first one was lost. The stream it
|
|
164
|
+
// created is registered under id + 1, which is not the id the SYN carries, so
|
|
165
|
+
// it has to be looked up explicitly. Without this a peer whose SYN-ack went
|
|
166
|
+
// missing would leave a duplicate stream behind on every retry.
|
|
167
|
+
if (Stream* s = find(from, std::uint16_t(h.connection_id + 1)); s != nullptr) {
|
|
168
|
+
s->on_packet(data, len, from, now);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!accept_incoming_ || streams_.size() >= kMaxStreams) return;
|
|
173
|
+
|
|
174
|
+
const std::uint16_t send_id = h.connection_id;
|
|
175
|
+
const std::uint16_t recv_id = std::uint16_t(h.connection_id + 1);
|
|
176
|
+
auto s = std::make_unique<Stream>(*this, recv_id, send_id);
|
|
177
|
+
Stream* raw = s.get();
|
|
178
|
+
streams_.emplace(recv_id, std::move(s));
|
|
179
|
+
|
|
180
|
+
if (!raw->on_packet(data, len, from, now)) {
|
|
181
|
+
// The SYN did not survive the stream's own checks; drop the stream again
|
|
182
|
+
// rather than leave an unusable one behind.
|
|
183
|
+
raw->reset(now);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
LOG_DEBUG("bt.utp", "inbound uTP stream from " << from.to_string()
|
|
187
|
+
<< " (id " << recv_id << ')');
|
|
188
|
+
if (accept_) accept_(*raw);
|
|
189
|
+
|
|
190
|
+
// The handler takes ownership by installing an observer. If it declined — the
|
|
191
|
+
// session-wide connection cap, or no handler at all — the stream would otherwise
|
|
192
|
+
// sit here connected and unreaped forever, since nothing would ever close it.
|
|
193
|
+
// Tell the peer rather than let it retransmit into a stream nobody is reading.
|
|
194
|
+
if (!raw->has_observer()) raw->reset(now);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void Manager::flush_deferred_acks(Stream::Clock::time_point now) {
|
|
198
|
+
if (deferred_.empty()) return;
|
|
199
|
+
// A stream may have registered more than once during the drain; the second
|
|
200
|
+
// flush is a no-op because send_deferred_ack() clears the flag.
|
|
201
|
+
for (Stream* s : deferred_) s->send_deferred_ack(now);
|
|
202
|
+
deferred_.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void Manager::tick() {
|
|
206
|
+
const auto now = Stream::Clock::now();
|
|
207
|
+
|
|
208
|
+
// Snapshot first: a stream's timeout fires its observer, which may close a
|
|
209
|
+
// PeerConnection and release streams back to us mid-iteration.
|
|
210
|
+
scratch_.clear();
|
|
211
|
+
scratch_.reserve(streams_.size());
|
|
212
|
+
for (auto& [id, s] : streams_) scratch_.push_back(s.get());
|
|
213
|
+
for (Stream* s : scratch_) s->tick(now);
|
|
214
|
+
scratch_.clear();
|
|
215
|
+
|
|
216
|
+
flush_deferred_acks(now);
|
|
217
|
+
reap(now);
|
|
218
|
+
|
|
219
|
+
tick_timer_ = reactor_.schedule(kTickInterval, [this] { tick(); });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
void Manager::reap(Stream::Clock::time_point now) {
|
|
223
|
+
for (auto it = streams_.begin(); it != streams_.end();) {
|
|
224
|
+
// A stream that still has an observer belongs to a live PeerConnection and
|
|
225
|
+
// is that connection's to end, however dead it looks from here.
|
|
226
|
+
if (!it->second->has_observer() && it->second->reapable(now)) {
|
|
227
|
+
it = streams_.erase(it);
|
|
228
|
+
} else {
|
|
229
|
+
++it;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
} // namespace librats::bittorrent::utp
|