1 # Public Domain SOCKS proxy protocol implementation
2 # Adapted from https://gist.github.com/bluec0re/cafd3764412967417fd3
4 # SOCKS4 protocol http://www.openssh.com/txt/socks4.protocol
5 # SOCKS4A protocol http://www.openssh.com/txt/socks4a.protocol
6 # SOCKS5 protocol https://tools.ietf.org/html/rfc1928
7 # SOCKS5 username/password authentication https://tools.ietf.org/html/rfc1929
13 from .compat
import compat_ord
15 __author__
= 'Timo Schmid <coding@timoschmid.de>'
18 SOCKS4_REPLY_VERSION
= 0x00
19 # Excerpt from SOCKS4A protocol:
20 # if the client cannot resolve the destination host's domain name to find its
21 # IP address, it should set the first three bytes of DSTIP to NULL and the last
22 # byte to a non-zero value.
23 SOCKS4_DEFAULT_DSTIP
= struct
.pack('!BBBB', 0, 0, 0, 0xFF)
26 SOCKS5_USER_AUTH_VERSION
= 0x01
27 SOCKS5_USER_AUTH_SUCCESS
= 0x00
35 class Socks5Command(Socks4Command
):
36 CMD_UDP_ASSOCIATE
= 0x03
43 AUTH_NO_ACCEPTABLE
= 0xFF # For server response
46 class Socks5AddressType
:
48 ATYP_DOMAINNAME
= 0x03
52 class ProxyError(socket
.error
):
55 def __init__(self
, code
=None, msg
=None):
56 if code
is not None and msg
is None:
57 msg
= self
.CODES
.get(code
) or 'unknown error'
58 super().__init
__(code
, msg
)
61 class InvalidVersionError(ProxyError
):
62 def __init__(self
, expected_version
, got_version
):
63 msg
= ('Invalid response version from server. Expected {:02x} got '
64 '{:02x}'.format(expected_version
, got_version
))
65 super().__init
__(0, msg
)
68 class Socks4Error(ProxyError
):
72 91: 'request rejected or failed',
73 92: 'request rejected because SOCKS server cannot connect to identd on the client',
74 93: 'request rejected because the client program and identd report different user-ids'
78 class Socks5Error(ProxyError
):
79 ERR_GENERAL_FAILURE
= 0x01
82 0x01: 'general SOCKS server failure',
83 0x02: 'connection not allowed by ruleset',
84 0x03: 'Network unreachable',
85 0x04: 'Host unreachable',
86 0x05: 'Connection refused',
88 0x07: 'Command not supported',
89 0x08: 'Address type not supported',
90 0xFE: 'unknown username or invalid password',
91 0xFF: 'all offered authentication methods were rejected'
101 Proxy
= collections
.namedtuple('Proxy', (
102 'type', 'host', 'port', 'username', 'password', 'remote_dns'))
105 class sockssocket(socket
.socket
):
106 def __init__(self
, *args
, **kwargs
):
108 super().__init
__(*args
, **kwargs
)
110 def setproxy(self
, proxytype
, addr
, port
, rdns
=True, username
=None, password
=None):
111 assert proxytype
in (ProxyType
.SOCKS4
, ProxyType
.SOCKS4A
, ProxyType
.SOCKS5
)
113 self
._proxy
= Proxy(proxytype
, addr
, port
, username
, password
, rdns
)
115 def recvall(self
, cnt
):
117 while len(data
) < cnt
:
118 cur
= self
.recv(cnt
- len(data
))
120 raise EOFError(f
'{cnt - len(data)} bytes missing')
124 def _recv_bytes(self
, cnt
):
125 data
= self
.recvall(cnt
)
126 return struct
.unpack(f
'!{cnt}B', data
)
129 def _len_and_data(data
):
130 return struct
.pack('!B', len(data
)) + data
132 def _check_response_version(self
, expected_version
, got_version
):
133 if got_version
!= expected_version
:
135 raise InvalidVersionError(expected_version
, got_version
)
137 def _resolve_address(self
, destaddr
, default
, use_remote_dns
):
139 return socket
.inet_aton(destaddr
)
141 if use_remote_dns
and self
._proxy
.remote_dns
:
144 return socket
.inet_aton(socket
.gethostbyname(destaddr
))
146 def _setup_socks4(self
, address
, is_4a
=False):
147 destaddr
, port
= address
149 ipaddr
= self
._resolve
_address
(destaddr
, SOCKS4_DEFAULT_DSTIP
, use_remote_dns
=is_4a
)
151 packet
= struct
.pack('!BBH', SOCKS4_VERSION
, Socks4Command
.CMD_CONNECT
, port
) + ipaddr
153 username
= (self
._proxy
.username
or '').encode()
154 packet
+= username
+ b
'\x00'
156 if is_4a
and self
._proxy
.remote_dns
:
157 packet
+= destaddr
.encode() + b
'\x00'
161 version
, resp_code
, dstport
, dsthost
= struct
.unpack('!BBHI', self
.recvall(8))
163 self
._check
_response
_version
(SOCKS4_REPLY_VERSION
, version
)
165 if resp_code
!= Socks4Error
.ERR_SUCCESS
:
167 raise Socks4Error(resp_code
)
169 return (dsthost
, dstport
)
171 def _setup_socks4a(self
, address
):
172 self
._setup
_socks
4(address
, is_4a
=True)
174 def _socks5_auth(self
):
175 packet
= struct
.pack('!B', SOCKS5_VERSION
)
177 auth_methods
= [Socks5Auth
.AUTH_NONE
]
178 if self
._proxy
.username
and self
._proxy
.password
:
179 auth_methods
.append(Socks5Auth
.AUTH_USER_PASS
)
181 packet
+= struct
.pack('!B', len(auth_methods
))
182 packet
+= struct
.pack(f
'!{len(auth_methods)}B', *auth_methods
)
186 version
, method
= self
._recv
_bytes
(2)
188 self
._check
_response
_version
(SOCKS5_VERSION
, version
)
190 if method
== Socks5Auth
.AUTH_NO_ACCEPTABLE
or (
191 method
== Socks5Auth
.AUTH_USER_PASS
and (not self
._proxy
.username
or not self
._proxy
.password
)):
193 raise Socks5Error(Socks5Auth
.AUTH_NO_ACCEPTABLE
)
195 if method
== Socks5Auth
.AUTH_USER_PASS
:
196 username
= self
._proxy
.username
.encode()
197 password
= self
._proxy
.password
.encode()
198 packet
= struct
.pack('!B', SOCKS5_USER_AUTH_VERSION
)
199 packet
+= self
._len
_and
_data
(username
) + self
._len
_and
_data
(password
)
202 version
, status
= self
._recv
_bytes
(2)
204 self
._check
_response
_version
(SOCKS5_USER_AUTH_VERSION
, version
)
206 if status
!= SOCKS5_USER_AUTH_SUCCESS
:
208 raise Socks5Error(Socks5Error
.ERR_GENERAL_FAILURE
)
210 def _setup_socks5(self
, address
):
211 destaddr
, port
= address
213 ipaddr
= self
._resolve
_address
(destaddr
, None, use_remote_dns
=True)
218 packet
= struct
.pack('!BBB', SOCKS5_VERSION
, Socks5Command
.CMD_CONNECT
, reserved
)
220 destaddr
= destaddr
.encode()
221 packet
+= struct
.pack('!B', Socks5AddressType
.ATYP_DOMAINNAME
)
222 packet
+= self
._len
_and
_data
(destaddr
)
224 packet
+= struct
.pack('!B', Socks5AddressType
.ATYP_IPV4
) + ipaddr
225 packet
+= struct
.pack('!H', port
)
229 version
, status
, reserved
, atype
= self
._recv
_bytes
(4)
231 self
._check
_response
_version
(SOCKS5_VERSION
, version
)
233 if status
!= Socks5Error
.ERR_SUCCESS
:
235 raise Socks5Error(status
)
237 if atype
== Socks5AddressType
.ATYP_IPV4
:
238 destaddr
= self
.recvall(4)
239 elif atype
== Socks5AddressType
.ATYP_DOMAINNAME
:
240 alen
= compat_ord(self
.recv(1))
241 destaddr
= self
.recvall(alen
)
242 elif atype
== Socks5AddressType
.ATYP_IPV6
:
243 destaddr
= self
.recvall(16)
244 destport
= struct
.unpack('!H', self
.recvall(2))[0]
246 return (destaddr
, destport
)
248 def _make_proxy(self
, connect_func
, address
):
250 return connect_func(self
, address
)
252 result
= connect_func(self
, (self
._proxy
.host
, self
._proxy
.port
))
253 if result
!= 0 and result
is not None:
256 ProxyType
.SOCKS4
: self
._setup
_socks
4,
257 ProxyType
.SOCKS4A
: self
._setup
_socks
4a
,
258 ProxyType
.SOCKS5
: self
._setup
_socks
5,
260 setup_funcs
[self
._proxy
.type](address
)
263 def connect(self
, address
):
264 self
._make
_proxy
(socket
.socket
.connect
, address
)
266 def connect_ex(self
, address
):
267 return self
._make
_proxy
(socket
.socket
.connect_ex
, address
)