From adb3a3e51de205cc69b1a58647c65c04fa6c6362 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 13 Apr 2023 13:31:08 -0400 Subject: [PATCH 01/12] configure: test for unix domain sockets Copied from https://github.com/bitcoin/bitcoin/pull/9979 Co-authored-by: laanwj <126646+laanwj@users.noreply.github.com> --- configure.ac | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 4f715158731..6a5c8722720 100644 --- a/configure.ac +++ b/configure.ac @@ -1195,10 +1195,23 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[ getauxval(AT_HWCAP); ]])], - [ AC_MSG_RESULT([yes]); HAVE_STRONG_GETAUXVAL=1; AC_DEFINE([HAVE_STRONG_GETAUXVAL], [1], [Define this symbol to build code that uses getauxval)]) ], + [ AC_MSG_RESULT([yes]); HAVE_STRONG_GETAUXVAL=1; AC_DEFINE([HAVE_STRONG_GETAUXVAL], [1], [Define this symbol to build code that uses getauxval]) ], [ AC_MSG_RESULT([no]); HAVE_STRONG_GETAUXVAL=0 ] ) +# Check for UNIX sockets +AC_MSG_CHECKING(for sockaddr_un) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ + #include + #include + ]], [[ + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + ]])], + [ AC_MSG_RESULT([yes]); AC_DEFINE([HAVE_SOCKADDR_UN], [1], [Define this symbol if platform supports unix domain sockets]) ], + [ AC_MSG_RESULT([no]); ] +) + have_any_system=no AC_MSG_CHECKING([for std::system]) AC_LINK_IFELSE( From bae86c8d318d06818aa75a9ebe3db864197f0bc6 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 26 May 2023 12:26:43 -0400 Subject: [PATCH 02/12] netbase: refactor CreateSock() to accept sa_family_t Also implement CService::GetSAFamily() to provide sa_family_t --- src/compat/compat.h | 7 +++++++ src/i2p.cpp | 2 +- src/net.cpp | 9 +++++---- src/netaddress.cpp | 13 +++++++++++++ src/netaddress.h | 5 +++++ src/netbase.cpp | 15 +++++---------- src/netbase.h | 10 +++++----- src/test/fuzz/fuzz.cpp | 2 +- src/test/i2p_tests.cpp | 8 ++++---- 9 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/compat/compat.h b/src/compat/compat.h index 9ff9a335f81..366c648ae72 100644 --- a/src/compat/compat.h +++ b/src/compat/compat.h @@ -32,6 +32,13 @@ #include // IWYU pragma: export #endif +// Windows does not have `sa_family_t` - it defines `sockaddr::sa_family` as `u_short`. +// Thus define `sa_family_t` on Windows too so that the rest of the code can use `sa_family_t`. +// See https://learn.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-sockaddr#syntax +#ifdef WIN32 +typedef u_short sa_family_t; +#endif + // We map Linux / BSD error functions and codes, to the equivalent // Windows definitions, and use the WSA* names throughout our code. // Note that glibc defines EWOULDBLOCK as EAGAIN (see errno.h). diff --git a/src/i2p.cpp b/src/i2p.cpp index 4b79a6826bd..2fce946e7d8 100644 --- a/src/i2p.cpp +++ b/src/i2p.cpp @@ -326,7 +326,7 @@ Session::Reply Session::SendRequestAndGetReply(const Sock& sock, std::unique_ptr Session::Hello() const { - auto sock = CreateSock(m_control_host); + auto sock = CreateSock(m_control_host.GetSAFamily()); if (!sock) { throw std::runtime_error("Cannot create socket"); diff --git a/src/net.cpp b/src/net.cpp index 7c82f01d757..d40c7109c2f 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -483,15 +483,16 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo addr_bind = CAddress{conn.me, NODE_NONE}; } } else if (use_proxy) { - sock = CreateSock(proxy.proxy); + sock = CreateSock(proxy.proxy.GetSAFamily()); if (!sock) { return nullptr; } + LogPrintLevel(BCLog::PROXY, BCLog::Level::Debug, "Using proxy: %s to connect to %s:%s\n", proxy.proxy.ToStringAddrPort(), addrConnect.ToStringAddr(), addrConnect.GetPort()); connected = ConnectThroughProxy(proxy, addrConnect.ToStringAddr(), addrConnect.GetPort(), *sock, nConnectTimeout, proxyConnectionFailed); } else { // no proxy needed (none set for target network) - sock = CreateSock(addrConnect); + sock = CreateSock(addrConnect.GetSAFamily()); if (!sock) { return nullptr; } @@ -504,7 +505,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo addrman.Attempt(addrConnect, fCountFailure); } } else if (pszDest && GetNameProxy(proxy)) { - sock = CreateSock(proxy.proxy); + sock = CreateSock(proxy.proxy.GetSAFamily()); if (!sock) { return nullptr; } @@ -2993,7 +2994,7 @@ bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, return false; } - std::unique_ptr sock = CreateSock(addrBind); + std::unique_ptr sock = CreateSock(addrBind.GetSAFamily()); if (!sock) { strError = strprintf(Untranslated("Couldn't open socket for incoming connections (socket returned error %s)"), NetworkErrorString(WSAGetLastError())); LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); diff --git a/src/netaddress.cpp b/src/netaddress.cpp index 7530334db1d..74ab6dd8d87 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -818,6 +818,19 @@ bool CService::SetSockAddr(const struct sockaddr *paddr) } } +sa_family_t CService::GetSAFamily() const +{ + switch (m_net) { + case NET_IPV4: + return AF_INET; + case NET_IPV6: + case NET_CJDNS: + return AF_INET6; + default: + return AF_UNSPEC; + } +} + uint16_t CService::GetPort() const { return port; diff --git a/src/netaddress.h b/src/netaddress.h index c697b7e0a33..c63bd4b4e5c 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -540,6 +540,11 @@ public: uint16_t GetPort() const; bool GetSockAddr(struct sockaddr* paddr, socklen_t* addrlen) const; bool SetSockAddr(const struct sockaddr* paddr); + /** + * Get the address family + * @returns AF_UNSPEC if unspecified + */ + [[nodiscard]] sa_family_t GetSAFamily() const; friend bool operator==(const CService& a, const CService& b); friend bool operator!=(const CService& a, const CService& b) { return !(a == b); } friend bool operator<(const CService& a, const CService& b); diff --git a/src/netbase.cpp b/src/netbase.cpp index 9fbd9f7dea3..973d888722d 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -444,18 +444,13 @@ bool Socks5(const std::string& strDest, uint16_t port, const ProxyCredentials* a } } -std::unique_ptr CreateSockTCP(const CService& address_family) +std::unique_ptr CreateSockOS(sa_family_t address_family) { - // Create a sockaddr from the specified service. - struct sockaddr_storage sockaddr; - socklen_t len = sizeof(sockaddr); - if (!address_family.GetSockAddr((struct sockaddr*)&sockaddr, &len)) { - LogPrintf("Cannot create socket for %s: unsupported network\n", address_family.ToStringAddrPort()); - return nullptr; - } + // Not IPv4 or IPv6 + if (address_family == AF_UNSPEC) return nullptr; // Create a TCP socket in the address family of the specified service. - SOCKET hSocket = socket(((struct sockaddr*)&sockaddr)->sa_family, SOCK_STREAM, IPPROTO_TCP); + SOCKET hSocket = socket(address_family, SOCK_STREAM, IPPROTO_TCP); if (hSocket == INVALID_SOCKET) { return nullptr; } @@ -493,7 +488,7 @@ std::unique_ptr CreateSockTCP(const CService& address_family) return sock; } -std::function(const CService&)> CreateSock = CreateSockTCP; +std::function(const sa_family_t&)> CreateSock = CreateSockOS; template static void LogConnectFailure(bool manual_connection, const char* fmt, const Args&... args) { diff --git a/src/netbase.h b/src/netbase.h index 1bd95ba0d97..6f2d86f1536 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -229,16 +229,16 @@ CService LookupNumeric(const std::string& name, uint16_t portDefault = 0, DNSLoo CSubNet LookupSubNet(const std::string& subnet_str); /** - * Create a TCP socket in the given address family. - * @param[in] address_family The socket is created in the same address family as this address. + * Create a socket in the given address family. + * @param[in] address_family to use for the socket. * @return pointer to the created Sock object or unique_ptr that owns nothing in case of failure */ -std::unique_ptr CreateSockTCP(const CService& address_family); +std::unique_ptr CreateSockOS(sa_family_t address_family); /** - * Socket factory. Defaults to `CreateSockTCP()`, but can be overridden by unit tests. + * Socket factory. Defaults to `CreateSockOS()`, but can be overridden by unit tests. */ -extern std::function(const CService&)> CreateSock; +extern std::function(const sa_family_t&)> CreateSock; /** * Try to connect to the specified service on the specified socket. diff --git a/src/test/fuzz/fuzz.cpp b/src/test/fuzz/fuzz.cpp index 6de480ff154..d1fb31644d5 100644 --- a/src/test/fuzz/fuzz.cpp +++ b/src/test/fuzz/fuzz.cpp @@ -81,7 +81,7 @@ static const TypeTestOneInput* g_test_one_input{nullptr}; void initialize() { // Terminate immediately if a fuzzing harness ever tries to create a TCP socket. - CreateSock = [](const CService&) -> std::unique_ptr { std::terminate(); }; + CreateSock = [](const sa_family_t&) -> std::unique_ptr { std::terminate(); }; // Terminate immediately if a fuzzing harness ever tries to perform a DNS lookup. g_dns_lookup = [](const std::string& name, bool allow_lookup) { diff --git a/src/test/i2p_tests.cpp b/src/test/i2p_tests.cpp index f80f07d190b..ef1d0b659cd 100644 --- a/src/test/i2p_tests.cpp +++ b/src/test/i2p_tests.cpp @@ -38,7 +38,7 @@ public: private: const BCLog::Level m_prev_log_level; - const std::function(const CService&)> m_create_sock_orig; + const std::function(const sa_family_t&)> m_create_sock_orig; }; BOOST_FIXTURE_TEST_SUITE(i2p_tests, EnvTestingSetup) @@ -46,7 +46,7 @@ BOOST_FIXTURE_TEST_SUITE(i2p_tests, EnvTestingSetup) BOOST_AUTO_TEST_CASE(unlimited_recv) { // Mock CreateSock() to create MockSock. - CreateSock = [](const CService&) { + CreateSock = [](const sa_family_t&) { return std::make_unique(std::string(i2p::sam::MAX_MSG_SIZE + 1, 'a')); }; @@ -66,7 +66,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv) BOOST_AUTO_TEST_CASE(listen_ok_accept_fail) { size_t num_sockets{0}; - CreateSock = [&num_sockets](const CService&) { + CreateSock = [&num_sockets](const sa_family_t&) { // clang-format off ++num_sockets; // First socket is the control socket for creating the session. @@ -130,7 +130,7 @@ BOOST_AUTO_TEST_CASE(damaged_private_key) { const auto CreateSockOrig = CreateSock; - CreateSock = [](const CService&) { + CreateSock = [](const sa_family_t&) { return std::make_unique("HELLO REPLY RESULT=OK VERSION=3.1\n" "SESSION STATUS RESULT=OK DESTINATION=\n"); }; From 74f568cb6fd5c74b7b9bf0ce69876430746a53b1 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 26 May 2023 14:05:28 -0400 Subject: [PATCH 03/12] netbase: allow CreateSock() to create UNIX sockets if supported --- src/netbase.cpp | 35 ++++++++++++++++++++++++++--------- src/netbase.h | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/netbase.cpp b/src/netbase.cpp index 973d888722d..bb65f412a21 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -3,6 +3,10 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#if defined(HAVE_CONFIG_H) +#include +#endif + #include #include @@ -21,6 +25,10 @@ #include #include +#if HAVE_SOCKADDR_UN +#include +#endif + // Settings static GlobalMutex g_proxyinfo_mutex; static Proxy proxyInfo[NET_MAX] GUARDED_BY(g_proxyinfo_mutex); @@ -446,11 +454,16 @@ bool Socks5(const std::string& strDest, uint16_t port, const ProxyCredentials* a std::unique_ptr CreateSockOS(sa_family_t address_family) { - // Not IPv4 or IPv6 + // Not IPv4, IPv6 or UNIX if (address_family == AF_UNSPEC) return nullptr; - // Create a TCP socket in the address family of the specified service. - SOCKET hSocket = socket(address_family, SOCK_STREAM, IPPROTO_TCP); + int protocol{IPPROTO_TCP}; +#if HAVE_SOCKADDR_UN + if (address_family == AF_UNIX) protocol = 0; +#endif + + // Create a socket in the specified address family. + SOCKET hSocket = socket(address_family, SOCK_STREAM, protocol); if (hSocket == INVALID_SOCKET) { return nullptr; } @@ -474,17 +487,21 @@ std::unique_ptr CreateSockOS(sa_family_t address_family) } #endif - // Set the no-delay option (disable Nagle's algorithm) on the TCP socket. - const int on{1}; - if (sock->SetSockOpt(IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == SOCKET_ERROR) { - LogPrint(BCLog::NET, "Unable to set TCP_NODELAY on a newly created socket, continuing anyway\n"); - } - // Set the non-blocking option on the socket. if (!sock->SetNonBlocking()) { LogPrintf("Error setting socket to non-blocking: %s\n", NetworkErrorString(WSAGetLastError())); return nullptr; } + +#if HAVE_SOCKADDR_UN + if (address_family == AF_UNIX) return sock; +#endif + + // Set the no-delay option (disable Nagle's algorithm) on the TCP socket. + const int on{1}; + if (sock->SetSockOpt(IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == SOCKET_ERROR) { + LogPrint(BCLog::NET, "Unable to set TCP_NODELAY on a newly created socket, continuing anyway\n"); + } return sock; } diff --git a/src/netbase.h b/src/netbase.h index 6f2d86f1536..e772a1c046b 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -229,7 +229,7 @@ CService LookupNumeric(const std::string& name, uint16_t portDefault = 0, DNSLoo CSubNet LookupSubNet(const std::string& subnet_str); /** - * Create a socket in the given address family. + * Create a TCP or UNIX socket in the given address family. * @param[in] address_family to use for the socket. * @return pointer to the created Sock object or unique_ptr that owns nothing in case of failure */ From 3a7d6548effa6cd9a4a5413b690c2fd85da4ef65 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 12 Jul 2023 12:18:52 -0400 Subject: [PATCH 04/12] net: move CreateSock() calls from ConnectNode() to netbase methods --- src/i2p.cpp | 6 +---- src/net.cpp | 25 ++++-------------- src/netbase.cpp | 67 ++++++++++++++++++++++++++++--------------------- src/netbase.h | 33 +++++++++++------------- 4 files changed, 59 insertions(+), 72 deletions(-) diff --git a/src/i2p.cpp b/src/i2p.cpp index 2fce946e7d8..fc9e741f40d 100644 --- a/src/i2p.cpp +++ b/src/i2p.cpp @@ -326,13 +326,9 @@ Session::Reply Session::SendRequestAndGetReply(const Sock& sock, std::unique_ptr Session::Hello() const { - auto sock = CreateSock(m_control_host.GetSAFamily()); + auto sock = ConnectDirectly(m_control_host, true); if (!sock) { - throw std::runtime_error("Cannot create socket"); - } - - if (!ConnectSocketDirectly(m_control_host, *sock, nConnectTimeout, true)) { throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToStringAddrPort())); } diff --git a/src/net.cpp b/src/net.cpp index d40c7109c2f..345c1c8b4b3 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -442,7 +442,6 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo } // Connect - bool connected = false; std::unique_ptr sock; Proxy proxy; CAddress addr_bind; @@ -455,6 +454,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo if (addrConnect.IsI2P() && use_proxy) { i2p::Connection conn; + bool connected{false}; if (m_i2p_sam_session) { connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed); @@ -483,21 +483,11 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo addr_bind = CAddress{conn.me, NODE_NONE}; } } else if (use_proxy) { - sock = CreateSock(proxy.proxy.GetSAFamily()); - if (!sock) { - return nullptr; - } LogPrintLevel(BCLog::PROXY, BCLog::Level::Debug, "Using proxy: %s to connect to %s:%s\n", proxy.proxy.ToStringAddrPort(), addrConnect.ToStringAddr(), addrConnect.GetPort()); - connected = ConnectThroughProxy(proxy, addrConnect.ToStringAddr(), addrConnect.GetPort(), - *sock, nConnectTimeout, proxyConnectionFailed); + sock = ConnectThroughProxy(proxy, addrConnect.ToStringAddr(), addrConnect.GetPort(), proxyConnectionFailed); } else { // no proxy needed (none set for target network) - sock = CreateSock(addrConnect.GetSAFamily()); - if (!sock) { - return nullptr; - } - connected = ConnectSocketDirectly(addrConnect, *sock, nConnectTimeout, - conn_type == ConnectionType::MANUAL); + sock = ConnectDirectly(addrConnect, conn_type == ConnectionType::MANUAL); } if (!proxyConnectionFailed) { // If a connection to the node was attempted, and failure (if any) is not caused by a problem connecting to @@ -505,18 +495,13 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo addrman.Attempt(addrConnect, fCountFailure); } } else if (pszDest && GetNameProxy(proxy)) { - sock = CreateSock(proxy.proxy.GetSAFamily()); - if (!sock) { - return nullptr; - } std::string host; uint16_t port{default_port}; SplitHostPort(std::string(pszDest), port, host); bool proxyConnectionFailed; - connected = ConnectThroughProxy(proxy, host, port, *sock, nConnectTimeout, - proxyConnectionFailed); + sock = ConnectThroughProxy(proxy, host, port, proxyConnectionFailed); } - if (!connected) { + if (!sock) { return nullptr; } diff --git a/src/netbase.cpp b/src/netbase.cpp index bb65f412a21..93a84a73b0d 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -517,18 +517,24 @@ static void LogConnectFailure(bool manual_connection, const char* fmt, const Arg } } -bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nTimeout, bool manual_connection) +std::unique_ptr ConnectDirectly(const CService& dest, bool manual_connection) { + auto sock = CreateSock(dest.GetSAFamily()); + if (!sock) { + LogPrintLevel(BCLog::NET, BCLog::Level::Error, "Cannot create a socket for connecting to %s\n", dest.ToStringAddrPort()); + return {}; + } + // Create a sockaddr from the specified service. struct sockaddr_storage sockaddr; socklen_t len = sizeof(sockaddr); - if (!addrConnect.GetSockAddr((struct sockaddr*)&sockaddr, &len)) { - LogPrintf("Cannot connect to %s: unsupported network\n", addrConnect.ToStringAddrPort()); - return false; + if (!dest.GetSockAddr((struct sockaddr*)&sockaddr, &len)) { + LogPrintf("Cannot connect to %s: unsupported network\n", dest.ToStringAddrPort()); + return {}; } - // Connect to the addrConnect service on the hSocket socket. - if (sock.Connect(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) { + // Connect to the dest service on the hSocket socket. + if (sock->Connect(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) { int nErr = WSAGetLastError(); // WSAEINVAL is here because some legacy version of winsock uses it if (nErr == WSAEINPROGRESS || nErr == WSAEWOULDBLOCK || nErr == WSAEINVAL) @@ -538,14 +544,14 @@ bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nT // synchronously to check for successful connection with a timeout. const Sock::Event requested = Sock::RECV | Sock::SEND; Sock::Event occurred; - if (!sock.Wait(std::chrono::milliseconds{nTimeout}, requested, &occurred)) { + if (!sock->Wait(std::chrono::milliseconds{nConnectTimeout}, requested, &occurred)) { LogPrintf("wait for connect to %s failed: %s\n", - addrConnect.ToStringAddrPort(), + dest.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); - return false; + return {}; } else if (occurred == 0) { - LogPrint(BCLog::NET, "connection attempt to %s timed out\n", addrConnect.ToStringAddrPort()); - return false; + LogPrint(BCLog::NET, "connection attempt to %s timed out\n", dest.ToStringAddrPort()); + return {}; } // Even if the wait was successful, the connect might not @@ -554,17 +560,17 @@ bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nT // sockerr here. int sockerr; socklen_t sockerr_len = sizeof(sockerr); - if (sock.GetSockOpt(SOL_SOCKET, SO_ERROR, (sockopt_arg_type)&sockerr, &sockerr_len) == + if (sock->GetSockOpt(SOL_SOCKET, SO_ERROR, (sockopt_arg_type)&sockerr, &sockerr_len) == SOCKET_ERROR) { - LogPrintf("getsockopt() for %s failed: %s\n", addrConnect.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); - return false; + LogPrintf("getsockopt() for %s failed: %s\n", dest.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); + return {}; } if (sockerr != 0) { LogConnectFailure(manual_connection, "connect() to %s failed after wait: %s", - addrConnect.ToStringAddrPort(), + dest.ToStringAddrPort(), NetworkErrorString(sockerr)); - return false; + return {}; } } #ifdef WIN32 @@ -573,11 +579,11 @@ bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nT else #endif { - LogConnectFailure(manual_connection, "connect() to %s failed: %s", addrConnect.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); - return false; + LogConnectFailure(manual_connection, "connect() to %s failed: %s", dest.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); + return {}; } } - return true; + return sock; } bool SetProxy(enum Network net, const Proxy &addrProxy) { @@ -628,27 +634,32 @@ bool IsProxy(const CNetAddr &addr) { return false; } -bool ConnectThroughProxy(const Proxy& proxy, const std::string& strDest, uint16_t port, const Sock& sock, int nTimeout, bool& outProxyConnectionFailed) +std::unique_ptr ConnectThroughProxy(const Proxy& proxy, + const std::string& dest, + uint16_t port, + bool& proxy_connection_failed) { // first connect to proxy server - if (!ConnectSocketDirectly(proxy.proxy, sock, nTimeout, true)) { - outProxyConnectionFailed = true; - return false; + auto sock = ConnectDirectly(proxy.proxy, /*manual_connection=*/true); + if (!sock) { + proxy_connection_failed = true; + return {}; } + // do socks negotiation if (proxy.randomize_credentials) { ProxyCredentials random_auth; static std::atomic_int counter(0); random_auth.username = random_auth.password = strprintf("%i", counter++); - if (!Socks5(strDest, port, &random_auth, sock)) { - return false; + if (!Socks5(dest, port, &random_auth, *sock)) { + return {}; } } else { - if (!Socks5(strDest, port, nullptr, sock)) { - return false; + if (!Socks5(dest, port, nullptr, *sock)) { + return {}; } } - return true; + return sock; } CSubNet LookupSubNet(const std::string& subnet_str) diff --git a/src/netbase.h b/src/netbase.h index e772a1c046b..c54056d25c5 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -241,35 +241,30 @@ std::unique_ptr CreateSockOS(sa_family_t address_family); extern std::function(const sa_family_t&)> CreateSock; /** - * Try to connect to the specified service on the specified socket. + * Create a socket and try to connect to the specified service. * - * @param addrConnect The service to which to connect. - * @param sock The socket on which to connect. - * @param nTimeout Wait this many milliseconds for the connection to be - * established. - * @param manual_connection Whether or not the connection was manually requested - * (e.g. through the addnode RPC) + * @param[in] dest The service to which to connect. + * @param[in] manual_connection Whether or not the connection was manually requested (e.g. through the addnode RPC) * - * @returns Whether or not a connection was successfully made. + * @returns the connected socket if the operation succeeded, empty unique_ptr otherwise */ -bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nTimeout, bool manual_connection); +std::unique_ptr ConnectDirectly(const CService& dest, bool manual_connection); /** * Connect to a specified destination service through a SOCKS5 proxy by first * connecting to the SOCKS5 proxy. * - * @param proxy The SOCKS5 proxy. - * @param strDest The destination service to which to connect. - * @param port The destination port. - * @param sock The socket on which to connect to the SOCKS5 proxy. - * @param nTimeout Wait this many milliseconds for the connection to the SOCKS5 - * proxy to be established. - * @param[out] outProxyConnectionFailed Whether or not the connection to the - * SOCKS5 proxy failed. + * @param[in] proxy The SOCKS5 proxy. + * @param[in] dest The destination service to which to connect. + * @param[in] port The destination port. + * @param[out] proxy_connection_failed Whether or not the connection to the SOCKS5 proxy failed. * - * @returns Whether or not the operation succeeded. + * @returns the connected socket if the operation succeeded. Otherwise an empty unique_ptr. */ -bool ConnectThroughProxy(const Proxy& proxy, const std::string& strDest, uint16_t port, const Sock& sock, int nTimeout, bool& outProxyConnectionFailed); +std::unique_ptr ConnectThroughProxy(const Proxy& proxy, + const std::string& dest, + uint16_t port, + bool& proxy_connection_failed); /** * Interrupt SOCKS5 reads or writes. From a89c3f59dc44eaf4f59912c1accfc0ce5d61933a Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 26 May 2023 14:21:43 -0400 Subject: [PATCH 05/12] netbase: extend Proxy class to wrap UNIX socket as well as TCP --- src/net.cpp | 2 +- src/netbase.cpp | 18 ++++++++++++++++++ src/netbase.h | 39 +++++++++++++++++++++++++++++++++++---- src/rpc/net.cpp | 2 +- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 345c1c8b4b3..2cc85aa488a 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -483,7 +483,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo addr_bind = CAddress{conn.me, NODE_NONE}; } } else if (use_proxy) { - LogPrintLevel(BCLog::PROXY, BCLog::Level::Debug, "Using proxy: %s to connect to %s:%s\n", proxy.proxy.ToStringAddrPort(), addrConnect.ToStringAddr(), addrConnect.GetPort()); + LogPrintLevel(BCLog::PROXY, BCLog::Level::Debug, "Using proxy: %s to connect to %s:%s\n", proxy.ToString(), addrConnect.ToStringAddr(), addrConnect.GetPort()); sock = ConnectThroughProxy(proxy, addrConnect.ToStringAddr(), addrConnect.GetPort(), proxyConnectionFailed); } else { // no proxy needed (none set for target network) diff --git a/src/netbase.cpp b/src/netbase.cpp index 93a84a73b0d..d3c3c36b370 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -216,6 +216,24 @@ CService LookupNumeric(const std::string& name, uint16_t portDefault, DNSLookupF return Lookup(name, portDefault, /*fAllowLookup=*/false, dns_lookup_function).value_or(CService{}); } +bool IsUnixSocketPath(const std::string& name) +{ +#if HAVE_SOCKADDR_UN + if (name.find(ADDR_PREFIX_UNIX) != 0) return false; + + // Split off "unix:" prefix + std::string str{name.substr(ADDR_PREFIX_UNIX.length())}; + + // Path size limit is platform-dependent + // see https://manpages.ubuntu.com/manpages/xenial/en/man7/unix.7.html + if (str.size() + 1 > sizeof(((sockaddr_un*)nullptr)->sun_path)) return false; + + return true; +#else + return false; +#endif +} + /** SOCKS version */ enum SOCKSVersion: uint8_t { SOCKS4 = 0x04, diff --git a/src/netbase.h b/src/netbase.h index c54056d25c5..e3b7da470c8 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -27,6 +27,9 @@ static const int DEFAULT_CONNECT_TIMEOUT = 5000; //! -dns default static const int DEFAULT_NAME_LOOKUP = true; +/** Prefix for unix domain socket addresses (which are local filesystem paths) */ +const std::string ADDR_PREFIX_UNIX = "unix:"; + enum class ConnectionDirection { None = 0, In = (1U << 0), @@ -43,16 +46,44 @@ static inline bool operator&(ConnectionDirection a, ConnectionDirection b) { return (underlying(a) & underlying(b)); } +/** + * Check if a string is a valid UNIX domain socket path + * + * @param name The string provided by the user representing a local path + * + * @returns Whether the string has proper format, length, and points to an existing file path + */ +bool IsUnixSocketPath(const std::string& name); + class Proxy { public: - Proxy(): randomize_credentials(false) {} - explicit Proxy(const CService &_proxy, bool _randomize_credentials=false): proxy(_proxy), randomize_credentials(_randomize_credentials) {} - - bool IsValid() const { return proxy.IsValid(); } + Proxy(): m_is_unix_socket(false), randomize_credentials(false) {} + explicit Proxy(const CService &_proxy, bool _randomize_credentials=false): proxy(_proxy), m_is_unix_socket(false), randomize_credentials(_randomize_credentials) {} + explicit Proxy(const std::string path, bool _randomize_credentials=false): m_unix_socket_path(path), m_is_unix_socket(true), randomize_credentials(_randomize_credentials) {} CService proxy; + std::string m_unix_socket_path; + bool m_is_unix_socket; bool randomize_credentials; + + bool IsValid() const + { + if (m_is_unix_socket) return IsUnixSocketPath(m_unix_socket_path); + return proxy.IsValid(); + } + + sa_family_t GetFamily() const + { + if (m_is_unix_socket) return AF_UNIX; + return proxy.GetSAFamily(); + } + + std::string ToString() const + { + if (m_is_unix_socket) return m_unix_socket_path; + return proxy.ToStringAddrPort(); + } }; /** Credentials for proxy authentication */ diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 5e6f42b5965..be07e6c2d5a 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -607,7 +607,7 @@ static UniValue GetNetworksInfo() obj.pushKV("name", GetNetworkName(network)); obj.pushKV("limited", !g_reachable_nets.Contains(network)); obj.pushKV("reachable", g_reachable_nets.Contains(network)); - obj.pushKV("proxy", proxy.IsValid() ? proxy.proxy.ToStringAddrPort() : std::string()); + obj.pushKV("proxy", proxy.IsValid() ? proxy.ToString() : std::string()); obj.pushKV("proxy_randomize_credentials", proxy.randomize_credentials); networks.push_back(obj); } From ac2ecf3182fb5ad9bcd41540b19382376114d6ee Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 5 Jun 2023 11:43:37 -0400 Subject: [PATCH 06/12] proxy: rename randomize_credentials to m_randomize_credentials --- src/netbase.cpp | 2 +- src/netbase.h | 8 ++++---- src/rpc/net.cpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/netbase.cpp b/src/netbase.cpp index d3c3c36b370..c8735bd43de 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -665,7 +665,7 @@ std::unique_ptr ConnectThroughProxy(const Proxy& proxy, } // do socks negotiation - if (proxy.randomize_credentials) { + if (proxy.m_randomize_credentials) { ProxyCredentials random_auth; static std::atomic_int counter(0); random_auth.username = random_auth.password = strprintf("%i", counter++); diff --git a/src/netbase.h b/src/netbase.h index e3b7da470c8..0cefddafead 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -58,14 +58,14 @@ bool IsUnixSocketPath(const std::string& name); class Proxy { public: - Proxy(): m_is_unix_socket(false), randomize_credentials(false) {} - explicit Proxy(const CService &_proxy, bool _randomize_credentials=false): proxy(_proxy), m_is_unix_socket(false), randomize_credentials(_randomize_credentials) {} - explicit Proxy(const std::string path, bool _randomize_credentials=false): m_unix_socket_path(path), m_is_unix_socket(true), randomize_credentials(_randomize_credentials) {} + Proxy() : m_is_unix_socket(false), m_randomize_credentials(false) {} + explicit Proxy(const CService& _proxy, bool _randomize_credentials = false) : proxy(_proxy), m_is_unix_socket(false), m_randomize_credentials(_randomize_credentials) {} + explicit Proxy(const std::string path, bool _randomize_credentials = false) : m_unix_socket_path(path), m_is_unix_socket(true), m_randomize_credentials(_randomize_credentials) {} CService proxy; std::string m_unix_socket_path; bool m_is_unix_socket; - bool randomize_credentials; + bool m_randomize_credentials; bool IsValid() const { diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index be07e6c2d5a..3fa2b184953 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -608,7 +608,7 @@ static UniValue GetNetworksInfo() obj.pushKV("limited", !g_reachable_nets.Contains(network)); obj.pushKV("reachable", g_reachable_nets.Contains(network)); obj.pushKV("proxy", proxy.IsValid() ? proxy.ToString() : std::string()); - obj.pushKV("proxy_randomize_credentials", proxy.randomize_credentials); + obj.pushKV("proxy_randomize_credentials", proxy.m_randomize_credentials); networks.push_back(obj); } return networks; From d9318a37ec09fe0b002815a7e48710e530620ae2 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 12 Jul 2023 16:10:05 -0400 Subject: [PATCH 07/12] net: split ConnectToSocket() from ConnectDirectly() for unix sockets --- src/netbase.cpp | 148 +++++++++++++++++++++++++++++++----------------- src/netbase.h | 2 + 2 files changed, 99 insertions(+), 51 deletions(-) diff --git a/src/netbase.cpp b/src/netbase.cpp index c8735bd43de..6af5d10ab1f 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -535,6 +535,61 @@ static void LogConnectFailure(bool manual_connection, const char* fmt, const Arg } } +static bool ConnectToSocket(const Sock& sock, struct sockaddr* sockaddr, socklen_t len, const std::string& dest_str, bool manual_connection) +{ + // Connect to `sockaddr` using `sock`. + if (sock.Connect(sockaddr, len) == SOCKET_ERROR) { + int nErr = WSAGetLastError(); + // WSAEINVAL is here because some legacy version of winsock uses it + if (nErr == WSAEINPROGRESS || nErr == WSAEWOULDBLOCK || nErr == WSAEINVAL) + { + // Connection didn't actually fail, but is being established + // asynchronously. Thus, use async I/O api (select/poll) + // synchronously to check for successful connection with a timeout. + const Sock::Event requested = Sock::RECV | Sock::SEND; + Sock::Event occurred; + if (!sock.Wait(std::chrono::milliseconds{nConnectTimeout}, requested, &occurred)) { + LogPrintf("wait for connect to %s failed: %s\n", + dest_str, + NetworkErrorString(WSAGetLastError())); + return false; + } else if (occurred == 0) { + LogPrint(BCLog::NET, "connection attempt to %s timed out\n", dest_str); + return false; + } + + // Even if the wait was successful, the connect might not + // have been successful. The reason for this failure is hidden away + // in the SO_ERROR for the socket in modern systems. We read it into + // sockerr here. + int sockerr; + socklen_t sockerr_len = sizeof(sockerr); + if (sock.GetSockOpt(SOL_SOCKET, SO_ERROR, (sockopt_arg_type)&sockerr, &sockerr_len) == + SOCKET_ERROR) { + LogPrintf("getsockopt() for %s failed: %s\n", dest_str, NetworkErrorString(WSAGetLastError())); + return false; + } + if (sockerr != 0) { + LogConnectFailure(manual_connection, + "connect() to %s failed after wait: %s", + dest_str, + NetworkErrorString(sockerr)); + return false; + } + } +#ifdef WIN32 + else if (WSAGetLastError() != WSAEISCONN) +#else + else +#endif + { + LogConnectFailure(manual_connection, "connect() to %s failed: %s", dest_str, NetworkErrorString(WSAGetLastError())); + return false; + } + } + return true; +} + std::unique_ptr ConnectDirectly(const CService& dest, bool manual_connection) { auto sock = CreateSock(dest.GetSAFamily()); @@ -547,63 +602,54 @@ std::unique_ptr ConnectDirectly(const CService& dest, bool manual_connecti struct sockaddr_storage sockaddr; socklen_t len = sizeof(sockaddr); if (!dest.GetSockAddr((struct sockaddr*)&sockaddr, &len)) { - LogPrintf("Cannot connect to %s: unsupported network\n", dest.ToStringAddrPort()); + LogPrintf("Cannot get sockaddr for %s: unsupported network\n", dest.ToStringAddrPort()); return {}; } - // Connect to the dest service on the hSocket socket. - if (sock->Connect(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) { - int nErr = WSAGetLastError(); - // WSAEINVAL is here because some legacy version of winsock uses it - if (nErr == WSAEINPROGRESS || nErr == WSAEWOULDBLOCK || nErr == WSAEINVAL) - { - // Connection didn't actually fail, but is being established - // asynchronously. Thus, use async I/O api (select/poll) - // synchronously to check for successful connection with a timeout. - const Sock::Event requested = Sock::RECV | Sock::SEND; - Sock::Event occurred; - if (!sock->Wait(std::chrono::milliseconds{nConnectTimeout}, requested, &occurred)) { - LogPrintf("wait for connect to %s failed: %s\n", - dest.ToStringAddrPort(), - NetworkErrorString(WSAGetLastError())); - return {}; - } else if (occurred == 0) { - LogPrint(BCLog::NET, "connection attempt to %s timed out\n", dest.ToStringAddrPort()); - return {}; - } - - // Even if the wait was successful, the connect might not - // have been successful. The reason for this failure is hidden away - // in the SO_ERROR for the socket in modern systems. We read it into - // sockerr here. - int sockerr; - socklen_t sockerr_len = sizeof(sockerr); - if (sock->GetSockOpt(SOL_SOCKET, SO_ERROR, (sockopt_arg_type)&sockerr, &sockerr_len) == - SOCKET_ERROR) { - LogPrintf("getsockopt() for %s failed: %s\n", dest.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); - return {}; - } - if (sockerr != 0) { - LogConnectFailure(manual_connection, - "connect() to %s failed after wait: %s", - dest.ToStringAddrPort(), - NetworkErrorString(sockerr)); - return {}; - } - } -#ifdef WIN32 - else if (WSAGetLastError() != WSAEISCONN) -#else - else -#endif - { - LogConnectFailure(manual_connection, "connect() to %s failed: %s", dest.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); - return {}; - } + if (!ConnectToSocket(*sock, (struct sockaddr*)&sockaddr, len, dest.ToStringAddrPort(), manual_connection)) { + LogPrintf("Cannot connect to socket for %s\n", dest.ToStringAddrPort()); + return {}; } + return sock; } +std::unique_ptr Proxy::Connect() const +{ + if (!IsValid()) { + LogPrintf("Cannot connect to invalid Proxy\n"); + return {}; + } + + if (!m_is_unix_socket) return ConnectDirectly(proxy, /*manual_connection=*/true); + +#if HAVE_SOCKADDR_UN + auto sock = CreateSock(AF_UNIX); + if (!sock) { + LogPrintLevel(BCLog::NET, BCLog::Level::Error, "Cannot create a socket for connecting to %s\n", m_unix_socket_path); + return {}; + } + + const std::string path{m_unix_socket_path.substr(ADDR_PREFIX_UNIX.length())}; + + struct sockaddr_un addrun; + memset(&addrun, 0, sizeof(addrun)); + addrun.sun_family = AF_UNIX; + // leave the last char in addrun.sun_path[] to be always '\0' + memcpy(addrun.sun_path, path.c_str(), std::min(sizeof(addrun.sun_path) - 1, path.length())); + socklen_t len = sizeof(addrun); + + if(!ConnectToSocket(*sock, (struct sockaddr*)&addrun, len, path, /*manual_connection=*/true)) { + LogPrintf("Cannot connect to socket for %s\n", path); + return {}; + } + + return sock; +#else + return {}; +#endif +} + bool SetProxy(enum Network net, const Proxy &addrProxy) { assert(net >= 0 && net < NET_MAX); if (!addrProxy.IsValid()) @@ -658,7 +704,7 @@ std::unique_ptr ConnectThroughProxy(const Proxy& proxy, bool& proxy_connection_failed) { // first connect to proxy server - auto sock = ConnectDirectly(proxy.proxy, /*manual_connection=*/true); + auto sock = proxy.Connect(); if (!sock) { proxy_connection_failed = true; return {}; diff --git a/src/netbase.h b/src/netbase.h index 0cefddafead..321c288f67b 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -84,6 +84,8 @@ public: if (m_is_unix_socket) return m_unix_socket_path; return proxy.ToStringAddrPort(); } + + std::unique_ptr Connect() const; }; /** Credentials for proxy authentication */ From a88bf9dedd1d8c1db0a9c8b663dab3e3c2f0f030 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 13 Jul 2023 12:40:42 -0400 Subject: [PATCH 08/12] i2p: construct Session with Proxy instead of CService --- src/i2p.cpp | 10 +++++----- src/i2p.h | 9 +++++---- src/net.cpp | 4 ++-- src/test/i2p_tests.cpp | 13 ++++++++++--- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/i2p.cpp b/src/i2p.cpp index fc9e741f40d..f3689a7e516 100644 --- a/src/i2p.cpp +++ b/src/i2p.cpp @@ -115,7 +115,7 @@ static CNetAddr DestB64ToAddr(const std::string& dest) namespace sam { Session::Session(const fs::path& private_key_file, - const CService& control_host, + const Proxy& control_host, CThreadInterrupt* interrupt) : m_private_key_file{private_key_file}, m_control_host{control_host}, @@ -124,7 +124,7 @@ Session::Session(const fs::path& private_key_file, { } -Session::Session(const CService& control_host, CThreadInterrupt* interrupt) +Session::Session(const Proxy& control_host, CThreadInterrupt* interrupt) : m_control_host{control_host}, m_interrupt{interrupt}, m_transient{true} @@ -326,10 +326,10 @@ Session::Reply Session::SendRequestAndGetReply(const Sock& sock, std::unique_ptr Session::Hello() const { - auto sock = ConnectDirectly(m_control_host, true); + auto sock = m_control_host.Connect(); if (!sock) { - throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToStringAddrPort())); + throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToString())); } SendRequestAndGetReply(*sock, "HELLO VERSION MIN=3.1 MAX=3.1"); @@ -413,7 +413,7 @@ void Session::CreateIfNotCreatedAlready() const auto session_type = m_transient ? "transient" : "persistent"; const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs - Log("Creating %s SAM session %s with %s", session_type, session_id, m_control_host.ToStringAddrPort()); + Log("Creating %s SAM session %s with %s", session_type, session_id, m_control_host.ToString()); auto sock = Hello(); diff --git a/src/i2p.h b/src/i2p.h index 375abaccfcc..8b0f1e11828 100644 --- a/src/i2p.h +++ b/src/i2p.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -67,7 +68,7 @@ public: * `Session` object. */ Session(const fs::path& private_key_file, - const CService& control_host, + const Proxy& control_host, CThreadInterrupt* interrupt); /** @@ -81,7 +82,7 @@ public: * `CThreadInterrupt` object is saved, so it must not be destroyed earlier than this * `Session` object. */ - Session(const CService& control_host, CThreadInterrupt* interrupt); + Session(const Proxy& control_host, CThreadInterrupt* interrupt); /** * Destroy the session, closing the internally used sockets. The sockets that have been @@ -235,9 +236,9 @@ private: const fs::path m_private_key_file; /** - * The host and port of the SAM control service. + * The SAM control service proxy. */ - const CService m_control_host; + const Proxy m_control_host; /** * Cease network activity when this is signaled. diff --git a/src/net.cpp b/src/net.cpp index 2cc85aa488a..66a863cd6ca 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -463,7 +463,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo LOCK(m_unused_i2p_sessions_mutex); if (m_unused_i2p_sessions.empty()) { i2p_transient_session = - std::make_unique(proxy.proxy, &interruptNet); + std::make_unique(proxy, &interruptNet); } else { i2p_transient_session.swap(m_unused_i2p_sessions.front()); m_unused_i2p_sessions.pop(); @@ -3186,7 +3186,7 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) Proxy i2p_sam; if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) { m_i2p_sam_session = std::make_unique(gArgs.GetDataDirNet() / "i2p_private_key", - i2p_sam.proxy, &interruptNet); + i2p_sam, &interruptNet); } for (const auto& strDest : connOptions.vSeedNodes) { diff --git a/src/test/i2p_tests.cpp b/src/test/i2p_tests.cpp index ef1d0b659cd..d7249d88f41 100644 --- a/src/test/i2p_tests.cpp +++ b/src/test/i2p_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,9 @@ BOOST_AUTO_TEST_CASE(unlimited_recv) }; CThreadInterrupt interrupt; - i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", CService{}, &interrupt); + const std::optional addr{Lookup("127.0.0.1", 9000, false)}; + const Proxy sam_proxy(addr.value(), false); + i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", sam_proxy, &interrupt); { ASSERT_DEBUG_LOG("Creating persistent SAM session"); @@ -111,8 +114,10 @@ BOOST_AUTO_TEST_CASE(listen_ok_accept_fail) }; CThreadInterrupt interrupt; + const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}; + const Proxy sam_proxy(addr, false); i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", - CService{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}, + sam_proxy, &interrupt); i2p::Connection conn; @@ -154,7 +159,9 @@ BOOST_AUTO_TEST_CASE(damaged_private_key) BOOST_REQUIRE(WriteBinaryFile(i2p_private_key_file, file_contents)); CThreadInterrupt interrupt; - i2p::sam::Session session(i2p_private_key_file, CService{}, &interrupt); + const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}; + const Proxy sam_proxy{addr, false}; + i2p::sam::Session session(i2p_private_key_file, sam_proxy, &interrupt); { ASSERT_DEBUG_LOG("Creating persistent SAM session"); From c3bd43142eba77dcf1acd4984e437759f65e237a Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 13 Jul 2023 14:31:14 -0400 Subject: [PATCH 09/12] gui: accomodate unix socket Proxy in updateDefaultProxyNets() This will require a follow-up to add unix socket options to the GUI --- src/qt/optionsdialog.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index a87bef796c3..dd654a7abe2 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -454,20 +454,24 @@ void OptionsDialog::updateProxyValidationState() void OptionsDialog::updateDefaultProxyNets() { - const std::optional ui_proxy_netaddr{LookupHost(ui->proxyIp->text().toStdString(), /*fAllowLookup=*/false)}; - const CService ui_proxy{ui_proxy_netaddr.value_or(CNetAddr{}), ui->proxyPort->text().toUShort()}; + std::string proxyIpText{ui->proxyIp->text().toStdString()}; + if (!IsUnixSocketPath(proxyIpText)) { + const std::optional ui_proxy_netaddr{LookupHost(proxyIpText, /*fAllowLookup=*/false)}; + const CService ui_proxy{ui_proxy_netaddr.value_or(CNetAddr{}), ui->proxyPort->text().toUShort()}; + proxyIpText = ui_proxy.ToStringAddrPort(); + } Proxy proxy; bool has_proxy; has_proxy = model->node().getProxy(NET_IPV4, proxy); - ui->proxyReachIPv4->setChecked(has_proxy && proxy.proxy == ui_proxy); + ui->proxyReachIPv4->setChecked(has_proxy && proxy.ToString() == proxyIpText); has_proxy = model->node().getProxy(NET_IPV6, proxy); - ui->proxyReachIPv6->setChecked(has_proxy && proxy.proxy == ui_proxy); + ui->proxyReachIPv6->setChecked(has_proxy && proxy.ToString() == proxyIpText); has_proxy = model->node().getProxy(NET_ONION, proxy); - ui->proxyReachTor->setChecked(has_proxy && proxy.proxy == ui_proxy); + ui->proxyReachTor->setChecked(has_proxy && proxy.ToString() == proxyIpText); } ProxyAddressValidator::ProxyAddressValidator(QObject *parent) : From c65c0d01630b44fa71321ea7ad68d5f9fbb7aefb Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 26 May 2023 14:23:57 -0400 Subject: [PATCH 10/12] init: allow UNIX socket path for -proxy and -onion --- src/init.cpp | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 988daefeec8..da7c626c6f0 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1298,7 +1298,14 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) std::string host_out; uint16_t port_out{0}; if (!SplitHostPort(socket_addr, port_out, host_out)) { +#if HAVE_SOCKADDR_UN + // Allow unix domain sockets for -proxy and -onion e.g. unix:/some/file/path + if ((port_option != "-proxy" && port_option != "-onion") || socket_addr.find(ADDR_PREFIX_UNIX) != 0) { + return InitError(InvalidPortErrMsg(port_option, socket_addr)); + } +#else return InitError(InvalidPortErrMsg(port_option, socket_addr)); +#endif } } } @@ -1365,12 +1372,18 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // -noproxy (or -proxy=0) as well as the empty string can be used to not set a proxy, this is the default std::string proxyArg = args.GetArg("-proxy", ""); if (proxyArg != "" && proxyArg != "0") { - const std::optional proxyAddr{Lookup(proxyArg, 9050, fNameLookup)}; - if (!proxyAddr.has_value()) { - return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); + Proxy addrProxy; + if (IsUnixSocketPath(proxyArg)) { + addrProxy = Proxy(proxyArg, proxyRandomize); + } else { + const std::optional proxyAddr{Lookup(proxyArg, 9050, fNameLookup)}; + if (!proxyAddr.has_value()) { + return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); + } + + addrProxy = Proxy(proxyAddr.value(), proxyRandomize); } - Proxy addrProxy = Proxy(proxyAddr.value(), proxyRandomize); if (!addrProxy.IsValid()) return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); @@ -1396,11 +1409,16 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) "reaching the Tor network is explicitly forbidden: -onion=0")); } } else { - const std::optional addr{Lookup(onionArg, 9050, fNameLookup)}; - if (!addr.has_value() || !addr->IsValid()) { - return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); + if (IsUnixSocketPath(onionArg)) { + onion_proxy = Proxy(onionArg, proxyRandomize); + } else { + const std::optional addr{Lookup(onionArg, 9050, fNameLookup)}; + if (!addr.has_value() || !addr->IsValid()) { + return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); + } + + onion_proxy = Proxy(addr.value(), proxyRandomize); } - onion_proxy = Proxy{addr.value(), proxyRandomize}; } } From bfe51928911daf484ae07deb52a7ff0bcb2526ae Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 26 May 2023 14:24:27 -0400 Subject: [PATCH 11/12] test: cover UNIX sockets in feature_proxy.py --- test/functional/feature_proxy.py | 88 ++++++++++++++++++++--- test/functional/test_framework/netutil.py | 9 +++ 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 662007d65e3..7a6f639021c 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -17,6 +17,7 @@ Test plan: - support no authentication (other proxy) - support no authentication + user/pass authentication (Tor) - proxy on IPv6 + - proxy over unix domain sockets - Create various proxies (as threads) - Create nodes that connect to them @@ -39,7 +40,9 @@ addnode connect to a CJDNS address - Test passing unknown -onlynet """ +import os import socket +import tempfile from test_framework.socks5 import Socks5Configuration, Socks5Command, Socks5Server, AddressType from test_framework.test_framework import BitcoinTestFramework @@ -47,7 +50,7 @@ from test_framework.util import ( assert_equal, p2p_port, ) -from test_framework.netutil import test_ipv6_local +from test_framework.netutil import test_ipv6_local, test_unix_socket # Networks returned by RPC getpeerinfo. NET_UNROUTABLE = "not_publicly_routable" @@ -60,14 +63,17 @@ NET_CJDNS = "cjdns" # Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS}) +# Use the shortest temp path possible since UNIX sockets may have as little as 92-char limit +socket_path = tempfile.NamedTemporaryFile().name class ProxyTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 5 + self.num_nodes = 7 self.setup_clean_chain = True def setup_nodes(self): self.have_ipv6 = test_ipv6_local() + self.have_unix_sockets = test_unix_socket() # Create two proxies on different ports # ... one unauthenticated self.conf1 = Socks5Configuration() @@ -89,6 +95,15 @@ class ProxyTest(BitcoinTestFramework): else: self.log.warning("Testing without local IPv6 support") + if self.have_unix_sockets: + self.conf4 = Socks5Configuration() + self.conf4.af = socket.AF_UNIX + self.conf4.addr = socket_path + self.conf4.unauth = True + self.conf4.auth = True + else: + self.log.warning("Testing without local unix domain sockets support") + self.serv1 = Socks5Server(self.conf1) self.serv1.start() self.serv2 = Socks5Server(self.conf2) @@ -96,6 +111,9 @@ class ProxyTest(BitcoinTestFramework): if self.have_ipv6: self.serv3 = Socks5Server(self.conf3) self.serv3.start() + if self.have_unix_sockets: + self.serv4 = Socks5Server(self.conf4) + self.serv4.start() # We will not try to connect to this. self.i2p_sam = ('127.0.0.1', 7656) @@ -109,10 +127,15 @@ class ProxyTest(BitcoinTestFramework): ['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'], [], ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1', - '-cjdnsreachable'] + '-cjdnsreachable'], + [], + [] ] if self.have_ipv6: args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion'] + if self.have_unix_sockets: + args[5] = ['-listen', f'-proxy=unix:{socket_path}'] + args[6] = ['-listen', f'-onion=unix:{socket_path}'] self.add_nodes(self.num_nodes, extra_args=args) self.start_nodes() @@ -124,7 +147,7 @@ class ProxyTest(BitcoinTestFramework): def node_test(self, node, *, proxies, auth, test_onion, test_cjdns): rv = [] addr = "15.61.23.23:1234" - self.log.debug(f"Test: outgoing IPv4 connection through node for address {addr}") + self.log.debug(f"Test: outgoing IPv4 connection through node {node.index} for address {addr}") node.addnode(addr, "onetry") cmd = proxies[0].queue.get() assert isinstance(cmd, Socks5Command) @@ -140,7 +163,7 @@ class ProxyTest(BitcoinTestFramework): if self.have_ipv6: addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443" - self.log.debug(f"Test: outgoing IPv6 connection through node for address {addr}") + self.log.debug(f"Test: outgoing IPv6 connection through node {node.index} for address {addr}") node.addnode(addr, "onetry") cmd = proxies[1].queue.get() assert isinstance(cmd, Socks5Command) @@ -156,7 +179,7 @@ class ProxyTest(BitcoinTestFramework): if test_onion: addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333" - self.log.debug(f"Test: outgoing onion connection through node for address {addr}") + self.log.debug(f"Test: outgoing onion connection through node {node.index} for address {addr}") node.addnode(addr, "onetry") cmd = proxies[2].queue.get() assert isinstance(cmd, Socks5Command) @@ -171,7 +194,7 @@ class ProxyTest(BitcoinTestFramework): if test_cjdns: addr = "[fc00:1:2:3:4:5:6:7]:8888" - self.log.debug(f"Test: outgoing CJDNS connection through node for address {addr}") + self.log.debug(f"Test: outgoing CJDNS connection through node {node.index} for address {addr}") node.addnode(addr, "onetry") cmd = proxies[1].queue.get() assert isinstance(cmd, Socks5Command) @@ -185,7 +208,7 @@ class ProxyTest(BitcoinTestFramework): self.network_test(node, addr, network=NET_CJDNS) addr = "node.noumenon:8333" - self.log.debug(f"Test: outgoing DNS name connection through node for address {addr}") + self.log.debug(f"Test: outgoing DNS name connection through node {node.index} for address {addr}") node.addnode(addr, "onetry") cmd = proxies[3].queue.get() assert isinstance(cmd, Socks5Command) @@ -230,6 +253,12 @@ class ProxyTest(BitcoinTestFramework): proxies=[self.serv1, self.serv1, self.serv1, self.serv1], auth=False, test_onion=True, test_cjdns=True) + if self.have_unix_sockets: + self.node_test(self.nodes[5], + proxies=[self.serv4, self.serv4, self.serv4, self.serv4], + auth=True, test_onion=True, test_cjdns=False) + + def networks_dict(d): r = {} for x in d['networks']: @@ -315,6 +344,37 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n4['i2p']['reachable'], False) assert_equal(n4['cjdns']['reachable'], True) + if self.have_unix_sockets: + n5 = networks_dict(nodes_network_info[5]) + assert_equal(NETWORKS, n5.keys()) + for net in NETWORKS: + if net == NET_I2P: + expected_proxy = '' + expected_randomize = False + else: + expected_proxy = 'unix:' + self.conf4.addr # no port number + expected_randomize = True + assert_equal(n5[net]['proxy'], expected_proxy) + assert_equal(n5[net]['proxy_randomize_credentials'], expected_randomize) + assert_equal(n5['onion']['reachable'], True) + assert_equal(n5['i2p']['reachable'], False) + assert_equal(n5['cjdns']['reachable'], False) + + n6 = networks_dict(nodes_network_info[6]) + assert_equal(NETWORKS, n6.keys()) + for net in NETWORKS: + if net != NET_ONION: + expected_proxy = '' + expected_randomize = False + else: + expected_proxy = 'unix:' + self.conf4.addr # no port number + expected_randomize = True + assert_equal(n6[net]['proxy'], expected_proxy) + assert_equal(n6[net]['proxy_randomize_credentials'], expected_randomize) + assert_equal(n6['onion']['reachable'], True) + assert_equal(n6['i2p']['reachable'], False) + assert_equal(n6['cjdns']['reachable'], False) + self.stop_node(1) self.log.info("Test passing invalid -proxy hostname raises expected init error") @@ -383,6 +443,18 @@ class ProxyTest(BitcoinTestFramework): msg = "Error: Unknown network specified in -onlynet: 'abc'" self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + self.log.info("Test passing too-long unix path to -proxy raises init error") + self.nodes[1].extra_args = [f"-proxy=unix:{'x' * 1000}"] + if self.have_unix_sockets: + msg = f"Error: Invalid -proxy address or hostname: 'unix:{'x' * 1000}'" + else: + # If unix sockets are not supported, the file path is incorrectly interpreted as host:port + msg = f"Error: Invalid port specified in -proxy: 'unix:{'x' * 1000}'" + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + # Cleanup socket path we established outside the individual test directory. + if self.have_unix_sockets: + os.unlink(socket_path) if __name__ == '__main__': ProxyTest().main() diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py index 30a4a58d6f3..08d41fe97fd 100644 --- a/test/functional/test_framework/netutil.py +++ b/test/functional/test_framework/netutil.py @@ -158,3 +158,12 @@ def test_ipv6_local(): except socket.error: have_ipv6 = False return have_ipv6 + +def test_unix_socket(): + '''Return True if UNIX sockets are available on this platform.''' + try: + socket.AF_UNIX + except AttributeError: + return False + else: + return True From 567cec9a05e1261e955535f734826b12341684b6 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 14 Jul 2023 13:59:15 -0400 Subject: [PATCH 12/12] doc: add release notes and help text for unix sockets --- doc/release-notes-27375.md | 6 ++++++ src/init.cpp | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 doc/release-notes-27375.md diff --git a/doc/release-notes-27375.md b/doc/release-notes-27375.md new file mode 100644 index 00000000000..e3f4ebdf776 --- /dev/null +++ b/doc/release-notes-27375.md @@ -0,0 +1,6 @@ +P2P +--- + +UNIX domain sockets can now be used for proxy connections. Set `-onion` or `-proxy` +to the local socket path with the prefix `unix:` (e.g. `-onion=unix:/home/me/torsocket`). +(#27375) \ No newline at end of file diff --git a/src/init.cpp b/src/init.cpp index da7c626c6f0..8dc279c5149 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -530,7 +530,11 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-maxsendbuffer=", strprintf("Maximum per-connection memory usage for the send buffer, *1000 bytes (default: %u)", DEFAULT_MAXSENDBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxtimeadjustment", strprintf("Maximum allowed median peer time offset adjustment. Local perspective of time may be influenced by outbound peers forward or backward by this amount (default: %u seconds).", DEFAULT_MAX_TIME_ADJUSTMENT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxuploadtarget=", strprintf("Tries to keep outbound traffic under the given target per 24h. Limit does not apply to peers with 'download' permission or blocks created within past week. 0 = no limit (default: %s). Optional suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 base while uppercase is 1024 base", DEFAULT_MAX_UPLOAD_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); +#if HAVE_SOCKADDR_UN + argsman.AddArg("-onion=", "Use separate SOCKS5 proxy to reach peers via Tor onion services, set -noonion to disable (default: -proxy). May be a local file path prefixed with 'unix:'.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); +#else argsman.AddArg("-onion=", "Use separate SOCKS5 proxy to reach peers via Tor onion services, set -noonion to disable (default: -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); +#endif argsman.AddArg("-i2psam=", "I2P SAM proxy to reach I2P peers and accept I2P connections (default: none)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-i2pacceptincoming", strprintf("Whether to accept inbound I2P connections (default: %i). Ignored if -i2psam is not set. Listening for inbound I2P connections is done through the SAM proxy, not by binding to a local address and port.", DEFAULT_I2P_ACCEPT_INCOMING), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-onlynet=", "Make automatic outbound connections only to network (" + Join(GetNetworkNames(), ", ") + "). Inbound and manual connections are not affected by this option. It can be specified multiple times to allow multiple networks.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -541,7 +545,11 @@ void SetupServerArgs(ArgsManager& argsman) // TODO: remove the sentence "Nodes not using ... incoming connections." once the changes from // https://github.com/bitcoin/bitcoin/pull/23542 have become widespread. argsman.AddArg("-port=", strprintf("Listen for connections on . Nodes not using the default ports (default: %u, testnet: %u, signet: %u, regtest: %u) are unlikely to get incoming connections. Not relevant for I2P (see doc/i2p.md).", defaultChainParams->GetDefaultPort(), testnetChainParams->GetDefaultPort(), signetChainParams->GetDefaultPort(), regtestChainParams->GetDefaultPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); +#if HAVE_SOCKADDR_UN + argsman.AddArg("-proxy=", "Connect through SOCKS5 proxy, set -noproxy to disable (default: disabled). May be a local file path prefixed with 'unix:' if the proxy supports it.", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_ELISION, OptionsCategory::CONNECTION); +#else argsman.AddArg("-proxy=", "Connect through SOCKS5 proxy, set -noproxy to disable (default: disabled)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_ELISION, OptionsCategory::CONNECTION); +#endif argsman.AddArg("-proxyrandomize", strprintf("Randomize credentials for every proxy connection. This enables Tor stream isolation (default: %u)", DEFAULT_PROXYRANDOMIZE), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-seednode=", "Connect to a node to retrieve peer addresses, and disconnect. This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-networkactive", "Enable all P2P network activity (default: 1). Can be changed by the setnetworkactive RPC command", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);