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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file mse.h
|
|
5
|
+
* @brief MSE/PE — Message Stream Encryption, the BitTorrent connection
|
|
6
|
+
* obfuscation protocol.
|
|
7
|
+
*
|
|
8
|
+
* Why it exists: a lot of the swarm is configured to *require* an obfuscated
|
|
9
|
+
* connection. Such a peer accepts our TCP connection, reads a plaintext
|
|
10
|
+
* "\x13BitTorrent protocol" handshake and closes without answering a byte — which
|
|
11
|
+
* is exactly what the log this was written for shows. Without MSE those peers are
|
|
12
|
+
* unreachable no matter how many times we dial them.
|
|
13
|
+
*
|
|
14
|
+
* What it is NOT: security. MSE hides *what* protocol is on the wire from a
|
|
15
|
+
* passive observer (and defeats naive traffic shaping); it is unauthenticated, so
|
|
16
|
+
* an active man in the middle can defeat it. It is implemented because the swarm
|
|
17
|
+
* expects it, not because it protects anything. The real transport security in
|
|
18
|
+
* this project is the Noise mesh in `src/librats/security` — unrelated code.
|
|
19
|
+
*
|
|
20
|
+
* The protocol (A dials, B accepts):
|
|
21
|
+
*
|
|
22
|
+
* 1. A->B Ya 96-byte DH public key, then 0..512 random pad
|
|
23
|
+
* 2. B->A Yb 96-byte DH public key, then 0..512 random pad
|
|
24
|
+
* 3. A->B HASH('req1', S) (20, plain)
|
|
25
|
+
* HASH('req2', SKEY) xor HASH('req3', S) (20, plain)
|
|
26
|
+
* RC4( VC | crypto_provide | len(PadC) | PadC | len(IA) | IA )
|
|
27
|
+
* 4. B->A RC4( VC | crypto_select | len(PadD) | PadD )
|
|
28
|
+
* 5. both the payload stream, RC4 or plaintext per crypto_select
|
|
29
|
+
*
|
|
30
|
+
* S is the DH shared secret, SKEY the torrent's info-hash, VC eight zero bytes and
|
|
31
|
+
* IA the initiator's first payload (for us: the 68-byte BitTorrent handshake).
|
|
32
|
+
* RC4 keys are HASH('keyA', S, SKEY) for A->B and HASH('keyB', S, SKEY) for B->A,
|
|
33
|
+
* each with the first 1024 keystream bytes discarded.
|
|
34
|
+
*
|
|
35
|
+
* Two consequences shape the code below:
|
|
36
|
+
*
|
|
37
|
+
* - **Both pads have unknown length**, so a reader cannot simply count bytes: B
|
|
38
|
+
* has to *scan* for HASH('req1', S) and A has to scan for the encrypted VC.
|
|
39
|
+
* Everything before the marker is pad and is thrown away.
|
|
40
|
+
* - **B does not learn which torrent it is until step 3.** It cannot: SKEY is
|
|
41
|
+
* never sent in the clear. It recovers it by XOR-ing the obfuscated hash with
|
|
42
|
+
* HASH('req3', S) and comparing against HASH('req2', ih) for each torrent it
|
|
43
|
+
* holds — hence the SkeyResolver callback.
|
|
44
|
+
*
|
|
45
|
+
* Everything here is plain computation over byte buffers: no sockets, no reactor,
|
|
46
|
+
* no torrent. Handshake is a pure state machine driven by consume(), which is what
|
|
47
|
+
* makes it testable end to end against itself.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
#include "librats/bittorrent/types.h"
|
|
51
|
+
#include "librats/core/bytes.h"
|
|
52
|
+
|
|
53
|
+
#include <array>
|
|
54
|
+
#include <cstddef>
|
|
55
|
+
#include <cstdint>
|
|
56
|
+
#include <functional>
|
|
57
|
+
#include <string>
|
|
58
|
+
|
|
59
|
+
namespace librats::bittorrent::mse {
|
|
60
|
+
|
|
61
|
+
constexpr std::size_t kKeyLen = 96; ///< DH public key / shared secret, 768 bits
|
|
62
|
+
constexpr std::size_t kMaxPad = 512; ///< spec ceiling on every pad field
|
|
63
|
+
constexpr std::size_t kVcLen = 8; ///< verification constant: eight zero bytes
|
|
64
|
+
|
|
65
|
+
/// crypto_provide / crypto_select bitmask.
|
|
66
|
+
enum Method : std::uint32_t {
|
|
67
|
+
kPlaintext = 0x01, ///< obfuscated handshake, then the payload in the clear
|
|
68
|
+
kRc4 = 0x02, ///< obfuscated handshake, then RC4 all the way
|
|
69
|
+
kBoth = 0x03,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/// RC4 keystream cipher. Symmetric: process() both encrypts and decrypts, and the
|
|
73
|
+
/// caller must apply it to the stream's bytes in order, exactly once each.
|
|
74
|
+
/// Copyable on purpose — the initiator clones its receive cipher to work out what
|
|
75
|
+
/// the encrypted VC will look like without disturbing the real stream position.
|
|
76
|
+
class Rc4Cipher {
|
|
77
|
+
public:
|
|
78
|
+
Rc4Cipher() = default;
|
|
79
|
+
/// Key the cipher and discard the first 1024 keystream bytes, per the spec.
|
|
80
|
+
void init(const std::uint8_t* key, std::size_t key_len);
|
|
81
|
+
/// XOR `len` bytes of keystream over `data`, in place.
|
|
82
|
+
void process(std::uint8_t* data, std::size_t len);
|
|
83
|
+
bool ready() const noexcept { return ready_; }
|
|
84
|
+
|
|
85
|
+
private:
|
|
86
|
+
std::uint8_t s_[256]{};
|
|
87
|
+
std::uint8_t i_ = 0;
|
|
88
|
+
std::uint8_t j_ = 0;
|
|
89
|
+
bool ready_ = false;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/// Diffie-Hellman over the fixed 768-bit MODP group the MSE spec mandates
|
|
93
|
+
/// (generator 2). Exposed separately so it can be tested on its own.
|
|
94
|
+
class DhKeyExchange {
|
|
95
|
+
public:
|
|
96
|
+
/// Draws a fresh 160-bit private exponent and computes Ya = 2^x mod P.
|
|
97
|
+
DhKeyExchange();
|
|
98
|
+
|
|
99
|
+
const std::array<std::uint8_t, kKeyLen>& public_key() const noexcept { return public_; }
|
|
100
|
+
|
|
101
|
+
/// Compute S = remote^x mod P. Returns false for a degenerate public key
|
|
102
|
+
/// (outside [2, P-2]): those force the shared secret into a tiny subgroup, so
|
|
103
|
+
/// anyone could predict it. libtorrent rejects the same range.
|
|
104
|
+
bool compute_secret(const std::uint8_t* remote_public);
|
|
105
|
+
|
|
106
|
+
/// Valid only after a successful compute_secret().
|
|
107
|
+
const std::array<std::uint8_t, kKeyLen>& secret() const noexcept { return secret_; }
|
|
108
|
+
|
|
109
|
+
private:
|
|
110
|
+
std::array<std::uint8_t, kKeyLen> public_{};
|
|
111
|
+
std::array<std::uint8_t, kKeyLen> secret_{};
|
|
112
|
+
std::array<std::uint8_t, 20> private_{};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/// Does @p candidate explain the obfuscated stream key a peer sent?
|
|
116
|
+
///
|
|
117
|
+
/// The receiver never learns SKEY directly: the peer sends
|
|
118
|
+
/// `HASH('req2', SKEY) xor HASH('req3', S)`, and the only way back is to guess
|
|
119
|
+
/// SKEY and check. A node holding N torrents does N of these per inbound
|
|
120
|
+
/// connection, which is why it is a cheap standalone predicate rather than
|
|
121
|
+
/// something that rebuilds state.
|
|
122
|
+
bool skey_matches(const std::uint8_t* obfuscated, const std::uint8_t* req3_hash,
|
|
123
|
+
const InfoHash& candidate);
|
|
124
|
+
|
|
125
|
+
/// One side of an MSE handshake, as a state machine over the byte stream.
|
|
126
|
+
///
|
|
127
|
+
/// Drive it by feeding everything that arrives to consume() and writing whatever
|
|
128
|
+
/// take_output() hands back. On Done, result() carries the two ciphers (already
|
|
129
|
+
/// advanced past the handshake, so payload encryption just continues them) and
|
|
130
|
+
/// leftover() the bytes that arrived after the handshake and still belong to the
|
|
131
|
+
/// caller — raw and *not* decrypted, because whether they should be decrypted at
|
|
132
|
+
/// all depends on the negotiated method.
|
|
133
|
+
class Handshake {
|
|
134
|
+
public:
|
|
135
|
+
enum class Status { NeedMore, Done, Failed };
|
|
136
|
+
|
|
137
|
+
/// Recover the torrent behind an obfuscated stream key. Called with the peer's
|
|
138
|
+
/// 20-byte `HASH('req2', SKEY) xor HASH('req3', S)` and our own 20-byte
|
|
139
|
+
/// `HASH('req3', S)`; XOR them to get `HASH('req2', SKEY)` and compare against
|
|
140
|
+
/// each torrent's. Returns false if none match (we do not hold that torrent).
|
|
141
|
+
using SkeyResolver = std::function<bool(const std::uint8_t* obfuscated,
|
|
142
|
+
const std::uint8_t* req3_hash,
|
|
143
|
+
InfoHash& out)>;
|
|
144
|
+
|
|
145
|
+
struct Result {
|
|
146
|
+
Rc4Cipher send_cipher; ///< positioned right after our handshake bytes
|
|
147
|
+
Rc4Cipher recv_cipher; ///< positioned right after the peer's
|
|
148
|
+
bool rc4_payload = false; ///< false => crypto_select was plaintext
|
|
149
|
+
InfoHash info_hash{}; ///< receiver only: the torrent SKEY named
|
|
150
|
+
Bytes initial_payload; ///< receiver only: the decrypted IA
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/// Initiator. @p ia is the payload to embed in step 3 — the BitTorrent
|
|
154
|
+
/// handshake. @p provide is the crypto_provide bitmask we offer.
|
|
155
|
+
Handshake(const InfoHash& info_hash, Bytes ia, std::uint32_t provide);
|
|
156
|
+
/// Receiver. @p allowed bounds what we will agree to in crypto_select.
|
|
157
|
+
Handshake(SkeyResolver resolver, std::uint32_t allowed);
|
|
158
|
+
|
|
159
|
+
/// Absorb received bytes and advance. Everything offered is taken.
|
|
160
|
+
Status consume(const std::uint8_t* data, std::size_t len);
|
|
161
|
+
|
|
162
|
+
/// Bytes to write to the socket, moved out (empty if there are none pending).
|
|
163
|
+
Bytes take_output();
|
|
164
|
+
|
|
165
|
+
/// Post-handshake bytes already received. Valid once consume() returns Done.
|
|
166
|
+
ByteView leftover() const;
|
|
167
|
+
|
|
168
|
+
const std::string& error() const noexcept { return error_; }
|
|
169
|
+
Result& result() noexcept { return result_; }
|
|
170
|
+
|
|
171
|
+
/// Largest IA we will accept from a peer. The only legitimate IA is the
|
|
172
|
+
/// 68-byte BitTorrent handshake; the slack covers a client that pipelines a
|
|
173
|
+
/// message or two behind it, and the cap stops a peer naming a huge length.
|
|
174
|
+
static constexpr std::size_t kMaxIa = 4096;
|
|
175
|
+
|
|
176
|
+
private:
|
|
177
|
+
enum class State {
|
|
178
|
+
ReadYb, SyncVc, ReadVcBody, ReadPadD, // initiator
|
|
179
|
+
ReadYa, SyncHash, ReadSkey, ReadVcCrypto, ReadPadC, ReadIa, // receiver
|
|
180
|
+
Done, Failed,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
Status advance(); ///< run the state machine until it blocks
|
|
184
|
+
Status fail(const std::string& why);
|
|
185
|
+
void derive_ciphers(const InfoHash& skey, bool outgoing);
|
|
186
|
+
/// Search buf_ from pos_ for `pattern`, giving up once `limit` bytes of the
|
|
187
|
+
/// stream have been scanned without a hit. Returns the absolute offset, or
|
|
188
|
+
/// npos while still searching (scanned_ carries the give-up budget).
|
|
189
|
+
std::size_t find_sync(const std::uint8_t* pattern, std::size_t len, std::size_t limit);
|
|
190
|
+
std::size_t available() const noexcept { return buf_.size() - pos_; }
|
|
191
|
+
/// Decrypt the next @p n buffered bytes in place, advance pos_ past them and
|
|
192
|
+
/// return where they start. Only ever called with a length the protocol has
|
|
193
|
+
/// already pinned down, so the receive cipher never runs past the end of the
|
|
194
|
+
/// encrypted region and into payload that may be plaintext.
|
|
195
|
+
const std::uint8_t* take_decrypted(std::size_t n);
|
|
196
|
+
|
|
197
|
+
bool initiator_ = false;
|
|
198
|
+
State state_ = State::ReadYb;
|
|
199
|
+
std::string error_;
|
|
200
|
+
Result result_;
|
|
201
|
+
|
|
202
|
+
DhKeyExchange dh_;
|
|
203
|
+
InfoHash info_hash_{}; ///< initiator: known up front; receiver: resolved
|
|
204
|
+
Bytes ia_; ///< initiator: the payload for step 3
|
|
205
|
+
std::uint32_t crypto_mask_ = 0; ///< provide (initiator) / allowed (receiver)
|
|
206
|
+
SkeyResolver resolver_;
|
|
207
|
+
|
|
208
|
+
Bytes buf_; ///< everything received, raw
|
|
209
|
+
std::size_t pos_ = 0; ///< how much of buf_ the state machine has eaten
|
|
210
|
+
std::size_t scanned_ = 0; ///< bytes skipped so far by the current sync scan
|
|
211
|
+
std::size_t pad_len_ = 0; ///< length of the pad field currently being read
|
|
212
|
+
std::size_t ia_len_ = 0;
|
|
213
|
+
Bytes out_; ///< pending bytes for the socket
|
|
214
|
+
|
|
215
|
+
std::array<std::uint8_t, kVcLen> expected_vc_{}; ///< initiator: RC4(8 zero bytes)
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
} // namespace librats::bittorrent::mse
|
|
@@ -10,18 +10,16 @@ namespace librats::bittorrent {
|
|
|
10
10
|
|
|
11
11
|
namespace {
|
|
12
12
|
|
|
13
|
-
#ifdef _WIN32
|
|
14
|
-
inline bool would_block() { return WSAGetLastError() == WSAEWOULDBLOCK; }
|
|
15
|
-
#else
|
|
16
|
-
inline bool would_block() { return errno == EAGAIN || errno == EWOULDBLOCK; }
|
|
17
|
-
#endif
|
|
18
|
-
|
|
19
13
|
/// Largest message we will accept. A bitfield for ~16M pieces fits; a piece
|
|
20
14
|
/// message is ~16 KiB. Anything larger is treated as a protocol violation.
|
|
21
15
|
constexpr std::uint32_t kMaxMessageLen = 2 * 1024 * 1024;
|
|
22
16
|
|
|
23
17
|
constexpr std::size_t kRecvChunk = 64 * 1024;
|
|
24
18
|
|
|
19
|
+
/// The fixed prefix of a plaintext handshake: the length byte plus the protocol
|
|
20
|
+
/// string. Enough on its own to tell a plaintext peer from an obfuscated one.
|
|
21
|
+
constexpr std::size_t kHandshakeHeaderSize = 1 + kProtocolStringLen;
|
|
22
|
+
|
|
25
23
|
/// Ceiling on how far rx_ may be grown on the strength of a *declared* message length
|
|
26
24
|
/// alone. Sizing the buffer for the whole message up front (as libtorrent does, growing
|
|
27
25
|
/// straight to packet_size) turns a large message into one allocation instead of a
|
|
@@ -52,12 +50,13 @@ constexpr auto kTickInterval = std::chrono::seconds(10);
|
|
|
52
50
|
|
|
53
51
|
} // namespace
|
|
54
52
|
|
|
55
|
-
PeerConnection::PeerConnection(Reactor& reactor,
|
|
53
|
+
PeerConnection::PeerConnection(Reactor& reactor, std::unique_ptr<PeerLink> link, bool outgoing,
|
|
56
54
|
const InfoHash& info_hash, const PeerId& our_peer_id,
|
|
57
55
|
std::uint32_t num_pieces, Observer* observer,
|
|
58
|
-
std::string remote_ip, std::uint16_t remote_port
|
|
56
|
+
std::string remote_ip, std::uint16_t remote_port,
|
|
57
|
+
DialOptions opts)
|
|
59
58
|
: reactor_(reactor)
|
|
60
|
-
,
|
|
59
|
+
, link_(std::move(link))
|
|
61
60
|
, outgoing_(outgoing)
|
|
62
61
|
, info_hash_(info_hash)
|
|
63
62
|
, our_peer_id_(our_peer_id)
|
|
@@ -66,12 +65,16 @@ PeerConnection::PeerConnection(Reactor& reactor, socket_t sock, bool outgoing,
|
|
|
66
65
|
, bound_(true)
|
|
67
66
|
, remote_ip_(std::move(remote_ip))
|
|
68
67
|
, remote_port_(remote_port)
|
|
68
|
+
, want_mse_(opts.obfuscate)
|
|
69
|
+
, fast_reconnect_(opts.retry_other_form_on_failure)
|
|
69
70
|
, peer_have_(num_pieces, false) {}
|
|
70
71
|
|
|
71
|
-
PeerConnection::PeerConnection(Reactor& reactor,
|
|
72
|
-
|
|
72
|
+
PeerConnection::PeerConnection(Reactor& reactor, std::unique_ptr<PeerLink> link,
|
|
73
|
+
const PeerId& our_peer_id,
|
|
74
|
+
Resolver resolver, std::string remote_ip, std::uint16_t remote_port,
|
|
75
|
+
EncPolicy enc_policy, mse::Handshake::SkeyResolver skey)
|
|
73
76
|
: reactor_(reactor)
|
|
74
|
-
,
|
|
77
|
+
, link_(std::move(link))
|
|
75
78
|
, outgoing_(false)
|
|
76
79
|
, info_hash_{}
|
|
77
80
|
, our_peer_id_(our_peer_id)
|
|
@@ -80,24 +83,25 @@ PeerConnection::PeerConnection(Reactor& reactor, socket_t sock, const PeerId& ou
|
|
|
80
83
|
, resolver_(std::move(resolver))
|
|
81
84
|
, bound_(false)
|
|
82
85
|
, remote_ip_(std::move(remote_ip))
|
|
83
|
-
, remote_port_(remote_port)
|
|
86
|
+
, remote_port_(remote_port)
|
|
87
|
+
, enc_policy_(enc_policy)
|
|
88
|
+
, skey_(std::move(skey))
|
|
89
|
+
// An inbound peer never says which of the two protocols it is opening, so
|
|
90
|
+
// unless policy has already settled the question we have to look at the
|
|
91
|
+
// first bytes before we can interpret any of them.
|
|
92
|
+
, detecting_(enc_policy != EncPolicy::Disabled && bool(skey_)) {}
|
|
84
93
|
|
|
85
94
|
PeerConnection::~PeerConnection() {
|
|
86
95
|
// Cancel the tick before we die so its captured `this` can never fire on freed
|
|
87
96
|
// memory. Same reactor thread owns both the timer and this destructor.
|
|
88
97
|
if (tick_timer_ != kInvalidTimerId) { reactor_.cancel(tick_timer_); tick_timer_ = kInvalidTimerId; }
|
|
89
|
-
if (!closed_
|
|
90
|
-
reactor_.remove(sock_);
|
|
91
|
-
close_socket(sock_);
|
|
92
|
-
sock_ = RATS_INVALID_SOCKET;
|
|
93
|
-
}
|
|
98
|
+
if (!closed_) link_->close();
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
void PeerConnection::start() {
|
|
97
102
|
if (started_) return;
|
|
98
103
|
started_ = true;
|
|
99
|
-
|
|
100
|
-
reactor_.add(sock_, PollIn, [this](std::uint32_t ev) { on_io(ev); });
|
|
104
|
+
link_->start(this);
|
|
101
105
|
|
|
102
106
|
const auto now = std::chrono::steady_clock::now();
|
|
103
107
|
created_ = last_recv_ = last_sent_ = now;
|
|
@@ -106,16 +110,26 @@ void PeerConnection::start() {
|
|
|
106
110
|
// An outgoing peer sends its handshake immediately; an incoming one waits to
|
|
107
111
|
// learn the info-hash, then replies (see parse_handshake()).
|
|
108
112
|
if (outgoing_) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
if (want_mse_) {
|
|
114
|
+
// Obfuscated dial: the BitTorrent handshake is not written directly,
|
|
115
|
+
// it rides inside step 3 of the MSE handshake as the initial payload.
|
|
116
|
+
handshake_sent_ = true;
|
|
117
|
+
mse_ = std::make_unique<mse::Handshake>(info_hash_, build_handshake(), mse::kBoth);
|
|
118
|
+
LOG_DEBUG("bt.peer", remote_ip_ << ':' << remote_port_ << " → MSE handshake sent ("
|
|
119
|
+
<< short_hash(info_hash_) << ')');
|
|
120
|
+
pump_mse(mse::Handshake::Status::NeedMore);
|
|
121
|
+
} else {
|
|
122
|
+
LOG_DEBUG("bt.peer", remote_ip_ << ':' << remote_port_ << " → handshake sent ("
|
|
123
|
+
<< short_hash(info_hash_) << ')');
|
|
124
|
+
send_handshake();
|
|
125
|
+
}
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
Bytes PeerConnection::build_handshake() {
|
|
130
|
+
Bytes hs(kHandshakeSize);
|
|
117
131
|
hs[0] = std::uint8_t(kProtocolStringLen);
|
|
118
|
-
std::memcpy(hs + 1, kProtocolString, kProtocolStringLen);
|
|
132
|
+
std::memcpy(hs.data() + 1, kProtocolString, kProtocolStringLen);
|
|
119
133
|
ReservedBytes reserved{};
|
|
120
134
|
reserved::enable_dht(reserved);
|
|
121
135
|
// NOTE: we deliberately do NOT advertise the Fast Extension (BEP 6). We do not
|
|
@@ -126,11 +140,15 @@ void PeerConnection::send_handshake() {
|
|
|
126
140
|
// nothing, so we'd never download from them. Re-enable only once BEP 6 is
|
|
127
141
|
// actually implemented in dispatch().
|
|
128
142
|
reserved::enable_extensions(reserved);
|
|
129
|
-
std::memcpy(hs + 20, reserved.data(), 8);
|
|
130
|
-
std::memcpy(hs + 28, info_hash_.data(), 20);
|
|
131
|
-
std::memcpy(hs + 48, our_peer_id_.data(), 20);
|
|
143
|
+
std::memcpy(hs.data() + 20, reserved.data(), 8);
|
|
144
|
+
std::memcpy(hs.data() + 28, info_hash_.data(), 20);
|
|
145
|
+
std::memcpy(hs.data() + 48, our_peer_id_.data(), 20);
|
|
146
|
+
return hs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
void PeerConnection::send_handshake() {
|
|
132
150
|
handshake_sent_ = true;
|
|
133
|
-
queue(
|
|
151
|
+
queue(build_handshake());
|
|
134
152
|
flush();
|
|
135
153
|
}
|
|
136
154
|
|
|
@@ -141,11 +159,7 @@ void PeerConnection::close(const std::string& reason) {
|
|
|
141
159
|
// consumer, remote close, torrent stop): one greppable line per disconnect.
|
|
142
160
|
LOG_DEBUG("bt.peer", remote_ip_ << ':' << remote_port_ << " disconnect: " << reason);
|
|
143
161
|
if (tick_timer_ != kInvalidTimerId) { reactor_.cancel(tick_timer_); tick_timer_ = kInvalidTimerId; }
|
|
144
|
-
|
|
145
|
-
reactor_.remove(sock_);
|
|
146
|
-
close_socket(sock_);
|
|
147
|
-
sock_ = RATS_INVALID_SOCKET;
|
|
148
|
-
}
|
|
162
|
+
link_->close();
|
|
149
163
|
// Drop the send backlog; rx_ is deliberately left alone — close() can be called
|
|
150
164
|
// from inside a message handler that still holds a ByteView into it.
|
|
151
165
|
tx_.clear();
|
|
@@ -154,13 +168,16 @@ void PeerConnection::close(const std::string& reason) {
|
|
|
154
168
|
|
|
155
169
|
// ---- I/O ----
|
|
156
170
|
|
|
157
|
-
void PeerConnection::
|
|
158
|
-
if (closed_)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (closed_)
|
|
163
|
-
|
|
171
|
+
void PeerConnection::on_link_readable() {
|
|
172
|
+
if (!closed_) do_read();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
void PeerConnection::on_link_writable() {
|
|
176
|
+
if (!closed_) flush();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void PeerConnection::on_link_error(const std::string& reason) {
|
|
180
|
+
if (!closed_) close(reason);
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
void PeerConnection::do_read() {
|
|
@@ -175,22 +192,30 @@ void PeerConnection::do_read() {
|
|
|
175
192
|
for (;;) {
|
|
176
193
|
const ByteSpan into = rx_.prepare(read_size());
|
|
177
194
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
close("recv error");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
195
|
+
const PeerLink::IoResult r = link_->read(into);
|
|
196
|
+
if (r.status == PeerLink::Status::WouldBlock) return;
|
|
197
|
+
if (r.status == PeerLink::Status::Closed) { close("peer closed connection"); return; }
|
|
198
|
+
if (r.status == PeerLink::Status::Error) { close("recv error"); return; }
|
|
199
|
+
const std::size_t n = r.bytes;
|
|
186
200
|
|
|
187
201
|
last_recv_ = std::chrono::steady_clock::now();
|
|
188
|
-
rx_.commit(std::size_t(n));
|
|
189
202
|
|
|
190
|
-
|
|
203
|
+
if (mse_) {
|
|
204
|
+
// The obfuscated handshake owns the stream while it runs: these bytes
|
|
205
|
+
// are its business, not rx_'s, and they are landed in rx_'s spare tail
|
|
206
|
+
// only because that is where the recv() had to go. Nothing is committed.
|
|
207
|
+
pump_mse(mse_->consume(into.data(), n));
|
|
208
|
+
} else {
|
|
209
|
+
// Past the handshake the payload cipher is a plain XOR over the stream,
|
|
210
|
+
// so decrypting each arrival in place — before anything else looks at
|
|
211
|
+
// it — is all it takes to make the rest of the class cipher-agnostic.
|
|
212
|
+
if (rc4_active_) rc4_recv_.process(into.data(), n);
|
|
213
|
+
rx_.commit(n);
|
|
214
|
+
parse();
|
|
215
|
+
}
|
|
191
216
|
if (closed_) return;
|
|
192
217
|
|
|
193
|
-
if (
|
|
218
|
+
if (n < into.size()) return; // the link has nothing more ready
|
|
194
219
|
}
|
|
195
220
|
}
|
|
196
221
|
|
|
@@ -211,9 +236,85 @@ std::size_t PeerConnection::read_size() const {
|
|
|
211
236
|
return kRecvChunk;
|
|
212
237
|
}
|
|
213
238
|
|
|
239
|
+
// ---- MSE / PE ----
|
|
240
|
+
|
|
241
|
+
bool PeerConnection::detect_inbound_encryption() {
|
|
242
|
+
// A plaintext peer opens with the 20-byte protocol header; an MSE peer opens
|
|
243
|
+
// with 96 bytes of DH public key, which is a uniformly random number. Twenty
|
|
244
|
+
// bytes is therefore both necessary and more than sufficient to tell them
|
|
245
|
+
// apart — the odds of a key beginning with this exact literal are 2^-160.
|
|
246
|
+
if (rx_.size() < kHandshakeHeaderSize) { rx_need_ = kHandshakeHeaderSize; return false; }
|
|
247
|
+
|
|
248
|
+
const bool plaintext = rx_.data()[0] == kProtocolStringLen &&
|
|
249
|
+
std::memcmp(rx_.data() + 1, kProtocolString, kProtocolStringLen) == 0;
|
|
250
|
+
detecting_ = false;
|
|
251
|
+
|
|
252
|
+
if (plaintext) {
|
|
253
|
+
if (enc_policy_ == EncPolicy::Forced) {
|
|
254
|
+
close("plaintext peer refused: encryption required");
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return true; // fall through to the ordinary handshake path
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Obfuscated. Hand the state machine everything received so far and take the
|
|
261
|
+
// bytes out of rx_ — from here until finish_mse() the stream is entirely its.
|
|
262
|
+
mse_ = std::make_unique<mse::Handshake>(skey_, mse::kBoth);
|
|
263
|
+
LOG_DEBUG("bt.peer", remote_ip_ << ':' << remote_port_ << " ← MSE handshake");
|
|
264
|
+
const auto status = mse_->consume(rx_.data(), rx_.size());
|
|
265
|
+
rx_.consume(rx_.size());
|
|
266
|
+
pump_mse(status);
|
|
267
|
+
return false; // parse() resumes from finish_mse(), not from here
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void PeerConnection::pump_mse(mse::Handshake::Status status) {
|
|
271
|
+
if (status == mse::Handshake::Status::Failed) {
|
|
272
|
+
close("MSE handshake failed: " + mse_->error());
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Output first, and always: on the receiving side the same advance() that
|
|
276
|
+
// reaches Done is the one that produced step 4, and the peer is waiting for it.
|
|
277
|
+
if (Bytes out = mse_->take_output(); !out.empty()) {
|
|
278
|
+
queue_raw(std::move(out));
|
|
279
|
+
flush();
|
|
280
|
+
if (closed_) return;
|
|
281
|
+
}
|
|
282
|
+
if (status == mse::Handshake::Status::Done) finish_mse();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
void PeerConnection::finish_mse() {
|
|
286
|
+
mse::Handshake::Result& r = mse_->result();
|
|
287
|
+
rc4_send_ = r.send_cipher;
|
|
288
|
+
rc4_recv_ = r.recv_cipher;
|
|
289
|
+
rc4_active_ = r.rc4_payload;
|
|
290
|
+
encrypted_ = true;
|
|
291
|
+
mse_skey_ = r.info_hash;
|
|
292
|
+
|
|
293
|
+
// The receiver's initial payload came out of the encrypted region already
|
|
294
|
+
// decrypted; whatever arrived behind it is raw, and only ciphertext if
|
|
295
|
+
// crypto_select actually chose RC4.
|
|
296
|
+
Bytes ia = std::move(r.initial_payload);
|
|
297
|
+
Bytes rest = mse_->leftover().to_bytes();
|
|
298
|
+
mse_.reset();
|
|
299
|
+
if (rc4_active_ && !rest.empty()) rc4_recv_.process(rest.data(), rest.size());
|
|
300
|
+
|
|
301
|
+
LOG_DEBUG("bt.peer", remote_ip_ << ':' << remote_port_ << " MSE established ("
|
|
302
|
+
<< (rc4_active_ ? "rc4" : "plaintext payload") << ')');
|
|
303
|
+
|
|
304
|
+
for (const Bytes* b : {&ia, &rest}) {
|
|
305
|
+
if (b->empty()) continue;
|
|
306
|
+
const ByteSpan into = rx_.prepare(b->size());
|
|
307
|
+
std::memcpy(into.data(), b->data(), b->size());
|
|
308
|
+
rx_.commit(b->size());
|
|
309
|
+
}
|
|
310
|
+
parse();
|
|
311
|
+
}
|
|
312
|
+
|
|
214
313
|
void PeerConnection::parse() {
|
|
215
314
|
rx_need_ = 0; // recomputed below: 0 == no message is mid-flight
|
|
216
315
|
|
|
316
|
+
if (detecting_ && !detect_inbound_encryption()) return;
|
|
317
|
+
|
|
217
318
|
if (!handshake_received_) {
|
|
218
319
|
if (rx_.size() < kHandshakeSize) { rx_need_ = kHandshakeSize; return; }
|
|
219
320
|
if (!parse_handshake()) return; // consumed 68 bytes (or closed)
|
|
@@ -248,6 +349,15 @@ bool PeerConnection::parse_handshake() {
|
|
|
248
349
|
InfoHash their_info{};
|
|
249
350
|
std::memcpy(their_info.data(), d + 28, 20);
|
|
250
351
|
|
|
352
|
+
// On an obfuscated inbound connection the torrent was already named once, by
|
|
353
|
+
// the stream key in MSE step 3. The handshake must agree with it: a peer that
|
|
354
|
+
// unlocks the RC4 keys with one info-hash and then asks for a different torrent
|
|
355
|
+
// is either broken or probing, and the two must not be allowed to diverge.
|
|
356
|
+
if (encrypted_ && !outgoing_ && their_info != mse_skey_) {
|
|
357
|
+
close("handshake info-hash does not match the MSE stream key");
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
251
361
|
if (bound_) {
|
|
252
362
|
// Outgoing: the info-hash must be the one we dialed for.
|
|
253
363
|
if (their_info != info_hash_) { close("info-hash mismatch"); return false; }
|
|
@@ -440,6 +550,23 @@ void PeerConnection::send_extended(std::uint8_t ext_id, ByteView payload) {
|
|
|
440
550
|
flush();
|
|
441
551
|
}
|
|
442
552
|
|
|
553
|
+
void PeerConnection::queue(ByteView bytes) {
|
|
554
|
+
if (closed_ || bytes.empty()) return;
|
|
555
|
+
if (!rc4_active_) { tx_.append(bytes); return; }
|
|
556
|
+
// The source is not ours to modify (a message header on the stack, a bitfield
|
|
557
|
+
// owned by the picker), so encrypt through a scratch buffer. assign() reuses
|
|
558
|
+
// its capacity, so this costs no allocation after the first message.
|
|
559
|
+
enc_scratch_.assign(bytes.begin(), bytes.end());
|
|
560
|
+
rc4_send_.process(enc_scratch_.data(), enc_scratch_.size());
|
|
561
|
+
tx_.append(ByteView(enc_scratch_.data(), enc_scratch_.size()));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
void PeerConnection::queue(Bytes bytes) {
|
|
565
|
+
if (closed_ || bytes.empty()) return;
|
|
566
|
+
if (rc4_active_) rc4_send_.process(bytes.data(), bytes.size()); // ours: encrypt in place
|
|
567
|
+
tx_.append(std::move(bytes));
|
|
568
|
+
}
|
|
569
|
+
|
|
443
570
|
void PeerConnection::flush() {
|
|
444
571
|
if (closed_) return;
|
|
445
572
|
|
|
@@ -448,14 +575,15 @@ void PeerConnection::flush() {
|
|
|
448
575
|
ByteView slices[kMaxSendSlices];
|
|
449
576
|
const std::size_t count = tx_.gather(slices, kMaxSendSlices);
|
|
450
577
|
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
578
|
+
const PeerLink::IoResult r = link_->write(slices, count);
|
|
579
|
+
if (r.status == PeerLink::Status::Ok && r.bytes > 0) {
|
|
453
580
|
last_sent_ = std::chrono::steady_clock::now();
|
|
454
|
-
tx_.pop_front(
|
|
581
|
+
tx_.pop_front(r.bytes);
|
|
455
582
|
continue;
|
|
456
583
|
}
|
|
457
|
-
|
|
458
|
-
|
|
584
|
+
// Nothing accepted: the link is congested and the backlog waits to be told
|
|
585
|
+
// it may write again.
|
|
586
|
+
if (r.status != PeerLink::Status::Error) break;
|
|
459
587
|
close("send error");
|
|
460
588
|
return;
|
|
461
589
|
}
|
|
@@ -471,7 +599,7 @@ void PeerConnection::flush() {
|
|
|
471
599
|
void PeerConnection::want_write(bool on) {
|
|
472
600
|
if (on == want_write_ || closed_) return;
|
|
473
601
|
want_write_ = on;
|
|
474
|
-
|
|
602
|
+
link_->want_write(on);
|
|
475
603
|
}
|
|
476
604
|
|
|
477
605
|
void PeerConnection::tick() {
|