-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
6be3562 rpc-tests: Add proxy test (Wladimir J. van der Laan) 67a7949 privacy: Stream isolation for Tor (Wladimir J. van der Laan)
- Loading branch information
Showing
9 changed files
with
436 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.