Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: Detect port errors in rpcconnect and rpcport #29521

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/bitcoin-cli.cpp
Expand Up @@ -743,8 +743,41 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
// 2. port in -rpcconnect (ie following : in ipv4 or ]: in ipv6)
// 3. default port for chain
uint16_t port{BaseParams().RPCPort()};
SplitHostPort(gArgs.GetArg("-rpcconnect", DEFAULT_RPCCONNECT), port, host);
port = static_cast<uint16_t>(gArgs.GetIntArg("-rpcport", port));
{
tdb3 marked this conversation as resolved.
Show resolved Hide resolved
uint16_t rpcconnect_port{0};
const std::string rpcconnect_str = gArgs.GetArg("-rpcconnect", DEFAULT_RPCCONNECT);
if (!SplitHostPort(rpcconnect_str, rpcconnect_port, host)) {
// Uses argument provided as-is
// (rather than value parsed)
// to aid the user in troubleshooting
throw std::runtime_error(strprintf("Invalid port provided in -rpcconnect: %s", rpcconnect_str));
} else {
if (rpcconnect_port != 0) {
tdb3 marked this conversation as resolved.
Show resolved Hide resolved
// Use the valid port provided in rpcconnect
port = rpcconnect_port;
} // else, no port was provided in rpcconnect (continue using default one)
}

if (std::optional<std::string> rpcport_arg = gArgs.GetArg("-rpcport")) {
// -rpcport was specified
const uint16_t rpcport_int{ToIntegral<uint16_t>(rpcport_arg.value()).value_or(0)};
if (rpcport_int == 0) {
// Uses argument provided as-is
// (rather than value parsed)
// to aid the user in troubleshooting
throw std::runtime_error(strprintf("Invalid port provided in -rpcport: %s", rpcport_arg.value()));
}

// Use the valid port provided
port = rpcport_int;

// If there was a valid port provided in rpcconnect,
// rpcconnect_port is non-zero.
if (rpcconnect_port != 0) {
tfm::format(std::cerr, "Warning: Port specified in both -rpcconnect and -rpcport. Using -rpcport %u\n", port);
}
}
}

// Obtain event base
raii_event_base base = obtain_event_base();
Expand Down
49 changes: 49 additions & 0 deletions test/functional/interface_bitcoin_cli.py
Expand Up @@ -8,13 +8,15 @@
import re

from test_framework.blocktools import COINBASE_MATURITY
from test_framework.netutil import test_ipv6_local
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than_or_equal,
assert_raises_process_error,
assert_raises_rpc_error,
get_auth_cookie,
rpc_port,
)
import time

Expand Down Expand Up @@ -107,6 +109,53 @@ def run_test(self):
self.log.info("Test connecting to a non-existing server")
assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo)

self.log.info("Test handling of invalid ports in rpcconnect")
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:notaport", self.nodes[0].cli("-rpcconnect=127.0.0.1:notaport").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:-1", self.nodes[0].cli("-rpcconnect=127.0.0.1:-1").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:0", self.nodes[0].cli("-rpcconnect=127.0.0.1:0").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:65536", self.nodes[0].cli("-rpcconnect=127.0.0.1:65536").echo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this line and it seemed to pass

assert_raises_process_error(1, 'timeout on transient error: Could not connect to the server 127.0.0.1:::', self.nodes[0].cli("-rpcconnect=127.0.0.1::").echo)

looks like SplitHostPort does not throw an exception if there are two colons and no port provided

I am unsure if that is needed logic in SplitHostPort

I'm seeing the same error when I do
self.nodes[0].cli("-rpcconnect=127.0.0.256")
probably because 255 is the max value

Copy link
Contributor Author

@tdb3 tdb3 Apr 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kevkevinpal. Good example of why review is valued. Your findings nudged me to add some assert test statements (which otherwise wouldn't be present) to help cover cases involving IPv6 hosts.

We're experiencing the result of the scope of SplitHostPort(). SplitHostPort() extracts the host and port. Although the port is validated, host is not (and the function declaration comment header mentions validating the port, but doesn't mention validating the host).

/**
* Splits socket address string into host string and port value.
* Validates port value.
*
* @param[in] in The socket address string to split.
* @param[out] portOut Port-portion of the input, if found and parsable.
* @param[out] hostOut Host-portion of the input, if found.
* @return true if port-portion is absent or within its allowed range, otherwise false
*/
bool SplitHostPort(std::string_view in, uint16_t& portOut, std::string& hostOut);

SplitHostPort() is treating "127.0.0.1::" as an IPv6 host since "::" is used for IPv6 address shortening. SplitHostPort() detects no port as part of the input string, which would be why Could not connect to the server 127.0.0.1:::18443 is the error provided for src/bitcoin-cli -rpcconnect=127.0.0.1:: getblockcount (the ":18443" is tacked on the end because it's the default port for regtest).

An enhancement would be to perform a validation of the host received from SplitHostPort() before bitcoin-cli attempts to contact the host. I'm thinking we would need to first define what is "valid" and what is "invalid" for use with bitcoin-cli. The gist of this kind of checking seems to be present in CNetAddr. In general, I would think bitcoin-cli shouldn't be overly restrictive in what it lets the user attempt to use as a host.

bitcoin/src/netaddress.h

Lines 157 to 181 in 7fee0ca

[[nodiscard]] bool IsIPv4() const { return m_net == NET_IPV4; } // IPv4 mapped address (::FFFF:0:0/96, 0.0.0.0/0)
[[nodiscard]] bool IsIPv6() const { return m_net == NET_IPV6; } // IPv6 address (not mapped IPv4, not Tor)
bool IsRFC1918() const; // IPv4 private networks (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12)
bool IsRFC2544() const; // IPv4 inter-network communications (198.18.0.0/15)
bool IsRFC6598() const; // IPv4 ISP-level NAT (100.64.0.0/10)
bool IsRFC5737() const; // IPv4 documentation addresses (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24)
bool IsRFC3849() const; // IPv6 documentation address (2001:0DB8::/32)
bool IsRFC3927() const; // IPv4 autoconfig (169.254.0.0/16)
bool IsRFC3964() const; // IPv6 6to4 tunnelling (2002::/16)
bool IsRFC4193() const; // IPv6 unique local (FC00::/7)
bool IsRFC4380() const; // IPv6 Teredo tunnelling (2001::/32)
bool IsRFC4843() const; // IPv6 ORCHID (deprecated) (2001:10::/28)
bool IsRFC7343() const; // IPv6 ORCHIDv2 (2001:20::/28)
bool IsRFC4862() const; // IPv6 autoconfig (FE80::/64)
bool IsRFC6052() const; // IPv6 well-known prefix for IPv4-embedded address (64:FF9B::/96)
bool IsRFC6145() const; // IPv6 IPv4-translated address (::FFFF:0:0:0/96) (actually defined in RFC2765)
bool IsHeNet() const; // IPv6 Hurricane Electric - https://he.net (2001:0470::/36)
[[nodiscard]] bool IsTor() const { return m_net == NET_ONION; }
[[nodiscard]] bool IsI2P() const { return m_net == NET_I2P; }
[[nodiscard]] bool IsCJDNS() const { return m_net == NET_CJDNS; }
[[nodiscard]] bool HasCJDNSPrefix() const { return m_addr[0] == CJDNS_PREFIX; }
bool IsLocal() const;
bool IsRoutable() const;
bool IsInternal() const;
bool IsValid() const;

For now, it might make the most sense to leave the enhancement of host validation to a new PR since this PR is focused on port error detection.


tdb3 marked this conversation as resolved.
Show resolved Hide resolved
self.log.info("Checking for IPv6")
have_ipv6 = test_ipv6_local()
if not have_ipv6:
self.log.info("Skipping IPv6 tests")

if have_ipv6:
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:notaport", self.nodes[0].cli("-rpcconnect=[::1]:notaport").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:-1", self.nodes[0].cli("-rpcconnect=[::1]:-1").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:0", self.nodes[0].cli("-rpcconnect=[::1]:0").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:65536", self.nodes[0].cli("-rpcconnect=[::1]:65536").echo)

self.log.info("Test handling of invalid ports in rpcport")
assert_raises_process_error(1, "Invalid port provided in -rpcport: notaport", self.nodes[0].cli("-rpcport=notaport").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcport: -1", self.nodes[0].cli("-rpcport=-1").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcport: 0", self.nodes[0].cli("-rpcport=0").echo)
assert_raises_process_error(1, "Invalid port provided in -rpcport: 65536", self.nodes[0].cli("-rpcport=65536").echo)

self.log.info("Test port usage preferences")
node_rpc_port = rpc_port(self.nodes[0].index)
# Prevent bitcoin-cli from using existing rpcport in conf
conf_rpcport = "rpcport=" + str(node_rpc_port)
self.nodes[0].replace_in_config([(conf_rpcport, "#" + conf_rpcport)])
# prefer rpcport over rpcconnect
assert_raises_process_error(1, "Could not connect to the server 127.0.0.1:1", self.nodes[0].cli(f"-rpcconnect=127.0.0.1:{node_rpc_port}", "-rpcport=1").echo)
if have_ipv6:
assert_raises_process_error(1, "Could not connect to the server ::1:1", self.nodes[0].cli(f"-rpcconnect=[::1]:{node_rpc_port}", "-rpcport=1").echo)

assert_equal(BLOCKS, self.nodes[0].cli("-rpcconnect=127.0.0.1:18999", f'-rpcport={node_rpc_port}').getblockcount())
if have_ipv6:
assert_equal(BLOCKS, self.nodes[0].cli("-rpcconnect=[::1]:18999", f'-rpcport={node_rpc_port}').getblockcount())

# prefer rpcconnect port over default
assert_equal(BLOCKS, self.nodes[0].cli(f"-rpcconnect=127.0.0.1:{node_rpc_port}").getblockcount())
if have_ipv6:
assert_equal(BLOCKS, self.nodes[0].cli(f"-rpcconnect=[::1]:{node_rpc_port}").getblockcount())

# prefer rpcport over default
assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcport={node_rpc_port}').getblockcount())
# Re-enable rpcport in conf if present
self.nodes[0].replace_in_config([("#" + conf_rpcport, conf_rpcport)])

tdb3 marked this conversation as resolved.
Show resolved Hide resolved
self.log.info("Test connecting with non-existing RPC cookie file")
assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo)

Expand Down