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())