Skip to content

Commit 6f55827

Browse files
laanwjPastaPastaPasta
authored andcommitted
Merge bitcoin#10143: [net] Allow disconnectnode RPC to be called with node id
d54297f [tests] disconnect_ban: add tests for disconnect-by-nodeid (John Newbery) 5cc3ee2 [tests] disconnect_ban: remove dependency on urllib (John Newbery) 12de2f2 [tests] disconnect_ban: use wait_until instead of sleep (John Newbery) 2077fda [tests] disconnect_ban: add logging (John Newbery) 395561b [tests] disconnectban test - only use two nodes (John Newbery) e367ad5 [tests] rename nodehandling to disconnectban (John Newbery) d6564a2 [tests] fix nodehandling.py flake8 warnings (John Newbery) 23e6e64 Allow disconnectnode() to be called with node id (John Newbery) Tree-SHA512: a371bb05a24a91cdb16a7ac4fbb091d5d3bf6554b29bd69d74522cb7523d3f1c5b1989d5e7b03f3fc7369fb727098dd2a549de551b731dd480c121d9517d3576
1 parent 4efa99c commit 6f55827

File tree

5 files changed

+138
-89
lines changed

5 files changed

+138
-89
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
158158
{ "getspecialtxes", 2, "count" },
159159
{ "getspecialtxes", 3, "skip" },
160160
{ "getspecialtxes", 4, "verbosity" },
161+
{ "disconnectnode", 1, "nodeid" },
161162
// Echo with conversion (For testing only)
162163
{ "echojson", 0, "arg0" },
163164
{ "echojson", 1, "arg1" },

src/rpc/net.cpp

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,23 +235,43 @@ UniValue addnode(const JSONRPCRequest& request)
235235

236236
UniValue disconnectnode(const JSONRPCRequest& request)
237237
{
238-
if (request.fHelp || request.params.size() != 1)
238+
if (request.fHelp || request.params.size() == 0 || request.params.size() >= 3)
239239
throw std::runtime_error(
240-
"disconnectnode \"address\" \n"
241-
"\nImmediately disconnects from the specified node.\n"
240+
"disconnectnode \"[address]\" [nodeid]\n"
241+
"\nImmediately disconnects from the specified peer node.\n"
242+
"\nStrictly one out of 'address' and 'nodeid' can be provided to identify the node.\n"
243+
"\nTo disconnect by nodeid, either set 'address' to the empty string, or call using the named 'nodeid' argument only.\n"
242244
"\nArguments:\n"
243-
"1. \"address\" (string, required) The IP address/port of the node\n"
245+
"1. \"address\" (string, optional) The IP address/port of the node\n"
246+
"2. \"nodeid\" (number, optional) The node ID (see getpeerinfo for node IDs)\n"
244247
"\nExamples:\n"
245248
+ HelpExampleCli("disconnectnode", "\"192.168.0.6:9999\"")
249+
+ HelpExampleCli("disconnectnode", "\"\" 1")
246250
+ HelpExampleRpc("disconnectnode", "\"192.168.0.6:9999\"")
251+
+ HelpExampleRpc("disconnectnode", "\"\", 1")
247252
);
248253

249254
if(!g_connman)
250255
throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled");
251256

252-
bool ret = g_connman->DisconnectNode(request.params[0].get_str());
253-
if (!ret)
257+
bool success;
258+
const UniValue &address_arg = request.params[0];
259+
const UniValue &id_arg = request.params.size() < 2 ? NullUniValue : request.params[1];
260+
261+
if (!address_arg.isNull() && id_arg.isNull()) {
262+
/* handle disconnect-by-address */
263+
success = g_connman->DisconnectNode(address_arg.get_str());
264+
} else if (!id_arg.isNull() && (address_arg.isNull() || (address_arg.isStr() && address_arg.get_str().empty()))) {
265+
/* handle disconnect-by-id */
266+
NodeId nodeid = (NodeId) id_arg.get_int64();
267+
success = g_connman->DisconnectNode(nodeid);
268+
} else {
269+
throw JSONRPCError(RPC_INVALID_PARAMS, "Only one of address and nodeid should be provided.");
270+
}
271+
272+
if (!success) {
254273
throw JSONRPCError(RPC_CLIENT_NODE_NOT_CONNECTED, "Node not found in connected nodes");
274+
}
255275

256276
return NullUniValue;
257277
}
@@ -608,7 +628,7 @@ static const CRPCCommand commands[] =
608628
{ "network", "ping", &ping, true, {} },
609629
{ "network", "getpeerinfo", &getpeerinfo, true, {} },
610630
{ "network", "addnode", &addnode, true, {"node","command"} },
611-
{ "network", "disconnectnode", &disconnectnode, true, {"address"} },
631+
{ "network", "disconnectnode", &disconnectnode, true, {"address", "nodeid"} },
612632
{ "network", "getaddednodeinfo", &getaddednodeinfo, true, {"node"} },
613633
{ "network", "getnettotals", &getnettotals, true, {} },
614634
{ "network", "getnetworkinfo", &getnetworkinfo, true, {} },

test/functional/disconnect_ban.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2014-2016 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test node disconnect and ban behavior"""
6+
7+
from test_framework.mininode import wait_until
8+
from test_framework.test_framework import BitcoinTestFramework
9+
from test_framework.util import (assert_equal,
10+
assert_raises_jsonrpc,
11+
connect_nodes_bi,
12+
start_node,
13+
stop_node,
14+
)
15+
16+
class DisconnectBanTest(BitcoinTestFramework):
17+
18+
def __init__(self):
19+
super().__init__()
20+
self.num_nodes = 2
21+
self.setup_clean_chain = False
22+
23+
def setup_network(self):
24+
self.nodes = self.setup_nodes()
25+
connect_nodes_bi(self.nodes, 0, 1)
26+
27+
def run_test(self):
28+
self.log.info("Test setban and listbanned RPCs")
29+
30+
self.log.info("setban: successfully ban single IP address")
31+
assert_equal(len(self.nodes[1].getpeerinfo()), 2) # node1 should have 2 connections to node0 at this point
32+
self.nodes[1].setban("127.0.0.1", "add")
33+
wait_until(lambda: len(self.nodes[1].getpeerinfo()) == 0)
34+
assert_equal(len(self.nodes[1].getpeerinfo()), 0) # all nodes must be disconnected at this point
35+
assert_equal(len(self.nodes[1].listbanned()), 1)
36+
37+
self.log.info("clearbanned: successfully clear ban list")
38+
self.nodes[1].clearbanned()
39+
assert_equal(len(self.nodes[1].listbanned()), 0)
40+
self.nodes[1].setban("127.0.0.0/24", "add")
41+
42+
self.log.info("setban: fail to ban an already banned subnet")
43+
assert_equal(len(self.nodes[1].listbanned()), 1)
44+
assert_raises_jsonrpc(-23, "IP/Subnet already banned", self.nodes[1].setban, "127.0.0.1", "add")
45+
46+
self.log.info("setban: fail to ban an invalid subnet")
47+
assert_raises_jsonrpc(-30, "Error: Invalid IP/Subnet", self.nodes[1].setban, "127.0.0.1/42", "add")
48+
assert_equal(len(self.nodes[1].listbanned()), 1) # still only one banned ip because 127.0.0.1 is within the range of 127.0.0.0/24
49+
50+
self.log.info("setban remove: fail to unban a non-banned subnet")
51+
assert_raises_jsonrpc(-30, "Error: Unban failed", self.nodes[1].setban, "127.0.0.1", "remove")
52+
assert_equal(len(self.nodes[1].listbanned()), 1)
53+
54+
self.log.info("setban remove: successfully unban subnet")
55+
self.nodes[1].setban("127.0.0.0/24", "remove")
56+
assert_equal(len(self.nodes[1].listbanned()), 0)
57+
self.nodes[1].clearbanned()
58+
assert_equal(len(self.nodes[1].listbanned()), 0)
59+
60+
self.log.info("setban: test persistence across node restart")
61+
self.nodes[1].setban("127.0.0.0/32", "add")
62+
self.nodes[1].setban("127.0.0.0/24", "add")
63+
self.nodes[1].setban("192.168.0.1", "add", 1) # ban for 1 seconds
64+
self.nodes[1].setban("2001:4d48:ac57:400:cacf:e9ff:fe1d:9c63/19", "add", 1000) # ban for 1000 seconds
65+
listBeforeShutdown = self.nodes[1].listbanned()
66+
assert_equal("192.168.0.1/32", listBeforeShutdown[2]['address'])
67+
wait_until(lambda: len(self.nodes[1].listbanned()) == 3)
68+
69+
stop_node(self.nodes[1], 1)
70+
71+
self.nodes[1] = start_node(1, self.options.tmpdir)
72+
listAfterShutdown = self.nodes[1].listbanned()
73+
assert_equal("127.0.0.0/24", listAfterShutdown[0]['address'])
74+
assert_equal("127.0.0.0/32", listAfterShutdown[1]['address'])
75+
assert_equal("/19" in listAfterShutdown[2]['address'], True)
76+
77+
# Clear ban lists
78+
self.nodes[1].clearbanned()
79+
connect_nodes_bi(self.nodes, 0, 1)
80+
81+
self.log.info("Test disconnectrnode RPCs")
82+
83+
self.log.info("disconnectnode: fail to disconnect when calling with address and nodeid")
84+
address1 = self.nodes[0].getpeerinfo()[0]['addr']
85+
node1 = self.nodes[0].getpeerinfo()[0]['addr']
86+
assert_raises_jsonrpc(-32602, "Only one of address and nodeid should be provided.", self.nodes[0].disconnectnode, address=address1, nodeid=node1)
87+
88+
self.log.info("disconnectnode: fail to disconnect when calling with junk address")
89+
assert_raises_jsonrpc(-29, "Node not found in connected nodes", self.nodes[0].disconnectnode, address="221B Baker Street")
90+
91+
self.log.info("disconnectnode: successfully disconnect node by address")
92+
address1 = self.nodes[0].getpeerinfo()[0]['addr']
93+
self.nodes[0].disconnectnode(address=address1)
94+
wait_until(lambda: len(self.nodes[0].getpeerinfo()) == 1)
95+
assert not [node for node in self.nodes[0].getpeerinfo() if node['addr'] == address1]
96+
97+
self.log.info("disconnectnode: successfully reconnect node")
98+
connect_nodes_bi(self.nodes, 0, 1) # reconnect the node
99+
assert_equal(len(self.nodes[0].getpeerinfo()), 2)
100+
assert [node for node in self.nodes[0].getpeerinfo() if node['addr'] == address1]
101+
102+
self.log.info("disconnectnode: successfully disconnect node by node id")
103+
id1 = self.nodes[0].getpeerinfo()[0]['id']
104+
self.nodes[0].disconnectnode(nodeid=id1)
105+
wait_until(lambda: len(self.nodes[0].getpeerinfo()) == 1)
106+
assert not [node for node in self.nodes[0].getpeerinfo() if node['id'] == id1]
107+
108+
if __name__ == '__main__':
109+
DisconnectBanTest().main()

test/functional/nodehandling.py

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +0,0 @@
1-
#!/usr/bin/env python3
2-
# Copyright (c) 2014-2016 The Bitcoin Core developers
3-
# Distributed under the MIT software license, see the accompanying
4-
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5-
"""Test node handling."""
6-
7-
from test_framework.test_framework import BitcoinTestFramework
8-
from test_framework.util import *
9-
10-
import urllib.parse
11-
12-
class NodeHandlingTest (BitcoinTestFramework):
13-
14-
def __init__(self):
15-
super().__init__()
16-
self.num_nodes = 4
17-
self.setup_clean_chain = False
18-
19-
def run_test(self):
20-
###########################
21-
# setban/listbanned tests #
22-
###########################
23-
assert_equal(len(self.nodes[2].getpeerinfo()), 4) #we should have 4 nodes at this point
24-
self.nodes[2].setban("127.0.0.1", "add")
25-
time.sleep(3) #wait till the nodes are disconected
26-
assert_equal(len(self.nodes[2].getpeerinfo()), 0) #all nodes must be disconnected at this point
27-
assert_equal(len(self.nodes[2].listbanned()), 1)
28-
self.nodes[2].clearbanned()
29-
assert_equal(len(self.nodes[2].listbanned()), 0)
30-
self.nodes[2].setban("127.0.0.0/24", "add")
31-
assert_equal(len(self.nodes[2].listbanned()), 1)
32-
# This will throw an exception because 127.0.0.1 is within range 127.0.0.0/24
33-
assert_raises_jsonrpc(-23, "IP/Subnet already banned", self.nodes[2].setban, "127.0.0.1", "add")
34-
# This will throw an exception because 127.0.0.1/42 is not a real subnet
35-
assert_raises_jsonrpc(-30, "Error: Invalid IP/Subnet", self.nodes[2].setban, "127.0.0.1/42", "add")
36-
assert_equal(len(self.nodes[2].listbanned()), 1) #still only one banned ip because 127.0.0.1 is within the range of 127.0.0.0/24
37-
# This will throw an exception because 127.0.0.1 was not added above
38-
assert_raises_jsonrpc(-30, "Error: Unban failed", self.nodes[2].setban, "127.0.0.1", "remove")
39-
assert_equal(len(self.nodes[2].listbanned()), 1)
40-
self.nodes[2].setban("127.0.0.0/24", "remove")
41-
assert_equal(len(self.nodes[2].listbanned()), 0)
42-
self.nodes[2].clearbanned()
43-
assert_equal(len(self.nodes[2].listbanned()), 0)
44-
45-
##test persisted banlist
46-
self.nodes[2].setban("127.0.0.0/32", "add")
47-
self.nodes[2].setban("127.0.0.0/24", "add")
48-
self.nodes[2].setban("192.168.0.1", "add", 1) #ban for 1 seconds
49-
self.nodes[2].setban("2001:4d48:ac57:400:cacf:e9ff:fe1d:9c63/19", "add", 1000) #ban for 1000 seconds
50-
listBeforeShutdown = self.nodes[2].listbanned()
51-
assert_equal("192.168.0.1/32", listBeforeShutdown[2]['address']) #must be here
52-
set_mocktime(get_mocktime() + 2) #make 100% sure we expired 192.168.0.1 node time
53-
set_node_times(self.nodes, get_mocktime()) #make 100% sure we expired 192.168.0.1 node time
54-
55-
#stop node
56-
stop_node(self.nodes[2], 2)
57-
58-
self.nodes[2] = start_node(2, self.options.tmpdir)
59-
listAfterShutdown = self.nodes[2].listbanned()
60-
assert_equal("127.0.0.0/24", listAfterShutdown[0]['address'])
61-
assert_equal("127.0.0.0/32", listAfterShutdown[1]['address'])
62-
assert_equal("/19" in listAfterShutdown[2]['address'], True)
63-
64-
###########################
65-
# RPC disconnectnode test #
66-
###########################
67-
url = urllib.parse.urlparse(self.nodes[1].url)
68-
self.nodes[0].disconnectnode(url.hostname+":"+str(p2p_port(1)))
69-
time.sleep(2) #disconnecting a node needs a little bit of time
70-
for node in self.nodes[0].getpeerinfo():
71-
assert(node['addr'] != url.hostname+":"+str(p2p_port(1)))
72-
73-
connect_nodes_bi(self.nodes,0,1) #reconnect the node
74-
found = False
75-
for node in self.nodes[0].getpeerinfo():
76-
if node['addr'] == url.hostname+":"+str(p2p_port(1)):
77-
found = True
78-
assert(found)
79-
80-
if __name__ == '__main__':
81-
NodeHandlingTest ().main ()

test/functional/test_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
'multi_rpc.py',
7676
'proxy_test.py',
7777
'signrawtransactions.py',
78-
'nodehandling.py',
78+
'disconnect_ban.py',
7979
'addressindex.py',
8080
'timestampindex.py',
8181
'spentindex.py',

0 commit comments

Comments
 (0)