Skip to content

Commit 6be3562

Browse files
committed
rpc-tests: Add proxy test
Add test for -proxy, -onion and -proxyrandomize.
1 parent 67a7949 commit 6be3562

File tree

3 files changed

+307
-0
lines changed

3 files changed

+307
-0
lines changed

qa/pull-tester/rpc-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ testScripts=(
2727
'mempool_coinbase_spends.py'
2828
'httpbasics.py'
2929
'zapwallettxes.py'
30+
'proxy_test.py'
3031
# 'forknotify.py'
3132
);
3233
if [ "x${ENABLE_BITCOIND}${ENABLE_UTILS}${ENABLE_WALLET}" = "x111" ]; then

qa/rpc-tests/proxy_test.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python2
2+
# Copyright (c) 2015 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+
import socket
6+
import traceback, sys
7+
from binascii import hexlify
8+
import time, os
9+
10+
from socks5 import Socks5Configuration, Socks5Command, Socks5Server, AddressType
11+
from test_framework import BitcoinTestFramework
12+
from util import *
13+
'''
14+
Test plan:
15+
- Start bitcoind's with different proxy configurations
16+
- Use addnode to initiate connections
17+
- Verify that proxies are connected to, and the right connection command is given
18+
- Proxy configurations to test on bitcoind side:
19+
- `-proxy` (proxy everything)
20+
- `-onion` (proxy just onions)
21+
- `-proxyrandomize` Circuit randomization
22+
- Proxy configurations to test on proxy side,
23+
- support no authentication (other proxy)
24+
- support no authentication + user/pass authentication (Tor)
25+
- proxy on IPv6
26+
27+
- Create various proxies (as threads)
28+
- Create bitcoinds that connect to them
29+
- Manipulate the bitcoinds using addnode (onetry) an observe effects
30+
31+
addnode connect to IPv4
32+
addnode connect to IPv6
33+
addnode connect to onion
34+
addnode connect to generic DNS name
35+
'''
36+
37+
class ProxyTest(BitcoinTestFramework):
38+
def __init__(self):
39+
# Create two proxies on different ports
40+
# ... one unauthenticated
41+
self.conf1 = Socks5Configuration()
42+
self.conf1.addr = ('127.0.0.1', 13000 + (os.getpid() % 1000))
43+
self.conf1.unauth = True
44+
self.conf1.auth = False
45+
# ... one supporting authenticated and unauthenticated (Tor)
46+
self.conf2 = Socks5Configuration()
47+
self.conf2.addr = ('127.0.0.1', 14000 + (os.getpid() % 1000))
48+
self.conf2.unauth = True
49+
self.conf2.auth = True
50+
# ... one on IPv6 with similar configuration
51+
self.conf3 = Socks5Configuration()
52+
self.conf3.af = socket.AF_INET6
53+
self.conf3.addr = ('::1', 15000 + (os.getpid() % 1000))
54+
self.conf3.unauth = True
55+
self.conf3.auth = True
56+
57+
self.serv1 = Socks5Server(self.conf1)
58+
self.serv1.start()
59+
self.serv2 = Socks5Server(self.conf2)
60+
self.serv2.start()
61+
self.serv3 = Socks5Server(self.conf3)
62+
self.serv3.start()
63+
64+
def setup_nodes(self):
65+
# Note: proxies are not used to connect to local nodes
66+
# this is because the proxy to use is based on CService.GetNetwork(), which return NET_UNROUTABLE for localhost
67+
return start_nodes(4, self.options.tmpdir, extra_args=[
68+
['-listen', '-debug=net', '-debug=proxy', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'],
69+
['-listen', '-debug=net', '-debug=proxy', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr),'-proxyrandomize=0'],
70+
['-listen', '-debug=net', '-debug=proxy', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'],
71+
['-listen', '-debug=net', '-debug=proxy', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0']
72+
])
73+
74+
def node_test(self, node, proxies, auth):
75+
rv = []
76+
# Test: outgoing IPv4 connection through node
77+
node.addnode("15.61.23.23:1234", "onetry")
78+
cmd = proxies[0].queue.get()
79+
assert(isinstance(cmd, Socks5Command))
80+
# Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6
81+
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
82+
assert_equal(cmd.addr, "15.61.23.23")
83+
assert_equal(cmd.port, 1234)
84+
if not auth:
85+
assert_equal(cmd.username, None)
86+
assert_equal(cmd.password, None)
87+
rv.append(cmd)
88+
89+
# Test: outgoing IPv6 connection through node
90+
node.addnode("[1233:3432:2434:2343:3234:2345:6546:4534]:5443", "onetry")
91+
cmd = proxies[1].queue.get()
92+
assert(isinstance(cmd, Socks5Command))
93+
# Note: bitcoind's SOCKS5 implementation only sends atyp DOMAINNAME, even if connecting directly to IPv4/IPv6
94+
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
95+
assert_equal(cmd.addr, "1233:3432:2434:2343:3234:2345:6546:4534")
96+
assert_equal(cmd.port, 5443)
97+
if not auth:
98+
assert_equal(cmd.username, None)
99+
assert_equal(cmd.password, None)
100+
rv.append(cmd)
101+
102+
# Test: outgoing onion connection through node
103+
node.addnode("bitcoinostk4e4re.onion:8333", "onetry")
104+
cmd = proxies[2].queue.get()
105+
assert(isinstance(cmd, Socks5Command))
106+
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
107+
assert_equal(cmd.addr, "bitcoinostk4e4re.onion")
108+
assert_equal(cmd.port, 8333)
109+
if not auth:
110+
assert_equal(cmd.username, None)
111+
assert_equal(cmd.password, None)
112+
rv.append(cmd)
113+
114+
# Test: outgoing DNS name connection through node
115+
node.addnode("node.noumenon:8333", "onetry")
116+
cmd = proxies[3].queue.get()
117+
assert(isinstance(cmd, Socks5Command))
118+
assert_equal(cmd.atyp, AddressType.DOMAINNAME)
119+
assert_equal(cmd.addr, "node.noumenon")
120+
assert_equal(cmd.port, 8333)
121+
if not auth:
122+
assert_equal(cmd.username, None)
123+
assert_equal(cmd.password, None)
124+
rv.append(cmd)
125+
126+
return rv
127+
128+
def run_test(self):
129+
# basic -proxy
130+
self.node_test(self.nodes[0], [self.serv1, self.serv1, self.serv1, self.serv1], False)
131+
132+
# -proxy plus -onion
133+
self.node_test(self.nodes[1], [self.serv1, self.serv1, self.serv2, self.serv1], False)
134+
135+
# -proxy plus -onion, -proxyrandomize
136+
rv = self.node_test(self.nodes[2], [self.serv2, self.serv2, self.serv2, self.serv2], True)
137+
# Check that credentials as used for -proxyrandomize connections are unique
138+
credentials = set((x.username,x.password) for x in rv)
139+
assert_equal(len(credentials), 4)
140+
141+
# proxy on IPv6 localhost
142+
self.node_test(self.nodes[3], [self.serv3, self.serv3, self.serv3, self.serv3], False)
143+
144+
if __name__ == '__main__':
145+
ProxyTest().main()
146+

qa/rpc-tests/socks5.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright (c) 2015 The Bitcoin Core developers
2+
# Distributed under the MIT software license, see the accompanying
3+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
'''
5+
Dummy Socks5 server for testing.
6+
'''
7+
from __future__ import print_function, division, unicode_literals
8+
import socket, threading, Queue
9+
import traceback, sys
10+
11+
### Protocol constants
12+
class Command:
13+
CONNECT = 0x01
14+
15+
class AddressType:
16+
IPV4 = 0x01
17+
DOMAINNAME = 0x03
18+
IPV6 = 0x04
19+
20+
### Utility functions
21+
def recvall(s, n):
22+
'''Receive n bytes from a socket, or fail'''
23+
rv = bytearray()
24+
while n > 0:
25+
d = s.recv(n)
26+
if not d:
27+
raise IOError('Unexpected end of stream')
28+
rv.extend(d)
29+
n -= len(d)
30+
return rv
31+
32+
### Implementation classes
33+
class Socks5Configuration(object):
34+
'''Proxy configuration'''
35+
def __init__(self):
36+
self.addr = None # Bind address (must be set)
37+
self.af = socket.AF_INET # Bind address family
38+
self.unauth = False # Support unauthenticated
39+
self.auth = False # Support authentication
40+
41+
class Socks5Command(object):
42+
'''Information about an incoming socks5 command'''
43+
def __init__(self, cmd, atyp, addr, port, username, password):
44+
self.cmd = cmd # Command (one of Command.*)
45+
self.atyp = atyp # Address type (one of AddressType.*)
46+
self.addr = addr # Address
47+
self.port = port # Port to connect to
48+
self.username = username
49+
self.password = password
50+
def __repr__(self):
51+
return 'Socks5Command(%s,%s,%s,%s,%s,%s)' % (self.cmd, self.atyp, self.addr, self.port, self.username, self.password)
52+
53+
class Socks5Connection(object):
54+
def __init__(self, serv, conn, peer):
55+
self.serv = serv
56+
self.conn = conn
57+
self.peer = peer
58+
59+
def handle(self):
60+
'''
61+
Handle socks5 request according to RFC1928
62+
'''
63+
try:
64+
# Verify socks version
65+
ver = recvall(self.conn, 1)[0]
66+
if ver != 0x05:
67+
raise IOError('Invalid socks version %i' % ver)
68+
# Choose authentication method
69+
nmethods = recvall(self.conn, 1)[0]
70+
methods = bytearray(recvall(self.conn, nmethods))
71+
method = None
72+
if 0x02 in methods and self.serv.conf.auth:
73+
method = 0x02 # username/password
74+
elif 0x00 in methods and self.serv.conf.unauth:
75+
method = 0x00 # unauthenticated
76+
if method is None:
77+
raise IOError('No supported authentication method was offered')
78+
# Send response
79+
self.conn.sendall(bytearray([0x05, method]))
80+
# Read authentication (optional)
81+
username = None
82+
password = None
83+
if method == 0x02:
84+
ver = recvall(self.conn, 1)[0]
85+
if ver != 0x01:
86+
raise IOError('Invalid auth packet version %i' % ver)
87+
ulen = recvall(self.conn, 1)[0]
88+
username = str(recvall(self.conn, ulen))
89+
plen = recvall(self.conn, 1)[0]
90+
password = str(recvall(self.conn, plen))
91+
# Send authentication response
92+
self.conn.sendall(bytearray([0x01, 0x00]))
93+
94+
# Read connect request
95+
(ver,cmd,rsv,atyp) = recvall(self.conn, 4)
96+
if ver != 0x05:
97+
raise IOError('Invalid socks version %i in connect request' % ver)
98+
if cmd != Command.CONNECT:
99+
raise IOError('Unhandled command %i in connect request' % cmd)
100+
101+
if atyp == AddressType.IPV4:
102+
addr = recvall(self.conn, 4)
103+
elif atyp == AddressType.DOMAINNAME:
104+
n = recvall(self.conn, 1)[0]
105+
addr = str(recvall(self.conn, n))
106+
elif atyp == AddressType.IPV6:
107+
addr = recvall(self.conn, 16)
108+
else:
109+
raise IOError('Unknown address type %i' % atyp)
110+
port_hi,port_lo = recvall(self.conn, 2)
111+
port = (port_hi << 8) | port_lo
112+
113+
# Send dummy response
114+
self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
115+
116+
cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
117+
self.serv.queue.put(cmdin)
118+
print('Proxy: ', cmdin)
119+
# Fall through to disconnect
120+
except Exception,e:
121+
traceback.print_exc(file=sys.stderr)
122+
self.serv.queue.put(e)
123+
finally:
124+
self.conn.close()
125+
126+
class Socks5Server(object):
127+
def __init__(self, conf):
128+
self.conf = conf
129+
self.s = socket.socket(conf.af)
130+
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
131+
self.s.bind(conf.addr)
132+
self.s.listen(5)
133+
self.running = False
134+
self.thread = None
135+
self.queue = Queue.Queue() # report connections and exceptions to client
136+
137+
def run(self):
138+
while self.running:
139+
(sockconn, peer) = self.s.accept()
140+
if self.running:
141+
conn = Socks5Connection(self, sockconn, peer)
142+
thread = threading.Thread(None, conn.handle)
143+
thread.daemon = True
144+
thread.start()
145+
146+
def start(self):
147+
assert(not self.running)
148+
self.running = True
149+
self.thread = threading.Thread(None, self.run)
150+
self.thread.daemon = True
151+
self.thread.start()
152+
153+
def stop(self):
154+
self.running = False
155+
# connect to self to end run loop
156+
s = socket.socket(self.conf.af)
157+
s.connect(self.conf.addr)
158+
s.close()
159+
self.thread.join()
160+

0 commit comments

Comments
 (0)