From c39f8f663ce26a12bd34664577275a10ec776bf0 Mon Sep 17 00:00:00 2001 From: stickz Date: Sat, 20 Jul 2024 11:36:40 -0400 Subject: [PATCH] libtorrent: Optimize UDP trackers for UDNS (#42) This commit optimizes UDP trackers with a fast path to ipv4 addresses. When using UDNS, we know the address is going to resolve to IPV4. We can skip a few checks to increase performance. --- libtorrent/rak/socket_address.h | 19 +++ libtorrent/src/net/socket_datagram.cc | 2 +- libtorrent/src/net/socket_datagram.h | 2 +- libtorrent/src/net/socket_fd.cc | 2 +- libtorrent/src/net/socket_fd.h | 5 +- libtorrent/src/torrent/connection_manager.cc | 10 +- libtorrent/src/torrent/connection_manager.h | 19 ++- libtorrent/src/tracker/tracker_udp.cc | 50 ++++++-- libtorrent/src/tracker/tracker_udp.h | 11 +- libtorrent/src/utils/udnsevent.cc | 124 +++++-------------- libtorrent/src/utils/udnsevent.h | 3 +- 11 files changed, 131 insertions(+), 116 deletions(-) diff --git a/libtorrent/rak/socket_address.h b/libtorrent/rak/socket_address.h index 961c53b24..7845e6129 100644 --- a/libtorrent/rak/socket_address.h +++ b/libtorrent/rak/socket_address.h @@ -155,6 +155,10 @@ class socket_address_inet { bool is_port_any() const { return port() == 0; } bool is_address_any() const { return m_sockaddr.sin_addr.s_addr == htonl(INADDR_ANY); } +#ifdef USE_UDNS + bool is_bindable() const { return !is_address_any(); } +#endif + void clear() { std::memset(this, 0, sizeof(socket_address_inet)); set_family(); } uint16_t port() const { return ntohs(m_sockaddr.sin_port); } @@ -168,6 +172,10 @@ class socket_address_inet { uint32_t address_n() const { return m_sockaddr.sin_addr.s_addr; } std::string address_str() const; bool address_c_str(char* buf, socklen_t size) const; + +#ifdef USE_UDNS + std::string pretty_address_str() const { return address_str(); } +#endif void set_address(in_addr a) { m_sockaddr.sin_addr = a; } void set_address_h(uint32_t a) { m_sockaddr.sin_addr.s_addr = htonl(a); } @@ -180,6 +188,10 @@ class socket_address_inet { sa_family_t family() const { return m_sockaddr.sin_family; } void set_family() { m_sockaddr.sin_family = AF_INET; } +#ifdef USE_UDNS + uint32_t length() const { return sizeof(sockaddr_in); } +#endif + sockaddr* c_sockaddr() { return reinterpret_cast(&m_sockaddr); } sockaddr_in* c_sockaddr_inet() { return &m_sockaddr; } @@ -187,6 +199,13 @@ class socket_address_inet { const sockaddr_in* c_sockaddr_inet() const { return &m_sockaddr; } socket_address_inet6 to_mapped_address() const; + +#ifdef USE_UDNS + static socket_address_inet* cast_from(sockaddr* sa) { return reinterpret_cast(sa); } + static const socket_address_inet* cast_from(const sockaddr* sa) { return reinterpret_cast(sa); } + static socket_address_inet* cast_from_inet(sockaddr_in* sa) { return reinterpret_cast(sa); } + static const socket_address_inet* cast_from_inet(const sockaddr_in* sa) { return reinterpret_cast(sa); } +#endif bool operator == (const socket_address_inet& rhs) const; bool operator < (const socket_address_inet& rhs) const; diff --git a/libtorrent/src/net/socket_datagram.cc b/libtorrent/src/net/socket_datagram.cc index 7371a57b8..986ac1d2c 100644 --- a/libtorrent/src/net/socket_datagram.cc +++ b/libtorrent/src/net/socket_datagram.cc @@ -87,7 +87,7 @@ SocketDatagram::write_datagram(const void* buffer, unsigned int length, rak::soc #ifdef USE_UDNS int -SocketDatagram::write_datagram_ipv4(const void* buffer, unsigned int length, rak::socket_address* sa) { +SocketDatagram::write_datagram_ipv4(const void* buffer, unsigned int length, rak::socket_address_inet* sa) { if (length == 0) throw internal_error("Tried to send buffer length 0"); diff --git a/libtorrent/src/net/socket_datagram.h b/libtorrent/src/net/socket_datagram.h index a814c7a07..9a3b82e21 100644 --- a/libtorrent/src/net/socket_datagram.h +++ b/libtorrent/src/net/socket_datagram.h @@ -49,7 +49,7 @@ class SocketDatagram : public SocketBase { int read_datagram(void* buffer, unsigned int length, rak::socket_address* sa = NULL); int write_datagram(const void* buffer, unsigned int length, rak::socket_address* sa = NULL); #ifdef USE_UDNS - int write_datagram_ipv4(const void* buffer, unsigned int length, rak::socket_address* sa = NULL); + int write_datagram_ipv4(const void* buffer, unsigned int length, rak::socket_address_inet* sa = NULL); #endif }; diff --git a/libtorrent/src/net/socket_fd.cc b/libtorrent/src/net/socket_fd.cc index e54e45953..cdaba54c2 100644 --- a/libtorrent/src/net/socket_fd.cc +++ b/libtorrent/src/net/socket_fd.cc @@ -208,7 +208,7 @@ SocketFd::bind(const rak::socket_address& sa) { #ifdef USE_UDNS bool -SocketFd::bind_ipv4(const rak::socket_address& sa) { +SocketFd::bind_ipv4(const rak::socket_address_inet& sa) { check_valid(); return !::bind(m_fd, sa.c_sockaddr(), sa.length()); diff --git a/libtorrent/src/net/socket_fd.h b/libtorrent/src/net/socket_fd.h index 2cd7c6119..0e2117c0f 100644 --- a/libtorrent/src/net/socket_fd.h +++ b/libtorrent/src/net/socket_fd.h @@ -41,6 +41,9 @@ namespace rak { class socket_address; +#ifdef USE_UDNS + class socket_address_inet; +#endif } namespace torrent { @@ -88,7 +91,7 @@ class SocketFd { bool getsockname(rak::socket_address* sa); #ifdef USE_UDNS - bool bind_ipv4(const rak::socket_address& sa); + bool bind_ipv4(const rak::socket_address_inet& sa); #endif bool listen(int size); diff --git a/libtorrent/src/torrent/connection_manager.cc b/libtorrent/src/torrent/connection_manager.cc index 431372033..055488cd9 100644 --- a/libtorrent/src/torrent/connection_manager.cc +++ b/libtorrent/src/torrent/connection_manager.cc @@ -62,8 +62,8 @@ class UdnsAsyncResolver : public AsyncResolver { public: UdnsAsyncResolver(ConnectionManager *cm) : AsyncResolver(cm) {} - void *enqueue(const char *name, int family, resolver_callback *cbck) { - return m_udnsevent.enqueue_resolve(name, family, cbck); + void *enqueue(const char *name, resolver_callback *cbck) { + return m_udnsevent.enqueue_resolve(name, cbck); } void flush() { @@ -79,7 +79,7 @@ class UdnsAsyncResolver : public AsyncResolver { }; void -ConnectionManager::start_udp_announce(uint64_t idx, const sockaddr* sa, int err) { +ConnectionManager::start_udp_announce(uint64_t idx, const sockaddr_in* sa, int err) { if (m_tracker_udp_list[idx] != NULL) { m_tracker_udp_list[idx]->start_announce(sa, err); } @@ -135,7 +135,11 @@ class StubAsyncResolver : public AsyncResolver { #endif static void +#ifdef USE_UDNS +resolve_host(const char* host, int family, int socktype, legacy_resolver_callback slot) { +#else resolve_host(const char* host, int family, int socktype, resolver_callback slot) { +#endif if (manager->main_thread_main()->is_current()) thread_base::release_global_lock(); diff --git a/libtorrent/src/torrent/connection_manager.h b/libtorrent/src/torrent/connection_manager.h index 7efd33376..2ba238ced 100644 --- a/libtorrent/src/torrent/connection_manager.h +++ b/libtorrent/src/torrent/connection_manager.h @@ -61,7 +61,12 @@ typedef std::pair ThrottlePair; // The sockaddr argument in the result call is NULL if the resolve failed, // and the int holds the error code. +#ifdef USE_UDNS +typedef std::function resolver_callback; +typedef std::function legacy_resolver_callback; +#else typedef std::function resolver_callback; +#endif // Encapsulates whether we do genuine async resolution or fall back to sync. // In a build with USE_UDNS, these do genuine asynchronous DNS resolution. @@ -74,7 +79,11 @@ class LIBTORRENT_EXPORT AsyncResolver { // this queues a DNS resolve but doesn't send it. it doesn't execute any callbacks // and returns control immediately. the return value is an opaque identifier that // can be used to cancel the query (as long as the callback hasn't been executed yet): +#ifdef USE_UDNS + virtual void* enqueue(const char *name, resolver_callback *cbck) = 0; +#else virtual void* enqueue(const char *name, int family, resolver_callback *cbck) = 0; +#endif // this sends any queued resolves. it can execute arbitrary callbacks // before returning control: virtual void flush() = 0; @@ -130,7 +139,11 @@ class LIBTORRENT_EXPORT ConnectionManager { typedef std::function slot_filter_type; typedef std::function slot_throttle_type; +#ifdef USE_UDNS + typedef std::function slot_resolver_type; +#else typedef std::function slot_resolver_type; +#endif ConnectionManager(); ~ConnectionManager(); @@ -182,12 +195,8 @@ class LIBTORRENT_EXPORT ConnectionManager { void set_listen_port(port_type p) { m_listen_port = p; } void set_listen_backlog(int v); - void* enqueue_async_resolve(const char *name, int family, resolver_callback *cbck); - void flush_async_resolves(); - void cancel_async_resolve(void *query); - #ifdef USE_UDNS - void start_udp_announce(uint64_t idx, const sockaddr* sa, int err); + void start_udp_announce(uint64_t idx, const sockaddr_in* sa, int err); void null_udp_tracker(uint64_t idx) { m_tracker_udp_list[idx] = NULL; } void add_udp_tracker(TrackerUdp* tracker) { m_tracker_udp_list.push_back(tracker); } uint64_t get_udp_tracker_count() { return m_tracker_udp_list.size(); } diff --git a/libtorrent/src/tracker/tracker_udp.cc b/libtorrent/src/tracker/tracker_udp.cc index fb606d412..42afba69c 100644 --- a/libtorrent/src/tracker/tracker_udp.cc +++ b/libtorrent/src/tracker/tracker_udp.cc @@ -110,24 +110,25 @@ TrackerUdp::send_state(int state) { m_resolver_query = manager->connection_manager()->async_resolver().enqueue( m_hostname.c_str(), - AF_UNSPEC, #ifdef USE_UDNS manager->connection_manager()->get_resolver_callback(m_vec_idx) #else + AF_UNSPEC, &m_resolver_callback #endif ); manager->connection_manager()->async_resolver().flush(); } +#ifdef USE_UDNS void -TrackerUdp::start_announce(const sockaddr* sa, int err) { +TrackerUdp::start_announce(const sockaddr_in* sa, int err) { m_resolver_query = NULL; if (sa == NULL) return receive_failed("could not resolve hostname"); - m_connectAddress = *rak::socket_address::cast_from(sa); + m_connectAddress = *rak::socket_address_inet::cast_from_inet(sa); m_connectAddress.set_port(m_port); LT_LOG_TRACKER(DEBUG, "address found (address:%s)", m_connectAddress.address_str().c_str()); @@ -136,20 +137,50 @@ TrackerUdp::start_announce(const sockaddr* sa, int err) { return receive_failed("invalid tracker address"); // TODO: Make each of these a separate error... at the very least separate open and bind. -#ifdef USE_UDNS if (!get_fd().open_datagram_ipv4() || !get_fd().set_nonblock()) + return receive_failed("could not open UDP socket"); + + auto bind_address = rak::socket_address_inet::cast_from(manager->connection_manager()->bind_address()); + + if (bind_address->is_bindable() && !get_fd().bind_ipv4(*bind_address)) + return receive_failed("failed to bind socket to udp address '" + bind_address->pretty_address_str() + "' with error '" + rak::error_number::current().c_str() + "'"); + + m_readBuffer = new ReadBuffer; + m_writeBuffer = new WriteBuffer; + + prepare_connect_input(); + + manager->poll()->open(this); + manager->poll()->insert_read(this); + manager->poll()->insert_write(this); + manager->poll()->insert_error(this); + + m_tries = m_parent->info()->udp_tries(); + priority_queue_insert(&taskScheduler, &m_taskTimeout, (cachedTime + rak::timer::from_seconds(m_parent->info()->udp_timeout())).round_seconds()); +} #else +void +TrackerUdp::start_announce(const sockaddr* sa, int err) { + m_resolver_query = NULL; + + if (sa == NULL) + return receive_failed("could not resolve hostname"); + + m_connectAddress = *rak::socket_address::cast_from(sa); + m_connectAddress.set_port(m_port); + + LT_LOG_TRACKER(DEBUG, "address found (address:%s)", m_connectAddress.address_str().c_str()); + + if (!m_connectAddress.is_valid()) + return receive_failed("invalid tracker address"); + + // TODO: Make each of these a separate error... at the very least separate open and bind. if (!get_fd().open_datagram() || !get_fd().set_nonblock()) -#endif return receive_failed("could not open UDP socket"); auto bind_address = rak::socket_address::cast_from(manager->connection_manager()->bind_address()); -#ifdef USE_UDNS - if (bind_address->is_bindable() && !get_fd().bind_ipv4(*bind_address)) -#else if (bind_address->is_bindable() && !get_fd().bind(*bind_address)) -#endif return receive_failed("failed to bind socket to udp address '" + bind_address->pretty_address_str() + "' with error '" + rak::error_number::current().c_str() + "'"); m_readBuffer = new ReadBuffer; @@ -165,6 +196,7 @@ TrackerUdp::start_announce(const sockaddr* sa, int err) { m_tries = m_parent->info()->udp_tries(); priority_queue_insert(&taskScheduler, &m_taskTimeout, (cachedTime + rak::timer::from_seconds(m_parent->info()->udp_timeout())).round_seconds()); } +#endif void TrackerUdp::close() { diff --git a/libtorrent/src/tracker/tracker_udp.h b/libtorrent/src/tracker/tracker_udp.h index 6adc00ab5..582d416bb 100644 --- a/libtorrent/src/tracker/tracker_udp.h +++ b/libtorrent/src/tracker/tracker_udp.h @@ -75,8 +75,12 @@ class TrackerUdp : public SocketDatagram, public Tracker { virtual void event_read(); virtual void event_write(); virtual void event_error(); - + +#ifdef USE_UDNS + void start_announce(const sockaddr_in* sa, int err); +#else void start_announce(const sockaddr* sa, int err); +#endif private: void close_directly(); @@ -93,7 +97,12 @@ class TrackerUdp : public SocketDatagram, public Tracker { bool parse_udp_url(const std::string& url, hostname_type& hostname, int& port) const; +#ifdef USE_UDNS + rak::socket_address_inet m_connectAddress; +#else rak::socket_address m_connectAddress; +#endif + int m_port; std::string m_hostname; diff --git a/libtorrent/src/utils/udnsevent.cc b/libtorrent/src/utils/udnsevent.cc index c92aac2a0..b8de78a52 100644 --- a/libtorrent/src/utils/udnsevent.cc +++ b/libtorrent/src/utils/udnsevent.cc @@ -1,6 +1,7 @@ #include "config.h" #ifdef USE_UDNS +#include #include #include #include @@ -15,79 +16,37 @@ namespace torrent { -int udnserror_to_gaierror(int udnserror) { - switch (udnserror) { - case DNS_E_TEMPFAIL: - return EAI_AGAIN; - case DNS_E_PROTOCOL: - // this isn't quite right - return EAI_FAIL; - case DNS_E_NXDOMAIN: - return EAI_NONAME; - case DNS_E_NODATA: - return EAI_ADDRFAMILY; - case DNS_E_NOMEM: - return EAI_MEMORY; - case DNS_E_BADQUERY: - return EAI_NONAME; - default: - return EAI_ADDRFAMILY; - } -} +const int udnserror_to_gaierror[] = { + EAI_AGAIN, + EAI_AGAIN, + EAI_FAIL, + EAI_NONAME, + EAI_ADDRFAMILY, + EAI_MEMORY, + EAI_NONAME +}; // Compatibility layers so udns can call std::function callbacks. - void a4_callback_wrapper(struct ::dns_ctx *ctx, ::dns_rr_a4 *result, void *data) { - struct sockaddr_in sa; udns_query *query = static_cast(data); // udns will free the a4_query after this callback exits query->a4_query = NULL; if (result == NULL || result->dnsa4_nrr == 0) { - if (query->a6_query == NULL) { - // nothing more to do: call the callback with a failure status - if (*query->callback) { (*(query->callback))(NULL, udnserror_to_gaierror(::dns_status(ctx))); } - delete query; - } + // nothing more to do: call the callback with a failure status + (*(query->callback))(NULL, udnserror_to_gaierror[abs(::dns_status(ctx))]); + delete query; // else: return and wait to see if we get an a6 response } else { + struct sockaddr_in sa; sa.sin_family = AF_INET; sa.sin_port = 0; sa.sin_addr = result->dnsa4_addr[0]; - if (query->a6_query != NULL) { - ::dns_cancel(ctx, query->a6_query); - } - if (*query->callback) { (*query->callback)(reinterpret_cast(&sa), 0); } - delete query; - } -} - -void a6_callback_wrapper(struct ::dns_ctx *ctx, ::dns_rr_a6 *result, void *data) { - struct sockaddr_in6 sa; - udns_query *query = static_cast(data); - // udns will free the a6_query after this callback exits - query->a6_query = NULL; - - if (result == NULL || result->dnsa6_nrr == 0) { - if (query->a4_query == NULL) { - // nothing more to do: call the callback with a failure status - (*(query->callback))(NULL, udnserror_to_gaierror(::dns_status(ctx))); - delete query; - } - // else: return and wait to see if we get an a6 response - } else { - sa.sin6_family = AF_INET6; - sa.sin6_port = 0; - sa.sin6_addr = result->dnsa6_addr[0]; - if (query->a4_query != NULL) { - ::dns_cancel(ctx, query->a4_query); - } - (*query->callback)(reinterpret_cast(&sa), 0); + (*query->callback)(&sa, 0); delete query; } } - UdnsEvent::UdnsEvent() { // reinitialize the default context, no-op // TODO don't do this here --- do it once in the manager, or in rtorrent @@ -121,40 +80,23 @@ void UdnsEvent::event_write() { void UdnsEvent::event_error() { } -struct udns_query *UdnsEvent::enqueue_resolve(const char *name, int family, resolver_callback *callback) { - struct udns_query *query = new udns_query { NULL, NULL, callback, 0 }; - - if (family == AF_INET || family == AF_UNSPEC) { - query->a4_query = ::dns_submit_a4(m_ctx, name, 0, a4_callback_wrapper, query); - if (query->a4_query == NULL) { - // XXX udns does query parsing up front and will fail immediately - // during submission of malformed domain names, e.g., `..`. In order to - // maintain a clean interface, keep track of this query internally - // so we can call the callback later with a failure code - if (::dns_status(m_ctx) == DNS_E_BADQUERY) { - // this is what getaddrinfo(3) would return: - query->error = EAI_NONAME; - m_malformed_queries.push_back(query); - return query; - } else { - // unrecoverable errors, like ENOMEM - throw new internal_error("dns_submit_a4 failed"); - } - } - } - - if (family == AF_INET6) { - query->a6_query = ::dns_submit_a6(m_ctx, name, 0, a6_callback_wrapper, query); - if (query->a6_query == NULL) { - // it should be impossible for dns_submit_a6 to fail if dns_submit_a4 - // succeeded, but just in case, make it a hard failure: - if (::dns_status(m_ctx) == DNS_E_BADQUERY && query->a4_query == NULL) { - query->error = EAI_NONAME; - m_malformed_queries.push_back(query); - return query; - } else { - throw new internal_error("dns_submit_a6 failed"); - } +struct udns_query *UdnsEvent::enqueue_resolve(const char *name, resolver_callback *callback) { + struct udns_query *query = new udns_query { NULL, callback, 0 }; + + query->a4_query = ::dns_submit_a4(m_ctx, name, 0, a4_callback_wrapper, query); + if (query->a4_query == NULL) { + // XXX udns does query parsing up front and will fail immediately + // during submission of malformed domain names, e.g., `..`. In order to + // maintain a clean interface, keep track of this query internally + // so we can call the callback later with a failure code + if (::dns_status(m_ctx) == DNS_E_BADQUERY) { + // this is what getaddrinfo(3) would return: + query->error = EAI_NONAME; + m_malformed_queries.push_back(query); + return query; + } else { + // unrecoverable errors, like ENOMEM + throw new internal_error("dns_submit_a4 failed"); } } @@ -190,8 +132,6 @@ void UdnsEvent::cancel(struct udns_query *query) { if (query->a4_query != NULL) ::dns_cancel(m_ctx, query->a4_query); - if (query->a6_query != NULL) ::dns_cancel(m_ctx, query->a6_query); - auto it = std::find(std::begin(m_malformed_queries), std::end(m_malformed_queries), query); if (it != std::end(m_malformed_queries)) m_malformed_queries.erase(it); diff --git a/libtorrent/src/utils/udnsevent.h b/libtorrent/src/utils/udnsevent.h index f214814e2..cd4e51927 100644 --- a/libtorrent/src/utils/udnsevent.h +++ b/libtorrent/src/utils/udnsevent.h @@ -17,7 +17,6 @@ namespace torrent { struct udns_query { ::dns_query *a4_query; - ::dns_query *a6_query; resolver_callback *callback; int error; }; @@ -37,7 +36,7 @@ class UdnsEvent : public Event { // wraps udns's dns_submit_a[46] functions. they and it return control immediately, // without either sending outgoing UDP packets or executing callbacks: - udns_query* enqueue_resolve(const char *name, int family, resolver_callback *callback); + udns_query* enqueue_resolve(const char *name, resolver_callback *callback); // wraps the dns_timeouts function. it sends packets and can execute arbitrary // callbacks: void flush_resolves();