diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -8,3 +8,13 @@ look at the documentation for the build instructions. To continue using the autotools build system, pass the --enable-deprecated-build-system flag to `configure`. + + - The rpcallowip option can no longer be used to automatically listen + on all network interfaces. Instead, the rpcbind parameter must also + be used to specify the IP addresses to listen on. Listening for RPC + commands over a public network connection is insecure and should be + disabled, so a warning is now printed if a user selects such a + configuration. If you need to expose RPC in order to use a tool + like Docker, ensure you only bind RPC to your localhost, e.g. docker run [...] -p 127.0.0.1:8332:8332 (this is an extra :8332 over the + normal Docker port specification). + diff --git a/src/httpserver.cpp b/src/httpserver.cpp --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -316,10 +316,14 @@ std::vector> endpoints; // Determine what addresses to bind to - if (!gArgs.IsArgSet("-rpcallowip")) { + if (!(gArgs.IsArgSet("-rpcallowip") && gArgs.IsArgSet("-rpcbind"))) { // Default to loopback if not allowing external IPs. endpoints.push_back(std::make_pair("::1", http_port)); endpoints.push_back(std::make_pair("127.0.0.1", http_port)); + if (gArgs.IsArgSet("-rpcallowip")) { + LogPrintf("WARNING: option -rpcallowip was specified without " + "-rpcbind; this doesn't usually make sense\n"); + } if (gArgs.IsArgSet("-rpcbind")) { LogPrintf("WARNING: option -rpcbind was ignored because " "-rpcallowip was not specified, refusing to allow " @@ -333,10 +337,6 @@ SplitHostPort(strRPCBind, port, host); endpoints.push_back(std::make_pair(host, port)); } - } else { - // No specific bind address specified, bind to any. - endpoints.push_back(std::make_pair("::", http_port)); - endpoints.push_back(std::make_pair("0.0.0.0", http_port)); } // Bind addresses @@ -348,6 +348,13 @@ evhttp_bound_socket *bind_handle = evhttp_bind_socket_with_handle( http, i->first.empty() ? nullptr : i->first.c_str(), i->second); if (bind_handle) { + CNetAddr addr; + if (i->first.empty() || + (LookupHost(i->first.c_str(), addr, false) && + addr.IsBindAny())) { + LogPrintf("WARNING: the RPC server is not safe to expose to " + "untrusted networks such as the public internet\n"); + } boundSockets.push_back(bind_handle); } else { LogPrintf("Binding RPC on address %s port %i failed.\n", i->first, diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1027,12 +1027,12 @@ ArgsManager::ALLOW_ANY, OptionsCategory::RPC); gArgs.AddArg( "-rpcbind=[:port]", - "Bind to given address to listen for JSON-RPC connections. This option " - "is ignored unless -rpcallowip is also passed. Port is optional and " - "overrides -rpcport. Use [host]:port notation for IPv6. This option " - "can be specified multiple times (default: 127.0.0.1 and ::1 i.e., " - "localhost, or if -rpcallowip has been specified, 0.0.0.0 and :: i.e., " - "all addresses)", + "Bind to given address to listen for JSON-RPC connections. Do not " + "expose the RPC server to untrusted networks such as the public " + "internet! This option is ignored unless -rpcallowip is also passed. " + "Port is optional and overrides -rpcport. Use [host]:port notation " + "for IPv6. This option can be specified multiple times (default: " + "127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC); gArgs.AddArg("-rpccookiefile=", diff --git a/src/netaddress.h b/src/netaddress.h --- a/src/netaddress.h +++ b/src/netaddress.h @@ -56,6 +56,8 @@ // for Tor addresses bool SetSpecial(const std::string &strName); + // INADDR_ANY equivalent + bool IsBindAny() const; // IPv4 mapped address (::FFFF:0:0/96, 0.0.0.0/0) bool IsIPv4() const; // IPv6 address (not mapped IPv4, not Tor) diff --git a/src/netaddress.cpp b/src/netaddress.cpp --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -82,6 +82,17 @@ return ip[15 - n]; } +bool CNetAddr::IsBindAny() const { + const int cmplen = IsIPv4() ? 4 : 16; + for (int i = 0; i < cmplen; ++i) { + if (GetByte(i)) { + return false; + } + } + + return true; +} + bool CNetAddr::IsIPv4() const { return (memcmp(ip, pchIPv4, sizeof(pchIPv4)) == 0); } diff --git a/test/functional/rpc_bind.py b/test/functional/rpc_bind.py --- a/test/functional/rpc_bind.py +++ b/test/functional/rpc_bind.py @@ -58,10 +58,14 @@ at a non-localhost IP. ''' self.log.info("Allow IP test for {}:{}".format(rpchost, rpcport)) - base_args = ['-disablewallet', '-nolisten'] + \ - ['-rpcallowip=' + x for x in allow_ips] + node_args = \ + ['-disablewallet', '-nolisten'] + \ + ['-rpcallowip=' + x for x in allow_ips] + \ + ['-rpcbind=' + addr for addr in ['127.0.0.1', + "{}:{}".format(rpchost, + rpcport)]] # Bind to localhost as well so start_nodes doesn't hang self.nodes[0].host = None - self.start_nodes([base_args]) + self.start_nodes([node_args]) # connect to node through non-loopback interface url = rpc_url(self.nodes[0].datadir, rpchost, rpcport) node = get_rpc_proxy(url, 0, coveragedir=self.options.coveragedir) @@ -105,9 +109,9 @@ # check default without rpcallowip (IPv4 and IPv6 localhost) self.run_bind_test(None, '127.0.0.1', [], [('127.0.0.1', defaultport), ('::1', defaultport)]) - # check default with rpcallowip (IPv6 any) + # check default with rpcallowip (IPv4 and IPv6 localhost) self.run_bind_test(['127.0.0.1'], '127.0.0.1', [], - [('::0', defaultport)]) + [('127.0.0.1', defaultport), ('::1', defaultport)]) # check only IPv4 localhost (explicit) self.run_bind_test(['127.0.0.1'], '127.0.0.1', ['127.0.0.1'], [('127.0.0.1', defaultport)])