transmission/libtransmission/peer-mgr.h

665 lines
20 KiB
C++

// This file Copyright © 2007-2023 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/net.h" /* tr_address */
#include "libtransmission/tr-assert.h"
#include "libtransmission/utils.h" /* tr_compare_3way */
/**
* @addtogroup peers Peers
* @{
*/
class tr_peer;
class tr_peer_socket;
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_SEED_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_;
}
// ---
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_;
}
// ---
[[nodiscard]] constexpr auto compare_by_failure_count(tr_peer_info const& that) const noexcept
{
return tr_compare_3way(num_consecutive_fails_, that.num_consecutive_fails_);
}
[[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) noexcept
{
connection_changed_at_ = now;
is_connected_ = is_connected;
if (is_connected_)
{
num_consecutive_fails_ = {};
piece_data_at_ = {};
}
}
[[nodiscard]] constexpr auto is_connected() const noexcept
{
return is_connected_;
}
// ---
[[nodiscard]] bool is_blocklisted(tr_session const* session) const;
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_);
}
// ---
constexpr void on_connection_failed() noexcept
{
if (num_consecutive_fails_ != std::numeric_limits<decltype(num_consecutive_fails_)>::max())
{
++num_consecutive_fails_;
}
}
[[nodiscard]] constexpr auto connection_failure_count() const noexcept
{
return num_consecutive_fails_;
}
// ---
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();
}
is_seed_ = (pex_flags & ADDED_F_SEED_FLAG) != 0U;
}
[[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_seed_)
{
ret |= ADDED_F_SEED_FLAG;
}
return ret;
}
// ---
// merge two peer info objects that supposedly describes the same peer
void merge(tr_peer_info const& that) noexcept
{
TR_ASSERT(is_connectable_.value_or(true) || !is_connected());
TR_ASSERT(that.is_connectable_.value_or(true) || !that.is_connected());
connection_attempted_at_ = std::max(connection_attempted_at_, that.connection_attempted_at_);
connection_changed_at_ = std::max(connection_changed_at_, that.connection_changed_at_);
piece_data_at_ = std::max(piece_data_at_, that.piece_data_at_);
/* no need to merge blocklist since it gets updated elsewhere */
{
// This part is frankly convoluted and confusing, but the idea is:
// 1. If the two peer info objects agree that this peer is connectable/non-connectable,
// then the answer is straightforward: We keep the agreed value.
// 2. If the two peer info objects disagrees as to whether this peer is connectable,
// then we reset the flag to an empty value, so that we can try for ourselves when
// initiating outgoing connections.
// 3. If one object has knowledge and the other doesn't, then we take the word of the
// peer info object with knowledge with one exception:
// - If the object with knowledge says the peer is not connectable, but we are
// currently connected to the peer, then we give it the benefit of the doubt.
// The connectable flag will be reset to an empty value.
// 4. In case both objects have no knowledge about whether this peer is connectable,
// we shall not make any assumptions: We keep the flag empty.
//
// Truth table:
// +-----------------+---------------+----------------------+--------------------+---------+
// | is_connectable_ | is_connected_ | that.is_connectable_ | that.is_connected_ | Result |
// +=================+===============+======================+====================+=========+
// | T | T | T | T | T |
// | T | T | T | F | T |
// | T | T | F | F | ? |
// | T | T | ? | T | T |
// | T | T | ? | F | T |
// | T | F | T | T | T |
// | T | F | T | F | T |
// | T | F | F | F | ? |
// | T | F | ? | T | T |
// | T | F | ? | F | T |
// | F | F | T | T | ? |
// | F | F | T | F | ? |
// | F | F | F | F | F |
// | F | F | ? | T | ? |
// | F | F | ? | F | F |
// | ? | T | T | T | T |
// | ? | T | T | F | T |
// | ? | T | F | F | ? |
// | ? | T | ? | T | ? |
// | ? | T | ? | F | ? |
// | ? | F | T | T | T |
// | ? | F | T | F | T |
// | ? | F | F | F | F |
// | ? | F | ? | T | ? |
// | ? | F | ? | F | ? |
// | N/A | N/A | F | T | Invalid |
// | F | T | N/A | N/A | Invalid |
// +-----------------+---------------+----------------------+--------------------+---------+
auto const conn_this = is_connectable_ && *is_connectable_;
auto const conn_that = that.is_connectable_ && *that.is_connectable_;
if ((!is_connectable_ && !that.is_connectable_) ||
is_connectable_.value_or(conn_that || is_connected()) !=
that.is_connectable_.value_or(conn_this || that.is_connected()))
{
is_connectable_.reset();
}
else
{
set_connectable(conn_this || conn_that);
}
}
set_utp_supported(supports_utp() || that.supports_utp());
/* from_first_ should never be modified */
found_at(that.from_best());
/* num_consecutive_fails_ is already the latest */
pex_flags_ |= that.pex_flags_;
if (that.is_banned())
{
ban();
}
/* is_connected_ should already be set */
set_seed(is_seed() || that.is_seed());
}
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 = this->num_consecutive_fails_;
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 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_;
tr_peer_from 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_fails_ = {};
uint8_t pex_flags_ = {};
bool is_banned_ = false;
bool is_connected_ = false;
bool is_seed_ = false;
};
struct tr_pex
{
tr_pex() = default;
tr_pex(tr_address addr_in, tr_port port_in, uint8_t flags_in = {})
: addr{ addr_in }
, port{ port_in }
, flags{ flags_in }
{
}
template<typename OutputIt>
OutputIt to_compact_ipv4(OutputIt out) const
{
return this->addr.to_compact_ipv4(out, this->port);
}
template<typename OutputIt>
OutputIt to_compact_ipv6(OutputIt out) const
{
return this->addr.to_compact_ipv6(out, this->port);
}
template<typename OutputIt>
static OutputIt to_compact_ipv4(OutputIt out, tr_pex const* pex, size_t n_pex)
{
for (size_t i = 0; i < n_pex; ++i)
{
out = pex[i].to_compact_ipv4(out);
}
return out;
}
template<typename OutputIt>
static OutputIt to_compact_ipv6(OutputIt out, tr_pex const* pex, size_t n_pex)
{
for (size_t i = 0; i < n_pex; ++i)
{
out = pex[i].to_compact_ipv6(out);
}
return out;
}
template<typename OutputIt>
static OutputIt to_compact(OutputIt out, tr_pex const* pex, size_t n_pex, tr_address_type type)
{
switch (type)
{
case TR_AF_INET:
return to_compact_ipv4(out, pex, n_pex);
case TR_AF_INET6:
return to_compact_ipv6(out, pex, n_pex);
default:
TR_ASSERT_MSG(false, "invalid type");
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);
template<typename OutputIt>
[[nodiscard]] OutputIt display_name(OutputIt out) const
{
return addr.display_name(out, port);
}
[[nodiscard]] std::string display_name() const
{
return addr.display_name(port);
}
[[nodiscard]] int compare(tr_pex const& that) const noexcept // <=>
{
if (auto const i = addr.compare(that.addr); i != 0)
{
return i;
}
return tr_compare_3way(port, that.port);
}
[[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_for_peers() const noexcept
{
return addr.is_valid_for_peers(port);
}
tr_address addr = {};
tr_port port = {}; /* this field is in network byte order */
uint8_t flags = 0;
};
constexpr bool tr_isPex(tr_pex const* pex)
{
return pex != nullptr && pex->addr.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);
[[nodiscard]] bool tr_peerMgrDidPeerRequest(tr_torrent const* torrent, tr_peer const* peer, tr_block_index_t block);
void tr_peerMgrClientSentRequests(tr_torrent* torrent, tr_peer* peer, tr_block_span_t span);
[[nodiscard]] size_t tr_peerMgrCountActiveRequestsToPeer(tr_torrent const* torrent, tr_peer const* peer);
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);
/* @} */