Merge bitcoin/bitcoin#32425: config: allow setting -proxy per network

e98c51fcce doc: update tor.md to mention the new -proxy=addr:port=tor (Vasil Dimov)
ca5781e23a config: allow setting -proxy per network (Vasil Dimov)

Pull request description:

  `-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

ACKs for top commit:
  pinheadmz:
    ACK e98c51fcce
  caesrcd:
    reACK e98c51fcce
  danielabrozzoni:
    Code Review ACK e98c51fcce
  1440000bytes:
    ACK e98c51fcce

Tree-SHA512: 0cb590cb72b9393cc36357e8bd7861514ec4c5bc044a154e59601420b1fd6240f336ab538ed138bc769fca3d17e03725d56de382666420dc0787895d5bfec131
This commit is contained in:
merge-script
2025-06-10 15:57:09 -04:00
5 changed files with 179 additions and 67 deletions

View File

@@ -34,33 +34,53 @@ You can use the `getnodeaddresses` RPC to fetch a number of onion peers known to
The first step is running Bitcoin Core behind a Tor proxy. This will already anonymize all
outgoing connections, but more is possible.
-proxy=ip:port Set the proxy server. If SOCKS5 is selected (default), this proxy
server will be used to try to reach .onion addresses as well.
You need to use -noonion or -onion=0 to explicitly disable
outbound access to onion services.
-proxy=ip[:port]
Set the proxy server. It will be used to try to reach .onion addresses
as well. You need to use -noonion or -onion=0 to explicitly disable
outbound access to onion services.
-onion=ip:port Set the proxy server to use for Tor onion services. You do not
need to set this if it's the same as -proxy. You can use -onion=0
to explicitly disable access to onion services.
------------------------------------------------------------------
Note: Only the -proxy option sets the proxy for DNS requests;
with -onion they will not route over Tor, so use -proxy if you
have privacy concerns.
------------------------------------------------------------------
-proxy=ip[:port]=tor
or
-onion=ip[:port]
Set the proxy server for reaching .onion addresses. You do not need to
set this if it's the same as the generic -proxy. You can use -onion=0 to
explicitly disable access to onion services.
------------------------------------------------------------------------
Note: The proxy for DNS requests is taken from
-proxy=addr:port or
-proxy=addr:port=ipv4 or
-proxy=addr:port=ipv6
(last one if multiple options are given). It is not taken from
-proxy=addr:port=tor or
-onion=addr:port.
If no proxy for DNS requests is configured, then they will be done using
the functions provided by the operating system, most likely resulting in
them being done over the clearnet to the DNS servers of the internet
service provider.
------------------------------------------------------------------------
-listen When using -proxy, listening is disabled by default. If you want
to manually configure an onion service (see section 3), you'll
need to enable it explicitly.
If -proxy or -onion is specified multiple times, later occurences override
earlier ones and command line overrides the config file. UNIX domain sockets may
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`).
-connect=X When behind a Tor proxy, you can specify .onion addresses instead
-addnode=X of IP addresses or hostnames in these parameters. It requires
-seednode=X SOCKS5. In Tor mode, such addresses can also be exchanged with
other P2P nodes.
-listen
When using -proxy, listening is disabled by default. If you want to
manually configure an onion service (see section 3), you'll need to
enable it explicitly.
-onlynet=onion Make automatic outbound connections only to .onion addresses.
Inbound and manual connections are not affected by this option.
It can be specified multiple times to allow multiple networks,
e.g. onlynet=onion, onlynet=i2p, onlynet=cjdns.
-connect=X
-addnode=X
-seednode=X
When behind a Tor proxy, you can specify .onion addresses instead of IP
addresses or hostnames in these parameters. Such addresses can also be
exchanged with other P2P nodes.
-onlynet=onion
Make automatic outbound connections only to .onion addresses. Inbound
and manual connections are not affected by this option. It can be
specified multiple times to allow multiple networks, e.g. onlynet=onion,
onlynet=i2p, onlynet=cjdns.
In a typical situation, this suffices to run behind a Tor proxy:

View File

@@ -549,11 +549,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=<port>", strprintf("Listen for connections on <port> (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=<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);
"<ip>[:<port>]|unix:<path>";
#else
argsman.AddArg("-proxy=<ip:port>", "Connect through SOCKS5 proxy, set -noproxy to disable (default: disabled)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_ELISION, OptionsCategory::CONNECTION);
"<ip>[:<port>]";
#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 + "[=<network>]",
"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=<ip>", "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);
@@ -1162,31 +1177,34 @@ bool CheckHostPortOptions(const ArgsManager& args) {
}
}
for ([[maybe_unused]] const auto& [arg, unix] : std::vector<std::pair<std::string, bool>>{
// 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<std::tuple<std::string, bool, bool>>{
// 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
}
}
@@ -1544,33 +1562,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<CService> 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<CService> 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)};
@@ -1591,7 +1642,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
if (IsUnixSocketPath(onionArg)) {
onion_proxy = Proxy(onionArg, /*tor_stream_isolation=*/proxyRandomize);
} else {
const std::optional<CService> addr{Lookup(onionArg, 9050, fNameLookup)};
const std::optional<CService> addr{Lookup(onionArg, DEFAULT_TOR_SOCKS_PORT, fNameLookup)};
if (!addr.has_value() || !addr->IsValid()) {
return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg));
}

View File

@@ -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 ********/

View File

@@ -19,6 +19,7 @@
#include <string>
#include <vector>
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;

View File

@@ -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: