Skip to content

Commit 29c1fbb

Browse files
committed
Merge pull request #3695
b5ad5e7 Add Python test for -rpcbind and -rpcallowip (Wladimir J. van der Laan) f923c07 Support IPv6 lookup in bitcoin-cli even when IPv6 only bound on localhost (Wladimir J. van der Laan) deb3572 Add -rpcbind option to allow binding RPC port on a specific interface (Wladimir J. van der Laan)
2 parents fa41db8 + b5ad5e7 commit 29c1fbb

File tree

6 files changed

+396
-44
lines changed

6 files changed

+396
-44
lines changed

qa/rpc-tests/netutil.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Linux network utilities
2+
import sys
3+
import socket
4+
import fcntl
5+
import struct
6+
import array
7+
import os
8+
import binascii
9+
10+
# Roughly based on http://voorloopnul.com/blog/a-python-netstat-in-less-than-100-lines-of-code/ by Ricardo Pascal
11+
STATE_ESTABLISHED = '01'
12+
STATE_SYN_SENT = '02'
13+
STATE_SYN_RECV = '03'
14+
STATE_FIN_WAIT1 = '04'
15+
STATE_FIN_WAIT2 = '05'
16+
STATE_TIME_WAIT = '06'
17+
STATE_CLOSE = '07'
18+
STATE_CLOSE_WAIT = '08'
19+
STATE_LAST_ACK = '09'
20+
STATE_LISTEN = '0A'
21+
STATE_CLOSING = '0B'
22+
23+
def get_socket_inodes(pid):
24+
'''
25+
Get list of socket inodes for process pid.
26+
'''
27+
base = '/proc/%i/fd' % pid
28+
inodes = []
29+
for item in os.listdir(base):
30+
target = os.readlink(os.path.join(base, item))
31+
if target.startswith('socket:'):
32+
inodes.append(int(target[8:-1]))
33+
return inodes
34+
35+
def _remove_empty(array):
36+
return [x for x in array if x !='']
37+
38+
def _convert_ip_port(array):
39+
host,port = array.split(':')
40+
# convert host from mangled-per-four-bytes form as used by kernel
41+
host = binascii.unhexlify(host)
42+
host_out = ''
43+
for x in range(0, len(host)/4):
44+
(val,) = struct.unpack('=I', host[x*4:(x+1)*4])
45+
host_out += '%08x' % val
46+
47+
return host_out,int(port,16)
48+
49+
def netstat(typ='tcp'):
50+
'''
51+
Function to return a list with status of tcp connections at linux systems
52+
To get pid of all network process running on system, you must run this script
53+
as superuser
54+
'''
55+
with open('/proc/net/'+typ,'r') as f:
56+
content = f.readlines()
57+
content.pop(0)
58+
result = []
59+
for line in content:
60+
line_array = _remove_empty(line.split(' ')) # Split lines and remove empty spaces.
61+
tcp_id = line_array[0]
62+
l_addr = _convert_ip_port(line_array[1])
63+
r_addr = _convert_ip_port(line_array[2])
64+
state = line_array[3]
65+
inode = int(line_array[9]) # Need the inode to match with process pid.
66+
nline = [tcp_id, l_addr, r_addr, state, inode]
67+
result.append(nline)
68+
return result
69+
70+
def get_bind_addrs(pid):
71+
'''
72+
Get bind addresses as (host,port) tuples for process pid.
73+
'''
74+
inodes = get_socket_inodes(pid)
75+
bind_addrs = []
76+
for conn in netstat('tcp') + netstat('tcp6'):
77+
if conn[3] == STATE_LISTEN and conn[4] in inodes:
78+
bind_addrs.append(conn[1])
79+
return bind_addrs
80+
81+
# from: http://code.activestate.com/recipes/439093/
82+
def all_interfaces():
83+
'''
84+
Return all interfaces that are up
85+
'''
86+
is_64bits = sys.maxsize > 2**32
87+
struct_size = 40 if is_64bits else 32
88+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
89+
max_possible = 8 # initial value
90+
while True:
91+
bytes = max_possible * struct_size
92+
names = array.array('B', '\0' * bytes)
93+
outbytes = struct.unpack('iL', fcntl.ioctl(
94+
s.fileno(),
95+
0x8912, # SIOCGIFCONF
96+
struct.pack('iL', bytes, names.buffer_info()[0])
97+
))[0]
98+
if outbytes == bytes:
99+
max_possible *= 2
100+
else:
101+
break
102+
namestr = names.tostring()
103+
return [(namestr[i:i+16].split('\0', 1)[0],
104+
socket.inet_ntoa(namestr[i+20:i+24]))
105+
for i in range(0, outbytes, struct_size)]
106+
107+
def addr_to_hex(addr):
108+
'''
109+
Convert string IPv4 or IPv6 address to binary address as returned by
110+
get_bind_addrs.
111+
Very naive implementation that certainly doesn't work for all IPv6 variants.
112+
'''
113+
if '.' in addr: # IPv4
114+
addr = [int(x) for x in addr.split('.')]
115+
elif ':' in addr: # IPv6
116+
sub = [[], []] # prefix, suffix
117+
x = 0
118+
addr = addr.split(':')
119+
for i,comp in enumerate(addr):
120+
if comp == '':
121+
if i == 0 or i == (len(addr)-1): # skip empty component at beginning or end
122+
continue
123+
x += 1 # :: skips to suffix
124+
assert(x < 2)
125+
else: # two bytes per component
126+
val = int(comp, 16)
127+
sub[x].append(val >> 8)
128+
sub[x].append(val & 0xff)
129+
nullbytes = 16 - len(sub[0]) - len(sub[1])
130+
assert((x == 0 and nullbytes == 0) or (x == 1 and nullbytes > 0))
131+
addr = sub[0] + ([0] * nullbytes) + sub[1]
132+
else:
133+
raise ValueError('Could not parse address %s' % addr)
134+
return binascii.hexlify(bytearray(addr))

qa/rpc-tests/rpcbind_test.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) 2014 The Bitcoin Core developers
3+
# Distributed under the MIT/X11 software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
# Test for -rpcbind, as well as -rpcallowip and -rpcconnect
7+
8+
# Add python-bitcoinrpc to module search path:
9+
import os
10+
import sys
11+
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "python-bitcoinrpc"))
12+
13+
import json
14+
import shutil
15+
import subprocess
16+
import tempfile
17+
import traceback
18+
19+
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
20+
from util import *
21+
from netutil import *
22+
23+
def run_bind_test(tmpdir, allow_ips, connect_to, addresses, expected):
24+
'''
25+
Start a node with requested rpcallowip and rpcbind parameters,
26+
then try to connect, and check if the set of bound addresses
27+
matches the expected set.
28+
'''
29+
expected = [(addr_to_hex(addr), port) for (addr, port) in expected]
30+
base_args = ['-disablewallet', '-nolisten']
31+
if allow_ips:
32+
base_args += ['-rpcallowip=' + x for x in allow_ips]
33+
binds = ['-rpcbind='+addr for addr in addresses]
34+
nodes = start_nodes(1, tmpdir, [base_args + binds], connect_to)
35+
try:
36+
pid = bitcoind_processes[0].pid
37+
assert_equal(set(get_bind_addrs(pid)), set(expected))
38+
finally:
39+
stop_nodes(nodes)
40+
wait_bitcoinds()
41+
42+
def run_allowip_test(tmpdir, allow_ips, rpchost):
43+
'''
44+
Start a node with rpcwallow IP, and request getinfo
45+
at a non-localhost IP.
46+
'''
47+
base_args = ['-disablewallet', '-nolisten'] + ['-rpcallowip='+x for x in allow_ips]
48+
nodes = start_nodes(1, tmpdir, [base_args])
49+
try:
50+
# connect to node through non-loopback interface
51+
url = "http://rt:rt@%s:%d" % (rpchost, START_RPC_PORT,)
52+
node = AuthServiceProxy(url)
53+
node.getinfo()
54+
finally:
55+
node = None # make sure connection will be garbage collected and closed
56+
stop_nodes(nodes)
57+
wait_bitcoinds()
58+
59+
60+
def run_test(tmpdir):
61+
assert(sys.platform == 'linux2') # due to OS-specific network stats queries, this test works only on Linux
62+
# find the first non-loopback interface for testing
63+
non_loopback_ip = None
64+
for name,ip in all_interfaces():
65+
if ip != '127.0.0.1':
66+
non_loopback_ip = ip
67+
break
68+
if non_loopback_ip is None:
69+
assert(not 'This test requires at least one non-loopback IPv4 interface')
70+
print("Using interface %s for testing" % non_loopback_ip)
71+
72+
# check default without rpcallowip (IPv4 and IPv6 localhost)
73+
run_bind_test(tmpdir, None, '127.0.0.1', [],
74+
[('127.0.0.1', 11100), ('::1', 11100)])
75+
# check default with rpcallowip (IPv6 any)
76+
run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1', [],
77+
[('::0', 11100)])
78+
# check only IPv4 localhost (explicit)
79+
run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1', ['127.0.0.1'],
80+
[('127.0.0.1', START_RPC_PORT)])
81+
# check only IPv4 localhost (explicit) with alternative port
82+
run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1:32171', ['127.0.0.1:32171'],
83+
[('127.0.0.1', 32171)])
84+
# check only IPv4 localhost (explicit) with multiple alternative ports on same host
85+
run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1:32171', ['127.0.0.1:32171', '127.0.0.1:32172'],
86+
[('127.0.0.1', 32171), ('127.0.0.1', 32172)])
87+
# check only IPv6 localhost (explicit)
88+
run_bind_test(tmpdir, ['[::1]'], '[::1]', ['[::1]'],
89+
[('::1', 11100)])
90+
# check both IPv4 and IPv6 localhost (explicit)
91+
run_bind_test(tmpdir, ['127.0.0.1'], '127.0.0.1', ['127.0.0.1', '[::1]'],
92+
[('127.0.0.1', START_RPC_PORT), ('::1', START_RPC_PORT)])
93+
# check only non-loopback interface
94+
run_bind_test(tmpdir, [non_loopback_ip], non_loopback_ip, [non_loopback_ip],
95+
[(non_loopback_ip, START_RPC_PORT)])
96+
97+
# Check that with invalid rpcallowip, we are denied
98+
run_allowip_test(tmpdir, [non_loopback_ip], non_loopback_ip)
99+
try:
100+
run_allowip_test(tmpdir, ['1.1.1.1'], non_loopback_ip)
101+
assert(not 'Connection not denied by rpcallowip as expected')
102+
except ValueError:
103+
pass
104+
105+
def main():
106+
import optparse
107+
108+
parser = optparse.OptionParser(usage="%prog [options]")
109+
parser.add_option("--nocleanup", dest="nocleanup", default=False, action="store_true",
110+
help="Leave bitcoinds and test.* datadir on exit or error")
111+
parser.add_option("--srcdir", dest="srcdir", default="../../src",
112+
help="Source directory containing bitcoind/bitcoin-cli (default: %default%)")
113+
parser.add_option("--tmpdir", dest="tmpdir", default=tempfile.mkdtemp(prefix="test"),
114+
help="Root directory for datadirs")
115+
(options, args) = parser.parse_args()
116+
117+
os.environ['PATH'] = options.srcdir+":"+os.environ['PATH']
118+
119+
check_json_precision()
120+
121+
success = False
122+
nodes = []
123+
try:
124+
print("Initializing test directory "+options.tmpdir)
125+
if not os.path.isdir(options.tmpdir):
126+
os.makedirs(options.tmpdir)
127+
initialize_chain(options.tmpdir)
128+
129+
run_test(options.tmpdir)
130+
131+
success = True
132+
133+
except AssertionError as e:
134+
print("Assertion failed: "+e.message)
135+
except Exception as e:
136+
print("Unexpected exception caught during testing: "+str(e))
137+
traceback.print_tb(sys.exc_info()[2])
138+
139+
if not options.nocleanup:
140+
print("Cleaning up")
141+
wait_bitcoinds()
142+
shutil.rmtree(options.tmpdir)
143+
144+
if success:
145+
print("Tests successful")
146+
sys.exit(0)
147+
else:
148+
print("Failed")
149+
sys.exit(1)
150+
151+
if __name__ == '__main__':
152+
main()

qa/rpc-tests/util.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import shutil
1616
import subprocess
1717
import time
18+
import re
1819

1920
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
2021
from util import *
@@ -112,20 +113,43 @@ def initialize_chain(test_dir):
112113
to_dir = os.path.join(test_dir, "node"+str(i))
113114
shutil.copytree(from_dir, to_dir)
114115

115-
def start_nodes(num_nodes, dir):
116+
def _rpchost_to_args(rpchost):
117+
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
118+
if rpchost is None:
119+
return []
120+
121+
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
122+
if not match:
123+
raise ValueError('Invalid RPC host spec ' + rpchost)
124+
125+
rpcconnect = match.group(1)
126+
rpcport = match.group(2)
127+
128+
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
129+
rpcconnect = rpcconnect[1:-1]
130+
131+
rv = ['-rpcconnect=' + rpcconnect]
132+
if rpcport:
133+
rv += ['-rpcport=' + rpcport]
134+
return rv
135+
136+
def start_nodes(num_nodes, dir, extra_args=None, rpchost=None):
116137
# Start bitcoinds, and wait for RPC interface to be up and running:
117138
devnull = open("/dev/null", "w+")
118139
for i in range(num_nodes):
119140
datadir = os.path.join(dir, "node"+str(i))
120141
args = [ "bitcoind", "-datadir="+datadir ]
142+
if extra_args is not None:
143+
args += extra_args[i]
121144
bitcoind_processes.append(subprocess.Popen(args))
122-
subprocess.check_call([ "bitcoin-cli", "-datadir="+datadir,
123-
"-rpcwait", "getblockcount"], stdout=devnull)
145+
subprocess.check_call([ "bitcoin-cli", "-datadir="+datadir] +
146+
_rpchost_to_args(rpchost) +
147+
["-rpcwait", "getblockcount"], stdout=devnull)
124148
devnull.close()
125149
# Create&return JSON-RPC connections
126150
rpc_connections = []
127151
for i in range(num_nodes):
128-
url = "http://rt:[email protected]:%d"%(START_RPC_PORT+i,)
152+
url = "http://rt:rt@%s:%d" % (rpchost or '127.0.0.1', START_RPC_PORT+i,)
129153
rpc_connections.append(AuthServiceProxy(url))
130154
return rpc_connections
131155

src/init.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,11 @@ std::string HelpMessage(HelpMessageMode hmm)
304304

305305
strUsage += "\n" + _("RPC server options:") + "\n";
306306
strUsage += " -server " + _("Accept command line and JSON-RPC commands") + "\n";
307+
strUsage += " -rpcbind=<addr> " + _("Bind to given address to listen for JSON-RPC connections. Use [host]:port notation for IPv6. This option can be specified multiple times (default: bind to all interfaces)") + "\n";
307308
strUsage += " -rpcuser=<user> " + _("Username for JSON-RPC connections") + "\n";
308309
strUsage += " -rpcpassword=<pw> " + _("Password for JSON-RPC connections") + "\n";
309310
strUsage += " -rpcport=<port> " + _("Listen for JSON-RPC connections on <port> (default: 8332 or testnet: 18332)") + "\n";
310-
strUsage += " -rpcallowip=<ip> " + _("Allow JSON-RPC connections from specified IP address") + "\n";
311+
strUsage += " -rpcallowip=<ip> " + _("Allow JSON-RPC connections from specified IP address. This option can be specified multiple times") + "\n";
311312
strUsage += " -rpcthreads=<n> " + _("Set the number of threads to service RPC calls (default: 4)") + "\n";
312313

313314
strUsage += "\n" + _("RPC SSL options: (see the Bitcoin Wiki for SSL setup instructions)") + "\n";

src/rpcprotocol.h

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,27 @@ class SSLIOStreamDevice : public boost::iostreams::device<boost::iostreams::bidi
103103
}
104104
bool connect(const std::string& server, const std::string& port)
105105
{
106-
boost::asio::ip::tcp::resolver resolver(stream.get_io_service());
107-
boost::asio::ip::tcp::resolver::query query(server.c_str(), port.c_str());
108-
boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
109-
boost::asio::ip::tcp::resolver::iterator end;
106+
using namespace boost::asio::ip;
107+
tcp::resolver resolver(stream.get_io_service());
108+
tcp::resolver::iterator endpoint_iterator;
109+
#if BOOST_VERSION >= 104300
110+
try {
111+
#endif
112+
// The default query (flags address_configured) tries IPv6 if
113+
// non-localhost IPv6 configured, and IPv4 if non-localhost IPv4
114+
// configured.
115+
tcp::resolver::query query(server.c_str(), port.c_str());
116+
endpoint_iterator = resolver.resolve(query);
117+
#if BOOST_VERSION >= 104300
118+
} catch(boost::system::system_error &e)
119+
{
120+
// If we at first don't succeed, try blanket lookup (IPv4+IPv6 independent of configured interfaces)
121+
tcp::resolver::query query(server.c_str(), port.c_str(), resolver_query_base::flags());
122+
endpoint_iterator = resolver.resolve(query);
123+
}
124+
#endif
110125
boost::system::error_code error = boost::asio::error::host_not_found;
126+
tcp::resolver::iterator end;
111127
while (error && endpoint_iterator != end)
112128
{
113129
stream.lowest_layer().close();

0 commit comments

Comments
 (0)