mirror of
https://github.com/transmission/transmission
synced 2024-12-22 07:42:37 +00:00
7e4b4f10a1
* chore: housekeeping * perf: short circuit peer has block check * refactor: track active requests in each respective peer * refactor: swap `ActiveRequests` with new request tracking method * refactor: use bitfield to store active requests per peer * perf: check active request numbers first * refactor: initialise candidate values in constructor * refactor: better naming * refactor: use `find_by_block()` more * refactor: store wishlist mediator in swarm object * test: make it compile * test: update endgame test * test: new test for choke event * test: remove redundant lines * test: new test for request event * test: new test for reject event * refactor: cache block have state in wishlist * test: fix `gotBlockResortsPiece` * fixup! refactor: track active requests in each respective peer * fixup! test: fix `gotBlockResortsPiece` * fix: count webseeds when calculating active requests * build: update xcode project * fix: add missing `candidates_dirty_` checks * chore: remove old `depends-on` comments * fixup! refactor: use bitfield to store active requests per peer * refactor: extract block peer event to separate function * perf: reorder conditions by overhead * perf: check for completed block instead of completed piece * chore: remove duplicated "unrequested piece" check * refactor: merge similar block size sanity check * refactor: use map to store number of requests in wishlist * refactor: add asserts * refactor: flush write buffer as soon as there is new data * refactor: more accurate function naming * fix: account for corrupt pieces in wishlist * fix: account for unaligned blocks in wishlist * Revert "fix: account for unaligned blocks in wishlist" This reverts commit c3fce93cbae49c11d62e26caccedf55c1987aa95. * fixup! refactor: use map to store number of requests in wishlist * fix: account for unaligned blocks in wishlist v2 * chore: add `[[nodiscard]]` * fixup! fix: account for unaligned blocks in wishlist v2 * fix: crash when handshake finishes in the middle of function
665 lines
16 KiB
C++
665 lines
16 KiB
C++
// This file Copyright © 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.
|
|
|
|
#pragma once
|
|
|
|
#ifndef __TRANSMISSION__
|
|
#error only libtransmission should #include this header.
|
|
#endif
|
|
|
|
#include <cstddef> // size_t
|
|
#include <cstdint> // uint8_t, uint64_t
|
|
#include <ctime>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "libtransmission/transmission.h" // tr_block_span_t (ptr only)
|
|
|
|
#include "libtransmission/blocklist.h"
|
|
#include "libtransmission/handshake.h"
|
|
#include "libtransmission/net.h" /* tr_address */
|
|
#include "libtransmission/tr-assert.h"
|
|
#include "libtransmission/utils.h" /* tr_compare_3way */
|
|
|
|
/**
|
|
* @addtogroup peers Peers
|
|
* @{
|
|
*/
|
|
|
|
class tr_peer_socket;
|
|
struct tr_peer;
|
|
struct tr_peerMgr;
|
|
struct tr_peer_stat;
|
|
struct tr_session;
|
|
struct tr_torrent;
|
|
|
|
/* added_f's bitwise-or'ed flags */
|
|
enum
|
|
{
|
|
/* true if the peer supports encryption */
|
|
ADDED_F_ENCRYPTION_FLAG = 1,
|
|
/* true if the peer is a seed or partial seed */
|
|
ADDED_F_UPLOAD_ONLY_FLAG = 2,
|
|
/* true if the peer supports µTP */
|
|
ADDED_F_UTP_FLAGS = 4,
|
|
/* true if the peer has holepunch support */
|
|
ADDED_F_HOLEPUNCH = 8,
|
|
/* true if the peer telling us about this peer
|
|
* initiated the connection (implying that it is connectible) */
|
|
ADDED_F_CONNECTABLE = 16
|
|
};
|
|
|
|
/**
|
|
* Peer information that should be retained even when not connected,
|
|
* e.g. to help us decide which peers to connect to.
|
|
*/
|
|
class tr_peer_info
|
|
{
|
|
public:
|
|
tr_peer_info(tr_socket_address socket_address, uint8_t pex_flags, tr_peer_from from)
|
|
: listen_socket_address_{ socket_address }
|
|
, from_first_{ from }
|
|
, from_best_{ from }
|
|
{
|
|
TR_ASSERT(!std::empty(socket_address.port()));
|
|
++n_known_connectable;
|
|
set_pex_flags(pex_flags);
|
|
}
|
|
|
|
tr_peer_info(tr_address address, uint8_t pex_flags, tr_peer_from from)
|
|
: listen_socket_address_{ address, tr_port{} }
|
|
, from_first_{ from }
|
|
, from_best_{ from }
|
|
{
|
|
set_pex_flags(pex_flags);
|
|
}
|
|
|
|
tr_peer_info(tr_peer_info&&) = delete;
|
|
tr_peer_info(tr_peer_info const&) = delete;
|
|
tr_peer_info& operator=(tr_peer_info&&) = delete;
|
|
tr_peer_info& operator=(tr_peer_info const&) = delete;
|
|
|
|
~tr_peer_info()
|
|
{
|
|
if (!std::empty(listen_socket_address_.port()))
|
|
{
|
|
[[maybe_unused]] auto const n_prev = n_known_connectable--;
|
|
TR_ASSERT(n_prev > 0U);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] static auto known_connectable_count() noexcept
|
|
{
|
|
return n_known_connectable;
|
|
}
|
|
|
|
// ---
|
|
|
|
[[nodiscard]] constexpr auto const& listen_socket_address() const noexcept
|
|
{
|
|
return listen_socket_address_;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto const& listen_address() const noexcept
|
|
{
|
|
return listen_socket_address_.address();
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto listen_port() const noexcept
|
|
{
|
|
return listen_socket_address_.port();
|
|
}
|
|
|
|
void set_listen_port(tr_port port_in) noexcept
|
|
{
|
|
if (!std::empty(port_in))
|
|
{
|
|
auto& port = listen_socket_address_.port_;
|
|
if (std::empty(port)) // increment known connectable peers if we did not know the listening port of this peer before
|
|
{
|
|
++n_known_connectable;
|
|
}
|
|
port = port_in;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] auto display_name() const
|
|
{
|
|
return listen_socket_address_.display_name();
|
|
}
|
|
|
|
// ---
|
|
|
|
[[nodiscard]] constexpr auto from_first() const noexcept
|
|
{
|
|
return from_first_;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto from_best() const noexcept
|
|
{
|
|
return from_best_;
|
|
}
|
|
|
|
constexpr void found_at(tr_peer_from from) noexcept
|
|
{
|
|
from_best_ = std::min(from_best_, from);
|
|
}
|
|
|
|
// ---
|
|
|
|
constexpr void set_seed(bool seed = true) noexcept
|
|
{
|
|
is_seed_ = seed;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto is_seed() const noexcept
|
|
{
|
|
return is_seed_;
|
|
}
|
|
|
|
constexpr void set_upload_only(bool value = true) noexcept
|
|
{
|
|
is_upload_only_ = value;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto is_upload_only() const noexcept
|
|
{
|
|
return is_upload_only_ || is_seed();
|
|
}
|
|
|
|
// ---
|
|
|
|
void set_connectable(bool value = true) noexcept
|
|
{
|
|
is_connectable_ = value;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto const& is_connectable() const noexcept
|
|
{
|
|
return is_connectable_;
|
|
}
|
|
|
|
// ---
|
|
|
|
void set_utp_supported(bool value = true) noexcept
|
|
{
|
|
is_utp_supported_ = value;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto supports_utp() const noexcept
|
|
{
|
|
return is_utp_supported_;
|
|
}
|
|
|
|
// ---
|
|
|
|
void set_encryption_preferred(bool value = true) noexcept
|
|
{
|
|
is_encryption_preferred_ = value;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto const& prefers_encryption() const noexcept
|
|
{
|
|
return is_encryption_preferred_;
|
|
}
|
|
|
|
// ---
|
|
|
|
void set_holepunch_supported(bool value = true) noexcept
|
|
{
|
|
is_holepunch_supported_ = value;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto const& supports_holepunch() const noexcept
|
|
{
|
|
return is_holepunch_supported_;
|
|
}
|
|
|
|
// ---
|
|
|
|
[[nodiscard]] constexpr auto compare_by_fruitless_count(tr_peer_info const& that) const noexcept
|
|
{
|
|
return tr_compare_3way(num_consecutive_fruitless_, that.num_consecutive_fruitless_);
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto compare_by_piece_data_time(tr_peer_info const& that) const noexcept
|
|
{
|
|
return tr_compare_3way(piece_data_at_, that.piece_data_at_);
|
|
}
|
|
|
|
// ---
|
|
|
|
constexpr auto set_connected(time_t now, bool is_connected = true, bool is_disconnecting = false) noexcept
|
|
{
|
|
if (is_connected_ == is_connected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
connection_changed_at_ = now;
|
|
|
|
is_connected_ = is_connected;
|
|
|
|
if (is_connected_)
|
|
{
|
|
piece_data_at_ = {};
|
|
}
|
|
else if (has_transferred_piece_data())
|
|
{
|
|
num_consecutive_fruitless_ = {};
|
|
}
|
|
else if (is_disconnecting)
|
|
{
|
|
on_fruitless_connection();
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto is_connected() const noexcept
|
|
{
|
|
return is_connected_;
|
|
}
|
|
|
|
[[nodiscard]] auto has_handshake() const noexcept
|
|
{
|
|
return static_cast<bool>(outgoing_handshake_);
|
|
}
|
|
|
|
template<typename... Args>
|
|
void start_handshake(Args&&... args)
|
|
{
|
|
TR_ASSERT(!outgoing_handshake_);
|
|
if (!outgoing_handshake_)
|
|
{
|
|
outgoing_handshake_ = std::make_unique<tr_handshake>(std::forward<Args>(args)...);
|
|
}
|
|
}
|
|
|
|
void destroy_handshake() noexcept
|
|
{
|
|
outgoing_handshake_.reset();
|
|
}
|
|
|
|
[[nodiscard]] auto is_in_use() const noexcept
|
|
{
|
|
return is_connected() || has_handshake();
|
|
}
|
|
|
|
// ---
|
|
|
|
[[nodiscard]] bool is_blocklisted(libtransmission::Blocklists const& blocklist) const
|
|
{
|
|
if (!blocklisted_.has_value())
|
|
{
|
|
blocklisted_ = blocklist.contains(listen_address());
|
|
}
|
|
|
|
return *blocklisted_;
|
|
}
|
|
|
|
void set_blocklisted_dirty()
|
|
{
|
|
blocklisted_.reset();
|
|
}
|
|
|
|
// ---
|
|
|
|
constexpr void ban() noexcept
|
|
{
|
|
is_banned_ = true;
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto is_banned() const noexcept
|
|
{
|
|
return is_banned_;
|
|
}
|
|
|
|
// ---
|
|
|
|
[[nodiscard]] constexpr auto connection_attempt_time() const noexcept
|
|
{
|
|
return connection_attempted_at_;
|
|
}
|
|
|
|
constexpr void set_connection_attempt_time(time_t value) noexcept
|
|
{
|
|
connection_attempted_at_ = value;
|
|
}
|
|
|
|
constexpr void set_latest_piece_data_time(time_t value) noexcept
|
|
{
|
|
piece_data_at_ = value;
|
|
}
|
|
|
|
[[nodiscard]] constexpr bool has_transferred_piece_data() const noexcept
|
|
{
|
|
return piece_data_at_ != time_t{};
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto reconnect_interval_has_passed(time_t const now) const noexcept
|
|
{
|
|
auto const interval = now - std::max(connection_attempted_at_, connection_changed_at_);
|
|
return interval >= get_reconnect_interval_secs(now);
|
|
}
|
|
|
|
[[nodiscard]] constexpr std::optional<time_t> idle_secs(time_t now) const noexcept
|
|
{
|
|
if (!is_connected_)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
return now - std::max(piece_data_at_, connection_changed_at_);
|
|
}
|
|
|
|
[[nodiscard]] auto is_inactive(time_t const now) const noexcept
|
|
{
|
|
return !is_in_use() && now > 0 && connection_changed_at_ > 0 && now - connection_changed_at_ >= InactiveThresSecs;
|
|
}
|
|
|
|
// ---
|
|
|
|
constexpr void on_fruitless_connection() noexcept
|
|
{
|
|
if (num_consecutive_fruitless_ != std::numeric_limits<decltype(num_consecutive_fruitless_)>::max())
|
|
{
|
|
++num_consecutive_fruitless_;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] constexpr auto fruitless_connection_count() const noexcept
|
|
{
|
|
return num_consecutive_fruitless_;
|
|
}
|
|
|
|
// ---
|
|
|
|
constexpr void set_pex_flags(uint8_t pex_flags) noexcept
|
|
{
|
|
pex_flags_ = pex_flags;
|
|
|
|
if ((pex_flags & ADDED_F_CONNECTABLE) != 0U)
|
|
{
|
|
set_connectable();
|
|
}
|
|
|
|
if ((pex_flags & ADDED_F_UTP_FLAGS) != 0U)
|
|
{
|
|
set_utp_supported();
|
|
}
|
|
|
|
if ((pex_flags & ADDED_F_ENCRYPTION_FLAG) != 0U)
|
|
{
|
|
set_encryption_preferred();
|
|
}
|
|
|
|
if ((pex_flags & ADDED_F_HOLEPUNCH) != 0U)
|
|
{
|
|
set_holepunch_supported();
|
|
}
|
|
|
|
if ((pex_flags & ADDED_F_UPLOAD_ONLY_FLAG) != 0U)
|
|
{
|
|
set_upload_only();
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] constexpr uint8_t pex_flags() const noexcept
|
|
{
|
|
auto ret = pex_flags_;
|
|
|
|
if (is_connectable_)
|
|
{
|
|
if (*is_connectable_)
|
|
{
|
|
ret |= ADDED_F_CONNECTABLE;
|
|
}
|
|
else
|
|
{
|
|
ret &= ~ADDED_F_CONNECTABLE;
|
|
}
|
|
}
|
|
|
|
if (is_utp_supported_)
|
|
{
|
|
if (*is_utp_supported_)
|
|
{
|
|
ret |= ADDED_F_UTP_FLAGS;
|
|
}
|
|
else
|
|
{
|
|
ret &= ~ADDED_F_UTP_FLAGS;
|
|
}
|
|
}
|
|
|
|
if (is_encryption_preferred_)
|
|
{
|
|
if (*is_encryption_preferred_)
|
|
{
|
|
ret |= ADDED_F_ENCRYPTION_FLAG;
|
|
}
|
|
else
|
|
{
|
|
ret &= ~ADDED_F_ENCRYPTION_FLAG;
|
|
}
|
|
}
|
|
|
|
if (is_holepunch_supported_)
|
|
{
|
|
if (*is_holepunch_supported_)
|
|
{
|
|
ret |= ADDED_F_HOLEPUNCH;
|
|
}
|
|
else
|
|
{
|
|
ret &= ~ADDED_F_HOLEPUNCH;
|
|
}
|
|
}
|
|
|
|
if (is_upload_only())
|
|
{
|
|
ret |= ADDED_F_UPLOAD_ONLY_FLAG;
|
|
}
|
|
else
|
|
{
|
|
ret &= ~ADDED_F_UPLOAD_ONLY_FLAG;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// ---
|
|
|
|
// merge two peer info objects that supposedly describes the same peer
|
|
void merge(tr_peer_info& that) noexcept;
|
|
|
|
private:
|
|
[[nodiscard]] constexpr time_t get_reconnect_interval_secs(time_t const now) const noexcept
|
|
{
|
|
// if we were recently connected to this peer and transferring piece
|
|
// data, try to reconnect to them sooner rather that later -- we don't
|
|
// want network troubles to get in the way of a good peer.
|
|
auto const unreachable = is_connectable_ && !*is_connectable_;
|
|
if (!unreachable && now - piece_data_at_ <= MinimumReconnectIntervalSecs * 2)
|
|
{
|
|
return MinimumReconnectIntervalSecs;
|
|
}
|
|
|
|
// otherwise, the interval depends on how many times we've tried
|
|
// and failed to connect to the peer. Penalize peers that were
|
|
// unreachable the last time we tried
|
|
auto step = num_consecutive_fruitless_;
|
|
if (unreachable)
|
|
{
|
|
step += 2;
|
|
}
|
|
|
|
switch (step)
|
|
{
|
|
case 0:
|
|
return 0U;
|
|
case 1:
|
|
return 10U;
|
|
case 2:
|
|
return 60U * 2U;
|
|
case 3:
|
|
return 60U * 15U;
|
|
case 4:
|
|
return 60U * 30U;
|
|
case 5:
|
|
return 60U * 60U;
|
|
default:
|
|
return 60U * 120U;
|
|
}
|
|
}
|
|
|
|
// the minimum we'll wait before attempting to reconnect to a peer
|
|
static auto constexpr MinimumReconnectIntervalSecs = time_t{ 5U };
|
|
static auto constexpr InactiveThresSecs = time_t{ 60 * 60 };
|
|
|
|
static auto inline n_known_connectable = size_t{};
|
|
|
|
// if the port is 0, it SHOULD mean we don't know this peer's listen socket address
|
|
tr_socket_address listen_socket_address_;
|
|
|
|
time_t connection_attempted_at_ = {};
|
|
time_t connection_changed_at_ = {};
|
|
time_t piece_data_at_ = {};
|
|
|
|
mutable std::optional<bool> blocklisted_;
|
|
std::optional<bool> is_connectable_;
|
|
std::optional<bool> is_utp_supported_;
|
|
std::optional<bool> is_encryption_preferred_;
|
|
std::optional<bool> is_holepunch_supported_;
|
|
|
|
tr_peer_from const from_first_; // where the peer was first found
|
|
tr_peer_from from_best_; // the "best" place where this peer was found
|
|
|
|
uint8_t num_consecutive_fruitless_ = {};
|
|
uint8_t pex_flags_ = {};
|
|
|
|
bool is_banned_ = false;
|
|
bool is_connected_ = false;
|
|
bool is_seed_ = false;
|
|
bool is_upload_only_ = false;
|
|
|
|
std::unique_ptr<tr_handshake> outgoing_handshake_;
|
|
};
|
|
|
|
struct tr_pex
|
|
{
|
|
tr_pex() = default;
|
|
|
|
explicit tr_pex(tr_socket_address socket_address_in, uint8_t flags_in = {})
|
|
: socket_address{ socket_address_in }
|
|
, flags{ flags_in }
|
|
{
|
|
}
|
|
|
|
template<typename OutputIt>
|
|
OutputIt to_compact(OutputIt out) const
|
|
{
|
|
return socket_address.to_compact(out);
|
|
}
|
|
|
|
template<typename OutputIt>
|
|
static OutputIt to_compact(OutputIt out, tr_pex const* pex, size_t n_pex)
|
|
{
|
|
for (size_t i = 0; i < n_pex; ++i)
|
|
{
|
|
out = pex[i].to_compact(out);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
[[nodiscard]] static std::vector<tr_pex> from_compact_ipv4(
|
|
void const* compact,
|
|
size_t compact_len,
|
|
uint8_t const* added_f,
|
|
size_t added_f_len);
|
|
|
|
[[nodiscard]] static std::vector<tr_pex> from_compact_ipv6(
|
|
void const* compact,
|
|
size_t compact_len,
|
|
uint8_t const* added_f,
|
|
size_t added_f_len);
|
|
|
|
[[nodiscard]] std::string display_name() const
|
|
{
|
|
return socket_address.display_name();
|
|
}
|
|
|
|
[[nodiscard]] int compare(tr_pex const& that) const noexcept // <=>
|
|
{
|
|
return socket_address.compare(that.socket_address);
|
|
}
|
|
|
|
[[nodiscard]] bool operator==(tr_pex const& that) const noexcept
|
|
{
|
|
return compare(that) == 0;
|
|
}
|
|
|
|
[[nodiscard]] bool operator<(tr_pex const& that) const noexcept
|
|
{
|
|
return compare(that) < 0;
|
|
}
|
|
|
|
[[nodiscard]] bool is_valid() const noexcept
|
|
{
|
|
return socket_address.is_valid();
|
|
}
|
|
|
|
[[nodiscard]] bool is_valid_for_peers(tr_peer_from from) const noexcept
|
|
{
|
|
return socket_address.is_valid_for_peers(from);
|
|
}
|
|
|
|
tr_socket_address socket_address;
|
|
|
|
uint8_t flags = 0;
|
|
};
|
|
|
|
constexpr bool tr_isPex(tr_pex const* pex)
|
|
{
|
|
return pex != nullptr && pex->socket_address.is_valid();
|
|
}
|
|
|
|
[[nodiscard]] tr_peerMgr* tr_peerMgrNew(tr_session* session);
|
|
|
|
void tr_peerMgrFree(tr_peerMgr* manager);
|
|
|
|
[[nodiscard]] std::vector<tr_block_span_t> tr_peerMgrGetNextRequests(tr_torrent* torrent, tr_peer const* peer, size_t numwant);
|
|
|
|
void tr_peerMgrAddIncoming(tr_peerMgr* manager, tr_peer_socket&& socket);
|
|
|
|
size_t tr_peerMgrAddPex(tr_torrent* tor, tr_peer_from from, tr_pex const* pex, size_t n_pex);
|
|
|
|
enum
|
|
{
|
|
TR_PEERS_CONNECTED,
|
|
TR_PEERS_INTERESTING
|
|
};
|
|
|
|
[[nodiscard]] std::vector<tr_pex> tr_peerMgrGetPeers(
|
|
tr_torrent const* tor,
|
|
uint8_t address_type,
|
|
uint8_t peer_list_mode,
|
|
size_t max_peer_count);
|
|
|
|
void tr_peerMgrAddTorrent(tr_peerMgr* manager, struct tr_torrent* tor);
|
|
|
|
// return the number of connected peers that have `piece`, or -1 if we already have it
|
|
[[nodiscard]] int8_t tr_peerMgrPieceAvailability(tr_torrent const* tor, tr_piece_index_t piece);
|
|
|
|
void tr_peerMgrTorrentAvailability(tr_torrent const* tor, int8_t* tab, unsigned int n_tabs);
|
|
|
|
[[nodiscard]] uint64_t tr_peerMgrGetDesiredAvailable(tr_torrent const* tor);
|
|
|
|
[[nodiscard]] struct tr_peer_stat* tr_peerMgrPeerStats(tr_torrent const* tor, size_t* setme_count);
|
|
|
|
[[nodiscard]] tr_webseed_view tr_peerMgrWebseed(tr_torrent const* tor, size_t i);
|
|
|
|
/* @} */
|