decrypting SOCKS proxy
at main 441 lines 11 kB view raw
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