Wladimir J. van der Laan
9 years ago
3 changed files with 307 additions and 0 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