rpc-tests: Add proxy test
Add test for -proxy, -onion and -proxyrandomize.
This commit is contained in:
parent
67a7949397
commit
6be3562e50
3 changed files with 307 additions and 0 deletions
|
@ -27,6 +27,7 @@ testScripts=(
|
||||||
'mempool_coinbase_spends.py'
|
'mempool_coinbase_spends.py'
|
||||||
'httpbasics.py'
|
'httpbasics.py'
|
||||||
'zapwallettxes.py'
|
'zapwallettxes.py'
|
||||||
|
'proxy_test.py'
|
||||||
# 'forknotify.py'
|
# 'forknotify.py'
|
||||||
);
|
);
|
||||||
if [ "x${ENABLE_BITCOIND}${ENABLE_UTILS}${ENABLE_WALLET}" = "x111" ]; then
|
if [ "x${ENABLE_BITCOIND}${ENABLE_UTILS}${ENABLE_WALLET}" = "x111" ]; then
|
||||||
|
|
146
qa/rpc-tests/proxy_test.py
Executable file
146
qa/rpc-tests/proxy_test.py
Executable file
|
@ -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()
|
||||||
|
|
160
qa/rpc-tests/socks5.py
Normal file
160
qa/rpc-tests/socks5.py
Normal file
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue