1#!/usr/bin/env python3
2# Copyright (c) 2015-2019 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
7Test 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- Proxy configurations to test on proxy side,
16    - support no authentication (other proxy)
17    - support no authentication + user/pass authentication (Tor)
18    - proxy on IPv6
19
20- Create various proxies (as threads)
21- Create nodes that connect to them
22- Manipulate the peer connections using addnode (onetry) and observe effects
23- Test the getpeerinfo `network` field for the peer
24
25addnode connect to IPv4
26addnode connect to IPv6
27addnode connect to onion
28addnode connect to generic DNS name
29
30- Test getnetworkinfo for each node
31"""
32
33import socket
34import os
35
36from test_framework.socks5 import Socks5Configuration, Socks5Command, Socks5Server, AddressType
37from test_framework.test_framework import BitcoinTestFramework
38from test_framework.util import (
39    PORT_MIN,
40    PORT_RANGE,
41    assert_equal,
42)
43from test_framework.netutil import test_ipv6_local
44
45RANGE_BEGIN = PORT_MIN + 2 * PORT_RANGE  # Start after p2p and rpc ports
46
47# Networks returned by RPC getpeerinfo, defined in src/netbase.cpp::GetNetworkName()
48NET_UNROUTABLE = "unroutable"
49NET_IPV4 = "ipv4"
50NET_IPV6 = "ipv6"
51NET_ONION = "onion"
52
53# Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo()
54NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION})
55
56
57class ProxyTest(BitcoinTestFramework):
58    def set_test_params(self):
59        self.num_nodes = 4
60        self.setup_clean_chain = True
61
62    def setup_nodes(self):
63        self.have_ipv6 = test_ipv6_local()
64        # Create two proxies on different ports
65        # ... one unauthenticated
66        self.conf1 = Socks5Configuration()
67        self.conf1.addr = ('127.0.0.1', RANGE_BEGIN + (os.getpid() % 1000))
68        self.conf1.unauth = True
69        self.conf1.auth = False
70        # ... one supporting authenticated and unauthenticated (Tor)
71        self.conf2 = Socks5Configuration()
72        self.conf2.addr = ('127.0.0.1', RANGE_BEGIN + 1000 + (os.getpid() % 1000))
73        self.conf2.unauth = True
74        self.conf2.auth = True
75        if self.have_ipv6:
76            # ... one on IPv6 with similar configuration
77            self.conf3 = Socks5Configuration()
78            self.conf3.af = socket.AF_INET6
79            self.conf3.addr = ('::1', RANGE_BEGIN + 2000 + (os.getpid() % 1000))
80            self.conf3.unauth = True
81            self.conf3.auth = True
82        else:
83            self.log.warning("Testing without local IPv6 support")
84
85        self.serv1 = Socks5Server(self.conf1)
86        self.serv1.start()
87        self.serv2 = Socks5Server(self.conf2)
88        self.serv2.start()
89        if self.have_ipv6:
90            self.serv3 = Socks5Server(self.conf3)
91            self.serv3.start()
92
93        # Note: proxies are not used to connect to local nodes. This is because the proxy to
94        # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost.
95        args = [
96            ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'],
97            ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr),'-proxyrandomize=0'],
98            ['-listen', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'],
99            []
100        ]
101        if self.have_ipv6:
102            args[3] = ['-listen', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0', '-noonion']
103        self.add_nodes(self.num_nodes, extra_args=args)
104        self.start_nodes()
105
106    def network_test(self, node, addr, network):
107        for peer in node.getpeerinfo():
108            if peer["addr"] == addr:
109                assert_equal(peer["network"], network)
110
111    def node_test(self, node, proxies, auth, test_onion=True):
112        rv = []
113        addr = "15.61.23.23:1234"
114        self.log.debug("Test: outgoing IPv4 connection through node for address {}".format(addr))
115        node.addnode(addr, "onetry")
116        cmd = proxies[0].queue.get()
117        assert isinstance(cmd, Socks5Command)
118        # Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6
119        assert_equal(cmd.atyp, AddressType.DOMAINNAME)
120        assert_equal(cmd.addr, b"15.61.23.23")
121        assert_equal(cmd.port, 1234)
122        if not auth:
123            assert_equal(cmd.username, None)
124            assert_equal(cmd.password, None)
125        rv.append(cmd)
126        self.network_test(node, addr, network=NET_IPV4)
127
128        if self.have_ipv6:
129            addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443"
130            self.log.debug("Test: outgoing IPv6 connection through node for address {}".format(addr))
131            node.addnode(addr, "onetry")
132            cmd = proxies[1].queue.get()
133            assert isinstance(cmd, Socks5Command)
134            # Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6
135            assert_equal(cmd.atyp, AddressType.DOMAINNAME)
136            assert_equal(cmd.addr, b"1233:3432:2434:2343:3234:2345:6546:4534")
137            assert_equal(cmd.port, 5443)
138            if not auth:
139                assert_equal(cmd.username, None)
140                assert_equal(cmd.password, None)
141            rv.append(cmd)
142            self.network_test(node, addr, network=NET_IPV6)
143
144        if test_onion:
145            addr = "bitcoinostk4e4re.onion:8333"
146            self.log.debug("Test: outgoing onion connection through node for address {}".format(addr))
147            node.addnode(addr, "onetry")
148            cmd = proxies[2].queue.get()
149            assert isinstance(cmd, Socks5Command)
150            assert_equal(cmd.atyp, AddressType.DOMAINNAME)
151            assert_equal(cmd.addr, b"bitcoinostk4e4re.onion")
152            assert_equal(cmd.port, 8333)
153            if not auth:
154                assert_equal(cmd.username, None)
155                assert_equal(cmd.password, None)
156            rv.append(cmd)
157            self.network_test(node, addr, network=NET_ONION)
158
159        addr = "node.noumenon:8333"
160        self.log.debug("Test: outgoing DNS name connection through node for address {}".format(addr))
161        node.addnode(addr, "onetry")
162        cmd = proxies[3].queue.get()
163        assert isinstance(cmd, Socks5Command)
164        assert_equal(cmd.atyp, AddressType.DOMAINNAME)
165        assert_equal(cmd.addr, b"node.noumenon")
166        assert_equal(cmd.port, 8333)
167        if not auth:
168            assert_equal(cmd.username, None)
169            assert_equal(cmd.password, None)
170        rv.append(cmd)
171        self.network_test(node, addr, network=NET_UNROUTABLE)
172
173        return rv
174
175    def run_test(self):
176        # basic -proxy
177        self.node_test(self.nodes[0], [self.serv1, self.serv1, self.serv1, self.serv1], False)
178
179        # -proxy plus -onion
180        self.node_test(self.nodes[1], [self.serv1, self.serv1, self.serv2, self.serv1], False)
181
182        # -proxy plus -onion, -proxyrandomize
183        rv = self.node_test(self.nodes[2], [self.serv2, self.serv2, self.serv2, self.serv2], True)
184        # Check that credentials as used for -proxyrandomize connections are unique
185        credentials = set((x.username,x.password) for x in rv)
186        assert_equal(len(credentials), len(rv))
187
188        if self.have_ipv6:
189            # proxy on IPv6 localhost
190            self.node_test(self.nodes[3], [self.serv3, self.serv3, self.serv3, self.serv3], False, False)
191
192        def networks_dict(d):
193            r = {}
194            for x in d['networks']:
195                r[x['name']] = x
196            return r
197
198        self.log.info("Test RPC getnetworkinfo")
199        n0 = networks_dict(self.nodes[0].getnetworkinfo())
200        assert_equal(NETWORKS, n0.keys())
201        for net in NETWORKS:
202            assert_equal(n0[net]['proxy'], '%s:%i' % (self.conf1.addr))
203            assert_equal(n0[net]['proxy_randomize_credentials'], True)
204        assert_equal(n0['onion']['reachable'], True)
205
206        n1 = networks_dict(self.nodes[1].getnetworkinfo())
207        assert_equal(NETWORKS, n1.keys())
208        for net in ['ipv4', 'ipv6']:
209            assert_equal(n1[net]['proxy'], '%s:%i' % (self.conf1.addr))
210            assert_equal(n1[net]['proxy_randomize_credentials'], False)
211        assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr))
212        assert_equal(n1['onion']['proxy_randomize_credentials'], False)
213        assert_equal(n1['onion']['reachable'], True)
214
215        n2 = networks_dict(self.nodes[2].getnetworkinfo())
216        assert_equal(NETWORKS, n2.keys())
217        for net in NETWORKS:
218            assert_equal(n2[net]['proxy'], '%s:%i' % (self.conf2.addr))
219            assert_equal(n2[net]['proxy_randomize_credentials'], True)
220        assert_equal(n2['onion']['reachable'], True)
221
222        if self.have_ipv6:
223            n3 = networks_dict(self.nodes[3].getnetworkinfo())
224            assert_equal(NETWORKS, n3.keys())
225            for net in NETWORKS:
226                assert_equal(n3[net]['proxy'], '[%s]:%i' % (self.conf3.addr))
227                assert_equal(n3[net]['proxy_randomize_credentials'], False)
228            assert_equal(n3['onion']['reachable'], False)
229
230
231if __name__ == '__main__':
232    ProxyTest().main()
233