diff --git a/libtransmission/announcer-common.h b/libtransmission/announcer-common.h index caba49332..156392bf2 100644 --- a/libtransmission/announcer-common.h +++ b/libtransmission/announcer-common.h @@ -157,10 +157,10 @@ struct tr_announce_response * This is only an upper bound: if the tracker complains about * length, announcer will incrementally lower the batch size. */ -auto inline constexpr TR_MULTISCRAPE_MAX = 60; +auto inline constexpr TrMultiscrapeMax = 60; -auto inline constexpr TR_ANNOUNCE_TIMEOUT_SEC = std::chrono::seconds{ 45 }; -auto inline constexpr TR_SCRAPE_TIMEOUT_SEC = std::chrono::seconds{ 30 }; +auto inline constexpr TrAnnounceTimeoutSec = std::chrono::seconds{ 45 }; +auto inline constexpr TrScrapeTimeoutSec = std::chrono::seconds{ 30 }; struct tr_scrape_request { @@ -171,7 +171,7 @@ struct tr_scrape_request std::string log_name; /* info hashes of the torrents to scrape */ - std::array info_hash; + std::array info_hash; /* how many hashes to use in the info_hash field */ int info_hash_count = 0; @@ -209,7 +209,7 @@ struct tr_scrape_response int row_count; /* the individual torrents' scrape results */ - std::array rows; + std::array rows; /* the raw scrape url */ tr_interned_string scrape_url; diff --git a/libtransmission/announcer-http.cc b/libtransmission/announcer-http.cc index dafd6053f..cfb44022f 100644 --- a/libtransmission/announcer-http.cc +++ b/libtransmission/announcer-http.cc @@ -265,7 +265,7 @@ void tr_tracker_http_announce( auto url = tr_urlbuf{}; announce_url_new(url, session, request); auto options = tr_web::FetchOptions{ url.sv(), onAnnounceDone, d }; - options.timeout_secs = TR_ANNOUNCE_TIMEOUT_SEC; + options.timeout_secs = TrAnnounceTimeoutSec; options.sndbuf = 4096; options.rcvbuf = 4096; @@ -542,7 +542,7 @@ void tr_tracker_http_scrape(tr_session const* session, tr_scrape_request const& scrape_url_new(scrape_url, request); tr_logAddTrace(fmt::format("Sending scrape to libcurl: '{}'", scrape_url), request.log_name); auto options = tr_web::FetchOptions{ scrape_url, onScrapeDone, d }; - options.timeout_secs = TR_SCRAPE_TIMEOUT_SEC; + options.timeout_secs = TrScrapeTimeoutSec; options.sndbuf = 4096; options.rcvbuf = 4096; session->fetch(std::move(options)); diff --git a/libtransmission/announcer-udp.cc b/libtransmission/announcer-udp.cc index a6b27451c..d4ca41a72 100644 --- a/libtransmission/announcer-udp.cc +++ b/libtransmission/announcer-udp.cc @@ -6,7 +6,6 @@ #include // for std::find_if() #include #include // operator""ms, literals -#include // CHAR_BIT #include // std::byte #include // uint32_t, uint64_t #include // memcpy() @@ -17,8 +16,8 @@ #include #include #include +#include #include -#include #ifdef _WIN32 #include @@ -55,13 +54,15 @@ namespace { using namespace std::literals; -// size defined by bep15 +// size defined by https://www.bittorrent.org/beps/bep_0015.html using tau_connection_t = uint64_t; using tau_transaction_t = uint32_t; using InBuf = libtransmission::BufferReader; using PayloadBuffer = libtransmission::StackBuffer<4096, std::byte>; +using ipp_t = std::underlying_type_t; + constexpr auto TauConnectionTtlSecs = time_t{ 45 }; auto tau_transaction_new() @@ -69,7 +70,8 @@ auto tau_transaction_new() return tr_rand_obj(); } -// used in the "action" field of a request. Values defined in bep 15. +// used in the "action" field of a request. +// Values defined in https://www.bittorrent.org/beps/bep_0015.html enum tau_action_t : uint8_t { TAU_ACTION_CONNECT = 0, @@ -85,22 +87,20 @@ struct tau_scrape_request tau_scrape_request(tr_scrape_request const& in, tr_scrape_response_func on_response) : on_response_{ std::move(on_response) } { - this->response.scrape_url = in.scrape_url; - this->response.row_count = in.info_hash_count; - for (int i = 0; i < this->response.row_count; ++i) + response.scrape_url = in.scrape_url; + response.row_count = in.info_hash_count; + for (int i = 0; i < response.row_count; ++i) { - this->response.rows[i].info_hash = in.info_hash[i]; + response.rows[i].info_hash = in.info_hash[i]; } // build the payload - auto buf = PayloadBuffer{}; - buf.add_uint32(TAU_ACTION_SCRAPE); - buf.add_uint32(transaction_id); + payload.add_uint32(TAU_ACTION_SCRAPE); + payload.add_uint32(transaction_id); for (int i = 0; i < in.info_hash_count; ++i) { - buf.add(in.info_hash[i]); + payload.add(in.info_hash[i]); } - this->payload.insert(std::end(this->payload), std::begin(buf), std::end(buf)); } [[nodiscard]] auto has_callback() const noexcept @@ -108,7 +108,7 @@ struct tau_scrape_request return !!on_response_; } - void requestFinished() const + void request_finished() const { if (on_response_) { @@ -121,10 +121,10 @@ struct tau_scrape_request response.did_connect = did_connect; response.did_timeout = did_timeout; response.errmsg = errmsg; - requestFinished(); + request_finished(); } - void onResponse(tau_action_t action, InBuf& buf) + void on_response(tau_action_t action, InBuf& buf) { response.did_connect = true; response.did_timeout = false; @@ -139,7 +139,7 @@ struct tau_scrape_request row.leechers = buf.to_uint32(); } - requestFinished(); + request_finished(); } else { @@ -148,18 +148,20 @@ struct tau_scrape_request } } - [[nodiscard]] constexpr auto expiresAt() const noexcept + [[nodiscard]] constexpr auto expires_at() const noexcept { - return created_at_ + TR_SCRAPE_TIMEOUT_SEC.count(); + return created_at_ + TrScrapeTimeoutSec.count(); } - std::vector payload; + PayloadBuffer payload; time_t sent_at = 0; tau_transaction_t const transaction_id = tau_transaction_new(); tr_scrape_response response = {}; + static auto constexpr ip_protocol = TR_AF_UNSPEC; // NOLINT readability-identifier-naming + private: time_t const created_at_ = tr_time(); @@ -171,38 +173,39 @@ private: struct tau_announce_request { tau_announce_request( + tr_address_type ip_protocol_in, std::optional announce_ip, tr_announce_request const& in, tr_announce_response_func on_response) - : on_response_{ std::move(on_response) } + : ip_protocol{ ip_protocol_in } + , on_response_{ std::move(on_response) } { // https://www.bittorrent.org/beps/bep_0015.html sets key size at 32 bits - static_assert(sizeof(tr_announce_request::key) * CHAR_BIT == 32); + static_assert(sizeof(tr_announce_request::key) == sizeof(uint32_t)); response.info_hash = in.info_hash; // build the payload - auto buf = PayloadBuffer{}; - buf.add_uint32(TAU_ACTION_ANNOUNCE); - buf.add_uint32(transaction_id); - buf.add(in.info_hash); - buf.add(in.peer_id); - buf.add_uint64(in.down); - buf.add_uint64(in.leftUntilComplete); - buf.add_uint64(in.up); - buf.add_uint32(get_tau_announce_event(in.event)); + payload.add_uint32(TAU_ACTION_ANNOUNCE); + payload.add_uint32(transaction_id); + payload.add(in.info_hash); + payload.add(in.peer_id); + payload.add_uint64(in.down); + payload.add_uint64(in.leftUntilComplete); + payload.add_uint64(in.up); + payload.add_uint32(get_tau_announce_event(in.event)); if (announce_ip && announce_ip->is_ipv4()) { - buf.add_address(*announce_ip); + // Since size of IP field is only 4 bytes long, we can only announce IPv4 addresses + payload.add_address(*announce_ip); } else { - buf.add_uint32(0U); + payload.add_uint32(0U); } - buf.add_uint32(in.key); - buf.add_uint32(in.numwant); - buf.add_port(in.port); - payload.insert(std::end(payload), std::begin(buf), std::end(buf)); + payload.add_uint32(in.key); + payload.add_uint32(in.numwant); + payload.add_port(in.port); } [[nodiscard]] auto has_callback() const noexcept @@ -210,28 +213,28 @@ struct tau_announce_request return !!on_response_; } - void requestFinished() const + void request_finished() const { if (on_response_) { - on_response_(this->response); + on_response_(response); } } void fail(bool did_connect, bool did_timeout, std::string_view errmsg) { - this->response.did_connect = did_connect; - this->response.did_timeout = did_timeout; - this->response.errmsg = errmsg; - this->requestFinished(); + response.did_connect = did_connect; + response.did_timeout = did_timeout; + response.errmsg = errmsg; + request_finished(); } - void onResponse(tau_action_t action, InBuf& buf) + void on_response(tr_address_type ip_protocol_resp, tau_action_t action, InBuf& buf) { auto const buflen = std::size(buf); - this->response.did_connect = true; - this->response.did_timeout = false; + response.did_connect = true; + response.did_timeout = false; if (action == TAU_ACTION_ANNOUNCE && buflen >= 3 * sizeof(uint32_t)) { @@ -239,8 +242,18 @@ struct tau_announce_request response.leechers = buf.to_uint32(); response.seeders = buf.to_uint32(); - response.pex = tr_pex::from_compact_ipv4(std::data(buf), std::size(buf), nullptr, 0); - requestFinished(); + switch (ip_protocol_resp) + { + case TR_AF_INET: + response.pex = tr_pex::from_compact_ipv4(std::data(buf), std::size(buf), nullptr, 0); + break; + case TR_AF_INET6: + response.pex6 = tr_pex::from_compact_ipv6(std::data(buf), std::size(buf), nullptr, 0); + break; + default: + break; + } + request_finished(); } else { @@ -249,23 +262,24 @@ struct tau_announce_request } } - [[nodiscard]] constexpr auto expiresAt() const noexcept + [[nodiscard]] constexpr auto expires_at() const noexcept { - return created_at_ + TR_ANNOUNCE_TIMEOUT_SEC.count(); + return created_at_ + TrAnnounceTimeoutSec.count(); } enum tau_announce_event : uint8_t { + // https://www.bittorrent.org/beps/bep_0015.html // Used in the "event" field of an announce request. - // These values come from BEP 15 TAU_ANNOUNCE_EVENT_NONE = 0, TAU_ANNOUNCE_EVENT_COMPLETED = 1, TAU_ANNOUNCE_EVENT_STARTED = 2, TAU_ANNOUNCE_EVENT_STOPPED = 3 }; - std::vector payload; + PayloadBuffer payload; + tr_address_type const ip_protocol; time_t sent_at = 0; tau_transaction_t const transaction_id = tau_transaction_new(); @@ -303,97 +317,144 @@ struct tau_tracker tau_tracker( Mediator& mediator, - std::string_view const interned_authority, - std::string_view const interned_host, + std::string_view const authority_in, + std::string_view const host_in, + std::string_view const host_lookup_in, tr_port const port_in) - : authority{ interned_authority } - , host{ interned_host } + : authority{ authority_in } + , host{ host_in } + , host_lookup{ host_lookup_in } , port{ port_in } , mediator_{ mediator } { } - void sendto(std::byte const* buf, size_t buflen) + void sendto(tr_address_type ip_protocol, std::byte const* buf, size_t buflen) { - TR_ASSERT(addr_); - if (!addr_) + TR_ASSERT(tr_address::is_valid(ip_protocol)); + if (!tr_address::is_valid(ip_protocol)) { return; } - auto const& [ss, sslen] = *addr_; + TR_ASSERT(addr_[ip_protocol]); + if (!addr_[ip_protocol]) + { + return; + } + + auto const& [ss, sslen] = *addr_[ip_protocol]; mediator_.sendto(buf, buflen, reinterpret_cast(&ss), sslen); } - void on_connection_response(tau_action_t action, InBuf& buf) + void on_connection_response(tr_address_type ip_protocol, tau_action_t action, InBuf& buf) { - this->connecting_at = 0; - this->connection_transaction_id = 0; + TR_ASSERT(tr_address::is_valid(ip_protocol)); + if (!tr_address::is_valid(ip_protocol)) + { + return; + } + + connecting_at[ip_protocol] = 0; + connection_transaction_id[ip_protocol] = 0; if (action == TAU_ACTION_CONNECT) { - this->connection_id = buf.to_uint64(); - this->connection_expiration_time = tr_time() + TauConnectionTtlSecs; - logdbg(log_name(), fmt::format("Got a new connection ID from tracker: {}", this->connection_id)); + connection_id[ip_protocol] = buf.to_uint64(); + connection_expiration_time[ip_protocol] = tr_time() + TauConnectionTtlSecs; + logdbg( + log_name(), + fmt::format( + "Got a new {} connection ID from tracker: {}", + tr_ip_protocol_to_sv(ip_protocol), + connection_id[ip_protocol])); } else if (action == TAU_ACTION_ERROR) { - std::string errmsg = !std::empty(buf) ? buf.to_string() : _("Connection failed"); - this->failAll(true, false, errmsg); + std::string errmsg = !std::empty(buf) ? + buf.to_string() : + fmt::format(_("{ip_protocol} connection failed"), fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol))); + fail_all(true, false, errmsg); logdbg(log_name(), std::move(errmsg)); } - this->upkeep(); + upkeep(); } void upkeep(bool timeout_reqs = true) { time_t const now = tr_time(); - // do we have a DNS request that's ready? - if (addr_pending_dns_ && addr_pending_dns_->wait_for(0ms) == std::future_status::ready) + for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) { - addr_ = addr_pending_dns_->get(); - addr_pending_dns_.reset(); - addr_expires_at_ = now + DnsRetryIntervalSecs; + // do we have a DNS request that's ready? + if (addr_pending_dns_[ipp] && addr_pending_dns_[ipp]->wait_for(0ms) == std::future_status::ready) + { + addr_[ipp] = addr_pending_dns_[ipp]->get(); + addr_pending_dns_[ipp].reset(); + addr_expires_at_[ipp] = now + DnsRetryIntervalSecs; + } } - // are there any requests pending? - if (this->is_idle()) + // are there any tracker requests pending? + if (is_idle()) { return; } - // update the addr if our lookup is past its shelf date - if (!addr_pending_dns_ && addr_expires_at_ <= now) + for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) + { + // update the addr if our lookup is past its shelf date + if (!addr_pending_dns_[ipp] && addr_expires_at_[ipp] <= now) + { + addr_[ipp].reset(); + addr_pending_dns_[ipp] = std::async( + std::launch::async, + [this](tr_address_type ip_protocol) { return lookup(ip_protocol); }, + static_cast(ipp)); + } + } + + // are there any dns requests pending? + if (is_dns_pending()) { - addr_.reset(); - addr_pending_dns_ = std::async(std::launch::async, lookup, this->log_name(), this->host, this->port); return; } - logtrace( - log_name(), - fmt::format( - "connected {} ({} {}) -- connecting_at {}", - is_connected(now), - this->connection_expiration_time, - now, - this->connecting_at)); - - /* also need a valid connection ID... */ - if (addr_ && !is_connected(now) && this->connecting_at == 0) + for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) { - this->connecting_at = now; - this->connection_transaction_id = tau_transaction_new(); - logtrace(log_name(), fmt::format("Trying to connect. Transaction ID is {}", this->connection_transaction_id)); + auto const ipp_enum = static_cast(ipp); + logtrace( + log_name(), + fmt::format( + "{} connected {} ({} {}) -- connecting_at {}", + tr_ip_protocol_to_sv(ipp_enum), + is_connected(ipp_enum, now), + connection_expiration_time[ipp], + now, + connecting_at[ipp])); - auto buf = PayloadBuffer{}; - buf.add_uint64(0x41727101980LL); - buf.add_uint32(TAU_ACTION_CONNECT); - buf.add_uint32(this->connection_transaction_id); + // also need a valid connection ID... + if (addr_[ipp] && !is_connected(ipp_enum, now) && connecting_at[ipp] == 0) + { + TR_ASSERT(addr_[ipp]->first.ss_family == tr_ip_protocol_to_af(ipp_enum)); - this->sendto(std::data(buf), std::size(buf)); + connecting_at[ipp] = now; + connection_transaction_id[ipp] = tau_transaction_new(); + logtrace( + log_name(), + fmt::format( + "Trying to connect {}. Transaction ID is {}", + tr_ip_protocol_to_sv(ipp_enum), + connection_transaction_id[ipp])); + + auto buf = PayloadBuffer{}; + buf.add_uint64(0x41727101980LL); + buf.add_uint32(TAU_ACTION_CONNECT); + buf.add_uint32(connection_transaction_id[ipp]); + + sendto(ipp_enum, std::data(buf), std::size(buf)); + } } if (timeout_reqs) @@ -401,91 +462,113 @@ struct tau_tracker timeout_requests(now); } - if (addr_ && is_connected(now)) - { - send_requests(); - } + maybe_send_requests(now); } - [[nodiscard]] bool is_idle() const noexcept + [[nodiscard]] constexpr bool is_idle() const noexcept { - return std::empty(announces) && std::empty(scrapes) && !addr_pending_dns_; + return std::empty(announces) && std::empty(scrapes); } private: using Sockaddr = std::pair; using MaybeSockaddr = std::optional; - [[nodiscard]] constexpr bool is_connected(time_t now) const noexcept + [[nodiscard]] constexpr bool is_connected(tr_address_type ip_protocol, time_t now) const noexcept { - return connection_id != tau_connection_t{} && now < connection_expiration_time; + return connection_id[ip_protocol] != tau_connection_t{} && now < connection_expiration_time[ip_protocol]; } - [[nodiscard]] static MaybeSockaddr lookup( - std::string_view const interned_log_name, - std::string_view const interned_host, - tr_port const port) + [[nodiscard]] TR_CONSTEXPR20 bool is_dns_pending() const noexcept + { + return std::any_of(std::begin(addr_pending_dns_), std::end(addr_pending_dns_), [](auto const& o) { return !!o; }); + } + + [[nodiscard]] TR_CONSTEXPR20 bool has_addr() const noexcept + { + return std::any_of(std::begin(addr_), std::end(addr_), [](auto const& o) { return !!o; }); + } + + [[nodiscard]] MaybeSockaddr lookup(tr_address_type ip_protocol) { auto szport = std::array{}; *fmt::format_to(std::data(szport), "{:d}", port.host()) = '\0'; auto hints = addrinfo{}; - hints.ai_family = AF_INET; // https://github.com/transmission/transmission/issues/4719 + hints.ai_family = tr_ip_protocol_to_af(ip_protocol); hints.ai_protocol = IPPROTO_UDP; hints.ai_socktype = SOCK_DGRAM; addrinfo* info = nullptr; - auto const szhost = tr_pathbuf{ interned_host }; + auto const szhost = tr_urlbuf{ host_lookup }; if (int const rc = getaddrinfo(szhost.c_str(), std::data(szport), &hints, &info); rc != 0) { logwarn( - interned_log_name, + log_name(), fmt::format( - _("Couldn't look up '{address}:{port}': {error} ({error_code})"), - fmt::arg("address", interned_host), + _("Couldn't look up '{address}:{port}' in {ip_protocol}: {error} ({error_code})"), + fmt::arg("address", host), fmt::arg("port", port.host()), + fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol)), fmt::arg("error", gai_strerror(rc)), fmt::arg("error_code", static_cast(rc)))); return {}; } + auto const info_uniq = std::unique_ptr{ info, + [](addrinfo* p) // MSVC forced my hands + { + freeaddrinfo(p); + } }; - auto ss = sockaddr_storage{}; - auto const len = info->ai_addrlen; - memcpy(&ss, info->ai_addr, len); - freeaddrinfo(info); + // N.B. getaddrinfo() will return IPv4-mapped addresses by default on macOS + auto socket_address = tr_socket_address::from_sockaddr(info->ai_addr); + if (!socket_address || socket_address->address().is_ipv4_mapped_address()) + { + logdbg( + log_name(), + fmt::format( + "Couldn't look up '{address}:{port}' in {ip_protocol}: got invalid address", + fmt::arg("address", host), + fmt::arg("port", port.host()), + fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol)))); + return {}; + } - logdbg(interned_log_name, "DNS lookup succeeded"); - return std::make_pair(ss, len); + logdbg(log_name(), fmt::format("{} DNS lookup succeeded", tr_ip_protocol_to_sv(ip_protocol))); + return socket_address->to_sockaddr(); } - void failAll(bool did_connect, bool did_timeout, std::string_view errmsg) + void fail_all(bool did_connect, bool did_timeout, std::string_view errmsg) { - for (auto& req : this->scrapes) + for (auto& req : scrapes) { req.fail(did_connect, did_timeout, errmsg); } - for (auto& req : this->announces) + for (auto& req : announces) { req.fail(did_connect, did_timeout, errmsg); } - this->scrapes.clear(); - this->announces.clear(); + scrapes.clear(); + announces.clear(); } - /// + // --- void timeout_requests(time_t now) { - if (this->connecting_at != 0 && this->connecting_at + ConnectionRequestTtl < now) + for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) { - auto empty_buf = PayloadBuffer{}; - on_connection_response(TAU_ACTION_ERROR, empty_buf); + if (connecting_at[ipp] != 0 && connecting_at[ipp] + ConnectionRequestTtl < now) + { + auto empty_buf = PayloadBuffer{}; + on_connection_response(static_cast(ipp), TAU_ACTION_ERROR, empty_buf); + } } - timeout_requests(this->announces, now, "announce"); - timeout_requests(this->scrapes, now, "scrape"); + timeout_requests(announces, now, "announce"sv); + timeout_requests(scrapes, now, "scrape"sv); } template @@ -493,7 +576,7 @@ private: { for (auto it = std::begin(requests); it != std::end(requests);) { - if (auto& req = *it; req.expiresAt() <= now) + if (auto& req = *it; req.expires_at() <= now) { logtrace(log_name(), fmt::format("timeout {} req {}", name, fmt::ptr(&req))); req.fail(false, true, ""); @@ -506,37 +589,35 @@ private: } } - /// + // --- - void send_requests() + void maybe_send_requests(time_t now) { - TR_ASSERT(!addr_pending_dns_); - TR_ASSERT(addr_); - TR_ASSERT(this->connecting_at == 0); - TR_ASSERT(this->connection_expiration_time > tr_time()); + TR_ASSERT(!is_dns_pending()); + if (is_dns_pending() || !has_addr()) + { + return; + } - send_requests(this->announces); - send_requests(this->scrapes); + maybe_send_requests(announces, now); + maybe_send_requests(scrapes, now); } template - void send_requests(std::list& reqs) + void maybe_send_requests(std::list& reqs, time_t now) { - auto const now = tr_time(); - for (auto it = std::begin(reqs); it != std::end(reqs);) { auto& req = *it; - if (req.sent_at != 0) // it's already been sent; we're awaiting a response + if (req.sent_at != 0 || // it's already been sent; we're awaiting a response + !maybe_send_request(req.ip_protocol, std::data(req.payload), std::size(req.payload), now)) { ++it; continue; } - - logdbg(log_name(), fmt::format("sending req {}", fmt::ptr(&req))); + logdbg(log_name(), fmt::format("sent req {}", fmt::ptr(&req))); req.sent_at = now; - send_request(std::data(req.payload), std::size(req.payload)); if (req.has_callback()) { @@ -549,15 +630,24 @@ private: } } - void send_request(std::byte const* payload, size_t payload_len) + bool maybe_send_request(tr_address_type ip_protocol, std::byte const* payload, size_t payload_len, time_t now) { - logdbg(log_name(), fmt::format("sending request w/connection id {}", this->connection_id)); + for (uint8_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) + { + auto const ipp_enum = static_cast(ipp); + if (addr_[ipp] && (ip_protocol == TR_AF_UNSPEC || ipp == ip_protocol) && is_connected(ipp_enum, now)) + { + logdbg(log_name(), fmt::format("sending request w/connection id {}", connection_id[ipp])); - auto buf = PayloadBuffer{}; - buf.add_uint64(this->connection_id); - buf.add(payload, payload_len); + auto buf = PayloadBuffer{}; + buf.add_uint64(connection_id[ipp]); + buf.add(payload, payload_len); - this->sendto(std::data(buf), std::size(buf)); + sendto(ipp_enum, std::data(buf), std::size(buf)); + return true; + } + } + return false; } public: @@ -566,14 +656,15 @@ public: return authority; } - std::string_view const authority; // interned - std::string_view const host; // interned + std::string_view const authority; + std::string_view const host; + std::string_view const host_lookup; tr_port const port; - time_t connecting_at = 0; - time_t connection_expiration_time = 0; - tau_connection_t connection_id = {}; - tau_transaction_t connection_transaction_id = {}; + std::array connecting_at = {}; + std::array connection_expiration_time = {}; + std::array connection_id = {}; + std::array connection_transaction_id = {}; std::list announces; std::list scrapes; @@ -581,13 +672,13 @@ public: private: Mediator& mediator_; - std::optional> addr_pending_dns_; + std::array>, NUM_TR_AF_INET_TYPES> addr_pending_dns_; - MaybeSockaddr addr_; - time_t addr_expires_at_ = 0; + std::array addr_ = {}; + std::array addr_expires_at_ = {}; static constexpr auto DnsRetryIntervalSecs = time_t{ 3600 }; - static constexpr auto ConnectionRequestTtl = 30; + static constexpr auto ConnectionRequestTtl = time_t{ 30 }; }; // --- SESSION @@ -602,20 +693,22 @@ public: void announce(tr_announce_request const& request, tr_announce_response_func on_response) override { - auto* const tracker = getTrackerFromUrl(request.announce_url); + auto* const tracker = get_tracker_from_url(request.announce_url); if (tracker == nullptr) { return; } - // Since size of IP field is only 4 bytes long, we can only announce IPv4 addresses - tracker->announces.emplace_back(mediator_.announce_ip(), request, std::move(on_response)); + for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) + { + tracker->announces.emplace_back(static_cast(ipp), mediator_.announce_ip(), request, on_response); + } tracker->upkeep(false); } void scrape(tr_scrape_request const& request, tr_scrape_response_func on_response) override { - auto* const tracker = getTrackerFromUrl(request.scrape_url); + auto* const tracker = get_tracker_from_url(request.scrape_url); if (tracker == nullptr) { return; @@ -635,7 +728,7 @@ public: // @brief process an incoming udp message if it's a tracker response. // @return true if msg was a tracker response; false otherwise - bool handle_message(uint8_t const* msg, size_t msglen) override + bool handle_message(uint8_t const* msg, size_t msglen, struct sockaddr const* from, socklen_t /*fromlen*/) override { if (msglen < sizeof(uint32_t) * 2) { @@ -647,21 +740,24 @@ public: buf.add(msg, msglen); auto const action_id = static_cast(buf.to_uint32()); - if (!isResponseMessage(action_id, msglen)) + if (!is_response_message(action_id, msglen)) { return false; } - /* extract the transaction_id and look for a match */ + // extract the transaction_id and look for a match tau_transaction_t const transaction_id = buf.to_uint32(); + auto const socket_address = tr_socket_address::from_sockaddr(from); + auto const ip_protocol = socket_address ? socket_address->address().type : NUM_TR_AF_INET_TYPES; for (auto& tracker : trackers_) { // is it a connection response? - if (tracker.connecting_at != 0 && transaction_id == tracker.connection_transaction_id) + if (tr_address::is_valid(ip_protocol) && tracker.connecting_at[ip_protocol] != 0 && + transaction_id == tracker.connection_transaction_id[ip_protocol]) { logtrace(tracker.log_name(), fmt::format("{} is my connection request!", transaction_id)); - tracker.on_connection_response(action_id, buf); + tracker.on_connection_response(ip_protocol, action_id, buf); return true; } @@ -675,9 +771,8 @@ public: it != std::end(reqs)) { logtrace(tracker.log_name(), fmt::format("{} is an announce request!", transaction_id)); - auto req = *it; - it = reqs.erase(it); - req.onResponse(action_id, buf); + it->on_response(ip_protocol, action_id, buf); + reqs.erase(it); return true; } } @@ -692,15 +787,14 @@ public: it != std::end(reqs)) { logtrace(tracker.log_name(), fmt::format("{} is a scrape request!", transaction_id)); - auto req = *it; - it = reqs.erase(it); - req.onResponse(action_id, buf); + it->on_response(action_id, buf); + reqs.erase(it); return true; } } } - /* no match... */ + // no match... return false; } @@ -712,7 +806,7 @@ public: private: // Finds the tau_tracker struct that corresponds to this url. // If it doesn't exist yet, create one. - tau_tracker* getTrackerFromUrl(tr_interned_string const announce_url) + tau_tracker* get_tracker_from_url(tr_interned_string const announce_url) { // build a lookup key for this tracker auto const parsed = tr_urlParseTracker(announce_url); @@ -733,12 +827,17 @@ private: } // we don't have it -- build a new one - auto& tracker = trackers_.emplace_back(mediator_, authority, parsed->host, tr_port::from_host(parsed->port)); + auto& tracker = trackers_.emplace_back( + mediator_, + authority, + parsed->host, + parsed->host_wo_brackets, + tr_port::from_host(parsed->port)); logtrace(tracker.log_name(), "New tau_tracker created"); return &tracker; } - [[nodiscard]] static constexpr bool isResponseMessage(tau_action_t action, size_t msglen) noexcept + [[nodiscard]] static constexpr bool is_response_message(tau_action_t action, size_t msglen) noexcept { if (action == TAU_ACTION_CONNECT) { diff --git a/libtransmission/announcer.cc b/libtransmission/announcer.cc index c536f11bf..e6ad7dad4 100644 --- a/libtransmission/announcer.cc +++ b/libtransmission/announcer.cc @@ -160,7 +160,7 @@ public: return nullptr; } - auto const [it, is_new] = scrape_info_.try_emplace(url, url, TR_MULTISCRAPE_MAX); + auto const [it, is_new] = scrape_info_.try_emplace(url, url, TrMultiscrapeMax); return &it->second; } diff --git a/libtransmission/announcer.h b/libtransmission/announcer.h index b496d4e70..f5e696b19 100644 --- a/libtransmission/announcer.h +++ b/libtransmission/announcer.h @@ -156,7 +156,7 @@ public: // @brief process an incoming udp message if it's a tracker response. // @return true if msg was a tracker response; false otherwise - virtual bool handle_message(uint8_t const* msg, size_t msglen) = 0; + virtual bool handle_message(uint8_t const* msg, size_t msglen, struct sockaddr const* from, socklen_t fromlen) = 0; [[nodiscard]] virtual bool is_idle() const noexcept = 0; }; diff --git a/libtransmission/net.cc b/libtransmission/net.cc index ff8a3f197..6df2a13c3 100644 --- a/libtransmission/net.cc +++ b/libtransmission/net.cc @@ -433,16 +433,6 @@ namespace namespace is_valid_for_peers_helpers { -[[nodiscard]] constexpr auto is_ipv4_mapped_address(tr_address const& addr) -{ - return addr.is_ipv6() && IN6_IS_ADDR_V4MAPPED(&addr.addr.addr6); -} - -[[nodiscard]] constexpr auto is_ipv6_link_local_address(tr_address const& addr) -{ - return addr.is_ipv6() && IN6_IS_ADDR_LINKLOCAL(&addr.addr.addr6); -} - /* isMartianAddr was written by Juliusz Chroboczek, and is covered under the same license as third-party/dht/dht.c. */ [[nodiscard]] auto is_martian_addr(tr_address const& addr, tr_peer_from from) @@ -811,7 +801,7 @@ bool tr_socket_address::is_valid_for_peers(tr_peer_from from) const noexcept { using namespace is_valid_for_peers_helpers; - return is_valid() && !std::empty(port_) && !is_ipv6_link_local_address(address_) && !is_ipv4_mapped_address(address_) && + return is_valid() && !std::empty(port_) && !address_.is_ipv6_link_local_address() && !address_.is_ipv4_mapped_address() && !is_martian_addr(address_, from); } diff --git a/libtransmission/net.h b/libtransmission/net.h index 02d14b450..99ccc4ccc 100644 --- a/libtransmission/net.h +++ b/libtransmission/net.h @@ -146,9 +146,10 @@ private: enum tr_address_type : uint8_t { - TR_AF_INET, + TR_AF_INET = 0, TR_AF_INET6, - NUM_TR_AF_INET_TYPES + NUM_TR_AF_INET_TYPES, + TR_AF_UNSPEC = NUM_TR_AF_INET_TYPES }; std::string_view tr_ip_protocol_to_sv(tr_address_type type); @@ -240,6 +241,16 @@ struct tr_address [[nodiscard]] bool is_global_unicast_address() const noexcept; + [[nodiscard]] constexpr bool is_ipv4_mapped_address() const noexcept + { + return is_ipv6() && IN6_IS_ADDR_V4MAPPED(&addr.addr6); + } + + [[nodiscard]] constexpr bool is_ipv6_link_local_address() const noexcept + { + return is_ipv6() && IN6_IS_ADDR_LINKLOCAL(&addr.addr6); + } + tr_address_type type = NUM_TR_AF_INET_TYPES; union { diff --git a/libtransmission/tr-udp.cc b/libtransmission/tr-udp.cc index a1d219033..1c5f8b503 100644 --- a/libtransmission/tr-udp.cc +++ b/libtransmission/tr-udp.cc @@ -126,7 +126,7 @@ void event_callback(evutil_socket_t s, [[maybe_unused]] short type, void* vsessi } else if (n_read >= 8 && buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] <= 3) { - if (!session->announcer_udp_->handle_message(std::data(buf), n_read)) + if (!session->announcer_udp_->handle_message(std::data(buf), n_read, from_sa, fromlen)) { tr_logAddTrace("Couldn't parse UDP tracker packet."); } diff --git a/libtransmission/web-utils.cc b/libtransmission/web-utils.cc index 25c233daa..ab7b1c287 100644 --- a/libtransmission/web-utils.cc +++ b/libtransmission/web-utils.cc @@ -291,6 +291,19 @@ std::string_view getSiteName(std::string_view host) return host; } + +// Not part of the RFC3986 standard, but included for convenience +// when using the result with API that does not accept IPv6 address +// strings that are wrapped in square brackets (e.g. inet_pton()) +std::string_view getHostWoBrackets(std::string_view host) +{ + if (tr_strv_starts_with(host, '[')) + { + host.remove_prefix(1); + host.remove_suffix(1); + } + return host; +} } // namespace std::optional tr_urlParse(std::string_view url) @@ -363,6 +376,7 @@ std::optional tr_urlParse(std::string_view url) { parsed.host = tr_strv_sep(&remain, ':'); } + parsed.host_wo_brackets = getHostWoBrackets(parsed.host); parsed.sitename = getSiteName(parsed.host); parsed.port = parsePort(!std::empty(remain) ? remain : getPortForScheme(parsed.scheme)); } diff --git a/libtransmission/web-utils.h b/libtransmission/web-utils.h index 7c2410c85..de15b8a8a 100644 --- a/libtransmission/web-utils.h +++ b/libtransmission/web-utils.h @@ -31,6 +31,7 @@ struct tr_url_parsed_t std::string_view scheme; // "http" std::string_view authority; // "example.com:80" std::string_view host; // "example.com" + std::string_view host_wo_brackets; // "example.com" ("[::1]" -> "::1") std::string_view sitename; // "example" std::string_view path; // /"over/there" std::string_view query; // "name=ferret" diff --git a/tests/libtransmission/announcer-udp-test.cc b/tests/libtransmission/announcer-udp-test.cc index 62ac9ca11..65787b373 100644 --- a/tests/libtransmission/announcer-udp-test.cc +++ b/tests/libtransmission/announcer-udp-test.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -45,6 +46,9 @@ using namespace std::literals; +using tau_connection_t = uint64_t; +using tau_transaction_t = uint32_t; + using MessageBuffer = libtransmission::StackBuffer<4096, std::byte>; class AnnouncerUdpTest : public ::testing::Test @@ -132,7 +136,7 @@ protected: } } - [[nodiscard]] static uint32_t parseConnectionRequest(std::vector const& data) + [[nodiscard]] static tau_transaction_t parseConnectionRequest(std::vector const& data) { auto buf = MessageBuffer(data); EXPECT_EQ(ProtocolId, buf.to_uint64()); @@ -169,7 +173,7 @@ protected: return std::make_pair(buildScrapeRequestFromResponse(response), response); } - [[nodiscard]] static auto parseScrapeRequest(std::vector const& data, uint64_t expected_connection_id) + [[nodiscard]] static auto parseScrapeRequest(std::vector const& data, tau_connection_t expected_connection_id) { auto buf = MessageBuffer(data); EXPECT_EQ(expected_connection_id, buf.to_uint64()); @@ -187,13 +191,19 @@ protected: [[nodiscard]] static auto waitForAnnouncerToSendMessage(MockMediator& mediator) { - libtransmission::test::waitFor(mediator.eventBase(), [&mediator]() { return !std::empty(mediator.sent_); }); + EXPECT_TRUE( + libtransmission::test::waitFor(mediator.eventBase(), [&mediator]() { return !std::empty(mediator.sent_); })); auto buf = std::move(mediator.sent_.back().buf_); mediator.sent_.pop_back(); return buf; } - [[nodiscard]] static bool sendError(tr_announcer_udp& announcer, uint32_t transaction_id, std::string_view errmsg) + [[nodiscard]] static bool sendError( + tr_announcer_udp& announcer, + tau_transaction_t transaction_id, + std::string_view errmsg, + struct sockaddr const* from, + socklen_t fromlen) { auto buf = MessageBuffer{}; buf.add_uint32(ErrorAction); @@ -204,21 +214,25 @@ protected: auto arr = std::array{}; buf.to_buf(std::data(arr), response_size); - return announcer.handle_message(std::data(arr), response_size); + return announcer.handle_message(std::data(arr), response_size, from, fromlen); } - [[nodiscard]] static auto sendConnectionResponse(tr_announcer_udp& announcer, uint32_t transaction_id) + [[nodiscard]] static auto sendConnectionResponse( + tr_announcer_udp& announcer, + tau_transaction_t transaction_id, + struct sockaddr const* from, + socklen_t fromlen) { - auto const connection_id = tr_rand_obj(); + auto const connection_id = tr_rand_obj(); auto buf = MessageBuffer{}; buf.add_uint32(ConnectAction); buf.add_uint32(transaction_id); buf.add_uint64(connection_id); - auto arr = std::array{}; + auto arr = std::array{}; auto response_size = std::size(buf); buf.to_buf(std::data(arr), response_size); - EXPECT_TRUE(announcer.handle_message(std::data(arr), response_size)); + EXPECT_TRUE(announcer.handle_message(std::data(arr), response_size, from, fromlen)); return connection_id; } @@ -227,7 +241,7 @@ protected: { uint64_t connection_id = 0; uint32_t action = 0; // 1: announce - uint32_t transaction_id = 0; + tau_transaction_t transaction_id = 0; tr_sha1_digest_t info_hash = {}; tr_peer_id_t peer_id = {}; uint64_t downloaded = 0; @@ -307,6 +321,16 @@ protected: return timer; } + static auto sockaddrFromUrl(std::string_view tracker_url) + { + auto parsed_url = tr_urlParse(tracker_url); + EXPECT_TRUE(parsed_url); + auto addr = tr_address::from_string(parsed_url->host); + EXPECT_TRUE(addr); + + return tr_socket_address{ *addr, tr_port::from_host(parsed_url->port) }.to_sockaddr(); + } + std::unique_ptr init_mgr_; // https://www.bittorrent.org/beps/bep_0015.html @@ -337,12 +361,16 @@ TEST_F(AnnouncerUdpTest, canScrape) auto response = std::optional{}; announcer->scrape(request, [&response](tr_scrape_response const& resp) { response = resp; }); + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(request.scrape_url); + auto const* const from_ptr = reinterpret_cast(&from); + // The announcer should have sent a UDP connection request. // Inspect that request for validity. auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); // Have the tracker respond to the request - auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id); + auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); // The announcer should have sent a UDP scrape request. // Inspect that request for validity. @@ -359,7 +387,7 @@ TEST_F(AnnouncerUdpTest, canScrape) auto response_size = std::size(buf); auto arr = std::array{}; buf.to_buf(std::data(arr), response_size); - EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size)); + EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen)); // confirm that announcer processed the response EXPECT_TRUE(response.has_value()); @@ -413,13 +441,17 @@ TEST_F(AnnouncerUdpTest, canMultiScrape) expected_response.scrape_url = DefaultScrapeUrl; expected_response.min_request_interval = 0; + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(expected_response.scrape_url); + auto const* const from_ptr = reinterpret_cast(&from); + auto request = buildScrapeRequestFromResponse(expected_response); auto response = std::optional{}; announcer->scrape(request, [&response](tr_scrape_response const& resp) { response = resp; }); // Announcer will request a connection. Verify and grant the request auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); - auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id); + auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); // The announcer should have sent a UDP scrape request. // Inspect that request for validity. @@ -439,7 +471,7 @@ TEST_F(AnnouncerUdpTest, canMultiScrape) auto response_size = std::size(buf); auto arr = std::array{}; buf.to_buf(std::data(arr), response_size); - EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size)); + EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen)); // Confirm that announcer processed the response EXPECT_TRUE(response.has_value()); @@ -463,6 +495,10 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError) expected_response.min_request_interval = 0; expected_response.errmsg = "Unrecognized info-hash"; + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(expected_response.scrape_url); + auto const* const from_ptr = reinterpret_cast(&from); + // build the request auto request = buildScrapeRequestFromResponse(expected_response); @@ -480,7 +516,7 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError) auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); // Have the tracker respond to the request - auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id); + auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); // The announcer should have sent a UDP scrape request. // Inspect that request for validity. @@ -489,7 +525,7 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError) connection_id); // Have the tracker respond to the request with an "unable to scrape" error - EXPECT_TRUE(sendError(*announcer, scrape_transaction_id, expected_response.errmsg)); + EXPECT_TRUE(sendError(*announcer, scrape_transaction_id, expected_response.errmsg, from_ptr, fromlen)); // confirm that announcer processed the response EXPECT_TRUE(response.has_value()); @@ -513,6 +549,10 @@ TEST_F(AnnouncerUdpTest, canHandleConnectError) expected_response.min_request_interval = 0; expected_response.errmsg = "Unable to Connect"; + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(expected_response.scrape_url); + auto const* const from_ptr = reinterpret_cast(&from); + // build the announcer auto mediator = MockMediator{}; auto announcer = tr_announcer_udp::create(mediator); @@ -529,7 +569,7 @@ TEST_F(AnnouncerUdpTest, canHandleConnectError) auto transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); // Have the tracker respond to the request with an "unable to connect" error - EXPECT_TRUE(sendError(*announcer, transaction_id, expected_response.errmsg)); + EXPECT_TRUE(sendError(*announcer, transaction_id, expected_response.errmsg, from_ptr, fromlen)); // Confirm that announcer processed the response EXPECT_TRUE(response.has_value()); @@ -545,6 +585,10 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage) request.info_hash_count = 1; request.info_hash[0] = tr_rand_obj(); + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(request.scrape_url); + auto const* const from_ptr = reinterpret_cast(&from); + // build the announcer auto mediator = MockMediator{}; auto announcer = tr_announcer_udp::create(mediator); @@ -566,7 +610,7 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage) auto response_size = std::size(buf); auto arr = std::array{}; buf.to_buf(std::data(arr), response_size); - EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size)); + EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen)); // send a connection response but with an *invalid* action buf.clear(); @@ -575,15 +619,15 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage) buf.add_uint64(tr_rand_obj()); response_size = std::size(buf); buf.to_buf(std::data(arr), response_size); - EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size)); + EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen)); // but after discarding invalid messages, // a valid connection response should still work - auto const connection_id = sendConnectionResponse(*announcer, transaction_id); + auto const connection_id = sendConnectionResponse(*announcer, transaction_id, from_ptr, fromlen); EXPECT_NE(0, connection_id); } -TEST_F(AnnouncerUdpTest, canAnnounce) +TEST_F(AnnouncerUdpTest, canAnnounceIPv4) { static auto constexpr Interval = uint32_t{ 3600 }; static auto constexpr Leechers = uint32_t{ 10 }; @@ -608,6 +652,10 @@ TEST_F(AnnouncerUdpTest, canAnnounce) request.peer_id = tr_peerIdInit(); request.info_hash = tr_rand_obj(); + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(request.announce_url); + auto const* const from_ptr = reinterpret_cast(&from); + auto expected_response = tr_announce_response{}; expected_response.info_hash = request.info_hash; expected_response.did_connect = true; @@ -634,7 +682,7 @@ TEST_F(AnnouncerUdpTest, canAnnounce) // Announcer will request a connection. Verify and grant the request auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); - auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id); + auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); // The announcer should have sent a UDP announce request. // Inspect that request for validity. @@ -658,7 +706,96 @@ TEST_F(AnnouncerUdpTest, canAnnounce) auto response_size = std::size(buf); auto arr = std::array{}; buf.to_buf(std::data(arr), response_size); - EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size)); + + EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen)); + + // Confirm that announcer processed the response + EXPECT_TRUE(response.has_value()); + assert(response.has_value()); + expectEqual(expected_response, *response); +} + +TEST_F(AnnouncerUdpTest, canAnnounceIPv6) +{ + static auto constexpr Interval = uint32_t{ 3600 }; + static auto constexpr Leechers = uint32_t{ 10 }; + static auto constexpr Seeders = uint32_t{ 20 }; + auto const addresses = std::array{ { + { tr_address::from_string("fd12:3456:789a:1::1").value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("fd12:3456:789a:1::2").value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("fd12:3456:789a:1::3").value_or(tr_address{}), tr_port::from_host(2022) }, + } }; + + auto request = tr_announce_request{}; + request.event = TR_ANNOUNCE_EVENT_STARTED; + request.port = tr_port::from_host(80); + request.key = 0xCAFE; + request.numwant = 20; + request.up = 1; + request.down = 2; + request.corrupt = 3; + request.leftUntilComplete = 100; + request.announce_url = "https://[::1]/announce"; + request.tracker_id = "fnord"; + request.peer_id = tr_peerIdInit(); + request.info_hash = tr_rand_obj(); + + // Obtain the source socket address from tracker url + auto [from, fromlen] = sockaddrFromUrl(request.announce_url); + auto const* const from_ptr = reinterpret_cast(&from); + + auto expected_response = tr_announce_response{}; + expected_response.info_hash = request.info_hash; + expected_response.did_connect = true; + expected_response.did_timeout = false; + expected_response.interval = Interval; + expected_response.min_interval = 0; // not specified in UDP announce + expected_response.seeders = Seeders; + expected_response.leechers = Leechers; + expected_response.downloads = std::nullopt; // not specified in UDP announce + expected_response.pex = {}; + expected_response.pex6 = std::vector{ tr_pex{ addresses[0] }, tr_pex{ addresses[1] }, tr_pex{ addresses[2] } }; + expected_response.errmsg = {}; + expected_response.warning = {}; + expected_response.tracker_id = {}; // not specified in UDP announce + expected_response.external_ip = {}; + + // build the announcer + auto mediator = MockMediator{}; + auto announcer = tr_announcer_udp::create(mediator); + auto upkeep_timer = createUpkeepTimer(mediator, announcer); + + auto response = std::optional{}; + announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + + // Announcer will request a connection. Verify and grant the request + auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); + + // The announcer should have sent a UDP announce request. + // Inspect that request for validity. + auto udp_ann_req = parseAnnounceRequest(waitForAnnouncerToSendMessage(mediator), connection_id); + expectEqual(request, udp_ann_req); + + // Have the tracker respond to the request + auto buf = MessageBuffer{}; + buf.add_uint32(AnnounceAction); + buf.add_uint32(udp_ann_req.transaction_id); + buf.add_uint32(expected_response.interval); + buf.add_uint32(expected_response.leechers.value_or(-1)); + buf.add_uint32(expected_response.seeders.value_or(-1)); + for (auto const& [addr, port] : addresses) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) + buf.add(&addr.addr.addr6.s6_addr, sizeof(addr.addr.addr6.s6_addr)); + buf.add_uint16(port.host()); + } + + auto response_size = std::size(buf); + auto arr = std::array{}; + buf.to_buf(std::data(arr), response_size); + + EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen)); // Confirm that announcer processed the response EXPECT_TRUE(response.has_value());