[Pkg-privacy-commits] [obfsproxy] 249/353: Use SOCKS5 instead of SOCKS4

Ximin Luo infinity0 at moszumanska.debian.org
Sat Aug 22 13:02:06 UTC 2015


This is an automated email from the git hooks/post-receive script.

infinity0 pushed a commit to branch master
in repository obfsproxy.

commit cde306e9c8098359bc828c21945906e8829f6995
Author: Yawning Angel <yawning at schwanenlied.me>
Date:   Mon Mar 3 09:54:28 2014 +0000

    Use SOCKS5 instead of SOCKS4
    
    A straight forward SOCKS5 server implementation with the following caveats:
    
     * It is tightly coupled with the obfsproxy channel/GenericNetworkProtocol code
     * It's idea of RFC1929 is the ugly hack in pt-spec.txt
     * Only supports CONNECT
     * Only supports IPv4/IPv6 addresses, because DNS leaks make me sad
---
 ChangeLog                             |   1 +
 obfsproxy/managed/client.py           |   2 +-
 obfsproxy/network/launch_transport.py |   2 +-
 obfsproxy/network/socks.py            | 427 +++++++++++++++++++++++++---------
 4 files changed, 324 insertions(+), 108 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 8fb2b21..a3dab45 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,6 +3,7 @@ Changes in version 0.2.7 - UNRELEASED
    exchange outside of the main event loop.  Patch by Yawning Angel.
  - Support gmpy2 if it is available in addition to gmpy.
    Patch by Yawning Angel.
+ - Support SOCKS5 instead of SOCKS4.  Patch by Yawning Angel.
 
 
 Changes in version 0.2.6 - 2014-02-03
diff --git a/obfsproxy/managed/client.py b/obfsproxy/managed/client.py
index 8169c40..bce5463 100644
--- a/obfsproxy/managed/client.py
+++ b/obfsproxy/managed/client.py
@@ -56,7 +56,7 @@ def do_managed_client():
 
         should_start_event_loop = True
         log.debug("Successfully launched '%s' at '%s'" % (transport, log.safe_addr_str(str(addrport))))
-        ptclient.reportMethodSuccess(transport, "socks4", addrport, None, None)
+        ptclient.reportMethodSuccess(transport, "socks5", addrport, None, None)
 
     ptclient.reportMethodsEnd()
 
diff --git a/obfsproxy/network/launch_transport.py b/obfsproxy/network/launch_transport.py
index 8ffa6b4..b9d7e9b 100644
--- a/obfsproxy/network/launch_transport.py
+++ b/obfsproxy/network/launch_transport.py
@@ -35,7 +35,7 @@ def launch_transport_listener(transport, bindaddr, role, remote_addrport, pt_con
     listen_port = int(bindaddr[1]) if bindaddr else 0
 
     if role == 'socks':
-        factory = socks.SOCKSv4Factory(transport_class, pt_config)
+        factory = socks.SOCKSv5Factory(transport_class, pt_config)
     elif role == 'ext_server':
         assert(remote_addrport and ext_or_cookie_file)
         factory = extended_orport.ExtORPortServerFactory(remote_addrport, ext_or_cookie_file, transport, transport_class, pt_config)
diff --git a/obfsproxy/network/socks.py b/obfsproxy/network/socks.py
index c7e8f47..e9b3bc1 100644
--- a/obfsproxy/network/socks.py
+++ b/obfsproxy/network/socks.py
@@ -1,15 +1,68 @@
-import csv
-
-from twisted.protocols import socks
+from twisted.internet import reactor, protocol
 from twisted.internet.protocol import Factory
 
 import obfsproxy.common.log as logging
 import obfsproxy.network.network as network
 import obfsproxy.transports.base as base
 
+import csv
+import socket
+import struct
+
 log = logging.get_obfslogger()
 
-def split_socks_args(args_str):
+"""
+SOCKS5 Server:
+
+This is a SOCKS5 server written specifically for Tor Pluggable Transports.  It
+is compliant with RFC 1928 and RFC 1929 with the following exceptions.
+
+ * GSSAPI Authentication is not and will not ever be supported.
+ * The SOCKS5 CMDs BIND and UDP ASSOCIATE are not supported.
+ * The SOCKS5 ATYP DOMAINNAME is not supported.  This is a intentional design
+   choice.  While it would be trivial to support this, Pluggable Transports
+   hitting up DNS is a DNS leak, and it is Tor's problem to pass acceptable
+   addresses.
+"""
+
+#
+# SOCKS Server States
+#
+_SOCKS_ST_READ_METHODS = 1
+_SOCKS_ST_AUTHENTICATING = 2
+_SOCKS_ST_READ_REQUEST = 3
+_SOCKS_ST_CONNECTING = 4
+_SOCKS_ST_ESTABLISHED = 5
+
+#
+# SOCKS5 Constants
+#
+_SOCKS_VERSION = 0x05
+_SOCKS_AUTH_NO_AUTHENTICATION_REQUIRED = 0x00
+#_SOCKS_AUTH_GSSAPI = 0x01
+_SOCKS_AUTH_USERNAME_PASSWORD = 0x02
+_SOCKS_AUTH_NO_ACCEPTABLE_METHODS = 0xFF
+_SOCKS_CMD_CONNECT = 0x01
+#_SOCKS_CMD_BIND = 0x02
+#_SOCKS_CMD_UDP_ASSOCIATE = 0x03
+_SOCKS_ATYP_IP_V4 = 0x01
+#_SOCKS_ATYP_DOMAINNAME = 0x03
+_SOCKS_ATYP_IP_V6 = 0x04
+_SOCKS_RSV = 0x00
+_SOCKS_REP_SUCCEDED = 0x00
+_SOCKS_REP_GENERAL_FAILURE = 0x01
+_SOCKS_REP_CONNECTION_NOT_ALLOWED = 0x02
+_SOCKS_REP_NETWORK_UNREACHABLE = 0x03
+_SOCKS_REP_HOSOCKS_ST_UNREACHABLE = 0x04
+_SOCKS_REP_CONNECTION_REFUSED = 0x05
+_SOCKS_REP_TTL_EXPIRED = 0x06
+_SOCKS_REP_COMMAND_NOT_SUPPORTED = 0x07
+_SOCKS_REP_ADDRESS_TYPE_NOT_SUPPORTED = 0x08
+_SOCKS_RFC1929_VER = 0x01
+_SOCKS_RFC1929_SUCCESS = 0x00
+_SOCKS_RFC1929_FAIL = 0x01
+
+def _split_socks_args(args_str):
     """
     Given a string containing the SOCKS arguments (delimited by
     semicolons, and with semicolons and backslashes escaped), parse it
@@ -17,146 +70,308 @@ def split_socks_args(args_str):
     """
     return csv.reader([args_str], delimiter=';', escapechar='\\').next()
 
-class MySOCKSv4Outgoing(socks.SOCKSv4Outgoing, network.GenericProtocol):
-    """
-    Represents a downstream connection from the SOCKS server to the
-    destination.
-
-    It monkey-patches socks.SOCKSv4Outgoing, because we need to pass
-    our data to the pluggable transport before proxying them
-    (Twisted's socks module did not support that).
+class SOCKSv5Outgoing(network.GenericProtocol):
 
-    Attributes:
-    circuit: The circuit this connection belongs to.
-    buffer: Buffer that holds data that can't be proxied right
-            away. This can happen because the circuit is not yet
-            complete, or because the pluggable transport needs more
-            data before deciding what to do.
-    """
+    name = None
 
-    def __init__(self, socksProtocol):
-        """
-        Constructor.
+    _socks = None
 
-        'socksProtocol' is a 'SOCKSv4Protocol' object.
-        """
+    def __init__(self, socks):
         self.name = "socks_down_%s" % hex(id(self))
-        self.socksProtocol = socksProtocol
+        self._socks = socks
+
+        network.GenericProtocol.__init__(self, socks.circuit)
 
-        network.GenericProtocol.__init__(self, socksProtocol.circuit)
-        return super(MySOCKSv4Outgoing, self).__init__(socksProtocol)
+    def connectionMade(self):
+        self._socks._other_conn = self
+        self._socks.setup_circuit()
+        # XXX: The transport should do this after handshaking
+        self._socks.send_reply(_SOCKS_REP_SUCCEDED)
 
     def dataReceived(self, data):
         log.debug("%s: Received %d bytes." % (self.name, len(data)))
 
-        # If the circuit was not set up, set it up now.
-        if not self.circuit.circuitIsReady():
-            self.socksProtocol.set_up_circuit()
-
+        assert self.circuit.circuitIsReady()
         self.buffer.write(data)
         self.circuit.dataReceived(self.buffer, self)
 
-    def close(self): # XXX code duplication
-        """
-        Close the connection.
-        """
-        if self.closed:
-            return # NOP if already closed
-
-        log.debug("%s: Closing connection." % self.name)
-
-        self.transport.loseConnection()
-        self.closed = True
-
-    def connectionLost(self, reason):
-        network.GenericProtocol.connectionLost(self, reason)
-
-# Monkey patches socks.SOCKSv4Outgoing with our own class.
-socks.SOCKSv4Outgoing = MySOCKSv4Outgoing
-
-class SOCKSv4Protocol(socks.SOCKSv4, network.GenericProtocol):
+class SOCKSv5Protocol(network.GenericProtocol):
     """
-    Represents an upstream connection from a SOCKS client to our SOCKS
-    server.
+    Represents an upstream connection from a SOCKS client to our SOCKS server.
 
-    It overrides socks.SOCKSv4 because py-obfsproxy's connections need
-    to have a circuit and obfuscate traffic before proxying it.
+    Attributes:
+    circuit: The curcuit object this connection belongs to.
+    buffer: Buffer that holds data waiting to be processed.
     """
 
+    name = None
+
+    _state = None
+    _auth_method = None
+    _other_conn = None
+
     def __init__(self, circuit):
         self.name = "socks_up_%s" % hex(id(self))
+        self._state = _SOCKS_ST_READ_METHODS
 
         network.GenericProtocol.__init__(self, circuit)
-        socks.SOCKSv4.__init__(self)
 
-    def dataReceived(self, data):
-        """
-        Received some 'data'. They might be SOCKS handshake data, or
-        actual upstream traffic. Figure out what it is and either
-        complete the SOCKS handshake or proxy the traffic.
-        """
-
-        # SOCKS handshake not completed yet: let the overriden socks
-        # module complete the handshake.
-        if not self.otherConn:
-            log.debug("%s: Received SOCKS handshake data." % self.name)
-            return socks.SOCKSv4.dataReceived(self, data)
+    def connectionLost(self, reason):
+        network.GenericProtocol.connectionLost(self, reason)
 
+    def dataReceived(self, data):
         log.debug("%s: Received %d bytes." % (self.name, len(data)))
         self.buffer.write(data)
 
+        if self._state == _SOCKS_ST_READ_METHODS:
+            self._process_method_select()
+        elif self._state == _SOCKS_ST_AUTHENTICATING:
+            self._process_authentication()
+        elif self._state == _SOCKS_ST_READ_REQUEST:
+            self._process_request()
+        elif self._state == _SOCKS_ST_CONNECTING:
+            # This is NEVER supposed to happen, we could be nice and buffer the
+            # data, but people that fail to implement a SOCKS5 client deserve to
+            # be laughed at.
+            log.warning("%s: Client sent data when connecting" % self.name)
+            self.transport.loseConnection()
+        elif self._state == _SOCKS_ST_ESTABLISHED:
+            assert self.circuit.circuitIsReady()
+            self.circuit.dataReceived(self.buffer, self)
+        else:
+            log.warning("%s: Invalid state in SOCKS5 Server: '%d'" % (self.name, self._state))
+            self.transport.loseConnection()
+
+    def _process_method_select(self):
         """
-        If we came here with an incomplete circuit, it means that we
-        finished the SOCKS handshake and connected downstream. Set up
-        our circuit and start proxying traffic.
-        """
-        if not self.circuit.circuitIsReady():
-            self.set_up_circuit()
-
-        self.circuit.dataReceived(self.buffer, self)
-
-    def set_up_circuit(self):
-        """
-        Set the upstream/downstream SOCKS connections on the circuit.
+        Parse Version Identifier/Method Selection Message, and send a response
         """
 
-        assert(self.otherConn)
-        self.circuit.setDownstreamConnection(self.otherConn)
-        self.circuit.setUpstreamConnection(self)
-
-    def authorize(self, code, server, port, user):
+        msg = self.buffer.peek()
+        if len(msg) < 2:
+            return
+        ver, nmethods = struct.unpack("BB", msg[0:2])
+        if ver != _SOCKS_VERSION:
+            log.warning("%s: Invalid SOCKS version: '%d'" % (self.name, ver))
+            self.transport.loseConnection()
+            return
+        if nmethods == 0:
+            log.warning("%s: No Authentication method present" % self.name)
+            self.transport.loseConnection()
+            return
+
+        #
+        # Ensure that the entire message is present, and find the "best"
+        # authentication method.
+        #
+        # In a perfect world, the transport will specify that authentication
+        # is required, but since tor will do the work for us, this will
+        # suffice.
+        #
+
+        if len(msg) < 2 + nmethods:
+            return
+        methods = msg[2:2 + nmethods]
+        if chr(_SOCKS_AUTH_USERNAME_PASSWORD) in methods:
+            log.debug("%s: Username/Password Authentication" % self.name)
+            self._auth_method = _SOCKS_AUTH_USERNAME_PASSWORD
+        elif chr(_SOCKS_AUTH_NO_AUTHENTICATION_REQUIRED) in methods:
+            log.debug("%s: No Authentication Required" % self.name)
+            self._auth_method = _SOCKS_AUTH_NO_AUTHENTICATION_REQUIRED
+        else:
+            log.warning("%s: No Compatible Authentication method" % self.name)
+            self._auth_method = _SOCKS_AUTH_NO_ACCEPTABLE_METHODS
+
+        # Ensure there is no trailing garbage
+        self.buffer.drain(2 + nmethods)
+        if len(self.buffer) > 0:
+            log.warning("%s: Peer sent trailing garbage after method select" % self.name)
+            self.transport.loseConnection()
+            return
+
+        # Send Method Selection Message
+        self.transport.write(struct.pack("BB", _SOCKS_VERSION, self._auth_method))
+        if self._auth_method == _SOCKS_AUTH_NO_ACCEPTABLE_METHODS:
+            self.transport.loseConnection()
+            return
+
+        self._state = _SOCKS_ST_AUTHENTICATING
+
+    def _process_authentication(self):
         """
-        (Overriden)
-        Accept or reject a SOCKS client that wants to connect to
-        'server':'port', with the SOCKS4 username 'user'.
+        Handle client data when authenticating
         """
 
-        if not user: # No SOCKS arguments were specified.
-            return True
-
-        # If the client sent us SOCKS arguments, we must parse them
-        # and send them to the appropriate transport.
-        log.debug("Got '%s' as SOCKS arguments." % user)
-
+        if self._auth_method == _SOCKS_AUTH_NO_AUTHENTICATION_REQUIRED:
+            self._state = _SOCKS_ST_READ_REQUEST
+            self._process_request()
+        elif self._auth_method == _SOCKS_AUTH_USERNAME_PASSWORD:
+            self._process_rfc1929_request()
+        else:
+            log.warning("%s: Peer sent data when we failed to negotiate auth" % (self.name))
+            self.buffer.drain()
+            self.transport.loseConnection()
+
+    def _process_rfc1929_request(self):
+        msg = self.buffer.peek()
+        if len(msg) < 2:
+            return
+
+        # Parse VER, ULEN
+        ver, ulen = struct.unpack("BB", msg[0:2])
+        if ver != _SOCKS_RFC1929_VER:
+            log.warning("%s: Invalid RFC1929 version: '%d'" % (self.name, ver))
+            self._send_rfc1929_reply(False)
+            return
+        if ulen == 0:
+            log.warning("%s: Username length is 0" % self.name)
+            self._send_rfc1929_reply(False)
+            return
+        if len(msg) < 2 + ulen:
+            return
+        uname = msg[2:2 + ulen]
+        if len(msg) < 2 + ulen + 1:
+            return
+        plen = struct.unpack("B", msg[2 + ulen])[0]
+        if len(msg) < 2 + ulen + 1 + plen:
+            return
+        if plen == 0:
+            log.warning("%s: Password length is 0" % self.name)
+            self._send_rfc1929_reply(False)
+            return
+        passwd = msg[2 + ulen + 1:2 + ulen + 1 + plen]
+
+        #
+        # Begin the pt-spec specific SOCKS auth braindamage:
+        #
+
+        args = uname
+        if plen > 1 or ord(passwd[0]) != 0:
+            args += passwd
         try:
-            socks_args = split_socks_args(user)
-        except csv.Error, err:
+            split_args = _split_socks_args(args)
+        except csvError, err:
             log.warning("split_socks_args failed (%s)" % str(err))
-            return False
-
+            self._send_rfc1929_reply(False)
+            return
         try:
-            self.circuit.transport.handle_socks_args(socks_args)
+            self.circuit.transport.handle_socks_args(split_args)
         except base.SOCKSArgsError:
-            return False # Transports should log the issue themselves
+            self._send_rfc1929_reply(False)
+            return
+
+        # Ensure there is no trailing garbage
+        self.buffer.drain(2 + ulen + 1 + plen)
+        if len(self.buffer) > 0:
+            log.warning("%s: Peer sent trailing garbage after RFC1929 auth" % self.name)
+            self.transport.loseConnection()
+            return
+
+        self._send_rfc1929_reply(True)
+
+    def _send_rfc1929_reply(self, success):
+        if success == True:
+            self.transport.write(struct.pack("BB", 1, _SOCKS_RFC1929_SUCCESS))
+            self._state = _SOCKS_ST_READ_REQUEST
+        else:
+            self.transport.write(struct.pack("BB", 1, _SOCKS_RFC1929_FAIL))
+            self.transport.loseConnection()
+
+    def _process_request(self):
+        """
+        Parse the client request, and setup the TCP/IP connection
+        """
 
-        return True
+        msg = self.buffer.peek()
+        if len(msg) < 4:
+            return
+
+        ver, cmd, rsv, atyp = struct.unpack("BBBB", msg[0:4])
+        if ver != _SOCKS_VERSION:
+            log.warning("%s: Invalid SOCKS version: '%d'" % (self.name, ver))
+            self.send_reply(_SOCKS_REP_GENERAL_FAILURE)
+            return
+        if cmd != _SOCKS_CMD_CONNECT:
+            log.warning("%s: Invalid SOCKS command: '%d'" % (self.name, cmd))
+            self.send_reply(_SOCKS_REP_COMMAND_NOT_SUPPORTED)
+            return
+        if rsv != _SOCKS_RSV:
+            log.warning("%s: Invalid SOCKS RSV: '%d'" % (self.name, rsv))
+            self.send_reply(_SOCKS_REP_GENERAL_FAILURE)
+            return
+
+        # Deal with the address
+        addr = None
+        if atyp == _SOCKS_ATYP_IP_V4:
+            if len(msg) < 4 + 4 + 2:
+                return
+            addr = socket.inet_ntoa(msg[4:8])
+            self.buffer.drain(4 + 4)
+            pass
+        elif atyp == _SOCKS_ATYP_IP_V6:
+            if len(msg) < 4 + 16 + 2:
+                return
+            addr = socket.inet_ntop(socket.AF_INET6, msg[4:16])
+            self.buffer.drain(4 + 16)
+            pass
+        else:
+            log.warning("%s: Invalid SOCKS address type: '%d'" % (self.name, atyp))
+            self.send_reply(_SOCKS_REP_ADDRESS_TYPE_NOT_SUPPORTED)
+            return
+
+        # Deal with the port
+        port = struct.unpack("!H", self.buffer.read(2))[0]
+
+        # Ensure there is no trailing garbage
+        if len(self.buffer) > 0:
+            log.warning("%s: Peer sent trailing garbage after request" % self.name)
+            self.send_reply(_SOCKS_REP_GENERAL_FAILURE)
+            return
+
+        # Connect -> addr/port
+        d = protocol.ClientCreator(reactor, SOCKSv5Outgoing, self).connectTCP(addr, port)
+        d.addErrback(self._handle_connect_failure)
+
+        self._state = _SOCKS_ST_CONNECTING
+
+    def _handle_connect_failure(self, failure):
+        log.warning("%s: Failed to connect to peer: %s" % (self.name, failure))
+        # XXX: Send a better error code based on reply
+        self.send_reply(_SOCKS_REP_GENERAL_FAILURE)
+
+    def setup_circuit(self):
+        assert self._other_conn
+        self.circuit.setDownstreamConnection(self._other_conn)
+        self.circuit.setUpstreamConnection(self)
 
-    def connectionLost(self, reason):
-        network.GenericProtocol.connectionLost(self, reason)
+    def send_reply(self, reply):
+        """
+        Send a reply to the request, and complete circuit setup
+        """
 
-class SOCKSv4Factory(Factory):
+        if reply == _SOCKS_REP_SUCCEDED:
+            host = self.transport.getHost()
+            port = host.port
+            try:
+                raw_addr = socket.inet_aton(host.host)
+                self.transport.write(struct.pack("BBBB", _SOCKS_VERSION, reply, _SOCKS_RSV, _SOCKS_ATYP_IP_V4) + raw_addr + struct.pack("!H",port))
+            except socket.error:
+                try:
+                    raw_addr = socket.inet_pton(socket.AF_INET6, host.host)
+                    self.transport.write(struct.pack("BBBB", _SOCKS_VERSION, reply, _SOCKS_RSV, _SOCKS_ATYP_IP_V6) + raw_addr + struct.pack("!H",port))
+                except:
+                    log.warning("%s: Failed to parse bound address" % self.name)
+                    self.send_reply(_SOCKS_REP_GENERAL_FAILURE)
+                    self.transport.loseConnection()
+
+            self._state = _SOCKS_ST_ESTABLISHED
+        else:
+            self.transport.write(struct.pack("!BBBBIH", _SOCKS_VERSION, reply, _SOCKS_RSV, _SOCKS_ATYP_IP_V4, 0, 0))
+            self.transport.loseConnection()
+
+class SOCKSv5Factory(Factory):
     """
-    A SOCKSv4 factory.
+    A SOCKSv5 Factory.
     """
 
     def __init__(self, transport_class, pt_config):
@@ -174,4 +389,4 @@ class SOCKSv4Factory(Factory):
 
         circuit = network.Circuit(self.transport_class())
 
-        return SOCKSv4Protocol(circuit)
+        return SOCKSv5Protocol(circuit)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/obfsproxy.git



More information about the Pkg-privacy-commits mailing list