[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