From cbc4e9dc3ae672c9727e580f9b995ed214b27a1c Mon Sep 17 00:00:00 2001 From: Yat Ho Date: Wed, 28 Jan 2026 02:38:24 +0800 Subject: [PATCH] refactor: mock udp announcer DNS resolver for tests (#8232) * refactor: move udp tracker dns lookup to mediator * test: mock udp announcer DNS resolver --- libtransmission/announcer-udp.cc | 107 +++++++++++--------- libtransmission/announcer.h | 5 + tests/libtransmission/announcer-udp-test.cc | 50 +++++++++ 3 files changed, 112 insertions(+), 50 deletions(-) diff --git a/libtransmission/announcer-udp.cc b/libtransmission/announcer-udp.cc index 02eea9694..3e7354727 100644 --- a/libtransmission/announcer-udp.cc +++ b/libtransmission/announcer-udp.cc @@ -439,7 +439,11 @@ struct tau_tracker // do we have a DNS request that's ready? if (auto& dns = addr_pending_dns_[ipp]; dns && dns->wait_for(0ms) == std::future_status::ready) { - addr_[ipp] = dns->get(); + // TODO(C++23): use std::optional::transform() instead + if (auto const& addr = dns->get(); addr.has_value()) + { + addr_[ipp] = addr->to_sockaddr(); + } dns.reset(); addr_expires_at_[ipp] = now + DnsRetryIntervalSecs; } @@ -453,17 +457,17 @@ struct tau_tracker for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp) { + auto const ipp_enum = static_cast(ipp); + // update the addr if our lookup is past its shelf date if (auto& dns = addr_pending_dns_[ipp]; !dns && addr_expires_at_[ipp] <= now) { addr_[ipp].reset(); dns = std::async( std::launch::async, - [this](tr_address_type ip_protocol) { return lookup(ip_protocol); }, - static_cast(ipp)); + [this, ipp_enum] { return mediator_.dns_lookup(ipp_enum, host_lookup, port.host(), log_name()); }); } - auto const ipp_enum = static_cast(ipp); auto& conn_at = connecting_at[ipp]; logtrace( log_name(), @@ -523,51 +527,6 @@ private: return std::ranges::any_of(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 = tr_ip_protocol_to_af(ip_protocol); - hints.ai_protocol = IPPROTO_UDP; - hints.ai_socktype = SOCK_DGRAM; - - addrinfo* info = nullptr; - auto const szhost = tr_urlbuf{ host_lookup }; - if (int const rc = getaddrinfo(szhost.c_str(), std::data(szport), &hints, &info); rc != 0) - { - logwarn( - log_name(), - fmt::format( - fmt::runtime(_("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, freeaddrinfo }; - - // 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_ipv6_ipv4_mapped()) - { - 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(log_name(), fmt::format("{} DNS lookup succeeded", tr_ip_protocol_to_sv(ip_protocol))); - return socket_address->to_sockaddr(); - } - void fail_all(bool did_connect, bool did_timeout, std::string_view errmsg) { for (auto& req : scrapes) @@ -702,7 +661,7 @@ public: private: Mediator& mediator_; - std::array>, NUM_TR_AF_INET_TYPES> addr_pending_dns_; + std::array>>, NUM_TR_AF_INET_TYPES> addr_pending_dns_; std::array addr_ = {}; std::array addr_expires_at_ = {}; @@ -902,3 +861,51 @@ std::unique_ptr tr_announcer_udp::create(Mediator& mediator) { return std::make_unique(mediator); } + +std::optional tr_announcer_udp::Mediator::dns_lookup( + tr_address_type const ip_protocol, + std::string_view const name, + uint16_t const service, + std::string_view const log_name) const +{ + auto hints = addrinfo{}; + 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 szname = tr_urlbuf{ name }; + auto szservice = std::array{}; + *fmt::format_to(std::data(szservice), "{:d}", service) = '\0'; + if (int const rc = getaddrinfo(szname.c_str(), std::data(szservice), &hints, &info); rc != 0) + { + logwarn( + log_name, + fmt::format( + fmt::runtime(_("Couldn't look up '{address}:{port}' in {ip_protocol}: {error} ({error_code})")), + fmt::arg("address", name), + fmt::arg("port", service), + 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, freeaddrinfo }; + + // 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_ipv6_ipv4_mapped()) + { + logdbg( + log_name, + fmt::format( + "Couldn't look up '{address}:{port}' in {ip_protocol}: got invalid address", + fmt::arg("address", name), + fmt::arg("port", service), + fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol)))); + return {}; + } + + logdbg(log_name, fmt::format("{} DNS lookup succeeded", tr_ip_protocol_to_sv(ip_protocol))); + return socket_address; +} diff --git a/libtransmission/announcer.h b/libtransmission/announcer.h index 54f0ac590..feab159b5 100644 --- a/libtransmission/announcer.h +++ b/libtransmission/announcer.h @@ -141,6 +141,11 @@ public: virtual ~Mediator() noexcept = default; virtual void sendto(void const* buf, size_t buflen, sockaddr const* addr, socklen_t addrlen) = 0; [[nodiscard]] virtual std::optional announce_ip() const = 0; + [[nodiscard]] virtual std::optional dns_lookup( + tr_address_type ip_protocol, + std::string_view name, + uint16_t service, + std::string_view log_name) const; }; virtual ~tr_announcer_udp() noexcept = default; diff --git a/tests/libtransmission/announcer-udp-test.cc b/tests/libtransmission/announcer-udp-test.cc index 87f53851b..30fc0e837 100644 --- a/tests/libtransmission/announcer-udp-test.cc +++ b/tests/libtransmission/announcer-udp-test.cc @@ -84,6 +84,56 @@ protected: return {}; } + // Mock DNS lookup that only resolves localhost + [[nodiscard]] std::optional dns_lookup( + tr_address_type const ip_protocol, + std::string_view const name, + uint16_t const service, + std::string_view /*log_name*/) const override + { + auto const is_localhost = name == "localhost"sv; + auto const port = tr_port::from_host(service); + switch (ip_protocol) + { + case TR_AF_INET: + if (is_localhost) + { + auto const addr = tr_address::from_string("127.0.0.1"sv); + EXPECT_TRUE(addr); + EXPECT_TRUE(addr->is_ipv4_loopback()); + return tr_socket_address{ *addr, port }; + } + + if (auto const addr = tr_address::from_string(name); addr && addr->is_ipv4_loopback()) + { + return tr_socket_address{ *addr, port }; + } + + break; + + case TR_AF_INET6: + if (is_localhost) + { + auto const addr = tr_address::from_string("::1"); + EXPECT_TRUE(addr); + EXPECT_TRUE(addr->is_ipv6_loopback()); + return tr_socket_address{ *addr, port }; + } + + if (auto const addr = tr_address::from_string(name); addr && addr->is_ipv6_loopback()) + { + return tr_socket_address{ *addr, port }; + } + + break; + + default: + break; + } + + return {}; + } + struct Sent { Sent() = default;