From ca5781e23a8f299ff4f143d2355218f551e65944 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 6 May 2025 14:12:55 +0200 Subject: [PATCH] config: allow setting -proxy per network `-proxy=addr:port` specifies the proxy for all networks (except I2P). Previously only the Tor proxy could have been specified separately via `-onion=addr:port`. Make it possible to specify separately the proxy for IPv4, IPv6, Tor and CJDNS by e.g. `-proxy=addr:port=ipv6`. Or remove the proxy for a given network, e.g. `-proxy=0=cjdns`. Resolves: https://github.com/bitcoin/bitcoin/issues/24450 --- src/init.cpp | 137 +++++++++++++++++++++---------- src/torcontrol.cpp | 1 - src/torcontrol.h | 1 + test/functional/feature_proxy.py | 41 +++++++++ 4 files changed, 136 insertions(+), 44 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index a151c9d0a9e..3c623780992 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -552,11 +552,26 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-peerblockfilters", strprintf("Serve compact block filters to peers per BIP 157 (default: %u)", DEFAULT_PEERBLOCKFILTERS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-txreconciliation", strprintf("Enable transaction reconciliations per BIP 330 (default: %d)", DEFAULT_TXRECONCILIATION_ENABLE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-port=", strprintf("Listen for connections on (default: %u, testnet3: %u, testnet4: %u, signet: %u, regtest: %u). Not relevant for I2P (see doc/i2p.md). If set to a value x, the default onion listening port will be set to x+1.", defaultChainParams->GetDefaultPort(), testnetChainParams->GetDefaultPort(), testnet4ChainParams->GetDefaultPort(), signetChainParams->GetDefaultPort(), regtestChainParams->GetDefaultPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); + const std::string proxy_doc_for_value = #ifdef 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); + "[:]|unix:"; #else - argsman.AddArg("-proxy=", "Connect through SOCKS5 proxy, set -noproxy to disable (default: disabled)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_ELISION, OptionsCategory::CONNECTION); + "[:]"; #endif + const std::string proxy_doc_for_unix_socket = +#ifdef HAVE_SOCKADDR_UN + "May be a local file path prefixed with 'unix:' if the proxy supports it. "; +#else + ""; +#endif + argsman.AddArg("-proxy=" + proxy_doc_for_value + "[=]", + "Connect through SOCKS5 proxy, set -noproxy to disable. " + + proxy_doc_for_unix_socket + + "Could end in =network to set the proxy only for that network. " + + "The network can be any of ipv4, ipv6, tor or cjdns. " + + "(default: disabled)", + ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_ELISION, + OptionsCategory::CONNECTION); 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. During startup, seednodes will be tried before dnsseeds.", 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); @@ -1189,31 +1204,34 @@ bool CheckHostPortOptions(const ArgsManager& args) { } } - for ([[maybe_unused]] const auto& [arg, unix] : std::vector>{ - // arg name UNIX socket support - {"-i2psam", false}, - {"-onion", true}, - {"-proxy", true}, - {"-rpcbind", false}, - {"-torcontrol", false}, - {"-whitebind", false}, - {"-zmqpubhashblock", true}, - {"-zmqpubhashtx", true}, - {"-zmqpubrawblock", true}, - {"-zmqpubrawtx", true}, - {"-zmqpubsequence", true}, + for ([[maybe_unused]] const auto& [param_name, unix, suffix_allowed] : std::vector>{ + // arg name UNIX socket support =suffix allowed + {"-i2psam", false, false}, + {"-onion", true, false}, + {"-proxy", true, true}, + {"-bind", false, true}, + {"-rpcbind", false, false}, + {"-torcontrol", false, false}, + {"-whitebind", false, false}, + {"-zmqpubhashblock", true, false}, + {"-zmqpubhashtx", true, false}, + {"-zmqpubrawblock", true, false}, + {"-zmqpubrawtx", true, false}, + {"-zmqpubsequence", true, false}, }) { - for (const std::string& socket_addr : args.GetArgs(arg)) { + for (const std::string& param_value : args.GetArgs(param_name)) { + const std::string param_value_hostport{ + suffix_allowed ? param_value.substr(0, param_value.rfind('=')) : param_value}; std::string host_out; uint16_t port_out{0}; - if (!SplitHostPort(socket_addr, port_out, host_out)) { + if (!SplitHostPort(param_value_hostport, port_out, host_out)) { #ifdef HAVE_SOCKADDR_UN // Allow unix domain sockets for some options e.g. unix:/some/file/path - if (!unix || !socket_addr.starts_with(ADDR_PREFIX_UNIX)) { - return InitError(InvalidPortErrMsg(arg, socket_addr)); + if (!unix || !param_value.starts_with(ADDR_PREFIX_UNIX)) { + return InitError(InvalidPortErrMsg(param_name, param_value)); } #else - return InitError(InvalidPortErrMsg(arg, socket_addr)); + return InitError(InvalidPortErrMsg(param_name, param_value)); #endif } } @@ -1569,33 +1587,66 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // Check for host lookup allowed before parsing any network related parameters fNameLookup = args.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP); - Proxy onion_proxy; - bool proxyRandomize = args.GetBoolArg("-proxyrandomize", DEFAULT_PROXYRANDOMIZE); - // -proxy sets a proxy for all outgoing network traffic - // -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") { - Proxy addrProxy; - if (IsUnixSocketPath(proxyArg)) { - addrProxy = Proxy(proxyArg, /*tor_stream_isolation=*/proxyRandomize); - } else { - const std::optional proxyAddr{Lookup(proxyArg, 9050, fNameLookup)}; - if (!proxyAddr.has_value()) { - return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); + // -proxy sets a proxy for outgoing network traffic, possibly per network. + // -noproxy, -proxy=0 or -proxy="" can be used to remove the proxy setting, this is the default + Proxy ipv4_proxy; + Proxy ipv6_proxy; + Proxy onion_proxy; + Proxy name_proxy; + Proxy cjdns_proxy; + for (const std::string& param_value : args.GetArgs("-proxy")) { + const auto eq_pos{param_value.rfind('=')}; + const std::string proxy_str{param_value.substr(0, eq_pos)}; // e.g. 127.0.0.1:9050=ipv4 -> 127.0.0.1:9050 + std::string net_str; + if (eq_pos != std::string::npos) { + if (eq_pos + 1 == param_value.length()) { + return InitError(strprintf(_("Invalid -proxy address or hostname, ends with '=': '%s'"), param_value)); } - - addrProxy = Proxy(proxyAddr.value(), /*tor_stream_isolation=*/proxyRandomize); + net_str = ToLower(param_value.substr(eq_pos + 1)); // e.g. 127.0.0.1:9050=ipv4 -> ipv4 } - if (!addrProxy.IsValid()) - return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); + Proxy proxy; + if (!proxy_str.empty() && proxy_str != "0") { + if (IsUnixSocketPath(proxy_str)) { + proxy = Proxy{proxy_str, /*tor_stream_isolation=*/proxyRandomize}; + } else { + const std::optional addr{Lookup(proxy_str, DEFAULT_TOR_SOCKS_PORT, fNameLookup)}; + if (!addr.has_value()) { + return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxy_str)); + } + proxy = Proxy{addr.value(), /*tor_stream_isolation=*/proxyRandomize}; + } + if (!proxy.IsValid()) { + return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxy_str)); + } + } - SetProxy(NET_IPV4, addrProxy); - SetProxy(NET_IPV6, addrProxy); - SetProxy(NET_CJDNS, addrProxy); - SetNameProxy(addrProxy); - onion_proxy = addrProxy; + if (net_str.empty()) { // For all networks. + ipv4_proxy = ipv6_proxy = name_proxy = cjdns_proxy = onion_proxy = proxy; + } else if (net_str == "ipv4") { + ipv4_proxy = name_proxy = proxy; + } else if (net_str == "ipv6") { + ipv6_proxy = name_proxy = proxy; + } else if (net_str == "tor" || net_str == "onion") { + onion_proxy = proxy; + } else if (net_str == "cjdns") { + cjdns_proxy = proxy; + } else { + return InitError(strprintf(_("Unrecognized network in -proxy='%s': '%s'"), param_value, net_str)); + } + } + if (ipv4_proxy.IsValid()) { + SetProxy(NET_IPV4, ipv4_proxy); + } + if (ipv6_proxy.IsValid()) { + SetProxy(NET_IPV6, ipv6_proxy); + } + if (name_proxy.IsValid()) { + SetNameProxy(name_proxy); + } + if (cjdns_proxy.IsValid()) { + SetProxy(NET_CJDNS, cjdns_proxy); } const bool onlynet_used_with_onion{!onlynets.empty() && g_reachable_nets.Contains(NET_ONION)}; @@ -1616,7 +1667,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (IsUnixSocketPath(onionArg)) { onion_proxy = Proxy(onionArg, /*tor_stream_isolation=*/proxyRandomize); } else { - const std::optional addr{Lookup(onionArg, 9050, fNameLookup)}; + const std::optional addr{Lookup(onionArg, DEFAULT_TOR_SOCKS_PORT, fNameLookup)}; if (!addr.has_value() || !addr->IsValid()) { return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); } diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 0e42b6e9235..1502c911063 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -71,7 +71,6 @@ static const float RECONNECT_TIMEOUT_MAX = 600.0; * this is belt-and-suspenders sanity limit to prevent memory exhaustion. */ static const int MAX_LINE_LENGTH = 100000; -static const uint16_t DEFAULT_TOR_SOCKS_PORT = 9050; /****** Low-level TorControlConnection ********/ diff --git a/src/torcontrol.h b/src/torcontrol.h index 0b66201cf1a..c9866bf8984 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -19,6 +19,7 @@ #include #include +constexpr uint16_t DEFAULT_TOR_SOCKS_PORT{9050}; constexpr int DEFAULT_TOR_CONTROL_PORT = 9051; extern const std::string DEFAULT_TOR_CONTROL; static const bool DEFAULT_LISTEN_ONION = true; diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 644ee5cc7f2..ba8a0212a6a 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -444,6 +444,47 @@ 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 trailing '=' raises expected init error") + self.nodes[1].extra_args = ["-proxy=127.0.0.1:9050="] + msg = "Error: Invalid -proxy address or hostname, ends with '=': '127.0.0.1:9050='" + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + self.log.info("Test passing unrecognized network raises expected init error") + self.nodes[1].extra_args = ["-proxy=127.0.0.1:9050=foo"] + msg = "Error: Unrecognized network in -proxy='127.0.0.1:9050=foo': 'foo'" + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + self.log.info("Test passing proxy only for IPv6") + self.start_node(1, extra_args=["-proxy=127.6.6.6:6666=ipv6"]) + nets = networks_dict(self.nodes[1].getnetworkinfo()) + assert_equal(nets["ipv4"]["proxy"], "") + assert_equal(nets["ipv6"]["proxy"], "127.6.6.6:6666") + self.stop_node(1) + + self.log.info("Test passing separate proxy for IPv4 and IPv6") + self.start_node(1, extra_args=["-proxy=127.4.4.4:4444=ipv4", "-proxy=127.6.6.6:6666=ipv6"]) + nets = networks_dict(self.nodes[1].getnetworkinfo()) + assert_equal(nets["ipv4"]["proxy"], "127.4.4.4:4444") + assert_equal(nets["ipv6"]["proxy"], "127.6.6.6:6666") + self.stop_node(1) + + self.log.info("Test overriding the Tor proxy") + self.start_node(1, extra_args=["-proxy=127.1.1.1:1111", "-proxy=127.2.2.2:2222=tor"]) + nets = networks_dict(self.nodes[1].getnetworkinfo()) + assert_equal(nets["ipv4"]["proxy"], "127.1.1.1:1111") + assert_equal(nets["ipv6"]["proxy"], "127.1.1.1:1111") + assert_equal(nets["onion"]["proxy"], "127.2.2.2:2222") + self.stop_node(1) + + self.log.info("Test removing CJDNS proxy") + self.start_node(1, extra_args=["-proxy=127.1.1.1:1111", "-proxy=0=cjdns"]) + nets = networks_dict(self.nodes[1].getnetworkinfo()) + assert_equal(nets["ipv4"]["proxy"], "127.1.1.1:1111") + assert_equal(nets["ipv6"]["proxy"], "127.1.1.1:1111") + assert_equal(nets["onion"]["proxy"], "127.1.1.1:1111") + assert_equal(nets["cjdns"]["proxy"], "") + self.stop_node(1) + 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: