socks5.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2015-2019 The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Dummy Socks5 server for testing."""
   6  
   7  import select
   8  import socket
   9  import threading
  10  import queue
  11  import logging
  12  
  13  from .netutil import (
  14      format_addr_port
  15  )
  16  
  17  logger = logging.getLogger("TestFramework.socks5")
  18  
  19  # Protocol constants
  20  class Command:
  21      CONNECT = 0x01
  22  
  23  class AddressType:
  24      IPV4 = 0x01
  25      DOMAINNAME = 0x03
  26      IPV6 = 0x04
  27  
  28  # Utility functions
  29  def recvall(s, n):
  30      """Receive n bytes from a socket, or fail."""
  31      rv = bytearray()
  32      while n > 0:
  33          d = s.recv(n)
  34          if not d:
  35              raise IOError('Unexpected end of stream')
  36          rv.extend(d)
  37          n -= len(d)
  38      return rv
  39  
  40  def sendall(s, data):
  41      """Send all data to a socket, or fail."""
  42      sent = 0
  43      while sent < len(data):
  44          _, wlist, _ = select.select([], [s], [])
  45          if len(wlist) > 0:
  46              n = s.send(data[sent:])
  47              if n == 0:
  48                  raise IOError('send() on socket returned 0')
  49              sent += n
  50  
  51  def forward_sockets(a, b):
  52      """Forward data received on socket a to socket b and vice versa, until EOF is received on one of the sockets."""
  53      # Mark as non-blocking so that we do not end up in a deadlock-like situation
  54      # where we block and wait on data from `a` while there is data ready to be
  55      # received on `b` and forwarded to `a`. And at the same time the application
  56      # at `a` is not sending anything because it waits for the data from `b` to
  57      # respond.
  58      a.setblocking(False)
  59      b.setblocking(False)
  60      sockets = [a, b]
  61      done = False
  62      while not done:
  63          rlist, _, xlist = select.select(sockets, [], sockets)
  64          if len(xlist) > 0:
  65              raise IOError('Exceptional condition on socket')
  66          for s in rlist:
  67              data = s.recv(4096)
  68              if data is None or len(data) == 0:
  69                  done = True
  70                  break
  71              if s == a:
  72                  sendall(b, data)
  73              else:
  74                  sendall(a, data)
  75  
  76  # Implementation classes
  77  class Socks5Configuration():
  78      """Proxy configuration."""
  79      def __init__(self):
  80          self.addr = None # Bind address (must be set)
  81          self.af = socket.AF_INET # Bind address family
  82          self.unauth = False  # Support unauthenticated
  83          self.auth = False  # Support authentication
  84          self.keep_alive = False  # Do not automatically close connections
  85          # This function is called whenever a new connection arrives to the proxy
  86          # and it decides where the connection is redirected to. It is passed:
  87          # - the address the client requested to connect to
  88          # - the port the client requested to connect to
  89          # It is supposed to return an object like:
  90          # {
  91          #     "actual_to_addr": "127.0.0.1"
  92          #     "actual_to_port": 28276
  93          # }
  94          # or None.
  95          # If it returns an object then the connection is redirected to actual_to_addr:actual_to_port.
  96          # If it returns None, or destinations_factory itself is None then the connection is closed.
  97          self.destinations_factory = None
  98  
  99  class Socks5Command():
 100      """Information about an incoming socks5 command."""
 101      def __init__(self, cmd, atyp, addr, port, username, password):
 102          self.cmd = cmd # Command (one of Command.*)
 103          self.atyp = atyp # Address type (one of AddressType.*)
 104          self.addr = addr # Address
 105          self.port = port # Port to connect to
 106          self.username = username
 107          self.password = password
 108      def __repr__(self):
 109          return 'Socks5Command(%s,%s,%s,%s,%s,%s)' % (self.cmd, self.atyp, self.addr, self.port, self.username, self.password)
 110  
 111  class Socks5Connection():
 112      def __init__(self, serv, conn):
 113          self.serv = serv
 114          self.conn = conn
 115  
 116      def handle(self):
 117          """Handle socks5 request according to RFC1928."""
 118          try:
 119              # Verify socks version
 120              ver = recvall(self.conn, 1)[0]
 121              if ver != 0x05:
 122                  raise IOError('Invalid socks version %i' % ver)
 123              # Choose authentication method
 124              nmethods = recvall(self.conn, 1)[0]
 125              methods = bytearray(recvall(self.conn, nmethods))
 126              method = None
 127              if 0x02 in methods and self.serv.conf.auth:
 128                  method = 0x02 # username/password
 129              elif 0x00 in methods and self.serv.conf.unauth:
 130                  method = 0x00 # unauthenticated
 131              if method is None:
 132                  raise IOError('No supported authentication method was offered')
 133              # Send response
 134              self.conn.sendall(bytearray([0x05, method]))
 135              # Read authentication (optional)
 136              username = None
 137              password = None
 138              if method == 0x02:
 139                  ver = recvall(self.conn, 1)[0]
 140                  if ver != 0x01:
 141                      raise IOError('Invalid auth packet version %i' % ver)
 142                  ulen = recvall(self.conn, 1)[0]
 143                  username = str(recvall(self.conn, ulen))
 144                  plen = recvall(self.conn, 1)[0]
 145                  password = str(recvall(self.conn, plen))
 146                  # Send authentication response
 147                  self.conn.sendall(bytearray([0x01, 0x00]))
 148  
 149              # Read connect request
 150              ver, cmd, _, atyp = recvall(self.conn, 4)
 151              if ver != 0x05:
 152                  raise IOError('Invalid socks version %i in connect request' % ver)
 153              if cmd != Command.CONNECT:
 154                  raise IOError('Unhandled command %i in connect request' % cmd)
 155  
 156              if atyp == AddressType.IPV4:
 157                  addr = recvall(self.conn, 4)
 158              elif atyp == AddressType.DOMAINNAME:
 159                  n = recvall(self.conn, 1)[0]
 160                  addr = recvall(self.conn, n)
 161              elif atyp == AddressType.IPV6:
 162                  addr = recvall(self.conn, 16)
 163              else:
 164                  raise IOError('Unknown address type %i' % atyp)
 165              port_hi,port_lo = recvall(self.conn, 2)
 166              port = (port_hi << 8) | port_lo
 167  
 168              # Send dummy response
 169              self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
 170  
 171              cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
 172              self.serv.queue.put(cmdin)
 173              logger.debug('Proxy: %s', cmdin)
 174  
 175              requested_to_addr = addr.decode("utf-8")
 176              requested_to = format_addr_port(requested_to_addr, port)
 177  
 178              if self.serv.conf.destinations_factory is not None:
 179                  dest = self.serv.conf.destinations_factory(requested_to_addr, port)
 180                  if dest is not None:
 181                      logger.debug(f"Serving connection to {requested_to}, will redirect it to "
 182                                   f"{dest['actual_to_addr']}:{dest['actual_to_port']} instead")
 183                      with socket.create_connection((dest["actual_to_addr"], dest["actual_to_port"])) as conn_to:
 184                          forward_sockets(self.conn, conn_to)
 185                  else:
 186                      logger.debug(f"Can't serve the connection to {requested_to}: the destinations factory returned None")
 187              else:
 188                  logger.debug(f"Can't serve the connection to {requested_to}: no destinations factory")
 189  
 190              # Fall through to disconnect
 191          except Exception as e:
 192              logger.exception("socks5 request handling failed.")
 193              self.serv.queue.put(e)
 194          finally:
 195              if not self.serv.keep_alive:
 196                  self.conn.close()
 197              else:
 198                  logger.debug("Keeping client connection alive")
 199  
 200  class Socks5Server():
 201      def __init__(self, conf):
 202          self.conf = conf
 203          self.s = socket.socket(conf.af)
 204          self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 205          self.s.bind(conf.addr)
 206          self.s.listen(5)
 207          self.running = False
 208          self.thread = None
 209          self.queue = queue.Queue() # report connections and exceptions to client
 210          self.keep_alive = conf.keep_alive
 211  
 212      def run(self):
 213          while self.running:
 214              (sockconn, _) = self.s.accept()
 215              if self.running:
 216                  conn = Socks5Connection(self, sockconn)
 217                  thread = threading.Thread(None, conn.handle)
 218                  thread.daemon = True
 219                  thread.start()
 220  
 221      def start(self):
 222          assert not self.running
 223          self.running = True
 224          self.thread = threading.Thread(None, self.run)
 225          self.thread.daemon = True
 226          self.thread.start()
 227  
 228      def stop(self):
 229          self.running = False
 230          # connect to self to end run loop
 231          s = socket.socket(self.conf.af)
 232          s.connect(self.conf.addr)
 233          s.close()
 234          self.thread.join()
 235