/ test / functional / feature_proxy.py
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()