bitcoin/test/functional/feature_proxy.py

461 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2015-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test bitcoind with different proxy configuration.
Test plan:
- Start bitcoind's with different proxy configurations
- Use addnode to initiate connections
- Verify that proxies are connected to, and the right connection command is given
- Proxy configurations to test on bitcoind side:
- `-proxy` (proxy everything)
- `-onion` (proxy just onions)
- `-proxyrandomize` Circuit randomization
- `-cjdnsreachable`
- Proxy configurations to test on proxy side,
- support no authentication (other proxy)
- support no authentication + user/pass authentication (Tor)
- proxy on IPv6
- proxy over unix domain sockets
- Create various proxies (as threads)
- Create nodes that connect to them
- Manipulate the peer connections using addnode (onetry) and observe effects
- Test the getpeerinfo `network` field for the peer
addnode connect to IPv4
addnode connect to IPv6
addnode connect to onion
addnode connect to generic DNS name
addnode connect to a CJDNS address
- Test getnetworkinfo for each node
- Test passing invalid -proxy
- Test passing invalid -onion
- Test passing invalid -i2psam
- Test passing -onlynet=onion without -proxy or -onion
- Test passing -onlynet=onion with -onion=0 and with -noonion
- Test passing unknown -onlynet
"""
import os
import socket
import tempfile
from test_framework.socks5 import Socks5Configuration, Socks5Command, Socks5Server, AddressType
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
p2p_port,
)
from test_framework.netutil import test_ipv6_local, test_unix_socket
# Networks returned by RPC getpeerinfo.
NET_UNROUTABLE = "not_publicly_routable"
NET_IPV4 = "ipv4"
NET_IPV6 = "ipv6"
NET_ONION = "onion"
NET_I2P = "i2p"
NET_CJDNS = "cjdns"
# Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo()
NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS})
# Use the shortest temp path possible since UNIX sockets may have as little as 92-char limit
socket_path = tempfile.NamedTemporaryFile().name
class ProxyTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 7
self.setup_clean_chain = True
def setup_nodes(self):
self.have_ipv6 = test_ipv6_local()
self.have_unix_sockets = test_unix_socket()
# Create two proxies on different ports
# ... one unauthenticated
self.conf1 = Socks5Configuration()
self.conf1.addr = ('127.0.0.1', p2p_port(self.num_nodes))
self.conf1.unauth = True
self.conf1.auth = False
# ... one supporting authenticated and unauthenticated (Tor)
self.conf2 = Socks5Configuration()
self.conf2.addr = ('127.0.0.1', p2p_port(self.num_nodes + 1))
self.conf2.unauth = True
self.conf2.auth = True
if self.have_ipv6:
# ... one on IPv6 with similar configuration
self.conf3 = Socks5Configuration()
self.conf3.af = socket.AF_INET6
self.conf3.addr = ('::1', p2p_port(self.num_nodes + 2))
self.conf3.unauth = True
self.conf3.auth = True
else:
self.log.warning("Testing without local IPv6 support")
if self.have_unix_sockets:
self.conf4 = Socks5Configuration()
self.conf4.af = socket.AF_UNIX
self.conf4.addr = socket_path
self.conf4.unauth = True
self.conf4.auth = True
else:
self.log.warning("Testing without local unix domain sockets support")
self.serv1 = Socks5Server(self.conf1)
self.serv1.start()
self.serv2 = Socks5Server(self.conf2)
self.serv2.start()
if self.have_ipv6:
self.serv3 = Socks5Server(self.conf3)
self.serv3.start()
if self.have_unix_sockets:
self.serv4 = Socks5Server(self.conf4)
self.serv4.start()
# We will not try to connect to this.
self.i2p_sam = ('127.0.0.1', 7656)
# Note: proxies are not used to connect to local nodes. This is because the proxy to
# use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost.
args = [
['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1'],
['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}',f'-onion={self.conf2.addr[0]}:{self.conf2.addr[1]}',
f'-i2psam={self.i2p_sam[0]}:{self.i2p_sam[1]}', '-i2pacceptincoming=0', '-proxyrandomize=0'],
['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'],
[],
['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1',
'-cjdnsreachable'],
[],
[]
]
if self.have_ipv6:
args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion']
if self.have_unix_sockets:
args[5] = ['-listen', f'-proxy=unix:{socket_path}']
args[6] = ['-listen', f'-onion=unix:{socket_path}']
self.add_nodes(self.num_nodes, extra_args=args)
self.start_nodes()
def network_test(self, node, addr, network):
for peer in node.getpeerinfo():
if peer["addr"] == addr:
assert_equal(peer["network"], network)
def node_test(self, node, *, proxies, auth, test_onion, test_cjdns):
rv = []
addr = "15.61.23.23:1234"
self.log.debug(f"Test: outgoing IPv4 connection through node {node.index} for address {addr}")
node.addnode(addr, "onetry")
cmd = proxies[0].queue.get()
assert isinstance(cmd, Socks5Command)
# Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
assert_equal(cmd.addr, b"15.61.23.23")
assert_equal(cmd.port, 1234)
if not auth:
assert_equal(cmd.username, None)
assert_equal(cmd.password, None)
rv.append(cmd)
self.network_test(node, addr, network=NET_IPV4)
if self.have_ipv6:
addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443"
self.log.debug(f"Test: outgoing IPv6 connection through node {node.index} for address {addr}")
node.addnode(addr, "onetry")
cmd = proxies[1].queue.get()
assert isinstance(cmd, Socks5Command)
# Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
assert_equal(cmd.addr, b"1233:3432:2434:2343:3234:2345:6546:4534")
assert_equal(cmd.port, 5443)
if not auth:
assert_equal(cmd.username, None)
assert_equal(cmd.password, None)
rv.append(cmd)
self.network_test(node, addr, network=NET_IPV6)
if test_onion:
addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333"
self.log.debug(f"Test: outgoing onion connection through node {node.index} for address {addr}")
node.addnode(addr, "onetry")
cmd = proxies[2].queue.get()
assert isinstance(cmd, Socks5Command)
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
assert_equal(cmd.addr, b"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion")
assert_equal(cmd.port, 8333)
if not auth:
assert_equal(cmd.username, None)
assert_equal(cmd.password, None)
rv.append(cmd)
self.network_test(node, addr, network=NET_ONION)
if test_cjdns:
addr = "[fc00:1:2:3:4:5:6:7]:8888"
self.log.debug(f"Test: outgoing CJDNS connection through node {node.index} for address {addr}")
node.addnode(addr, "onetry")
cmd = proxies[1].queue.get()
assert isinstance(cmd, Socks5Command)
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
assert_equal(cmd.addr, b"fc00:1:2:3:4:5:6:7")
assert_equal(cmd.port, 8888)
if not auth:
assert_equal(cmd.username, None)
assert_equal(cmd.password, None)
rv.append(cmd)
self.network_test(node, addr, network=NET_CJDNS)
addr = "node.noumenon:8333"
self.log.debug(f"Test: outgoing DNS name connection through node {node.index} for address {addr}")
node.addnode(addr, "onetry")
cmd = proxies[3].queue.get()
assert isinstance(cmd, Socks5Command)
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
assert_equal(cmd.addr, b"node.noumenon")
assert_equal(cmd.port, 8333)
if not auth:
assert_equal(cmd.username, None)
assert_equal(cmd.password, None)
rv.append(cmd)
self.network_test(node, addr, network=NET_UNROUTABLE)
return rv
def run_test(self):
# basic -proxy
self.node_test(self.nodes[0],
proxies=[self.serv1, self.serv1, self.serv1, self.serv1],
auth=False, test_onion=True, test_cjdns=False)
# -proxy plus -onion
self.node_test(self.nodes[1],
proxies=[self.serv1, self.serv1, self.serv2, self.serv1],
auth=False, test_onion=True, test_cjdns=False)
# -proxy plus -onion, -proxyrandomize
rv = self.node_test(self.nodes[2],
proxies=[self.serv2, self.serv2, self.serv2, self.serv2],
auth=True, test_onion=True, test_cjdns=False)
# Check that credentials as used for -proxyrandomize connections are unique
credentials = set((x.username,x.password) for x in rv)
assert_equal(len(credentials), len(rv))
if self.have_ipv6:
# proxy on IPv6 localhost
self.node_test(self.nodes[3],
proxies=[self.serv3, self.serv3, self.serv3, self.serv3],
auth=False, test_onion=False, test_cjdns=False)
# -proxy=unauth -proxyrandomize=1 -cjdnsreachable
self.node_test(self.nodes[4],
proxies=[self.serv1, self.serv1, self.serv1, self.serv1],
auth=False, test_onion=True, test_cjdns=True)
if self.have_unix_sockets:
self.node_test(self.nodes[5],
proxies=[self.serv4, self.serv4, self.serv4, self.serv4],
auth=True, test_onion=True, test_cjdns=False)
def networks_dict(d):
r = {}
for x in d['networks']:
r[x['name']] = x
return r
self.log.info("Test RPC getnetworkinfo")
nodes_network_info = []
self.log.debug("Test that setting -proxy disables local address discovery, i.e. -discover=0")
for node in self.nodes:
network_info = node.getnetworkinfo()
assert_equal(network_info["localaddresses"], [])
nodes_network_info.append(network_info)
n0 = networks_dict(nodes_network_info[0])
assert_equal(NETWORKS, n0.keys())
for net in NETWORKS:
if net == NET_I2P:
expected_proxy = ''
expected_randomize = False
else:
expected_proxy = '%s:%i' % (self.conf1.addr)
expected_randomize = True
assert_equal(n0[net]['proxy'], expected_proxy)
assert_equal(n0[net]['proxy_randomize_credentials'], expected_randomize)
assert_equal(n0['onion']['reachable'], True)
assert_equal(n0['i2p']['reachable'], False)
assert_equal(n0['cjdns']['reachable'], False)
n1 = networks_dict(nodes_network_info[1])
assert_equal(NETWORKS, n1.keys())
for net in ['ipv4', 'ipv6']:
assert_equal(n1[net]['proxy'], f'{self.conf1.addr[0]}:{self.conf1.addr[1]}')
assert_equal(n1[net]['proxy_randomize_credentials'], False)
assert_equal(n1['onion']['proxy'], f'{self.conf2.addr[0]}:{self.conf2.addr[1]}')
assert_equal(n1['onion']['proxy_randomize_credentials'], False)
assert_equal(n1['onion']['reachable'], True)
assert_equal(n1['i2p']['proxy'], f'{self.i2p_sam[0]}:{self.i2p_sam[1]}')
assert_equal(n1['i2p']['proxy_randomize_credentials'], False)
assert_equal(n1['i2p']['reachable'], True)
n2 = networks_dict(nodes_network_info[2])
assert_equal(NETWORKS, n2.keys())
proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}'
for net in NETWORKS:
if net == NET_I2P:
expected_proxy = ''
expected_randomize = False
else:
expected_proxy = proxy
expected_randomize = True
assert_equal(n2[net]['proxy'], expected_proxy)
assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize)
assert_equal(n2['onion']['reachable'], True)
assert_equal(n2['i2p']['reachable'], False)
assert_equal(n2['cjdns']['reachable'], False)
if self.have_ipv6:
n3 = networks_dict(nodes_network_info[3])
assert_equal(NETWORKS, n3.keys())
proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}'
for net in NETWORKS:
expected_proxy = '' if net == NET_I2P or net == NET_ONION else proxy
assert_equal(n3[net]['proxy'], expected_proxy)
assert_equal(n3[net]['proxy_randomize_credentials'], False)
assert_equal(n3['onion']['reachable'], False)
assert_equal(n3['i2p']['reachable'], False)
assert_equal(n3['cjdns']['reachable'], False)
n4 = networks_dict(nodes_network_info[4])
assert_equal(NETWORKS, n4.keys())
for net in NETWORKS:
if net == NET_I2P:
expected_proxy = ''
expected_randomize = False
else:
expected_proxy = '%s:%i' % (self.conf1.addr)
expected_randomize = True
assert_equal(n4[net]['proxy'], expected_proxy)
assert_equal(n4[net]['proxy_randomize_credentials'], expected_randomize)
assert_equal(n4['onion']['reachable'], True)
assert_equal(n4['i2p']['reachable'], False)
assert_equal(n4['cjdns']['reachable'], True)
if self.have_unix_sockets:
n5 = networks_dict(nodes_network_info[5])
assert_equal(NETWORKS, n5.keys())
for net in NETWORKS:
if net == NET_I2P:
expected_proxy = ''
expected_randomize = False
else:
expected_proxy = 'unix:' + self.conf4.addr # no port number
expected_randomize = True
assert_equal(n5[net]['proxy'], expected_proxy)
assert_equal(n5[net]['proxy_randomize_credentials'], expected_randomize)
assert_equal(n5['onion']['reachable'], True)
assert_equal(n5['i2p']['reachable'], False)
assert_equal(n5['cjdns']['reachable'], False)
n6 = networks_dict(nodes_network_info[6])
assert_equal(NETWORKS, n6.keys())
for net in NETWORKS:
if net != NET_ONION:
expected_proxy = ''
expected_randomize = False
else:
expected_proxy = 'unix:' + self.conf4.addr # no port number
expected_randomize = True
assert_equal(n6[net]['proxy'], expected_proxy)
assert_equal(n6[net]['proxy_randomize_credentials'], expected_randomize)
assert_equal(n6['onion']['reachable'], True)
assert_equal(n6['i2p']['reachable'], False)
assert_equal(n6['cjdns']['reachable'], False)
self.stop_node(1)
self.log.info("Test passing invalid -proxy hostname raises expected init error")
self.nodes[1].extra_args = ["-proxy=abc..abc:23456"]
msg = "Error: Invalid -proxy address or hostname: 'abc..abc:23456'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -proxy port raises expected init error")
self.nodes[1].extra_args = ["-proxy=192.0.0.1:def"]
msg = "Error: Invalid port specified in -proxy: '192.0.0.1:def'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -onion hostname raises expected init error")
self.nodes[1].extra_args = ["-onion=xyz..xyz:23456"]
msg = "Error: Invalid -onion address or hostname: 'xyz..xyz:23456'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -onion port raises expected init error")
self.nodes[1].extra_args = ["-onion=192.0.0.1:def"]
msg = "Error: Invalid port specified in -onion: '192.0.0.1:def'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -i2psam hostname raises expected init error")
self.nodes[1].extra_args = ["-i2psam=def..def:23456"]
msg = "Error: Invalid -i2psam address or hostname: 'def..def:23456'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -i2psam port raises expected init error")
self.nodes[1].extra_args = ["-i2psam=192.0.0.1:def"]
msg = "Error: Invalid port specified in -i2psam: '192.0.0.1:def'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -onlynet=i2p without -i2psam raises expected init error")
self.nodes[1].extra_args = ["-onlynet=i2p"]
msg = "Error: Outbound connections restricted to i2p (-onlynet=i2p) but -i2psam is not provided"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing invalid -onlynet=cjdns without -cjdnsreachable raises expected init error")
self.nodes[1].extra_args = ["-onlynet=cjdns"]
msg = "Error: Outbound connections restricted to CJDNS (-onlynet=cjdns) but -cjdnsreachable is not provided"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing -onlynet=onion with -onion=0/-noonion raises expected init error")
msg = (
"Error: Outbound connections restricted to Tor (-onlynet=onion) but "
"the proxy for reaching the Tor network is explicitly forbidden: -onion=0"
)
for arg in ["-onion=0", "-noonion"]:
self.nodes[1].extra_args = ["-onlynet=onion", arg]
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing -onlynet=onion without -proxy, -onion or -listenonion raises expected init error")
self.nodes[1].extra_args = ["-onlynet=onion", "-listenonion=0"]
msg = (
"Error: Outbound connections restricted to Tor (-onlynet=onion) but the proxy for "
"reaching the Tor network is not provided: none of -proxy, -onion or -listenonion is given"
)
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing -onlynet=onion without -proxy or -onion but with -listenonion=1 is ok")
self.start_node(1, extra_args=["-onlynet=onion", "-listenonion=1"])
self.stop_node(1)
self.log.info("Test passing unknown network to -onlynet raises expected init error")
self.nodes[1].extra_args = ["-onlynet=abc"]
msg = "Error: Unknown network specified in -onlynet: 'abc'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
self.log.info("Test passing too-long unix path to -proxy raises init error")
self.nodes[1].extra_args = [f"-proxy=unix:{'x' * 1000}"]
if self.have_unix_sockets:
msg = f"Error: Invalid -proxy address or hostname: 'unix:{'x' * 1000}'"
else:
# If unix sockets are not supported, the file path is incorrectly interpreted as host:port
msg = f"Error: Invalid port specified in -proxy: 'unix:{'x' * 1000}'"
self.nodes[1].assert_start_raises_init_error(expected_msg=msg)
# Cleanup socket path we established outside the individual test directory.
if self.have_unix_sockets:
os.unlink(socket_path)
if __name__ == '__main__':
ProxyTest().main()