From 41dd2cfd530e9eba666485709f20b91be46f64ee Mon Sep 17 00:00:00 2001 From: Yat Ho Date: Tue, 28 Oct 2025 01:08:59 +0800 Subject: [PATCH] fix: accept either one of udp announce response (#7583) * fix: accept either one of udp announce response * fix: udp announcer tests should read packets in-order * test: housekeeping * test: new tests for the new logic --- libtransmission/announcer-common.h | 25 + libtransmission/announcer-udp.cc | 77 ++- tests/libtransmission/announcer-udp-test.cc | 581 +++++++++++++++++--- 3 files changed, 579 insertions(+), 104 deletions(-) diff --git a/libtransmission/announcer-common.h b/libtransmission/announcer-common.h index 388357b4c..d59ff055d 100644 --- a/libtransmission/announcer-common.h +++ b/libtransmission/announcer-common.h @@ -25,6 +25,7 @@ #include "libtransmission/net.h" #include "libtransmission/peer-mgr.h" // tr_pex #include "libtransmission/tr-macros.h" // tr_peer_id_t +#include "libtransmission/utils.h" struct tr_url_parsed_t; @@ -145,6 +146,30 @@ struct tr_announce_response /* tracker extension that returns the client's public IP address. * https://www.bittorrent.org/beps/bep_0024.html */ std::optional external_ip; + + static constexpr struct + { + static constexpr int compare(tr_announce_response const& lhs, tr_announce_response const& rhs) + { + if (auto val = tr_compare_3way(lhs.did_connect, rhs.did_connect); val != 0) + { + return val; + } + + if (auto val = tr_compare_3way(lhs.did_timeout, rhs.did_timeout); val != 0) + { + return -val; + } + + // Non-empty error message most likely means we reached the tracker + return -tr_compare_3way(std::empty(lhs.errmsg), std::empty(rhs.errmsg)); + } + + constexpr bool operator()(tr_announce_response const& lhs, tr_announce_response const& rhs) const noexcept + { + return compare(lhs, rhs) > 0; + } + } CompareFailed{}; }; // --- SCRAPE diff --git a/libtransmission/announcer-udp.cc b/libtransmission/announcer-udp.cc index 28857790d..76af1aac8 100644 --- a/libtransmission/announcer-udp.cc +++ b/libtransmission/announcer-udp.cc @@ -171,15 +171,67 @@ private: // --- ANNOUNCE +class tau_announce_data +{ +public: + explicit tau_announce_data(tr_announce_response_func&& on_response) + : on_response_{ std::move(on_response) } + { + } + + constexpr void inc_request_sent_count() noexcept + { + ++requests_sent_count_; + } + + void on_response(tr_announce_response&& response, bool is_success) + { + TR_ASSERT(on_response_); + if (!on_response_) + { + return; + } + + auto const got_all_responses = ++requests_answered_count_ == requests_sent_count_; + + if (is_success) + { + on_response_(response); + succeeded_ = true; + } + else if (!succeeded_) + { + failed_responses_.emplace_back(std::move(response)); + + if (got_all_responses) + { + auto const begin = std::begin(failed_responses_); + std::partial_sort(begin, std::next(begin), std::end(failed_responses_), tr_announce_response::CompareFailed); + on_response_(failed_responses_.front()); + } + } + } + +private: + bool succeeded_ = false; + + std::vector failed_responses_; + + tr_announce_response_func on_response_; + + uint8_t requests_sent_count_ = {}; + uint8_t requests_answered_count_ = {}; +}; + 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) + std::shared_ptr data) : ip_protocol{ ip_protocol_in } - , on_response_{ std::move(on_response) } + , data_{ std::move(data) } { // https://www.bittorrent.org/beps/bep_0015.html sets key size at 32 bits static_assert(sizeof(tr_announce_request::key) == sizeof(uint32_t)); @@ -207,19 +259,13 @@ struct tau_announce_request payload.add_uint32(in.key); payload.add_uint32(in.numwant); payload.add_port(in.port); + + data_->inc_request_sent_count(); } [[nodiscard]] auto has_callback() const noexcept { - return !!on_response_; - } - - void request_finished() const - { - if (on_response_) - { - on_response_(response); - } + return !!data_; } void fail(bool did_connect, bool did_timeout, std::string_view errmsg) @@ -227,7 +273,7 @@ struct tau_announce_request response.did_connect = did_connect; response.did_timeout = did_timeout; response.errmsg = errmsg; - request_finished(); + data_->on_response(std::move(response), false); } void on_response(tr_address_type ip_protocol_resp, tau_action_t action, InBuf& buf) @@ -254,7 +300,7 @@ struct tau_announce_request default: break; } - request_finished(); + data_->on_response(std::move(response), true); } else { @@ -307,7 +353,7 @@ private: time_t const created_at_ = tr_time(); - tr_announce_response_func on_response_; + std::shared_ptr data_; }; // --- TRACKER @@ -681,9 +727,10 @@ public: return; } + auto const data = std::make_shared(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->announces.emplace_back(static_cast(ipp), mediator_.announce_ip(), request, data); } tracker->upkeep(false); } diff --git a/tests/libtransmission/announcer-udp-test.cc b/tests/libtransmission/announcer-udp-test.cc index 7a17dd88a..71dac760f 100644 --- a/tests/libtransmission/announcer-udp-test.cc +++ b/tests/libtransmission/announcer-udp-test.cc @@ -5,7 +5,6 @@ #include #include -#include #include // std::byte #include // uint32_t, uint64_t #include // for std::memcpy() @@ -189,12 +188,24 @@ protected: return std::make_pair(transaction_id, info_hashes); } - [[nodiscard]] static auto waitForAnnouncerToSendMessage(MockMediator& mediator) + [[nodiscard]] static auto waitForAnnouncerToSendMessage( + MockMediator& mediator, + sockaddr* const from = nullptr, + socklen_t* const fromlen = nullptr) { 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(); + auto& sent = mediator.sent_.front(); + auto const buf = std::move(sent.buf_); + if (from != nullptr) + { + std::memcpy(from, &sent.ss_, sent.sslen_); + } + if (fromlen != nullptr) + { + *fromlen = sent.sslen_; + } + mediator.sent_.pop_front(); return buf; } @@ -321,16 +332,6 @@ 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(); - } - // https://www.bittorrent.org/beps/bep_0015.html static auto constexpr ProtocolId = uint64_t{ 0x41727101980ULL }; static auto constexpr ConnectAction = uint32_t{ 0 }; @@ -359,20 +360,22 @@ 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); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; // The announcer should have sent a UDP connection request. // Inspect that request for validity. - auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); // Have the tracker respond to the request 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. - auto [scrape_transaction_id, info_hashes] = parseScrapeRequest(waitForAnnouncerToSendMessage(mediator), connection_id); + auto [scrape_transaction_id, info_hashes] = parseScrapeRequest( + waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen), + connection_id); expectEqual(request, info_hashes); // Have the tracker respond to the request @@ -388,8 +391,7 @@ TEST_F(AnnouncerUdpTest, canScrape) 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()); + ASSERT_TRUE(response.has_value()); expectEqual(expected_response, *response); // Now scrape again. @@ -439,21 +441,23 @@ 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; }); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + // Announcer will request a connection. Verify and grant the request - auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); 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. - auto [scrape_transaction_id, info_hashes] = parseScrapeRequest(waitForAnnouncerToSendMessage(mediator), connection_id); + auto [scrape_transaction_id, info_hashes] = parseScrapeRequest( + waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen), + connection_id); expectEqual(request, info_hashes); // Have the tracker respond to the request @@ -472,8 +476,7 @@ TEST_F(AnnouncerUdpTest, canMultiScrape) 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()); + ASSERT_TRUE(response.has_value()); expectEqual(expected_response, *response); } @@ -491,11 +494,7 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError) expected_response.rows[0].downloaders = std::nullopt; expected_response.scrape_url = DefaultScrapeUrl; 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); + expected_response.errmsg = "Unrecognized info-hash"s; // build the request auto request = buildScrapeRequestFromResponse(expected_response); @@ -509,9 +508,13 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError) auto response = std::optional{}; announcer->scrape(request, [&response](tr_scrape_response const& resp) { response = resp; }); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + // The announcer should have sent a UDP connection request. // Inspect that request for validity. - auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); // Have the tracker respond to the request auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); @@ -519,15 +522,14 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError) // The announcer should have sent a UDP scrape request. // Inspect that request for validity. auto const [scrape_transaction_id, info_hashes] = parseScrapeRequest( - waitForAnnouncerToSendMessage(mediator), + waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen), 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, from_ptr, fromlen)); // confirm that announcer processed the response - EXPECT_TRUE(response.has_value()); - assert(response.has_value()); + ASSERT_TRUE(response.has_value()); expectEqual(expected_response, *response); } @@ -545,11 +547,7 @@ TEST_F(AnnouncerUdpTest, canHandleConnectError) expected_response.rows[0].downloaders = std::nullopt; expected_response.scrape_url = DefaultScrapeUrl; 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); + expected_response.errmsg = "Unable to Connect"s; // build the announcer auto mediator = MockMediator{}; @@ -562,16 +560,19 @@ TEST_F(AnnouncerUdpTest, canHandleConnectError) buildScrapeRequestFromResponse(expected_response), [&response](tr_scrape_response const& resp) { response = resp; }); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + // The announcer should have sent a UDP connection request. // Inspect that request for validity. - auto transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); // Have the tracker respond to the request with an "unable to connect" error EXPECT_TRUE(sendError(*announcer, transaction_id, expected_response.errmsg, from_ptr, fromlen)); // Confirm that announcer processed the response - EXPECT_TRUE(response.has_value()); - assert(response.has_value()); + ASSERT_TRUE(response.has_value()); expectEqual(expected_response, *response); } @@ -583,10 +584,6 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage) request.info_hash_count = 1U; 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); @@ -596,9 +593,13 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage) auto response = std::optional{}; announcer->scrape(request, [&response](tr_scrape_response const& resp) { response = resp; }); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + // The announcer should have sent a UDP connection request. // Inspect that request for validity. - auto transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); // send a connection response but with an *invalid* transaction id auto buf = MessageBuffer{}; @@ -631,9 +632,9 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv4) static auto constexpr Leechers = uint32_t{ 10 }; static auto constexpr Seeders = uint32_t{ 20 }; auto const addresses = std::array{ { - { tr_address::from_string("10.10.10.5").value_or(tr_address{}), tr_port::from_host(128) }, - { tr_address::from_string("192.168.1.2").value_or(tr_address{}), tr_port::from_host(2021) }, - { tr_address::from_string("192.168.1.3").value_or(tr_address{}), tr_port::from_host(2022) }, + { tr_address::from_string("10.10.10.5"sv).value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("192.168.1.2"sv).value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("192.168.1.3"sv).value_or(tr_address{}), tr_port::from_host(2022) }, } }; auto request = tr_announce_request{}; @@ -645,15 +646,11 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv4) request.down = 2; request.corrupt = 3; request.leftUntilComplete = 100; - request.announce_url = "https://127.0.0.1/announce"; - request.tracker_id = "fnord"; + request.announce_url = "https://127.0.0.1/announce"sv; + request.tracker_id = "fnord"s; 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; @@ -673,18 +670,22 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv4) // build the announcer auto mediator = MockMediator{}; auto announcer = tr_announcer_udp::create(mediator); - auto upkeep_timer = createUpkeepTimer(mediator, announcer); + auto const upkeep_timer = createUpkeepTimer(mediator, announcer); auto response = std::optional{}; announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + // Announcer will request a connection. Verify and grant the request - auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto const connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); 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); + auto const udp_ann_req = parseAnnounceRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen), connection_id); expectEqual(request, udp_ann_req); // Have the tracker respond to the request @@ -701,15 +702,10 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv4) 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)); + EXPECT_TRUE(announcer->handle_message(reinterpret_cast(std::data(buf)), std::size(buf), from_ptr, fromlen)); // Confirm that announcer processed the response - EXPECT_TRUE(response.has_value()); - assert(response.has_value()); + ASSERT_TRUE(response.has_value()); expectEqual(expected_response, *response); } @@ -719,9 +715,9 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv6) 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) }, + { tr_address::from_string("fd12:3456:789a:1::1"sv).value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("fd12:3456:789a:1::2"sv).value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("fd12:3456:789a:1::3"sv).value_or(tr_address{}), tr_port::from_host(2022) }, } }; auto request = tr_announce_request{}; @@ -733,15 +729,11 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv6) request.down = 2; request.corrupt = 3; request.leftUntilComplete = 100; - request.announce_url = "https://[::1]/announce"; - request.tracker_id = "fnord"; + request.announce_url = "https://[::1]/announce"sv; + request.tracker_id = "fnord"s; 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; @@ -761,18 +753,22 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv6) // build the announcer auto mediator = MockMediator{}; auto announcer = tr_announcer_udp::create(mediator); - auto upkeep_timer = createUpkeepTimer(mediator, announcer); + auto const upkeep_timer = createUpkeepTimer(mediator, announcer); auto response = std::optional{}; announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + // Announcer will request a connection. Verify and grant the request - auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator)); + auto const connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); 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); + auto const udp_ann_req = parseAnnounceRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen), connection_id); expectEqual(request, udp_ann_req); // Have the tracker respond to the request @@ -789,14 +785,421 @@ TEST_F(AnnouncerUdpTest, canAnnounceIPv6) 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)); + EXPECT_TRUE(announcer->handle_message(reinterpret_cast(std::data(buf)), std::size(buf), from_ptr, fromlen)); // Confirm that announcer processed the response - EXPECT_TRUE(response.has_value()); - assert(response.has_value()); + ASSERT_TRUE(response.has_value()); expectEqual(expected_response, *response); } + +TEST_F(AnnouncerUdpTest, canAnnounceDualStack) +{ + static auto constexpr Interval = time_t{ 3600 }; + static auto constexpr Leechers = uint32_t{ 10 }; + static auto constexpr Seeders = uint32_t{ 20 }; + auto const addresses = std::array{ + std::array{ { + { tr_address::from_string("10.10.10.5"sv).value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("192.168.1.2"sv).value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("192.168.1.3"sv).value_or(tr_address{}), tr_port::from_host(2022) }, + } }, + std::array{ { + { tr_address::from_string("fd12:3456:789a:1::1"sv).value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("fd12:3456:789a:1::2"sv).value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("fd12:3456:789a:1::3"sv).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://localhost/announce"sv; + request.tracker_id = "fnord"s; + request.peer_id = tr_peerIdInit(); + request.info_hash = tr_rand_obj(); + + auto expected_responses = std::array{}; + for (auto& expected_response : expected_responses) + { + 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.errmsg = {}; + expected_response.warning = {}; + expected_response.tracker_id = {}; // not specified in UDP announce + expected_response.external_ip = {}; + } + expected_responses[TR_AF_INET].pex = std::vector{ + tr_pex{ addresses[TR_AF_INET][0] }, + tr_pex{ addresses[TR_AF_INET][1] }, + tr_pex{ addresses[TR_AF_INET][2] }, + }; + expected_responses[TR_AF_INET6].pex6 = std::vector{ + tr_pex{ addresses[TR_AF_INET6][0] }, + tr_pex{ addresses[TR_AF_INET6][1] }, + tr_pex{ addresses[TR_AF_INET6][2] }, + }; + + // build the announcer + auto mediator = MockMediator{}; + auto announcer = tr_announcer_udp::create(mediator); + auto const upkeep_timer = createUpkeepTimer(mediator, announcer); + + auto response = std::optional{}; + announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + + auto connection_ids = std::array{}; + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + // Announcer will request a connection. Verify and grant the request + auto const connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + connection_ids[ipp] = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); + } + + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + response.reset(); + + // The announcer should have sent a UDP announce request. + // Inspect that request for validity. + auto const data = waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + auto const udp_ann_req = parseAnnounceRequest(data, connection_ids[ipp]); + expectEqual(request, udp_ann_req); + + // Have the tracker respond to the request + auto const& expected_response = expected_responses[ipp]; + 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[ipp]) + { + if (ipp == TR_AF_INET) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) + buf.add(&addr.addr.addr4.s_addr, sizeof(addr.addr.addr4.s_addr)); + } + else + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) + buf.add(&addr.addr.addr6.s6_addr, sizeof(addr.addr.addr6.s6_addr)); + } + buf.add_uint16(port.host()); + } + + EXPECT_TRUE( + announcer->handle_message(reinterpret_cast(std::data(buf)), std::size(buf), from_ptr, fromlen)); + + // Confirm that announcer processed the response + ASSERT_TRUE(response.has_value()); + expectEqual(expected_response, *response); + } +} + +TEST_F(AnnouncerUdpTest, announceDualStackOnlyIPv4Successful) +{ + static auto constexpr Interval = time_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("10.10.10.5"sv).value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("192.168.1.2"sv).value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("192.168.1.3"sv).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://localhost/announce"sv; + request.tracker_id = "fnord"s; + request.peer_id = tr_peerIdInit(); + request.info_hash = tr_rand_obj(); + + 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 = std::vector{ tr_pex{ addresses[0] }, tr_pex{ addresses[1] }, tr_pex{ addresses[2] } }; + expected_response.pex6 = {}; + 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 const upkeep_timer = createUpkeepTimer(mediator, announcer); + + auto response = std::optional{}; + announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + + auto connection_ids = std::array{}; + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + // Announcer will request a connection. Verify and grant the request + auto const connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + connection_ids[ipp] = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); + } + + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + response.reset(); + + // The announcer should have sent a UDP announce request. + // Inspect that request for validity. + auto const data = waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + auto const udp_ann_req = parseAnnounceRequest(data, connection_ids[ipp]); + expectEqual(request, udp_ann_req); + + // Have the tracker respond to the request + if (ipp == TR_AF_INET) + { + 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.addr4.s_addr, sizeof(addr.addr.addr4.s_addr)); + buf.add_uint16(port.host()); + } + + EXPECT_TRUE( + announcer->handle_message(reinterpret_cast(std::data(buf)), std::size(buf), from_ptr, fromlen)); + } + else + { + EXPECT_TRUE(sendError(*announcer, udp_ann_req.transaction_id, "Failed"sv, from_ptr, fromlen)); + } + + // Failed responses won't be processed if one of the other announce requests succeeded + EXPECT_TRUE(ipp == TR_AF_INET6 ? !response.has_value() : response.has_value()); + if (response) + { + expectEqual(expected_response, *response); + } + } +} + +TEST_F(AnnouncerUdpTest, announceDualStackOnlyIPv6Successful) +{ + static auto constexpr Interval = time_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"sv).value_or(tr_address{}), tr_port::from_host(128) }, + { tr_address::from_string("fd12:3456:789a:1::2"sv).value_or(tr_address{}), tr_port::from_host(2021) }, + { tr_address::from_string("fd12:3456:789a:1::3"sv).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://localhost/announce"sv; + request.tracker_id = "fnord"s; + request.peer_id = tr_peerIdInit(); + request.info_hash = tr_rand_obj(); + + 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 const upkeep_timer = createUpkeepTimer(mediator, announcer); + + auto response = std::optional{}; + announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + + auto connection_ids = std::array{}; + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + // Announcer will request a connection. Verify and grant the request + auto const connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + connection_ids[ipp] = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); + } + + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + response.reset(); + + // The announcer should have sent a UDP announce request. + // Inspect that request for validity. + auto const data = waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + auto const udp_ann_req = parseAnnounceRequest(data, connection_ids[ipp]); + expectEqual(request, udp_ann_req); + + // Have the tracker respond to the request + if (ipp == TR_AF_INET6) + { + 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()); + } + + EXPECT_TRUE( + announcer->handle_message(reinterpret_cast(std::data(buf)), std::size(buf), from_ptr, fromlen)); + } + else + { + EXPECT_TRUE(sendError(*announcer, udp_ann_req.transaction_id, "Failed"sv, from_ptr, fromlen)); + } + + // Failed responses won't be processed if one of the other announce requests succeeded + EXPECT_TRUE(ipp == TR_AF_INET ? !response.has_value() : response.has_value()); + if (response) + { + expectEqual(expected_response, *response); + } + } +} + +TEST_F(AnnouncerUdpTest, announceDualStackNoneSuccessful) +{ + 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://localhost/announce"sv; + request.tracker_id = "fnord"s; + request.peer_id = tr_peerIdInit(); + request.info_hash = tr_rand_obj(); + + 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 = {}; + expected_response.min_interval = 0; // not specified in UDP announce + expected_response.seeders = {}; + expected_response.leechers = {}; + expected_response.downloads = std::nullopt; // not specified in UDP announce + expected_response.pex = {}; + expected_response.pex6 = {}; + expected_response.errmsg = "Failed"s; + 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 const upkeep_timer = createUpkeepTimer(mediator, announcer); + + auto response = std::optional{}; + announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; }); + + auto from = sockaddr_storage{}; + auto* const from_ptr = reinterpret_cast(&from); + auto fromlen = socklen_t{}; + + auto connection_ids = std::array{}; + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + // Announcer will request a connection. Verify and grant the request + auto const connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen)); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + connection_ids[ipp] = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen); + } + + for (uint8_t i = 0U; i < NUM_TR_AF_INET_TYPES; ++i) + { + auto const received_response = response.has_value(); + + // The announcer should have sent a UDP announce request. + // Inspect that request for validity. + auto const data = waitForAnnouncerToSendMessage(mediator, from_ptr, &fromlen); + auto const ipp = tr_af_to_ip_protocol(from_ptr->sa_family); + auto const udp_ann_req = parseAnnounceRequest(data, connection_ids[ipp]); + expectEqual(request, udp_ann_req); + + // Have the tracker respond to the request + EXPECT_TRUE(sendError(*announcer, udp_ann_req.transaction_id, expected_response.errmsg, from_ptr, fromlen)); + + // Failed responses will only be processed if none of the announce requests are successful + // Or in other words, at most one failed response will be processed for each announce event + EXPECT_FALSE(received_response && response.has_value()); + if (response) + { + expectEqual(expected_response, *response); + } + } +}