From 4318a6f1ac1eda1cbaae9472d76b6119503dbf8a Mon Sep 17 00:00:00 2001 From: Yat Ho Date: Thu, 13 Nov 2025 04:46:26 +0800 Subject: [PATCH] fix: caching a source address doesn't imply public internet connectivity (#7520) --- libtransmission/ip-cache.cc | 15 +++++++++++---- libtransmission/ip-cache.h | 6 +++--- libtransmission/net.h | 10 ++++++++++ libtransmission/session.cc | 2 +- libtransmission/session.h | 4 ++-- libtransmission/tr-udp.cc | 6 +++--- tests/libtransmission/ip-cache-test.cc | 8 ++++---- 7 files changed, 34 insertions(+), 17 deletions(-) diff --git a/libtransmission/ip-cache.cc b/libtransmission/ip-cache.cc index 4fb9e0044..3c0f3d27d 100644 --- a/libtransmission/ip-cache.cc +++ b/libtransmission/ip-cache.cc @@ -63,6 +63,15 @@ namespace global_source_ip_helpers // and check its source address. // // Since it's a UDP socket, this doesn't actually send any packets +// +// N.B. Successfully obtaining a source address does not imply +// connectivity to the given destination address, since all connect() +// does is setting the default remote address for subsequent send() and +// recv() calls. +// +// Having said that, the connect() step is still needed because on Windows, +// calling getsockname() might not return what we want before calling +// connect() if we are binding to 0.0.0.0 or ::. [[nodiscard]] std::optional get_source_address( tr_address const& dst_addr, tr_port dst_port, @@ -221,7 +230,7 @@ bool tr_ip_cache::set_global_addr(tr_address const& addr_new) noexcept void tr_ip_cache::update_addr(tr_address_type type) noexcept { update_source_addr(type); - if (global_source_addr(type)) + if (source_addr(type)) { update_global_addr(type); } @@ -272,11 +281,9 @@ void tr_ip_cache::update_source_addr(tr_address_type type) noexcept TR_ASSERT(is_updating_[type] == is_updating_t::YES); auto const protocol = tr_ip_protocol_to_sv(type); - auto err = 0; - auto const& source_addr = get_global_source_address(bind_addr(type), err); source_addr_checked_[type] = true; - if (source_addr) + if (auto const& source_addr = get_global_source_address(bind_addr(type), err); source_addr) { set_source_addr(*source_addr); tr_logAddDebug(fmt::format( diff --git a/libtransmission/ip-cache.h b/libtransmission/ip-cache.h index 32eff2c1d..ec045b732 100644 --- a/libtransmission/ip-cache.h +++ b/libtransmission/ip-cache.h @@ -28,10 +28,10 @@ * This class caches 3 useful info: * 1. Whether your machine supports the IP protocol * 2. Source address used for global connections - * 3. Global address + * 3. Global address (IPv4 public address/IPv6 global unicast address) * * The idea is, if this class successfully cached a source address, that means - * you have connectivity to the public internet. And if the global address is + * your system is capable in that IP protocol. And if the global address is * the same as the source address, then you are not behind a NAT. */ class tr_ip_cache @@ -70,7 +70,7 @@ public: return global_addr_[type]; } - [[nodiscard]] std::optional global_source_addr(tr_address_type type) const noexcept + [[nodiscard]] std::optional source_addr(tr_address_type type) const noexcept { auto const lock = std::shared_lock{ source_addr_mutex_[type] }; return source_addr_[type]; diff --git a/libtransmission/net.h b/libtransmission/net.h index feee17313..fd6d68d8d 100644 --- a/libtransmission/net.h +++ b/libtransmission/net.h @@ -257,6 +257,16 @@ struct tr_address return is_ipv6() && IN6_IS_ADDR_LINKLOCAL(&addr.addr6); } + [[nodiscard]] constexpr bool is_ipv4_loopback_address() const noexcept + { + return is_ipv4() && reinterpret_cast(&addr.addr4.s_addr)[0] == 127U; + } + + [[nodiscard]] constexpr bool is_ipv6_loopback_address() const noexcept + { + return is_ipv6() && IN6_IS_ADDR_LOOPBACK(&addr.addr6); + } + [[nodiscard]] std::optional from_ipv4_mapped() const noexcept; tr_address_type type = NUM_TR_AF_INET_TYPES; diff --git a/libtransmission/session.cc b/libtransmission/session.cc index 0c1e32343..572c4225a 100644 --- a/libtransmission/session.cc +++ b/libtransmission/session.cc @@ -447,7 +447,7 @@ tr_address tr_session::bind_address(tr_address_type type) const noexcept // if user provided an address, use it. // otherwise, if we can determine which one to use via global_source_address(ipv6) magic, use it. // otherwise, use any_ipv6 (::). - auto const source_addr = global_source_address(type); + auto const source_addr = source_address(type); auto const default_addr = source_addr && source_addr->is_global_unicast_address() ? *source_addr : tr_address::any(TR_AF_INET6); return tr_address::from_string(settings_.bind_address_ipv6).value_or(default_addr); diff --git a/libtransmission/session.h b/libtransmission/session.h index cf076cff1..c396e89b7 100644 --- a/libtransmission/session.h +++ b/libtransmission/session.h @@ -1045,10 +1045,10 @@ public: return ip_cache_.set_global_addr(addr); } - [[nodiscard]] std::optional global_source_address(tr_address_type type) const noexcept + [[nodiscard]] std::optional source_address(tr_address_type type) const noexcept { TR_ASSERT(tr_address::is_valid(type)); - return ip_cache_.global_source_addr(type); + return ip_cache_.source_addr(type); } [[nodiscard]] auto speed_limit(tr_direction const dir) const noexcept diff --git a/libtransmission/tr-udp.cc b/libtransmission/tr-udp.cc index 44e94b90a..a25d0a607 100644 --- a/libtransmission/tr-udp.cc +++ b/libtransmission/tr-udp.cc @@ -284,10 +284,10 @@ void tr_session::tr_udp_core::sendto(void const* buf, size_t buflen, struct sock return; } else if ( - addrport && addrport->address().is_global_unicast_address() && - !session_.global_source_address(tr_af_to_ip_protocol(to->sa_family))) + addrport && !addrport->address().is_ipv4_loopback_address() && !addrport->address().is_ipv6_loopback_address() && + !session_.source_address(tr_af_to_ip_protocol(to->sa_family))) { - // don't try to connect to a global address if we don't have connectivity to public internet + // don't try to send if we don't have a route in this IP protocol return; } else if (::sendto(sock, static_cast(buf), buflen, 0, to, tolen) != -1) diff --git a/tests/libtransmission/ip-cache-test.cc b/tests/libtransmission/ip-cache-test.cc index e572bf469..72f572002 100644 --- a/tests/libtransmission/ip-cache-test.cc +++ b/tests/libtransmission/ip-cache-test.cc @@ -175,13 +175,13 @@ TEST_F(IPCacheTest, globalSourceIPv4) ip_cache_ = std::make_unique(mediator); ip_cache_->update_source_addr(TR_AF_INET); - auto const addr = ip_cache_->global_source_addr(TR_AF_INET); + auto const addr = ip_cache_->source_addr(TR_AF_INET); if (!addr) { GTEST_SKIP() << "globalSourceIPv4 did not return an address, either:\n" << "1. globalSourceIPv4 is broken\n" << "2. Your system does not support IPv4\n" - << "3. You don't have IPv4 connectivity to public internet"; + << "3. You don't have an IPv4 address to your interface"; } EXPECT_TRUE(addr->is_ipv4()); } @@ -199,13 +199,13 @@ TEST_F(IPCacheTest, globalSourceIPv6) ip_cache_ = std::make_unique(mediator); ip_cache_->update_source_addr(TR_AF_INET6); - auto const addr = ip_cache_->global_source_addr(TR_AF_INET6); + auto const addr = ip_cache_->source_addr(TR_AF_INET6); if (!addr) { GTEST_SKIP() << "globalSourceIPv6 did not return an address, either:\n" << "1. globalSourceIPv6 is broken\n" << "2. Your system does not support IPv6\n" - << "3. You don't have IPv6 connectivity to public internet"; + << "3. You don't have an IPv6 address to your interface"; } EXPECT_TRUE(addr->is_ipv6()); }