Source code for trixy.proxy

'''
The Trixy proxy inputs speak a variety of common proxy protocols, such
as SOCKS4, SOCKS4a, and SOCKS5. Their default behavior is to act as a
normal proxy and open a connection to the desired endpoint. However,
this behavior can be overridden to create different results.

Additionally, the proxy outputs allow a connection to be subsequently
made to a proxy server. This allows intercepted traffic to be easily
routed on networks that require a proxy. It also makes it easier to
route traffic into the Tor network.
'''
import struct
import socket
import trixy


[docs]class Socks4Input(trixy.TrixyInput): ''' Implements the SOCKS4 protocol as defined in this document: http://www.openssh.com/txt/socks4.protocol ''' # TODO: decide if binding will be allowed. Probably off by default # but can be enabled by an option in __init__? def __init__(self, sock, addr): super().__init__(sock, addr) self.first_packet = True def handle_packet_down(self, data): if self.first_packet: self.handle_proxy_request(data) self.first_packet = False return self.forward_packet_down(data)
[docs] def handle_proxy_request(self, data): ''' In SOCKS4, the first packet in a connection is a request to either initiate a connection to a remote host and port, or it is a request to bind a port. This method is responsible for processing those requests. ''' if data.startswith(b'\x04\x01'): # CONNECT request port = struct.unpack('!H', data[2:4])[0] addr = socket.inet_ntoa(data[4:8]) userid = data[8:-1] self.handle_connect_request(addr, port, userid) elif data.startswith(b'\x04\x02'): # BIND request pass # TODO: implement binding behavior; see note above.
[docs] def handle_connect_request(self, addr, port, userid): ''' The application connecting to this SOCKS4 input has requested that a connection be made to a remote host. At this point, that request can be accepted, modified, or declined. The default behavior is to accept the request as-is. ''' self.connect_node(trixy.TrixyOutput(addr, port)) # TODO: need functionality to detect if the connection fails to # notify the application accordingly. self.reply_request_granted(addr, port)
[docs] def reply_request_granted(self, addr, port): ''' Send a reply stating that the connection or bind request has been granted and that the connection or bind attempt was successfully completed. ''' # 90 is the response for a granted request self.send(struct.pack('!BBH4s', 0x00, 90, port, socket.inet_aton(addr)))
[docs] def reply_request_failed(self, addr, port): ''' Send a reply stating that the request was rejected (perhaps due to a firewall rule forbidding the connection or binding) or that it failed (i.e., the remote host could not be connected to or the requested port could not be bound). ''' # 91 is the response for a rejected or failed request self.send(struct.pack('!BBH4s', 0x00, 91, port, socket.inet_aton(addr)))
[docs] def reply_request_rejected(self, addr, port): ''' Send a reply saying that the request was rejected because the SOCKS server could not connect to the client's identd server. ''' # 92 is the response for a request being rejected because the SOCKS # server cannot connect to identd on the client. self.send(struct.pack('!BBH4s', 0x00, 92, port, socket.inet_aton(addr)))
[docs] def reply_request_rejected_id_mismatch(self, addr, port): ''' Send a reply saying that the request was rejected because the SOCKS server was sent an ID by the client that did not match the ID returned by identd on the client's computer. ''' # 93 is the response for rejections due to the client program and # identd reporting different user-ids. self.send(struct.pack('!BBH4s', 0x00, 93, port, socket.inet_aton(addr)))
[docs]class Socks4aInput(Socks4Input): ''' Implements the SOCKS4a protocol, which is the same as the SOCKS4 protocol except for the addition of DNS resolution as described here: http://www.openssh.com/txt/socks4a.protocol ''' # TODO: decide if binding will be allowed. Probably off by default # but can be enabled by an option in __init__? def __init__(self, sock, addr): super().__init__(sock, addr) print('Got connect') self.first_packet = True
[docs] def handle_proxy_request(self, data): ''' In SOCKS4, the first packet in a connection is a request to either initiate a connection to a remote host and port, or it is a request to bind a port. This method is responsible for processing those requests. ''' print('handle_proxy_request: ', data) if data.startswith(b'\x04\x01'): # CONNECT request port = struct.unpack('!H', data[2:4])[0] addr = socket.inet_ntoa(data[4:8]) userid = data[8:-1] # TODO: test if the address is invalid, which suggests that # we need to resolve the hostname contained later in the data. print(' ', addr, ':', port, ' username: ', userid) self.handle_connect_request(addr, port, userid) elif data.startswith(b'\x04\x02'): # BIND request pass # TODO: implement binding behavior; see note above.
[docs] def handle_connect_request(self, addr, port, userid): ''' The application connecting to this SOCKS4 input has requested that a connection be made to a remote host. At this point, that request can be accepted, modified, or declined. The default behavior is to accept the request as-is. ''' print('Handling a connect request:', addr, ':', port, userid) self.connect_node(trixy.TrixyOutput(addr, port)) # TODO: need functionality to detect if the connection fails to # notify the application accordingly. self.reply_request_granted(addr, port)
[docs]class Socks5Input(trixy.TrixyInput): ''' Implements the SOCKS5 protocol as defined in RFC1928. At present, only CONNECT requests are supported. ''' STATE_WAITING_FOR_METHODS = 0 STATE_WAITING_FOR_AUTH = 1 STATE_WAITING_FOR_REQUEST = 2 STATE_PROXY_ACTIVE = 255 SUPPORTED_METHODS = [b'\x00'] def __init__(self, sock, addr): super().__init__(sock, addr) self.state = self.STATE_WAITING_FOR_METHODS def handle_packet_down(self, data): if self.state == self.STATE_PROXY_ACTIVE: self.forward_packet_down(data) elif self.state == self.STATE_WAITING_FOR_METHODS: if data.startswith(b'\x05') and len(data) > 2: nmethods = data[1] methods = data[2:] # Truncate method list if nmethods smaller, but attempt to # work regardless of a method count and actual count mismatch. # TODO: truncating is fingerpritable; is this desired? # Is there another implementation to copy? if len(methods) > nmethods: methods = methods[0:nmethods] self.handle_method_select(methods) elif self.state == self.STATE_WAITING_FOR_REQUEST: if not data.startswith(b'\x05'): # Invalid self.close() # Disconnect return if data[1] == 0x01: # CONNECT request if data[3] == 0x01: # IPv4 address dst_addr = socket.inet_ntoa(data[4:8]) port = struct.unpack('!H', data[8:10])[0] elif data[3] == 0x03: # Domain name dst_addr_len = data[4] dst_addr = data[5:5 + dst_addr_len].decode('ascii') port = data[5 + dst_addr_len:7 + dst_addr_len] port = struct.unpack('!H', port)[0] elif data[3] == 0x04: # IPv6 address dst_addr = socket.inet_ntop(socket.AF_INET6, data[4:20]) port = struct.unpack('!H', data[20:22])[0] else: self.close() # Disconnect; unsupported address type return self.handle_connect_request(dst_addr, port, data[3:4]) else: self.close() # Disconnect return
[docs] def handle_method_select(self, methods): ''' Select the preferred authentication method from the list of client-supplied supported methods. The byte object of length one should be sent to self.reply_method to notify the client of the method selection. ''' for method in self.SUPPORTED_METHODS: if method in methods: self.reply_method(method) self.state = self.STATE_WAITING_FOR_REQUEST else: self.close() # Disconnect
[docs] def handle_connect_request(self, addr, port, addrtype): ''' The application connecting to this SOCKS4 input has requested that a connection be made to a remote host. At this point, that request can be accepted, modified, or declined. The default behavior is to accept the request as-is. ''' self.connect_node(trixy.TrixyOutput(addr, port)) # TODO: need functionality to detect if the connection fails to # notify the application accordingly. self.reply_request_granted(addr, port, addrtype) self.state = self.STATE_PROXY_ACTIVE
[docs] def reply_request_granted(self, addr, port, addrtype): ''' Send a reply stating that the connection or bind request has been granted and that the connection or bind attempt was successfully completed. ''' pkt = b'\x05\x00\x00' + addrtype if addrtype == b'\x01': # IPv4 pkt += socket.inet_aton(addr) elif addrtype == b'\x03': # Domain name pkt += bytes((len(addr),)) pkt += addr.encode('ascii') elif addrtype == b'\x04': # IPv6 pkt += socket.inet_pton(socket.AF_INET6, addr) else: raise Exception('Invalid address mode given') pkt += struct.pack('!H', port) self.send(pkt)
[docs] def reply_method(self, method): ''' Send a reply to the user letting them know which authentication method the server has selected. If the method 0xff is selected, close the connection because no method is supported. ''' self.send(b'\x05' + method) if method == b'\xff': self.handle_close()
class Socks4Output(trixy.TrixyOutput): # TODO: implement assumed connections (useful for SOCKS over SSL) supports_assumed_connections = False class Socks4aOutput(trixy.TrixyOutput): # TODO: implement assumed connections (useful for SOCKS over SSL) supports_assumed_connections = False
[docs]class Socks5Output(trixy.TrixyOutput): ''' Implements the SOCKS5 protocol as defined in RFC1928. ''' STATE_NONE = 0 STATE_WAITING_FOR_SERVER_METHOD_SELECT = 1 STATE_WAITING_FOR_BIND_RESPONSE = 251 STATE_PROXY_ACTIVE = 254 STATE_PROXY_DISABLED = 255 IP_TYPE_V4 = 1 IP_TYPE_V6 = 4 IP_TYPE_DOMAIN = 3 supported_auth_methods = [] state = STATE_NONE # TODO: implement assumed connections (useful for SOCKS over SSL) supports_assumed_connections = False def __init__(self, host, port, autoconnect=True, proxyhost='127.0.0.1', proxyport=1080): super().__init__(proxyhost, proxyport, autoconnect) self.dsthost = dsthost = host self.dstport = port self.supported_auth_methods = [b'\x00'] self.state = self.STATE_NONE self.downstream_buffer = b'' # Check if the given host is an IP address try: self.dsthost_bytes = socket.inet_pton(socket.AF_INET, dsthost) self.ip_type = self.IP_TYPE_V4 except socket.error: try: self.dsthost_bytes = socket.inet_pton(socket.AF_INET6, dsthost) self.ip_type = self.IP_TYPE_V6 except socket.error: self.dsthost_bytes = (bytes((len(dsthost),)) + bytes(dsthost, 'ascii')) self.ip_type = self.IP_TYPE_DOMAIN def add_supported_auth_method(self, method): if isinstance(method, (int, float)): method = bytes((method,)) elif not isinstance(method, bytes): raise TypeError('The supplied method must be a bytes object') if len(method) != 1: raise ValueError('The supplied method must be a single byte') if method not in self.supported_auth_methods: self.supported_auth_methods.append(method) def remove_supported_auth_method(self, method): if isinstance(method, (int, float)): method = bytes((method,)) elif not isinstance(method, bytes): raise TypeError('The supplied method must be a bytes object') if len(method) != 1: raise ValueError('The supplied method must be a single byte') while method in self.supported_auth_methods: index = self.supported_auth_methods.index(method) self.supported_auth_methods.pop(index) def handle_connect(self): nummethods = len(self.supported_auth_methods) self.send(struct.pack('!BB%ip' % nummethods, 5, nummethods, b''.join(self.supported_auth_methods))) self.set_state(self.STATE_WAITING_FOR_SERVER_METHOD_SELECT) def handle_packet_down(self, data): print('socks5:', data) if self.state == self.STATE_PROXY_ACTIVE: print('socks5-active:', data) self.send(data) else: self.downstream_buffer += data def handle_packet_up(self, data): if self.state == self.STATE_PROXY_DISABLED: return elif self.state == self.STATE_PROXY_ACTIVE: self.forward_packet_up(data) elif self.state == self.STATE_WAITING_FOR_SERVER_METHOD_SELECT: if len(data) == 2 and data.startswith(b'\x05'): selected_auth_method = data[1:2] if selected_auth_method not in self.supported_auth_methods: # TODO: check the RFC for graceful disconnection approach raise SocksProtocolError('Server selected bad auth method') # Authentication complete; attempt the connection self.send(b'\x05\x01\x00' + bytes((self.ip_type,)) + self.dsthost_bytes + struct.pack('!H', self.dstport)) self.set_state(self.STATE_WAITING_FOR_BIND_RESPONSE) elif self.state == self.STATE_WAITING_FOR_BIND_RESPONSE: if len(data) > 7 and data.startswith(b'\x05'): response = data[1] if response == 0: # Success self.set_state(self.STATE_PROXY_ACTIVE) self.send(self.downstream_buffer) elif response < 9: self.handle_close() else: SocksProtocolError('Unassigned bind response used') def set_state(self, state): old_state = self.state self.state = state self.handle_state_change(oldstate=old_state, newstate=state)
[docs] def handle_state_change(self, oldstate, newstate): ''' Be able to process events when they occur. It allows easier detection of when events occur if it is desired to implement different responses. It also allows detection of when the proxy is ready for use and can be used to use assume_connectecd to transfer control to a TrixyOutput. :param int oldstate: The old state number. :param int newstate: The new state number. ''' pass
[docs]class SocksProtocolError(Exception): ''' Someone sent some invalid data on the wire, and this is how to deal with it. ''' pass