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 @@
@@ -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 @@
@@ -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