diff --git a/configure.ac b/configure.ac index d25bebeb1a1f..5e2804338ab7 100644 --- a/configure.ac +++ b/configure.ac @@ -601,6 +601,21 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include [ AC_MSG_RESULT(no)] ) +# Check for UNIX sockets +AC_MSG_CHECKING(for sockaddr_un) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include + #include ]], + [[ struct sockaddr_un addr; + addr.sun_family = AF_UNIX; ]])], + [ AC_MSG_RESULT(yes); + AC_DEFINE(HAVE_SOCKADDR_UN, 1,[Define this symbol if the sockaddr_un is available]) + AM_CONDITIONAL([BUILD_EVUNIX],[true]) + ], + [ AC_MSG_RESULT(no) + AM_CONDITIONAL([BUILD_EVUNIX],[false]) + ] +) + # Check for reduced exports if test x$use_reduce_exports = xyes; then AX_CHECK_COMPILE_FLAG([-fvisibility=hidden],[RE_CXXFLAGS="-fvisibility=hidden"], diff --git a/qa/README.md b/qa/README.md index f4dce7af5c3d..190b44db5d54 100644 --- a/qa/README.md +++ b/qa/README.md @@ -63,6 +63,10 @@ Possible options, which apply to each individual test run: If you set the environment variable `PYTHON_DEBUG=1` you will get some debug output (example: `PYTHON_DEBUG=1 qa/pull-tester/rpc-tests.py wallet`). +To force the tests to use RPC over TCP instead of a UNIX socket (this +can be useful for troubleshooting) define the environment variable +`BITCOIN_TEST_RPC_TCP` as `1`. + A 200-block -regtest blockchain and wallets for four nodes is created the first time a regression test is run and is stored in the cache/ directory. Each node has 25 mature diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 68e11b4c2ba0..ee3d04a08bf2 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -83,6 +83,7 @@ 'rpcnamedargs.py', 'listsinceblock.py', 'p2p-leaktests.py', + 'p2p-unixconn.py', ] ZMQ_SCRIPTS = [ diff --git a/qa/rpc-tests/abandonconflict.py b/qa/rpc-tests/abandonconflict.py index 887dbebd4f10..59cef159173a 100755 --- a/qa/rpc-tests/abandonconflict.py +++ b/qa/rpc-tests/abandonconflict.py @@ -23,8 +23,8 @@ def __init__(self): def setup_network(self): self.nodes = [] - self.nodes.append(start_node(0, self.options.tmpdir, ["-minrelaytxfee=0.00001"])) - self.nodes.append(start_node(1, self.options.tmpdir)) + self.nodes.append(start_node(0, self.options.tmpdir, ["-minrelaytxfee=0.00001"], rpchost="127.0.0.1")) + self.nodes.append(start_node(1, self.options.tmpdir, [], rpchost="127.0.0.1")) connect_nodes(self.nodes[0], 1) def run_test(self): diff --git a/qa/rpc-tests/httpbasics.py b/qa/rpc-tests/httpbasics.py index 8f35f0ab8799..67d5db366a93 100755 --- a/qa/rpc-tests/httpbasics.py +++ b/qa/rpc-tests/httpbasics.py @@ -17,7 +17,7 @@ def __init__(self): self.setup_clean_chain = False def setup_network(self): - self.nodes = self.setup_nodes() + self.nodes = self.setup_nodes(rpchost="127.0.0.1") def run_test(self): diff --git a/qa/rpc-tests/multi_rpc.py b/qa/rpc-tests/multi_rpc.py index 3b74bf1c4625..b0e9b82cec70 100755 --- a/qa/rpc-tests/multi_rpc.py +++ b/qa/rpc-tests/multi_rpc.py @@ -18,17 +18,13 @@ def __init__(self): self.setup_clean_chain = False self.num_nodes = 1 - def setup_chain(self): - super().setup_chain() - #Append rpcauth to bitcoin.conf before initialization - rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144" - rpcauth2 = "rpcauth=rt2:f8607b1a88861fac29dfccf9b52ff9f$ff36a0c23c8c62b4846112e50fa888416e94c17bfd4c42f88fd8f55ec6a3137e" - with open(os.path.join(self.options.tmpdir+"/node0", "bitcoin.conf"), 'a', encoding='utf8') as f: - f.write(rpcauth+"\n") - f.write(rpcauth2+"\n") - def setup_network(self): - self.nodes = self.setup_nodes() + # Pass in extra RPC authentication information + extra_args = [[ + "-rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144", + "-rpcauth=rt2:f8607b1a88861fac29dfccf9b52ff9f$ff36a0c23c8c62b4846112e50fa888416e94c17bfd4c42f88fd8f55ec6a3137e" + ]] + self.nodes = self.setup_nodes(rpchost='127.0.0.1', extra_args=extra_args) def run_test(self): diff --git a/qa/rpc-tests/nodehandling.py b/qa/rpc-tests/nodehandling.py index a6b10a0d83d2..c5e34294d2fb 100755 --- a/qa/rpc-tests/nodehandling.py +++ b/qa/rpc-tests/nodehandling.py @@ -16,6 +16,9 @@ def __init__(self): self.num_nodes = 4 self.setup_clean_chain = False + def setup_nodes(self): + return start_nodes(self.num_nodes, self.options.tmpdir, rpchost='127.0.0.1') + def run_test(self): ########################### # setban/listbanned tests # diff --git a/qa/rpc-tests/p2p-unixconn.py b/qa/rpc-tests/p2p-unixconn.py new file mode 100755 index 000000000000..988281efa6b9 --- /dev/null +++ b/qa/rpc-tests/p2p-unixconn.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test P2P connectivity over UNIX socket.""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * +from test_framework.uhttpconnection import have_af_unix + +def unix_connect_nodes(dirname, from_connection, node_num): + sock_path = ":unix:" + os.path.join(dirname, "node"+str(node_num), 'regtest', 'p2p_sock') + from_connection.addnode(sock_path, "onetry") + # poll until version handshake complete to avoid race conditions + # with transaction relaying + while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()): + time.sleep(0.1) + +def unix_connect_nodes_bi(dirname, nodes, a, b): + unix_connect_nodes(dirname, nodes[a], b) + unix_connect_nodes(dirname, nodes[b], a) + +class UnixP2PTest (BitcoinTestFramework): + '''Test P2P over UNIX socket''' + + def check_fee_amount(self, curr_balance, balance_with_fee, fee_per_byte, tx_size): + """Return curr_balance after asserting the fee was in range""" + fee = balance_with_fee - curr_balance + assert_fee_amount(fee, tx_size, fee_per_byte * 1000) + return curr_balance + + def __init__(self): + super().__init__() + self.setup_clean_chain = False + self.num_nodes = 3 + # tell nodes to bind P2P on UNIX socket + self.extra_args = [['-bind=:unix:p2p_sock'] for i in range(3)] + + def setup_network(self, split=False): + self.nodes = start_nodes(3, self.options.tmpdir, self.extra_args[:3]) + unix_connect_nodes_bi(self.options.tmpdir, self.nodes,0,1) + unix_connect_nodes_bi(self.options.tmpdir, self.nodes,0,2) + unix_connect_nodes_bi(self.options.tmpdir, self.nodes,1,2) + self.is_network_split=False + self.sync_all() + + def run_test (self): + assert_equal(self.nodes[0].getbalance(), 1250) + assert_equal(self.nodes[1].getbalance(), 1250) + assert_equal(self.nodes[2].getbalance(), 1250) + + # send 42 BTC to node 2 from node 0 and 1 + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 11) + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 10) + self.nodes[0].generate(1) + self.sync_all() + + self.nodes[1].sendtoaddress(self.nodes[2].getnewaddress(), 11) + self.nodes[1].sendtoaddress(self.nodes[2].getnewaddress(), 10) + self.nodes[1].generate(1) + self.sync_all() + + assert_equal(self.nodes[2].getbalance(), 1250 + 42) + + +if __name__ == '__main__': + if have_af_unix: + UnixP2PTest().main() diff --git a/qa/rpc-tests/rest.py b/qa/rpc-tests/rest.py index 776211d301f9..9fe5d488af02 100755 --- a/qa/rpc-tests/rest.py +++ b/qa/rpc-tests/rest.py @@ -49,7 +49,7 @@ def __init__(self): self.num_nodes = 3 def setup_network(self, split=False): - self.nodes = start_nodes(self.num_nodes, self.options.tmpdir) + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, rpchost='127.0.0.1') connect_nodes_bi(self.nodes,0,1) connect_nodes_bi(self.nodes,1,2) connect_nodes_bi(self.nodes,0,2) diff --git a/qa/rpc-tests/test_framework/conninfo.py b/qa/rpc-tests/test_framework/conninfo.py new file mode 100644 index 000000000000..18145c224fc9 --- /dev/null +++ b/qa/rpc-tests/test_framework/conninfo.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +import os + +from .uhttpconnection import UHTTPConnection + +# UNIX socket name for RPC connection (in data directory) +RPC_SOCKET_NAME = "rpc_socket" + +class RPCConnectInfo: + ''' + Base class for RPC connection info. This class encapsulates both information + required to connect to RPC, as to configure bitcoind to listen on that transport. + ''' + def make_connection(self): + ''' + Returns the 'connection' argument to pass to the RPCAuthService. + Can be None to let that class figure it out itself. + ''' + raise NotImplementedError + + @property + def bitcoind_args(self): + ''' + Return arguments for setting up bitcoind. + ''' + raise NotImplementedError + +class RPCConnectInfoTCP: + '''RPC connection info for connecting over TCP''' + def __init__(self, node_number, auth, rpchost, rpcport): + self.node_number = node_number # node number is just for informational purposes + self.auth = auth + self.host = '127.0.0.1' + self.port = rpcport + if rpchost: # "rpchost" can override both host and port + parts = rpchost.split(':') + self.host = parts[0] + if len(parts) == 2: + self.port = int(parts[1]) + self.url = "http://%s:%s@%s:%d" % (auth[0], auth[1], self.host, self.port) + + def make_connection(self): + # This is done inside the AuthServiceProxy + return None + + @property + def bitcoind_args(self): + # rpchost is ignored here because the test will take care of passing in the + # appropriate rpcbind arguments. + return [("rpcuser", self.auth[0]), + ("rpcpassword", self.auth[1]), + ("rpcport", str(self.port))] + +class RPCConnectInfoUNIX: + '''RPC connection info for connecting over UNIX socket''' + def __init__(self, node_number, auth, dirname): + self.node_number = node_number + self.auth = auth + self.sockname = os.path.join(dirname, RPC_SOCKET_NAME) + # use "localhost" as fake hostname. It doesn't matter. + self.url = "http://%s:%s@localhost" % auth + + def make_connection(self): + return UHTTPConnection(self.sockname) + + @property + def bitcoind_args(self): + return [("rpcuser", self.auth[0]), + ("rpcpassword", self.auth[1]), + ("rpcbind", ":unix:"+self.sockname), + ("rpcconnect", ":unix:"+self.sockname)] + diff --git a/qa/rpc-tests/test_framework/test_framework.py b/qa/rpc-tests/test_framework/test_framework.py index d7072fa78d23..090143dce470 100755 --- a/qa/rpc-tests/test_framework/test_framework.py +++ b/qa/rpc-tests/test_framework/test_framework.py @@ -51,8 +51,8 @@ def setup_chain(self): def stop_node(self, num_node): stop_node(self.nodes[num_node], num_node) - def setup_nodes(self): - return start_nodes(self.num_nodes, self.options.tmpdir) + def setup_nodes(self, rpchost=None, extra_args=None): + return start_nodes(self.num_nodes, self.options.tmpdir, rpchost=rpchost, extra_args=extra_args) def setup_network(self, split = False): self.nodes = self.setup_nodes() diff --git a/qa/rpc-tests/test_framework/uhttpconnection.py b/qa/rpc-tests/test_framework/uhttpconnection.py new file mode 100644 index 000000000000..acb49e759a22 --- /dev/null +++ b/qa/rpc-tests/test_framework/uhttpconnection.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# +# Inspired by "HTTP on Unix sockets with Python" +# From: http://7bits.nl/blog/posts/http-on-unix-sockets-with-python +import http.client +import socket + +def have_af_unix(): + '''Return True if UNIX sockets are available on this platform.''' + try: + socket.AF_UNIX + except AttributeError: + return False + else: + return True + +class UHTTPConnection(http.client.HTTPConnection): + """Subclass of Python library HTTPConnection that + uses a unix-domain socket. + """ + + def __init__(self, path): + http.client.HTTPConnection.__init__(self, 'localhost') + self.path = path + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock + diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index 23ac32451038..458b0e2bffa6 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -23,6 +23,8 @@ from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException +from .conninfo import RPCConnectInfoTCP, RPCConnectInfoUNIX +from .uhttpconnection import have_af_unix COVERAGE_DIR = None @@ -69,11 +71,10 @@ def enable_coverage(dirname): COVERAGE_DIR = dirname -def get_rpc_proxy(url, node_number, timeout=None): +def get_rpc_proxy(conninfo, timeout=None): """ Args: - url (str): URL of the RPC server to call - node_number (int): the node number (or id) that this calls to + urlinfo (RPCConnectInfo): Connection info of the RPC server to call Kwargs: timeout (int): HTTP timeout in seconds @@ -85,12 +86,13 @@ def get_rpc_proxy(url, node_number, timeout=None): proxy_kwargs = {} if timeout is not None: proxy_kwargs['timeout'] = timeout + proxy_kwargs['connection'] = conninfo.make_connection() - proxy = AuthServiceProxy(url, **proxy_kwargs) - proxy.url = url # store URL on proxy for info + proxy = AuthServiceProxy(conninfo.url, **proxy_kwargs) + proxy.url = conninfo.url # store URL on proxy for info coverage_logfile = coverage.get_filename( - COVERAGE_DIR, node_number) if COVERAGE_DIR else None + COVERAGE_DIR, conninfo.node_number) if COVERAGE_DIR else None return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) @@ -177,36 +179,39 @@ def sync_mempools(rpc_connections, *, wait=1, timeout=60): bitcoind_processes = {} -def initialize_datadir(dirname, n): +def initialize_datadir(dirname, n, rpchost=None): + ''' + Initialize datadir and write bitcoin configuration. + ''' datadir = os.path.join(dirname, "node"+str(n)) + conninfo = conninfo_for(n, rpchost, datadir) if not os.path.isdir(datadir): os.makedirs(datadir) - rpc_u, rpc_p = rpc_auth_pair(n) with open(os.path.join(datadir, "bitcoin.conf"), 'w', encoding='utf8') as f: f.write("regtest=1\n") - f.write("rpcuser=" + rpc_u + "\n") - f.write("rpcpassword=" + rpc_p + "\n") f.write("port="+str(p2p_port(n))+"\n") - f.write("rpcport="+str(rpc_port(n))+"\n") f.write("listenonion=0\n") - return datadir + for (key, value) in conninfo.bitcoind_args: + f.write('%s=%s\n' % (key, value)) + return datadir, conninfo def rpc_auth_pair(n): return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n) -def rpc_url(i, rpchost=None): - rpc_u, rpc_p = rpc_auth_pair(i) - host = '127.0.0.1' - port = rpc_port(i) - if rpchost: - parts = rpchost.split(':') - if len(parts) == 2: - host, port = parts - else: - host = rpchost - return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port)) +def conninfo_for(node_number, rpchost, datadir): + ''' + Return connection info for connecting to a certain node, by number. + This is where the decision of what transport to use is made. + + rpchost: Override host to connect to (if provided, forces connection through TCP). + datadir: Data directory + ''' + if have_af_unix() and rpchost is None and not int(os.getenv("BITCOIN_TEST_RPC_TCP","0")): # Prefer connecting over a UNIX socket, if available + return RPCConnectInfoUNIX(node_number, rpc_auth_pair(node_number), datadir) + else: + return RPCConnectInfoTCP(node_number, rpc_auth_pair(node_number), rpchost, rpc_port(node_number)) -def wait_for_bitcoind_start(process, url, i): +def wait_for_bitcoind_start(process, conninfo): ''' Wait for bitcoind to start. This means that RPC is accessible and fully initialized. Raise an exception if bitcoind exits during initialization. @@ -215,11 +220,11 @@ def wait_for_bitcoind_start(process, url, i): if process.poll() is not None: raise Exception('bitcoind exited with status %i during initialization' % process.returncode) try: - rpc = get_rpc_proxy(url, i) + rpc = get_rpc_proxy(conninfo) blocks = rpc.getblockcount() break # break out of loop on success except IOError as e: - if e.errno != errno.ECONNREFUSED: # Port not yet open? + if e.errno != errno.ECONNREFUSED and e.errno != errno.ENOENT: # Port not yet open or socket not yet created? raise # unknown IO error except JSONRPCException as e: # Initialization phase if e.error['code'] != -28: # RPC in warmup? @@ -248,20 +253,22 @@ def initialize_chain(test_dir, num_nodes, cachedir): shutil.rmtree(os.path.join(cachedir,"node"+str(i))) # Create cache directories, run bitcoinds: + conninfos = [] for i in range(MAX_NODES): - datadir=initialize_datadir(cachedir, i) + datadir, conninfo = initialize_datadir(cachedir, i) args = [ os.getenv("BITCOIND", "bitcoind"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ] if i > 0: args.append("-connect=127.0.0.1:"+str(p2p_port(0))) bitcoind_processes[i] = subprocess.Popen(args) logger.debug("initialize_chain: bitcoind started, waiting for RPC to come up") - wait_for_bitcoind_start(bitcoind_processes[i], rpc_url(i), i) + wait_for_bitcoind_start(bitcoind_processes[i], conninfo) logger.debug("initialize_chain: RPC successfully started") + conninfos.append(conninfo) rpcs = [] for i in range(MAX_NODES): try: - rpcs.append(get_rpc_proxy(rpc_url(i), i)) + rpcs.append(get_rpc_proxy(conninfos[i])) except: sys.stderr.write("Error connecting to "+url+"\n") sys.exit(1) @@ -305,24 +312,23 @@ def initialize_chain_clean(test_dir, num_nodes): Useful if a test case wants complete control over initialization. """ for i in range(num_nodes): - datadir=initialize_datadir(test_dir, i) + initialize_datadir(test_dir, i) def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None): """ Start a bitcoind and return RPC connection to it """ - datadir = os.path.join(dirname, "node"+str(i)) + datadir,conninfo = initialize_datadir(dirname, i, rpchost) if binary is None: binary = os.getenv("BITCOIND", "bitcoind") args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", "-mocktime="+str(get_mocktime()) ] if extra_args is not None: args.extend(extra_args) bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr) logger.debug("initialize_chain: bitcoind started, waiting for RPC to come up") - url = rpc_url(i, rpchost) - wait_for_bitcoind_start(bitcoind_processes[i], url, i) + wait_for_bitcoind_start(bitcoind_processes[i], conninfo) logger.debug("initialize_chain: RPC successfully started") - proxy = get_rpc_proxy(url, i, timeout=timewait) + proxy = get_rpc_proxy(conninfo, timeout=timewait) if COVERAGE_DIR: coverage.write_all_rpc_commands(COVERAGE_DIR, proxy) diff --git a/src/Makefile.am b/src/Makefile.am index e8d22313dca3..216cba5b44ab 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -136,6 +136,7 @@ BITCOIN_CORE_H = \ support/allocators/zeroafterfree.h \ support/cleanse.h \ support/events.h \ + support/evunix.h \ support/lockedpool.h \ sync.h \ threadsafety.h \ @@ -211,6 +212,9 @@ libbitcoin_server_a_SOURCES = \ validationinterface.cpp \ versionbits.cpp \ $(BITCOIN_CORE_H) +if BUILD_EVUNIX +libbitcoin_server_a_SOURCES += support/evunix.cpp +endif if ENABLE_ZMQ libbitcoin_zmq_a_CPPFLAGS = $(BITCOIN_INCLUDES) $(ZMQ_CFLAGS) @@ -377,6 +381,9 @@ bitcoind_LDADD += $(BOOST_LIBS) $(BDB_LIBS) $(SSL_LIBS) $(CRYPTO_LIBS) $(MINIUPN # bitcoin-cli binary # bitcoin_cli_SOURCES = bitcoin-cli.cpp +if BUILD_EVUNIX +bitcoin_cli_SOURCES += support/evunix.cpp +endif bitcoin_cli_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(EVENT_CFLAGS) bitcoin_cli_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) bitcoin_cli_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index ed8ca7e14c50..d0ebe31e75a1 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -15,11 +15,14 @@ #include "utilstrencodings.h" #include +#include #include #include #include +#include #include "support/events.h" +#include "support/evunix.h" #include @@ -195,8 +198,35 @@ UniValue CallRPC(const std::string& strMethod, const UniValue& params) // Obtain event base raii_event_base base = obtain_event_base(); - // Synchronously look up hostname - raii_evhttp_connection evcon = obtain_evhttp_connection_base(base.get(), host, port); + raii_evhttp_connection evcon; + if (boost::starts_with(host, RPC_ADDR_PREFIX_UNIX)) { +#if defined(HAVE_SOCKADDR_UN) && defined(LIBEVENT_EXPERIMENTAL) + // This requires a small patch to libevent to be able to pass in a file + // descriptor of an existing connection, which can be found here: + // https://github.com/libevent/libevent/pull/479 + // When this lands into a release, replace the LIBEVENT_EXPERIMENTAL with a + // check on LIBEVENT_VERSION_NUMBER. + boost::filesystem::path name = boost::filesystem::path(host.substr(RPC_ADDR_PREFIX_UNIX.size())); + // If path is not complete, it is interpreted relative to the data directory. + if (!name.is_complete()) { + name = GetDataDir(true) / name; + } + struct bufferevent *bev = evunix_connect(base.get(), name); + if (bev == NULL) { + throw CConnectionFailed("couldn't connect to UNIX socket " + name.string()); + } + evcon = raii_evhttp_connection( + evhttp_connection_base_bufferevent_new(base.get(), NULL, bev, "", 0) + ); +#elif defined(HAVE_SOCKADDR_UN) + throw std::runtime_error("RPC was asked to connect to a UNIX socket. However, the version of libevent that this utility was compiled with has no support for that."); +#else + throw std::runtime_error("RPC was asked to connect to a UNIX socket, which is not supported on this system"); +#endif + } else { + // Synchronously look up hostname + evcon = obtain_evhttp_connection_base(base.get(), host, port); + } evhttp_connection_set_timeout(evcon.get(), GetArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT)); HTTPReply response; diff --git a/src/httpserver.cpp b/src/httpserver.cpp index e1763c6ad276..915c7651c786 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -21,12 +21,14 @@ #include #include +#include #include #include #include #include #include #include +#include "support/evunix.h" #ifdef EVENT__HAVE_NETINET_IN_H #include @@ -181,6 +183,10 @@ static WorkQueue* workQueue = 0; std::vector pathHandlers; //! Bound listening sockets std::vector boundSockets; +#ifdef HAVE_SOCKADDR_UN +//! UNIX sockets to clean up on shutdown +std::vector cleanupUNIXSockets; +#endif /** Check if a network address is allowed to access the HTTP server */ static bool ClientAllowed(const CNetAddr& netaddr) @@ -331,6 +337,8 @@ static bool HTTPBindAddresses(struct evhttp* http) } else if (mapMultiArgs.count("-rpcbind")) { // Specific bind address const std::vector& vbind = mapMultiArgs.at("-rpcbind"); for (std::vector::const_iterator i = vbind.begin(); i != vbind.end(); ++i) { + if (boost::starts_with(*i, RPC_ADDR_PREFIX_UNIX)) // Skip UNIX sockets here + continue; int port = defaultPort; std::string host; SplitHostPort(*i, port, host); @@ -351,6 +359,40 @@ static bool HTTPBindAddresses(struct evhttp* http) LogPrintf("Binding RPC on address %s port %i failed.\n", i->first, i->second); } } + + // UNIX socket binding: this is done in a separate pass because it does not care whether + // `-rpcallowip` is set. + if (mapMultiArgs.count("-rpcbind")) { + const std::vector& vbind = mapMultiArgs.at("-rpcbind"); + for (std::vector::const_iterator i = vbind.begin(); i != vbind.end(); ++i) { + if (boost::starts_with(*i, RPC_ADDR_PREFIX_UNIX)) { +#ifdef HAVE_SOCKADDR_UN + boost::filesystem::path name = boost::filesystem::path(i->substr(RPC_ADDR_PREFIX_UNIX.size())); + int fd; + evhttp_bound_socket *bind_handle = 0; + + // If path is not complete, it is interpreted relative to the data directory. + if (!name.is_complete()) { + name = GetDataDir(true) / name; + } + + LogPrint("http", "Binding RPC on UNIX socket %s\n", name.string()); + if ((fd = evunix_bind_fd(name)) != -1) { + bind_handle = evhttp_accept_socket_with_handle(http, fd); + } + if (bind_handle) { + boundSockets.push_back(bind_handle); + cleanupUNIXSockets.push_back(name); + } else { + LogPrintf("Binding RPC on UNIX socket %s failed.\n", name.string()); + } +#else + LogPrintf("WARNING: RPC was asked to bind on UNIX socket, which is not supported on this system\n"); +#endif + } + } + } + return !boundSockets.empty(); } @@ -505,6 +547,12 @@ void StopHTTPServer() event_base_free(eventBase); eventBase = 0; } +#ifdef HAVE_SOCKADDR_UN + //! Clean up UNIX sockets on shutdown + for (const auto& path: cleanupUNIXSockets) { + evunix_remove_socket(path); + } +#endif LogPrint("http", "Stopped HTTP server\n"); } @@ -615,6 +663,17 @@ CService HTTPRequest::GetPeer() evhttp_connection* con = evhttp_request_get_connection(req); CService peer; if (con) { +#ifdef HAVE_SOCKADDR_UN + // evhttp has no way to query what bindsocket a connection + // came in on, so we need this low-level code to be able to + // correctly mark UNIX socket connections as coming from localhost. + struct bufferevent *bev = evhttp_connection_get_bufferevent(con); + if (bev && evunix_is_conn_from_unix(bev)) { + // As we have no way to signify "UNIX localhost" here, just return + // IPv6 localhost. + return LookupNumeric("::1", 0); + } +#endif // evhttp retains ownership over returned address string const char* address = ""; uint16_t port = 0; diff --git a/src/init.cpp b/src/init.cpp index 93131b4f94c4..6a8c8ea119c6 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -288,6 +288,21 @@ bool static Bind(CConnman& connman, const CService &addr, unsigned int flags) { } return true; } + +bool static BindUNIX(CConnman& connman, boost::filesystem::path path, unsigned int flags) { + // If path is not complete, it is interpreted relative to the data directory. + if (!path.is_complete()) { + path = GetDataDir(true) / path; + } + std::string strError; + if (!connman.BindUNIX(path, strError, (flags & BF_WHITELIST) != 0)) { + if (flags & BF_REPORT_ERROR) + return InitError(strError); + return false; + } + return true; +} + void OnRPCStarted() { uiInterface.NotifyBlockTip.connect(&RPCNotifyBlockChange); @@ -1318,20 +1333,28 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) bool fBound = false; if (mapMultiArgs.count("-bind")) { BOOST_FOREACH(const std::string& strBind, mapMultiArgs.at("-bind")) { - CService addrBind; - if (!Lookup(strBind.c_str(), addrBind, GetListenPort(), false)) - return InitError(ResolveErrMsg("bind", strBind)); - fBound |= Bind(connman, addrBind, (BF_EXPLICIT | BF_REPORT_ERROR)); + if (boost::starts_with(strBind, P2P_ADDR_PREFIX_UNIX)) { + fBound |= BindUNIX(connman, boost::filesystem::path(strBind.substr(RPC_ADDR_PREFIX_UNIX.size())), (BF_EXPLICIT | BF_REPORT_ERROR)); + } else { + CService addrBind; + if (!Lookup(strBind.c_str(), addrBind, GetListenPort(), false)) + return InitError(ResolveErrMsg("bind", strBind)); + fBound |= Bind(connman, addrBind, (BF_EXPLICIT | BF_REPORT_ERROR)); + } } } if (mapMultiArgs.count("-whitebind")) { BOOST_FOREACH(const std::string& strBind, mapMultiArgs.at("-whitebind")) { - CService addrBind; - if (!Lookup(strBind.c_str(), addrBind, 0, false)) - return InitError(ResolveErrMsg("whitebind", strBind)); - if (addrBind.GetPort() == 0) - return InitError(strprintf(_("Need to specify a port with -whitebind: '%s'"), strBind)); - fBound |= Bind(connman, addrBind, (BF_EXPLICIT | BF_REPORT_ERROR | BF_WHITELIST)); + if (boost::starts_with(strBind, P2P_ADDR_PREFIX_UNIX)) { + fBound |= BindUNIX(connman, boost::filesystem::path(strBind.substr(RPC_ADDR_PREFIX_UNIX.size())), (BF_EXPLICIT | BF_REPORT_ERROR | BF_WHITELIST)); + } else { + CService addrBind; + if (!Lookup(strBind.c_str(), addrBind, 0, false)) + return InitError(ResolveErrMsg("whitebind", strBind)); + if (addrBind.GetPort() == 0) + return InitError(strprintf(_("Need to specify a port with -whitebind: '%s'"), strBind)); + fBound |= Bind(connman, addrBind, (BF_EXPLICIT | BF_REPORT_ERROR | BF_WHITELIST)); + } } } if (!mapMultiArgs.count("-bind") && !mapMultiArgs.count("-whitebind")) { diff --git a/src/net.cpp b/src/net.cpp index 6ff63d47309e..6fbaad8762bb 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -17,6 +17,7 @@ #include "crypto/sha256.h" #include "hash.h" #include "primitives/transaction.h" +#include "support/evunix.h" #include "netbase.h" #include "scheduler.h" #include "ui_interface.h" @@ -2092,6 +2093,38 @@ bool CConnman::BindListenPort(const CService &addrBind, std::string& strError, b return true; } +bool CConnman::BindUNIX(const boost::filesystem::path &path, std::string& strError, bool fWhitelisted) +{ +#ifdef HAVE_SOCKADDR_UN + // Bind on UNIX socket + int fd = evunix_bind_fd(path); + if (fd < 0) + { + strError = "Unable to bind to UNIX socket " + path.string(); + LogPrintf("%s\n", strError); + return false; + } + SOCKET hListenSocket = (SOCKET)fd; + LogPrintf("P2P bound to %s\n", path.string()); + + // Listen for incoming connections + if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR) + { + strError = strprintf(_("Error: Listening for incoming connections failed (listen returned error %s)"), NetworkErrorString(WSAGetLastError())); + LogPrintf("%s\n", strError); + CloseSocket(hListenSocket); + return false; + } + + vhListenSocket.push_back(ListenSocket(hListenSocket, fWhitelisted)); + return true; +#else + strError = "Error: P2P was asked to bind on UNIX socket, which is not supported on this system"; + LogPrintf("%s\n", strError); + return false; +#endif +} + void Discover(boost::thread_group& threadGroup) { if (!fDiscover) diff --git a/src/net.h b/src/net.h index 29b6a44c88ff..7c64ada52710 100644 --- a/src/net.h +++ b/src/net.h @@ -152,6 +152,7 @@ class CConnman void Stop(); void Interrupt(); bool BindListenPort(const CService &bindAddr, std::string& strError, bool fWhitelisted = false); + bool BindUNIX(const boost::filesystem::path &path, std::string& strError, bool fWhitelisted = false); bool GetNetworkActive() const { return fNetworkActive; }; void SetNetworkActive(bool active); bool OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false, bool fFeeler = false, bool fAddnode = false); diff --git a/src/netbase.cpp b/src/netbase.cpp index fc9a6ed0be8e..1fbedb180cfb 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -15,6 +15,7 @@ #include "random.h" #include "util.h" #include "utilstrencodings.h" +#include "support/evunix.h" #include @@ -40,6 +41,9 @@ bool fNameLookup = DEFAULT_NAME_LOOKUP; static const int SOCKS5_RECV_TIMEOUT = 20 * 1000; static std::atomic interruptSocks5Recv(false); +/** Prefix for UNIX socket addresses on bind/connect */ +const std::string P2P_ADDR_PREFIX_UNIX = ":unix:"; + enum Network ParseNetwork(std::string net) { boost::to_lower(net); if (net == "ipv4") return NET_IPV4; @@ -587,14 +591,38 @@ bool ConnectSocket(const CService &addrDest, SOCKET& hSocketRet, int nTimeout, b return ConnectSocketDirectly(addrDest, hSocketRet, nTimeout); } -bool ConnectSocketByName(CService &addr, SOCKET& hSocketRet, const char *pszDest, int portDefault, int nTimeout, bool *outProxyConnectionFailed) +static bool ConnectUNIXSocket(SOCKET& hSocketRet, boost::filesystem::path path) { - std::string strDest; - int port = portDefault; + if (!path.is_complete()) { + path = GetDataDir(true) / path; + } +#ifdef HAVE_SOCKADDR_UN + int fd = evunix_connect_fd(path); + if (fd >= 0) { + hSocketRet = (SOCKET)fd; + return true; + } else { + return false; + } +#else + LogPrintf("WARNING: P2P was asked to connect on UNIX socket %s, which is not supported on this system\n", path.string()); + return false; +#endif +} +bool ConnectSocketByName(CService &addr, SOCKET& hSocketRet, const char *pszDest, int portDefault, int nTimeout, bool *outProxyConnectionFailed) +{ if (outProxyConnectionFailed) *outProxyConnectionFailed = false; + std::string pszDestStr(pszDest); + if (boost::starts_with(pszDestStr, P2P_ADDR_PREFIX_UNIX)) { + boost::filesystem::path name = boost::filesystem::path(pszDestStr.substr(P2P_ADDR_PREFIX_UNIX.size())); + return ConnectUNIXSocket(hSocketRet, name); + } + + std::string strDest; + int port = portDefault; SplitHostPort(std::string(pszDest), port, strDest); proxyType proxy; diff --git a/src/netbase.h b/src/netbase.h index dd33b6e47e1b..aed5ec69a89b 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -25,6 +25,9 @@ static const int DEFAULT_CONNECT_TIMEOUT = 5000; //! -dns default static const int DEFAULT_NAME_LOOKUP = true; +/** Prefix for UNIX socket addresses on bind/connect */ +extern const std::string P2P_ADDR_PREFIX_UNIX; + class proxyType { public: diff --git a/src/rpc/protocol.cpp b/src/rpc/protocol.cpp index 2be1edb5a6f5..0f768dfdc3c0 100644 --- a/src/rpc/protocol.cpp +++ b/src/rpc/protocol.cpp @@ -15,6 +15,9 @@ #include #include +/** To bind to a UNIX socket, use this prefix on rpcbind: */ +const std::string RPC_ADDR_PREFIX_UNIX = ":unix:"; + /** * JSON-RPC protocol. Bitcoin speaks version 1.0 for maximum compatibility, * but uses JSON-RPC 1.1/2.0 standards for parts of the 1.0 standard that were diff --git a/src/rpc/protocol.h b/src/rpc/protocol.h index 85bc4db10177..0aa5f1185ca0 100644 --- a/src/rpc/protocol.h +++ b/src/rpc/protocol.h @@ -88,6 +88,9 @@ UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const Un std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id); UniValue JSONRPCError(int code, const std::string& message); +/** To bind to a UNIX socket, use this prefix on rpcbind. */ +extern const std::string RPC_ADDR_PREFIX_UNIX; + /** Get name of RPC authentication cookie file */ boost::filesystem::path GetAuthCookieFile(); /** Generate a new RPC authentication cookie and write it to disk */ diff --git a/src/support/evunix.cpp b/src/support/evunix.cpp new file mode 100644 index 000000000000..dc26be7c3ba3 --- /dev/null +++ b/src/support/evunix.cpp @@ -0,0 +1,128 @@ +// Copyright (c) 2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include "evunix.h" + +#include +#include + +#include +#include + +/** Helper function to initialize a sockaddr_un from a path */ +static bool sockaddr_from_path(struct sockaddr_un *addr, const boost::filesystem::path &path) +{ + std::string name = path.string(); + if (name.size() >= sizeof(addr->sun_path)) { + /* Name too long */ + return false; + } + memset(addr, 0, sizeof(struct sockaddr_un)); + addr->sun_family = AF_UNIX; + strncpy(addr->sun_path, name.c_str(), sizeof(addr->sun_path)-1); + return true; +} + +bool evunix_remove_socket(const boost::filesystem::path &path) +{ + if (boost::filesystem::status(path).type() == boost::filesystem::socket_file) { + boost::system::error_code ec; + boost::filesystem::remove(path, ec); + if (ec) { /* error while deleting */ + return false; + } + } + return true; +} + +int evunix_bind_fd(const boost::filesystem::path &path) +{ + struct sockaddr_un addr; + if (!sockaddr_from_path(&addr, path)) { + return -1; + } + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + /* Remove any previous sockets left behind. listen() will refuse to overwrite + * any file or socket. Remove only sockets, not other files that happen to have + * the same name. + */ + if (!evunix_remove_socket(path)) { + close(fd); + return -1; + } + /* Bind and listen */ + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + close(fd); + return -1; + } + if (listen(fd, 5) < 0) { + close(fd); + return -1; + } + evutil_make_socket_nonblocking(fd); + return fd; +} + +int evunix_connect_fd(const boost::filesystem::path &path) +{ + struct sockaddr_un addr; + if (!sockaddr_from_path(&addr, path)) { + return -1; + } + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + close(fd); + return -1; + } + evutil_make_socket_nonblocking(fd); + return fd; +} + +bool evunix_is_conn_from_unix_fd(int fd) +{ + struct sockaddr_un peer_unix; + socklen_t peer_unix_len = sizeof(peer_unix); + if (getpeername(fd, (sockaddr*)&peer_unix, &peer_unix_len) == 0) { + if (peer_unix.sun_family == AF_UNIX) { + return true; + } + } + return false; +} + +struct bufferevent *evunix_bind(struct event_base *base, const boost::filesystem::path &path) +{ + struct bufferevent *rv; + int fd = evunix_bind_fd(path); + if ((rv = bufferevent_socket_new(base, fd, 0)) == NULL) { + close(fd); + return NULL; + } + return rv; +} + +struct bufferevent *evunix_connect(struct event_base *base, const boost::filesystem::path &path) +{ + struct bufferevent *rv; + int fd = evunix_connect_fd(path); + if ((rv = bufferevent_socket_new(base, fd, 0)) == NULL) { + close(fd); + return NULL; + } + return rv; +} + +bool evunix_is_conn_from_unix(struct bufferevent *bev) +{ + int fd; + if ((fd = bufferevent_getfd(bev)) != -1) { + return evunix_is_conn_from_unix_fd(fd); + } + return false; +} diff --git a/src/support/evunix.h b/src/support/evunix.h new file mode 100644 index 000000000000..7dd2ad477ccd --- /dev/null +++ b/src/support/evunix.h @@ -0,0 +1,56 @@ +// Copyright (c) 2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +#ifndef BITCOIN_EVUNIX_H +#define BITCOIN_EVUNIX_H + +/** Libevent<->UNIX socket bridge functions */ + +#include + +struct event_base; +struct bufferevent; + +// All these functions come in a plain (high-level) and _fd (low-level) +// variant. The plain version takes/yields a libevent bufferevent*, the _fd +// functions file descriptor. + +/** Bind on a UNIX socket. + * Returns a bufferevent that can be used to send or receive data on the socket, or NULL + * on failure. + */ +struct bufferevent *evunix_bind(const boost::filesystem::path &path); + +/** Bind on a UNIX socket, return fd. + * Return a file descriptor ready to pass to evhttp_accept_socket_with_handle, or -1 + * on failure. + */ +int evunix_bind_fd(const boost::filesystem::path &path); + +/** Connect to a UNIX socket. + * Returns a bufferevent that can be used to send or receive data on the socket, or NULL + * on failure. + */ +struct bufferevent *evunix_connect(struct event_base *base, const boost::filesystem::path &path); + +/** Connect to a UNIX socket, return fd. + * Return a file descriptor ready to use, or -1 on failure. + */ +int evunix_connect_fd(const boost::filesystem::path &path); + +/* Remove only sockets, not other files that happen to have + * the same name. + */ +bool evunix_remove_socket(const boost::filesystem::path &path); + +/** Return whether incoming connection fd came in on a UNIX socket. + */ +bool evunix_is_conn_from_unix_fd(int fd); + +/** Return whether incoming connection bev came in on a UNIX socket. + * This is a hack because evhttp won't let us know what bound socket a connection + * came in on. + */ +bool evunix_is_conn_from_unix(struct bufferevent *bev); + +#endif