librats 2.3.3 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, socket_t sock, bool outgoing,
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
- , sock_(sock)
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, socket_t sock, const PeerId& our_peer_id,
72
- Resolver resolver, std::string remote_ip, std::uint16_t remote_port)
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
- , sock_(sock)
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_ && is_valid_socket(sock_)) {
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
- set_socket_nonblocking(sock_);
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
- LOG_DEBUG("bt.peer", remote_ip_ << ':' << remote_port_ << " → handshake sent ("
110
- << short_hash(info_hash_) << ')');
111
- send_handshake();
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
- void PeerConnection::send_handshake() {
116
- std::uint8_t hs[kHandshakeSize];
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(ByteView(hs, kHandshakeSize));
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
- if (is_valid_socket(sock_)) {
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::on_io(std::uint32_t events) {
158
- if (closed_) return;
159
- if (events & PollOut) flush();
160
- if (closed_) return;
161
- if (events & PollIn) do_read();
162
- if (closed_) return;
163
- if (events & (PollErr | PollHup)) close("socket error");
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 int n = ::recv(sock_, reinterpret_cast<char*>(into.data()),
179
- static_cast<int>(into.size()), 0);
180
- if (n == 0) { close("peer closed connection"); return; }
181
- if (n < 0) {
182
- if (would_block()) return;
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
- parse();
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 (std::size_t(n) < into.size()) return; // kernel buffer drained
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 std::ptrdiff_t n = send_vectored(sock_, slices, count);
452
- if (n > 0) {
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(std::size_t(n));
581
+ tx_.pop_front(r.bytes);
455
582
  continue;
456
583
  }
457
- if (n == 0) break; // nothing accepted; retry on PollOut
458
- if (would_block()) break; // congested; the backlog waits for PollOut
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
- reactor_.modify(sock_, PollIn | (on ? PollOut : PollNone));
602
+ link_->want_write(on);
475
603
  }
476
604
 
477
605
  void PeerConnection::tick() {