feature_proxy.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2015-2022 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 bitcoind with different proxy configuration. 6 7 Test plan: 8 - Start bitcoind's with different proxy configurations 9 - Use addnode to initiate connections 10 - Verify that proxies are connected to, and the right connection command is given 11 - Proxy configurations to test on bitcoind side: 12 - `-proxy` (proxy everything) 13 - `-onion` (proxy just onions) 14 - `-proxyrandomize` Circuit randomization 15 - `-cjdnsreachable` 16 - Proxy configurations to test on proxy side, 17 - support no authentication (other proxy) 18 - support no authentication + user/pass authentication (Tor) 19 - proxy on IPv6 20 - proxy over unix domain sockets 21 22 - Create various proxies (as threads) 23 - Create nodes that connect to them 24 - Manipulate the peer connections using addnode (onetry) and observe effects 25 - Test the getpeerinfo `network` field for the peer 26 27 addnode connect to IPv4 28 addnode connect to IPv6 29 addnode connect to onion 30 addnode connect to generic DNS name 31 addnode connect to a CJDNS address 32 33 - Test getnetworkinfo for each node 34 35 - Test passing invalid -proxy 36 - Test passing invalid -onion 37 - Test passing invalid -i2psam 38 - Test passing -onlynet=onion without -proxy or -onion 39 - Test passing -onlynet=onion with -onion=0 and with -noonion 40 - Test passing unknown -onlynet 41 """ 42 43 import os 44 import socket 45 import tempfile 46 47 from test_framework.socks5 import Socks5Configuration, Socks5Command, Socks5Server, AddressType 48 from test_framework.test_framework import BitcoinTestFramework 49 from test_framework.util import ( 50 assert_equal, 51 p2p_port, 52 ) 53 from test_framework.netutil import test_ipv6_local, test_unix_socket 54 55 # Networks returned by RPC getpeerinfo. 56 NET_UNROUTABLE = "not_publicly_routable" 57 NET_IPV4 = "ipv4" 58 NET_IPV6 = "ipv6" 59 NET_ONION = "onion" 60 NET_I2P = "i2p" 61 NET_CJDNS = "cjdns" 62 63 # Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() 64 NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS}) 65 66 # Use the shortest temp path possible since UNIX sockets may have as little as 92-char limit 67 socket_path = tempfile.NamedTemporaryFile().name 68 69 class ProxyTest(BitcoinTestFramework): 70 def set_test_params(self): 71 self.num_nodes = 7 72 self.setup_clean_chain = True 73 74 def setup_nodes(self): 75 self.have_ipv6 = test_ipv6_local() 76 self.have_unix_sockets = test_unix_socket() 77 # Create two proxies on different ports 78 # ... one unauthenticated 79 self.conf1 = Socks5Configuration() 80 self.conf1.addr = ('127.0.0.1', p2p_port(self.num_nodes)) 81 self.conf1.unauth = True 82 self.conf1.auth = False 83 # ... one supporting authenticated and unauthenticated (Tor) 84 self.conf2 = Socks5Configuration() 85 self.conf2.addr = ('127.0.0.1', p2p_port(self.num_nodes + 1)) 86 self.conf2.unauth = True 87 self.conf2.auth = True 88 if self.have_ipv6: 89 # ... one on IPv6 with similar configuration 90 self.conf3 = Socks5Configuration() 91 self.conf3.af = socket.AF_INET6 92 self.conf3.addr = ('::1', p2p_port(self.num_nodes + 2)) 93 self.conf3.unauth = True 94 self.conf3.auth = True 95 else: 96 self.log.warning("Testing without local IPv6 support") 97 98 if self.have_unix_sockets: 99 self.conf4 = Socks5Configuration() 100 self.conf4.af = socket.AF_UNIX 101 self.conf4.addr = socket_path 102 self.conf4.unauth = True 103 self.conf4.auth = True 104 else: 105 self.log.warning("Testing without local unix domain sockets support") 106 107 self.serv1 = Socks5Server(self.conf1) 108 self.serv1.start() 109 self.serv2 = Socks5Server(self.conf2) 110 self.serv2.start() 111 if self.have_ipv6: 112 self.serv3 = Socks5Server(self.conf3) 113 self.serv3.start() 114 if self.have_unix_sockets: 115 self.serv4 = Socks5Server(self.conf4) 116 self.serv4.start() 117 118 # We will not try to connect to this. 119 self.i2p_sam = ('127.0.0.1', 7656) 120 121 # Note: proxies are not used to connect to local nodes. This is because the proxy to 122 # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost. 123 args = [ 124 ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1'], 125 ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}',f'-onion={self.conf2.addr[0]}:{self.conf2.addr[1]}', 126 f'-i2psam={self.i2p_sam[0]}:{self.i2p_sam[1]}', '-i2pacceptincoming=0', '-proxyrandomize=0'], 127 ['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'], 128 [], 129 ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1', 130 '-cjdnsreachable'], 131 [], 132 [] 133 ] 134 if self.have_ipv6: 135 args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion'] 136 if self.have_unix_sockets: 137 args[5] = ['-listen', f'-proxy=unix:{socket_path}'] 138 args[6] = ['-listen', f'-onion=unix:{socket_path}'] 139 self.add_nodes(self.num_nodes, extra_args=args) 140 self.start_nodes() 141 142 def network_test(self, node, addr, network): 143 for peer in node.getpeerinfo(): 144 if peer["addr"] == addr: 145 assert_equal(peer["network"], network) 146 147 def node_test(self, node, *, proxies, auth, test_onion, test_cjdns): 148 rv = [] 149 addr = "15.61.23.23:1234" 150 self.log.debug(f"Test: outgoing IPv4 connection through node {node.index} for address {addr}") 151 # v2transport=False is used to avoid reconnections with v1 being scheduled. These could interfere with later checks. 152 node.addnode(addr, "onetry", v2transport=False) 153 cmd = proxies[0].queue.get() 154 assert isinstance(cmd, Socks5Command) 155 # Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6 156 assert_equal(cmd.atyp, AddressType.DOMAINNAME) 157 assert_equal(cmd.addr, b"15.61.23.23") 158 assert_equal(cmd.port, 1234) 159 if not auth: 160 assert_equal(cmd.username, None) 161 assert_equal(cmd.password, None) 162 rv.append(cmd) 163 self.network_test(node, addr, network=NET_IPV4) 164 165 if self.have_ipv6: 166 addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443" 167 self.log.debug(f"Test: outgoing IPv6 connection through node {node.index} for address {addr}") 168 node.addnode(addr, "onetry", v2transport=False) 169 cmd = proxies[1].queue.get() 170 assert isinstance(cmd, Socks5Command) 171 # Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6 172 assert_equal(cmd.atyp, AddressType.DOMAINNAME) 173 assert_equal(cmd.addr, b"1233:3432:2434:2343:3234:2345:6546:4534") 174 assert_equal(cmd.port, 5443) 175 if not auth: 176 assert_equal(cmd.username, None) 177 assert_equal(cmd.password, None) 178 rv.append(cmd) 179 self.network_test(node, addr, network=NET_IPV6) 180 181 if test_onion: 182 addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333" 183 self.log.debug(f"Test: outgoing onion connection through node {node.index} for address {addr}") 184 node.addnode(addr, "onetry", v2transport=False) 185 cmd = proxies[2].queue.get() 186 assert isinstance(cmd, Socks5Command) 187 assert_equal(cmd.atyp, AddressType.DOMAINNAME) 188 assert_equal(cmd.addr, b"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion") 189 assert_equal(cmd.port, 8333) 190 if not auth: 191 assert_equal(cmd.username, None) 192 assert_equal(cmd.password, None) 193 rv.append(cmd) 194 self.network_test(node, addr, network=NET_ONION) 195 196 if test_cjdns: 197 addr = "[fc00:1:2:3:4:5:6:7]:8888" 198 self.log.debug(f"Test: outgoing CJDNS connection through node {node.index} for address {addr}") 199 node.addnode(addr, "onetry", v2transport=False) 200 cmd = proxies[1].queue.get() 201 assert isinstance(cmd, Socks5Command) 202 assert_equal(cmd.atyp, AddressType.DOMAINNAME) 203 assert_equal(cmd.addr, b"fc00:1:2:3:4:5:6:7") 204 assert_equal(cmd.port, 8888) 205 if not auth: 206 assert_equal(cmd.username, None) 207 assert_equal(cmd.password, None) 208 rv.append(cmd) 209 self.network_test(node, addr, network=NET_CJDNS) 210 211 addr = "node.noumenon:8333" 212 self.log.debug(f"Test: outgoing DNS name connection through node {node.index} for address {addr}") 213 node.addnode(addr, "onetry", v2transport=False) 214 cmd = proxies[3].queue.get() 215 assert isinstance(cmd, Socks5Command) 216 assert_equal(cmd.atyp, AddressType.DOMAINNAME) 217 assert_equal(cmd.addr, b"node.noumenon") 218 assert_equal(cmd.port, 8333) 219 if not auth: 220 assert_equal(cmd.username, None) 221 assert_equal(cmd.password, None) 222 rv.append(cmd) 223 self.network_test(node, addr, network=NET_UNROUTABLE) 224 225 return rv 226 227 def run_test(self): 228 # basic -proxy 229 self.node_test(self.nodes[0], 230 proxies=[self.serv1, self.serv1, self.serv1, self.serv1], 231 auth=False, test_onion=True, test_cjdns=False) 232 233 # -proxy plus -onion 234 self.node_test(self.nodes[1], 235 proxies=[self.serv1, self.serv1, self.serv2, self.serv1], 236 auth=False, test_onion=True, test_cjdns=False) 237 238 # -proxy plus -onion, -proxyrandomize 239 rv = self.node_test(self.nodes[2], 240 proxies=[self.serv2, self.serv2, self.serv2, self.serv2], 241 auth=True, test_onion=True, test_cjdns=False) 242 # Check that credentials as used for -proxyrandomize connections are unique 243 credentials = set((x.username,x.password) for x in rv) 244 assert_equal(len(credentials), len(rv)) 245 246 if self.have_ipv6: 247 # proxy on IPv6 localhost 248 self.node_test(self.nodes[3], 249 proxies=[self.serv3, self.serv3, self.serv3, self.serv3], 250 auth=False, test_onion=False, test_cjdns=False) 251 252 # -proxy=unauth -proxyrandomize=1 -cjdnsreachable 253 self.node_test(self.nodes[4], 254 proxies=[self.serv1, self.serv1, self.serv1, self.serv1], 255 auth=False, test_onion=True, test_cjdns=True) 256 257 if self.have_unix_sockets: 258 self.node_test(self.nodes[5], 259 proxies=[self.serv4, self.serv4, self.serv4, self.serv4], 260 auth=True, test_onion=True, test_cjdns=False) 261 262 263 def networks_dict(d): 264 r = {} 265 for x in d['networks']: 266 r[x['name']] = x 267 return r 268 269 self.log.info("Test RPC getnetworkinfo") 270 nodes_network_info = [] 271 272 self.log.debug("Test that setting -proxy disables local address discovery, i.e. -discover=0") 273 for node in self.nodes: 274 network_info = node.getnetworkinfo() 275 assert_equal(network_info["localaddresses"], []) 276 nodes_network_info.append(network_info) 277 278 n0 = networks_dict(nodes_network_info[0]) 279 assert_equal(NETWORKS, n0.keys()) 280 for net in NETWORKS: 281 if net == NET_I2P: 282 expected_proxy = '' 283 expected_randomize = False 284 else: 285 expected_proxy = '%s:%i' % (self.conf1.addr) 286 expected_randomize = True 287 assert_equal(n0[net]['proxy'], expected_proxy) 288 assert_equal(n0[net]['proxy_randomize_credentials'], expected_randomize) 289 assert_equal(n0['onion']['reachable'], True) 290 assert_equal(n0['i2p']['reachable'], False) 291 assert_equal(n0['cjdns']['reachable'], False) 292 293 n1 = networks_dict(nodes_network_info[1]) 294 assert_equal(NETWORKS, n1.keys()) 295 for net in ['ipv4', 'ipv6']: 296 assert_equal(n1[net]['proxy'], f'{self.conf1.addr[0]}:{self.conf1.addr[1]}') 297 assert_equal(n1[net]['proxy_randomize_credentials'], False) 298 assert_equal(n1['onion']['proxy'], f'{self.conf2.addr[0]}:{self.conf2.addr[1]}') 299 assert_equal(n1['onion']['proxy_randomize_credentials'], False) 300 assert_equal(n1['onion']['reachable'], True) 301 assert_equal(n1['i2p']['proxy'], f'{self.i2p_sam[0]}:{self.i2p_sam[1]}') 302 assert_equal(n1['i2p']['proxy_randomize_credentials'], False) 303 assert_equal(n1['i2p']['reachable'], True) 304 305 n2 = networks_dict(nodes_network_info[2]) 306 assert_equal(NETWORKS, n2.keys()) 307 proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}' 308 for net in NETWORKS: 309 if net == NET_I2P: 310 expected_proxy = '' 311 expected_randomize = False 312 else: 313 expected_proxy = proxy 314 expected_randomize = True 315 assert_equal(n2[net]['proxy'], expected_proxy) 316 assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) 317 assert_equal(n2['onion']['reachable'], True) 318 assert_equal(n2['i2p']['reachable'], False) 319 assert_equal(n2['cjdns']['reachable'], False) 320 321 if self.have_ipv6: 322 n3 = networks_dict(nodes_network_info[3]) 323 assert_equal(NETWORKS, n3.keys()) 324 proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}' 325 for net in NETWORKS: 326 expected_proxy = '' if net == NET_I2P or net == NET_ONION else proxy 327 assert_equal(n3[net]['proxy'], expected_proxy) 328 assert_equal(n3[net]['proxy_randomize_credentials'], False) 329 assert_equal(n3['onion']['reachable'], False) 330 assert_equal(n3['i2p']['reachable'], False) 331 assert_equal(n3['cjdns']['reachable'], False) 332 333 n4 = networks_dict(nodes_network_info[4]) 334 assert_equal(NETWORKS, n4.keys()) 335 for net in NETWORKS: 336 if net == NET_I2P: 337 expected_proxy = '' 338 expected_randomize = False 339 else: 340 expected_proxy = '%s:%i' % (self.conf1.addr) 341 expected_randomize = True 342 assert_equal(n4[net]['proxy'], expected_proxy) 343 assert_equal(n4[net]['proxy_randomize_credentials'], expected_randomize) 344 assert_equal(n4['onion']['reachable'], True) 345 assert_equal(n4['i2p']['reachable'], False) 346 assert_equal(n4['cjdns']['reachable'], True) 347 348 if self.have_unix_sockets: 349 n5 = networks_dict(nodes_network_info[5]) 350 assert_equal(NETWORKS, n5.keys()) 351 for net in NETWORKS: 352 if net == NET_I2P: 353 expected_proxy = '' 354 expected_randomize = False 355 else: 356 expected_proxy = 'unix:' + self.conf4.addr # no port number 357 expected_randomize = True 358 assert_equal(n5[net]['proxy'], expected_proxy) 359 assert_equal(n5[net]['proxy_randomize_credentials'], expected_randomize) 360 assert_equal(n5['onion']['reachable'], True) 361 assert_equal(n5['i2p']['reachable'], False) 362 assert_equal(n5['cjdns']['reachable'], False) 363 364 n6 = networks_dict(nodes_network_info[6]) 365 assert_equal(NETWORKS, n6.keys()) 366 for net in NETWORKS: 367 if net != NET_ONION: 368 expected_proxy = '' 369 expected_randomize = False 370 else: 371 expected_proxy = 'unix:' + self.conf4.addr # no port number 372 expected_randomize = True 373 assert_equal(n6[net]['proxy'], expected_proxy) 374 assert_equal(n6[net]['proxy_randomize_credentials'], expected_randomize) 375 assert_equal(n6['onion']['reachable'], True) 376 assert_equal(n6['i2p']['reachable'], False) 377 assert_equal(n6['cjdns']['reachable'], False) 378 379 self.stop_node(1) 380 381 self.log.info("Test passing invalid -proxy hostname raises expected init error") 382 self.nodes[1].extra_args = ["-proxy=abc..abc:23456"] 383 msg = "Error: Invalid -proxy address or hostname: 'abc..abc:23456'" 384 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 385 386 self.log.info("Test passing invalid -proxy port raises expected init error") 387 self.nodes[1].extra_args = ["-proxy=192.0.0.1:def"] 388 msg = "Error: Invalid port specified in -proxy: '192.0.0.1:def'" 389 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 390 391 self.log.info("Test passing invalid -onion hostname raises expected init error") 392 self.nodes[1].extra_args = ["-onion=xyz..xyz:23456"] 393 msg = "Error: Invalid -onion address or hostname: 'xyz..xyz:23456'" 394 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 395 396 self.log.info("Test passing invalid -onion port raises expected init error") 397 self.nodes[1].extra_args = ["-onion=192.0.0.1:def"] 398 msg = "Error: Invalid port specified in -onion: '192.0.0.1:def'" 399 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 400 401 self.log.info("Test passing invalid -i2psam hostname raises expected init error") 402 self.nodes[1].extra_args = ["-i2psam=def..def:23456"] 403 msg = "Error: Invalid -i2psam address or hostname: 'def..def:23456'" 404 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 405 406 self.log.info("Test passing invalid -i2psam port raises expected init error") 407 self.nodes[1].extra_args = ["-i2psam=192.0.0.1:def"] 408 msg = "Error: Invalid port specified in -i2psam: '192.0.0.1:def'" 409 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 410 411 self.log.info("Test passing invalid -onlynet=i2p without -i2psam raises expected init error") 412 self.nodes[1].extra_args = ["-onlynet=i2p"] 413 msg = "Error: Outbound connections restricted to i2p (-onlynet=i2p) but -i2psam is not provided" 414 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 415 416 self.log.info("Test passing invalid -onlynet=cjdns without -cjdnsreachable raises expected init error") 417 self.nodes[1].extra_args = ["-onlynet=cjdns"] 418 msg = "Error: Outbound connections restricted to CJDNS (-onlynet=cjdns) but -cjdnsreachable is not provided" 419 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 420 421 self.log.info("Test passing -onlynet=onion with -onion=0/-noonion raises expected init error") 422 msg = ( 423 "Error: Outbound connections restricted to Tor (-onlynet=onion) but " 424 "the proxy for reaching the Tor network is explicitly forbidden: -onion=0" 425 ) 426 for arg in ["-onion=0", "-noonion"]: 427 self.nodes[1].extra_args = ["-onlynet=onion", arg] 428 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 429 430 self.log.info("Test passing -onlynet=onion without -proxy, -onion or -listenonion raises expected init error") 431 self.nodes[1].extra_args = ["-onlynet=onion", "-listenonion=0"] 432 msg = ( 433 "Error: Outbound connections restricted to Tor (-onlynet=onion) but the proxy for " 434 "reaching the Tor network is not provided: none of -proxy, -onion or -listenonion is given" 435 ) 436 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 437 438 self.log.info("Test passing -onlynet=onion without -proxy or -onion but with -listenonion=1 is ok") 439 self.start_node(1, extra_args=["-onlynet=onion", "-listenonion=1"]) 440 self.stop_node(1) 441 442 self.log.info("Test passing unknown network to -onlynet raises expected init error") 443 self.nodes[1].extra_args = ["-onlynet=abc"] 444 msg = "Error: Unknown network specified in -onlynet: 'abc'" 445 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 446 447 self.log.info("Test passing too-long unix path to -proxy raises init error") 448 self.nodes[1].extra_args = [f"-proxy=unix:{'x' * 1000}"] 449 if self.have_unix_sockets: 450 msg = f"Error: Invalid -proxy address or hostname: 'unix:{'x' * 1000}'" 451 else: 452 # If unix sockets are not supported, the file path is incorrectly interpreted as host:port 453 msg = f"Error: Invalid port specified in -proxy: 'unix:{'x' * 1000}'" 454 self.nodes[1].assert_start_raises_init_error(expected_msg=msg) 455 456 # Cleanup socket path we established outside the individual test directory. 457 if self.have_unix_sockets: 458 os.unlink(socket_path) 459 460 if __name__ == '__main__': 461 ProxyTest(__file__).main()