#!/usr/bin/env python """Minimal non-feature complete socks proxy""" import random import socket from SocketServer import StreamRequestHandler, ThreadingTCPServer from struct import pack, unpack import threading import sys def debug(s): print >>sys.stderr, 'socks.py: ', s def error(s): print >>sys.stderr, 'socks.py, ERROR: ', s class MyTCPServer(ThreadingTCPServer): allow_reuse_address = True def handle_timeout(self): raise Exception('timeout') CLOSE = object() VERSION = '\x05' NOAUTH = '\x00' USERPASS = '\x02' CONNECT = '\x01' UDP_ASSOCIATE = '\x03' IPV4 = '\x01' IPV6 = '\x04' DOMAIN_NAME = '\x03' SUCCESS = '\x00' password = None username = None allow_v4 = False def send(dest, msg): if msg == CLOSE: try: dest.shutdown(socket.SHUT_WR) except: pass dest.close() return 0 else: return dest.sendall(msg) def recv(source, buffer): data = source.recv(buffer) if data == '': return CLOSE else: return data def forward(source, dest, name): while True: data = recv(source, 4000) if data == CLOSE: send(dest, CLOSE) debug('%s hung up' % name) return # debug('Forwarding (%d) %r' % (len(data), data)) send(dest, data) def spawn_forwarder(source, dest, name): t = threading.Thread(target=forward, args=(source, dest, name)) t.daemon = True t.start() class SocksHandler(StreamRequestHandler): """Highly feature incomplete SOCKS 5 implementation""" def close_request(self): self.server.close_request(self.request) def read(self, n): data = '' while len(data) < n: extra = self.rfile.read(n) if extra == '': raise Exception('Connection closed') data += extra return data def handle(self): # IMRPOVEMENT: Report who requests are from in logging # IMPROVEMENT: Timeout on client debug('Connection - authenticating') version = self.read(1) if allow_v4 and version == '\x04': cmd = self.read(1) if cmd != CONNECT and cmd != UDP_ASSOCIATE: error('Only supports connect and udp-associate method not (%r) closing' % cmd) self.close_request() return raw_dest_port = self.read(2) dest_port, = unpack('>H', raw_dest_port) raw_dest_address = self.read(4) dest_address = '.'.join(map(str, unpack('>4B', raw_dest_address))) user_id = '' c = self.read(1) while c != '\0': user_id += c c = self.read(1) outbound_sock = socket.socket(socket.AF_INET) out_address = socket.getaddrinfo(dest_address,dest_port)[0][4] debug("Creating forwarder connection to %s:%d" % (out_address[0], out_address[1])) outbound_sock.connect(out_address) self.send_reply_v4(outbound_sock.getsockname()) spawn_forwarder(outbound_sock, self.request, 'destination') forward(self.request, outbound_sock, 'client') return if version != '\x05': error('Wrong version number (%r) closing...' % version) self.close_request() return nmethods = ord(self.read(1)) method_list = self.read(nmethods) global password global username if password == None and NOAUTH in method_list: self.send_no_auth_method() debug('Authenticated (no-auth)') elif USERPASS in method_list: self.send_user_pass_auth_method() auth_version = self.read(1) if auth_version != '\x01': error('Wrong sub-negotiation version number (%r) closing...' % version) self.close_request() return usr_len = ord(self.read(1)) usr_name = self.read(usr_len) pwd_len = ord(self.read(1)) pwd = self.read(pwd_len) if usr_name != username or pwd != password: error('Invalid username or password') self.close_request() return debug('Authenticated (user/password)') self.send_authenticated() else: error('Server only supports NOAUTH and user/pass') self.send_no_method() return # If we were authenticating it would go here version, cmd, zero, address_type = self.read(4) if version != '\x05': error('Wrong version number (%r) closing...' % version) self.close_request() elif cmd != CONNECT and cmd != UDP_ASSOCIATE: error('Only supports connect and udp-associate method not (%r) closing' % cmd) self.close_request() elif zero != '\x00': error('Mangled request. Reserved field (%r) is not null' % zero) self.close_request() if address_type == IPV4: raw_dest_address = self.read(4) dest_address = '.'.join(map(str, unpack('>4B', raw_dest_address))) elif address_type == IPV6: raw_dest_address = self.read(16) dest_address = ":".join(map(lambda x: hex(x)[2:],unpack('>8H',raw_dest_address))) elif address_type == DOMAIN_NAME: dns_length = ord(self.read(1)) dns_name = self.read(dns_length) dest_address = dns_name else: error('Unknown addressing (%r)' % address_type) self.close_request() raw_dest_port = self.read(2) dest_port, = unpack('>H', raw_dest_port) if address_type == IPV6: outbound_sock = socket.socket(socket.AF_INET6) else: outbound_sock = socket.socket(socket.AF_INET) try: out_address = socket.getaddrinfo(dest_address,dest_port)[0][4] except Exception, e: print e return if cmd == UDP_ASSOCIATE: debug("no UDP support yet, closing") return; debug("Creating forwarder connection to %s:%d" % (out_address[0], out_address[1])) try: outbound_sock.connect(out_address) except Exception, e: print e return if address_type == IPV6: self.send_reply6(outbound_sock.getsockname()) else: self.send_reply(outbound_sock.getsockname()) spawn_forwarder(outbound_sock, self.request, 'destination') try: forward(self.request, outbound_sock, 'client') except Exception,e: print e def send_reply_v4(self, (bind_addr, bind_port)): self.wfile.write('\0\x5a\0\0\0\0\0\0') self.wfile.flush() def send_reply(self, (bind_addr, bind_port)): bind_tuple = tuple(map(int, bind_addr.split('.'))) full_address = bind_tuple + (bind_port,) debug('Setting up forwarding port %r' % (full_address,)) msg = pack('>cccc4BH', VERSION, SUCCESS, '\x00', IPV4, *full_address) self.wfile.write(msg) def send_reply6(self, (bind_addr, bind_port, unused1, unused2)): bind_tuple = tuple(map(lambda x: int(x,16), bind_addr.split(':'))) full_address = bind_tuple + (bind_port,) debug('Setting up forwarding port %r' % (full_address,)) msg = pack('>cccc8HH', VERSION, SUCCESS, '\x00', IPV6, *full_address) self.wfile.write(msg) def send_no_method(self): self.wfile.write('\x05\xff') self.close_request() def send_no_auth_method(self): self.wfile.write('\x05\x00') self.wfile.flush() def send_user_pass_auth_method(self): self.wfile.write('\x05\x02') self.wfile.flush() def send_authenticated(self): self.wfile.write('\x01\x00') self.wfile.flush() if __name__ == '__main__': listen_port = 8002 i = 1 while i < len(sys.argv): if sys.argv[i] == '--username': username = sys.argv[i+1] i += 1 elif sys.argv[i] == '--password': password = sys.argv[i+1] i += 1 elif sys.argv[i] == '--port': listen_port = int(sys.argv[i+1]) i += 1 elif sys.argv[i] == '--allow-v4': allow_v4 = True else: if sys.argv[i] != '--help': debug('unknown option "%s"' % sys.argv[i]) print('usage: socks.py [--username --password ] [--port ]') sys.exit(1) i += 1 debug('Listening on port %d...' % listen_port) server = MyTCPServer(('localhost', listen_port), SocksHandler) server.timeout = 190 while True: server.handle_request()