Skip to content

Commit b6ea3bc

Browse files
committed
Merge pull request #5911
6be3562 rpc-tests: Add proxy test (Wladimir J. van der Laan) 67a7949 privacy: Stream isolation for Tor (Wladimir J. van der Laan)
2 parents 71900b4 + 6be3562 commit b6ea3bc

File tree

9 files changed

+436
-79
lines changed

9 files changed

+436
-79
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+

src/init.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ std::string HelpMessage(HelpMessageMode mode)
301301
strUsage += HelpMessageOpt("-permitbaremultisig", strprintf(_("Relay non-P2SH multisig (default: %u)"), 1));
302302
strUsage += HelpMessageOpt("-port=<port>", strprintf(_("Listen for connections on <port> (default: %u or testnet: %u)"), 8333, 18333));
303303
strUsage += HelpMessageOpt("-proxy=<ip:port>", _("Connect through SOCKS5 proxy"));
304+
strUsage += HelpMessageOpt("-proxyrandomize", strprintf(_("Randomize credentials for every proxy connection. This enables Tor stream isolation (default: %u)"), 1));
304305
strUsage += HelpMessageOpt("-seednode=<ip>", _("Connect to a node to retrieve peer addresses, and disconnect"));
305306
strUsage += HelpMessageOpt("-timeout=<n>", strprintf(_("Specify connection timeout in milliseconds (minimum: 1, default: %d)"), DEFAULT_CONNECT_TIMEOUT));
306307
#ifdef USE_UPNP
@@ -351,7 +352,7 @@ std::string HelpMessage(HelpMessageMode mode)
351352
strUsage += HelpMessageOpt("-flushwallet", strprintf(_("Run a thread to flush wallet periodically (default: %u)"), 1));
352353
strUsage += HelpMessageOpt("-stopafterblockimport", strprintf(_("Stop running after importing blocks from disk (default: %u)"), 0));
353354
}
354-
string debugCategories = "addrman, alert, bench, coindb, db, lock, rand, rpc, selectcoins, mempool, net"; // Don't translate these and qt below
355+
string debugCategories = "addrman, alert, bench, coindb, db, lock, rand, rpc, selectcoins, mempool, net, proxy"; // Don't translate these and qt below
355356
if (mode == HMM_BITCOIN_QT)
356357
debugCategories += ", qt";
357358
strUsage += HelpMessageOpt("-debug=<category>", strprintf(_("Output debugging information (default: %u, supplying <category> is optional)"), 0) + ". " +
@@ -891,10 +892,10 @@ bool AppInit2(boost::thread_group& threadGroup)
891892
}
892893
}
893894

894-
CService addrProxy;
895+
proxyType addrProxy;
895896
bool fProxy = false;
896897
if (mapArgs.count("-proxy")) {
897-
addrProxy = CService(mapArgs["-proxy"], 9050);
898+
addrProxy = proxyType(CService(mapArgs["-proxy"], 9050), GetArg("-proxyrandomize", true));
898899
if (!addrProxy.IsValid())
899900
return InitError(strprintf(_("Invalid -proxy address: '%s'"), mapArgs["-proxy"]));
900901

@@ -904,14 +905,14 @@ bool AppInit2(boost::thread_group& threadGroup)
904905
fProxy = true;
905906
}
906907

907-
// -onion can override normal proxy, -noonion disables tor entirely
908+
// -onion can override normal proxy, -noonion disables connecting to .onion entirely
908909
if (!(mapArgs.count("-onion") && mapArgs["-onion"] == "0") &&
909910
(fProxy || mapArgs.count("-onion"))) {
910-
CService addrOnion;
911+
proxyType addrOnion;
911912
if (!mapArgs.count("-onion"))
912913
addrOnion = addrProxy;
913914
else
914-
addrOnion = CService(mapArgs["-onion"], 9050);
915+
addrOnion = proxyType(CService(mapArgs["-onion"], 9050), GetArg("-proxyrandomize", true));
915916
if (!addrOnion.IsValid())
916917
return InitError(strprintf(_("Invalid -onion address: '%s'"), mapArgs["-onion"]));
917918
SetProxy(NET_TOR, addrOnion);

0 commit comments

Comments
 (0)