From fe91f0435158640afeb24974062dba92fa2c0516 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sat, 16 Jul 2022 15:25:44 -0500 Subject: [PATCH] test: add plaintext & encrypted handshake tests (#3455) --- libtransmission/handshake.cc | 36 +- libtransmission/log.h | 2 +- libtransmission/peer-io.cc | 15 +- libtransmission/peer-io.h | 15 + libtransmission/peer-mse.cc | 43 +-- libtransmission/peer-msgs.cc | 2 +- tests/libtransmission/CMakeLists.txt | 1 + tests/libtransmission/handshake-test.cc | 433 ++++++++++++++++++++++++ 8 files changed, 484 insertions(+), 63 deletions(-) create mode 100644 tests/libtransmission/handshake-test.cc diff --git a/libtransmission/handshake.cc b/libtransmission/handshake.cc index 979c80947..5198c1dc7 100644 --- a/libtransmission/handshake.cc +++ b/libtransmission/handshake.cc @@ -135,24 +135,24 @@ struct tr_handshake std::shared_ptr const mediator; - bool haveReadAnythingFromPeer; - bool haveSentBitTorrentHandshake; - tr_peerIo* io; - DH dh; + bool haveReadAnythingFromPeer = false; + bool haveSentBitTorrentHandshake = false; + tr_peerIo* io = nullptr; + DH dh = {}; handshake_state_t state; tr_encryption_mode encryptionMode; - uint16_t pad_c_len; - uint16_t pad_d_len; - uint16_t ia_len; - uint32_t crypto_select; - uint32_t crypto_provide; - tr_sha1_digest_t myReq1; - struct event* timeout_timer; + uint16_t pad_c_len = {}; + uint16_t pad_d_len = {}; + uint16_t ia_len = {}; + uint32_t crypto_select = {}; + uint32_t crypto_provide = {}; + tr_sha1_digest_t myReq1 = {}; + struct event* timeout_timer = nullptr; std::optional peer_id; - tr_handshake_done_func done_func; - void* done_func_user_data; + tr_handshake_done_func done_func = nullptr; + void* done_func_user_data = nullptr; }; /** @@ -474,7 +474,7 @@ static ReadState readYb(tr_handshake* handshake, struct evbuffer* inbuf) /* cleanup */ evbuffer_free(outbuf); - return READ_LATER; + return READ_NOW; } // MSE spec: "Since the length of [PadB is] unknown, @@ -719,13 +719,15 @@ static ReadState readYa(tr_handshake* handshake, struct evbuffer* inbuf) evbuffer_remove(inbuf, std::data(peer_public_key), std::size(peer_public_key)); handshake->dh.setPeerPublicKey(peer_public_key); - auto req1 = tr_sha1("req1"sv, handshake->dh.secret()); - if (!req1) + if (auto const req1 = tr_sha1("req1"sv, handshake->dh.secret()); req1) + { + handshake->myReq1 = *req1; + } + else { tr_logAddTraceHand(handshake, "error while computing req1 hash after Ya"); return tr_handshakeDone(handshake, false); } - handshake->myReq1 = *req1; // send our public key to the peer tr_logAddTraceHand(handshake, "sending B->A: Diffie Hellman Yb, PadB"); diff --git a/libtransmission/log.h b/libtransmission/log.h index b490139b0..bc549098f 100644 --- a/libtransmission/log.h +++ b/libtransmission/log.h @@ -98,7 +98,7 @@ void tr_logAddMessage( #define tr_logAddLevel(level, ...) \ do \ { \ - if (tr_logGetLevel() >= level) \ + if (tr_logLevelIsActive(level)) \ { \ tr_logAddMessage(__FILE__, __LINE__, level, __VA_ARGS__); \ } \ diff --git a/libtransmission/peer-io.cc b/libtransmission/peer-io.cc index 09e6a1294..9f4e1f62f 100644 --- a/libtransmission/peer-io.cc +++ b/libtransmission/peer-io.cc @@ -351,17 +351,16 @@ static void maybeSetCongestionAlgorithm(tr_socket_t socket, std::string const& a #ifdef WITH_UTP /* UTP callbacks */ -static void utp_on_read(tr_peerIo* const io, uint8_t const* const buf, size_t const buflen) +void tr_peerIo::readBufferAdd(void const* data, size_t n_bytes) { - if (auto const rc = evbuffer_add(io->inbuf.get(), buf, buflen); rc < 0) + if (auto const rc = evbuffer_add(inbuf.get(), data, n_bytes); rc < 0) { tr_logAddWarn(_("Couldn't write to peer")); return; } - tr_logAddTraceIo(io, fmt::format("utp_on_read got {} bytes", buflen)); - tr_peerIoSetEnabled(io, TR_DOWN, true); - canReadWrapper(io); + tr_peerIoSetEnabled(this, TR_DOWN, true); + canReadWrapper(this); } static size_t utp_get_rb_size(tr_peerIo* const io) @@ -467,7 +466,7 @@ static uint64 utp_callback(utp_callback_arguments* args) switch (args->callback_type) { case UTP_ON_READ: - utp_on_read(io, args->buf, args->len); + io->readBufferAdd(args->buf, args->len); break; case UTP_GET_READ_BUFFER_SIZE: @@ -491,7 +490,7 @@ static uint64 utp_callback(utp_callback_arguments* args) #endif /* #ifdef WITH_UTP */ -static tr_peerIo* tr_peerIoNew( +tr_peerIo* tr_peerIoNew( tr_session* session, tr_bandwidth* parent, tr_address const* addr, @@ -504,7 +503,7 @@ static tr_peerIo* tr_peerIoNew( { TR_ASSERT(session != nullptr); TR_ASSERT(session->events != nullptr); - TR_ASSERT(tr_amInEventThread(session)); + auto lock = session->unique_lock(); #ifdef WITH_UTP TR_ASSERT(socket.type == TR_PEER_SOCKET_TYPE_TCP || socket.type == TR_PEER_SOCKET_TYPE_UTP); diff --git a/libtransmission/peer-io.h b/libtransmission/peer-io.h index 470664b4d..1ccbe05e1 100644 --- a/libtransmission/peer-io.h +++ b/libtransmission/peer-io.h @@ -112,6 +112,8 @@ public: return inbuf.get(); } + void readBufferAdd(void const* data, size_t n_bytes); + [[nodiscard]] auto hasBandwidthLeft(tr_direction dir) noexcept { return bandwidth_.clamp(dir, 1024) > 0; @@ -304,6 +306,19 @@ tr_peerIo* tr_peerIoNewIncoming( time_t current_time, struct tr_peer_socket const socket); +// this is only public for testing purposes. +// production code should use tr_peerIoNewOutgoing() or tr_peerIoNewIncoming() +tr_peerIo* tr_peerIoNew( + tr_session* session, + tr_bandwidth* parent, + tr_address const* addr, + tr_port port, + time_t current_time, + tr_sha1_digest_t const* torrent_hash, + bool is_incoming, + bool is_seed, + struct tr_peer_socket const socket); + void tr_peerIoRefImpl(char const* file, int line, tr_peerIo* io); #define tr_peerIoRef(io) tr_peerIoRefImpl(__FILE__, __LINE__, (io)) diff --git a/libtransmission/peer-mse.cc b/libtransmission/peer-mse.cc index 94d281b2b..09cf75fcb 100644 --- a/libtransmission/peer-mse.cc +++ b/libtransmission/peer-mse.cc @@ -13,18 +13,10 @@ #include "transmission.h" #include "crypto-utils.h" // tr_sha1 -#include "net.h" // includes the headers for htonl() #include "peer-mse.h" using namespace std::literals; -// source: https://stackoverflow.com/a/1001330/6568470 -// nb: when we bump to std=C++20, use `std::endian` -static bool is_big_endian() -{ - return htonl(47) == 47; -} - namespace wi { using key_t = math::wide_integer::uintwide_t< @@ -37,22 +29,12 @@ template auto import_bits(std::array::digits> const& bigend_bin) { auto ret = UIntWide{}; + static_assert(sizeof(UIntWide) == sizeof(bigend_bin)); - if (is_big_endian()) + for (auto const walk : bigend_bin) { - for (auto walk = std::rbegin(bigend_bin), end = std::rend(bigend_bin); walk != end; ++walk) - { - ret <<= 8; - ret += static_cast(*walk); - } - } - else - { - for (auto const walk : bigend_bin) - { - ret <<= 8; - ret += static_cast(walk); - } + ret <<= 8; + ret += static_cast(walk); } return ret; @@ -63,21 +45,10 @@ auto export_bits(UIntWide i) { auto ret = std::array::digits>{}; - if (is_big_endian()) + for (auto walk = std::rbegin(ret), end = std::rend(ret); walk != end; ++walk) { - for (auto& walk : ret) - { - walk = std::byte(static_cast(i & 0xFF)); - i >>= 8; - } - } - else - { - for (auto walk = std::rbegin(ret), end = std::rend(ret); walk != end; ++walk) - { - *walk = std::byte(static_cast(i & 0xFF)); - i >>= 8; - } + *walk = std::byte(static_cast(i & 0xFF)); + i >>= 8; } return ret; diff --git a/libtransmission/peer-msgs.cc b/libtransmission/peer-msgs.cc index d397c36ca..8f5e49685 100644 --- a/libtransmission/peer-msgs.cc +++ b/libtransmission/peer-msgs.cc @@ -227,7 +227,7 @@ using UniqueTimer = std::unique_ptr; #define myLogMacro(msgs, level, text) \ do \ { \ - if (tr_logGetLevel() >= (level)) \ + if (tr_logLevelIsActive(level)) \ { \ tr_logAddMessage( \ __FILE__, \ diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index f4ba81c61..0af92cbda 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable(libtransmission-test file-piece-map-test.cc file-test.cc getopt-test.cc + handshake-test.cc history-test.cc json-test.cc magnet-metainfo-test.cc diff --git a/tests/libtransmission/handshake-test.cc b/tests/libtransmission/handshake-test.cc new file mode 100644 index 000000000..fbfed7283 --- /dev/null +++ b/tests/libtransmission/handshake-test.cc @@ -0,0 +1,433 @@ +// This file Copyright (C) 2022 Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0), GPLv3 (SPDX: GPL-3.0), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#include +#include +#include +#include + +#include + +#include "transmission.h" + +#include "handshake.h" +#include "peer-io.h" +#include "session.h" // tr_peerIdInit() + +#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 +{ +namespace test +{ + +auto constexpr MaxWaitMsec = int{ 5000 }; + +using HandshakeTest = SessionTest; + +class MediatorMock final : public tr_handshake_mediator +{ +public: + MediatorMock(tr_session* session) + : session_{ session } + { + } + + [[nodiscard]] std::optional torrentInfo(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 torrentInfoFromObfuscated(tr_sha1_digest_t const& obfuscated) const override + { + for (auto const& [info_hash, info] : torrents) + { + if (obfuscated == *tr_sha1("req2"sv, info.info_hash)) + { + return info; + } + } + + return {}; + } + + [[nodiscard]] event_base* eventBase() const override + { + return session_->event_base; + } + + [[nodiscard]] bool isDHTEnabled() const override + { + return false; + } + + [[nodiscard]] bool isPeerKnownSeed(tr_torrent_id_t /*tor_id*/, tr_address /*addr*/) 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(setme), 10, ' '); + return len; + } + + [[nodiscard]] tr_message_stream_encryption::DH::private_key_bigend_t privateKey() const override + { + return private_key_; + } + + void setUTPFailed(tr_sha1_digest_t const& /*info_hash*/, tr_address /*addr*/) 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::data(str)), std::size(str), std::begin(private_key_)); + } + + tr_session* const session_; + std::map torrents; + tr_message_stream_encryption::DH::private_key_bigend_t private_key_ = {}; +}; + +template +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(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)); +} + +auto constexpr ReservedBytesNoExtensions = std::array{ 0, 0, 0, 0, 0, 0, 0, 0 }; +auto constexpr PlaintextProtocolName = "\023BitTorrent protocol"sv; +auto const default_peer_addr = *tr_address::fromString("127.0.0.1"sv); +auto const default_peer_port = tr_port::fromHost(8080); +auto const torrent_we_are_seeding = tr_handshake_mediator::torrent_info{ *tr_sha1("abcde"sv), + tr_peerIdInit(), + tr_torrent_id_t{ 100 }, + true /*is_done*/ }; +auto const ubuntu_torrent = tr_handshake_mediator::torrent_info{ *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{ -1, -1 }; + EXPECT_EQ(0, evutil_socketpair(LOCAL_SOCKETPAIR_AF, SOCK_STREAM, 0, std::data(sockpair))) << tr_strerror(errno); + auto const now = tr_time(); + auto const peer_socket = tr_peer_socket_tcp_create(sockpair[0]); + auto* const + io = tr_peerIoNewIncoming(session, &session->top_bandwidth_, &default_peer_addr, default_peer_port, now, peer_socket); + return std::make_pair(io, sockpair[1]); +} + +auto createOutgoingIo(tr_session* session, tr_sha1_digest_t const& info_hash) +{ + auto sockpair = std::array{ -1, -1 }; + EXPECT_EQ(0, evutil_socketpair(LOCAL_SOCKETPAIR_AF, SOCK_STREAM, 0, std::data(sockpair))) << tr_strerror(errno); + auto const now = tr_time(); + auto const peer_socket = tr_peer_socket_tcp_create(sockpair[0]); + auto* const io = tr_peerIoNew( + session, + &session->top_bandwidth_, + &default_peer_addr, + default_peer_port, + now, + &info_hash, + false /*is_incoming*/, + false /*is_seed*/, + peer_socket); + return std::make_pair(io, sockpair[1]); +} + +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; +} + +auto makeRandomPeerId() +{ + auto peer_id = tr_peer_id_t{}; + tr_rand_buffer(std::data(peer_id), std::size(peer_id)); + 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; +} + +auto runHandshake( + std::shared_ptr mediator, + tr_peerIo* io, + tr_encryption_mode encryption_mode = TR_CLEAR_PREFERRED) +{ + auto result = std::optional{}; + + static auto const done_callback = [](auto const& resin) + { + *static_cast*>(resin.userData) = resin; + return true; + }; + + tr_handshakeNew(mediator, io, encryption_mode, done_callback, &result); + + waitFor([&result]() { return result.has_value(); }, MaxWaitMsec); + + return result; +} + +TEST_F(HandshakeTest, incomingPlaintext) +{ + auto const peer_id = makeRandomPeerId(); + auto mediator = std::make_shared(session_); + mediator->torrents.emplace(torrent_we_are_seeding.info_hash, torrent_we_are_seeding); + + // 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. + auto [io, sock] = createIncomingIo(session_); + sendToClient(sock, PlaintextProtocolName); + sendToClient(sock, ReservedBytesNoExtensions); + sendToClient(sock, torrent_we_are_seeding.info_hash); + sendToClient(sock, peer_id); + + auto const res = runHandshake(mediator, io); + + // check the results + EXPECT_TRUE(res); + EXPECT_TRUE(res->isConnected); + EXPECT_TRUE(res->readAnythingFromPeer); + EXPECT_EQ(io, res->io); + EXPECT_TRUE(res->peer_id); + EXPECT_EQ(peer_id, res->peer_id); + EXPECT_TRUE(io->torrentHash()); + EXPECT_EQ(torrent_we_are_seeding.info_hash, *io->torrentHash()); + + tr_peerIoUnref(io); + 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 = std::make_shared(session_); + mediator->torrents.emplace(torrent_we_are_seeding.info_hash, torrent_we_are_seeding); + + auto [io, sock] = createIncomingIo(session_); + sendToClient(sock, PlaintextProtocolName); + sendToClient(sock, ReservedBytesNoExtensions); + sendToClient(sock, *tr_sha1("some other torrent unknown to us"sv)); + sendToClient(sock, makeRandomPeerId()); + + auto const res = runHandshake(mediator, io); + + // check the results + EXPECT_TRUE(res); + EXPECT_FALSE(res->isConnected); + EXPECT_TRUE(res->readAnythingFromPeer); + EXPECT_EQ(io, res->io); + EXPECT_FALSE(res->peer_id); + EXPECT_FALSE(io->torrentHash()); + + tr_peerIoUnref(io); + evutil_closesocket(sock); +} + +TEST_F(HandshakeTest, outgoingPlaintext) +{ + auto const peer_id = makeRandomPeerId(); + auto mediator = std::make_shared(session_); + mediator->torrents.emplace(ubuntu_torrent.info_hash, torrent_we_are_seeding); + + auto [io, sock] = createOutgoingIo(session_, ubuntu_torrent.info_hash); + sendToClient(sock, PlaintextProtocolName); + sendToClient(sock, ReservedBytesNoExtensions); + sendToClient(sock, ubuntu_torrent.info_hash); + sendToClient(sock, peer_id); + + auto const res = runHandshake(mediator, io); + + // check the results + EXPECT_TRUE(res); + EXPECT_TRUE(res->isConnected); + EXPECT_TRUE(res->readAnythingFromPeer); + EXPECT_EQ(io, res->io); + EXPECT_TRUE(res->peer_id); + EXPECT_EQ(peer_id, res->peer_id); + EXPECT_TRUE(io->torrentHash()); + EXPECT_EQ(ubuntu_torrent.info_hash, *io->torrentHash()); + EXPECT_EQ(tr_sha1_to_string(ubuntu_torrent.info_hash), tr_sha1_to_string(*io->torrentHash())); + + tr_peerIoUnref(io); + evutil_closesocket(sock); +} + +TEST_F(HandshakeTest, incomingEncrypted) +{ + static auto constexpr ExpectedPeerId = makePeerId("-TR300Z-w4bd4mkebkbi"sv); + + auto mediator = std::make_shared(session_); + mediator->torrents.emplace(ubuntu_torrent.info_hash, ubuntu_torrent); + 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); + EXPECT_TRUE(res->isConnected); + EXPECT_TRUE(res->readAnythingFromPeer); + EXPECT_EQ(io, res->io); + EXPECT_TRUE(res->peer_id); + EXPECT_EQ(ExpectedPeerId, res->peer_id); + EXPECT_TRUE(io->torrentHash()); + EXPECT_EQ(ubuntu_torrent.info_hash, *io->torrentHash()); + EXPECT_EQ(tr_sha1_to_string(ubuntu_torrent.info_hash), tr_sha1_to_string(*io->torrentHash())); + + tr_peerIoUnref(io); + 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 = std::make_shared(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); + EXPECT_FALSE(res->isConnected); + EXPECT_TRUE(res->readAnythingFromPeer); + EXPECT_FALSE(io->torrentHash()); + + tr_peerIoUnref(io); + evutil_closesocket(sock); +} + +TEST_F(HandshakeTest, outgoingEncrypted) +{ + static auto constexpr ExpectedPeerId = makePeerId("-qB4250-scysDI_JuVN3"sv); + + auto mediator = std::make_shared(session_); + mediator->torrents.emplace(ubuntu_torrent.info_hash, ubuntu_torrent); + mediator->setPrivateKeyFromBase64("0EYKCwBWQ4Dg9kX3c5xxjVtBDKw="sv); + + auto [io, sock] = createOutgoingIo(session_, ubuntu_torrent.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); + EXPECT_TRUE(res->isConnected); + EXPECT_TRUE(res->readAnythingFromPeer); + EXPECT_EQ(io, res->io); + EXPECT_TRUE(res->peer_id); + EXPECT_EQ(ExpectedPeerId, res->peer_id); + EXPECT_TRUE(io->torrentHash()); + EXPECT_EQ(ubuntu_torrent.info_hash, *io->torrentHash()); + EXPECT_EQ(tr_sha1_to_string(ubuntu_torrent.info_hash), tr_sha1_to_string(*io->torrentHash())); + + tr_peerIoUnref(io); + evutil_closesocket(sock); +} + +} // namespace test +} // namespace libtransmission