diff options
-rw-r--r-- | configure.ac | 15 | ||||
-rw-r--r-- | doc/release-notes-27375.md | 6 | ||||
-rw-r--r-- | src/compat/compat.h | 7 | ||||
-rw-r--r-- | src/i2p.cpp | 14 | ||||
-rw-r--r-- | src/i2p.h | 9 | ||||
-rw-r--r-- | src/init.cpp | 42 | ||||
-rw-r--r-- | src/net.cpp | 32 | ||||
-rw-r--r-- | src/netaddress.cpp | 13 | ||||
-rw-r--r-- | src/netaddress.h | 5 | ||||
-rw-r--r-- | src/netbase.cpp | 175 | ||||
-rw-r--r-- | src/netbase.h | 86 | ||||
-rw-r--r-- | src/qt/optionsdialog.cpp | 14 | ||||
-rw-r--r-- | src/rpc/net.cpp | 4 | ||||
-rw-r--r-- | src/test/fuzz/fuzz.cpp | 2 | ||||
-rw-r--r-- | src/test/i2p_tests.cpp | 21 | ||||
-rwxr-xr-x | test/functional/feature_proxy.py | 88 | ||||
-rw-r--r-- | test/functional/test_framework/netutil.py | 9 |
17 files changed, 401 insertions, 141 deletions
diff --git a/configure.ac b/configure.ac index aeb8b71437..03b5ff1d69 100644 --- a/configure.ac +++ b/configure.ac @@ -1198,10 +1198,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 <sys/socket.h> + #include <sys/un.h> + ]], [[ + 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( diff --git a/doc/release-notes-27375.md b/doc/release-notes-27375.md new file mode 100644 index 0000000000..e3f4ebdf77 --- /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/compat/compat.h b/src/compat/compat.h index 9ff9a335f8..366c648ae7 100644 --- a/src/compat/compat.h +++ b/src/compat/compat.h @@ -32,6 +32,13 @@ #include <unistd.h> // 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 02f2c1cea2..962adb124d 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} @@ -327,14 +327,10 @@ Session::Reply Session::SendRequestAndGetReply(const Sock& sock, std::unique_ptr<Sock> Session::Hello() const { - auto sock = CreateSock(m_control_host); + auto sock = m_control_host.Connect(); 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())); + throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToString())); } SendRequestAndGetReply(*sock, "HELLO VERSION MIN=3.1 MAX=3.1"); @@ -418,7 +414,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(); @@ -7,6 +7,7 @@ #include <compat/compat.h> #include <netaddress.h> +#include <netbase.h> #include <sync.h> #include <util/fs.h> #include <util/sock.h> @@ -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/init.cpp b/src/init.cpp index 0ecfba18c1..1a4fce4678 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -534,7 +534,11 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-maxsendbuffer=<n>", strprintf("Maximum per-connection memory usage for the send buffer, <n>*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=<n>", 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=<ip:port|path>", "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=<ip:port>", "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=<ip:port>", "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=<net>", "Make automatic outbound connections only to network <net> (" + 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); @@ -545,7 +549,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=<port>", strprintf("Listen for connections on <port>. 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=<ip:port|path>", "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=<ip:port>", "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=<ip>", "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); @@ -1323,7 +1331,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 } } } @@ -1390,12 +1405,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<CService> 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<CService> 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)); @@ -1421,11 +1442,16 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) "reaching the Tor network is explicitly forbidden: -onion=0")); } } else { - const std::optional<CService> 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<CService> 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}; } } diff --git a/src/net.cpp b/src/net.cpp index ab462b2994..e388f05b03 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -437,7 +437,6 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo } // Connect - bool connected = false; std::unique_ptr<Sock> sock; Proxy proxy; CAddress addr_bind; @@ -450,6 +449,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); @@ -458,7 +458,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<i2p::sam::Session>(proxy.proxy, &interruptNet); + std::make_unique<i2p::sam::Session>(proxy, &interruptNet); } else { i2p_transient_session.swap(m_unused_i2p_sessions.front()); m_unused_i2p_sessions.pop(); @@ -478,20 +478,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); - if (!sock) { - return nullptr; - } - connected = ConnectThroughProxy(proxy, addrConnect.ToStringAddr(), addrConnect.GetPort(), - *sock, nConnectTimeout, proxyConnectionFailed); + 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) - sock = CreateSock(addrConnect); - 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 @@ -499,18 +490,13 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo addrman.Attempt(addrConnect, fCountFailure); } } else if (pszDest && GetNameProxy(proxy)) { - sock = CreateSock(proxy.proxy); - 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; } @@ -2990,7 +2976,7 @@ bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, return false; } - std::unique_ptr<Sock> sock = CreateSock(addrBind); + std::unique_ptr<Sock> 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); @@ -3197,7 +3183,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<i2p::sam::Session>(gArgs.GetDataDirNet() / "i2p_private_key", - i2p_sam.proxy, &interruptNet); + i2p_sam, &interruptNet); } for (const auto& strDest : connOptions.vSeedNodes) { diff --git a/src/netaddress.cpp b/src/netaddress.cpp index 7530334db1..74ab6dd8d8 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 c697b7e0a3..c63bd4b4e5 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 49c334d332..61e96c0657 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 <config/bitcoin-config.h> +#endif + #include <netbase.h> #include <compat/compat.h> @@ -21,6 +25,10 @@ #include <limits> #include <memory> +#if HAVE_SOCKADDR_UN +#include <sys/un.h> +#endif + // Settings static GlobalMutex g_proxyinfo_mutex; static Proxy proxyInfo[NET_MAX] GUARDED_BY(g_proxyinfo_mutex); @@ -208,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, @@ -461,18 +487,18 @@ bool Socks5(const std::string& strDest, uint16_t port, const ProxyCredentials* a } } -std::unique_ptr<Sock> CreateSockTCP(const CService& address_family) +std::unique_ptr<Sock> 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, IPv6 or UNIX + if (address_family == AF_UNSPEC) return nullptr; + + int protocol{IPPROTO_TCP}; +#if HAVE_SOCKADDR_UN + if (address_family == AF_UNIX) protocol = 0; +#endif - // Create a TCP socket in the address family of the specified service. - SOCKET hSocket = socket(((struct sockaddr*)&sockaddr)->sa_family, SOCK_STREAM, IPPROTO_TCP); + // Create a socket in the specified address family. + SOCKET hSocket = socket(address_family, SOCK_STREAM, protocol); if (hSocket == INVALID_SOCKET) { return nullptr; } @@ -496,21 +522,25 @@ std::unique_ptr<Sock> CreateSockTCP(const CService& 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; } -std::function<std::unique_ptr<Sock>(const CService&)> CreateSock = CreateSockTCP; +std::function<std::unique_ptr<Sock>(const sa_family_t&)> CreateSock = CreateSockOS; template<typename... Args> static void LogConnectFailure(bool manual_connection, const char* fmt, const Args&... args) { @@ -522,18 +552,10 @@ static void LogConnectFailure(bool manual_connection, const char* fmt, const Arg } } -bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nTimeout, bool manual_connection) +static bool ConnectToSocket(const Sock& sock, struct sockaddr* sockaddr, socklen_t len, const std::string& dest_str, bool manual_connection) { - // 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; - } - - // Connect to the addrConnect service on the hSocket socket. - if (sock.Connect(reinterpret_cast<struct sockaddr*>(&sockaddr), len) == SOCKET_ERROR) { + // 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) @@ -543,13 +565,13 @@ 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_str, NetworkErrorString(WSAGetLastError())); return false; } else if (occurred == 0) { - LogPrint(BCLog::NET, "connection attempt to %s timed out\n", addrConnect.ToStringAddrPort()); + LogPrint(BCLog::NET, "connection attempt to %s timed out\n", dest_str); return false; } @@ -561,13 +583,13 @@ bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nT 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", addrConnect.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); + 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", - addrConnect.ToStringAddrPort(), + dest_str, NetworkErrorString(sockerr)); return false; } @@ -578,13 +600,73 @@ bool ConnectSocketDirectly(const CService &addrConnect, const Sock& sock, int nT else #endif { - LogConnectFailure(manual_connection, "connect() to %s failed: %s", addrConnect.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); + LogConnectFailure(manual_connection, "connect() to %s failed: %s", dest_str, NetworkErrorString(WSAGetLastError())); return false; } } return true; } +std::unique_ptr<Sock> 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 (!dest.GetSockAddr((struct sockaddr*)&sockaddr, &len)) { + LogPrintf("Cannot get sockaddr for %s: unsupported network\n", dest.ToStringAddrPort()); + 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<Sock> 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()) @@ -633,27 +715,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<Sock> 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 = proxy.Connect(); + if (!sock) { + proxy_connection_failed = true; + return {}; } + // 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++); - 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 1bd95ba0d9..321c288f67 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,46 @@ 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), 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; - bool randomize_credentials; + std::string m_unix_socket_path; + bool m_is_unix_socket; + bool m_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(); + } + + std::unique_ptr<Sock> Connect() const; }; /** Credentials for proxy authentication */ @@ -229,47 +262,42 @@ 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 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 */ -std::unique_ptr<Sock> CreateSockTCP(const CService& address_family); +std::unique_ptr<Sock> 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<std::unique_ptr<Sock>(const CService&)> CreateSock; +extern std::function<std::unique_ptr<Sock>(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<Sock> 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<Sock> ConnectThroughProxy(const Proxy& proxy, + const std::string& dest, + uint16_t port, + bool& proxy_connection_failed); /** * Interrupt SOCKS5 reads or writes. diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index a87bef796c..dd654a7abe 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -454,20 +454,24 @@ void OptionsDialog::updateProxyValidationState() void OptionsDialog::updateDefaultProxyNets() { - const std::optional<CNetAddr> 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<CNetAddr> 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) : diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 5e6f42b596..3fa2b18495 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -607,8 +607,8 @@ 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_randomize_credentials", proxy.randomize_credentials); + obj.pushKV("proxy", proxy.IsValid() ? proxy.ToString() : std::string()); + obj.pushKV("proxy_randomize_credentials", proxy.m_randomize_credentials); networks.push_back(obj); } return networks; diff --git a/src/test/fuzz/fuzz.cpp b/src/test/fuzz/fuzz.cpp index d1a67cb0d8..a8e490b459 100644 --- a/src/test/fuzz/fuzz.cpp +++ b/src/test/fuzz/fuzz.cpp @@ -83,7 +83,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<Sock> { std::terminate(); }; + CreateSock = [](const sa_family_t&) -> std::unique_ptr<Sock> { 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 f80f07d190..d7249d88f4 100644 --- a/src/test/i2p_tests.cpp +++ b/src/test/i2p_tests.cpp @@ -6,6 +6,7 @@ #include <i2p.h> #include <logging.h> #include <netaddress.h> +#include <netbase.h> #include <test/util/logging.h> #include <test/util/net.h> #include <test/util/setup_common.h> @@ -38,7 +39,7 @@ public: private: const BCLog::Level m_prev_log_level; - const std::function<std::unique_ptr<Sock>(const CService&)> m_create_sock_orig; + const std::function<std::unique_ptr<Sock>(const sa_family_t&)> m_create_sock_orig; }; BOOST_FIXTURE_TEST_SUITE(i2p_tests, EnvTestingSetup) @@ -46,12 +47,14 @@ 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<StaticContentsSock>(std::string(i2p::sam::MAX_MSG_SIZE + 1, 'a')); }; CThreadInterrupt interrupt; - i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", CService{}, &interrupt); + const std::optional<CService> 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"); @@ -66,7 +69,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. @@ -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; @@ -130,7 +135,7 @@ BOOST_AUTO_TEST_CASE(damaged_private_key) { const auto CreateSockOrig = CreateSock; - CreateSock = [](const CService&) { + CreateSock = [](const sa_family_t&) { return std::make_unique<StaticContentsSock>("HELLO REPLY RESULT=OK VERSION=3.1\n" "SESSION STATUS RESULT=OK DESTINATION=\n"); }; @@ -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"); diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 662007d65e..7a6f639021 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 30a4a58d6f..08d41fe97f 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 |