feat: add global IP cache, fix UDP connection failure warnings

This commit is contained in:
tearfur 2023-05-06 01:17:40 +08:00 committed by GitHub
parent bd8b50ef7b
commit 474a30ab2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 791 additions and 120 deletions

View File

@ -453,6 +453,8 @@
ED8A16402735A8AA000D61F9 /* peer-mgr-active-requests.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED8A163C2735A8AA000D61F9 /* peer-mgr-active-requests.cc */; };
ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */ = {isa = PBXBuildFile; fileRef = ED8A163D2735A8AA000D61F9 /* peer-mgr-wishlist.h */; };
ED8A16422735A8AA000D61F9 /* peer-mgr-wishlist.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED8A163E2735A8AA000D61F9 /* peer-mgr-wishlist.cc */; };
EDBAAC8C29E486BC00D9495F /* global-ip-cache.h in Headers */ = {isa = PBXBuildFile; fileRef = EDBAAC8B29E486BC00D9495F /* global-ip-cache.h */; };
EDBAAC8E29E486C200D9495F /* global-ip-cache.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDBAAC8D29E486C200D9495F /* global-ip-cache.cc */; };
EDBDFA9E25AFCCA60093D9C1 /* evutil_time.c in Sources */ = {isa = PBXBuildFile; fileRef = EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */; };
F11545ACA7C4D7A464F703AB /* block-info.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A044CBD8C049AFCBD4DB411 /* block-info.h */; settings = {ATTRIBUTES = (Project, ); }; };
F63480631E1D7274005B9E09 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F63480621E1D7274005B9E09 /* Images.xcassets */; };
@ -1240,6 +1242,8 @@
ED8A163C2735A8AA000D61F9 /* peer-mgr-active-requests.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "peer-mgr-active-requests.cc"; sourceTree = "<group>"; };
ED8A163D2735A8AA000D61F9 /* peer-mgr-wishlist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "peer-mgr-wishlist.h"; sourceTree = "<group>"; };
ED8A163E2735A8AA000D61F9 /* peer-mgr-wishlist.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "peer-mgr-wishlist.cc"; sourceTree = "<group>"; };
EDBAAC8B29E486BC00D9495F /* global-ip-cache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "global-ip-cache.h"; sourceTree = "<group>"; };
EDBAAC8D29E486C200D9495F /* global-ip-cache.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "global-ip-cache.cc"; sourceTree = "<group>"; };
EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = evutil_time.c; sourceTree = "<group>"; };
F63480621E1D7274005B9E09 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Images/Images.xcassets; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -1696,6 +1700,8 @@
C1077A4C183EB29600634C22 /* file-posix.cc */,
C1305EB8186A134000F03351 /* file.cc */,
C1077A4D183EB29600634C22 /* file.h */,
EDBAAC8D29E486C200D9495F /* global-ip-cache.cc */,
EDBAAC8B29E486BC00D9495F /* global-ip-cache.h */,
4D36BA630CA2F00800A63CA5 /* handshake.cc */,
4D36BA640CA2F00800A63CA5 /* handshake.h */,
A209EE5B1144B51E002B02D1 /* history.h */,
@ -2298,6 +2304,7 @@
CAB35C64252F6F5E00552A55 /* mime-types.h in Headers */,
2856E0656A49F2665D69E760 /* benc.h in Headers */,
E975121263DD973CAF4AEBA0 /* timer.h in Headers */,
EDBAAC8C29E486BC00D9495F /* global-ip-cache.h in Headers */,
E975121263DD973CAF4AEBA2 /* timer-ev.h in Headers */,
C1077A4F183EB29600634C22 /* error.h in Headers */,
A2679295130E00A000CB7464 /* tr-utp.h in Headers */,
@ -3027,6 +3034,7 @@
files = (
BEFC1E2B0C07861A00B0BB3C /* utils.cc in Sources */,
A2AAB65F0DE0CF6200E04DDA /* rpcimpl.cc in Sources */,
EDBAAC8E29E486C200D9495F /* global-ip-cache.cc in Sources */,
BEFC1E2D0C07861A00B0BB3C /* port-forwarding-upnp.cc in Sources */,
A2AAB65C0DE0CF6200E04DDA /* rpc-server.cc in Sources */,
ED8A16402735A8AA000D61F9 /* peer-mgr-active-requests.cc in Sources */,

View File

@ -92,8 +92,8 @@ Here is a sample of the three basic types: respectively Boolean, Number and Stri
* **utp-enabled:** Boolean (default = true) Enable [Micro Transport Protocol (µTP)](https://en.wikipedia.org/wiki/Micro_Transport_Protocol)
#### Peers
* **bind-address-ipv4:** String (default = "0.0.0.0") Where to listen for peer connections. When no valid IPv4 address is provided, Transmission will default to "0.0.0.0".
* **bind-address-ipv6:** String (default = "::") Where to listen for peer connections. When no valid IPv6 address is provided, Transmission will determine your default public IPv6 address and use it.
* **bind-address-ipv4:** String (default = "0.0.0.0") Where to listen for peer connections. When no valid IPv4 address is provided, Transmission will bind to "0.0.0.0".
* **bind-address-ipv6:** String (default = "::") Where to listen for peer connections. When no valid IPv6 address is provided, Transmission will try to bind to your default global IPv6 address. If that didn't work, then Transmission will bind to "::".
* **peer-congestion-algorithm:** String. This is documented on https://www.pps.jussieu.fr/~jch/software/bittorrent/tcp-congestion-control.html.
* **peer-limit-global:** Number (default = 240)
* **peer-limit-per-torrent:** Number (default = 60)

View File

@ -57,6 +57,8 @@ target_sources(${TR_NAME}
file-win32.cc
file.cc
file.h
global-ip-cache.cc
global-ip-cache.h
handshake.cc
handshake.h
history.h

View File

@ -1006,7 +1006,7 @@ void tr_announcer_impl::onAnnounceDone(
if (response.external_ip)
{
session->setExternalIP(*response.external_ip);
session->set_global_address(*response.external_ip);
}
if (!response.did_connect)

View File

@ -0,0 +1,363 @@
// This file Copyright © 2023-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.
#include <algorithm> // std::all_of
#include <chrono>
#include <cstddef>
#include <string_view>
#include <utility> // std::move
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <cerrno>
#include <sys/socket.h>
#endif
#include <fmt/core.h>
#include "libtransmission/log.h"
#include "libtransmission/global-ip-cache.h"
#include "libtransmission/tr-assert.h"
#include "libtransmission/utils.h"
namespace
{
using namespace std::literals;
auto constexpr IPQueryServices = std::array{ "https://icanhazip.com"sv, "https://api64.ipify.org"sv };
auto constexpr UpkeepInterval = 30min;
auto constexpr RetryUpkeepInterval = 30s;
} // namespace
namespace
{
// Functions contained in external_source_ip_helpers are modified from code
// by Juliusz Chroboczek and is covered under the same license as dht.cc.
// Please feel free to copy them into your software if it can help
// unbreaking the double-stack Internet.
namespace external_source_ip_helpers
{
// Get the source address used for a given destination address.
// Since there is no official interface to get this information,
// we create a connected UDP socket (connected UDP... hmm...)
// and check its source address.
//
// Since it's a UDP socket, this doesn't actually send any packets
[[nodiscard]] std::optional<tr_address> get_source_address(
tr_address const& dst_addr,
tr_port dst_port,
tr_address const& bind_addr,
int& err_out) noexcept
{
TR_ASSERT(dst_addr.type == bind_addr.type);
auto const save = errno;
auto const [dst_ss, dst_sslen] = dst_addr.to_sockaddr(dst_port);
auto const [bind_ss, bind_sslen] = bind_addr.to_sockaddr(tr_port::fromHost(0));
if (auto const sock = socket(dst_ss.ss_family, SOCK_DGRAM, 0); sock != TR_BAD_SOCKET)
{
if (bind(sock, reinterpret_cast<sockaddr const*>(&bind_ss), bind_sslen) == 0)
{
if (connect(sock, reinterpret_cast<sockaddr const*>(&dst_ss), dst_sslen) == 0)
{
auto src_ss = sockaddr_storage{};
auto src_sslen = socklen_t{ sizeof(src_ss) };
if (getsockname(sock, reinterpret_cast<sockaddr*>(&src_ss), &src_sslen) == 0)
{
if (auto const addrport = tr_address::from_sockaddr(reinterpret_cast<sockaddr*>(&src_ss)); addrport)
{
tr_net_close_socket(sock);
errno = save;
return addrport->first;
}
}
}
}
tr_net_close_socket(sock);
}
err_out = errno;
errno = save;
return {};
}
[[nodiscard]] std::optional<tr_address> get_global_source_address(tr_address const& bind_addr, int& err_out) noexcept
{
// Pick some destination address to pretend to send a packet to
static auto constexpr DstIPv4 = "91.121.74.28"sv;
static auto constexpr DstIPv6 = "2001:1890:1112:1::20"sv;
auto const dst_addr = tr_address::from_string(bind_addr.is_ipv4() ? DstIPv4 : DstIPv6);
auto const dst_port = tr_port::fromHost(6969);
// In order for address selection to work right,
// this should be a global unicast address, not Teredo or 6to4
TR_ASSERT(dst_addr && dst_addr->is_global_unicast_address());
if (dst_addr)
{
return get_source_address(*dst_addr, dst_port, bind_addr, err_out);
}
return {};
}
} // namespace external_source_ip_helpers
} // namespace
tr_global_ip_cache::tr_global_ip_cache(tr_web& web_in, libtransmission::TimerMaker& timer_maker_in)
: web_{ web_in }
, upkeep_timers_{ timer_maker_in.create(), timer_maker_in.create() }
{
static_assert(TR_AF_INET == 0);
static_assert(TR_AF_INET6 == 1);
static_assert(NUM_TR_AF_INET_TYPES == 2);
for (std::size_t i = 0; i < NUM_TR_AF_INET_TYPES; ++i)
{
auto const type = static_cast<tr_address_type>(i);
auto const cb = [this, type]()
{
update_addr(type);
};
upkeep_timers_[i]->setCallback(cb);
start_timer(type, UpkeepInterval);
}
}
tr_global_ip_cache::~tr_global_ip_cache()
{
// Destroying mutex while someone owns it is undefined behaviour, so we acquire it first
auto const locks = std::scoped_lock{ is_updating_mutex_[TR_AF_INET], is_updating_mutex_[TR_AF_INET6],
global_addr_mutex_[TR_AF_INET], global_addr_mutex_[TR_AF_INET6],
source_addr_mutex_[TR_AF_INET], source_addr_mutex_[TR_AF_INET6] };
TR_ASSERT(std::all_of(
std::begin(is_updating_),
std::end(is_updating_),
[](is_updating_t const& v) { return v == is_updating_t::ABORT; }));
}
bool tr_global_ip_cache::try_shutdown() noexcept
{
for (auto& timer : upkeep_timers_)
{
timer->stop();
}
for (std::size_t i = 0; i < NUM_TR_AF_INET_TYPES; ++i)
{
auto const lock = std::unique_lock{ is_updating_mutex_[i], std::try_to_lock };
if (!lock.owns_lock() || is_updating_[i] == is_updating_t::YES)
{
return false;
}
is_updating_[i] = is_updating_t::ABORT; // Abort any future updates
}
return true;
}
void tr_global_ip_cache::set_settings_bind_addr(tr_address_type type, std::string_view bind_address) noexcept
{
settings_bind_addr_[type] = tr_address::from_string(bind_address);
if (settings_bind_addr_[type] && type != settings_bind_addr_[type]->type)
{
settings_bind_addr_[type].reset();
}
update_addr(type);
}
tr_address tr_global_ip_cache::bind_addr(tr_address_type type) const noexcept
{
if (type == TR_AF_INET || type == TR_AF_INET6)
{
return settings_bind_addr_[type].value_or(type == TR_AF_INET ? tr_address::any_ipv4() : tr_address::any_ipv6());
}
TR_ASSERT_MSG(false, "invalid type");
return {};
}
void tr_global_ip_cache::update_addr(tr_address_type type) noexcept
{
update_source_addr(type);
/* TODO: Temporarily disable because there is currently no way for this to work without using third party services */
// if (global_source_addr(type))
// {
// update_global_addr(type);
// }
}
bool tr_global_ip_cache::set_global_addr(tr_address_type type, tr_address const& addr) noexcept
{
if (type == addr.type && addr.is_global_unicast_address())
{
auto const lock = std::lock_guard{ global_addr_mutex_[addr.type] };
global_addr_[addr.type] = addr;
tr_logAddTrace(fmt::format("Cached global address {}", addr.display_name()));
return true;
}
return false;
}
void tr_global_ip_cache::unset_global_addr(tr_address_type type) noexcept
{
auto const lock = std::lock_guard{ global_addr_mutex_[type] };
global_addr_[type].reset();
tr_logAddTrace(fmt::format("Unset {} global address cache", type == TR_AF_INET ? "IPv4"sv : "IPv6"sv));
}
void tr_global_ip_cache::set_source_addr(tr_address const& addr) noexcept
{
auto const lock = std::lock_guard{ source_addr_mutex_[addr.type] };
source_addr_[addr.type] = addr;
tr_logAddTrace(fmt::format("Cached source address {}", addr.display_name()));
}
void tr_global_ip_cache::unset_addr(tr_address_type type) noexcept
{
auto const lock = std::lock_guard{ source_addr_mutex_[type] };
source_addr_[type].reset();
tr_logAddTrace(fmt::format("Unset {} source address cache", type == TR_AF_INET ? "IPv4"sv : "IPv6"sv));
// No public internet connectivity means no global IP address
unset_global_addr(type);
}
bool tr_global_ip_cache::set_is_updating(tr_address_type type) noexcept
{
auto lock = std::unique_lock{ is_updating_mutex_[type] };
is_updating_cv_[type].wait(
lock,
[this, type]() { return is_updating_[type] == is_updating_t::NO || is_updating_[type] == is_updating_t::ABORT; });
if (is_updating_[type] != is_updating_t::NO)
{
return false;
}
is_updating_[type] = is_updating_t::YES;
lock.unlock();
is_updating_cv_[type].notify_one();
return true;
}
void tr_global_ip_cache::unset_is_updating(tr_address_type type) noexcept
{
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
auto lock = std::unique_lock{ is_updating_mutex_[type] };
is_updating_[type] = is_updating_t::NO;
lock.unlock();
is_updating_cv_[type].notify_one();
}
void tr_global_ip_cache::update_global_addr(tr_address_type type) noexcept
{
TR_ASSERT(has_ip_protocol_[type]);
TR_ASSERT(global_source_addr(type));
TR_ASSERT(ix_service_[type] < std::size(IPQueryServices));
if (ix_service_[type] == 0U && !set_is_updating(type))
{
return;
}
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
// Update global address
auto options = tr_web::FetchOptions{ IPQueryServices[ix_service_[type]],
[this, type](tr_web::FetchResponse const& response)
{ this->on_response_ip_query(type, response); },
nullptr };
options.ip_proto = type == TR_AF_INET ? tr_web::FetchOptions::IPProtocol::V4 : tr_web::FetchOptions::IPProtocol::V6;
options.sndbuf = 4096;
options.rcvbuf = 4096;
web_.fetch(std::move(options));
}
void tr_global_ip_cache::update_source_addr(tr_address_type type) noexcept
{
using namespace external_source_ip_helpers;
TR_ASSERT(has_ip_protocol_[type]);
if (!set_is_updating(type))
{
return;
}
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
auto const protocol = type == TR_AF_INET ? "IPv4"sv : "IPv6"sv;
auto err = int{};
auto const& source_addr = get_global_source_address(bind_addr(type), err);
if (source_addr)
{
set_source_addr(*source_addr);
tr_logAddInfo(fmt::format(
_("Successfully updated source {protocol} address to {ip}"),
fmt::arg("protocol", protocol),
fmt::arg("ip", source_addr->display_name())));
}
else
{
// Stop the update process since we have no public internet connectivity
unset_addr(type);
upkeep_timers_[type]->setInterval(RetryUpkeepInterval);
tr_logAddDebug(fmt::format(_("Couldn't obtain source {protocol} address"), fmt::arg("protocol", protocol)));
if (err == EAFNOSUPPORT)
{
stop_timer(type); // No point in retrying
has_ip_protocol_[type] = false;
tr_logAddInfo(fmt::format(_("Your machine does not support {protocol}"), fmt::arg("protocol", protocol)));
}
}
unset_is_updating(type);
}
void tr_global_ip_cache::on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept
{
TR_ASSERT(is_updating_[type] == is_updating_t::YES);
TR_ASSERT(ix_service_[type] < std::size(IPQueryServices));
auto const protocol = type == TR_AF_INET ? "IPv4"sv : "IPv6"sv;
auto success = bool{ false };
if (response.status == 200 /* HTTP_OK */)
{
// Update member
if (auto addr = tr_address::from_string(tr_strvStrip(response.body)); addr && set_global_addr(type, *addr))
{
success = true;
upkeep_timers_[type]->setInterval(UpkeepInterval);
tr_logAddInfo(fmt::format(
_("Successfully updated global {type} address to {ip} using {url}"),
fmt::arg("type", protocol),
fmt::arg("ip", addr->display_name()),
fmt::arg("url", IPQueryServices[ix_service_[type]])));
}
}
// Try next IP query URL
if (!success && ++ix_service_[type] < std::size(IPQueryServices))
{
update_global_addr(type);
return;
}
if (!success)
{
tr_logAddDebug(fmt::format("Couldn't obtain global {} address", protocol));
unset_global_addr(type);
upkeep_timers_[type]->setInterval(RetryUpkeepInterval);
}
ix_service_[type] = 0U;
unset_is_updating(type);
}

View File

@ -0,0 +1,133 @@
// This file Copyright © 2023-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.
#include <array>
#include <atomic>
#include <condition_variable>
#include <chrono> // std::chrono::milliseconds
#include <memory> // std::unique_ptr
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <string>
#include <string_view>
#include "libtransmission/net.h"
#include "libtransmission/timer.h"
#include "libtransmission/web.h"
#pragma once
#ifndef __TRANSMISSION__
#error only libtransmission should #include this header.
#endif
/**
* Cache global IP addresses.
*
* This class caches 3 useful info:
* 1. Whether your machine supports the IP protocol
* 2. Source address used for global connections
* 3. Global address
*
* The idea is, if this class successfully cached a source address, that means
* you have connectivity to the public internet. And if the global address is
* the same with the source address, then you are not behind an NAT.
*
*/
class tr_global_ip_cache
{
public:
tr_global_ip_cache(tr_web& web_in, libtransmission::TimerMaker& timer_maker_in);
tr_global_ip_cache() = delete;
~tr_global_ip_cache();
tr_global_ip_cache(tr_global_ip_cache const&) = delete;
tr_global_ip_cache(tr_global_ip_cache&&) = delete;
tr_global_ip_cache& operator=(tr_global_ip_cache const&) = delete;
tr_global_ip_cache& operator=(tr_global_ip_cache&&) = delete;
bool try_shutdown() noexcept;
[[nodiscard]] std::optional<tr_address> global_addr(tr_address_type type) const noexcept
{
auto const lock = std::shared_lock{ global_addr_mutex_[type] };
return global_addr_[type];
}
[[nodiscard]] std::optional<tr_address> global_source_addr(tr_address_type type) const noexcept
{
auto const lock = std::shared_lock{ source_addr_mutex_[type] };
return source_addr_[type];
}
void set_settings_bind_addr(tr_address_type type, std::string_view bind_address) noexcept;
[[nodiscard]] tr_address bind_addr(tr_address_type type) const noexcept;
void update_addr(tr_address_type type) noexcept;
bool set_global_addr(tr_address_type type, tr_address const& addr) noexcept;
[[nodiscard]] constexpr auto has_ip_protocol(tr_address_type type) const noexcept
{
return has_ip_protocol_[type];
}
private:
template<typename T>
using array_ip_t = std::array<T, NUM_TR_AF_INET_TYPES>;
void unset_global_addr(tr_address_type type) noexcept;
void set_source_addr(tr_address const& addr) noexcept;
void unset_addr(tr_address_type type) noexcept;
void start_timer(tr_address_type type, std::chrono::milliseconds msec) noexcept
{
upkeep_timers_[type]->startRepeating(msec);
}
void stop_timer(tr_address_type type) noexcept
{
upkeep_timers_[type]->stop();
}
[[nodiscard]] bool set_is_updating(tr_address_type type) noexcept;
void unset_is_updating(tr_address_type type) noexcept;
void update_global_addr(tr_address_type type) noexcept;
void update_source_addr(tr_address_type type) noexcept;
// Only use as a callback for web_->fetch()
void on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept;
tr_web& web_;
array_ip_t<std::optional<tr_address>> settings_bind_addr_;
enum class is_updating_t
{
NO,
YES,
ABORT
};
array_ip_t<is_updating_t> is_updating_ = {};
array_ip_t<std::mutex> is_updating_mutex_;
array_ip_t<std::condition_variable> is_updating_cv_;
// Never directly read/write IP addresses for the sake of being thread safe
// Use global_*_addr() for read, and set_*_addr()/unset_*_addr() for write instead
mutable array_ip_t<std::shared_mutex> global_addr_mutex_;
array_ip_t<std::optional<tr_address>> global_addr_;
mutable array_ip_t<std::shared_mutex> source_addr_mutex_;
array_ip_t<std::optional<tr_address>> source_addr_;
// Keep the timer at the bottom of the class definition so that it will be destructed first
// We don't want it to trigger after the IP addresses have been destroyed
// (The destructor will acquire the IP address locks before proceeding, but still)
array_ip_t<std::unique_ptr<libtransmission::Timer>> upkeep_timers_;
// Whether this machine supports this IP protocol
array_ip_t<bool> has_ip_protocol_ = { true, true };
array_ip_t<std::atomic_size_t> ix_service_ = {};
};

View File

@ -417,94 +417,6 @@ void tr_net_close_socket(tr_socket_t sockfd)
evutil_closesocket(sockfd);
}
namespace
{
// code in global_ipv6_herlpers is written by Juliusz Chroboczek
// and is covered under the same license as dht.cc.
// Please feel free to copy them into your software if it can help
// unbreaking the double-stack Internet.
namespace global_ipv6_helpers
{
// Get the source address used for a given destination address.
// Since there is no official interface to get this information,
// we create a connected UDP socket (connected UDP... hmm...)
// and check its source address.
//
// Since it's a UDP socket, this doesn't actually send any packets
[[nodiscard]] std::optional<tr_address> get_source_address(tr_address const& dst_addr, tr_port dst_port)
{
auto const save = errno;
auto const [dst_ss, dst_sslen] = dst_addr.to_sockaddr(dst_port);
if (auto const sock = socket(dst_ss.ss_family, SOCK_DGRAM, 0); sock != TR_BAD_SOCKET)
{
if (connect(sock, reinterpret_cast<sockaddr const*>(&dst_ss), dst_sslen) == 0)
{
auto src_ss = sockaddr_storage{};
auto src_sslen = socklen_t{ sizeof(src_ss) };
if (getsockname(sock, reinterpret_cast<sockaddr*>(&src_ss), &src_sslen) == 0)
{
if (auto const addrport = tr_address::from_sockaddr(reinterpret_cast<sockaddr*>(&src_ss)); addrport)
{
evutil_closesocket(sock);
errno = save;
return addrport->first;
}
}
}
evutil_closesocket(sock);
}
errno = save;
return {};
}
[[nodiscard]] std::optional<tr_address> global_address(int af)
{
// Pick some destination address to pretend to send a packet to
static auto constexpr DstIPv4 = "91.121.74.28"sv;
static auto constexpr DstIPv6 = "2001:1890:1112:1::20"sv;
auto const dst_addr = tr_address::from_string(af == AF_INET ? DstIPv4 : DstIPv6);
auto const dst_port = tr_port::fromHost(6969);
// In order for address selection to work right,
// this should be a native IPv6 address, not Teredo or 6to4
TR_ASSERT(dst_addr.has_value() && dst_addr->is_global_unicast_address());
if (dst_addr)
{
if (auto addr = get_source_address(*dst_addr, dst_port); addr && addr->is_global_unicast_address())
{
return addr;
}
}
return {};
}
} // namespace global_ipv6_helpers
} // namespace
/* Return our global IPv6 address, with caching. */
std::optional<tr_address> tr_globalIPv6()
{
using namespace global_ipv6_helpers;
// recheck our cached value every half hour
static auto constexpr CacheSecs = 1800;
static auto cache_val = std::optional<tr_address>{};
static auto cache_expires_at = time_t{};
if (auto const now = tr_time(); cache_expires_at <= now)
{
cache_expires_at = now + CacheSecs;
cache_val = global_address(AF_INET6);
}
return cache_val;
}
// ---
namespace

View File

@ -327,7 +327,7 @@ void tr_netSetCongestionControl(tr_socket_t s, char const* algorithm);
void tr_net_close_socket(tr_socket_t fd);
bool tr_net_hasIPv6(tr_port);
[[nodiscard]] bool tr_net_hasIPv6(tr_port);
// --- TOS / DSCP
@ -402,5 +402,3 @@ void tr_netSetTOS(tr_socket_t sock, int tos, tr_address_type type);
* @param err an errno on Unix/Linux and an WSAError on win32)
*/
[[nodiscard]] std::string tr_net_strerror(int err);
[[nodiscard]] std::optional<tr_address> tr_globalIPv6();

View File

@ -41,6 +41,7 @@
#include "libtransmission/error-types.h"
#include "libtransmission/error.h"
#include "libtransmission/file.h"
#include "libtransmission/global-ip-cache.h"
#include "libtransmission/log.h"
#include "libtransmission/net.h"
#include "libtransmission/peer-io.h"
@ -422,17 +423,16 @@ tr_address tr_session::publicAddress(tr_address_type type) const noexcept
{
// if user provided an address, use it.
// otherwise, use any_ipv4 (0.0.0.0).
return tr_address::from_string(settings_.bind_address_ipv4).value_or(tr_address::any_ipv4());
return global_ip_cache_->bind_addr(type);
}
if (type == TR_AF_INET6)
{
// if user provided an address, use it.
// otherwise, if we can determine which one to use via tr_globalIPv6 magic, use it.
// otherwise, if we can determine which one to use via globalSourceIPv6 magic, use it.
// otherwise, use any_ipv6 (::).
static auto constexpr AnyAddr = tr_address::any_ipv6();
auto const default_addr = tr_globalIPv6().value_or(AnyAddr);
return tr_address::from_string(settings_.bind_address_ipv6).value_or(default_addr);
auto const source_addr = global_source_address(type);
return source_addr && source_addr->is_global_unicast_address() ? *source_addr : global_ip_cache_->bind_addr(type);
}
TR_ASSERT_MSG(false, "invalid type");
@ -639,6 +639,8 @@ void tr_session::initImpl(init_data& data)
this->blocklists_ = libtransmission::Blocklist::loadBlocklists(blocklist_dir_, useBlocklist());
this->global_ip_cache_ = std::make_unique<tr_global_ip_cache>(*web_, timerMaker());
tr_logAddInfo(fmt::format(_("Transmission version {version} starting"), fmt::arg("version", LONG_VERSION_STRING)));
setSettings(client_settings, true);
@ -697,6 +699,15 @@ void tr_session::setSettings(tr_session_settings&& settings_in, bool force)
tr_sessionSetCacheLimit_MB(this, val);
}
if (auto const& val = new_settings.bind_address_ipv4; force || val != old_settings.bind_address_ipv4)
{
global_ip_cache_->set_settings_bind_addr(TR_AF_INET, new_settings.bind_address_ipv4);
}
if (auto const& val = new_settings.bind_address_ipv6; force || val != old_settings.bind_address_ipv6)
{
global_ip_cache_->set_settings_bind_addr(TR_AF_INET6, new_settings.bind_address_ipv6);
}
if (auto const& val = new_settings.default_trackers_str; force || val != old_settings.default_trackers_str)
{
setDefaultTrackers(val);
@ -1279,7 +1290,11 @@ void tr_session::closeImplPart1(std::promise<void>* closed_promise, std::chrono:
// Tell the announcer to start shutdown, which sends out the stop
// events and stops scraping.
this->announcer_->startShutdown();
// ...and now that those are queued, tell web_ that we're shutting
// ...since global_ip_cache_ relies on web_ to update global addresses,
// we tell it to stop updating before web_ starts to refuse new requests.
// But we keep it intact for now, so that udp_core_ can continue.
this->global_ip_cache_->try_shutdown();
// ...and now that those are done, tell web_ that we're shutting
// down soon. This leaves the `event=stopped` going but refuses any
// new tasks.
this->web_->startShutdown(10s);
@ -1294,8 +1309,10 @@ void tr_session::closeImplPart1(std::promise<void>* closed_promise, std::chrono:
void tr_session::closeImplPart2(std::promise<void>* closed_promise, std::chrono::time_point<std::chrono::steady_clock> deadline)
{
// try to keep the UDP announcer alive long enough to send out
// all the &event=stopped tracker announces
if (n_pending_stops_ != 0U && std::chrono::steady_clock::now() < deadline)
// all the &event=stopped tracker announces.
// also wait for all ip cache updates to finish so that web_ can
// safely destruct.
if ((n_pending_stops_ != 0U || !global_ip_cache_->try_shutdown()) && std::chrono::steady_clock::now() < deadline)
{
announcer_udp_->upkeep();
return;
@ -2035,6 +2052,22 @@ void tr_session::closeTorrentFile(tr_torrent* tor, tr_file_index_t file_num) noe
// ---
std::string tr_session::bindAddress(tr_address_type type) const noexcept
{
switch (type)
{
case TR_AF_INET:
return settings_.bind_address_ipv4;
case TR_AF_INET6:
return settings_.bind_address_ipv6;
default:
TR_ASSERT_MSG(false, "invalid type");
return {};
}
}
// ---
void tr_sessionSetQueueStartCallback(tr_session* session, void (*callback)(tr_session*, tr_torrent*, void*), void* user_data)
{
session->setQueueStartCallback(callback, user_data);

View File

@ -33,6 +33,7 @@
#include "bandwidth.h"
#include "bitfield.h"
#include "cache.h"
#include "global-ip-cache.h"
#include "interned-string.h"
#include "net.h" // tr_socket_t
#include "open-files.h"
@ -464,11 +465,6 @@ public:
[[nodiscard]] bool useRpcWhitelist() const;
void setExternalIP(tr_address external_ip)
{
external_ip_ = external_ip;
}
// peer networking
[[nodiscard]] constexpr auto const& peerCongestionAlgorithm() const noexcept
@ -510,6 +506,10 @@ public:
void closeTorrentFiles(tr_torrent* tor) noexcept;
void closeTorrentFile(tr_torrent* tor, tr_file_index_t file_num) noexcept;
// bind address
[[nodiscard]] std::string bindAddress(tr_address_type type) const noexcept;
// announce ip
[[nodiscard]] constexpr std::string const& announceIP() const noexcept
@ -777,8 +777,31 @@ public:
[[nodiscard]] bool addressIsBlocked(tr_address const& addr) const noexcept;
[[nodiscard]] bool has_ip_protocol(tr_address_type type) const noexcept
{
TR_ASSERT(type == TR_AF_INET || type == TR_AF_INET6);
return global_ip_cache_->has_ip_protocol(type);
}
[[nodiscard]] tr_address publicAddress(tr_address_type type) const noexcept;
[[nodiscard]] std::optional<tr_address> global_address(tr_address_type type) const noexcept
{
TR_ASSERT(type == TR_AF_INET || type == TR_AF_INET6);
return global_ip_cache_->global_addr(type);
}
bool set_global_address(tr_address const& addr) noexcept
{
return global_ip_cache_->set_global_addr(addr.type, addr);
}
[[nodiscard]] std::optional<tr_address> global_source_address(tr_address_type type) const noexcept
{
TR_ASSERT(type == TR_AF_INET || type == TR_AF_INET6);
return global_ip_cache_->global_source_addr(type);
}
[[nodiscard]] constexpr auto speedLimitKBps(tr_direction dir) const noexcept
{
return dir == TR_DOWN ? settings_.speed_limit_down : settings_.speed_limit_up;
@ -905,7 +928,6 @@ private:
static void onIncomingPeerConnection(tr_socket_t fd, void* vsession);
friend class libtransmission::test::SessionTest;
friend struct tr_bindinfo;
friend bool tr_blocklistExists(tr_session const* session);
friend bool tr_sessionGetAntiBruteForceEnabled(tr_session const* session);
@ -1002,7 +1024,6 @@ private:
/// trivial type fields
tr_session_settings settings_;
std::optional<tr_address> external_ip_;
queue_start_callback_t queue_start_callback_ = nullptr;
void* queue_start_user_data_ = nullptr;
@ -1060,14 +1081,14 @@ private:
/// other fields
// depends-on: session_thread_, settings_.bind_address_ipv4, local_peer_port_
// depends-on: session_thread_, settings_.bind_address_ipv4, local_peer_port_, global_ip_cache (via tr_session::publicAddress())
std::optional<BoundSocket> bound_ipv4_;
// depends-on: session_thread_, settings_.bind_address_ipv6, local_peer_port_
// depends-on: session_thread_, settings_.bind_address_ipv6, local_peer_port_, global_ip_cache (via tr_session::publicAddress())
std::optional<BoundSocket> bound_ipv6_;
public:
// depends-on: settings_, announcer_udp_
// depends-on: settings_, announcer_udp_, global_ip_cache_
// FIXME(ckerr): circular dependency udp_core -> announcer_udp -> announcer_udp_mediator -> udp_core
std::unique_ptr<tr_udp_core> udp_core_;
@ -1094,7 +1115,10 @@ private:
// depends-on: open_files_
tr_torrents torrents_;
// depends-on: settings_, session_thread_, torrents_
// depends-on: settings_, session_thread_, timer_maker_, web_
std::unique_ptr<tr_global_ip_cache> global_ip_cache_;
// depends-on: settings_, session_thread_, torrents_, global_ip_cache (via tr_session::publicAddress())
WebMediator web_mediator_{ this };
std::unique_ptr<tr_web> web_ = tr_web::create(this->web_mediator_);

View File

@ -238,6 +238,7 @@ tr_session::tr_udp_core::~tr_udp_core()
void tr_session::tr_udp_core::sendto(void const* buf, size_t buflen, struct sockaddr const* to, socklen_t const tolen) const
{
auto const addrport = tr_address::from_sockaddr(to);
if (to->sa_family != AF_INET && to->sa_family != AF_INET6)
{
errno = EAFNOSUPPORT;
@ -247,13 +248,20 @@ void tr_session::tr_udp_core::sendto(void const* buf, size_t buflen, struct sock
// don't warn on bad sockets; the system may not support IPv6
return;
}
else if (
addrport && addrport->first.is_global_unicast_address() &&
!session_.global_source_address(to->sa_family == AF_INET ? TR_AF_INET : TR_AF_INET6))
{
// don't try to connect to a global address if we don't have connectivity to public internet
return;
}
else if (::sendto(sock, static_cast<char const*>(buf), buflen, 0, to, tolen) != -1)
{
return;
}
auto display_name = std::string{};
if (auto const addrport = tr_address::from_sockaddr(to); addrport)
if (addrport)
{
auto const& [addr, port] = *addrport;
display_name = addr.display_name(port);

View File

@ -22,6 +22,7 @@ target_sources(libtransmission-test
file-piece-map-test.cc
file-test.cc
getopt-test.cc
global-ip-cache-test.cc
handshake-test.cc
history-test.cc
json-test.cc

View File

@ -0,0 +1,195 @@
// This file Copyright © 2023-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.
#include <array>
#include <chrono>
#include <ctime>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <libtransmission/global-ip-cache.h>
#include <libtransmission/net.h>
#include <libtransmission/timer.h>
#include <libtransmission/web.h>
#include "gtest/gtest.h"
using namespace std::literals;
class GlobalIPCacheTest : public ::testing::Test
{
protected:
class MockMediator final : public tr_web::Mediator
{
[[nodiscard]] time_t now() const override
{
return std::time(nullptr);
}
};
class MockTimer final : public libtransmission::Timer
{
void stop() override
{
}
void setCallback(std::function<void()> /* callback */) override
{
}
void setRepeating(bool /* is_repeating */ = true) override
{
}
void setInterval(std::chrono::milliseconds /* msec */) override
{
}
void start() override
{
}
[[nodiscard]] std::chrono::milliseconds interval() const noexcept override
{
return {};
}
[[nodiscard]] bool isRepeating() const noexcept override
{
return {};
}
};
class MockTimerMaker final : public libtransmission::TimerMaker
{
public:
[[nodiscard]] std::unique_ptr<libtransmission::Timer> create() override
{
return std::make_unique<MockTimer>();
}
};
void SetUp() override
{
::testing::Test::SetUp();
web_->startShutdown(std::chrono::milliseconds::max()); // Prevent sending actual HTTP requests
global_ip_cache_ = std::make_unique<tr_global_ip_cache>(*web_, timer_maker_);
}
void TearDown() override
{
::testing::Test::TearDown();
global_ip_cache_->try_shutdown();
}
std::unique_ptr<tr_global_ip_cache> global_ip_cache_;
MockMediator mediator_{};
std::unique_ptr<tr_web> web_ = tr_web::create(mediator_);
MockTimerMaker timer_maker_{};
};
TEST_F(GlobalIPCacheTest, bindAddr)
{
static auto const Ipv4Tests = std::array<std::pair<std::string, std::string_view>, 4>{
{ { "8.8.8.8"s, "8.8.8.8"sv },
{ "192.168.133.133"s, "192.168.133.133"sv },
{ "2001:1890:1112:1::20"s, "0.0.0.0"sv },
{ "asdasd"s, "0.0.0.0"sv } }
};
static auto const Ipv6Tests = std::array<std::pair<std::string, std::string_view>, 4>{
{ { "fd12:3456:789a:1::1"s, "fd12:3456:789a:1::1"sv },
{ "192.168.133.133"s, "::"sv },
{ "2001:1890:1112:1::20"s, "2001:1890:1112:1::20"sv },
{ "asdasd"s, "::"sv } }
};
// IPv4
for (auto const& [addr_str, expected] : Ipv4Tests)
{
global_ip_cache_->set_settings_bind_addr(TR_AF_INET, addr_str);
auto const addr = global_ip_cache_->bind_addr(TR_AF_INET);
EXPECT_EQ(addr.display_name(), expected);
}
// IPv6
for (auto const& [addr_str, expected] : Ipv6Tests)
{
global_ip_cache_->set_settings_bind_addr(TR_AF_INET6, addr_str);
auto const addr = global_ip_cache_->bind_addr(TR_AF_INET6);
EXPECT_EQ(addr.display_name(), expected);
}
}
TEST_F(GlobalIPCacheTest, setGlobalAddr)
{
auto constexpr AddrStr = std::array{ "8.8.8.8"sv,
"192.168.133.133"sv,
"172.16.241.133"sv,
"2001:1890:1112:1::20"sv,
"fd12:3456:789a:1::1"sv };
auto constexpr IPv4Tests = std::array{ true, false, false, false, false };
auto constexpr IPv6Tests = std::array{ false, false, false, true, false };
static_assert(std::size(AddrStr) == std::size(IPv4Tests));
static_assert(std::size(AddrStr) == std::size(IPv6Tests));
// IPv4
for (std::size_t i = 0; i < std::size(AddrStr); ++i)
{
auto const addr = tr_address::from_string(AddrStr[i]);
ASSERT_TRUE(addr);
EXPECT_EQ(global_ip_cache_->set_global_addr(TR_AF_INET, *addr), IPv4Tests[i]);
if (IPv4Tests[i])
{
EXPECT_EQ(global_ip_cache_->global_addr(TR_AF_INET)->display_name(), AddrStr[i]);
}
}
// IPv6
for (std::size_t i = 0; i < std::size(AddrStr); ++i)
{
auto const addr = tr_address::from_string(AddrStr[i]);
ASSERT_TRUE(addr);
EXPECT_EQ(global_ip_cache_->set_global_addr(TR_AF_INET6, *addr), IPv6Tests[i]);
if (IPv6Tests[i])
{
EXPECT_EQ(global_ip_cache_->global_addr(TR_AF_INET6)->display_name(), AddrStr[i]);
}
}
}
TEST_F(GlobalIPCacheTest, globalSourceIPv4)
{
global_ip_cache_->set_settings_bind_addr(TR_AF_INET, "0.0.0.0"s);
auto const addr = global_ip_cache_->global_source_addr(TR_AF_INET);
if (!addr)
{
GTEST_SKIP() << "globalSourceIPv4 did not return an address, either:\n"
<< "1. globalSourceIPv4 is broken\n"
<< "2. Your system does not support IPv4\n"
<< "3. You don't have IPv4 connectivity to public internet";
}
EXPECT_TRUE(addr->is_ipv4());
}
TEST_F(GlobalIPCacheTest, globalSourceIPv6)
{
global_ip_cache_->set_settings_bind_addr(TR_AF_INET6, "::"s);
auto const addr = global_ip_cache_->global_source_addr(TR_AF_INET6);
if (!addr)
{
GTEST_SKIP() << "globalSourceIPv6 did not return an address, either:\n"
<< "1. globalSourceIPv6 is broken\n"
<< "2. Your system does not support IPv6\n"
<< "3. You don't have IPv6 connectivity to public internet";
}
EXPECT_TRUE(addr->is_ipv6());
}

View File

@ -179,9 +179,3 @@ TEST_F(NetTest, isGlobalUnicastAddress)
EXPECT_EQ(expected, address->is_global_unicast_address()) << presentation;
}
}
TEST_F(NetTest, globalIPv6)
{
auto const addr = tr_globalIPv6();
EXPECT_TRUE(!addr || addr->is_global_unicast_address());
}