Source code for socks.socks

# Copyright 2014, 2015, Nik Kinkel and David Johnston
# See LICENSE for licensing information

'''
.. topic:: Details

    OppySOCKSProtocol instances handle the initial SOCKS 5 handshake with
    client applications, passing the remote resource request to a stream,
    and then forwarding data back and forth between a circuit and a local
    application.

    An OppySOCKSProtocol instance is assigned to each incoming request on
    the SOCKS port, and upon successful handshake negotiaton a stream is
    created for the client application's request.

    Once the client has made a successful request, this object goes into the
    forwarding state in which it:

        - forwards all subsequent data from the client to the remote resource
          (through a circuit)
        - sends all data written to this object's writeData() method to the
          client application

    Throughout this process, this object communicates with the client
    application via transport, as usual for Twisted Protocol objects.

'''
import logging
import struct

from twisted.internet.protocol import Protocol, ServerFactory

from oppy.stream.stream import Stream
from oppy.util.exitrequest import ExitRequest
from oppy.util.tools import enum


VER = "\x05"
RSV = "\x00"


# supported authentication methods
NO_AUTH_REQUIRED        = "\x00"
NO_ACCEPTABLE_METHODS   = "\xFF"


# SOCKS commands
CONNECT = "\x01"
BIND    = "\x02"
UDP     = "\x03"


# SOCKS replies
SUCCEEDED                   = "\x00"
SOCKS_FAILURE               = "\x01"
NETWORK_FAILURE             = "\x02"
NETWORK_UNREACHABLE         = "\x03"
HOST_UNREACHABLE            = "\x04"
CONNECTION_REFUSED          = "\x05"
TTL_EXPIRED                 = "\x06"
COMMAND_NOT_SUPPORTED       = "\x07"
ADDRESS_TYPE_NOT_SUPPORTED  = "\x08"


# SOCKS address types
IPv4        = "\x01"
DOMAIN_NAME = "\x03"
IPv6        = "\x04"


State = enum(
    HANDSHAKE=0,
    REQUEST=1,
    FORWARDING=2,
)


[docs]class OppySOCKSProtocol(Protocol): '''Do SOCKS 5 handshake and forward local traffic to streams.''' def __init__(self): self.state = State.HANDSHAKE self.request = None # An `oppy.stream.stream` object over which the SOCKS client's data # will be forwarded. This will be set by the time that state has # become forwarding self.stream = None
[docs] def dataReceived(self, data): '''Either handle an incoming SOCKS handshake, make a new stream request, or forward application data on to a stream. Called when data is received from a local application. :param str data: data that was received ''' if self.state == State.HANDSHAKE: self._handleHandshake(data) elif self.state == State.REQUEST: self._handleRequest(data) else: self.stream.writeData(data)
[docs] def writeData(self, data): '''Write received *data* to local client application. :param str data: data to write ''' self.transport.write(data)
[docs] def closeFromStream(self): '''Lose this transports local connection. Called by the attached stream when we want to signal to a local application that this connection has closed. ''' self.transport.loseConnection()
def _handleHandshake(self, data): '''Check for supported versions and methods in the SOCKS 5 handshake. If we get a good version and a method we can support, send back a SUCCESS response to the client and advance to a state expecting a connection request. .. warning:: oppy only supports the NO_AUTH method. :param str data: handshake data ''' VER_LEN = 1 NMETHODS_LEN = 1 offset = 0 version = data[: VER_LEN] offset += VER_LEN nmethods = data[offset : offset + NMETHODS_LEN] offset += NMETHODS_LEN if version != VER: logging.error("Unsupported SOCKS version: {}.".format(version)) self.transport.loseConnection() return if nmethods == "\x00": logging.error("No SOCKS methods received.") self.transport.loseConnection() return _nmethods = struct.unpack("!B", nmethods)[0] methods = data[offset : offset + _nmethods] if NO_AUTH_REQUIRED not in methods: logging.error("SOCKS client does not support NO_AUTH method.") self.transport.write(VER + NO_ACCEPTABLE_METHODS) self.transport.loseConnection() return # for now, always use NO_AUTH method self.transport.write(VER + NO_AUTH_REQUIRED) self.state = State.REQUEST def _handleRequest(self, data): '''Process an incoming connection request and assign the request to an oppy.stream.stream.Stream. Send a SUCCESS reply to the client and advance to the FORWARDING state if we get a good request. :param str data: incoming request data to process ''' VER_LEN = 1 CMD_LEN = 1 RSV_LEN = 1 ADDR_TYPE_LEN = 1 offset = 0 ver = data[: VER_LEN] offset += VER_LEN cmd = data[offset : offset + CMD_LEN] offset += CMD_LEN rsv = data[offset : offset + RSV_LEN] offset += RSV_LEN addr_type = data[offset : offset + ADDR_TYPE_LEN] offset += ADDR_TYPE_LEN if ver != VER: logging.error("Unsupported SOCKS version: {}.".format(ver)) self._sendReply(SOCKS_FAILURE) self.transport.loseConnection() return if cmd != CONNECT: msg = "SOCKS client tried an unsupported request: {}." logging.error(msg.format(cmd)) self._sendReply(COMMAND_NOT_SUPPORTED) self.transport.loseConnection() return if rsv != RSV: msg = "Reserved byte was non-zero in SOCKS client request." logging.error(msg) self._sendReply(SOCKS_FAILURE) self.transport.loseConnection() return IPv4_LEN = 4 IPv6_LEN = 16 PORT_LEN = 2 if addr_type == IPv4: addr = data[offset : offset + IPv4_LEN] offset += IPv4_LEN port = port = data[offset : offset + PORT_LEN] self.request = ExitRequest(port, addr=addr) elif addr_type == DOMAIN_NAME: length = struct.unpack("!B", data[offset])[0] # hostname length is 1 byte offset += 1 host = data[offset : offset + length] offset += length port = data[offset : offset + PORT_LEN] self.request = ExitRequest(port, host=host) elif addr_type == IPv6: addr = data[offset : offset + IPv6_LEN] offset += IPv6_LEN port = data[offset : offset + PORT_LEN] self.request = ExitRequest(port, addr=addr) else: msg = "SOCKS client made a request with unsupported address " msg += "type: {}.".format(addr_type) self._sendReply(ADDRESS_TYPE_NOT_SUPPORTED) self.transport.loseConnection() return self.stream = Stream(self.request, self) self.state = State.FORWARDING self._sendReply(SUCCEEDED) def _sendReply(self, REP): '''Send a valid SOCKS 5 reply to a local client. .. note:: We currently always use 127.0.0.1 for the address and 0 for the port. :param str REP: reply to send ''' # 127.0.0.1 ADDR = "\x7f\x00\x00\x01" # port 0 for localhost PORT = "\x00\x00" self.transport.write(VER + REP + RSV + IPv4 + ADDR + PORT)
[docs] def connectionLost(self, reason): '''If we have been assigned a good stream, just log that we've lost the connection. :param reason reason: reason this connection was lost ''' # sometimes, if stream creation is attempted before circuit # manager exists, a SOCKS object will get a stream that has no # stream id. in this case just skip logging and closing that # stream and let it die if self.stream is not None and hasattr(self.stream, 'stream_id'): msg = "SOCKS on stream {} is done with its local connection." logging.debug(msg.format(self.stream.stream_id)) self.stream.closeFromSOCKS()
[docs] def connectionMade(self): '''Log that SOCKS has made a local connection and wait for an incoming handshake request. ''' logging.debug("SOCKS made a local connection.")
[docs]class OppySOCKSProtocolFactory(ServerFactory): '''Serve *OppySOCKSProtocol* instances.''' protocol = OppySOCKSProtocol