decrypting SOCKS proxy
1#!/usr/bin/env ruby
2#
3# sockhole: a SOCKS5 decrypting proxy
4# Copyright (c) 2020 joshua stein <jcs@jcs.org>
5#
6# Permission to use, copy, modify, and distribute this software for any
7# purpose with or without fee is hereby granted, provided that the above
8# copyright notice and this permission notice appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17#
18
19require "eventmachine"
20require "socket"
21require "logger"
22require "ipaddr"
23require "resolv"
24require "openssl"
25
26def usage
27 STDERR.puts "usage: #{$0} [-a allowed range] [-d] [-p port] [-i ip]"
28 exit 1
29end
30
31CONFIG = {
32 # a connection to these ports will make a TLS connection and decrypt data
33 # before handing it back to the client
34 :tls_ports => [
35 443, # https
36 993, # imaps
37 995, # pop3s
38 ],
39
40 # by default, listen on the first non-loopback IPv4 address we can find or
41 # fallback to 127.0.0.1
42 :listen_port => 1080,
43 :listen_ip => (Socket.ip_address_list.
44 select{|a| a.ipv4? && !a.ipv4_loopback? }.
45 map{|i| i.ip_unpack[0] }.first || "127.0.0.1"),
46
47 :allowed_ranges => [],
48}
49
50while ARGV.any?
51 case ARGV[0]
52 when "-a"
53 ARGV.shift
54 begin
55 ipr = IPAddr.new(ARGV[0])
56 CONFIG[:allowed_ranges].push ipr
57 rescue IPAddr::InvalidAddressError
58 STDERR.puts "invalid IP range #{ARGV[0]}"
59 usage
60 end
61 ARGV.shift
62 when "-d"
63 ARGV.shift
64 CONFIG[:debug] = true
65 when "-p"
66 ARGV.shift
67 if !ARGV[0].to_s.match(/^\d+/)
68 STDERR.puts "invalid port value"
69 usage
70 end
71 CONFIG[:port] = ARGV.shift
72 when "-i"
73 ARGV.shift
74 begin
75 ip = IPAddr.new(ARGV[0])
76 rescue IPAddr::InvalidAddressError
77 STDERR.puts "invalid IP #{ARGV[0]}"
78 usage
79 end
80 CONFIG[:listen_ip] = ARGV.shift
81 else
82 usage
83 end
84end
85
86# unless specified otherwise, allow connections from the listen ip's network
87if !CONFIG[:allowed_ranges].any?
88 CONFIG[:allowed_ranges].push IPAddr.new("127.0.0.1/32")
89 CONFIG[:allowed_ranges].push IPAddr.new("#{CONFIG[:listen_ip]}/24")
90end
91
92LOGGER = Logger.new(STDOUT)
93LOGGER.level = (CONFIG[:debug] ? Logger::DEBUG : Logger::INFO)
94LOGGER.datetime_format = "%Y-%m-%d %H:%M:%S"
95LOGGER.formatter = proc do |severity, datetime, progname, msg|
96 "[#{datetime}] [#{severity[0]}] #{msg}\n"
97end
98
99VERSION_SOCKS5 = 0x05
100
101METHOD_MIN_LENGTH = 3
102METHOD_AUTH_NONE = 0x0
103
104REQUEST_MIN_LENGTH = 9
105REQUEST_COMMAND_CONNECT = 0x1
106REQUEST_ATYP_IP = 0x1
107REQUEST_ATYP_HOSTNAME = 0x3
108REQUEST_ATYP_IP6 = 0x4
109
110REPLY_SUCCESS = 0x0
111REPLY_FAIL = 0x1
112REPLY_EPERM = 0x02
113REPLY_NET_UNREACHABLE = 0x03
114REPLY_HOST_UNREACHABLE = 0x04
115REPLY_CONN_REFUSED = 0x05
116REPLY_TTL_EXPIRED = 0x06
117REPLY_BAD_COMMAND = 0x07
118REPLY_BAD_ADDRESS = 0x08
119
120class NilClass
121 def empty?
122 true
123 end
124end
125
126class ClientDead < StandardError; end
127
128module EMProxyConnection
129 attr_reader :client, :hostname, :connected, :tls, :certificate_store,
130 :last_cert
131
132 def initialize(client, hostname, tls)
133 @client = client
134 @hostname = hostname
135 @connected = false
136 @tls = tls
137 @did_tls_verification = false
138 @last_cert = nil
139
140 @certificate_store = OpenSSL::X509::Store.new
141 @certificate_store.set_default_paths
142 end
143
144 def connection_completed
145 @connected = true
146
147 # tls connections will call back once verification completes
148 if !tls
149 client.send_reply REPLY_SUCCESS
150 end
151 end
152
153 def log(prio, str)
154 client.log(prio, str)
155 end
156
157 def post_init
158 if tls
159 start_tls(:verify_peer => true, :sni_hostname => hostname)
160 end
161 end
162
163 def receive_data(_data)
164 client.send_data _data
165 end
166
167 def ssl_handshake_completed
168 if !last_cert ||
169 !OpenSSL::SSL.verify_certificate_identity(last_cert, hostname)
170 log :warn, "TLS verification failed for #{hostname.inspect}, aborting"
171 close_connection
172 return
173 end
174
175 log :info, "TLS verification succeeded for #{hostname.inspect}, sending reply"
176 client.send_reply REPLY_SUCCESS
177 end
178
179 def ssl_verify_peer(pem)
180 cert = OpenSSL::X509::Certificate.new(pem)
181
182 if certificate_store.verify(cert)
183 @last_cert = cert
184 certificate_store.add_cert(cert)
185 end
186
187 return true
188
189 rescue => e
190 log :warn, "error in ssl_verify_peer: #{e.inspect}"
191 return false
192 end
193
194 def unbind
195 if connected
196 log :info, "closed remote connection"
197 client.close_connection_after_writing
198 else
199 log :info, "failed connecting to remote"
200 client.send_reply REPLY_FAIL
201 end
202 end
203
204private
205 def ssl_cert_chain_file
206 [ "/etc/ssl/cert.pem", "/etc/ssl/certs/ca-certificates.crt" ].each do |f|
207 if File.exists?(f)
208 return f
209 end
210 end
211
212 raise "can't find ssl certificate chain file"
213 end
214end
215
216module EMSOCKS5Connection
217 attr_reader :state, :ip, :data, :remote_connection, :tls_decrypt
218 attr_accessor :remote_hostname, :remote_ip, :remote_port
219
220 def initialize
221 @state = :INIT
222 port, @ip = Socket.unpack_sockaddr_in(get_peername)
223
224 if !allow_connection?
225 # TODO: does eventmachine have a way to prevent the connection from even
226 # happening in the first place?
227 log :warn, "connection from #{ip} denied, not in allow list"
228 close_connection
229 end
230 end
231
232 def allow_connection?
233 CONFIG[:allowed_ranges].each do |r|
234 if r.to_range.include?(ip)
235 return true
236 end
237 end
238
239 false
240 end
241
242 def do_connect
243 if CONFIG[:tls_ports].include?(remote_port)
244 @tls_decrypt = true
245 end
246
247 l = "connecting to " << remote_ip << ":" << remote_port.to_s
248 if remote_hostname
249 l << " (#{remote_hostname})"
250 end
251 if tls_decrypt
252 l << " (TLS decrypt)"
253 end
254 log :info, l
255
256 # this will call back with send_reply(REPLY_SUCCESS) once connected
257 @remote_connection = EventMachine.connect(remote_ip, remote_port,
258 EMProxyConnection, self, remote_hostname, tls_decrypt)
259 end
260
261 def fail_close(code)
262 send_data [
263 VERSION_SOCKS5,
264 code,
265 0,
266 REQUEST_ATYP_IP,
267 0, 0, 0, 0,
268 0, 0,
269 ].pack("C*")
270
271 close_connection_after_writing
272 @state = :DEAD
273 end
274
275 def handle_request
276 if data[0].ord != VERSION_SOCKS5
277 log :error, "unsupported request version: #{data[0].inspect}"
278 return fail_close(REPLY_FAIL)
279 end
280
281 if (command = data[1].ord) != REQUEST_COMMAND_CONNECT
282 log :error, "unsupported request command: #{data[1].inspect}"
283 return fail_close(REPLY_BAD_COMMAND)
284 end
285
286 case atype = data[3].ord
287 when REQUEST_ATYP_IP
288 begin
289 tmp_ip = data[4, 4].unpack("C*").join(".")
290 self.remote_ip = IPAddr.new(tmp_ip).to_s
291 rescue IPAddr::InvalidAddressError => e
292 log :error, "bogus IP: #{tmp_ip.inspect}"
293 return fail_close(REPLY_BAD_ADDRESS)
294 end
295
296 # network order
297 self.remote_port = data[8, 2].unpack("n")[0]
298
299 when REQUEST_ATYP_HOSTNAME
300 len = data[4].ord
301 if data.bytesize - 4 < len
302 log :error, "hostname len #{len}, but #{data.bytesize - 4} left"
303 return fail_close(REPLY_BAD_ADDRESS)
304 end
305
306 self.remote_hostname = data[5, len].unpack("a*")[0]
307
308 # network order
309 self.remote_port = data[5 + len, 2].unpack("n")[0]
310
311 names = Resolv.getaddresses(remote_hostname).
312 select{|n| IPAddr.new(n).ipv4? }
313 if names.length == 0
314 log :error, "failed to resolve #{remote_hostname.inspect}"
315 return fail_close(REPLY_BAD_ADDRESS)
316 end
317
318 self.remote_ip = names.shuffle[0]
319
320 # e.g., curl --preproxy socks5h://1.2.3.4 ...
321 if self.remote_ip == self.remote_hostname
322 @remote_hostname = nil
323 end
324
325 when ADDRESS_TYPE_IP_V6
326 log :error, "ipv6 not supported"
327 return fail_close(REPLY_BAD_ADDRESS)
328 end
329
330 if self.remote_port < 1 || self.remote_port >= 65535
331 log :error, "bogus port: #{remote_port.inspect}"
332 return fail_close(REPLY_BAD_ADDRESS)
333 end
334
335 case command
336 when REQUEST_COMMAND_CONNECT
337 do_connect
338 else
339 log :error, "unsupported command #{command.inspect}"
340 end
341 end
342
343 def hex(data)
344 data.unpack("C*").map{|c| sprintf("%02x", c) }.join(" ")
345 end
346
347 def log(prio, str)
348 LOGGER.send(prio, "[#{ip}] #{str}")
349 end
350
351 def receive_data(_data)
352 log :debug, "<-C #{_data.inspect} #{hex(_data)}"
353
354 (@data ||= "") << _data
355
356 case state
357 when :INIT
358 if data.bytesize < METHOD_MIN_LENGTH
359 return
360 end
361
362 @state = :METHOD
363 verify_method
364
365 when :REQUEST
366 if data.bytesize < REQUEST_MIN_LENGTH
367 return
368 end
369
370 handle_request
371
372 when :PROXY
373 remote_connection.send_data data
374 @data = ""
375 end
376 end
377
378 def send_data(_data)
379 log :debug, "->C #{_data.inspect} #{hex(_data)}"
380 super
381 end
382
383 def send_reply(code)
384 resp = [ VERSION_SOCKS5, code, 0, REQUEST_ATYP_IP ]
385 resp += IPAddr.new(remote_ip).hton.unpack("C*")
386 resp += remote_port.to_s.unpack("n2").map(&:to_i)
387 send_data resp.pack("C*")
388
389 if code == REPLY_SUCCESS
390 @state = :PROXY
391 @data = ""
392 else
393 close_connection_after_writing
394 @state = :DEAD
395 end
396 end
397
398 def unbind
399 if remote_connection
400 remote_connection.close_connection
401 end
402
403 log :info, "closed connection"
404 end
405
406 def verify_method
407 if data[0].ord != VERSION_SOCKS5
408 log :error, "unsupported version: #{data[0].inspect}"
409 return fail_close(REPLY_FAIL)
410 end
411
412 data[1].ord.times do |i|
413 case data[2 + i].ord
414 when METHOD_AUTH_NONE
415 send_data [ VERSION_SOCKS5, METHOD_AUTH_NONE ].pack("C*")
416 @state = :REQUEST
417 @data = ""
418 return
419 end
420 end
421
422 log :error, "no supported auth methods"
423 fail_close(REPLY_FAIL)
424 end
425end
426
427if !EM.ssl?
428 raise "EventMachine was not compiled with SSL support"
429end
430
431if RUBY_PLATFORM.match(/bsd/i)
432 EM.kqueue = true
433end
434
435EM.run do
436 EM.start_server(CONFIG[:listen_ip], CONFIG[:listen_port], EMSOCKS5Connection)
437 LOGGER.info "[server] listening on #{CONFIG[:listen_ip]}:" <<
438 "#{CONFIG[:listen_port]}"
439 LOGGER.info "[server] allowing connections from " <<
440 CONFIG[:allowed_ranges].map{|i| "#{i.to_s}/#{i.prefix}" }.join(", ")
441end