Skip to content

Commit df3f63c

Browse files
committed
Merge bitcoin#30509: multiprocess: Add -ipcbind option to bitcoin-node
30073e6 multiprocess: Add -ipcbind option to bitcoin-node (Russell Yanofsky) 73fe7d7 multiprocess: Add unit tests for connect, serve, and listen functions (Ryan Ofsky) 955d407 multiprocess: Add IPC connectAddress and listenAddress methods (Russell Yanofsky) 4da2043 depends: Update libmultiprocess library for CustomMessage function and ThreadContext bugfix (Ryan Ofsky) Pull request description: Add `-ipcbind` option to `bitcoin-node` to make it listen on a unix socket and accept connections from other processes. The default socket path is `<datadir>/node.sock`, but this can be customized. This option lets potential wallet, gui, index, and mining processes connect to the node and control it. See examples in bitcoin#19460, bitcoin#19461, and bitcoin#30437. Motivation for this PR, in combination with bitcoin#30510, is be able to release a bitcoin core node binary that can generate block templates for a separate Stratum v2 mining service, like the one being implemented in Sjors#48, that connects over IPC. Other things to know about this PR: - While the `-ipcbind` option lets other processes to connect to the `bitcoin-node` process, the only thing they can actually do after connecting is call methods on the [`Init`](https://github.com/bitcoin/bitcoin/blob/master/src/ipc/capnp/init.capnp#L17-L20) interface which is currently very limited and doesn't do much. But PRs [bitcoin#30510](bitcoin#30510), [bitcoin#29409](bitcoin#29409), and [bitcoin#10102](bitcoin#10102) expand the `Init` interface to expose mining, wallet, and gui functionality respectively. - This PR is not needed for [bitcoin#10102](bitcoin#10102), which runs GUI, node, and wallet code in different processes, because [bitcoin#10102](bitcoin#10102) does not use unix sockets or allow outside processes to connect to existing processes. [bitcoin#10102](bitcoin#10102) lets parent and child processes communicate over internal socketpairs, not externally accessible sockets. --- This PR is part of the [process separation project](bitcoin#28722). ACKs for top commit: achow101: ACK 30073e6 TheCharlatan: Re-ACK 30073e6 itornaza: Code review ACK 30073e6 Tree-SHA512: 2b766e60535f57352e8afda9c3748a32acb5a57b2827371b48ba865fa9aa1df00f340732654f2e300c6823dbc6f3e14377fca87e4e959e613fe85a6d2312d9c8
2 parents 712a2b5 + 30073e6 commit df3f63c

21 files changed

+359
-18
lines changed

depends/packages/native_libmultiprocess.mk

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package=native_libmultiprocess
2-
$(package)_version=6aca5f389bacf2942394b8738bbe15d6c9edfb9b
2+
$(package)_version=c1b4ab4eb897d3af09bc9b3cc30e2e6fff87f3e2
33
$(package)_download_path=https://github.com/chaincodelabs/libmultiprocess/archive
44
$(package)_file_name=$($(package)_version).tar.gz
5-
$(package)_sha256_hash=2efeed53542bc1d8af3291f2b6f0e5d430d86a5e04e415ce33c136f2c226a51d
5+
$(package)_sha256_hash=6edf5ad239ca9963c78f7878486fb41411efc9927c6073928a7d6edf947cac4a
66
$(package)_dependencies=native_capnp
77

88
define $(package)_config_cmds

src/bitcoind.cpp

+4-3
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ int fork_daemon(bool nochdir, bool noclose, TokenPipeEnd& endpoint)
109109

110110
#endif
111111

112-
static bool ParseArgs(ArgsManager& args, int argc, char* argv[])
112+
static bool ParseArgs(NodeContext& node, int argc, char* argv[])
113113
{
114+
ArgsManager& args{*Assert(node.args)};
114115
// If Qt is used, parameters/bitcoin.conf are parsed in qt/bitcoin.cpp's main()
115-
SetupServerArgs(args);
116+
SetupServerArgs(args, node.init->canListenIpc());
116117
std::string error;
117118
if (!args.ParseParameters(argc, argv, error)) {
118119
return InitError(Untranslated(strprintf("Error parsing command line arguments: %s", error)));
@@ -268,7 +269,7 @@ MAIN_FUNCTION
268269

269270
// Interpret command line arguments
270271
ArgsManager& args = *Assert(node.args);
271-
if (!ParseArgs(args, argc, argv)) return EXIT_FAILURE;
272+
if (!ParseArgs(node, argc, argv)) return EXIT_FAILURE;
272273
// Process early info return commands such as -help or -version
273274
if (ProcessInitCommands(args)) return EXIT_SUCCESS;
274275

src/common/args.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,9 @@ std::string ArgsManager::GetHelpMessage() const
635635
case OptionsCategory::RPC:
636636
usage += HelpMessageGroup("RPC server options:");
637637
break;
638+
case OptionsCategory::IPC:
639+
usage += HelpMessageGroup("IPC interprocess connection options:");
640+
break;
638641
case OptionsCategory::WALLET:
639642
usage += HelpMessageGroup("Wallet options:");
640643
break;

src/common/args.h

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ enum class OptionsCategory {
6464
COMMANDS,
6565
REGISTER_COMMANDS,
6666
CLI_COMMANDS,
67+
IPC,
6768

6869
HIDDEN // Always the last option to avoid printing these in the help
6970
};

src/init.cpp

+16-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <init/common.h>
3030
#include <interfaces/chain.h>
3131
#include <interfaces/init.h>
32+
#include <interfaces/ipc.h>
3233
#include <interfaces/mining.h>
3334
#include <interfaces/node.h>
3435
#include <kernel/context.h>
@@ -441,7 +442,7 @@ static void OnRPCStopped()
441442
LogDebug(BCLog::RPC, "RPC stopped.\n");
442443
}
443444

444-
void SetupServerArgs(ArgsManager& argsman)
445+
void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
445446
{
446447
SetupHelpOptions(argsman);
447448
argsman.AddArg("-help-debug", "Print help message with debugging options and exit", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); // server-only for now
@@ -676,6 +677,9 @@ void SetupServerArgs(ArgsManager& argsman)
676677
argsman.AddArg("-rpcwhitelistdefault", "Sets default behavior for rpc whitelisting. Unless rpcwhitelistdefault is set to 0, if any -rpcwhitelist is set, the rpc server acts as if all rpc users are subject to empty-unless-otherwise-specified whitelists. If rpcwhitelistdefault is set to 1 and no -rpcwhitelist is set, rpc server acts as if all rpc users are subject to empty whitelists.", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
677678
argsman.AddArg("-rpcworkqueue=<n>", strprintf("Set the depth of the work queue to service RPC calls (default: %d)", DEFAULT_HTTP_WORKQUEUE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
678679
argsman.AddArg("-server", "Accept command line and JSON-RPC commands", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
680+
if (can_listen_ipc) {
681+
argsman.AddArg("-ipcbind=<address>", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, <datadir>/node.sock, or \"unix:/custom/path\" to specify a custom path. Can be specified multiple times to listen on multiple paths. Default behavior is not to listen on any path. If relative paths are specified, they are interpreted relative to the network data directory. If paths include any parent directory components and the parent directories do not exist, they will be created.", ArgsManager::ALLOW_ANY, OptionsCategory::IPC);
682+
}
679683

680684
#if HAVE_DECL_FORK
681685
argsman.AddArg("-daemon", strprintf("Run in the background as a daemon and accept commands (default: %d)", DEFAULT_DAEMON), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -1200,6 +1204,17 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
12001204
g_wallet_init_interface.Construct(node);
12011205
uiInterface.InitWallet();
12021206

1207+
if (interfaces::Ipc* ipc = node.init->ipc()) {
1208+
for (std::string address : gArgs.GetArgs("-ipcbind")) {
1209+
try {
1210+
ipc->listenAddress(address);
1211+
} catch (const std::exception& e) {
1212+
return InitError(strprintf(Untranslated("Unable to bind to IPC address '%s'. %s"), address, e.what()));
1213+
}
1214+
LogPrintf("Listening for IPC requests on address %s\n", address);
1215+
}
1216+
}
1217+
12031218
/* Register RPC commands regardless of -server setting so they will be
12041219
* available in the GUI RPC console even if external calls are disabled.
12051220
*/

src/init.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ bool AppInitMain(node::NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip
7474
/**
7575
* Register all arguments with the ArgsManager
7676
*/
77-
void SetupServerArgs(ArgsManager& argsman);
77+
void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc=false);
7878

7979
/** Validates requirements to run the indexes and spawns each index initial sync thread */
8080
bool StartIndexBackgroundSync(node::NodeContext& node);

src/init/bitcoin-gui.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ class BitcoinGuiInit : public interfaces::Init
3434
}
3535
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
3636
interfaces::Ipc* ipc() override { return m_ipc.get(); }
37+
// bitcoin-gui accepts -ipcbind option even though it does not use it
38+
// directly. It just returns true here to accept the option because
39+
// bitcoin-node accepts the option, and bitcoin-gui accepts all bitcoin-node
40+
// options and will start the node with those options.
41+
bool canListenIpc() override { return true; }
3742
node::NodeContext m_node;
3843
std::unique_ptr<interfaces::Ipc> m_ipc;
3944
};

src/init/bitcoin-node.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class BitcoinNodeInit : public interfaces::Init
3737
}
3838
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
3939
interfaces::Ipc* ipc() override { return m_ipc.get(); }
40+
bool canListenIpc() override { return true; }
4041
node::NodeContext& m_node;
4142
std::unique_ptr<interfaces::Ipc> m_ipc;
4243
};

src/interfaces/init.h

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Init
3737
virtual std::unique_ptr<WalletLoader> makeWalletLoader(Chain& chain) { return nullptr; }
3838
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
3939
virtual Ipc* ipc() { return nullptr; }
40+
virtual bool canListenIpc() { return false; }
4041
};
4142

4243
//! Return implementation of Init interface for the node process. If the argv

src/interfaces/ipc.h

+16
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ class Init;
4141
//! to make other proxy objects calling other remote interfaces. It can also
4242
//! destroy the initial interfaces::Init object to close the connection and
4343
//! shut down the spawned process.
44+
//!
45+
//! When connecting to an existing process, the steps are similar to spawning a
46+
//! new process, except a socket is created instead of a socketpair, and
47+
//! destroying an Init interface doesn't end the process, since there can be
48+
//! multiple connections.
4449
class Ipc
4550
{
4651
public:
@@ -54,6 +59,17 @@ class Ipc
5459
//! true. If this is not a spawned child process, return false.
5560
virtual bool startSpawnedProcess(int argc, char* argv[], int& exit_status) = 0;
5661

62+
//! Connect to a socket address and make a client interface proxy object
63+
//! using provided callback. connectAddress returns an interface pointer if
64+
//! the connection was established, returns null if address is empty ("") or
65+
//! disabled ("0") or if a connection was refused but not required ("auto"),
66+
//! and throws an exception if there was an unexpected error.
67+
virtual std::unique_ptr<Init> connectAddress(std::string& address) = 0;
68+
69+
//! Connect to a socket address and make a client interface proxy object
70+
//! using provided callback. Throws an exception if there was an error.
71+
virtual void listenAddress(std::string& address) = 0;
72+
5773
//! Add cleanup callback to remote interface that will run when the
5874
//! interface is deleted.
5975
template<typename Interface>

src/ipc/capnp/protocol.cpp

+12-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include <mutex>
2424
#include <optional>
2525
#include <string>
26+
#include <sys/socket.h>
27+
#include <system_error>
2628
#include <thread>
2729

2830
namespace ipc {
@@ -51,11 +53,20 @@ class CapnpProtocol : public Protocol
5153
startLoop(exe_name);
5254
return mp::ConnectStream<messages::Init>(*m_loop, fd);
5355
}
54-
void serve(int fd, const char* exe_name, interfaces::Init& init) override
56+
void listen(int listen_fd, const char* exe_name, interfaces::Init& init) override
57+
{
58+
startLoop(exe_name);
59+
if (::listen(listen_fd, /*backlog=*/5) != 0) {
60+
throw std::system_error(errno, std::system_category());
61+
}
62+
mp::ListenConnections<messages::Init>(*m_loop, listen_fd, init);
63+
}
64+
void serve(int fd, const char* exe_name, interfaces::Init& init, const std::function<void()>& ready_fn = {}) override
5565
{
5666
assert(!m_loop);
5767
mp::g_thread_context.thread_name = mp::ThreadName(exe_name);
5868
m_loop.emplace(exe_name, &IpcLogFn, &m_context);
69+
if (ready_fn) ready_fn();
5970
mp::ServeStream<messages::Init>(*m_loop, fd, init);
6071
m_loop->loop();
6172
m_loop.reset();

src/ipc/interfaces.cpp

+30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <common/args.h>
56
#include <common/system.h>
67
#include <interfaces/init.h>
78
#include <interfaces/ipc.h>
@@ -56,6 +57,35 @@ class IpcImpl : public interfaces::Ipc
5657
exit_status = EXIT_SUCCESS;
5758
return true;
5859
}
60+
std::unique_ptr<interfaces::Init> connectAddress(std::string& address) override
61+
{
62+
if (address.empty() || address == "0") return nullptr;
63+
int fd;
64+
if (address == "auto") {
65+
// Treat "auto" the same as "unix" except don't treat it an as error
66+
// if the connection is not accepted. Just return null so the caller
67+
// can work offline without a connection, or spawn a new
68+
// bitcoin-node process and connect to it.
69+
address = "unix";
70+
try {
71+
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
72+
} catch (const std::system_error& e) {
73+
// If connection type is auto and socket path isn't accepting connections, or doesn't exist, catch the error and return null;
74+
if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory) {
75+
return nullptr;
76+
}
77+
throw;
78+
}
79+
} else {
80+
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
81+
}
82+
return m_protocol->connect(fd, m_exe_name);
83+
}
84+
void listenAddress(std::string& address) override
85+
{
86+
int fd = m_process->bind(gArgs.GetDataDirNet(), m_exe_name, address);
87+
m_protocol->listen(fd, m_exe_name, m_init);
88+
}
5989
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
6090
{
6191
m_protocol->addCleanup(type, iface, std::move(cleanup));

src/ipc/process.cpp

+95-1
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,28 @@
44

55
#include <ipc/process.h>
66
#include <ipc/protocol.h>
7+
#include <logging.h>
78
#include <mp/util.h>
89
#include <tinyformat.h>
910
#include <util/fs.h>
1011
#include <util/strencodings.h>
12+
#include <util/syserror.h>
1113

1214
#include <cstdint>
1315
#include <cstdlib>
16+
#include <errno.h>
1417
#include <exception>
1518
#include <iostream>
1619
#include <stdexcept>
1720
#include <string.h>
18-
#include <system_error>
21+
#include <sys/socket.h>
22+
#include <sys/un.h>
1923
#include <unistd.h>
2024
#include <utility>
2125
#include <vector>
2226

27+
using util::RemovePrefixView;
28+
2329
namespace ipc {
2430
namespace {
2531
class ProcessImpl : public Process
@@ -54,7 +60,95 @@ class ProcessImpl : public Process
5460
}
5561
return true;
5662
}
63+
int connect(const fs::path& data_dir,
64+
const std::string& dest_exe_name,
65+
std::string& address) override;
66+
int bind(const fs::path& data_dir, const std::string& exe_name, std::string& address) override;
5767
};
68+
69+
static bool ParseAddress(std::string& address,
70+
const fs::path& data_dir,
71+
const std::string& dest_exe_name,
72+
struct sockaddr_un& addr,
73+
std::string& error)
74+
{
75+
if (address.compare(0, 4, "unix") == 0 && (address.size() == 4 || address[4] == ':')) {
76+
fs::path path;
77+
if (address.size() <= 5) {
78+
path = data_dir / fs::PathFromString(strprintf("%s.sock", RemovePrefixView(dest_exe_name, "bitcoin-")));
79+
} else {
80+
path = data_dir / fs::PathFromString(address.substr(5));
81+
}
82+
std::string path_str = fs::PathToString(path);
83+
address = strprintf("unix:%s", path_str);
84+
if (path_str.size() >= sizeof(addr.sun_path)) {
85+
error = strprintf("Unix address path %s exceeded maximum socket path length", fs::quoted(fs::PathToString(path)));
86+
return false;
87+
}
88+
memset(&addr, 0, sizeof(addr));
89+
addr.sun_family = AF_UNIX;
90+
strncpy(addr.sun_path, path_str.c_str(), sizeof(addr.sun_path)-1);
91+
return true;
92+
}
93+
94+
error = strprintf("Unrecognized address '%s'", address);
95+
return false;
96+
}
97+
98+
int ProcessImpl::connect(const fs::path& data_dir,
99+
const std::string& dest_exe_name,
100+
std::string& address)
101+
{
102+
struct sockaddr_un addr;
103+
std::string error;
104+
if (!ParseAddress(address, data_dir, dest_exe_name, addr, error)) {
105+
throw std::invalid_argument(error);
106+
}
107+
108+
int fd;
109+
if ((fd = ::socket(addr.sun_family, SOCK_STREAM, 0)) == -1) {
110+
throw std::system_error(errno, std::system_category());
111+
}
112+
if (::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
113+
return fd;
114+
}
115+
int connect_error = errno;
116+
if (::close(fd) != 0) {
117+
LogPrintf("Error closing file descriptor %i '%s': %s\n", fd, address, SysErrorString(errno));
118+
}
119+
throw std::system_error(connect_error, std::system_category());
120+
}
121+
122+
int ProcessImpl::bind(const fs::path& data_dir, const std::string& exe_name, std::string& address)
123+
{
124+
struct sockaddr_un addr;
125+
std::string error;
126+
if (!ParseAddress(address, data_dir, exe_name, addr, error)) {
127+
throw std::invalid_argument(error);
128+
}
129+
130+
if (addr.sun_family == AF_UNIX) {
131+
fs::path path = addr.sun_path;
132+
if (path.has_parent_path()) fs::create_directories(path.parent_path());
133+
if (fs::symlink_status(path).type() == fs::file_type::socket) {
134+
fs::remove(path);
135+
}
136+
}
137+
138+
int fd;
139+
if ((fd = ::socket(addr.sun_family, SOCK_STREAM, 0)) == -1) {
140+
throw std::system_error(errno, std::system_category());
141+
}
142+
143+
if (::bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
144+
return fd;
145+
}
146+
int bind_error = errno;
147+
if (::close(fd) != 0) {
148+
LogPrintf("Error closing file descriptor %i: %s\n", fd, SysErrorString(errno));
149+
}
150+
throw std::system_error(bind_error, std::system_category());
151+
}
58152
} // namespace
59153

60154
std::unique_ptr<Process> MakeProcess() { return std::make_unique<ProcessImpl>(); }

src/ipc/process.h

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ class Process
3434
//! process. If so, return true and a file descriptor for communicating
3535
//! with the parent process.
3636
virtual bool checkSpawned(int argc, char* argv[], int& fd) = 0;
37+
38+
//! Canonicalize and connect to address, returning socket descriptor.
39+
virtual int connect(const fs::path& data_dir,
40+
const std::string& dest_exe_name,
41+
std::string& address) = 0;
42+
43+
//! Create listening socket, bind and canonicalize address, and return socket descriptor.
44+
virtual int bind(const fs::path& data_dir,
45+
const std::string& exe_name,
46+
std::string& address) = 0;
3747
};
3848

3949
//! Constructor for Process interface. Implementation will vary depending on

0 commit comments

Comments
 (0)