-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
handshake-test.cc
428 lines (357 loc) · 15.3 KB
/
handshake-test.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
// This file Copyright (C) 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <algorithm>
#include <array>
#include <cassert>
#include <string_view>
#include <event2/util.h>
#include <libtransmission/transmission.h>
#include <libtransmission/handshake.h>
#include <libtransmission/peer-io.h>
#include <libtransmission/session.h> // tr_peerIdInit()
#include <libtransmission/timer.h>
#include "test-fixtures.h"
using namespace std::literals;
#ifdef _WIN32
#define LOCAL_SOCKETPAIR_AF AF_INET
#else
#define LOCAL_SOCKETPAIR_AF AF_UNIX
#endif
namespace libtransmission::test
{
auto constexpr MaxWaitMsec = int{ 5000 };
class HandshakeTest : public SessionTest
{
public:
class MediatorMock final : public tr_handshake::Mediator
{
public:
explicit MediatorMock(tr_session* session)
: session_{ session }
{
}
[[nodiscard]] std::optional<TorrentInfo> torrent(tr_sha1_digest_t const& info_hash) const override
{
if (auto const iter = torrents.find(info_hash); iter != std::end(torrents))
{
return iter->second;
}
return {};
}
[[nodiscard]] std::optional<TorrentInfo> torrent_from_obfuscated(tr_sha1_digest_t const& obfuscated) const override
{
for (auto const& [info_hash, info] : torrents)
{
if (obfuscated == tr_sha1::digest("req2"sv, info.info_hash))
{
return info;
}
}
return {};
}
[[nodiscard]] libtransmission::TimerMaker& timer_maker() override
{
return session_->timerMaker();
}
[[nodiscard]] bool allows_dht() const override
{
return false;
}
[[nodiscard]] bool allows_tcp() const override
{
return true;
}
[[nodiscard]] bool is_peer_known_seed(
tr_torrent_id_t /*tor_id*/,
std::pair<tr_address, tr_port> const& /*socket_address*/) const override
{
return false;
}
[[nodiscard]] size_t pad(void* setme, [[maybe_unused]] size_t maxlen) const override
{
TR_ASSERT(maxlen > 10);
auto const len = size_t{ 10 };
std::fill_n(static_cast<char*>(setme), 10, ' ');
return len;
}
[[nodiscard]] tr_message_stream_encryption::DH::private_key_bigend_t private_key() const override
{
return private_key_;
}
void set_utp_failed(tr_sha1_digest_t const& /*info_hash*/, std::pair<tr_address, tr_port> const& /*socket_address*/)
override
{
}
void setPrivateKeyFromBase64(std::string_view b64)
{
auto const str = tr_base64_decode(b64);
assert(std::size(str) == std::size(private_key_));
std::copy_n(reinterpret_cast<std::byte const*>(std::data(str)), std::size(str), std::begin(private_key_));
}
tr_session* const session_;
std::map<tr_sha1_digest_t, TorrentInfo> torrents;
tr_message_stream_encryption::DH::private_key_bigend_t private_key_ = {};
};
template<typename Span>
void sendToClient(evutil_socket_t sock, Span const& data)
{
auto const* walk = std::data(data);
static_assert(sizeof(*walk) == 1);
size_t len = std::size(data);
while (len > 0)
{
#if defined(_WIN32)
auto const n = send(sock, reinterpret_cast<char const*>(walk), len, 0);
#else
auto const n = write(sock, walk, len);
#endif
assert(n >= 0);
len -= n;
walk += n;
}
}
void sendB64ToClient(evutil_socket_t sock, std::string_view b64)
{
sendToClient(sock, tr_base64_decode(b64));
}
static auto constexpr ReservedBytesNoExtensions = std::array<uint8_t, 8>{ 0, 0, 0, 0, 0, 0, 0, 0 };
static auto constexpr PlaintextProtocolName = "\023BitTorrent protocol"sv;
tr_address const DefaultPeerAddr = *tr_address::from_string("127.0.0.1"sv);
tr_port const DefaultPeerPort = tr_port::fromHost(8080);
tr_handshake::Mediator::TorrentInfo const TorrentWeAreSeeding{ tr_sha1::digest("abcde"sv),
tr_peerIdInit(),
tr_torrent_id_t{ 100 },
true /*is_done*/ };
tr_handshake::Mediator::TorrentInfo const UbuntuTorrent{ *tr_sha1_from_string("2c6b6858d61da9543d4231a71db4b1c9264b0685"sv),
tr_peerIdInit(),
tr_torrent_id_t{ 101 },
false /*is_done*/ };
auto createIncomingIo(tr_session* session)
{
auto sockpair = std::array<evutil_socket_t, 2>{ -1, -1 };
EXPECT_EQ(0, evutil_socketpair(LOCAL_SOCKETPAIR_AF, SOCK_STREAM, 0, std::data(sockpair))) << tr_strerror(errno);
return std::make_pair(
tr_peerIo::new_incoming(
session,
&session->top_bandwidth_,
tr_peer_socket(session, DefaultPeerAddr, DefaultPeerPort, sockpair[0])),
sockpair[1]);
}
auto createOutgoingIo(tr_session* session, tr_sha1_digest_t const& info_hash)
{
auto sockpair = std::array<evutil_socket_t, 2>{ -1, -1 };
EXPECT_EQ(0, evutil_socketpair(LOCAL_SOCKETPAIR_AF, SOCK_STREAM, 0, std::data(sockpair))) << tr_strerror(errno);
auto peer_io = tr_peerIo::create(session, &session->top_bandwidth_, &info_hash, false /*incoming*/, false /*seed*/);
peer_io->set_socket(tr_peer_socket(session, DefaultPeerAddr, DefaultPeerPort, sockpair[0]));
return std::make_pair(std::move(peer_io), sockpair[1]);
}
static constexpr auto makePeerId(std::string_view sv)
{
auto peer_id = tr_peer_id_t{};
for (size_t i = 0, n = std::size(sv); i < n; ++i)
{
peer_id[i] = sv[i];
}
return peer_id;
}
static auto makeRandomPeerId()
{
auto peer_id = tr_rand_obj<tr_peer_id_t>();
auto const peer_id_prefix = "-UW110Q-"sv;
std::copy(std::begin(peer_id_prefix), std::end(peer_id_prefix), std::begin(peer_id));
return peer_id;
}
static auto runHandshake(
tr_handshake::Mediator* mediator,
std::shared_ptr<tr_peerIo> const& peer_io,
tr_encryption_mode encryption_mode = TR_CLEAR_PREFERRED)
{
auto result = std::optional<tr_handshake::Result>{};
auto handshake = tr_handshake{ mediator,
peer_io,
encryption_mode,
[&result](auto const& resin)
{
result = resin;
return true;
} };
waitFor([&result]() { return result.has_value(); }, MaxWaitMsec);
return result;
}
};
TEST_F(HandshakeTest, incomingPlaintext)
{
auto const peer_id = makeRandomPeerId();
auto mediator = MediatorMock{ session_ };
mediator.torrents.emplace(TorrentWeAreSeeding.info_hash, TorrentWeAreSeeding);
// The simplest handshake there is. "The handshake starts with character
// nineteen (decimal) followed by the string 'BitTorrent protocol'.
// The leading character is a length prefix[.]. After the fixed headers
// come eight reserved bytes, which are all zero in all current
// implementations[.] Next comes the 20 byte sha1 hash of the bencoded
// form of the info value from the metainfo file[.] After the download
// hash comes the 20-byte peer id which is reported in tracker requests
// and contained in peer lists in tracker responses."
// https://www.bittorrent.org/beps/bep_0052.html
auto [io, sock] = createIncomingIo(session_);
sendToClient(sock, PlaintextProtocolName);
sendToClient(sock, ReservedBytesNoExtensions);
sendToClient(sock, TorrentWeAreSeeding.info_hash);
sendToClient(sock, peer_id);
auto const res = runHandshake(&mediator, io);
// check the results
EXPECT_TRUE(res.has_value());
assert(res.has_value());
EXPECT_TRUE(res->is_connected);
EXPECT_TRUE(res->read_anything_from_peer);
EXPECT_EQ(io, res->io);
EXPECT_TRUE(res->peer_id);
EXPECT_EQ(peer_id, res->peer_id);
EXPECT_EQ(TorrentWeAreSeeding.info_hash, io->torrent_hash());
evutil_closesocket(sock);
}
// The datastream is identical to HandshakeTest.incomingPlaintext,
// but this time we don't recognize the infohash sent by the peer.
TEST_F(HandshakeTest, incomingPlaintextUnknownInfoHash)
{
auto mediator = MediatorMock{ session_ };
mediator.torrents.emplace(TorrentWeAreSeeding.info_hash, TorrentWeAreSeeding);
auto [io, sock] = createIncomingIo(session_);
sendToClient(sock, PlaintextProtocolName);
sendToClient(sock, ReservedBytesNoExtensions);
sendToClient(sock, tr_sha1::digest("some other torrent unknown to us"sv));
sendToClient(sock, makeRandomPeerId());
auto const res = runHandshake(&mediator, io);
// check the results
EXPECT_TRUE(res.has_value());
assert(res.has_value());
EXPECT_FALSE(res->is_connected);
EXPECT_TRUE(res->read_anything_from_peer);
EXPECT_EQ(io, res->io);
EXPECT_FALSE(res->peer_id);
EXPECT_EQ(tr_sha1_digest_t{}, io->torrent_hash());
evutil_closesocket(sock);
}
TEST_F(HandshakeTest, outgoingPlaintext)
{
auto const peer_id = makeRandomPeerId();
auto mediator = MediatorMock{ session_ };
mediator.torrents.emplace(UbuntuTorrent.info_hash, TorrentWeAreSeeding);
auto [io, sock] = createOutgoingIo(session_, UbuntuTorrent.info_hash);
sendToClient(sock, PlaintextProtocolName);
sendToClient(sock, ReservedBytesNoExtensions);
sendToClient(sock, UbuntuTorrent.info_hash);
sendToClient(sock, peer_id);
auto const res = runHandshake(&mediator, io);
// check the results
EXPECT_TRUE(res.has_value());
assert(res.has_value());
EXPECT_TRUE(res->is_connected);
EXPECT_TRUE(res->read_anything_from_peer);
EXPECT_EQ(io, res->io);
EXPECT_TRUE(res->peer_id);
EXPECT_EQ(peer_id, res->peer_id);
EXPECT_EQ(UbuntuTorrent.info_hash, io->torrent_hash());
EXPECT_EQ(tr_sha1_to_string(UbuntuTorrent.info_hash), tr_sha1_to_string(io->torrent_hash()));
evutil_closesocket(sock);
}
TEST_F(HandshakeTest, incomingEncrypted)
{
static auto constexpr ExpectedPeerId = makePeerId("-TR300Z-w4bd4mkebkbi"sv);
auto mediator = MediatorMock{ session_ };
mediator.torrents.emplace(UbuntuTorrent.info_hash, UbuntuTorrent);
mediator.setPrivateKeyFromBase64("0EYKCwBWQ4Dg9kX3c5xxjVtBDKw="sv);
auto [io, sock] = createIncomingIo(session_);
// Peer->Client data from a successful encrypted handshake recorded
// in the wild for replay here
sendB64ToClient(
sock,
"svkySIFcCsrDTeHjPt516UFbsoR+5vfbe5/m6stE7u5JLZ10kJ19NmP64E10qI"
"nn78sCrJgjw1yEHHwrzOcKiRlYvcMotzJMe+SjrFUnaw3KBfn2bcKBhxb/sfM9"
"J7nJ"sv);
sendB64ToClient(
sock,
"ICAgICAgICAgIKdr4jIBZ4xFfO4xNiRV7Gl2azTSuTFuu06NU1WyRPif018JYe"
"VGwrTPstEPu3V5lmzjtMGVLaL5EErlpJ93Xrz+ea6EIQEUZA+D4jKaV/to9NVi"
"04/1W1A2PHgg+I9puac/i9BsFPcjdQeoVtU73lNCbTDQgTieyjDWmwo="sv);
auto const res = runHandshake(&mediator, io);
// check the results
EXPECT_TRUE(res.has_value());
assert(res.has_value());
EXPECT_TRUE(res->is_connected);
EXPECT_TRUE(res->read_anything_from_peer);
EXPECT_EQ(io, res->io);
EXPECT_TRUE(res->peer_id);
EXPECT_EQ(ExpectedPeerId, res->peer_id);
EXPECT_EQ(UbuntuTorrent.info_hash, io->torrent_hash());
EXPECT_EQ(tr_sha1_to_string(UbuntuTorrent.info_hash), tr_sha1_to_string(io->torrent_hash()));
evutil_closesocket(sock);
}
// The datastream is identical to HandshakeTest.incomingEncrypted,
// but this time we don't recognize the infohash sent by the peer.
TEST_F(HandshakeTest, incomingEncryptedUnknownInfoHash)
{
auto mediator = MediatorMock{ session_ };
mediator.setPrivateKeyFromBase64("0EYKCwBWQ4Dg9kX3c5xxjVtBDKw="sv);
auto [io, sock] = createIncomingIo(session_);
// Peer->Client data from a successful encrypted handshake recorded
// in the wild for replay here
sendB64ToClient(
sock,
"svkySIFcCsrDTeHjPt516UFbsoR+5vfbe5/m6stE7u5JLZ10kJ19NmP64E10qI"
"nn78sCrJgjw1yEHHwrzOcKiRlYvcMotzJMe+SjrFUnaw3KBfn2bcKBhxb/sfM9"
"J7nJ"sv);
sendB64ToClient(
sock,
"ICAgICAgICAgIKdr4jIBZ4xFfO4xNiRV7Gl2azTSuTFuu06NU1WyRPif018JYe"
"VGwrTPstEPu3V5lmzjtMGVLaL5EErlpJ93Xrz+ea6EIQEUZA+D4jKaV/to9NVi"
"04/1W1A2PHgg+I9puac/i9BsFPcjdQeoVtU73lNCbTDQgTieyjDWmwo="sv);
auto const res = runHandshake(&mediator, io);
// check the results
EXPECT_TRUE(res.has_value());
assert(res.has_value());
EXPECT_FALSE(res->is_connected);
EXPECT_TRUE(res->read_anything_from_peer);
EXPECT_EQ(tr_sha1_digest_t{}, io->torrent_hash());
evutil_closesocket(sock);
}
TEST_F(HandshakeTest, outgoingEncrypted)
{
static auto constexpr ExpectedPeerId = makePeerId("-qB4250-scysDI_JuVN3"sv);
auto mediator = MediatorMock{ session_ };
mediator.torrents.emplace(UbuntuTorrent.info_hash, UbuntuTorrent);
mediator.setPrivateKeyFromBase64("0EYKCwBWQ4Dg9kX3c5xxjVtBDKw="sv);
auto [io, sock] = createOutgoingIo(session_, UbuntuTorrent.info_hash);
// Peer->Client data from a successful encrypted handshake recorded
// in the wild for replay here
sendB64ToClient(
sock,
"Sfgoq/nrQfD4Iwirfk+uhOmQMOC/QwK/vYiOact1NF9TpWXms3cvlKEKxs0VU"
"mnmytRh9bh4Lcs1bswlC6R05XrJGzLhZqAqcLUUAR1VTLA5oKSjR1038zFbhn"
"c71jqlpney15ChMTnx02Qt+88l0Z9OWLUUJrUVy+OoIaTMSKDDFVOjuj0y+Ii"
"cE0ZnN61e0/R/g+APRK5tegw0SLZ3Nr8+y4Dl77sZyc141PR9xvDj0da1eAvf"
"BvXyyDem4vUjqiLUNCEV8KDXEMPCPYAQoDZzLvMyOEtJM/if0o0UN88SWtt1k"
"jRD8UNvUlXIfM0YsnJhKA6fJ7/4geK7+Wo2aicfaLFOyG5IEJbTg9OQYbDHFa"
"oVzD0xY0Dx+J0loqM+CzrPj8UpeXIcbD7pJrT3XPECbFQ12cCY5LW5RymVIx8"
"TP0ajGiTxou1L7DbGD54SYgV/4qFbafRsWp9AO+YDJcouFd/jiVN+r3loxvfT"
"0A9H9DRAMR0rZKpQpXZ1ZAhAuAOXGHFIvtw8wd6dPybeu5+LoR2S90/IpwHWI"
"jbNbypQZuA9hn4JfFMWPP9TG/E11loB4+MkrP22U72ezjL5ipd74AEEP0/u8w"
"Gj1t2kXhND9ONfasA+pY25y8GM04M0B7+0xKmsHP7tntwQLAGZATH83rOxaSO"
"3+o/RdiKQJAsGxMIU08scBc5VOmrAmjeYrLNpFnpXVuavH5if7490zMCu3DEn"
"G9hpbYbiX95T+EUcRbM6pSCvr3Twq1Q="sv);
auto const res = runHandshake(&mediator, io, TR_ENCRYPTION_PREFERRED);
// check the results
EXPECT_TRUE(res.has_value());
assert(res.has_value());
EXPECT_TRUE(res->is_connected);
EXPECT_TRUE(res->read_anything_from_peer);
EXPECT_EQ(io, res->io);
EXPECT_TRUE(res->peer_id);
EXPECT_EQ(ExpectedPeerId, res->peer_id);
EXPECT_EQ(UbuntuTorrent.info_hash, io->torrent_hash());
EXPECT_EQ(tr_sha1_to_string(UbuntuTorrent.info_hash), tr_sha1_to_string(io->torrent_hash()));
evutil_closesocket(sock);
}
} // namespace libtransmission::test