aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xqa/pull-tester/rpc-tests.sh1
-rwxr-xr-xqa/rpc-tests/proxy_test.py146
-rw-r--r--qa/rpc-tests/socks5.py160
3 files changed, 307 insertions, 0 deletions
diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh
index efeee45530..053e8b8a7f 100755
--- a/qa/pull-tester/rpc-tests.sh
+++ b/qa/pull-tester/rpc-tests.sh
@@ -27,6 +27,7 @@ testScripts=(
'mempool_coinbase_spends.py'
'httpbasics.py'
'zapwallettxes.py'
+ 'proxy_test.py'
# 'forknotify.py'
);
if [ "x${ENABLE_BITCOIND}${ENABLE_UTILS}${ENABLE_WALLET}" = "x111" ]; then
diff --git a/qa/rpc-tests/proxy_test.py b/qa/rpc-tests/proxy_test.py
new file mode 100755
index 0000000000..d6d9e6725b
--- /dev/null
+++ b/qa/rpc-tests/proxy_test.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python2
+# Copyright (c) 2015 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+import socket
+import traceback, sys
+from binascii import hexlify
+import time, os
+
+from socks5 import Socks5Configuration, Socks5Command, Socks5Server, AddressType
+from test_framework import BitcoinTestFramework
+from util import *
+'''
+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
+- Proxy configurations to test on proxy side,
+ - support no authentication (other proxy)
+ - support no authentication + user/pass authentication (Tor)
+ - proxy on IPv6
+
+- Create various proxies (as threads)
+- Create bitcoinds that connect to them
+- Manipulate the bitcoinds using addnode (onetry) an observe effects
+
+addnode connect to IPv4
+addnode connect to IPv6
+addnode connect to onion
+addnode connect to generic DNS name
+'''
+
+class ProxyTest(BitcoinTestFramework):
+ def __init__(self):
+ # Create two proxies on different ports
+ # ... one unauthenticated
+ self.conf1 = Socks5Configuration()
+ self.conf1.addr = ('127.0.0.1', 13000 + (os.getpid() % 1000))
+ self.conf1.unauth = True
+ self.conf1.auth = False
+ # ... one supporting authenticated and unauthenticated (Tor)
+ self.conf2 = Socks5Configuration()
+ self.conf2.addr = ('127.0.0.1', 14000 + (os.getpid() % 1000))
+ self.conf2.unauth = True
+ self.conf2.auth = True
+ # ... one on IPv6 with similar configuration
+ self.conf3 = Socks5Configuration()
+ self.conf3.af = socket.AF_INET6
+ self.conf3.addr = ('::1', 15000 + (os.getpid() % 1000))
+ self.conf3.unauth = True
+ self.conf3.auth = True
+
+ self.serv1 = Socks5Server(self.conf1)
+ self.serv1.start()
+ self.serv2 = Socks5Server(self.conf2)
+ self.serv2.start()
+ self.serv3 = Socks5Server(self.conf3)
+ self.serv3.start()
+
+ def setup_nodes(self):
+ # Note: proxies are not used to connect to local nodes
+ # this is because the proxy to use is based on CService.GetNetwork(), which return NET_UNROUTABLE for localhost
+ return start_nodes(4, self.options.tmpdir, extra_args=[
+ ['-listen', '-debug=net', '-debug=proxy', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'],
+ ['-listen', '-debug=net', '-debug=proxy', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr),'-proxyrandomize=0'],
+ ['-listen', '-debug=net', '-debug=proxy', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'],
+ ['-listen', '-debug=net', '-debug=proxy', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0']
+ ])
+
+ def node_test(self, node, proxies, auth):
+ rv = []
+ # Test: outgoing IPv4 connection through node
+ node.addnode("15.61.23.23:1234", "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, "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)
+
+ # Test: outgoing IPv6 connection through node
+ node.addnode("[1233:3432:2434:2343:3234:2345:6546:4534]:5443", "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, "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)
+
+ # Test: outgoing onion connection through node
+ node.addnode("bitcoinostk4e4re.onion:8333", "onetry")
+ cmd = proxies[2].queue.get()
+ assert(isinstance(cmd, Socks5Command))
+ assert_equal(cmd.atyp, AddressType.DOMAINNAME)
+ assert_equal(cmd.addr, "bitcoinostk4e4re.onion")
+ assert_equal(cmd.port, 8333)
+ if not auth:
+ assert_equal(cmd.username, None)
+ assert_equal(cmd.password, None)
+ rv.append(cmd)
+
+ # Test: outgoing DNS name connection through node
+ node.addnode("node.noumenon:8333", "onetry")
+ cmd = proxies[3].queue.get()
+ assert(isinstance(cmd, Socks5Command))
+ assert_equal(cmd.atyp, AddressType.DOMAINNAME)
+ assert_equal(cmd.addr, "node.noumenon")
+ assert_equal(cmd.port, 8333)
+ if not auth:
+ assert_equal(cmd.username, None)
+ assert_equal(cmd.password, None)
+ rv.append(cmd)
+
+ return rv
+
+ def run_test(self):
+ # basic -proxy
+ self.node_test(self.nodes[0], [self.serv1, self.serv1, self.serv1, self.serv1], False)
+
+ # -proxy plus -onion
+ self.node_test(self.nodes[1], [self.serv1, self.serv1, self.serv2, self.serv1], False)
+
+ # -proxy plus -onion, -proxyrandomize
+ rv = self.node_test(self.nodes[2], [self.serv2, self.serv2, self.serv2, self.serv2], True)
+ # Check that credentials as used for -proxyrandomize connections are unique
+ credentials = set((x.username,x.password) for x in rv)
+ assert_equal(len(credentials), 4)
+
+ # proxy on IPv6 localhost
+ self.node_test(self.nodes[3], [self.serv3, self.serv3, self.serv3, self.serv3], False)
+
+if __name__ == '__main__':
+ ProxyTest().main()
+
diff --git a/qa/rpc-tests/socks5.py b/qa/rpc-tests/socks5.py
new file mode 100644
index 0000000000..1dbfb98d5d
--- /dev/null
+++ b/qa/rpc-tests/socks5.py
@@ -0,0 +1,160 @@
+# Copyright (c) 2015 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+'''
+Dummy Socks5 server for testing.
+'''
+from __future__ import print_function, division, unicode_literals
+import socket, threading, Queue
+import traceback, sys
+
+### Protocol constants
+class Command:
+ CONNECT = 0x01
+
+class AddressType:
+ IPV4 = 0x01
+ DOMAINNAME = 0x03
+ IPV6 = 0x04
+
+### Utility functions
+def recvall(s, n):
+ '''Receive n bytes from a socket, or fail'''
+ rv = bytearray()
+ while n > 0:
+ d = s.recv(n)
+ if not d:
+ raise IOError('Unexpected end of stream')
+ rv.extend(d)
+ n -= len(d)
+ return rv
+
+### Implementation classes
+class Socks5Configuration(object):
+ '''Proxy configuration'''
+ def __init__(self):
+ self.addr = None # Bind address (must be set)
+ self.af = socket.AF_INET # Bind address family
+ self.unauth = False # Support unauthenticated
+ self.auth = False # Support authentication
+
+class Socks5Command(object):
+ '''Information about an incoming socks5 command'''
+ def __init__(self, cmd, atyp, addr, port, username, password):
+ self.cmd = cmd # Command (one of Command.*)
+ self.atyp = atyp # Address type (one of AddressType.*)
+ self.addr = addr # Address
+ self.port = port # Port to connect to
+ self.username = username
+ self.password = password
+ def __repr__(self):
+ return 'Socks5Command(%s,%s,%s,%s,%s,%s)' % (self.cmd, self.atyp, self.addr, self.port, self.username, self.password)
+
+class Socks5Connection(object):
+ def __init__(self, serv, conn, peer):
+ self.serv = serv
+ self.conn = conn
+ self.peer = peer
+
+ def handle(self):
+ '''
+ Handle socks5 request according to RFC1928
+ '''
+ try:
+ # Verify socks version
+ ver = recvall(self.conn, 1)[0]
+ if ver != 0x05:
+ raise IOError('Invalid socks version %i' % ver)
+ # Choose authentication method
+ nmethods = recvall(self.conn, 1)[0]
+ methods = bytearray(recvall(self.conn, nmethods))
+ method = None
+ if 0x02 in methods and self.serv.conf.auth:
+ method = 0x02 # username/password
+ elif 0x00 in methods and self.serv.conf.unauth:
+ method = 0x00 # unauthenticated
+ if method is None:
+ raise IOError('No supported authentication method was offered')
+ # Send response
+ self.conn.sendall(bytearray([0x05, method]))
+ # Read authentication (optional)
+ username = None
+ password = None
+ if method == 0x02:
+ ver = recvall(self.conn, 1)[0]
+ if ver != 0x01:
+ raise IOError('Invalid auth packet version %i' % ver)
+ ulen = recvall(self.conn, 1)[0]
+ username = str(recvall(self.conn, ulen))
+ plen = recvall(self.conn, 1)[0]
+ password = str(recvall(self.conn, plen))
+ # Send authentication response
+ self.conn.sendall(bytearray([0x01, 0x00]))
+
+ # Read connect request
+ (ver,cmd,rsv,atyp) = recvall(self.conn, 4)
+ if ver != 0x05:
+ raise IOError('Invalid socks version %i in connect request' % ver)
+ if cmd != Command.CONNECT:
+ raise IOError('Unhandled command %i in connect request' % cmd)
+
+ if atyp == AddressType.IPV4:
+ addr = recvall(self.conn, 4)
+ elif atyp == AddressType.DOMAINNAME:
+ n = recvall(self.conn, 1)[0]
+ addr = str(recvall(self.conn, n))
+ elif atyp == AddressType.IPV6:
+ addr = recvall(self.conn, 16)
+ else:
+ raise IOError('Unknown address type %i' % atyp)
+ port_hi,port_lo = recvall(self.conn, 2)
+ port = (port_hi << 8) | port_lo
+
+ # Send dummy response
+ self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
+
+ cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
+ self.serv.queue.put(cmdin)
+ print('Proxy: ', cmdin)
+ # Fall through to disconnect
+ except Exception,e:
+ traceback.print_exc(file=sys.stderr)
+ self.serv.queue.put(e)
+ finally:
+ self.conn.close()
+
+class Socks5Server(object):
+ def __init__(self, conf):
+ self.conf = conf
+ self.s = socket.socket(conf.af)
+ self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.s.bind(conf.addr)
+ self.s.listen(5)
+ self.running = False
+ self.thread = None
+ self.queue = Queue.Queue() # report connections and exceptions to client
+
+ def run(self):
+ while self.running:
+ (sockconn, peer) = self.s.accept()
+ if self.running:
+ conn = Socks5Connection(self, sockconn, peer)
+ thread = threading.Thread(None, conn.handle)
+ thread.daemon = True
+ thread.start()
+
+ def start(self):
+ assert(not self.running)
+ self.running = True
+ self.thread = threading.Thread(None, self.run)
+ self.thread.daemon = True
+ self.thread.start()
+
+ def stop(self):
+ self.running = False
+ # connect to self to end run loop
+ s = socket.socket(self.conf.af)
+ s.connect(self.conf.addr)
+ s.close()
+ self.thread.join()
+