/ src / network / socks5.py
socks5.py
  1  """
  2  SOCKS5 proxy module
  3  """
  4  # pylint: disable=attribute-defined-outside-init
  5  
  6  import logging
  7  import socket
  8  import struct
  9  
 10  from .node import Peer
 11  from .proxy import GeneralProxyError, Proxy, ProxyError
 12  
 13  logger = logging.getLogger('default')
 14  
 15  
 16  class Socks5AuthError(ProxyError):
 17      """Rised when the socks5 protocol encounters an authentication error"""
 18      errorCodes = (
 19          "Succeeded",
 20          "Authentication is required",
 21          "All offered authentication methods were rejected",
 22          "Unknown username or invalid password",
 23          "Unknown error"
 24      )
 25  
 26  
 27  class Socks5Error(ProxyError):
 28      """Rised when socks5 protocol encounters an error"""
 29      errorCodes = (
 30          "Succeeded",
 31          "General SOCKS server failure",
 32          "Connection not allowed by ruleset",
 33          "Network unreachable",
 34          "Host unreachable",
 35          "Connection refused",
 36          "TTL expired",
 37          "Command not supported",
 38          "Address type not supported",
 39          "Unknown error"
 40      )
 41  
 42  
 43  class Socks5(Proxy):
 44      """A socks5 proxy base class"""
 45      def __init__(self, address=None):
 46          Proxy.__init__(self, address)
 47          self.ipaddr = None
 48          self.destport = address[1]
 49  
 50      def state_init(self):
 51          """Protocol initialization (before connection is established)"""
 52          if self._auth:
 53              self.append_write_buf(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
 54          else:
 55              self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00))
 56          self.set_state("auth_1", length=0, expectBytes=2)
 57          return True
 58  
 59      def state_auth_1(self):
 60          """Perform authentication if peer is requesting it."""
 61          ret = struct.unpack('BB', self.read_buf[:2])
 62          if ret[0] != 5:
 63              # general error
 64              raise GeneralProxyError(1)
 65          elif ret[1] == 0:
 66              # no auth required
 67              self.set_state("auth_done", length=2)
 68          elif ret[1] == 2:
 69              # username/password
 70              self.append_write_buf(
 71                  struct.pack(
 72                      'BB', 1, len(self._auth[0])) + self._auth[0] + struct.pack(
 73                          'B', len(self._auth[1])) + self._auth[1])
 74              self.set_state("auth_needed", length=2, expectBytes=2)
 75          else:
 76              if ret[1] == 0xff:
 77                  # auth error
 78                  raise Socks5AuthError(2)
 79              else:
 80                  # other error
 81                  raise GeneralProxyError(1)
 82          return True
 83  
 84      def state_auth_needed(self):
 85          """Handle response to authentication attempt"""
 86          ret = struct.unpack('BB', self.read_buf[0:2])
 87          if ret[0] != 1:
 88              # general error
 89              raise GeneralProxyError(1)
 90          if ret[1] != 0:
 91              # auth error
 92              raise Socks5AuthError(3)
 93          # all ok
 94          self.set_state("auth_done", length=2)
 95          return True
 96  
 97      def state_pre_connect(self):
 98          """Handle feedback from socks5 while it is connecting on our behalf."""
 99          # Get the response
100          if self.read_buf[0:1] != chr(0x05).encode():
101              self.close()
102              raise GeneralProxyError(1)
103          elif self.read_buf[1:2] != chr(0x00).encode():
104              # Connection failed
105              self.close()
106              if ord(self.read_buf[1:2]) <= 8:
107                  raise Socks5Error(ord(self.read_buf[1:2]))
108              else:
109                  raise Socks5Error(9)
110          # Get the bound address/port
111          elif self.read_buf[3:4] == chr(0x01).encode():
112              self.set_state("proxy_addr_1", length=4, expectBytes=4)
113          elif self.read_buf[3:4] == chr(0x03).encode():
114              self.set_state("proxy_addr_2_1", length=4, expectBytes=1)
115          else:
116              self.close()
117              raise GeneralProxyError(1)
118          return True
119  
120      def state_proxy_addr_1(self):
121          """Handle IPv4 address returned for peer"""
122          self.boundaddr = self.read_buf[0:4]
123          self.set_state("proxy_port", length=4, expectBytes=2)
124          return True
125  
126      def state_proxy_addr_2_1(self):
127          """
128          Handle other addresses than IPv4 returned for peer
129          (e.g. IPv6, onion, ...). This is part 1 which retrieves the
130          length of the data.
131          """
132          self.address_length = ord(self.read_buf[0:1])
133          self.set_state(
134              "proxy_addr_2_2", length=1, expectBytes=self.address_length)
135          return True
136  
137      def state_proxy_addr_2_2(self):
138          """
139          Handle other addresses than IPv4 returned for peer
140          (e.g. IPv6, onion, ...). This is part 2 which retrieves the data.
141          """
142          self.boundaddr = self.read_buf[0:self.address_length]
143          self.set_state("proxy_port", length=self.address_length, expectBytes=2)
144          return True
145  
146      def state_proxy_port(self):
147          """Handle peer's port being returned."""
148          self.boundport = struct.unpack(">H", self.read_buf[0:2])[0]
149          self.__proxysockname = (self.boundaddr, self.boundport)
150          if self.ipaddr is not None:
151              self.__proxypeername = (
152                  socket.inet_ntoa(self.ipaddr), self.destination[1])
153          else:
154              self.__proxypeername = (self.destination[0], self.destport)
155          self.set_state("proxy_handshake_done", length=2)
156          return True
157  
158      def proxy_sock_name(self):
159          """Handle return value when using SOCKS5
160          for DNS resolving instead of connecting."""
161          return socket.inet_ntoa(self.__proxysockname[0])
162  
163  
164  class Socks5Connection(Socks5):
165      """Child socks5 class used for making outbound connections."""
166      def state_auth_done(self):
167          """Request connection to be made"""
168          # Now we can request the actual connection
169          self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00))
170          # If the given destination address is an IP address, we'll
171          # use the IPv4 address request even if remote resolving was specified.
172          try:
173              self.ipaddr = socket.inet_aton(self.destination[0])
174              self.append_write_buf(chr(0x01).encode() + self.ipaddr)
175          except socket.error:  # may be IPv6!
176              # Well it's not an IP number,  so it's probably a DNS name.
177              if self._remote_dns:
178                  # Resolve remotely
179                  self.ipaddr = None
180                  self.append_write_buf(chr(0x03).encode() + chr(
181                      len(self.destination[0])).encode() + self.destination[0])
182              else:
183                  # Resolve locally
184                  self.ipaddr = socket.inet_aton(
185                      socket.gethostbyname(self.destination[0]))
186                  self.append_write_buf(chr(0x01).encode() + self.ipaddr)
187          self.append_write_buf(struct.pack(">H", self.destination[1]))
188          self.set_state("pre_connect", length=0, expectBytes=4)
189          return True
190  
191      def state_pre_connect(self):
192          """Tell socks5 to initiate a connection"""
193          try:
194              return Socks5.state_pre_connect(self)
195          except Socks5Error as e:
196              self.close_reason = e.message
197              self.set_state("close")
198  
199  
200  class Socks5Resolver(Socks5):
201      """DNS resolver class using socks5"""
202      def __init__(self, host):
203          self.host = host
204          self.port = 8444
205          Socks5.__init__(self, address=Peer(self.host, self.port))
206  
207      def state_auth_done(self):
208          """Perform resolving"""
209          # Now we can request the actual connection
210          self.append_write_buf(struct.pack('BBB', 0x05, 0xF0, 0x00))
211          self.append_write_buf(chr(0x03).encode() + chr(
212              len(self.host)).encode() + str(self.host))
213          self.append_write_buf(struct.pack(">H", self.port))
214          self.set_state("pre_connect", length=0, expectBytes=4)
215          return True
216  
217      def resolved(self):
218          """
219          Resolving is done, process the return value.
220          To use this within PyBitmessage, a callback needs to be
221          implemented which hasn't been done yet.
222          """
223          logger.debug(
224              'Resolved %s as %s', self.host, self.proxy_sock_name())