Browse Source
0.136be3562
rpc-tests: Add proxy test (Wladimir J. van der Laan)67a7949
privacy: Stream isolation for Tor (Wladimir J. van der Laan)
Wladimir J. van der Laan
10 years ago
9 changed files with 436 additions and 79 deletions
@ -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() |
||||||
|
|
@ -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 new issue